Skip to content

fix(react-breadcrumb): make BreadcrumbButtonBaseProps distribute over the ARIA button union - #36663

Open
Ray Knight (ArrayKnight) wants to merge 5 commits into
microsoft:masterfrom
ArrayKnight:fix/breadcrumb-distributive-omit-36645
Open

fix(react-breadcrumb): make BreadcrumbButtonBaseProps distribute over the ARIA button union#36663
Ray Knight (ArrayKnight) wants to merge 5 commits into
microsoft:masterfrom
ArrayKnight:fix/breadcrumb-distributive-omit-36645

Conversation

@ArrayKnight

@ArrayKnight Ray Knight (ArrayKnight) commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

BreadcrumbButtonBaseProps is declared as a plain Omit<BreadcrumbButtonProps, 'size'>. BreadcrumbButtonProps includes ComponentProps<ButtonSlots>, whose root is ARIAButtonSlotProps — a distributive union over as: 'button' | 'a'. A plain Omit collapses that union into a single object type keyed on the intersection of its members, so the anchor arm's href disappears and the anchor spelling of BreadcrumbButton (the one the component's own Default story uses) no longer type-checks against the base props type.

The fix switches to DistributiveOmit, matching what @fluentui/react-button already does for the same reason. Type-level only — no runtime change — and the widened type is strictly a superset of the current one, so no existing consumer code stops compiling. The etc/react-breadcrumb.api.md report is regenerated to match.

Fixes #36645.

Extracted from #36656 per maintainer request — each in-tree fix from that PR as an isolated change.


Also fixes #36681: while adding the requested base-hook test, we found that controlType's as ?? href ? 'a' : 'button' parses as (as ?? href) ? 'a' : 'button', so an explicit as: 'button' (truthy) computed 'a' and the ARIA button pipeline emitted the anchor arm (<a role="button">). Parenthesized so an explicit as short-circuits the href inference, with a red/green renderHook regression test alongside the existing anchor-arm test.

… the ARIA button union

`BreadcrumbButtonBaseProps` was declared with a plain `Omit`:

    export type BreadcrumbButtonBaseProps = Omit<BreadcrumbButtonProps, 'size'>;

`BreadcrumbButtonProps` includes `ComponentProps<ButtonSlots>`, whose `root`
slot is `ARIAButtonSlotProps<'a'>` -- a union over `{ as?: 'button' } & button
attrs` and `{ as: 'a' } & anchor attrs`. Plain `Omit` is `Pick<T, Exclude<keyof
T, K>>`, and `keyof` a union keeps only the keys common to every member, so the
omit collapses the union and every anchor-only prop (`href`, `target`, `rel`,
...) disappears from the derived type. `@fluentui/react-button` avoids exactly
this on the same shape by using `DistributiveOmit`
(Button.types.ts:72,84); this makes `react-breadcrumb` consistent with it.

Healed -- all three now type-check against the base surface where none did
before:

  <BreadcrumbButton href="#a">              (the spelling react-breadcrumb's
                                             own Default story uses)
  <BreadcrumbButton as="a" href="#a">
  <BreadcrumbButton as="a" href target rel>

Not changed, and honestly not a regression: a props object literal whose only
property is `href` is still rejected. That is TypeScript weak-type detection --
the `{ as?: 'button' }` union member has no required properties and shares no
property with `{ href }`, so the source only matches the `a` member, which then
demands an explicit `as: 'a'`. It fires identically on the Griffel
`BreadcrumbButtonProps`, and adding any shared property (`children`, which every
real JSX usage has) satisfies it on both. Runtime behaviour is unchanged: this
file declares types only and emits nothing.

etc/react-breadcrumb.api.md regenerated by the build.

Verified: react-breadcrumb type-check + lint pass; react-headless-components-preview
and react-components type-check pass. react-breadcrumb:test is 2 failed / 105
passed both with and without this change (pre-existing @fluentui/react-icons
snapshot drift -- SVG path data and the `fui-Icon` class -- unrelated to it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aj9uA3rCVgosnh2zNn8qkc
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

📊 Bundle size report

Package & Exports Baseline (minified/GZIP) PR Change
react-breadcrumb
@fluentui/react-breadcrumb - package
103.669 kB
29.081 kB
103.667 kB
29.079 kB
-2 B
-2 B
react-components
react-components: all base hooks
217.395 kB
68.176 kB
217.393 kB
68.174 kB
-2 B
-2 B
react-components
react-components: entire library
1.283 MB
322.109 kB
1.283 MB
322.107 kB
-2 B
-2 B
react-headless-components-preview
react-headless-components-preview: entire library
239.151 kB
67.341 kB
239.149 kB
67.339 kB
-2 B
-2 B
Unchanged fixtures
Package & Exports Size (minified/GZIP)
react-components
react-components: Button, FluentProvider & webLightTheme
67.471 kB
19.465 kB
react-components
react-components: Accordion, Button, FluentProvider, Image, Menu, Popover
227.136 kB
68.41 kB
react-components
react-components: FluentProvider & webLightTheme
40.694 kB
13.549 kB
react-headless-components-preview
@fluentui/react-headless-components-preview/tag-picker
54.012 kB
17.756 kB
react-headless-components-preview
@fluentui/react-headless-components-preview/teaching-popover
36.073 kB
12.006 kB
react-portal-compat
PortalCompatProvider
5.341 kB
2.146 kB
react-timepicker-compat
TimePicker
142.037 kB
46.44 kB
🤖 This report was generated against eebb4337934edf01b1e4b0fda31a50786fa040da

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

Pull request demo site: URL

// union and dropped the anchor arm's `href`. These assignments are validated by the package's
// type-check target.
it('keeps both arms of the ARIA button union assignable to BreadcrumbButtonBaseProps', () => {
const anchorProps: BreadcrumbButtonBaseProps = { as: 'a', href: '/somewhere' };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apologies for the confusion, but could we test the base hook with needed args, something like:

const { result } = renderHook(() => useBreadcrumbButtonBase({ as: 'a', href: '/somewhere' })

expect(result.current).toMatchObject({
  components: { root: 'a' },
  root: { href: '/somewhere' }
})

it will cover both base hook and types, as we run typecheck against tests as well

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 6c355b2 — reworked to the renderHook shape. One adjustment from your sketch: components.root stays 'button' (the slot's declared default element type in useButtonBase); the anchor arm resolves through the slot props, where useARIAButtonProps carries as: 'a' to the render layer. So the assertion pins root: { as: 'a', href: '/somewhere' }, which is the field that actually witnesses the anchor path. 108/108 green.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets clean up the unnecessary comments in tests and should be good to go.

thank you!

…Type ternary (microsoft#36681)

`as ?? href ? 'a' : 'button'` parses as `(as ?? href) ? 'a' : 'button'`,
so an explicit `as: 'button'` (truthy) computed controlType 'a' and the
ARIA button pipeline emitted the anchor arm (as: 'a', role="button") for
a caller who asked for a real <button>. Parenthesize so an explicit `as`
short-circuits the href inference, and add a red/green renderHook
regression test alongside the existing anchor-arm test.
@ArrayKnight

Copy link
Copy Markdown
Contributor Author

Dmytro Kirpa (@dmytrokirpa) While adding the base-hook test you asked for, we hit an operator-precedence bug in the same hook: controlType's as ?? href ? 'a' : 'button' parses as (as ?? href) ? 'a' : 'button', so an explicit as: 'button' computed 'a' and rendered the anchor arm (<a role="button">). Filed #36681 and fixed it here in the same line the PR already touches conceptually, with a red/green renderHook regression test — happy to split it out into a separate PR if you'd prefer it isolated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants