diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 519558c4fef05d..1a35e8e4f9ab73 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -331,6 +331,8 @@ packages/react-components/component-selector-preview/library @microsoft/teams-pr packages/react-components/component-selector-preview/stories @microsoft/teams-prg packages/react-components/react-menu-grid-preview/library @microsoft/teams-prg packages/react-components/react-menu-grid-preview/stories @microsoft/teams-prg +packages/react-components/react-headless/library @microsoft/cxe-prg +packages/react-components/react-headless/stories @microsoft/cxe-prg # <%= NX-CODEOWNER-PLACEHOLDER %> # Deprecated v9 packages - exposed as part of `/unstable` api diff --git a/change/@fluentui-react-headless-e353fbc0-1423-4a34-ba93-1d47bf2afae3.json b/change/@fluentui-react-headless-e353fbc0-1423-4a34-ba93-1d47bf2afae3.json new file mode 100644 index 00000000000000..602d0d7c1e8a9e --- /dev/null +++ b/change/@fluentui-react-headless-e353fbc0-1423-4a34-ba93-1d47bf2afae3.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: introduce new package for creating base components", + "packageName": "@fluentui/react-headless", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-avatar/library/src/components/AvatarGroupPopover/useAvatarGroupPopover.tsx b/packages/react-components/react-avatar/library/src/components/AvatarGroupPopover/useAvatarGroupPopover.tsx index a4a16df9d5bd23..c60dc6298888d6 100644 --- a/packages/react-components/react-avatar/library/src/components/AvatarGroupPopover/useAvatarGroupPopover.tsx +++ b/packages/react-components/react-avatar/library/src/components/AvatarGroupPopover/useAvatarGroupPopover.tsx @@ -5,19 +5,8 @@ import { useAvatarGroupContext_unstable } from '../../contexts/AvatarGroupContex import { defaultAvatarGroupSize } from '../AvatarGroup/useAvatarGroup'; import { useControllableState, slot } from '@fluentui/react-utilities'; import { MoreHorizontalRegular } from '@fluentui/react-icons'; -import { - OnOpenChangeData, - OpenPopoverEvents, - Popover, - type PopoverProps, - PopoverSurface, -} from '@fluentui/react-popover'; -import type { - AvatarGroupPopoverBaseProps, - AvatarGroupPopoverBaseState, - AvatarGroupPopoverProps, - AvatarGroupPopoverState, -} from './AvatarGroupPopover.types'; +import { OnOpenChangeData, OpenPopoverEvents, Popover, PopoverSurface } from '@fluentui/react-popover'; +import type { AvatarGroupPopoverProps, AvatarGroupPopoverState } from './AvatarGroupPopover.types'; import { Tooltip } from '@fluentui/react-tooltip'; /** @@ -31,52 +20,12 @@ import { Tooltip } from '@fluentui/react-tooltip'; export const useAvatarGroupPopover_unstable = (props: AvatarGroupPopoverProps): AvatarGroupPopoverState => { const size = useAvatarGroupContext_unstable(ctx => ctx.size) ?? defaultAvatarGroupSize; const layout = useAvatarGroupContext_unstable(ctx => ctx.layout); - const { indicator = size < 24 ? 'icon' : 'count', ...baseProps } = props; - - const state = useAvatarGroupPopoverBase_unstable({ - indicator, - ...baseProps, - }); - - if (layout === 'pie') { - state.triggerButton.children = null; - } else if (indicator === 'icon') { - state.triggerButton.children = ; - } - - return { - size, - ...state, - - components: { - // eslint-disable-next-line @typescript-eslint/no-deprecated - ...state.components, - root: Popover, - popoverSurface: PopoverSurface, - tooltip: Tooltip, - }, - root: slot.always(state.root as PopoverProps, { elementType: Popover }), - popoverSurface: slot.always(props.popoverSurface, { - defaultProps: state.popoverSurface, - elementType: PopoverSurface, - }), - tooltip: slot.always(props.tooltip, { - defaultProps: state.tooltip, - elementType: Tooltip, - }), - }; -}; - -/** - * Handles popover open/closed state, indicator display, and slot configuration. - * Use directly for custom implementations or use useAvatarGroupPopover_unstable for defaults. - * - * @param props - AvatarGroupPopover props - * @returns AvatarGroupPopover state - */ -export const useAvatarGroupPopoverBase_unstable = (props: AvatarGroupPopoverBaseProps): AvatarGroupPopoverBaseState => { - const layout = useAvatarGroupContext_unstable(ctx => ctx.layout); - const { indicator = 'count', count = React.Children.count(props.children), children, ...restOfProps } = props; + const { + indicator = size < 24 ? 'icon' : 'count', + count = React.Children.count(props.children), + children, + ...restOfProps + } = props; const [popoverOpen, setPopoverOpen] = useControllableState({ state: props.open, @@ -92,7 +41,9 @@ export const useAvatarGroupPopoverBase_unstable = (props: AvatarGroupPopoverBase let triggerButtonChildren; if (layout === 'pie') { triggerButtonChildren = null; - } else if (indicator === 'count') { + } else if (indicator === 'icon') { + triggerButtonChildren = ; + } else { triggerButtonChildren = count > 99 ? '99+' : `+${count}`; } @@ -101,6 +52,7 @@ export const useAvatarGroupPopoverBase_unstable = (props: AvatarGroupPopoverBase indicator, layout, popoverOpen, + size, components: { root: Popover, @@ -119,7 +71,7 @@ export const useAvatarGroupPopoverBase_unstable = (props: AvatarGroupPopoverBase open: popoverOpen, onOpenChange: handleOnPopoverChange, }, - { elementType: 'div' }, + { elementType: Popover }, ), triggerButton: slot.always(props.triggerButton, { defaultProps: { @@ -140,14 +92,14 @@ export const useAvatarGroupPopoverBase_unstable = (props: AvatarGroupPopoverBase 'aria-label': 'Overflow', tabIndex: 0, }, - elementType: 'div', + elementType: PopoverSurface, }), tooltip: slot.always(props.tooltip, { defaultProps: { content: 'View more people.', relationship: 'label', }, - elementType: 'div', + elementType: Tooltip, }), }; }; 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 bb8538b9591ef4..eba860b3137ba3 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 @@ -14,26 +14,13 @@ import type { JSXElement } from '@fluentui/react-utilities'; import * as React_2 from 'react'; import type { Slot } from '@fluentui/react-utilities'; import type { SlotClassNames } from '@fluentui/react-utilities'; -import { TabsterDOMAttribute } from '@fluentui/react-tabster'; // @public export const Breadcrumb: ForwardRefComponent; -// @public (undocumented) -export type BreadcrumbBaseProps = Omit; - -// @public (undocumented) -export type BreadcrumbBaseState = Omit; - // @public export const BreadcrumbButton: ForwardRefComponent; -// @public (undocumented) -export type BreadcrumbButtonBaseProps = Omit; - -// @public (undocumented) -export type BreadcrumbButtonBaseState = Omit; - // @public export const breadcrumbButtonClassNames: SlotClassNames; @@ -57,12 +44,6 @@ export type BreadcrumbContextValues = Required>; // @public export const BreadcrumbDivider: ForwardRefComponent; -// @public -export type BreadcrumbDividerBaseProps = BreadcrumbDividerProps; - -// @public -export type BreadcrumbDividerBaseState = Omit; - // @public (undocumented) export const breadcrumbDividerClassNames: SlotClassNames; @@ -80,12 +61,6 @@ export type BreadcrumbDividerState = ComponentState & Pi // @public export const BreadcrumbItem: ForwardRefComponent; -// @public -export type BreadcrumbItemBaseProps = Omit; - -// @public -export type BreadcrumbItemBaseState = Omit; - // @public (undocumented) export const breadcrumbItemClassNames: SlotClassNames; @@ -159,18 +134,9 @@ export const truncateBreadcrumLongTooltip: (content: string, maxLength?: number) // @public export const useBreadcrumb_unstable: (props: BreadcrumbProps, ref: React_2.Ref) => BreadcrumbState; -// @public -export const useBreadcrumbA11yBehavior_unstable: ({ focusMode, }: Pick) => Partial; - -// @public -export const useBreadcrumbBase_unstable: (props: BreadcrumbBaseProps, ref: React_2.Ref) => BreadcrumbBaseState; - // @public export const useBreadcrumbButton_unstable: (props: BreadcrumbButtonProps, ref: React_2.Ref) => BreadcrumbButtonState; -// @public -export const useBreadcrumbButtonBase_unstable: (props: BreadcrumbButtonBaseProps, ref: React_2.Ref) => BreadcrumbButtonBaseState; - // @public export const useBreadcrumbButtonStyles_unstable: (state: BreadcrumbButtonState) => BreadcrumbButtonState; @@ -180,18 +146,12 @@ export const useBreadcrumbContext_unstable: () => BreadcrumbContextValues; // @public export const useBreadcrumbDivider_unstable: (props: BreadcrumbDividerProps, ref: React_2.Ref) => BreadcrumbDividerState; -// @public -export const useBreadcrumbDividerBase_unstable: (props: BreadcrumbDividerBaseProps, ref: React_2.Ref) => BreadcrumbDividerBaseState; - // @public export const useBreadcrumbDividerStyles_unstable: (state: BreadcrumbDividerState) => BreadcrumbDividerState; // @public export const useBreadcrumbItem_unstable: (props: BreadcrumbItemProps, ref: React_2.Ref) => BreadcrumbItemState; -// @public -export const useBreadcrumbItemBase_unstable: (props: BreadcrumbItemBaseProps, ref: React_2.Ref) => BreadcrumbItemBaseState; - // @public export const useBreadcrumbItemStyles_unstable: (state: BreadcrumbItemState) => BreadcrumbItemState; diff --git a/packages/react-components/react-breadcrumb/library/src/Breadcrumb.ts b/packages/react-components/react-breadcrumb/library/src/Breadcrumb.ts index fdd3135d7e98f3..2feb00f9ed21b8 100644 --- a/packages/react-components/react-breadcrumb/library/src/Breadcrumb.ts +++ b/packages/react-components/react-breadcrumb/library/src/Breadcrumb.ts @@ -3,8 +3,6 @@ export type { BreadcrumbProps, BreadcrumbSlots, BreadcrumbState, - BreadcrumbBaseProps, - BreadcrumbBaseState, } from './components/Breadcrumb/index'; export { Breadcrumb, @@ -15,6 +13,4 @@ export { useBreadcrumbContext_unstable, useBreadcrumbStyles_unstable, useBreadcrumb_unstable, - useBreadcrumbBase_unstable, - useBreadcrumbA11yBehavior_unstable, } from './components/Breadcrumb/index'; diff --git a/packages/react-components/react-breadcrumb/library/src/components/Breadcrumb/index.ts b/packages/react-components/react-breadcrumb/library/src/components/Breadcrumb/index.ts index 6b3b07b704f117..8da2c7b8036135 100644 --- a/packages/react-components/react-breadcrumb/library/src/components/Breadcrumb/index.ts +++ b/packages/react-components/react-breadcrumb/library/src/components/Breadcrumb/index.ts @@ -1,17 +1,6 @@ export { Breadcrumb } from './Breadcrumb'; -export type { - BreadcrumbBaseProps, - BreadcrumbBaseState, - BreadcrumbContextValues, - BreadcrumbProps, - BreadcrumbSlots, - BreadcrumbState, -} from './Breadcrumb.types'; +export type { BreadcrumbContextValues, BreadcrumbProps, BreadcrumbSlots, BreadcrumbState } from './Breadcrumb.types'; export { BreadcrumbProvider, breadcrumbDefaultValue, useBreadcrumbContext_unstable } from './BreadcrumbContext'; export { renderBreadcrumb_unstable } from './renderBreadcrumb'; -export { - useBreadcrumb_unstable, - useBreadcrumbBase_unstable, - useBreadcrumbA11yBehavior_unstable, -} from './useBreadcrumb'; +export { useBreadcrumb_unstable } from './useBreadcrumb'; export { breadcrumbClassNames, useBreadcrumbStyles_unstable } from './useBreadcrumbStyles.styles'; diff --git a/packages/react-components/react-breadcrumb/library/src/index.ts b/packages/react-components/react-breadcrumb/library/src/index.ts index 34d462af12c6e2..563d3928982a64 100644 --- a/packages/react-components/react-breadcrumb/library/src/index.ts +++ b/packages/react-components/react-breadcrumb/library/src/index.ts @@ -2,48 +2,26 @@ export { Breadcrumb, renderBreadcrumb_unstable, useBreadcrumb_unstable, - useBreadcrumbBase_unstable, - useBreadcrumbA11yBehavior_unstable, useBreadcrumbStyles_unstable, breadcrumbClassNames, } from './Breadcrumb'; -export type { - BreadcrumbSlots, - BreadcrumbProps, - BreadcrumbState, - BreadcrumbBaseProps, - BreadcrumbBaseState, -} from './Breadcrumb'; +export type { BreadcrumbSlots, BreadcrumbProps, BreadcrumbState } from './Breadcrumb'; export { BreadcrumbDivider, breadcrumbDividerClassNames, renderBreadcrumbDivider_unstable, useBreadcrumbDividerStyles_unstable, useBreadcrumbDivider_unstable, - useBreadcrumbDividerBase_unstable, -} from './BreadcrumbDivider'; -export type { - BreadcrumbDividerProps, - BreadcrumbDividerSlots, - BreadcrumbDividerState, - BreadcrumbDividerBaseProps, - BreadcrumbDividerBaseState, } from './BreadcrumbDivider'; +export type { BreadcrumbDividerProps, BreadcrumbDividerSlots, BreadcrumbDividerState } from './BreadcrumbDivider'; export { BreadcrumbItem, breadcrumbItemClassNames, renderBreadcrumbItem_unstable, useBreadcrumbItemStyles_unstable, useBreadcrumbItem_unstable, - useBreadcrumbItemBase_unstable, -} from './BreadcrumbItem'; -export type { - BreadcrumbItemProps, - BreadcrumbItemSlots, - BreadcrumbItemState, - BreadcrumbItemBaseProps, - BreadcrumbItemBaseState, } from './BreadcrumbItem'; +export type { BreadcrumbItemProps, BreadcrumbItemSlots, BreadcrumbItemState } from './BreadcrumbItem'; export { partitionBreadcrumbItems, truncateBreadcrumbLongName, @@ -57,14 +35,7 @@ export { renderBreadcrumbButton_unstable, useBreadcrumbButtonStyles_unstable, useBreadcrumbButton_unstable, - useBreadcrumbButtonBase_unstable, -} from './BreadcrumbButton'; -export type { - BreadcrumbButtonProps, - BreadcrumbButtonSlots, - BreadcrumbButtonState, - BreadcrumbButtonBaseProps, - BreadcrumbButtonBaseState, } from './BreadcrumbButton'; +export type { BreadcrumbButtonProps, BreadcrumbButtonSlots, BreadcrumbButtonState } from './BreadcrumbButton'; export { BreadcrumbProvider, useBreadcrumbContext_unstable } from './Breadcrumb'; export type { BreadcrumbContextValues } from './Breadcrumb'; diff --git a/packages/react-components/react-button/library/etc/react-button.api.md b/packages/react-components/react-button/library/etc/react-button.api.md index b5a6596d4b6723..9148c6c57585eb 100644 --- a/packages/react-components/react-button/library/etc/react-button.api.md +++ b/packages/react-components/react-button/library/etc/react-button.api.md @@ -7,7 +7,6 @@ import type { ARIAButtonSlotProps } from '@fluentui/react-aria'; import type { ComponentProps } from '@fluentui/react-utilities'; import type { ComponentState } from '@fluentui/react-utilities'; -import type { DistributiveOmit } from '@fluentui/react-utilities'; import { ForwardRefComponent } from '@fluentui/react-utilities'; import type { JSXElement } from '@fluentui/react-utilities'; import * as React_2 from 'react'; @@ -17,12 +16,6 @@ import type { SlotClassNames } from '@fluentui/react-utilities'; // @public export const Button: ForwardRefComponent; -// @public (undocumented) -export type ButtonBaseProps = DistributiveOmit; - -// @public (undocumented) -export type ButtonBaseState = DistributiveOmit; - // @public (undocumented) export const buttonClassNames: SlotClassNames; @@ -127,12 +120,6 @@ export type SplitButtonState = ComponentState & Omit; -// @public (undocumented) -export type ToggleButtonBaseProps = ButtonBaseProps & Pick; - -// @public (undocumented) -export type ToggleButtonBaseState = ButtonBaseState & Required>; - // @public (undocumented) export const toggleButtonClassNames: SlotClassNames; @@ -140,18 +127,14 @@ export const toggleButtonClassNames: SlotClassNames; export type ToggleButtonProps = ButtonProps & { defaultChecked?: boolean; checked?: boolean; - isAccessible?: boolean; }; // @public (undocumented) -export type ToggleButtonState = ButtonState & Required>; +export type ToggleButtonState = ButtonState & Required>; // @public export const useButton_unstable: (props: ButtonProps, ref: React_2.Ref) => ButtonState; -// @public -export const useButtonBase_unstable: (props: ButtonBaseProps, ref?: React_2.Ref) => ButtonBaseState; - // @internal export const useButtonContext: () => ButtonContextValue; @@ -179,14 +162,11 @@ export const useSplitButtonStyles_unstable: (state: SplitButtonState) => SplitBu // @public export const useToggleButton_unstable: (props: ToggleButtonProps, ref: React_2.Ref) => ToggleButtonState; -// @public -export const useToggleButtonBase_unstable: (props: ToggleButtonProps, ref?: React_2.Ref) => ToggleButtonBaseState; - // @public (undocumented) export const useToggleButtonStyles_unstable: (state: ToggleButtonState) => ToggleButtonState; // @public (undocumented) -export function useToggleState, TButtonState extends Pick, TToggleButtonState extends Pick>(props: TToggleButtonProps, state: TButtonState): TToggleButtonState; +export function useToggleState, TButtonState extends Pick, TToggleButtonState extends Pick>(props: TToggleButtonProps, state: TButtonState): TToggleButtonState; // (No @packageDocumentation comment for this package) diff --git a/packages/react-components/react-field/library/src/components/Field/index.ts b/packages/react-components/react-field/library/src/components/Field/index.ts index 3ebf540ebfb891..69f10821694067 100644 --- a/packages/react-components/react-field/library/src/components/Field/index.ts +++ b/packages/react-components/react-field/library/src/components/Field/index.ts @@ -1,6 +1,4 @@ export type { - FieldBaseProps, - FieldBaseState, FieldContextValue, FieldContextValues, FieldControlProps, @@ -10,5 +8,5 @@ export type { } from './Field.types'; export { Field } from './Field'; export { renderField_unstable } from './renderField'; -export { useField_unstable, useFieldBase_unstable } from './useField'; +export { useField_unstable } from './useField'; export { fieldClassNames, useFieldStyles_unstable } from './useFieldStyles.styles'; diff --git a/packages/react-components/react-field/library/src/components/Field/useField.tsx b/packages/react-components/react-field/library/src/components/Field/useField.tsx index 25d36c6265380d..303d328f75bde2 100644 --- a/packages/react-components/react-field/library/src/components/Field/useField.tsx +++ b/packages/react-components/react-field/library/src/components/Field/useField.tsx @@ -1,14 +1,12 @@ -'use client'; - import * as React from 'react'; -import { CheckmarkCircle12Filled, DiamondDismiss12Filled, Warning12Filled } from '@fluentui/react-icons'; +import { CheckmarkCircle12Filled, ErrorCircle12Filled, Warning12Filled } from '@fluentui/react-icons'; import { Label } from '@fluentui/react-label'; import { getIntrinsicElementProps, useId, slot } from '@fluentui/react-utilities'; -import type { FieldBaseProps, FieldBaseState, FieldProps, FieldState } from './Field.types'; +import type { FieldProps, FieldState } from './Field.types'; const validationMessageIcons = { - error: , + error: , warning: , success: , none: undefined, @@ -24,38 +22,13 @@ const validationMessageIcons = { * @param ref - Ref to the root */ export const useField_unstable = (props: FieldProps, ref: React.Ref): FieldState => { - const { orientation = 'vertical', size = 'medium', ...fieldProps } = props; - const state = useFieldBase_unstable(fieldProps, ref); - - const defaultIcon = validationMessageIcons[state.validationState]; - - return { - ...state, - // eslint-disable-next-line @typescript-eslint/no-deprecated - components: { ...state.components, label: Label }, - label: slot.optional(props.label, { - defaultProps: { size, ...state.label }, - elementType: Label, - }), - validationMessageIcon: slot.optional(props.validationMessageIcon, { - renderByDefault: !!defaultIcon, - defaultProps: { children: defaultIcon }, - elementType: 'span', - }), - orientation, - size, - }; -}; - -/** - * Base hook for Field component, which manages state related to validation, ARIA attributes, - * ID generation, and slot structure without design props. - * - * @param props - Props passed to this field - * @param ref - Ref to the root - */ -export const useFieldBase_unstable = (props: FieldBaseProps, ref: React.Ref): FieldBaseState => { - const { children, required = false, validationState = props.validationMessage ? 'error' : 'none' } = props; + const { + children, + orientation = 'vertical', + required = false, + validationState = props.validationMessage ? 'error' : 'none', + size = 'medium', + } = props; const baseId = useId('field-'); const generatedControlId = baseId + '__control'; @@ -64,8 +37,8 @@ export const useFieldBase_unstable = (props: FieldBaseProps, ref: React.Ref **Accessibility note:** Base hooks provide ARIA attributes and semantic structure, but not visual accessibility (e.g., focus indicators, sufficient contrast). Consumers are responsible for implementing these in their custom styles. + +## Installation + +```sh +npm install @fluentui/react-headless +``` + +## Usage + +```tsx +import * as React from 'react'; +import { useButton, renderButton } from '@fluentui/react-headless'; +import type { ButtonProps, ButtonState } from '@fluentui/react-headless'; + +type CustomButtonProps = ButtonProps & { + variant?: 'primary' | 'secondary' | 'tertiary'; + tone?: 'neutral' | 'success' | 'warning' | 'danger'; +}; + +export const CustomButton = React.forwardRef( + ({ variant = 'primary', tone = 'neutral', ...props }, ref) => { + const state = useButton(props, ref); + + state.root.className = ['custom-btn', `custom-btn--${variant}`, `custom-btn--${tone}`, state.root.className] + .filter(Boolean) + .join(' '); + + if (state.icon) { + state.icon.className = ['custom-btn__icon', state.icon.className].filter(Boolean).join(' '); + } + + return renderButton(state as ButtonState); + }, +); +``` + +## API + +### Naming conventions + +| Artifact | Pattern | Example | +| ---------- | ------------------------ | -------------- | +| Hook | `use${ComponentName}` | `useButton` | +| Props type | `${ComponentName}Props` | `ButtonProps` | +| State type | `${ComponentName}State` | `ButtonState` | +| Render fn | `render${ComponentName}` | `renderButton` | + +Hooks are stable aliases of the internal `use${ComponentName}Base_unstable` hooks from individual component packages. Types use `Base` internally but are exported without the suffix (e.g., `ButtonProps`, `ButtonState`). + +### `composeComponent` + +A utility to compose a complete component from a base hook and render function: + +```ts +import { composeComponent } from '@fluentui/react-headless'; +``` + +### Component base hooks and render functions + +| Component | Hooks | Context values hook | Render functions | +| ---------- | ----------------------------------------------------------------------------------- | -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| Accordion | `useAccordion`, `useAccordionItem`, `useAccordionHeader`, `useAccordionPanel` | `useAccordionContextValues`, `useAccordionHeaderContextValues` | `renderAccordion`, `renderAccordionItem`, `renderAccordionHeader`, `renderAccordionPanel` | +| Badge | `useBadge`, `usePresenceBadge`, `useCounterBadge` | — | `renderBadge` | +| Breadcrumb | `useBreadcrumb`, `useBreadcrumbDivider`, `useBreadcrumbItem`, `useBreadcrumbButton` | `useBreadcrumbContextValues` | `renderBreadcrumb`, `renderBreadcrumbDivider`, `renderBreadcrumbItem`, `renderBreadcrumbButton` | +| Button | `useButton`, `useToggleButton` | — | `renderButton`, `renderToggleButton` | +| Card | `useCard`, `useCardFooter`, `useCardHeader`, `useCardPreview` | `useCardContextValues` | `renderCard`, `renderCardFooter`, `renderCardHeader`, `renderCardPreview` | +| Divider | `useDivider` | — | `renderDivider` | +| Image | `useImage` | — | `renderImage` | +| Input | `useInput` | — | `renderInput` | +| Link | `useLink` | — | `renderLink` | +| Persona | `usePersona` | — | `renderPersona` | +| Radio | `useRadioGroup`, `useRadio` | `useRadioGroupContextValues` | `renderRadioGroup`, `renderRadio` | +| Rating | `useRating`, `useRatingDisplay`, `useRatingItem` | `useRatingContextValues`, `useRatingDisplayContextValues` | `renderRating`, `renderRatingDisplay`, `renderRatingItem` | +| SearchBox | `useSearchBox` | — | `renderSearchBox` | +| Skeleton | `useSkeleton`, `useSkeletonItem` | `useSkeletonContextValues` | `renderSkeleton`, `renderSkeletonItem` | +| Slider | `useSlider` | — | `renderSlider` | +| SpinButton | `useSpinButton` | — | `renderSpinButton` | +| Spinner | `useSpinner` | — | `renderSpinner` | +| Switch | `useSwitch` | — | `renderSwitch` | +| Textarea | `useTextarea` | — | `renderTextarea` | + +### Re-exported utilities + +#### From `@fluentui/react-shared-contexts` + +| Export | Description | +| ------------------------- | ---------------------------------------- | +| `AnnounceProvider` | Provider for screen reader announcements | +| `PortalMountNodeProvider` | Provider for portal mount node | +| `useAnnounce` | Hook to trigger announcements | +| `useFluent` | Hook to access Fluent context | +| `usePortalMountNode` | Hook to access portal mount node | +| `useThemeClassName` | Hook to access theme class name | +| `useTooltipVisibility` | Hook to access tooltip visibility state | + +#### From `@fluentui/react-utilities` + +`getIntrinsicElementProps`, `getPartitionedNativeProps`, `getSlotClassNameProp_unstable`, `slot`, `assertSlots`, `IdPrefixProvider`, `resetIdsForTests`, `SSRProvider`, `useAnimationFrame`, `useId`, `useIsomorphicLayoutEffect`, `useEventCallback`, `mergeCallbacks`, `useIsSSR`, `useMergedRefs`, `useApplyScrollbarWidth`, `useScrollbarWidth`, `useSelection`, `useTimeout`, `isHTMLElement` + +Also exports deprecated (but available) APIs: `getNativeElementProps`, `getSlots`, `resolveShorthand`. + +## Composition layers + +``` +use{Component}Base_unstable ← this package (base hook — logic + accessibility) + ↓ +use{Component}_unstable (adds design props) + ↓ +{Component} (adds Griffel styles + tokens) +``` + +This package exposes the **first layer** only. Styled components in `@fluentui/react-components` compose on top of it. + +## Migration + +This is a new package; there is no migration from v8 or v0. For teams currently using full Fluent UI components that want to adopt base hooks: + +1. Replace `useButton_unstable(props, ref)` with `useButton(props, ref)` from this package +2. Remove design props (`appearance`, `size`, `shape`) from your props type — use `ButtonProps` (the base variant) instead +3. Apply your own class names or styles to the returned state slots before passing to the render function + +## Accessibility + +Base hooks provide the semantic foundation for accessibility. Consumers must ensure their custom styles maintain: + +- **Visible focus indicators** — base hooks do not apply focus ring styles +- **Sufficient color contrast** — base hooks do not apply colors or tokens +- **Appropriate visual feedback** for all interactive states (hover, active, disabled) + +Each component follows its corresponding [WAI-ARIA authoring practice](https://www.w3.org/WAI/ARIA/apg/). Keyboard navigation, focus management, and state announcements are identical to the styled counterparts in `@fluentui/react-components`. diff --git a/packages/react-components/react-headless/library/bundle-size/ReactComponents.fixture.js b/packages/react-components/react-headless/library/bundle-size/ReactComponents.fixture.js new file mode 100644 index 00000000000000..79fa50ae8a1429 --- /dev/null +++ b/packages/react-components/react-headless/library/bundle-size/ReactComponents.fixture.js @@ -0,0 +1,7 @@ +import * as rbc from '@fluentui/react-headless'; + +console.log(rbc); + +export default { + name: 'react-headless: entire library', +}; diff --git a/packages/react-components/react-headless/library/bundle-size/webpack.analyze.js b/packages/react-components/react-headless/library/bundle-size/webpack.analyze.js new file mode 100644 index 00000000000000..4cda047ad759a4 --- /dev/null +++ b/packages/react-components/react-headless/library/bundle-size/webpack.analyze.js @@ -0,0 +1,39 @@ +// @ts-check +const path = require('path'); +const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); + +const libraryRoot = path.resolve(__dirname, '..'); + +/** @type {import('webpack').Configuration} */ +module.exports = { + mode: 'production', + entry: path.resolve(__dirname, 'ReactComponents.fixture.js'), + output: { + filename: 'bundle.js', + path: path.resolve(__dirname, '../dist/bundle-analyze'), + }, + resolve: { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + // Resolve @fluentui/react-headless to the local lib (ESM for best tree-shaking) + alias: { + '@fluentui/react-headless': path.resolve(libraryRoot, 'lib/index.js'), + }, + }, + externals: { + react: 'React', + 'react-dom': 'ReactDOM', + }, + optimization: { + // Disable scope hoisting so bundle-analyzer shows each module individually + concatenateModules: false, + }, + plugins: [ + new BundleAnalyzerPlugin({ + analyzerMode: 'static', + reportFilename: path.resolve(__dirname, '../dist/bundle-analyze/report.html'), + openAnalyzer: true, + generateStatsFile: true, + statsFilename: path.resolve(__dirname, '../dist/bundle-analyze/stats.json'), + }), + ], +}; diff --git a/packages/react-components/react-headless/library/config/api-extractor.json b/packages/react-components/react-headless/library/config/api-extractor.json new file mode 100644 index 00000000000000..8d482156d10d53 --- /dev/null +++ b/packages/react-components/react-headless/library/config/api-extractor.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "@fluentui/scripts-api-extractor/api-extractor.common.v-next.json", + "mainEntryPointFilePath": "/../../../../../../dist/out-tsc/types/packages/react-components//library/src/index.d.ts" +} diff --git a/packages/react-components/react-headless/library/config/tests.js b/packages/react-components/react-headless/library/config/tests.js new file mode 100644 index 00000000000000..c6c67de97059e8 --- /dev/null +++ b/packages/react-components/react-headless/library/config/tests.js @@ -0,0 +1,3 @@ +/** Jest test setup file. */ + +require('@testing-library/jest-dom'); diff --git a/packages/react-components/react-headless/library/docs/Spec.md b/packages/react-components/react-headless/library/docs/Spec.md new file mode 100644 index 00000000000000..f0a890246fe8ac --- /dev/null +++ b/packages/react-components/react-headless/library/docs/Spec.md @@ -0,0 +1,230 @@ +# @fluentui/react-headless Spec + +## Background + +`@fluentui/react-headless` is an **advanced, opt-in** package that exposes the base layer of Fluent UI v9 components: pure component logic, accessibility patterns, and semantic slot structure — without any styling opinions. + +It is intended for teams building custom design systems that significantly diverge from Fluent 2. For most teams, the default styled components in `@fluentui/react-components` remain the recommended path. + +**What this package provides:** + +- Component behavior, structure, and ARIA patterns +- Keyboard handling +- Semantic slot structure +- Base hooks and render functions for each component +- Re-exports of foundational utilities from `@fluentui/react-utilities` and `@fluentui/react-shared-contexts` + +**What this package does NOT provide:** + +- Design props (`appearance`, `size`, `shape`, etc.) +- Style logic (Griffel, design tokens) +- Motion logic (animations, transitions) +- Default slot implementations (icons, components) + +> **Important:** Base hooks provide ARIA attributes and semantic structure, but not visual accessibility (e.g., focus indicators, sufficient contrast). Consumers are responsible for implementing these in their custom styles. + +## Prior Art + +- [RFC: Component Base State Hooks](../../../../../../docs/react-v9/contributing/rfcs/react-components/convergence/base-state-hooks.md) +- Fluent UI v9 `_unstable` hook convention used throughout `@fluentui/react-*` packages + +## Sample Code + +Building a fully custom button using base hooks: + +```tsx +import * as React from 'react'; +import { useButton, renderButton_unstable } from '@fluentui/react-headless'; +import type { ButtonBaseProps, ButtonState } from '@fluentui/react-headless'; + +type CustomButtonProps = ButtonBaseProps & { + variant?: 'primary' | 'secondary' | 'tertiary'; + tone?: 'neutral' | 'success' | 'warning' | 'danger'; +}; + +export const CustomButton = React.forwardRef( + ({ variant = 'primary', tone = 'neutral', ...props }, ref) => { + const state = useButton(props, ref); + + state.root.className = ['custom-btn', `custom-btn--${variant}`, `custom-btn--${tone}`, state.root.className] + .filter(Boolean) + .join(' '); + + if (state.icon) { + state.icon.className = ['custom-btn__icon', state.icon.className].filter(Boolean).join(' '); + } + + return renderButton_unstable(state as ButtonState); + }, +); +``` + +## API + +### Naming Conventions + +| Artifact | Pattern | Example | +| ---------- | ------------------------------------ | ----------------------- | +| Hook | `use${ComponentName}` (public alias) | `useButton` | +| Props type | `${ComponentName}BaseProps` | `ButtonBaseProps` | +| State type | `${ComponentName}BaseState` | `ButtonBaseState` | +| Render fn | `render${ComponentName}_unstable` | `renderButton_unstable` | + +Public exports in this package use stable names (e.g., `useButton`) that alias the internal `use${ComponentName}Base_unstable` hooks from individual component packages. + +### Type Hierarchy + +```tsx +// Base types — component logic only +type ButtonBaseProps = ComponentProps & { + disabled?: boolean; + disabledFocusable?: boolean; + iconPosition?: 'before' | 'after'; +}; + +type ButtonBaseState = ComponentState & { + disabled: boolean; + disabledFocusable: boolean; + iconPosition: 'before' | 'after'; + iconOnly: boolean; +}; + +// Styled types — extend base with design props +type ButtonProps = ButtonBaseProps & { + appearance?: 'primary' | 'secondary' | 'outline' | 'subtle' | 'transparent'; + size?: 'small' | 'medium' | 'large'; + shape?: 'rounded' | 'circular' | 'square'; +}; +``` + +### Exported Components + +#### Utilities (from `@fluentui/react-shared-contexts`) + +| Export | Description | +| ------------------------- | ---------------------------------------- | +| `AnnounceProvider` | Provider for screen reader announcements | +| `PortalMountNodeProvider` | Provider for portal mount node | +| `useAnnounce` | Hook to trigger announcements | +| `useFluent` | Hook to access Fluent context | +| `usePortalMountNode` | Hook to access portal mount node | +| `useTooltipVisibility` | Hook to access tooltip visibility state | +| `useThemeClassName` | Hook to access theme class name | + +#### Utilities (from `@fluentui/react-utilities`) + +Includes: `getIntrinsicElementProps`, `getPartitionedNativeProps`, `getSlotClassNameProp_unstable`, `slot`, `assertSlots`, `IdPrefixProvider`, `resetIdsForTests`, `SSRProvider`, `useAnimationFrame`, `useId`, `useIsomorphicLayoutEffect`, `useEventCallback`, `mergeCallbacks`, `useIsSSR`, `useMergedRefs`, `useApplyScrollbarWidth`, `useScrollbarWidth`, `useSelection`, `useTimeout`, `isHTMLElement`. + +Also exports deprecated (but still available) APIs: `getNativeElementProps`, `getSlots`, `resolveShorthand`. + +#### Component Base Hooks and Types + +- **Accordion** — hooks: `useAccordion`, `useAccordionItem`, `useAccordionHeader`, `useAccordionPanel`; context: `useAccordionContextValues`, `useAccordionHeaderContextValues`; render: `renderAccordion`, `renderAccordionItem`, `renderAccordionHeader`, `renderAccordionPanel` +- **Badge** — hooks: `useBadge`, `usePresenceBadge`, `useCounterBadge`; render: `renderBadge` +- **Breadcrumb** — hooks: `useBreadcrumb`, `useBreadcrumbDivider`, `useBreadcrumbItem`, `useBreadcrumbButton`; context: `useBreadcrumbContextValues`; render: `renderBreadcrumb`, `renderBreadcrumbDivider`, `renderBreadcrumbItem`, `renderBreadcrumbButton` +- **Button** — hooks: `useButton`, `useToggleButton`; render: `renderButton_unstable`, `renderToggleButton_unstable` +- **Card** — hooks: `useCard`, `useCardFooter`, `useCardHeader`, `useCardPreview`; context: `useCardContextValues`; render: `renderCard_unstable`, `renderCardFooter_unstable`, `renderCardHeader_unstable`, `renderCardPreview_unstable` +- **Divider** — hook: `useDivider`; render: `renderDivider_unstable` +- **Image** — hook: `useImage`; render: `renderImage_unstable` +- **Input** — hook: `useInput`; render: `renderInput_unstable` +- **Link** — hook: `useLink`; render: `renderLink_unstable` +- **Persona** — hook: `usePersona`; render: `renderPersona` +- **Radio** — hooks: `useRadioGroup`, `useRadio`; context: `useRadioGroupContextValues`; render: `renderRadioGroup_unstable`, `renderRadio_unstable` +- **Rating** — hooks: `useRating`, `useRatingDisplay`, `useRatingItem`; context: `useRatingContextValues`, `useRatingDisplayContextValues`; render: `renderRating_unstable`, `renderRatingDisplay_unstable`, `renderRatingItem_unstable` +- **SearchBox** — hook: `useSearchBox`; render: `renderSearchBox_unstable` +- **Skeleton** — hooks: `useSkeleton`, `useSkeletonItem`; context: `useSkeletonContextValues`; render: `renderSkeleton_unstable`, `renderSkeletonItem_unstable` +- **Slider** — hook: `useSlider`; render: `renderSlider_unstable` +- **SpinButton** — hook: `useSpinButton`; render: `renderSpinButton_unstable` +- **Spinner** — hook: `useSpinner`; render: `renderSpinner_unstable` +- **Switch** — hook: `useSwitch`; render: `renderSwitch_unstable` +- **Textarea** — hook: `useTextarea`; render: `renderTextarea_unstable` + +#### Utilities (this package) + +| Export | Description | +| ------------------ | --------------------------------------------------------- | +| `composeComponent` | Utility to compose a component from base hook + render fn | + +## Structure + +### Composition Layers + +```text +use{Component}Base_unstable (base state hook — component logic + accessibility) + ↓ +use{Component}_unstable (component state hook — adds design props) + ↓ +{Component} (styled component — adds Griffel styles + tokens) +``` + +This package exposes the **first layer** only. Styled components in `@fluentui/react-components` compose on top of it. + +### Public API + +Base hooks are exported from this package under clean aliases (e.g., `useButton` instead of `useButtonBase_unstable`). Types use the `Base` suffix (`ButtonBaseProps`, `ButtonBaseState`). + +### Internal + +Each component's base logic lives in its individual package (e.g., `@fluentui/react-button`). This package re-exports them under a stable surface. Context value hooks with `_unstable` originals are wrapped as typed `const` exports to strip the suffix. + +## Migration + +This package is a new addition; there is no migration from v8 or v0. For teams currently using full Fluent UI components that want to adopt base hooks, the path is: + +1. Replace `useButton_unstable(props, ref)` with `useButton(props, ref)` from this package +2. Remove design props (`appearance`, `size`, `shape`) from your props type — use `ButtonBaseProps` instead +3. Apply your own class names or styles to the returned state slots before passing to the render function + +## Behaviors + +Base hooks encapsulate all interactive behavior inherited by the styled component layer: + +### Component States + +- **Disabled**: ARIA `disabled` and `aria-disabled` attributes set; keyboard events suppressed where appropriate +- **DisabledFocusable**: Element remains focusable while being semantically disabled (`aria-disabled="true"`) +- **Checked / selected**: Toggle components (ToggleButton, Radio, Switch) manage checked/pressed state via ARIA + +### Interaction + +#### Keyboard + +Keyboard behavior is component-specific and follows WAI-ARIA authoring practices. Each base hook applies the same keyboard handling as the corresponding styled component. + +#### Cursor + +No cursor styles are applied by base hooks. Consumers are responsible for setting appropriate cursor styles. + +#### Touch + +Touch events are handled via the same event handlers applied to root slots. + +#### Screen readers + +- ARIA roles, states, and properties are applied by the base hooks +- Live region announcements use `AnnounceProvider` / `useAnnounce` from `@fluentui/react-shared-contexts` + +## Accessibility + +Base hooks provide the semantic foundation for accessibility, but consumers must ensure their custom styles maintain: + +- **Visible focus indicators** — base hooks do not apply focus ring styles +- **Sufficient color contrast** — base hooks do not apply colors or tokens +- **Appropriate visual feedback** for all interactive states (hover, active, disabled) + +### ARIA Patterns Applied + +Each component follows its corresponding WAI-ARIA authoring practice: + +| Component | ARIA pattern | +| ---------- | --------------------- | +| Accordion | Accordion pattern | +| Button | Button / Link pattern | +| RadioGroup | Radio Group pattern | +| Switch | Switch pattern | +| Slider | Slider pattern | +| SpinButton | Spinbutton pattern | +| Rating | Slider/radio hybrid | +| Skeleton | Status / live region | + +> Keyboard navigation, focus management, and state announcements are delegated to the individual component packages and are identical to their styled counterparts. diff --git a/packages/react-components/react-headless/library/eslint.config.js b/packages/react-components/react-headless/library/eslint.config.js new file mode 100644 index 00000000000000..ec2e7cb1fc479f --- /dev/null +++ b/packages/react-components/react-headless/library/eslint.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const fluentPlugin = require('@fluentui/eslint-plugin'); + +module.exports = [...fluentPlugin.configs['flat/react']]; diff --git a/packages/react-components/react-headless/library/etc/react-headless.api.md b/packages/react-components/react-headless/library/etc/react-headless.api.md new file mode 100644 index 00000000000000..02ff5a25be14f6 --- /dev/null +++ b/packages/react-components/react-headless/library/etc/react-headless.api.md @@ -0,0 +1,848 @@ +## API Report File for "@fluentui/react-headless" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { AccordionContextValue } from '@fluentui/react-accordion'; +import { AccordionContextValues } from '@fluentui/react-accordion'; +import { AccordionHeaderContextValues } from '@fluentui/react-accordion'; +import { AccordionHeaderBaseProps as AccordionHeaderProps } from '@fluentui/react-accordion'; +import { AccordionHeaderSlots } from '@fluentui/react-accordion'; +import { AccordionHeaderBaseState as AccordionHeaderState } from '@fluentui/react-accordion'; +import { AccordionItemContextValue } from '@fluentui/react-accordion'; +import { AccordionItemContextValues } from '@fluentui/react-accordion'; +import { AccordionItemProps } from '@fluentui/react-accordion'; +import { AccordionItemSlots } from '@fluentui/react-accordion'; +import { AccordionItemState } from '@fluentui/react-accordion'; +import { AccordionPanelBaseProps as AccordionPanelProps } from '@fluentui/react-accordion'; +import { AccordionPanelSlots } from '@fluentui/react-accordion'; +import { AccordionPanelBaseState as AccordionPanelState } from '@fluentui/react-accordion'; +import { AccordionBaseProps as AccordionProps } from '@fluentui/react-accordion'; +import { AccordionSlots } from '@fluentui/react-accordion'; +import { AccordionBaseState as AccordionState } from '@fluentui/react-accordion'; +import { AnnounceContextValue } from '@fluentui/react-shared-contexts'; +import { AnnounceProvider } from '@fluentui/react-shared-contexts'; +import { assertSlots } from '@fluentui/react-utilities'; +import { AvatarGroupContextValues } from '@fluentui/react-avatar'; +import { AvatarGroupItemBaseProps as AvatarGroupItemProps } from '@fluentui/react-avatar'; +import { AvatarGroupItemSlots } from '@fluentui/react-avatar'; +import { AvatarGroupItemBaseState as AvatarGroupItemState } from '@fluentui/react-avatar'; +import { AvatarGroupBaseProps as AvatarGroupProps } from '@fluentui/react-avatar'; +import { AvatarGroupSlots } from '@fluentui/react-avatar'; +import { AvatarGroupBaseState as AvatarGroupState } from '@fluentui/react-avatar'; +import { AvatarBaseProps as AvatarProps } from '@fluentui/react-avatar'; +import { AvatarSlots } from '@fluentui/react-avatar'; +import { AvatarBaseState as AvatarState } from '@fluentui/react-avatar'; +import { BadgeBaseProps as BadgeProps } from '@fluentui/react-badge'; +import { BadgeSlots } from '@fluentui/react-badge'; +import { BadgeBaseState as BadgeState } from '@fluentui/react-badge'; +import { BreadcrumbButtonBaseProps as BreadcrumbButtonProps } from '@fluentui/react-breadcrumb'; +import { BreadcrumbButtonSlots } from '@fluentui/react-breadcrumb'; +import { BreadcrumbButtonBaseState as BreadcrumbButtonState } from '@fluentui/react-breadcrumb'; +import { BreadcrumbContextValues } from '@fluentui/react-breadcrumb'; +import { BreadcrumbDividerBaseProps as BreadcrumbDividerProps } from '@fluentui/react-breadcrumb'; +import { BreadcrumbDividerSlots } from '@fluentui/react-breadcrumb'; +import { BreadcrumbDividerBaseState as BreadcrumbDividerState } from '@fluentui/react-breadcrumb'; +import { BreadcrumbItemBaseProps as BreadcrumbItemProps } from '@fluentui/react-breadcrumb'; +import { BreadcrumbItemSlots } from '@fluentui/react-breadcrumb'; +import { BreadcrumbItemBaseState as BreadcrumbItemState } from '@fluentui/react-breadcrumb'; +import { BreadcrumbBaseProps as BreadcrumbProps } from '@fluentui/react-breadcrumb'; +import { BreadcrumbSlots } from '@fluentui/react-breadcrumb'; +import { BreadcrumbBaseState as BreadcrumbState } from '@fluentui/react-breadcrumb'; +import { ButtonBaseProps as ButtonProps } from '@fluentui/react-button'; +import { ButtonSlots } from '@fluentui/react-button'; +import { ButtonBaseState as ButtonState } from '@fluentui/react-button'; +import { CheckboxBaseProps as CheckboxProps } from '@fluentui/react-checkbox'; +import { CheckboxSlots } from '@fluentui/react-checkbox'; +import { CheckboxBaseState as CheckboxState } from '@fluentui/react-checkbox'; +import { ComponentProps } from '@fluentui/react-utilities'; +import { ComponentState } from '@fluentui/react-utilities'; +import { CounterBadgeBaseProps as CounterBadgeProps } from '@fluentui/react-badge'; +import { CounterBadgeBaseState as CounterBadgeState } from '@fluentui/react-badge'; +import { DividerBaseProps as DividerProps } from '@fluentui/react-divider'; +import { DividerSlots } from '@fluentui/react-divider'; +import { DividerBaseState as DividerState } from '@fluentui/react-divider'; +import { FieldContextValue } from '@fluentui/react-field'; +import { FieldContextValues } from '@fluentui/react-field'; +import { FieldBaseProps as FieldProps } from '@fluentui/react-field'; +import { FieldSlots } from '@fluentui/react-field'; +import { FieldBaseState as FieldState } from '@fluentui/react-field'; +import { ForwardRefComponent } from '@fluentui/react-utilities'; +import { getIntrinsicElementProps } from '@fluentui/react-utilities'; +import { getNativeElementProps } from '@fluentui/react-utilities'; +import { getPartitionedNativeProps } from '@fluentui/react-utilities'; +import { getSlotClassNameProp_unstable } from '@fluentui/react-utilities'; +import { getSlots } from '@fluentui/react-utilities'; +import { IdPrefixProvider } from '@fluentui/react-utilities'; +import { ImageBaseProps as ImageProps } from '@fluentui/react-image'; +import { ImageSlots } from '@fluentui/react-image'; +import { ImageBaseState as ImageState } from '@fluentui/react-image'; +import { InputBaseProps as InputProps } from '@fluentui/react-input'; +import { InputSlots } from '@fluentui/react-input'; +import { InputBaseState as InputState } from '@fluentui/react-input'; +import { isHTMLElement } from '@fluentui/react-utilities'; +import { JSXElement } from '@fluentui/react-utilities'; +import { JSXIntrinsicElement } from '@fluentui/react-utilities'; +import { JSXIntrinsicElementKeys } from '@fluentui/react-utilities'; +import { LabelBaseProps as LabelProps } from '@fluentui/react-label'; +import { LabelSlots } from '@fluentui/react-label'; +import { LabelBaseState as LabelState } from '@fluentui/react-label'; +import { LinkBaseProps as LinkProps } from '@fluentui/react-link'; +import { LinkSlots } from '@fluentui/react-link'; +import { LinkBaseState as LinkState } from '@fluentui/react-link'; +import { mergeCallbacks } from '@fluentui/react-utilities'; +import { OnSelectionChangeCallback } from '@fluentui/react-utilities'; +import { OnSelectionChangeData } from '@fluentui/react-utilities'; +import { PersonaBaseProps as PersonaProps } from '@fluentui/react-persona'; +import { PersonaSlots } from '@fluentui/react-persona'; +import { PersonaBaseState as PersonaState } from '@fluentui/react-persona'; +import { PortalMountNodeProvider } from '@fluentui/react-shared-contexts'; +import { PresenceBadgeBaseProps as PresenceBadgeProps } from '@fluentui/react-badge'; +import { PresenceBadgeBaseState as PresenceBadgeState } from '@fluentui/react-badge'; +import { ProgressBarBaseProps as ProgressBarProps } from '@fluentui/react-progress'; +import { ProgressBarSlots } from '@fluentui/react-progress'; +import { ProgressBarBaseState as ProgressBarState } from '@fluentui/react-progress'; +import { RadioGroupContextValue } from '@fluentui/react-radio'; +import { RadioGroupContextValues } from '@fluentui/react-radio'; +import { RadioGroupBaseProps as RadioGroupProps } from '@fluentui/react-radio'; +import { RadioGroupSlots } from '@fluentui/react-radio'; +import { RadioGroupBaseState as RadioGroupState } from '@fluentui/react-radio'; +import { RadioBaseProps as RadioProps } from '@fluentui/react-radio'; +import { RadioSlots } from '@fluentui/react-radio'; +import { RadioBaseState as RadioState } from '@fluentui/react-radio'; +import { RatingContextValues } from '@fluentui/react-rating'; +import { RatingDisplayContextValues } from '@fluentui/react-rating'; +import { RatingDisplayBaseProps as RatingDisplayProps } from '@fluentui/react-rating'; +import { RatingDisplayBaseState as RatingDisplayState } from '@fluentui/react-rating'; +import { RatingItemBaseProps as RatingItemProps } from '@fluentui/react-rating'; +import { RatingItemSlots } from '@fluentui/react-rating'; +import { RatingItemBaseState as RatingItemState } from '@fluentui/react-rating'; +import { RatingBaseProps as RatingProps } from '@fluentui/react-rating'; +import { RatingBaseState as RatingState } from '@fluentui/react-rating'; +import * as React_2 from 'react'; +import { renderAccordion_unstable as renderAccordion } from '@fluentui/react-accordion'; +import { renderAccordionHeader_unstable as renderAccordionHeader } from '@fluentui/react-accordion'; +import { renderAccordionItem_unstable as renderAccordionItem } from '@fluentui/react-accordion'; +import { renderAvatar_unstable as renderAvatar } from '@fluentui/react-avatar'; +import { renderAvatarGroup_unstable as renderAvatarGroup } from '@fluentui/react-avatar'; +import { renderAvatarGroupItem_unstable as renderAvatarGroupItem } from '@fluentui/react-avatar'; +import { renderBadge_unstable as renderBadge } from '@fluentui/react-badge'; +import { renderBreadcrumb_unstable as renderBreadcrumb } from '@fluentui/react-breadcrumb'; +import { renderBreadcrumbButton_unstable as renderBreadcrumbButton } from '@fluentui/react-breadcrumb'; +import { renderBreadcrumbDivider_unstable as renderBreadcrumbDivider } from '@fluentui/react-breadcrumb'; +import { renderBreadcrumbItem_unstable as renderBreadcrumbItem } from '@fluentui/react-breadcrumb'; +import { renderButton_unstable as renderButton } from '@fluentui/react-button'; +import { renderCheckbox_unstable as renderCheckbox } from '@fluentui/react-checkbox'; +import { renderDivider_unstable as renderDivider } from '@fluentui/react-divider'; +import { renderField_unstable as renderField } from '@fluentui/react-field'; +import { renderImage_unstable as renderImage } from '@fluentui/react-image'; +import { renderInput_unstable as renderInput } from '@fluentui/react-input'; +import { renderLabel_unstable as renderLabel } from '@fluentui/react-label'; +import { renderLink_unstable as renderLink } from '@fluentui/react-link'; +import { renderProgressBar_unstable as renderProgressBar } from '@fluentui/react-progress'; +import { renderRadio_unstable as renderRadio } from '@fluentui/react-radio'; +import { renderRadioGroup_unstable as renderRadioGroup } from '@fluentui/react-radio'; +import { renderRating_unstable as renderRating } from '@fluentui/react-rating'; +import { renderRatingDisplay_unstable as renderRatingDisplay } from '@fluentui/react-rating'; +import { renderRatingItem_unstable as renderRatingItem } from '@fluentui/react-rating'; +import { renderSearchBox_unstable as renderSearchBox } from '@fluentui/react-search'; +import { renderSelect_unstable as renderSelect } from '@fluentui/react-select'; +import { renderSkeleton_unstable as renderSkeleton } from '@fluentui/react-skeleton'; +import { renderSkeletonItem_unstable as renderSkeletonItem } from '@fluentui/react-skeleton'; +import { renderSlider_unstable as renderSlider } from '@fluentui/react-slider'; +import { renderSpinButton_unstable as renderSpinButton } from '@fluentui/react-spinbutton'; +import { renderSpinner_unstable as renderSpinner } from '@fluentui/react-spinner'; +import { renderSwitch_unstable as renderSwitch } from '@fluentui/react-switch'; +import { renderTab_unstable as renderTab } from '@fluentui/react-tabs'; +import { renderTabList_unstable as renderTabList } from '@fluentui/react-tabs'; +import { renderTextarea_unstable as renderTextarea } from '@fluentui/react-textarea'; +import { renderToggleButton_unstable as renderToggleButton } from '@fluentui/react-button'; +import { resetIdsForTests } from '@fluentui/react-utilities'; +import { resolveShorthand } from '@fluentui/react-utilities'; +import { ResolveShorthandFunction } from '@fluentui/react-utilities'; +import { ResolveShorthandOptions } from '@fluentui/react-utilities'; +import { SearchBoxBaseProps as SearchBoxProps } from '@fluentui/react-search'; +import { SearchBoxSlots } from '@fluentui/react-search'; +import { SearchBoxBaseState as SearchBoxState } from '@fluentui/react-search'; +import { SelectionHookParams } from '@fluentui/react-utilities'; +import { SelectionItemId } from '@fluentui/react-utilities'; +import { SelectionMethods } from '@fluentui/react-utilities'; +import { SelectionMode as SelectionMode_2 } from '@fluentui/react-utilities'; +import { SelectBaseProps as SelectProps } from '@fluentui/react-select'; +import { SelectSlots } from '@fluentui/react-select'; +import { SelectBaseState as SelectState } from '@fluentui/react-select'; +import { SkeletonContextValues } from '@fluentui/react-skeleton'; +import { SkeletonItemBaseProps as SkeletonItemProps } from '@fluentui/react-skeleton'; +import { SkeletonItemSlots } from '@fluentui/react-skeleton'; +import { SkeletonItemBaseState as SkeletonItemState } from '@fluentui/react-skeleton'; +import { SkeletonBaseProps as SkeletonProps } from '@fluentui/react-skeleton'; +import { SkeletonBaseState as SkeletonState } from '@fluentui/react-skeleton'; +import { SliderBaseProps as SliderProps } from '@fluentui/react-slider'; +import { SliderSlots } from '@fluentui/react-slider'; +import { SliderBaseState as SliderState } from '@fluentui/react-slider'; +import { Slot } from '@fluentui/react-utilities'; +import { slot } from '@fluentui/react-utilities'; +import { SlotClassNames } from '@fluentui/react-utilities'; +import { SlotComponentType } from '@fluentui/react-utilities'; +import { SlotOptions } from '@fluentui/react-utilities'; +import { SlotPropsRecord } from '@fluentui/react-utilities'; +import { SlotRenderFunction } from '@fluentui/react-utilities'; +import { SpinButtonBaseProps as SpinButtonProps } from '@fluentui/react-spinbutton'; +import { SpinButtonSlots } from '@fluentui/react-spinbutton'; +import { SpinButtonBaseState as SpinButtonState } from '@fluentui/react-spinbutton'; +import { SpinnerBaseProps as SpinnerProps } from '@fluentui/react-spinner'; +import { SpinnerSlots } from '@fluentui/react-spinner'; +import { SpinnerBaseState as SpinnerState } from '@fluentui/react-spinner'; +import { SSRProvider } from '@fluentui/react-utilities'; +import { SwitchBaseProps as SwitchProps } from '@fluentui/react-switch'; +import { SwitchSlots } from '@fluentui/react-switch'; +import { SwitchBaseState as SwitchState } from '@fluentui/react-switch'; +import { TabListContextValue } from '@fluentui/react-tabs'; +import { TabListContextValues } from '@fluentui/react-tabs'; +import { TabListBaseProps as TabListProps } from '@fluentui/react-tabs'; +import { TabListSlots } from '@fluentui/react-tabs'; +import { TabListBaseState as TabListState } from '@fluentui/react-tabs'; +import { TabProps } from '@fluentui/react-tabs'; +import { TabSlots } from '@fluentui/react-tabs'; +import { TabState } from '@fluentui/react-tabs'; +import { TabValue } from '@fluentui/react-tabs'; +import { TextareaBaseProps as TextareaProps } from '@fluentui/react-textarea'; +import { TextareaSlots } from '@fluentui/react-textarea'; +import { TextareaBaseState as TextareaState } from '@fluentui/react-textarea'; +import { ToggleButtonBaseProps as ToggleButtonProps } from '@fluentui/react-button'; +import { ToggleButtonBaseState as ToggleButtonState } from '@fluentui/react-button'; +import { useAccordionBase_unstable as useAccordion } from '@fluentui/react-accordion'; +import { useAccordionHeaderBase_unstable as useAccordionHeader } from '@fluentui/react-accordion'; +import { useAccordionItem_unstable as useAccordionItem } from '@fluentui/react-accordion'; +import { useAccordionItemContext_unstable as useAccordionItemContext } from '@fluentui/react-accordion'; +import { useAccordionItemContextValues_unstable as useAccordionItemContextValues } from '@fluentui/react-accordion'; +import { useAccordionPanelBase_unstable as useAccordionPanel } from '@fluentui/react-accordion'; +import { useAnimationFrame } from '@fluentui/react-utilities'; +import { useAnnounce } from '@fluentui/react-shared-contexts'; +import { useApplyScrollbarWidth } from '@fluentui/react-utilities'; +import { useAvatarBase_unstable as useAvatar } from '@fluentui/react-avatar'; +import { useAvatarGroupBase_unstable as useAvatarGroup } from '@fluentui/react-avatar'; +import { useAvatarGroupItemBase_unstable as useAvatarGroupItem } from '@fluentui/react-avatar'; +import { useBadgeBase_unstable as useBadge } from '@fluentui/react-badge'; +import { useBreadcrumbBase_unstable as useBreadcrumb } from '@fluentui/react-breadcrumb'; +import { useBreadcrumbButtonBase_unstable as useBreadcrumbButton } from '@fluentui/react-breadcrumb'; +import { useBreadcrumbDividerBase_unstable as useBreadcrumbDivider } from '@fluentui/react-breadcrumb'; +import { useBreadcrumbItemBase_unstable as useBreadcrumbItem } from '@fluentui/react-breadcrumb'; +import { useButtonBase_unstable as useButton } from '@fluentui/react-button'; +import { useCheckboxBase_unstable as useCheckbox } from '@fluentui/react-checkbox'; +import { useCounterBadgeBase_unstable as useCounterBadge } from '@fluentui/react-badge'; +import { useDividerBase_unstable as useDivider } from '@fluentui/react-divider'; +import { useEventCallback } from '@fluentui/react-utilities'; +import { useFieldBase_unstable as useField } from '@fluentui/react-field'; +import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; +import { useId } from '@fluentui/react-utilities'; +import { useImageBase_unstable as useImage } from '@fluentui/react-image'; +import { useInputBase_unstable as useInput } from '@fluentui/react-input'; +import { useIsomorphicLayoutEffect } from '@fluentui/react-utilities'; +import { useIsSSR } from '@fluentui/react-utilities'; +import { useLabelBase_unstable as useLabel } from '@fluentui/react-label'; +import { useLinkBase_unstable as useLink } from '@fluentui/react-link'; +import { useMergedRefs } from '@fluentui/react-utilities'; +import { usePersonaBase_unstable as usePersona } from '@fluentui/react-persona'; +import { usePortalMountNode } from '@fluentui/react-shared-contexts'; +import { usePresenceBadgeBase_unstable as usePresenceBadge } from '@fluentui/react-badge'; +import { useProgressBarBase_unstable as useProgressBar } from '@fluentui/react-progress'; +import { useRadioBase_unstable as useRadio } from '@fluentui/react-radio'; +import { useRadioGroupBase_unstable as useRadioGroup } from '@fluentui/react-radio'; +import { useRatingBase_unstable as useRating } from '@fluentui/react-rating'; +import { useRatingDisplayBase_unstable as useRatingDisplay } from '@fluentui/react-rating'; +import { useRatingItemBase_unstable as useRatingItem } from '@fluentui/react-rating'; +import { useScrollbarWidth } from '@fluentui/react-utilities'; +import { useSearchBoxBase_unstable as useSearchBox } from '@fluentui/react-search'; +import { useSelectBase_unstable as useSelect } from '@fluentui/react-select'; +import { useSelection } from '@fluentui/react-utilities'; +import { useSkeletonBase_unstable as useSkeleton } from '@fluentui/react-skeleton'; +import { useSkeletonItemBase_unstable as useSkeletonItem } from '@fluentui/react-skeleton'; +import { useSliderBase_unstable as useSlider } from '@fluentui/react-slider'; +import { useSpinButtonBase_unstable as useSpinButton } from '@fluentui/react-spinbutton'; +import { useSpinnerBase_unstable as useSpinner } from '@fluentui/react-spinner'; +import { useSwitchBase_unstable as useSwitch } from '@fluentui/react-switch'; +import { useTabBase_unstable as useTab } from '@fluentui/react-tabs'; +import { useTabListBase_unstable as useTabList } from '@fluentui/react-tabs'; +import { useTextareaBase_unstable as useTextarea } from '@fluentui/react-textarea'; +import { useThemeClassName_unstable as useThemeClassName } from '@fluentui/react-shared-contexts'; +import { useTimeout } from '@fluentui/react-utilities'; +import { useToggleButtonBase_unstable as useToggleButton } from '@fluentui/react-button'; +import { useTooltipVisibility_unstable as useTooltipVisibility } from '@fluentui/react-shared-contexts'; + +export { AccordionContextValue } + +export { AccordionContextValues } + +export { AccordionHeaderContextValues } + +export { AccordionHeaderProps } + +export { AccordionHeaderSlots } + +export { AccordionHeaderState } + +export { AccordionItemContextValue } + +export { AccordionItemContextValues } + +export { AccordionItemProps } + +export { AccordionItemSlots } + +export { AccordionItemState } + +export { AccordionPanelProps } + +export { AccordionPanelSlots } + +export { AccordionPanelState } + +export { AccordionProps } + +export { AccordionSlots } + +export { AccordionState } + +export { AnnounceContextValue } + +export { AnnounceProvider } + +export { assertSlots } + +export { AvatarGroupContextValues } + +export { AvatarGroupItemProps } + +export { AvatarGroupItemSlots } + +export { AvatarGroupItemState } + +export { AvatarGroupProps } + +export { AvatarGroupSlots } + +export { AvatarGroupState } + +export { AvatarProps } + +export { AvatarSlots } + +export { AvatarState } + +export { BadgeProps } + +export { BadgeSlots } + +export { BadgeState } + +export { BreadcrumbButtonProps } + +export { BreadcrumbButtonSlots } + +export { BreadcrumbButtonState } + +export { BreadcrumbContextValues } + +export { BreadcrumbDividerProps } + +export { BreadcrumbDividerSlots } + +export { BreadcrumbDividerState } + +export { BreadcrumbItemProps } + +export { BreadcrumbItemSlots } + +export { BreadcrumbItemState } + +export { BreadcrumbProps } + +export { BreadcrumbSlots } + +export { BreadcrumbState } + +export { ButtonProps } + +export { ButtonSlots } + +export { ButtonState } + +export { CheckboxProps } + +export { CheckboxSlots } + +export { CheckboxState } + +export { ComponentProps } + +export { ComponentState } + +// @public +export function composeComponent(options: ComposeComponentOptions): ForwardRefComponent; + +// @public +export type ComposeComponentOptions = [ContextValues] extends [never] ? ComposeComponentOptionsWithoutContext : ComposeComponentOptionsWithContext; + +export { CounterBadgeProps } + +export { CounterBadgeState } + +export { DividerProps } + +export { DividerSlots } + +export { DividerState } + +export { FieldContextValue } + +export { FieldContextValues } + +export { FieldProps } + +export { FieldSlots } + +export { FieldState } + +export { ForwardRefComponent } + +export { getIntrinsicElementProps } + +export { getNativeElementProps } + +export { getPartitionedNativeProps } + +export { getSlotClassNameProp_unstable } + +export { getSlots } + +export { IdPrefixProvider } + +export { ImageProps } + +export { ImageSlots } + +export { ImageState } + +export { InputProps } + +export { InputSlots } + +export { InputState } + +export { isHTMLElement } + +export { JSXElement } + +export { JSXIntrinsicElement } + +export { JSXIntrinsicElementKeys } + +export { LabelProps } + +export { LabelSlots } + +export { LabelState } + +export { LinkProps } + +export { LinkSlots } + +export { LinkState } + +export { mergeCallbacks } + +export { OnSelectionChangeCallback } + +export { OnSelectionChangeData } + +export { PersonaProps } + +export { PersonaSlots } + +export { PersonaState } + +export { PortalMountNodeProvider } + +export { PresenceBadgeProps } + +export { PresenceBadgeState } + +export { ProgressBarProps } + +export { ProgressBarSlots } + +export { ProgressBarState } + +export { RadioGroupContextValue } + +export { RadioGroupContextValues } + +export { RadioGroupProps } + +export { RadioGroupSlots } + +export { RadioGroupState } + +export { RadioProps } + +export { RadioSlots } + +export { RadioState } + +export { RatingContextValues } + +export { RatingDisplayContextValues } + +export { RatingDisplayProps } + +export { RatingDisplayState } + +export { RatingItemProps } + +export { RatingItemSlots } + +export { RatingItemState } + +export { RatingProps } + +export { RatingState } + +export { renderAccordion } + +export { renderAccordionHeader } + +export { renderAccordionItem } + +// @public (undocumented) +export const renderAccordionPanel: (state: AccordionPanelState) => JSXElement; + +export { renderAvatar } + +export { renderAvatarGroup } + +export { renderAvatarGroupItem } + +export { renderBadge } + +export { renderBreadcrumb } + +export { renderBreadcrumbButton } + +export { renderBreadcrumbDivider } + +export { renderBreadcrumbItem } + +export { renderButton } + +export { renderCheckbox } + +export { renderDivider } + +export { renderField } + +export { renderImage } + +export { renderInput } + +export { renderLabel } + +export { renderLink } + +// @public (undocumented) +export const renderPersona: (state: PersonaState) => JSXElement; + +export { renderProgressBar } + +export { renderRadio } + +export { renderRadioGroup } + +export { renderRating } + +export { renderRatingDisplay } + +export { renderRatingItem } + +export { renderSearchBox } + +export { renderSelect } + +export { renderSkeleton } + +export { renderSkeletonItem } + +export { renderSlider } + +export { renderSpinButton } + +export { renderSpinner } + +export { renderSwitch } + +export { renderTab } + +export { renderTabList } + +export { renderTextarea } + +export { renderToggleButton } + +export { resetIdsForTests } + +export { resolveShorthand } + +export { ResolveShorthandFunction } + +export { ResolveShorthandOptions } + +export { SearchBoxProps } + +export { SearchBoxSlots } + +export { SearchBoxState } + +export { SelectionHookParams } + +export { SelectionItemId } + +export { SelectionMethods } + +export { SelectionMode_2 as SelectionMode } + +export { SelectProps } + +export { SelectSlots } + +export { SelectState } + +export { SkeletonContextValues } + +export { SkeletonItemProps } + +export { SkeletonItemSlots } + +export { SkeletonItemState } + +export { SkeletonProps } + +export { SkeletonState } + +export { SliderProps } + +export { SliderSlots } + +export { SliderState } + +export { Slot } + +export { slot } + +export { SlotClassNames } + +export { SlotComponentType } + +export { SlotOptions } + +export { SlotPropsRecord } + +export { SlotRenderFunction } + +export { SpinButtonProps } + +export { SpinButtonSlots } + +export { SpinButtonState } + +export { SpinnerProps } + +export { SpinnerSlots } + +export { SpinnerState } + +export { SSRProvider } + +export { SwitchProps } + +export { SwitchSlots } + +export { SwitchState } + +export { TabListContextValue } + +export { TabListContextValues } + +export { TabListProps } + +export { TabListSlots } + +export { TabListState } + +export { TabProps } + +export { TabSlots } + +export { TabState } + +export { TabValue } + +export { TextareaProps } + +export { TextareaSlots } + +export { TextareaState } + +export { ToggleButtonProps } + +export { ToggleButtonState } + +export { useAccordion } + +// @public (undocumented) +export const useAccordionContextValues: (state: AccordionState) => AccordionContextValues; + +export { useAccordionHeader } + +// @public (undocumented) +export const useAccordionHeaderContextValues: (state: AccordionHeaderState) => AccordionHeaderContextValues; + +export { useAccordionItem } + +export { useAccordionItemContext } + +export { useAccordionItemContextValues } + +export { useAccordionPanel } + +export { useAnimationFrame } + +export { useAnnounce } + +export { useApplyScrollbarWidth } + +export { useAvatar } + +export { useAvatarGroup } + +// @public (undocumented) +export const useAvatarGroupContextValues: (state: AvatarGroupState) => AvatarGroupContextValues; + +export { useAvatarGroupItem } + +export { useBadge } + +export { useBreadcrumb } + +export { useBreadcrumbButton } + +// @public (undocumented) +export const useBreadcrumbContextValues: (state: BreadcrumbState) => BreadcrumbContextValues; + +export { useBreadcrumbDivider } + +export { useBreadcrumbItem } + +export { useButton } + +export { useCheckbox } + +export { useCounterBadge } + +export { useDivider } + +export { useEventCallback } + +export { useField } + +// @public (undocumented) +export const useFieldContextValues: (state: FieldState) => FieldContextValues; + +export { useFluent } + +export { useId } + +export { useImage } + +export { useInput } + +export { useIsomorphicLayoutEffect } + +export { useIsSSR } + +export { useLabel } + +export { useLink } + +export { useMergedRefs } + +export { usePersona } + +export { usePortalMountNode } + +export { usePresenceBadge } + +export { useProgressBar } + +export { useRadio } + +export { useRadioGroup } + +// @public (undocumented) +export const useRadioGroupContextValues: (state: RadioGroupState) => RadioGroupContextValues; + +export { useRating } + +// @public (undocumented) +export const useRatingContextValues: (state: RatingState) => RatingContextValues; + +export { useRatingDisplay } + +// @public (undocumented) +export const useRatingDisplayContextValues: (state: RatingDisplayState) => RatingDisplayContextValues; + +export { useRatingItem } + +export { useScrollbarWidth } + +export { useSearchBox } + +export { useSelect } + +export { useSelection } + +export { useSkeleton } + +// @public (undocumented) +export const useSkeletonContextValues: (state: SkeletonState) => SkeletonContextValues; + +export { useSkeletonItem } + +export { useSlider } + +export { useSpinButton } + +export { useSpinner } + +export { useSwitch } + +export { useTab } + +export { useTabList } + +// @public (undocumented) +export const useTabListContextValues: (state: TabListState) => TabListContextValues; + +export { useTextarea } + +export { useThemeClassName } + +export { useTimeout } + +export { useToggleButton } + +export { useTooltipVisibility } + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/react-components/react-headless/library/jest.config.js b/packages/react-components/react-headless/library/jest.config.js new file mode 100644 index 00000000000000..965f897082eb4d --- /dev/null +++ b/packages/react-components/react-headless/library/jest.config.js @@ -0,0 +1,34 @@ +// @ts-check +/* eslint-disable */ + +const { readFileSync } = require('node:fs'); +const { join } = require('node:path'); + +// Reading the SWC compilation config and remove the "exclude" +// for the test files to be compiled by SWC +const { exclude: _, ...swcJestConfig } = JSON.parse(readFileSync(join(__dirname, '.swcrc'), 'utf-8')); + +// disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves. +// If we do not disable this, SWC Core will read .swcrc and won't transform our test files due to "exclude" +if (swcJestConfig.swcrc === undefined) { + swcJestConfig.swcrc = false; +} + +// Uncomment if using global setup/teardown files being transformed via swc +// https://nx.dev/packages/jest/documents/overview#global-setup/teardown-with-nx-libraries +// jest needs EsModule Interop to find the default exported setup/teardown functions +// swcJestConfig.module.noInterop = false; + +/** + * @type {import('@jest/types').Config.InitialOptions} + */ +module.exports = { + displayName: 'react-headless', + preset: '../../../../jest.preset.js', + transform: { + '^.+\\.tsx?$': ['@swc/jest', swcJestConfig], + }, + coverageDirectory: './coverage', + setupFilesAfterEnv: ['./config/tests.js'], + snapshotSerializers: ['@griffel/jest-serializer'], +}; diff --git a/packages/react-components/react-headless/library/package.json b/packages/react-components/react-headless/library/package.json new file mode 100644 index 00000000000000..87a927f83ed41d --- /dev/null +++ b/packages/react-components/react-headless/library/package.json @@ -0,0 +1,93 @@ +{ + "name": "@fluentui/react-headless", + "version": "0.0.1", + "description": "Compose custom React components using Fluent UI's base components", + "main": "lib-commonjs/index.js", + "module": "lib/index.js", + "typings": "./dist/index.d.ts", + "sideEffects": false, + "files": [ + "*.md", + "dist/*.d.ts", + "lib", + "lib-commonjs" + ], + "repository": { + "type": "git", + "url": "https://github.com/microsoft/fluentui" + }, + "license": "MIT", + "dependencies": { + "@fluentui/react-accordion": "^9.10.0", + "@fluentui/react-avatar": "^9.10.4", + "@fluentui/react-badge": "^9.5.1", + "@fluentui/react-button": "^9.9.0", + "@fluentui/react-breadcrumb": "^9.4.0", + "@fluentui/react-card": "^9.6.0", + "@fluentui/react-checkbox": "^9.5.17", + "@fluentui/react-combobox": "^9.16.18", + "@fluentui/react-dialog": "^9.17.3", + "@fluentui/react-divider": "^9.7.0", + "@fluentui/react-drawer": "^9.11.6", + "@fluentui/react-field": "^9.4.16", + "@fluentui/react-image": "^9.4.0", + "@fluentui/react-infolabel": "^9.4.18", + "@fluentui/react-input": "^9.8.0", + "@fluentui/react-label": "^9.3.15", + "@fluentui/react-link": "^9.8.0", + "@fluentui/react-list": "^9.6.12", + "@fluentui/react-menu": "^9.23.1", + "@fluentui/react-overflow": "^9.7.1", + "@fluentui/react-persona": "^9.7.1", + "@fluentui/react-portal": "^9.8.11", + "@fluentui/react-popover": "^9.14.1", + "@fluentui/react-positioning": "^9.22.0", + "@fluentui/react-progress": "^9.4.17", + "@fluentui/react-provider": "^9.22.15", + "@fluentui/react-radio": "^9.6.0", + "@fluentui/react-rating": "^9.4.0", + "@fluentui/react-search": "^9.4.0", + "@fluentui/react-select": "^9.4.16", + "@fluentui/react-shared-contexts": "^9.26.2", + "@fluentui/react-skeleton": "^9.7.0", + "@fluentui/react-slider": "^9.6.0", + "@fluentui/react-spinbutton": "^9.6.0", + "@fluentui/react-spinner": "^9.8.0", + "@fluentui/react-swatch-picker": "^9.5.0", + "@fluentui/react-switch": "^9.7.0", + "@fluentui/react-table": "^9.19.13", + "@fluentui/react-tabs": "^9.11.2", + "@fluentui/react-tabster": "^9.26.13", + "@fluentui/react-tags": "^9.7.19", + "@fluentui/react-textarea": "^9.7.0", + "@fluentui/react-theme": "^9.2.1", + "@fluentui/react-toast": "^9.7.16", + "@fluentui/react-toolbar": "^9.7.6", + "@fluentui/react-tooltip": "^9.9.3", + "@fluentui/react-utilities": "^9.26.2", + "@fluentui/react-text": "^9.6.15", + "@fluentui/react-tree": "^9.15.15", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <20.0.0", + "@types/react-dom": ">=16.9.0 <20.0.0", + "react": ">=16.14.0 <20.0.0", + "react-dom": ">=16.14.0 <20.0.0" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "node": "./lib-commonjs/index.js", + "import": "./lib/index.js", + "require": "./lib-commonjs/index.js" + }, + "./package.json": "./package.json" + }, + "beachball": { + "disallowedChangeTypes": [ + "major", + "prerelease" + ] + } +} diff --git a/packages/react-components/react-headless/library/project.json b/packages/react-components/react-headless/library/project.json new file mode 100644 index 00000000000000..d2fa3b07866e06 --- /dev/null +++ b/packages/react-components/react-headless/library/project.json @@ -0,0 +1,8 @@ +{ + "name": "react-headless", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "packages/react-components/react-headless/library/src", + "tags": ["platform:web", "vNext"], + "implicitDependencies": [] +} diff --git a/packages/react-components/react-headless/library/src/index.ts b/packages/react-components/react-headless/library/src/index.ts new file mode 100644 index 00000000000000..b7e019e90fb932 --- /dev/null +++ b/packages/react-components/react-headless/library/src/index.ts @@ -0,0 +1,618 @@ +'use client'; + +import type { JSXElement } from '@fluentui/react-utilities'; + +export { + AnnounceProvider, + PortalMountNodeProvider, + useAnnounce, + useFluent_unstable as useFluent, + usePortalMountNode, + useTooltipVisibility_unstable as useTooltipVisibility, + useThemeClassName_unstable as useThemeClassName, +} from '@fluentui/react-shared-contexts'; +export type { AnnounceContextValue } from '@fluentui/react-shared-contexts'; +export { + // getNativeElementProps is deprecated but removing it would be a breaking change + // eslint-disable-next-line @typescript-eslint/no-deprecated + getNativeElementProps, + getIntrinsicElementProps, + getPartitionedNativeProps, + getSlotClassNameProp_unstable, + // getSlots is deprecated but removing it would be a breaking change + // eslint-disable-next-line @typescript-eslint/no-deprecated + getSlots, + slot, + assertSlots, + IdPrefixProvider, + resetIdsForTests, + // resolveShorthand is deprecated but removing it would be a breaking change + // eslint-disable-next-line @typescript-eslint/no-deprecated + resolveShorthand, + SSRProvider, + useAnimationFrame, + useId, + useIsomorphicLayoutEffect, + useEventCallback, + mergeCallbacks, + useIsSSR, + useMergedRefs, + useApplyScrollbarWidth, + useScrollbarWidth, + useSelection, + useTimeout, + isHTMLElement, +} from '@fluentui/react-utilities'; +export type { + ComponentProps, + ComponentState, + ForwardRefComponent, + JSXElement, + JSXIntrinsicElement, + JSXIntrinsicElementKeys, + // ResolveShorthandFunction is deprecated but removing it would be a breaking change + // eslint-disable-next-line @typescript-eslint/no-deprecated + ResolveShorthandFunction, + // ResolveShorthandOptions is deprecated but removing it would be a breaking change + // eslint-disable-next-line @typescript-eslint/no-deprecated + ResolveShorthandOptions, + Slot, + SlotOptions, + SlotComponentType, + SlotClassNames, + SlotPropsRecord, + SlotRenderFunction, + OnSelectionChangeCallback, + OnSelectionChangeData, + SelectionHookParams, + SelectionItemId, + SelectionMethods, + SelectionMode, +} from '@fluentui/react-utilities'; + +// ─── @fluentui/react-accordion ─────────────────────────────────────────────── + +import type { + AccordionBaseState, + AccordionHeaderBaseState, + AccordionPanelBaseState, + AccordionContextValues, + AccordionHeaderContextValues, +} from '@fluentui/react-accordion'; +import { + useAccordionContextValues_unstable, + useAccordionHeaderContextValues_unstable, + renderAccordionPanel_unstable, +} from '@fluentui/react-accordion'; + +export type { + AccordionSlots, + AccordionBaseState as AccordionState, + AccordionBaseProps as AccordionProps, + AccordionContextValue, + AccordionContextValues, + AccordionItemSlots, + AccordionItemProps, + AccordionItemState, + AccordionItemContextValues, + AccordionItemContextValue, + AccordionHeaderSlots, + AccordionHeaderBaseState as AccordionHeaderState, + AccordionHeaderBaseProps as AccordionHeaderProps, + AccordionHeaderContextValues, + AccordionPanelSlots, + AccordionPanelBaseState as AccordionPanelState, + AccordionPanelBaseProps as AccordionPanelProps, +} from '@fluentui/react-accordion'; + +export { + useAccordionBase_unstable as useAccordion, + renderAccordion_unstable as renderAccordion, + useAccordionItemContextValues_unstable as useAccordionItemContextValues, + useAccordionItemContext_unstable as useAccordionItemContext, + useAccordionItem_unstable as useAccordionItem, + renderAccordionItem_unstable as renderAccordionItem, + useAccordionHeaderBase_unstable as useAccordionHeader, + renderAccordionHeader_unstable as renderAccordionHeader, + useAccordionPanelBase_unstable as useAccordionPanel, +} from '@fluentui/react-accordion'; + +export const useAccordionContextValues = useAccordionContextValues_unstable as ( + state: AccordionBaseState, +) => AccordionContextValues; + +export const useAccordionHeaderContextValues = useAccordionHeaderContextValues_unstable as ( + state: AccordionHeaderBaseState, +) => AccordionHeaderContextValues; + +export const renderAccordionPanel = renderAccordionPanel_unstable as (state: AccordionPanelBaseState) => JSXElement; + +// ─── @fluentui/react-badge ─────────────────────────────────────────────────── + +export type { + BadgeSlots, + BadgeBaseState as BadgeState, + BadgeBaseProps as BadgeProps, + PresenceBadgeBaseState as PresenceBadgeState, + PresenceBadgeBaseProps as PresenceBadgeProps, + CounterBadgeBaseState as CounterBadgeState, + CounterBadgeBaseProps as CounterBadgeProps, +} from '@fluentui/react-badge'; + +export { + useBadgeBase_unstable as useBadge, + renderBadge_unstable as renderBadge, + usePresenceBadgeBase_unstable as usePresenceBadge, + useCounterBadgeBase_unstable as useCounterBadge, +} from '@fluentui/react-badge'; + +// ─── @fluentui/react-breadcrumb ────────────────────────────────────────────── + +import { + type BreadcrumbBaseState, + type BreadcrumbContextValues, + useBreadcrumbContextValues_unstable, +} from '@fluentui/react-breadcrumb'; + +export type { + BreadcrumbSlots, + BreadcrumbBaseState as BreadcrumbState, + BreadcrumbBaseProps as BreadcrumbProps, + BreadcrumbDividerSlots, + BreadcrumbDividerBaseState as BreadcrumbDividerState, + BreadcrumbDividerBaseProps as BreadcrumbDividerProps, + BreadcrumbItemSlots, + BreadcrumbItemBaseState as BreadcrumbItemState, + BreadcrumbItemBaseProps as BreadcrumbItemProps, + BreadcrumbButtonSlots, + BreadcrumbButtonBaseState as BreadcrumbButtonState, + BreadcrumbButtonBaseProps as BreadcrumbButtonProps, + BreadcrumbContextValues, +} from '@fluentui/react-breadcrumb'; + +export { + useBreadcrumbBase_unstable as useBreadcrumb, + useBreadcrumbDividerBase_unstable as useBreadcrumbDivider, + useBreadcrumbItemBase_unstable as useBreadcrumbItem, + useBreadcrumbButtonBase_unstable as useBreadcrumbButton, + renderBreadcrumb_unstable as renderBreadcrumb, + renderBreadcrumbDivider_unstable as renderBreadcrumbDivider, + renderBreadcrumbItem_unstable as renderBreadcrumbItem, + renderBreadcrumbButton_unstable as renderBreadcrumbButton, +} from '@fluentui/react-breadcrumb'; + +export const useBreadcrumbContextValues = useBreadcrumbContextValues_unstable as ( + state: BreadcrumbBaseState, +) => BreadcrumbContextValues; + +// ─── @fluentui/react-button ────────────────────────────────────────────────── + +export type { + ButtonSlots, + ButtonBaseState as ButtonState, + ButtonBaseProps as ButtonProps, + ToggleButtonBaseState as ToggleButtonState, + ToggleButtonBaseProps as ToggleButtonProps, +} from '@fluentui/react-button'; + +export { + useButtonBase_unstable as useButton, + renderButton_unstable as renderButton, + useToggleButtonBase_unstable as useToggleButton, + renderToggleButton_unstable as renderToggleButton, +} from '@fluentui/react-button'; + +// // ─── @fluentui/react-card ──────────────────────────────────────────────────── +// import { type CardBaseState, type CardContextValue, useCardContext_unstable } from '@fluentui/react-card'; + +// export type { +// CardSlots, +// CardBaseState as CardState, +// CardBaseProps as CardProps, +// CardFooterSlots, +// CardFooterBaseState as CardFooterState, +// CardFooterBaseProps as CardFooterProps, +// CardHeaderSlots, +// CardHeaderBaseState as CardHeaderState, +// CardHeaderBaseProps as CardHeaderProps, +// CardPreviewSlots, +// CardPreviewBaseState as CardPreviewState, +// CardPreviewBaseProps as CardPreviewProps, +// } from '@fluentui/react-card'; + +// export { +// useCardBase_unstable as useCard, +// renderCard_unstable as renderCard, +// useCardFooterBase_unstable as useCardFooter, +// renderCardFooter_unstable as renderCardFooter, +// useCardHeaderBase_unstable as useCardHeader, +// renderCardHeader_unstable as renderCardHeader, +// useCardPreviewBase_unstable as useCardPreview, +// renderCardPreview_unstable as renderCardPreview, +// } from '@fluentui/react-card'; + +// export const useCardContextValues = useCardContext_unstable as (state: CardBaseState) => CardContextValue; + +// ─── @fluentui/react-divider ───────────────────────────────────────────────── + +export type { + DividerSlots, + DividerBaseState as DividerState, + DividerBaseProps as DividerProps, +} from '@fluentui/react-divider'; + +export { + useDividerBase_unstable as useDivider, + renderDivider_unstable as renderDivider, +} from '@fluentui/react-divider'; + +// ─── @fluentui/react-image ─────────────────────────────────────────────────── + +export type { ImageSlots, ImageBaseState as ImageState, ImageBaseProps as ImageProps } from '@fluentui/react-image'; + +export { useImageBase_unstable as useImage, renderImage_unstable as renderImage } from '@fluentui/react-image'; + +// ─── @fluentui/react-input ─────────────────────────────────────────────────── + +export type { InputSlots, InputBaseState as InputState, InputBaseProps as InputProps } from '@fluentui/react-input'; + +export { useInputBase_unstable as useInput, renderInput_unstable as renderInput } from '@fluentui/react-input'; + +// ─── @fluentui/react-link ──────────────────────────────────────────────────── + +export type { LinkSlots, LinkBaseState as LinkState, LinkBaseProps as LinkProps } from '@fluentui/react-link'; + +export { useLinkBase_unstable as useLink, renderLink_unstable as renderLink } from '@fluentui/react-link'; + +// ─── @fluentui/react-persona ───────────────────────────────────────────────── + +import type { PersonaBaseState } from '@fluentui/react-persona'; +import { renderPersona_unstable } from '@fluentui/react-persona'; + +export type { + PersonaSlots, + PersonaBaseState as PersonaState, + PersonaBaseProps as PersonaProps, +} from '@fluentui/react-persona'; + +export { usePersonaBase_unstable as usePersona } from '@fluentui/react-persona'; + +export const renderPersona = renderPersona_unstable as (state: PersonaBaseState) => JSXElement; + +// ─── @fluentui/react-radio ─────────────────────────────────────────────────── + +import { + type RadioGroupBaseState, + type RadioGroupContextValues, + useRadioGroupContextValues as useRadioGroupContextValues_unstable, +} from '@fluentui/react-radio'; + +export type { + RadioGroupSlots, + RadioGroupBaseState as RadioGroupState, + RadioGroupBaseProps as RadioGroupProps, + RadioSlots, + RadioBaseState as RadioState, + RadioBaseProps as RadioProps, + RadioGroupContextValue, + RadioGroupContextValues, +} from '@fluentui/react-radio'; + +export { + useRadioGroupBase_unstable as useRadioGroup, + renderRadioGroup_unstable as renderRadioGroup, + useRadioBase_unstable as useRadio, + renderRadio_unstable as renderRadio, +} from '@fluentui/react-radio'; + +export const useRadioGroupContextValues = useRadioGroupContextValues_unstable as ( + state: RadioGroupBaseState, +) => RadioGroupContextValues; + +// ─── @fluentui/react-rating ────────────────────────────────────────────────── + +import type { + RatingBaseState, + RatingContextValues, + RatingDisplayContextValues, + RatingDisplayBaseState, +} from '@fluentui/react-rating'; +import { + useRatingContextValues as useRatingContextValues_unstable, + useRatingDisplayContextValues as useRatingDisplayContextValues_unstable, +} from '@fluentui/react-rating'; + +export type { + RatingBaseState as RatingState, + RatingBaseProps as RatingProps, + RatingContextValues, + RatingDisplayBaseState as RatingDisplayState, + RatingDisplayBaseProps as RatingDisplayProps, + RatingDisplayContextValues, + RatingItemSlots, + RatingItemBaseState as RatingItemState, + RatingItemBaseProps as RatingItemProps, +} from '@fluentui/react-rating'; + +export { + useRatingBase_unstable as useRating, + useRatingDisplayBase_unstable as useRatingDisplay, + renderRating_unstable as renderRating, + renderRatingDisplay_unstable as renderRatingDisplay, + useRatingItemBase_unstable as useRatingItem, + renderRatingItem_unstable as renderRatingItem, +} from '@fluentui/react-rating'; + +export const useRatingContextValues = useRatingContextValues_unstable as ( + state: RatingBaseState, +) => RatingContextValues; + +export const useRatingDisplayContextValues = useRatingDisplayContextValues_unstable as ( + state: RatingDisplayBaseState, +) => RatingDisplayContextValues; + +// ─── @fluentui/react-search ────────────────────────────────────────────────── + +export type { + SearchBoxSlots, + SearchBoxBaseState as SearchBoxState, + SearchBoxBaseProps as SearchBoxProps, +} from '@fluentui/react-search'; + +export { + useSearchBoxBase_unstable as useSearchBox, + renderSearchBox_unstable as renderSearchBox, +} from '@fluentui/react-search'; + +// ─── @fluentui/react-skeleton ──────────────────────────────────────────────── + +import type { SkeletonBaseState, SkeletonContextValues } from '@fluentui/react-skeleton'; +import { useSkeletonContextValues as useSkeletonContextValues_unstable } from '@fluentui/react-skeleton'; + +export type { + SkeletonBaseState as SkeletonState, + SkeletonBaseProps as SkeletonProps, + SkeletonItemSlots, + SkeletonItemBaseState as SkeletonItemState, + SkeletonItemBaseProps as SkeletonItemProps, + SkeletonContextValues, +} from '@fluentui/react-skeleton'; + +export { + useSkeletonBase_unstable as useSkeleton, + useSkeletonItemBase_unstable as useSkeletonItem, + renderSkeletonItem_unstable as renderSkeletonItem, + renderSkeleton_unstable as renderSkeleton, +} from '@fluentui/react-skeleton'; + +export const useSkeletonContextValues = useSkeletonContextValues_unstable as ( + state: SkeletonBaseState, +) => SkeletonContextValues; + +// ─── @fluentui/react-slider ────────────────────────────────────────────────── + +export type { + SliderSlots, + SliderBaseState as SliderState, + SliderBaseProps as SliderProps, +} from '@fluentui/react-slider'; + +export { useSliderBase_unstable as useSlider, renderSlider_unstable as renderSlider } from '@fluentui/react-slider'; + +// ─── @fluentui/react-spinbutton ────────────────────────────────────────────── + +export type { + SpinButtonSlots, + SpinButtonBaseState as SpinButtonState, + SpinButtonBaseProps as SpinButtonProps, +} from '@fluentui/react-spinbutton'; + +export { + useSpinButtonBase_unstable as useSpinButton, + renderSpinButton_unstable as renderSpinButton, +} from '@fluentui/react-spinbutton'; + +// ─── @fluentui/react-spinner ───────────────────────────────────────────────── + +export type { + SpinnerSlots, + SpinnerBaseState as SpinnerState, + SpinnerBaseProps as SpinnerProps, +} from '@fluentui/react-spinner'; + +export { + useSpinnerBase_unstable as useSpinner, + renderSpinner_unstable as renderSpinner, +} from '@fluentui/react-spinner'; + +// ─── @fluentui/react-switch ────────────────────────────────────────────────── + +export type { + SwitchSlots, + SwitchBaseState as SwitchState, + SwitchBaseProps as SwitchProps, +} from '@fluentui/react-switch'; + +export { useSwitchBase_unstable as useSwitch, renderSwitch_unstable as renderSwitch } from '@fluentui/react-switch'; + +// ─── @fluentui/react-textarea ──────────────────────────────────────────────── + +export type { + TextareaSlots, + TextareaBaseState as TextareaState, + TextareaBaseProps as TextareaProps, +} from '@fluentui/react-textarea'; + +export { + useTextareaBase_unstable as useTextarea, + renderTextarea_unstable as renderTextarea, +} from '@fluentui/react-textarea'; + +// ─── @fluentui/react-avatar ─────────────────────────────────────────────────── + +import type { AvatarGroupBaseState, AvatarGroupContextValues } from '@fluentui/react-avatar'; +import { useAvatarGroupContextValues as useAvatarGroupContextValues_unstable } from '@fluentui/react-avatar'; + +export type { + AvatarSlots, + AvatarBaseState as AvatarState, + AvatarBaseProps as AvatarProps, + AvatarGroupSlots, + AvatarGroupBaseState as AvatarGroupState, + AvatarGroupBaseProps as AvatarGroupProps, + AvatarGroupContextValues, + AvatarGroupItemSlots, + AvatarGroupItemBaseState as AvatarGroupItemState, + AvatarGroupItemBaseProps as AvatarGroupItemProps, +} from '@fluentui/react-avatar'; + +export { + useAvatarBase_unstable as useAvatar, + renderAvatar_unstable as renderAvatar, + useAvatarGroupBase_unstable as useAvatarGroup, + renderAvatarGroup_unstable as renderAvatarGroup, + useAvatarGroupItemBase_unstable as useAvatarGroupItem, + renderAvatarGroupItem_unstable as renderAvatarGroupItem, +} from '@fluentui/react-avatar'; + +export const useAvatarGroupContextValues = useAvatarGroupContextValues_unstable as ( + state: AvatarGroupBaseState, +) => AvatarGroupContextValues; + +// ─── @fluentui/react-checkbox ──────────────────────────────────────────────── + +export type { + CheckboxSlots, + CheckboxBaseState as CheckboxState, + CheckboxBaseProps as CheckboxProps, +} from '@fluentui/react-checkbox'; + +export { + useCheckboxBase_unstable as useCheckbox, + renderCheckbox_unstable as renderCheckbox, +} from '@fluentui/react-checkbox'; + +// ─── @fluentui/react-label ─────────────────────────────────────────────────── + +export type { LabelSlots, LabelBaseState as LabelState, LabelBaseProps as LabelProps } from '@fluentui/react-label'; + +export { useLabelBase_unstable as useLabel, renderLabel_unstable as renderLabel } from '@fluentui/react-label'; + +// ─── @fluentui/react-progress ──────────────────────────────────────────────── + +export type { + ProgressBarSlots, + ProgressBarBaseState as ProgressBarState, + ProgressBarBaseProps as ProgressBarProps, +} from '@fluentui/react-progress'; + +export { + useProgressBarBase_unstable as useProgressBar, + renderProgressBar_unstable as renderProgressBar, +} from '@fluentui/react-progress'; + +// ─── @fluentui/react-select ────────────────────────────────────────────────── + +export type { + SelectSlots, + SelectBaseState as SelectState, + SelectBaseProps as SelectProps, +} from '@fluentui/react-select'; + +export { useSelectBase_unstable as useSelect, renderSelect_unstable as renderSelect } from '@fluentui/react-select'; + +// ─── @fluentui/react-tabs ──────────────────────────────────────────────────── + +import type { TabListBaseState, TabListContextValues } from '@fluentui/react-tabs'; +import { useTabListContextValues_unstable } from '@fluentui/react-tabs'; + +export type { + TabSlots, + TabState, + TabProps, + TabValue, + TabListSlots, + TabListBaseState as TabListState, + TabListBaseProps as TabListProps, + TabListContextValues, + TabListContextValue, +} from '@fluentui/react-tabs'; + +export { + useTabBase_unstable as useTab, + renderTab_unstable as renderTab, + useTabListBase_unstable as useTabList, + renderTabList_unstable as renderTabList, +} from '@fluentui/react-tabs'; + +export const useTabListContextValues = useTabListContextValues_unstable as ( + state: TabListBaseState, +) => TabListContextValues; + +// ─── @fluentui/react-tags ──────────────────────────────────────────────────── + +// import type { TagBaseState, TagGroupBaseState, TagGroupContextValues } from '@fluentui/react-tags'; +// import { useTagAvatarContextValues_unstable, useTagGroupContextValues_unstable } from '@fluentui/react-tags'; + +// export type { +// TagSlots, +// TagBaseState as TagState, +// TagBaseProps as TagProps, +// InteractionTagSlots, +// InteractionTagBaseState as InteractionTagState, +// InteractionTagBaseProps as InteractionTagProps, +// InteractionTagPrimaryContextValues, +// InteractionTagPrimarySlots, +// InteractionTagPrimaryBaseState as InteractionTagPrimaryState, +// InteractionTagPrimaryBaseProps as InteractionTagPrimaryProps, +// InteractionTagSecondarySlots, +// InteractionTagSecondaryBaseState as InteractionTagSecondaryState, +// InteractionTagSecondaryBaseProps as InteractionTagSecondaryProps, +// TagGroupSlots, +// TagGroupBaseState as TagGroupState, +// TagGroupBaseProps as TagGroupProps, +// TagGroupContextValues, +// } from '@fluentui/react-tags'; + +// export { +// useTagBase_unstable as useTag, +// renderTag_unstable as renderTag, +// useInteractionTagBase_unstable as useInteractionTag, +// renderInteractionTag_unstable as renderInteractionTag, +// useInteractionTagPrimaryBase_unstable as useInteractionTagPrimary, +// renderInteractionTagPrimary_unstable as renderInteractionTagPrimary, +// useInteractionTagSecondaryBase_unstable as useInteractionTagSecondary, +// renderInteractionTagSecondary_unstable as renderInteractionTagSecondary, +// useTagGroupBase_unstable as useTagGroup, +// renderTagGroup_unstable as renderTagGroup, +// } from '@fluentui/react-tags'; + +// export const useTagGroupContextValues = useTagGroupContextValues_unstable as ( +// state: TagGroupBaseState, +// ) => TagGroupContextValues; + +// export const useTagContextValues = useTagAvatarContextValues_unstable as unknown as ( +// state: TagBaseState, +// ) => ReturnType; + +// ─── @fluentui/react-field ─────────────────────────────────────────────────── + +import type { FieldBaseState, FieldContextValues } from '@fluentui/react-field'; +import { useFieldContextValues_unstable } from '@fluentui/react-field'; + +export type { + FieldSlots, + FieldBaseState as FieldState, + FieldBaseProps as FieldProps, + FieldContextValues, + FieldContextValue, +} from '@fluentui/react-field'; + +export { useFieldBase_unstable as useField, renderField_unstable as renderField } from '@fluentui/react-field'; + +export const useFieldContextValues = useFieldContextValues_unstable as (state: FieldBaseState) => FieldContextValues; + +// ─── Utilities ─────────────────────────────────────────────────────────────── + +export { composeComponent } from './utils/composeComponent'; +export type { ComposeComponentOptions } from './utils/composeComponent'; diff --git a/packages/react-components/react-headless/library/src/utils/composeComponent/composeComponent.tsx b/packages/react-components/react-headless/library/src/utils/composeComponent/composeComponent.tsx new file mode 100644 index 00000000000000..14641fe4861fbc --- /dev/null +++ b/packages/react-components/react-headless/library/src/utils/composeComponent/composeComponent.tsx @@ -0,0 +1,187 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent, JSXElement } from '@fluentui/react-utilities'; + +type BaseComposeComponentOptions = { + /** + * The `displayName` set on the resulting React component — visible in React DevTools + * and error messages. + */ + displayName: string; + + /** + * Hook that derives internal `State` from the component's public `Props` and forwarded `ref`. + * Called on every render. Must follow the Rules of Hooks. + * + * @param props - The props passed to the component. + * @param ref - The forwarded ref. + * @returns The resolved internal state object. + */ + useState: (props: Props, ref: React.Ref) => State; + + /** + * Optional hook that applies CSS-in-JS styles by mutating the state in place + * (e.g. setting `className` on slot objects). + * Called after `useState` on every render. Must follow the Rules of Hooks. + * When omitted, no styles are applied. + * + * @param state - The state returned by `useState`. + */ + useStyles?: (state: State) => void; +}; + +type ComposeComponentOptionsWithoutContext = BaseComposeComponentOptions< + Element, + Props, + State +> & { + /** + * Pure render function that converts `State` into JSX. + * Must NOT call hooks — all hook calls belong in `useState` or `useStyles`. + * + * @param state - The state returned by `useState`. + * @returns The rendered JSX element, or `null` to render nothing. + */ + render: (state: State) => JSXElement | null; +}; + +type ComposeComponentOptionsWithContext = BaseComposeComponentOptions< + Element, + Props, + State +> & { + /** + * Hook that derives context values from state and passes them to + * child components via React context providers set up inside `render`. + * Called after `useStyles` on every render. Must follow the Rules of Hooks. + * + * @param state - The state returned by `useState`. + * @returns An object whose values are distributed to context consumers. + */ + useContextValues: (state: State) => ContextValues; + + /** + * Pure render function that converts `State` and `ContextValues` into JSX. + * Must NOT call hooks — all hook calls belong in `useState`, `useStyles`, or `useContextValues`. + * + * @param state - The state returned by `useState`. + * @param contextValues - The values returned by `useContextValues`. + * @returns The rendered JSX element, or `null` to render nothing. + */ + render: (state: State, contextValues: ContextValues) => JSXElement | null; +}; + +/** + * Options for creating a composed component via {@link composeComponent}. + * + * @template Element + * @template Props + * @template State + * @template ContextValues + * `Element` is the underlying DOM element type (for example, `HTMLButtonElement`). + * `Props` is the public props accepted by the component. + * `State` is the internal state derived from props by `useState`. + * `ContextValues` is the shape of values passed via React context. It defaults to `never` + * (no context) when omitted. + * + * @example + * ```tsx + * type BadgeOptions = ComposeComponentOptions< + * HTMLSpanElement, + * BadgeProps, + * BadgeState + * >; + * ``` + */ +export type ComposeComponentOptions = [ContextValues] extends [never] + ? ComposeComponentOptionsWithoutContext + : ComposeComponentOptionsWithContext; + +/** + * Creates a strongly-typed, `forwardRef`-wrapped React component from a set of + * composable hooks and a render function. + * + * The component lifecycle runs in this order on every render: + * 1. `useState` — derives internal state from props and the forwarded ref. + * 2. `useContextValues` — derives context values from state *(optional)*. + * 3. `useStyles` — applies styles by mutating the state *(optional)*. + * 4. `render` — converts state and context values into JSX. + * + * @template Element + * @template Props + * @template State + * @template ContextValues + * `Element` is the underlying DOM element type the ref will point to. + * `Props` is the public prop surface of the component. + * `State` is the internal state shape produced by `useState`. + * `ContextValues` contains values passed down via React context and defaults to `never`. + * + * @param options - {@link ComposeComponentOptions} that define the component's behavior. + * @returns A `ForwardRefComponent` with `displayName` set. + * + * @example Basic usage — unstyled, no context values + * ```tsx + * import { useBadgeBase_unstable, renderBadge_unstable } from '@fluentui/react-components'; + * + * const UnstyledBadge = composeComponent({ + * displayName: 'UnstyledBadge', + * useState: useBadgeBase_unstable, + * render: renderBadge_unstable, + * }); + * ``` + * + * @example With custom styles + * ```tsx + * import { useBadgeBase_unstable, useBadgeStyles_unstable, renderBadge_unstable } from '@fluentui/react-components'; + * + * const CustomBadge = composeComponent({ + * displayName: 'CustomBadge', + * useState: useBadgeBase_unstable, + * useStyles(state) { + * state.root.className = `CustomBadge CustomBadge--${state.variant}`; + * }, + * render: renderBadge_unstable, + * }); + * ``` + * + * @example With context values passed to child slots + * ```tsx + * import { useMenuBase_unstable, useMenuStyles_unstable, useMenuContextValues_unstable, renderMenu_unstable } from '@fluentui/react-components'; + * + * const Menu = composeComponent({ + * displayName: 'Menu', + * useState: useMenuBase_unstable, + * useContextValues: useMenuContextValues_unstable, + * useStyles: useMenuStyles_unstable, + * render: renderMenu_unstable, + * }); + * ``` + */ +export function composeComponent( + options: ComposeComponentOptions, +): ForwardRefComponent { + const { + displayName, + useState, + // eslint-disable-next-line @typescript-eslint/no-empty-function + useStyles = () => {}, + render, + } = options; + + const useContextValues: (state: State) => ContextValues = + 'useContextValues' in options ? options.useContextValues! : () => ({} as ContextValues); + + const Component = React.forwardRef((props, ref) => { + const state = useState(props as Props, ref as React.Ref); + const contextValues = useContextValues(state); + + useStyles(state); + + return render(state, contextValues); + }); + + Component.displayName = displayName; + + return Component as ForwardRefComponent; +} diff --git a/packages/react-components/react-headless/library/src/utils/composeComponent/composeComponents.test.tsx b/packages/react-components/react-headless/library/src/utils/composeComponent/composeComponents.test.tsx new file mode 100644 index 00000000000000..191554c5a6b6b8 --- /dev/null +++ b/packages/react-components/react-headless/library/src/utils/composeComponent/composeComponents.test.tsx @@ -0,0 +1,401 @@ +/** @jsxRuntime automatic */ +/** @jsxImportSource @fluentui/react-jsx-runtime */ + +import * as React from 'react'; +import { render } from '@testing-library/react'; + +import { composeComponent } from './composeComponent'; +import { assertSlots, slot } from '@fluentui/react-utilities'; +import type { ComponentProps, ComponentState, RefAttributes, Slot } from '@fluentui/react-utilities'; + +type TestForwardRefComponent = React.ForwardRefExoticComponent< + React.PropsWithoutRef & RefAttributes +>; + +describe('composeComponent', () => { + it('sets displayName on the returned component', () => { + const Comp = composeComponent({ + displayName: 'TestComp', + useState: () => ({}), + render: () => null, + }); + + expect(Comp.displayName).toBe('TestComp'); + }); + + it('calls useState with props and the forwarded ref', () => { + const ref = React.createRef(); + const mockUseState = jest.fn((_props: { id?: string }, _ref: React.Ref) => ({})); + + const Comp = composeComponent({ + displayName: 'Comp', + useState: mockUseState, + render: () => null, + }); + + // ForwardRefComponent<{}> infers ref type as never via InferredElementRefType, + // so we bypass the JSX type-check with createElement + cast. + render(React.createElement(Comp as TestForwardRefComponent<{ id?: string }, HTMLSpanElement>, { id: 'test', ref })); + + expect(mockUseState).toHaveBeenCalledTimes(1); + expect(mockUseState).toHaveBeenCalledWith({ id: 'test' }, ref); + }); + + it('passes state returned by useState to render', () => { + const state = { tag: 'sentinel' }; + const mockRender = jest.fn(() => null); + + const Comp = composeComponent({ + displayName: 'Comp', + useState: () => state, + render: mockRender, + }); + + render(); + + expect(mockRender).toHaveBeenCalledWith(state, expect.anything()); + }); + + it('forwards ref to the root DOM element', () => { + const ref = React.createRef(); + + const Comp = composeComponent }>({ + displayName: 'Comp', + useState: (_props, _ref) => ({ elRef: _ref }), + render: state => React.createElement('span', { ref: state.elRef }), + }); + + render(React.createElement(Comp as TestForwardRefComponent<{}, HTMLSpanElement>, { ref })); + + expect(ref.current).toBeInstanceOf(HTMLSpanElement); + }); + + // ------------------------------------------------------------------------- + // useStyles + // ------------------------------------------------------------------------- + + describe('useStyles', () => { + it('calls useStyles with state after useState', () => { + const state = { tag: 'sentinel' }; + const mockUseStyles = jest.fn(); + + const Comp = composeComponent({ + displayName: 'Comp', + useState: () => state, + useStyles: mockUseStyles, + render: () => null, + }); + + render(); + + expect(mockUseStyles).toHaveBeenCalledTimes(1); + expect(mockUseStyles).toHaveBeenCalledWith(state); + }); + + it('calls useStyles again on every re-render with the latest state', () => { + const mockUseStyles = jest.fn(); + + const Comp = composeComponent({ + displayName: 'Comp', + useState: props => ({ value: props.value }), + useStyles: mockUseStyles, + render: () => null, + }); + + const { rerender } = render(); + + expect(mockUseStyles).toHaveBeenCalledTimes(1); + expect(mockUseStyles).toHaveBeenLastCalledWith({ value: 'first' }); + + rerender(); + + expect(mockUseStyles).toHaveBeenCalledTimes(2); + expect(mockUseStyles).toHaveBeenLastCalledWith({ value: 'second' }); + }); + + it('state mutations from useStyles are visible in the render output', () => { + type State = { className: string }; + + const Comp = composeComponent({ + displayName: 'Comp', + useState: () => ({ className: '' }), + useStyles: state => { + state.className = 'applied'; + }, + render: state => React.createElement('span', { 'data-testid': 'el', className: state.className }), + }); + + const { getByTestId } = render(); + + expect((getByTestId('el') as HTMLElement).getAttribute('class')).toBe('applied'); + }); + + it('does not crash when useStyles is omitted', () => { + const Comp = composeComponent({ + displayName: 'Comp', + useState: () => ({}), + render: () => React.createElement('span', { 'data-testid': 'el' }), + }); + + const { getByTestId } = render(); + + expect(getByTestId('el')).toBeTruthy(); + }); + }); + + // ------------------------------------------------------------------------- + // useContextValues + // ------------------------------------------------------------------------- + + describe('useContextValues', () => { + // Shared fixture: a minimal Menu that exposes `open` via React context. + const Ctx = React.createContext<{ open: boolean } | undefined>(undefined); + + type MenuProps = { open: boolean; children?: React.ReactNode }; + type MenuState = { open: boolean; children?: React.ReactNode }; + + const Menu = composeComponent({ + displayName: 'Menu', + useState: props => ({ open: props.open, children: props.children }), + useContextValues: state => ({ menu: { open: state.open } }), + render: (state, contextValues) => + React.createElement(Ctx.Provider, { value: contextValues!.menu }, state.children), + }); + + // React.createElement avoids native JSX in this file, which uses @fluentui/react-jsx-runtime + // that does not declare JSX.IntrinsicElements. + const Consumer: React.FC = () => { + const context = React.useContext(Ctx); + + if (!context) { + throw new Error('Menu context is missing'); + } + + const { open } = context; + return React.createElement('span', { 'data-testid': 'consumer' }, open ? 'open' : 'closed'); + }; + + it('passes context values returned by useContextValues to render', () => { + const contextValues = { flag: true }; + const mockRender = jest.fn(() => null); + + const Comp = composeComponent({ + displayName: 'Comp', + useState: () => ({}), + useContextValues: () => contextValues, + render: mockRender, + }); + + render(); + + expect(mockRender).toHaveBeenCalledWith({}, contextValues); + }); + + it('makes context values derived from state available to child consumers', () => { + const { getByTestId } = render( + + + , + ); + + expect(getByTestId('consumer').textContent).toBe('open'); + }); + + it('re-derives context values when props change', () => { + const { getByTestId, rerender } = render( + + + , + ); + + expect(getByTestId('consumer').textContent).toBe('closed'); + + rerender( + + + , + ); + + expect(getByTestId('consumer').textContent).toBe('open'); + }); + + it('passes an empty object to render when useContextValues is omitted', () => { + const mockRender = jest.fn(() => null); + + const Comp = composeComponent({ + displayName: 'Comp', + useState: () => ({}), + render: mockRender, + }); + + render(); + + expect(mockRender).toHaveBeenCalledWith({}, {}); + }); + }); + + // ------------------------------------------------------------------------- + // Generic type parameters + // ------------------------------------------------------------------------- + + describe('generic type parameters', () => { + it('accepts explicit Element, Props, and State type parameters', () => { + type Props = { label: string }; + type State = { text: string }; + + // Exercises the internal `props as Props` and `ref as React.Ref` casts. + const Comp = composeComponent({ + displayName: 'TypedComp', + useState: props => ({ text: props.label }), + render: state => React.createElement('span', { 'data-testid': 'el' }, state.text), + }); + + const { getByTestId } = render(); + + expect(getByTestId('el').textContent).toBe('hello'); + }); + + it('sets displayName when generic type parameters are explicit', () => { + const Comp = composeComponent({ + displayName: 'TypedComp', + useState: props => ({ text: props.label }), + render: () => null, + }); + + expect(Comp.displayName).toBe('TypedComp'); + }); + + it('forwards the ref when generic element type is explicit', () => { + const ref = React.createRef(); + + const Comp = composeComponent }>({ + displayName: 'TypedComp', + useState: (_props, _ref) => ({ elRef: _ref }), + render: state => React.createElement('span', { ref: state.elRef }), + }); + + render(React.createElement(Comp as TestForwardRefComponent<{}, HTMLSpanElement>, { ref })); + + expect(ref.current).toBeInstanceOf(HTMLSpanElement); + }); + + it('supports generic component props by casting the return value', () => { + type ListProps = { items: T[]; renderItem: (item: T, index: number) => React.ReactElement }; + type ListState = Pick, 'items' | 'renderItem'>; + type GenericListComponent = ( + props: ListProps & RefAttributes, + ) => React.ReactElement | null; + + const List = composeComponent, ListState>({ + displayName: 'List', + useState: ({ items = [], renderItem }) => ({ items, renderItem }), + render: state => React.createElement('ul', { 'data-testid': 'list' }, state.items.map(state.renderItem)), + }) as GenericListComponent; + + const { getByTestId } = render( + + items={['a', 'b', 'c']} + renderItem={(item: string, i: number) => React.createElement('li', { key: i }, item)} + />, + ); + + expect(getByTestId('list').children).toHaveLength(3); + }); + }); + + // ------------------------------------------------------------------------- + // Integration: styled and unstyled component variants + // + // Demonstrates the primary use-case of composeComponent: sharing the same + // useState + render between a styled variant (with useStyles) and an unstyled + // variant (without useStyles), mirroring the real Fluent UI component pattern. + // ------------------------------------------------------------------------- + + describe('styled and unstyled component variants', () => { + type BadgeSlots = { root: NonNullable> }; + type BadgeBaseProps = ComponentProps; + type BadgeProps = BadgeBaseProps & { variant?: 'primary' | 'secondary' }; + type BadgeBaseState = ComponentState; + type BadgeState = BadgeBaseState & Required>; + + /** + * Base state hook, provides state slots and ARIA attributes, but no styles. + */ + const useBadgeBase = (props: BadgeBaseProps, ref: React.Ref): BadgeBaseState => { + return { + components: { root: 'span' }, + root: slot.always>(props, { + defaultProps: { ref }, + elementType: 'span', + }), + }; + }; + + /** + * State hook for the styled variant, adds style-related props to the base state. + */ + const useBadge = (props: BadgeProps, ref: React.Ref): BadgeState => { + const { variant = 'primary', ...rest } = props; + return { + ...useBadgeBase(rest, ref), + variant, + }; + }; + + /** + * Style hook that applies a className based on the variant + */ + const useBadgeStyles = (state: BadgeState) => { + state.root.className = `Fui-Badge Fui-Badge--${state.variant}`; + }; + + /** + * Render function shared by both styled and unstyled variants, asserts the presence of slots and renders the root slot as a span. + */ + const renderBadge = (state: BadgeBaseState) => { + assertSlots(state); + return ; + }; + + /** + * Styled component variant that uses the full state and styles. + */ + const Badge = composeComponent({ + displayName: 'Badge', + useState: useBadge, + useStyles: useBadgeStyles, + render: renderBadge, + }); + + /** + * Unstyled component variant that uses the base state without styles. + */ + const BadgeUnstyled = composeComponent({ + displayName: 'BadgeUnstyled', + useState: useBadgeBase, + render: renderBadge, + }); + + it('styled variant applies className via useStyles', () => { + const { container } = render(Label); + const el = container.firstChild as HTMLElement; + + expect(el.tagName).toBe('SPAN'); + expect(el.getAttribute('class')).toBe('Fui-Badge Fui-Badge--secondary'); + }); + + it('styled variant uses the default prop value when variant is omitted', () => { + const { container } = render(Label); + + expect((container.firstChild as HTMLElement).getAttribute('class')).toBe('Fui-Badge Fui-Badge--primary'); + }); + + it('unstyled variant renders the same structure without any className', () => { + const { container } = render(Label); + const el = container.firstChild as HTMLElement; + + expect(el.tagName).toBe('SPAN'); + expect(el.getAttribute('class')).toBeNull(); + }); + }); +}); diff --git a/packages/react-components/react-headless/library/src/utils/composeComponent/index.ts b/packages/react-components/react-headless/library/src/utils/composeComponent/index.ts new file mode 100644 index 00000000000000..0dfbaf5838586f --- /dev/null +++ b/packages/react-components/react-headless/library/src/utils/composeComponent/index.ts @@ -0,0 +1,2 @@ +export { composeComponent } from './composeComponent'; +export type { ComposeComponentOptions } from './composeComponent'; diff --git a/packages/react-components/react-headless/library/tsconfig.json b/packages/react-components/react-headless/library/tsconfig.json new file mode 100644 index 00000000000000..32bdbdf1ac26f0 --- /dev/null +++ b/packages/react-components/react-headless/library/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "target": "ES2019", + "noEmit": true, + "isolatedModules": true, + "importHelpers": true, + "jsx": "react", + "noUnusedLocals": true, + "preserveConstEnums": true + }, + "include": [], + "files": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/react-components/react-headless/library/tsconfig.lib.json b/packages/react-components/react-headless/library/tsconfig.lib.json new file mode 100644 index 00000000000000..53066fdd11fff0 --- /dev/null +++ b/packages/react-components/react-headless/library/tsconfig.lib.json @@ -0,0 +1,22 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "lib": ["ES2019", "dom"], + "declaration": true, + "declarationDir": "../../../../dist/out-tsc/types", + "outDir": "../../../../dist/out-tsc", + "inlineSources": true, + "types": ["static-assets", "environment"] + }, + "exclude": [ + "./src/testing/**", + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.test.ts", + "**/*.test.tsx", + "**/*.stories.ts", + "**/*.stories.tsx" + ], + "include": ["./src/**/*.ts", "./src/**/*.tsx"] +} diff --git a/packages/react-components/react-headless/library/tsconfig.spec.json b/packages/react-components/react-headless/library/tsconfig.spec.json new file mode 100644 index 00000000000000..0e881941843de8 --- /dev/null +++ b/packages/react-components/react-headless/library/tsconfig.spec.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "outDir": "dist", + "types": ["jest", "node", "@testing-library/jest-dom"] + }, + "include": [ + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.test.ts", + "**/*.test.tsx", + "**/*.d.ts", + "./src/testing/**/*.ts", + "./src/testing/**/*.tsx" + ] +} diff --git a/packages/react-components/react-headless/stories/.storybook/main.js b/packages/react-components/react-headless/stories/.storybook/main.js new file mode 100644 index 00000000000000..67905c6bfe15f2 --- /dev/null +++ b/packages/react-components/react-headless/stories/.storybook/main.js @@ -0,0 +1,14 @@ +const rootMain = require('../../../../../.storybook/main'); + +module.exports = /** @type {Omit} */ ({ + ...rootMain, + stories: [...rootMain.stories, '../src/**/*.mdx', '../src/**/index.stories.@(ts|tsx)'], + addons: [...rootMain.addons], + webpackFinal: (config, options) => { + const localConfig = { ...rootMain.webpackFinal(config, options) }; + + // add your own webpack tweaks if needed + + return localConfig; + }, +}); diff --git a/packages/react-components/react-headless/stories/.storybook/preview.js b/packages/react-components/react-headless/stories/.storybook/preview.js new file mode 100644 index 00000000000000..98274ed0b8095f --- /dev/null +++ b/packages/react-components/react-headless/stories/.storybook/preview.js @@ -0,0 +1,9 @@ +import * as rootPreview from '../../../../../.storybook/preview'; + +/** @type {typeof rootPreview.decorators} */ +export const decorators = [...rootPreview.decorators]; + +/** @type {typeof rootPreview.parameters} */ +export const parameters = { ...rootPreview.parameters }; + +export const tags = ['autodocs']; diff --git a/packages/react-components/react-headless/stories/.storybook/tsconfig.json b/packages/react-components/react-headless/stories/.storybook/tsconfig.json new file mode 100644 index 00000000000000..4cdd1ce9d006f1 --- /dev/null +++ b/packages/react-components/react-headless/stories/.storybook/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "", + "allowJs": true, + "checkJs": true, + "types": ["static-assets", "environment"] + }, + "include": ["*.js"] +} diff --git a/packages/react-components/react-headless/stories/README.md b/packages/react-components/react-headless/stories/README.md new file mode 100644 index 00000000000000..f85cde84b9f86d --- /dev/null +++ b/packages/react-components/react-headless/stories/README.md @@ -0,0 +1,17 @@ +# @fluentui/react-headless-stories + +Storybook stories for packages/react-components/react-headless + +## Usage + +To include within storybook specify stories globs: + +\`\`\`js +module.exports = { +stories: ['../packages/react-components/react-headless/stories/src/**/*.mdx', '../packages/react-components/react-headless/stories/src/**/index.stories.@(ts|tsx)'], +} +\`\`\` + +## API + +no public API available diff --git a/packages/react-components/react-headless/stories/eslint.config.js b/packages/react-components/react-headless/stories/eslint.config.js new file mode 100644 index 00000000000000..f8362c3e413031 --- /dev/null +++ b/packages/react-components/react-headless/stories/eslint.config.js @@ -0,0 +1,10 @@ +// @ts-check + +const fluentPlugin = require('@fluentui/eslint-plugin'); + +module.exports = [ + ...fluentPlugin.configs['flat/react'], + { + rules: {}, + }, +]; diff --git a/packages/react-components/react-headless/stories/package.json b/packages/react-components/react-headless/stories/package.json new file mode 100644 index 00000000000000..0a378117947b4a --- /dev/null +++ b/packages/react-components/react-headless/stories/package.json @@ -0,0 +1,6 @@ +{ + "name": "@fluentui/react-headless-stories", + "version": "0.0.0", + "private": true, + "devDependencies": {} +} diff --git a/packages/react-components/react-headless/stories/project.json b/packages/react-components/react-headless/stories/project.json new file mode 100644 index 00000000000000..4bbb63c78b4477 --- /dev/null +++ b/packages/react-components/react-headless/stories/project.json @@ -0,0 +1,8 @@ +{ + "name": "react-headless-stories", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "packages/react-components/react-headless/stories/src", + "tags": ["vNext", "platform:web", "type:stories"], + "implicitDependencies": [] +} diff --git a/packages/react-components/react-headless/stories/src/Accordion/ExampleAccordion.stories.css b/packages/react-components/react-headless/stories/src/Accordion/ExampleAccordion.stories.css new file mode 100644 index 00000000000000..eb91874e8e1d96 --- /dev/null +++ b/packages/react-components/react-headless/stories/src/Accordion/ExampleAccordion.stories.css @@ -0,0 +1,48 @@ +.accordion { + width: 300px; +} + +.accordion-item { + margin-bottom: 8px; +} + +.accordion-header { + display: flex; + align-items: center; + width: 100%; + padding: 12px 16px; + background-color: #f5f5f5; + border: 1px solid #e5e5e5; + border-radius: 6px 6px 0 0; + cursor: pointer; + font-size: 14px; + font-weight: 600; + text-align: left; +} + +.accordion-header-button { + all: unset; + display: flex; + align-items: center; + gap: 8px; + width: 100%; +} + +.accordion-panel { + width: 100%; + padding: 12px 16px; + background-color: #ffffff; + border: 1px solid #e5e5e5; + border-top-width: 0; + border-radius: 0 0 6px 6px; + font-size: 14px; + line-height: 1.5; +} + +.accordion-panel--open { + display: block; +} + +.accordion-panel--closed { + display: none; +} diff --git a/packages/react-components/react-headless/stories/src/Accordion/ExampleAccordion.stories.tsx b/packages/react-components/react-headless/stories/src/Accordion/ExampleAccordion.stories.tsx new file mode 100644 index 00000000000000..4b83855ad5aaba --- /dev/null +++ b/packages/react-components/react-headless/stories/src/Accordion/ExampleAccordion.stories.tsx @@ -0,0 +1,96 @@ +import * as React from 'react'; + +import { + composeComponent, + useAccordionHeader, + renderAccordionHeader, + useAccordionPanel, + renderAccordionPanel, + useAccordion, + useAccordionContextValues, + renderAccordion, + type AccordionProps, + type AccordionState, + type AccordionContextValues, + useAccordionItem, + renderAccordionItem, + useAccordionItemContextValues, + useAccordionHeaderContextValues, + AccordionHeaderProps, +} from '@fluentui/react-headless'; + +import './ExampleAccordion.stories.css'; + +const Accordion = composeComponent({ + displayName: 'Accordion', + useState: useAccordion, + useContextValues: useAccordionContextValues, + useStyles: (state: AccordionState) => { + state.root.className = 'accordion'; + }, + render: renderAccordion, +}); + +const AccordionItem = composeComponent({ + displayName: 'AccordionItem', + useState: useAccordionItem, + useContextValues: useAccordionItemContextValues, + useStyles(state) { + state.root.className = 'accordion-item'; + }, + render: renderAccordionItem, +}); + +const AccordionHeader = composeComponent({ + displayName: 'AccordionHeader', + useState(props: AccordionHeaderProps, ref: React.Ref) { + const state = useAccordionHeader(props, ref); + + if (state.expandIcon) { + state.expandIcon.children = state.open ? '-' : '+'; + } + + return state; + }, + useContextValues: useAccordionHeaderContextValues, + useStyles: state => { + state.root.className = 'accordion-header'; + state.button.className = 'accordion-header-button'; + }, + render: renderAccordionHeader, +}); + +const AccordionPanel = composeComponent({ + displayName: 'AccordionPanel', + useState: useAccordionPanel, + useStyles: state => { + state.root.className = `accordion-panel ${state.open ? 'accordion-panel--open' : 'accordion-panel--closed'}`; + }, + render: renderAccordionPanel, +}); + +export const Example = (): React.ReactNode => ( + console.log('Toggled item:', data.value)}> + + Section #1 + Section #1 content. + + + Section #2 + Section #2 content. + + + Section #3 + Section #3 content. + + +); + +Example.parameters = { + docs: { + description: { + story: + 'Apply custom styles to `AccordionHeader` and `AccordionPanel` using the `useStyles` hook inside `composeComponent`. Set styles on the `state.root` and `state.button` slot objects. This pattern works with any styling approach — inline styles, CSS modules, or CSS-in-JS — while retaining all accessibility and keyboard interaction from the base hooks.', + }, + }, +}; diff --git a/packages/react-components/react-headless/stories/src/Accordion/index.stories.ts b/packages/react-components/react-headless/stories/src/Accordion/index.stories.ts new file mode 100644 index 00000000000000..4417fffffb9158 --- /dev/null +++ b/packages/react-components/react-headless/stories/src/Accordion/index.stories.ts @@ -0,0 +1,20 @@ +import type { Meta } from '@storybook/react-webpack5'; +import { AccordionProps } from '@fluentui/react-headless'; + +export { Example } from './ExampleAccordion.stories'; + +const meta: Meta = { + title: 'Base Components/Accordion', + parameters: { + docs: { + hideArgsTable: true, + skipPrimaryStory: true, + description: { + component: + 'Base Accordion components for building custom expandable section implementations. Compose `useAccordion`, `useAccordionHeader`, and `useAccordionPanel` with their corresponding render functions via `composeComponent`. The base hooks provide keyboard navigation, ARIA attributes, and expand/collapse state management while leaving all visual styling to you.', + }, + }, + }, +}; + +export default meta; diff --git a/packages/react-components/react-headless/stories/src/Avatar/ExampleAvatar.stories.css b/packages/react-components/react-headless/stories/src/Avatar/ExampleAvatar.stories.css new file mode 100644 index 00000000000000..e09034ea5a485e --- /dev/null +++ b/packages/react-components/react-headless/stories/src/Avatar/ExampleAvatar.stories.css @@ -0,0 +1,55 @@ +.avatar-container { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + +.avatar { + display: inline-flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border-radius: 50%; + background-color: #0078d4; + color: #ffffff; + font-size: 14px; + font-weight: 600; + overflow: hidden; + user-select: none; +} + +.avatar-initials { + font-size: 14px; + font-weight: 600; + line-height: 1; +} + +.avatar-icon { + display: flex; + align-items: center; + justify-content: center; +} + +.avatar-image { + width: 100%; + height: 100%; + object-fit: cover; +} + +.avatar-group { + display: flex; +} + +.avatar-group-item { + margin-left: -8px; +} + +.avatar-group-item:first-child { + margin-left: 0; +} + +.avatar-group-item .avatar { + border: 2px solid #ffffff; +} diff --git a/packages/react-components/react-headless/stories/src/Avatar/ExampleAvatar.stories.tsx b/packages/react-components/react-headless/stories/src/Avatar/ExampleAvatar.stories.tsx new file mode 100644 index 00000000000000..c4868e82dba68c --- /dev/null +++ b/packages/react-components/react-headless/stories/src/Avatar/ExampleAvatar.stories.tsx @@ -0,0 +1,87 @@ +import * as React from 'react'; + +import { + composeComponent, + useAvatar, + renderAvatar, + useAvatarGroup, + renderAvatarGroup, + useAvatarGroupContextValues, + useAvatarGroupItem, + renderAvatarGroupItem, +} from '@fluentui/react-headless'; +import type { + AvatarProps, + AvatarState, + AvatarGroupProps, + AvatarGroupState, + AvatarGroupContextValues, + AvatarGroupItemProps, + AvatarGroupItemState, +} from '@fluentui/react-headless'; + +import './ExampleAvatar.stories.css'; + +const Avatar = composeComponent({ + displayName: 'Avatar', + useState: useAvatar, + useStyles(state) { + state.root.className = 'avatar'; + if (state.initials) { + state.initials.className = 'avatar-initials'; + } + if (state.icon) { + state.icon.className = 'avatar-icon'; + } + if (state.image) { + state.image.className = 'avatar-image'; + } + }, + render: renderAvatar, +}); + +const AvatarGroupItem = composeComponent({ + displayName: 'AvatarGroupItem', + useState: useAvatarGroupItem, + useStyles(state) { + state.root.className = 'avatar-group-item'; + }, + render: renderAvatarGroupItem, +}); + +const AvatarGroup = composeComponent({ + displayName: 'AvatarGroup', + useState: useAvatarGroup, + useContextValues: useAvatarGroupContextValues, + useStyles(state) { + state.root.className = 'avatar-group'; + }, + render: renderAvatarGroup, +}); + +export const Example = (): React.ReactNode => ( +
+ + + + + + + + + + + + + +
+); + +Example.parameters = { + docs: { + description: { + story: + 'Compose `useAvatar` with `renderAvatar` to build a standalone Avatar that derives initials from the `name` prop. For a group, compose `useAvatarGroup` with `useAvatarGroupContextValues` and `renderAvatarGroup`, then wrap each item in a composed `AvatarGroupItem`. The group context propagates layout and size to all children, so each Avatar adapts automatically.', + }, + }, +}; diff --git a/packages/react-components/react-headless/stories/src/Avatar/index.stories.ts b/packages/react-components/react-headless/stories/src/Avatar/index.stories.ts new file mode 100644 index 00000000000000..d65d7e74bb5a66 --- /dev/null +++ b/packages/react-components/react-headless/stories/src/Avatar/index.stories.ts @@ -0,0 +1,20 @@ +import type { Meta } from '@storybook/react-webpack5'; +import { AvatarProps } from '@fluentui/react-headless'; + +export { Example } from './ExampleAvatar.stories'; + +const meta: Meta = { + title: 'Base Components/Avatar', + parameters: { + docs: { + hideArgsTable: true, + skipPrimaryStory: true, + description: { + component: + 'Base Avatar components (`Avatar`, `AvatarGroup`, `AvatarGroupItem`) for building custom user identity display. Compose `useAvatar` with `renderAvatar` for a standalone avatar that auto-generates initials from the `name` prop. For groups, compose `useAvatarGroup` with `useAvatarGroupContextValues` and `renderAvatarGroup` to propagate layout and size to children via context.', + }, + }, + }, +}; + +export default meta; diff --git a/packages/react-components/react-headless/stories/src/Badge/ExampleBadge.stories.css b/packages/react-components/react-headless/stories/src/Badge/ExampleBadge.stories.css new file mode 100644 index 00000000000000..efa425b6ce98f3 --- /dev/null +++ b/packages/react-components/react-headless/stories/src/Badge/ExampleBadge.stories.css @@ -0,0 +1,42 @@ +.badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 20px; + height: 20px; + padding: 0 6px; + border-radius: 10px; + font-size: 12px; + font-weight: 600; +} + +.badge--success { + background-color: #107c10; + color: #ffffff; +} + +.badge--warning { + background-color: #797673; + color: #ffffff; +} + +.badge--error { + background-color: #a4262c; + color: #ffffff; +} + +.badge--info { + background-color: #0078d4; + color: #ffffff; +} + +.badge--counter { + background-color: #a4262c; + color: #ffffff; +} + +.badge-container { + display: flex; + gap: 8px; + align-items: center; +} diff --git a/packages/react-components/react-headless/stories/src/Badge/ExampleBadge.stories.tsx b/packages/react-components/react-headless/stories/src/Badge/ExampleBadge.stories.tsx new file mode 100644 index 00000000000000..0e5c0c63742ee2 --- /dev/null +++ b/packages/react-components/react-headless/stories/src/Badge/ExampleBadge.stories.tsx @@ -0,0 +1,52 @@ +import * as React from 'react'; + +import { composeComponent, useBadge, renderBadge, useCounterBadge } from '@fluentui/react-headless'; +import type { BadgeProps, BadgeState, CounterBadgeState } from '@fluentui/react-headless'; + +import './ExampleBadge.stories.css'; + +type BadgeColor = 'success' | 'warning' | 'error' | 'info'; + +type CustomBadgeProps = BadgeProps & { color?: BadgeColor }; +type CustomBadgeState = BadgeState & { color: BadgeColor }; + +const Badge = composeComponent({ + displayName: 'Badge', + useState(props, ref) { + const { color = 'info', ...badgeProps } = props; + const state = useBadge(badgeProps, ref); + return { ...state, color }; + }, + useStyles(state: CustomBadgeState) { + state.root.className = `badge badge--${state.color}`; + }, + render: renderBadge, +}); + +const CounterBadge = composeComponent({ + displayName: 'CounterBadge', + useState: useCounterBadge, + useStyles(state: CounterBadgeState) { + state.root.className = 'badge badge--counter'; + }, + render: renderBadge, +}); + +export const Example = (): React.ReactNode => ( +
+ Done + Review + Blocked + 12 + +
+); + +Example.parameters = { + docs: { + description: { + story: + 'Extend the base badge with a custom `color` prop and apply color-mapped styles in `useStyles`. This same pattern applies to `PresenceBadge` and `CounterBadge` — all three share the `renderBadge` render function and accept custom styling through `useStyles`. Mix and match the three variants in your design system to handle notification counts, status indicators, and presence badges.', + }, + }, +}; diff --git a/packages/react-components/react-headless/stories/src/Badge/ExamplePresenceBadge.stories.css b/packages/react-components/react-headless/stories/src/Badge/ExamplePresenceBadge.stories.css new file mode 100644 index 00000000000000..0af943bd40fc59 --- /dev/null +++ b/packages/react-components/react-headless/stories/src/Badge/ExamplePresenceBadge.stories.css @@ -0,0 +1,49 @@ +.presence-badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 50%; + font-size: 12px; + font-weight: 600; + color: #ffffff; +} + +.presence-badge--available { + background-color: #107c10; +} + +.presence-badge--busy { + background-color: #a4262c; +} + +.presence-badge--away { + background-color: #ff8c00; +} + +.presence-badge--offline { + background-color: #605e5c; +} + +.presence-badge--unknown { + background-color: #605e5c; +} + +.presence-badge--out-of-office { + background-color: #605e5c; +} + +.presence-badge--do-not-disturb { + background-color: #a4262c; +} + +.presence-badge--blocked { + background-color: #605e5c; +} + +.presence-badge-container { + display: flex; + gap: 8px; + align-items: center; +} diff --git a/packages/react-components/react-headless/stories/src/Badge/ExamplePresenceBadge.stories.tsx b/packages/react-components/react-headless/stories/src/Badge/ExamplePresenceBadge.stories.tsx new file mode 100644 index 00000000000000..5736d15d178bab --- /dev/null +++ b/packages/react-components/react-headless/stories/src/Badge/ExamplePresenceBadge.stories.tsx @@ -0,0 +1,58 @@ +import * as React from 'react'; + +import { composeComponent, usePresenceBadge, renderBadge } from '@fluentui/react-headless'; +import type { PresenceBadgeState } from '@fluentui/react-headless'; +import { + PresenceAwayFilled, + PresenceBusyFilled, + PresenceAvailableFilled, + PresenceOfflineRegular, +} from '@fluentui/react-icons'; + +import './ExamplePresenceBadge.stories.css'; + +const getPresenceIcon = (status: PresenceBadgeState['status']): React.ReactNode => { + switch (status) { + case 'available': + return ; + case 'busy': + return ; + case 'away': + return ; + case 'offline': + return ; + default: + return null; + } +}; + +const PresenceBadge = composeComponent({ + displayName: 'PresenceBadge', + useState: usePresenceBadge, + useStyles(state: PresenceBadgeState) { + state.root.className = `presence-badge presence-badge--${state.status}`; + + if (state.icon) { + state.icon.children ??= getPresenceIcon(state.status); + } + }, + render: renderBadge, +}); + +export const ExamplePresenceBadge = (): React.ReactNode => ( +
+ + + + +
+); + +ExamplePresenceBadge.parameters = { + docs: { + description: { + story: + 'Compose `usePresenceBadge` with `renderBadge` via `composeComponent` to build a presence indicator badge. The hook computes the correct ARIA role and status props — apply styles in `useStyles`.', + }, + }, +}; diff --git a/packages/react-components/react-headless/stories/src/Badge/index.stories.ts b/packages/react-components/react-headless/stories/src/Badge/index.stories.ts new file mode 100644 index 00000000000000..fbe844d05b37b3 --- /dev/null +++ b/packages/react-components/react-headless/stories/src/Badge/index.stories.ts @@ -0,0 +1,21 @@ +import type { Meta } from '@storybook/react-webpack5'; +import { BadgeProps } from '@fluentui/react-headless'; + +export { Example } from './ExampleBadge.stories'; +export { ExamplePresenceBadge } from './ExamplePresenceBadge.stories'; + +const meta: Meta = { + title: 'Base Components/Badge', + parameters: { + docs: { + hideArgsTable: true, + skipPrimaryStory: true, + description: { + component: + 'Base Badge components (`Badge`, `PresenceBadge`, `CounterBadge`) for building custom badge implementations. Compose `useBadge`, `usePresenceBadge`, or `useCounterBadge` with `renderBadge` via `composeComponent`. The base hooks compute the correct ARIA roles, variant props, and slot structure so you can focus purely on styling.', + }, + }, + }, +}; + +export default meta; diff --git a/packages/react-components/react-headless/stories/src/Breadcrumb/ExampleBreadcrumb.stories.css b/packages/react-components/react-headless/stories/src/Breadcrumb/ExampleBreadcrumb.stories.css new file mode 100644 index 00000000000000..933d9c7f6d97b7 --- /dev/null +++ b/packages/react-components/react-headless/stories/src/Breadcrumb/ExampleBreadcrumb.stories.css @@ -0,0 +1,48 @@ +.breadcrumb { + display: flex; + align-items: center; + list-style: none; + padding: 0; + margin: 0; +} + +.breadcrumb-list { + display: flex; + align-items: center; + gap: 4px; + list-style: none; + padding: 0; + margin: 0; +} + +.breadcrumb-item { + display: flex; + align-items: center; +} + +.breadcrumb-divider { + color: #8a8886; + font-size: 12px; + user-select: none; +} + +.breadcrumb-button { + padding: 2px 4px; + border-radius: 4px; + font-size: 13px; + text-decoration: none; + background: none; + border: none; +} + +.breadcrumb-button--current { + font-weight: 600; + color: #323130; + cursor: default; +} + +.breadcrumb-button--default { + font-weight: 400; + color: #0078d4; + cursor: pointer; +} diff --git a/packages/react-components/react-headless/stories/src/Breadcrumb/ExampleBreadcrumb.stories.tsx b/packages/react-components/react-headless/stories/src/Breadcrumb/ExampleBreadcrumb.stories.tsx new file mode 100644 index 00000000000000..149210b68f60cc --- /dev/null +++ b/packages/react-components/react-headless/stories/src/Breadcrumb/ExampleBreadcrumb.stories.tsx @@ -0,0 +1,89 @@ +import * as React from 'react'; + +import { + composeComponent, + useBreadcrumb, + renderBreadcrumb, + useBreadcrumbItem, + renderBreadcrumbItem, + useBreadcrumbDivider, + renderBreadcrumbDivider, + useBreadcrumbButton, + renderBreadcrumbButton, + useBreadcrumbContextValues, +} from '@fluentui/react-headless'; +import type { + BreadcrumbState, + BreadcrumbItemState, + BreadcrumbDividerState, + BreadcrumbButtonState, +} from '@fluentui/react-headless'; + +import './ExampleBreadcrumb.stories.css'; + +const Breadcrumb = composeComponent({ + displayName: 'Breadcrumb', + useState: useBreadcrumb, + useStyles(state: BreadcrumbState) { + state.root.className = 'breadcrumb'; + if (state.list) { + state.list.className = 'breadcrumb-list'; + } + }, + useContextValues: useBreadcrumbContextValues, + render: renderBreadcrumb, +}); + +const BreadcrumbItem = composeComponent({ + displayName: 'BreadcrumbItem', + useState: useBreadcrumbItem, + useStyles(state: BreadcrumbItemState) { + state.root.className = 'breadcrumb-item'; + }, + render: renderBreadcrumbItem, +}); + +const BreadcrumbDivider = composeComponent({ + displayName: 'BreadcrumbDivider', + useState: useBreadcrumbDivider, + useStyles(state: BreadcrumbDividerState) { + state.root.className = 'breadcrumb-divider'; + }, + render: renderBreadcrumbDivider, +}); + +const BreadcrumbButton = composeComponent({ + displayName: 'BreadcrumbButton', + useState: useBreadcrumbButton, + useStyles(state: BreadcrumbButtonState) { + state.root.className = `breadcrumb-button ${ + state.current ? 'breadcrumb-button--current' : 'breadcrumb-button--default' + }`; + }, + render: renderBreadcrumbButton, +}); + +export const Example = (): React.ReactNode => ( + + + Home + + / + + Documents + + / + + Report.pdf + + +); + +Example.parameters = { + docs: { + description: { + story: + 'Style each breadcrumb sub-component individually via `useStyles`, reading from state to apply conditional styles (e.g. bold + non-clickable for the current item). The `state.current` flag on `BreadcrumbButton` drives the visual distinction between navigable links and the active page. Use the same approach with CSS modules or a CSS-in-JS library by setting `state.root.className` instead of `state.root.style`.', + }, + }, +}; diff --git a/packages/react-components/react-headless/stories/src/Breadcrumb/index.stories.ts b/packages/react-components/react-headless/stories/src/Breadcrumb/index.stories.ts new file mode 100644 index 00000000000000..eb4f0d25d5467d --- /dev/null +++ b/packages/react-components/react-headless/stories/src/Breadcrumb/index.stories.ts @@ -0,0 +1,20 @@ +import type { Meta } from '@storybook/react-webpack5'; +import { BreadcrumbProps } from '@fluentui/react-headless'; + +export { Example } from './ExampleBreadcrumb.stories'; + +const meta: Meta = { + title: 'Base Components/Breadcrumb', + parameters: { + docs: { + hideArgsTable: true, + skipPrimaryStory: true, + description: { + component: + 'Base Breadcrumb components (`Breadcrumb`, `BreadcrumbItem`, `BreadcrumbDivider`, `BreadcrumbButton`) for building custom navigation trails. Compose each sub-component individually using `useBreadcrumb*` hooks with their render functions. The base hooks produce a semantic `