From 7f171a41bf8d9ef28f5dedcaab0996298b8a0756 Mon Sep 17 00:00:00 2001 From: Ray Knight Date: Sat, 22 Aug 2026 23:35:48 -0700 Subject: [PATCH 1/6] fix(react-breadcrumb): make BreadcrumbButtonBaseProps distribute over the ARIA button union `BreadcrumbButtonBaseProps` was declared with a plain `Omit`: export type BreadcrumbButtonBaseProps = Omit; `BreadcrumbButtonProps` includes `ComponentProps`, whose `root` slot is `ARIAButtonSlotProps<'a'>` -- a union over `{ as?: 'button' } & button attrs` and `{ as: 'a' } & anchor attrs`. Plain `Omit` is `Pick>`, 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: (the spelling react-breadcrumb's own Default story uses) 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 Claude-Session: https://claude.ai/code/session_013wmpBCYJpDJCLXcScCWz1i --- .../react-breadcrumb/library/etc/react-breadcrumb.api.md | 3 ++- .../src/components/BreadcrumbButton/BreadcrumbButton.types.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/react-components/react-breadcrumb/library/etc/react-breadcrumb.api.md b/packages/react-components/react-breadcrumb/library/etc/react-breadcrumb.api.md index 21b597d5eb78d6..e16f1f764e21fa 100644 --- a/packages/react-components/react-breadcrumb/library/etc/react-breadcrumb.api.md +++ b/packages/react-components/react-breadcrumb/library/etc/react-breadcrumb.api.md @@ -9,6 +9,7 @@ import type { ButtonSlots } from '@fluentui/react-button'; import type { ButtonState } from '@fluentui/react-button'; import type { ComponentProps } from '@fluentui/react-utilities'; import type { ComponentState } from '@fluentui/react-utilities'; +import type { DistributiveOmit } from '@fluentui/react-utilities'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; import type { JSXElement } from '@fluentui/react-utilities'; import * as React_2 from 'react'; @@ -29,7 +30,7 @@ export type BreadcrumbBaseState = Omit; export const BreadcrumbButton: ForwardRefComponent; // @public (undocumented) -export type BreadcrumbButtonBaseProps = Omit; +export type BreadcrumbButtonBaseProps = DistributiveOmit; // @public (undocumented) export type BreadcrumbButtonBaseState = Omit; diff --git a/packages/react-components/react-breadcrumb/library/src/components/BreadcrumbButton/BreadcrumbButton.types.ts b/packages/react-components/react-breadcrumb/library/src/components/BreadcrumbButton/BreadcrumbButton.types.ts index a9220b965983e5..95bf28bd914499 100644 --- a/packages/react-components/react-breadcrumb/library/src/components/BreadcrumbButton/BreadcrumbButton.types.ts +++ b/packages/react-components/react-breadcrumb/library/src/components/BreadcrumbButton/BreadcrumbButton.types.ts @@ -1,4 +1,4 @@ -import type { ComponentProps, ComponentState } from '@fluentui/react-utilities'; +import type { ComponentProps, ComponentState, DistributiveOmit } from '@fluentui/react-utilities'; import type { ButtonProps, ButtonSlots, ButtonState } from '@fluentui/react-button'; import type { BreadcrumbProps } from '../Breadcrumb/Breadcrumb.types'; @@ -25,6 +25,6 @@ export type BreadcrumbButtonState = ComponentState & Omit & Required>; -export type BreadcrumbButtonBaseProps = Omit; +export type BreadcrumbButtonBaseProps = DistributiveOmit; export type BreadcrumbButtonBaseState = Omit; From adfef67fb110833a76ffcad3556106379d113cb9 Mon Sep 17 00:00:00 2001 From: Ray Knight Date: Mon, 31 Aug 2026 13:07:57 -0700 Subject: [PATCH 2/6] chore: add change file Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Aj9uA3rCVgosnh2zNn8qkc --- ...ct-breadcrumb-78372ea0-823d-4939-b9f5-600d215feb68.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@fluentui-react-breadcrumb-78372ea0-823d-4939-b9f5-600d215feb68.json diff --git a/change/@fluentui-react-breadcrumb-78372ea0-823d-4939-b9f5-600d215feb68.json b/change/@fluentui-react-breadcrumb-78372ea0-823d-4939-b9f5-600d215feb68.json new file mode 100644 index 00000000000000..07a3955938b1b2 --- /dev/null +++ b/change/@fluentui-react-breadcrumb-78372ea0-823d-4939-b9f5-600d215feb68.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: make BreadcrumbButtonBaseProps distribute over the ARIA button union so the anchor arm's href stays assignable", + "packageName": "@fluentui/react-breadcrumb", + "email": "array.knight@gmail.com", + "dependentChangeType": "patch" +} From fe946b561fd12a2e71182006d5699f68d7ea5c15 Mon Sep 17 00:00:00 2001 From: Ray Knight Date: Tue, 1 Sep 2026 12:58:06 -0700 Subject: [PATCH 3/6] test(react-breadcrumb): add type-level regression coverage for BreadcrumbButtonBaseProps --- .../BreadcrumbButton/BreadcrumbButton.test.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/react-components/react-breadcrumb/library/src/components/BreadcrumbButton/BreadcrumbButton.test.tsx b/packages/react-components/react-breadcrumb/library/src/components/BreadcrumbButton/BreadcrumbButton.test.tsx index 6e39e8274ceac9..cc7798388d746d 100644 --- a/packages/react-components/react-breadcrumb/library/src/components/BreadcrumbButton/BreadcrumbButton.test.tsx +++ b/packages/react-components/react-breadcrumb/library/src/components/BreadcrumbButton/BreadcrumbButton.test.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { render } from '@testing-library/react'; import { BreadcrumbButton } from './BreadcrumbButton'; -import type { BreadcrumbButtonProps } from './BreadcrumbButton.types'; +import type { BreadcrumbButtonBaseProps, BreadcrumbButtonProps } from './BreadcrumbButton.types'; import { isConformant } from '../../testing/isConformant'; import { breadcrumbButtonClassNames } from './useBreadcrumbButtonStyles.styles'; import { ArrowRight16Filled } from '@fluentui/react-icons'; @@ -67,4 +67,20 @@ describe('BreadcrumbButton', () => { `); }); + + // Type-level regression test for https://github.com/microsoft/fluentui/issues/36645. + // `BreadcrumbButtonBaseProps` used a plain `Omit`, which collapsed the distributive ARIA button + // 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' }; + const buttonProps: BreadcrumbButtonBaseProps = { as: 'button' }; + + // @ts-expect-error - `size` is omitted from BreadcrumbButtonBaseProps + const sizeProps: BreadcrumbButtonBaseProps = { as: 'button', size: 'small' }; + + expect(anchorProps).toBeDefined(); + expect(buttonProps).toBeDefined(); + expect(sizeProps).toBeDefined(); + }); }); From 6c355b23f8aa72ff960ff51d6424e6056c94b30d Mon Sep 17 00:00:00 2001 From: Ray Knight Date: Tue, 1 Sep 2026 17:50:50 -0700 Subject: [PATCH 4/6] test(react-breadcrumb): exercise the anchor arm through the base hook per review --- .../BreadcrumbButton.test.tsx | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/react-components/react-breadcrumb/library/src/components/BreadcrumbButton/BreadcrumbButton.test.tsx b/packages/react-components/react-breadcrumb/library/src/components/BreadcrumbButton/BreadcrumbButton.test.tsx index cc7798388d746d..a772a6ddb615e6 100644 --- a/packages/react-components/react-breadcrumb/library/src/components/BreadcrumbButton/BreadcrumbButton.test.tsx +++ b/packages/react-components/react-breadcrumb/library/src/components/BreadcrumbButton/BreadcrumbButton.test.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; -import { render } from '@testing-library/react'; +import { render, renderHook } from '@testing-library/react'; import { BreadcrumbButton } from './BreadcrumbButton'; -import type { BreadcrumbButtonBaseProps, BreadcrumbButtonProps } from './BreadcrumbButton.types'; +import { useBreadcrumbButtonBase_unstable } from './useBreadcrumbButton'; +import type { BreadcrumbButtonProps } from './BreadcrumbButton.types'; import { isConformant } from '../../testing/isConformant'; import { breadcrumbButtonClassNames } from './useBreadcrumbButtonStyles.styles'; import { ArrowRight16Filled } from '@fluentui/react-icons'; @@ -68,19 +69,18 @@ describe('BreadcrumbButton', () => { `); }); - // Type-level regression test for https://github.com/microsoft/fluentui/issues/36645. - // `BreadcrumbButtonBaseProps` used a plain `Omit`, which collapsed the distributive ARIA button - // 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' }; - const buttonProps: BreadcrumbButtonBaseProps = { as: 'button' }; - - // @ts-expect-error - `size` is omitted from BreadcrumbButtonBaseProps - const sizeProps: BreadcrumbButtonBaseProps = { as: 'button', size: 'small' }; + // Regression test for https://github.com/microsoft/fluentui/issues/36645: a plain `Omit` + // collapsed the distributive ARIA button union, so the anchor arm's `href` was not assignable. + // Type-check runs against tests, so this covers the base hook and the types together. + it('accepts the anchor arm of the ARIA button union in the base hook', () => { + const { result } = renderHook(() => + useBreadcrumbButtonBase_unstable({ as: 'a', href: '/somewhere' }, React.createRef()), + ); - expect(anchorProps).toBeDefined(); - expect(buttonProps).toBeDefined(); - expect(sizeProps).toBeDefined(); + // `components.root` stays 'button' (the slot's declared default); the anchor arm resolves + // through the slot props, where useARIAButtonProps carries `as: 'a'` to the render layer. + expect(result.current).toMatchObject({ + root: { as: 'a', href: '/somewhere' }, + }); }); }); From 7a8947f9672b36818b61c3288755ae5a6c9a8522 Mon Sep 17 00:00:00 2001 From: Ray Knight Date: Tue, 1 Sep 2026 19:46:59 -0700 Subject: [PATCH 5/6] fix(react-breadcrumb): parenthesize useBreadcrumbButtonBase's controlType ternary (#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