diff --git a/package.json b/package.json index 66909233e2a5f..bf69faa4d3e76 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "@microsoft/api-extractor": "7.51.0", "@microsoft/api-extractor-model": "7.31.2", "@microsoft/eslint-plugin-sdl": "1.0.1", + "@microsoft/focusgroup-polyfill": "^1.2.1", "@microsoft/load-themed-styles": "1.10.26", "@microsoft/loader-load-themed-styles": "2.0.17", "@microsoft/tsdoc": "0.15.1", diff --git a/packages/react-components/react-headless-components-preview/library/etc/react-headless-components-preview.api.md b/packages/react-components/react-headless-components-preview/library/etc/react-headless-components-preview.api.md index b20d9b2f1f00a..41da91dfe8743 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/react-headless-components-preview.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/react-headless-components-preview.api.md @@ -20,16 +20,107 @@ import type { AccordionPanelBaseProps } from '@fluentui/react-accordion'; import type { AccordionPanelBaseState } from '@fluentui/react-accordion'; import type { AccordionPanelSlots as AccordionPanelSlots_2 } from '@fluentui/react-accordion'; import type { AccordionSlots as AccordionSlots_2 } from '@fluentui/react-accordion'; +import type { AvatarBaseProps } from '@fluentui/react-avatar'; +import { AvatarBaseState } from '@fluentui/react-avatar'; +import type { BadgeBaseProps } from '@fluentui/react-badge'; +import { BadgeBaseState } from '@fluentui/react-badge'; +import type { BreadcrumbBaseProps } from '@fluentui/react-breadcrumb'; +import { BreadcrumbBaseState } from '@fluentui/react-breadcrumb'; +import type { BreadcrumbButtonBaseProps } from '@fluentui/react-breadcrumb'; +import type { BreadcrumbButtonBaseState } from '@fluentui/react-breadcrumb'; +import type { BreadcrumbButtonSlots as BreadcrumbButtonSlots_2 } from '@fluentui/react-breadcrumb'; +import { BreadcrumbContextValues as BreadcrumbContextValues_2 } from '@fluentui/react-breadcrumb'; +import type { BreadcrumbDividerBaseProps } from '@fluentui/react-breadcrumb'; +import { BreadcrumbDividerBaseState } from '@fluentui/react-breadcrumb'; +import type { BreadcrumbDividerSlots as BreadcrumbDividerSlots_2 } from '@fluentui/react-breadcrumb'; +import type { BreadcrumbItemBaseProps } from '@fluentui/react-breadcrumb'; +import { BreadcrumbItemBaseState } from '@fluentui/react-breadcrumb'; +import type { BreadcrumbItemSlots as BreadcrumbItemSlots_2 } from '@fluentui/react-breadcrumb'; +import type { BreadcrumbSlots as BreadcrumbSlots_2 } from '@fluentui/react-breadcrumb'; import type { ButtonBaseProps } from '@fluentui/react-button'; import { ButtonBaseState } from '@fluentui/react-button'; import type { ButtonSlots as ButtonSlots_2 } from '@fluentui/react-button'; +import type { CheckboxBaseProps } from '@fluentui/react-checkbox'; +import { CheckboxBaseState } from '@fluentui/react-checkbox'; +import type { CheckboxSlots as CheckboxSlots_2 } from '@fluentui/react-checkbox'; +import { ComponentProps } from '@fluentui/react-utilities'; import { ContextSelector } from '@fluentui/react-context-selector'; import type { DividerBaseProps } from '@fluentui/react-divider'; import { DividerBaseState } from '@fluentui/react-divider'; import type { DividerSlots as DividerSlots_2 } from '@fluentui/react-divider'; +import type { FieldBaseProps } from '@fluentui/react-field'; +import { FieldBaseState } from '@fluentui/react-field'; +import { FieldContextValues } from '@fluentui/react-field'; +import type { FieldSlots as FieldSlots_2 } from '@fluentui/react-field'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { InputBaseProps } from '@fluentui/react-input'; +import { InputBaseState } from '@fluentui/react-input'; +import type { InputSlots as InputSlots_2 } from '@fluentui/react-input'; import { JSXElement } from '@fluentui/react-utilities'; -import type * as React_2 from 'react'; +import type { LinkBaseProps } from '@fluentui/react-link'; +import { LinkBaseState } from '@fluentui/react-link'; +import type { LinkSlots as LinkSlots_2 } from '@fluentui/react-link'; +import type { ProgressBarBaseProps } from '@fluentui/react-progress'; +import type { ProgressBarBaseState } from '@fluentui/react-progress'; +import type { ProgressBarSlots as ProgressBarSlots_2 } from '@fluentui/react-progress'; +import type { RadioBaseProps } from '@fluentui/react-radio'; +import { RadioBaseState } from '@fluentui/react-radio'; +import type { RadioGroupBaseProps } from '@fluentui/react-radio'; +import { RadioGroupBaseState } from '@fluentui/react-radio'; +import { RadioGroupContextValues } from '@fluentui/react-radio'; +import type { RadioGroupSlots as RadioGroupSlots_2 } from '@fluentui/react-radio'; +import { RadioOnChangeData } from '@fluentui/react-radio'; +import { RadioSlots as RadioSlots_2 } from '@fluentui/react-radio'; +import type { RatingBaseProps } from '@fluentui/react-rating'; +import { RatingBaseState } from '@fluentui/react-rating'; +import { RatingContextValues } from '@fluentui/react-rating'; +import type { RatingDisplayBaseProps } from '@fluentui/react-rating'; +import { RatingDisplayBaseState } from '@fluentui/react-rating'; +import { RatingDisplayContextValues } from '@fluentui/react-rating'; +import type { RatingDisplaySlots as RatingDisplaySlots_2 } from '@fluentui/react-rating'; +import type { RatingItemBaseProps } from '@fluentui/react-rating'; +import { RatingItemBaseState } from '@fluentui/react-rating'; +import { RatingItemSlots as RatingItemSlots_2 } from '@fluentui/react-rating'; +import type { RatingSlots as RatingSlots_2 } from '@fluentui/react-rating'; +import * as React_2 from 'react'; +import type { SearchBoxBaseProps } from '@fluentui/react-search'; +import { SearchBoxBaseState } from '@fluentui/react-search'; +import type { SearchBoxSlots as SearchBoxSlots_2 } from '@fluentui/react-search'; +import type { SelectBaseProps } from '@fluentui/react-select'; +import { SelectBaseState } from '@fluentui/react-select'; +import type { SelectSlots as SelectSlots_2 } from '@fluentui/react-select'; +import { SkeletonBaseProps } from '@fluentui/react-skeleton'; +import { SkeletonBaseState } from '@fluentui/react-skeleton'; +import { SkeletonContextValues } from '@fluentui/react-skeleton'; +import { SkeletonItemBaseProps } from '@fluentui/react-skeleton'; +import { SkeletonItemBaseState } from '@fluentui/react-skeleton'; +import type { SkeletonItemSlots as SkeletonItemSlots_2 } from '@fluentui/react-skeleton'; +import type { SkeletonSlots as SkeletonSlots_2 } from '@fluentui/react-skeleton'; +import type { SliderBaseProps } from '@fluentui/react-slider'; +import { SliderBaseState } from '@fluentui/react-slider'; +import type { SliderSlots as SliderSlots_2 } from '@fluentui/react-slider'; +import type { SpinButtonBaseProps } from '@fluentui/react-spinbutton'; +import { SpinButtonBaseState } from '@fluentui/react-spinbutton'; +import type { SpinButtonSlots as SpinButtonSlots_2 } from '@fluentui/react-spinbutton'; +import type { SpinnerBaseProps } from '@fluentui/react-spinner'; +import { SpinnerBaseState } from '@fluentui/react-spinner'; +import type { SpinnerSlots as SpinnerSlots_2 } from '@fluentui/react-spinner'; +import type { SwitchBaseProps } from '@fluentui/react-switch'; +import { SwitchBaseState } from '@fluentui/react-switch'; +import type { SwitchSlots as SwitchSlots_2 } from '@fluentui/react-switch'; +import type { TabBaseProps } from '@fluentui/react-tabs'; +import { TabBaseState } from '@fluentui/react-tabs'; +import type { TabListBaseProps } from '@fluentui/react-tabs'; +import { TabListBaseState } from '@fluentui/react-tabs'; +import { TabListContextValues } from '@fluentui/react-tabs'; +import type { TabListSlots as TabListSlots_2 } from '@fluentui/react-tabs'; +import { TabSlots } from '@fluentui/react-tabs'; +import { TabValue } from '@fluentui/react-tabs'; +import type { TextareaBaseProps } from '@fluentui/react-textarea'; +import { TextareaBaseState } from '@fluentui/react-textarea'; +import type { TextareaSlots as TextareaSlots_2 } from '@fluentui/react-textarea'; +import type { ToggleButtonBaseProps } from '@fluentui/react-button'; +import type { ToggleButtonBaseState } from '@fluentui/react-button'; // @public export const Accordion: ForwardRefComponent; @@ -47,7 +138,13 @@ export type AccordionHeaderProps = AccordionHeaderBaseProps; export type AccordionHeaderSlots = AccordionHeaderSlots_2; // @public (undocumented) -export type AccordionHeaderState = AccordionHeaderBaseState; +export type AccordionHeaderState = AccordionHeaderBaseState & { + root: { + 'data-open'?: string; + 'data-disabled'?: string; + 'data-expand-icon-position'?: string; + }; +}; // @public export const AccordionItem: ForwardRefComponent; @@ -59,7 +156,12 @@ export type AccordionItemProps = AccordionItemProps_2; export type AccordionItemSlots = AccordionItemSlots_2; // @public (undocumented) -export type AccordionItemState = AccordionItemState_2; +export type AccordionItemState = AccordionItemState_2 & { + root: { + 'data-disabled'?: string; + 'data-open'?: string; + }; +}; // @public export const AccordionPanel: ForwardRefComponent; @@ -71,7 +173,11 @@ export type AccordionPanelProps = AccordionPanelBaseProps; export type AccordionPanelSlots = AccordionPanelSlots_2; // @public (undocumented) -export type AccordionPanelState = AccordionPanelBaseState; +export type AccordionPanelState = AccordionPanelBaseState & { + root: { + 'data-open'?: string; + }; +}; // @public (undocumented) export type AccordionProps = AccordionBaseProps; @@ -80,7 +186,89 @@ export type AccordionProps = AccordionBaseProps; export type AccordionSlots = AccordionSlots_2; // @public (undocumented) -export type AccordionState = AccordionBaseState; +export type AccordionState = AccordionBaseState & { + root: { + 'data-collapsible'?: string; + 'data-multiple'?: string; + }; +}; + +// @public +export const Avatar: ForwardRefComponent; + +// @public +export type AvatarProps = AvatarBaseProps; + +// @public +export type AvatarState = AvatarBaseState; + +// @public +export const Badge: ForwardRefComponent; + +// @public +export type BadgeProps = BadgeBaseProps; + +// @public +export type BadgeState = BadgeBaseState & { + root: { + 'data-icon-position'?: 'before' | 'after'; + }; +}; + +// @public +export const Breadcrumb: ForwardRefComponent; + +// @public +export const BreadcrumbButton: ForwardRefComponent; + +// @public +export type BreadcrumbButtonProps = BreadcrumbButtonBaseProps; + +// @public +export type BreadcrumbButtonSlots = BreadcrumbButtonSlots_2; + +// @public +export type BreadcrumbButtonState = BreadcrumbButtonBaseState & { + root: { + 'data-current'?: string; + }; +}; + +// @public +export type BreadcrumbContextValues = BreadcrumbContextValues_2; + +// @public +export const BreadcrumbDivider: ForwardRefComponent; + +// @public +export type BreadcrumbDividerProps = BreadcrumbDividerBaseProps; + +// @public +export type BreadcrumbDividerSlots = BreadcrumbDividerSlots_2; + +// @public +export type BreadcrumbDividerState = BreadcrumbDividerBaseState; + +// @public +export const BreadcrumbItem: ForwardRefComponent; + +// @public +export type BreadcrumbItemProps = BreadcrumbItemBaseProps; + +// @public +export type BreadcrumbItemSlots = BreadcrumbItemSlots_2; + +// @public +export type BreadcrumbItemState = BreadcrumbItemBaseState; + +// @public +export type BreadcrumbProps = BreadcrumbBaseProps; + +// @public +export type BreadcrumbSlots = BreadcrumbSlots_2; + +// @public +export type BreadcrumbState = BreadcrumbBaseState; // @public export const Button: ForwardRefComponent; @@ -92,7 +280,30 @@ export type ButtonProps = ButtonBaseProps; export type ButtonSlots = ButtonSlots_2; // @public -export type ButtonState = ButtonBaseState; +export type ButtonState = ButtonBaseState & { + root: { + 'data-disabled'?: string; + 'data-disabled-focusable'?: string; + 'data-icon-only'?: string; + }; +}; + +// @public +export const Checkbox: ForwardRefComponent; + +// @public +export type CheckboxProps = CheckboxBaseProps; + +// @public (undocumented) +export type CheckboxSlots = CheckboxSlots_2; + +// @public +export type CheckboxState = CheckboxBaseState & { + root: { + 'data-disabled'?: string; + 'data-checked'?: string; + }; +}; // @public export const Divider: ForwardRefComponent; @@ -104,7 +315,151 @@ export type DividerProps = DividerBaseProps; export type DividerSlots = DividerSlots_2; // @public (undocumented) -export type DividerState = DividerBaseState; +export type DividerState = DividerBaseState & { + root: { + 'data-orientation'?: 'vertical' | 'horizontal'; + }; +}; + +// @public +export const Field: ForwardRefComponent; + +// @public +export type FieldProps = FieldBaseProps; + +// @public +export type FieldSlots = FieldSlots_2; + +// @public +export type FieldState = FieldBaseState & { + root: { + 'data-validate-state'?: FieldBaseState['validationState']; + }; +}; + +// @public +export const Input: ForwardRefComponent; + +// @public +export type InputProps = InputBaseProps; + +// @public +export type InputSlots = InputSlots_2; + +// @public +export type InputState = InputBaseState & { + root: { + 'data-disabled'?: string; + }; +}; + +// @public +export const Link: ForwardRefComponent; + +// @public +export type LinkProps = LinkBaseProps; + +// @public +export type LinkSlots = LinkSlots_2; + +// @public +export type LinkState = LinkBaseState & { + root: { + 'data-disabled'?: string; + 'data-disabled-focusable'?: string; + }; +}; + +// @public +export const ProgressBar: ForwardRefComponent; + +// @public +export type ProgressBarProps = ProgressBarBaseProps; + +// @public (undocumented) +export type ProgressBarSlots = ProgressBarSlots_2; + +// @public +export type ProgressBarState = ProgressBarBaseState; + +// @public +export const Radio: React_2.ForwardRefExoticComponent, "input">, "onChange" | "size"> & { + value?: string; + labelPosition?: "after" | "below"; + disabled?: boolean; + onChange?: (ev: React_2.ChangeEvent, data: RadioOnChangeData) => void; +} & React_2.RefAttributes>; + +// @public +export const RadioGroup: ForwardRefComponent; + +// @public +export type RadioGroupProps = RadioGroupBaseProps; + +// @public +export type RadioGroupSlots = RadioGroupSlots_2; + +// @public +export type RadioGroupState = RadioGroupBaseState; + +// @public +export type RadioProps = RadioBaseProps; + +// @public +export type RadioSlots = RadioSlots_2; + +// @public +export type RadioState = RadioBaseState & { + root: { + 'data-disabled'?: string; + }; +}; + +// @public +export const Rating: ForwardRefComponent; + +// @public +export const RatingDisplay: ForwardRefComponent; + +// @public +export type RatingDisplayProps = RatingDisplayBaseProps; + +// @public +export type RatingDisplaySlots = RatingDisplaySlots_2; + +// @public +export type RatingDisplayState = RatingDisplayBaseState; + +// @public +export const RatingItem: React_2.ForwardRefExoticComponent, "root"> & Omit<{ + as?: "span" | undefined; +} & Omit, HTMLSpanElement>, "children"> & { + children?: any; +}, "ref"> & { + value?: number; +} & React_2.RefAttributes>; + +// @public +export type RatingItemProps = RatingItemBaseProps; + +// @public +export type RatingItemSlots = RatingItemSlots_2; + +// @public +export type RatingItemState = RatingItemBaseState & { + root: { + 'data-appearance'?: 'filled' | 'filled-half' | 'outline'; + }; +}; + +// @public +export type RatingProps = RatingBaseProps; + +// @public +export type RatingSlots = RatingSlots_2; + +// @public +export type RatingState = RatingBaseState; // @public export const renderAccordion: (state: AccordionBaseState, contextValues: AccordionContextValues_2) => JSXElement; @@ -118,12 +473,289 @@ export const renderAccordionItem: (state: AccordionItemState_2, contextValues: A // @public export const renderAccordionPanel: (state: AccordionPanelState) => JSXElement; +// @public +export const renderAvatar: (state: AvatarBaseState) => JSXElement; + +// @public +export const renderBadge: (state: BadgeBaseState) => JSXElement; + +// @public +export const renderBreadcrumb: (state: BreadcrumbBaseState, contextValues: BreadcrumbContextValues_2) => JSXElement; + +// @public +export const renderBreadcrumbButton: (state: BreadcrumbButtonState) => JSXElement; + +// @public +export const renderBreadcrumbDivider: (state: BreadcrumbDividerBaseState) => JSXElement; + +// @public +export const renderBreadcrumbItem: (state: BreadcrumbItemBaseState) => JSXElement; + // @public export const renderButton: (state: ButtonBaseState) => JSXElement; +// @public +export const renderCheckbox: (state: CheckboxBaseState) => JSXElement; + // @public export const renderDivider: (state: DividerBaseState) => JSXElement; +// @public +export const renderField: (state: FieldBaseState, contextValues: FieldContextValues) => JSXElement; + +// @public +export const renderInput: (state: InputBaseState) => JSXElement; + +// @public +export const renderLink: (state: LinkBaseState) => JSXElement; + +// @public +export const renderProgressBar: (state: ProgressBarState) => JSXElement; + +// @public +export const renderRadio: (state: RadioBaseState) => JSXElement; + +// @public +export const renderRadioGroup: (state: RadioGroupBaseState, contextValues: RadioGroupContextValues) => JSXElement; + +// @public +export const renderRating: (state: RatingBaseState, contextValues: RatingContextValues) => JSXElement; + +// @public +export const renderRatingDisplay: (state: RatingDisplayBaseState, contextValues: RatingDisplayContextValues) => JSXElement; + +// @public +export const renderRatingItem: (state: RatingItemBaseState) => JSXElement; + +// @public +export const renderSearchBox: (state: SearchBoxBaseState) => JSXElement; + +// @public +export const renderSelect: (state: SelectBaseState) => JSXElement; + +// @public +export const renderSkeleton: (state: SkeletonBaseState, contextValues: SkeletonContextValues) => JSXElement; + +// @public +export const renderSkeletonItem: (state: SkeletonItemBaseState) => JSXElement; + +// @public +export const renderSlider: (state: SliderBaseState) => JSXElement; + +// @public +export const renderSpinButton: (state: SpinButtonBaseState) => JSXElement; + +// @public +export const renderSpinner: (state: SpinnerBaseState) => JSXElement; + +// @public +export const renderSwitch: (state: SwitchBaseState) => JSXElement; + +// @public +export const renderTab: (state: TabBaseState) => JSXElement; + +// @public +export const renderTabList: (state: TabListBaseState, contextValues: TabListContextValues) => JSXElement; + +// @public +export const renderTextarea: (state: TextareaBaseState) => JSXElement; + +// @public +export const renderToggleButton: (state: ButtonBaseState) => JSXElement; + +// @public +export const SearchBox: ForwardRefComponent; + +// @public +export type SearchBoxProps = SearchBoxBaseProps; + +// @public +export type SearchBoxSlots = SearchBoxSlots_2; + +// @public +export type SearchBoxState = SearchBoxBaseState & { + root: { + 'data-disabled'?: string; + 'data-focused'?: string; + }; +}; + +// @public +export const Select: ForwardRefComponent; + +// @public +export type SelectProps = SelectBaseProps; + +// @public (undocumented) +export type SelectSlots = SelectSlots_2; + +// @public +export type SelectState = SelectBaseState & { + root: { + 'data-disabled'?: string; + }; +}; + +// @public +export const Skeleton: React_2.ForwardRefExoticComponent>; + +// @public +export const SkeletonItem: React_2.ForwardRefExoticComponent>; + +// @public +export type SkeletonItemProps = SkeletonItemBaseProps; + +// @public +export type SkeletonItemSlots = SkeletonItemSlots_2; + +// @public +export type SkeletonItemState = SkeletonItemBaseState; + +// @public +export type SkeletonProps = SkeletonBaseProps; + +// @public +export type SkeletonSlots = SkeletonSlots_2; + +// @public +export type SkeletonState = SkeletonBaseState; + +// @public +export const Slider: ForwardRefComponent; + +// @public +export type SliderProps = SliderBaseProps; + +// @public +export type SliderSlots = SliderSlots_2; + +// @public +export type SliderState = SliderBaseState & { + root: { + 'data-disabled'?: string; + 'data-vertical'?: string; + }; +}; + +// @public +export const SpinButton: ForwardRefComponent; + +// @public +export type SpinButtonProps = SpinButtonBaseProps; + +// @public +export type SpinButtonSlots = SpinButtonSlots_2; + +// @public +export type SpinButtonState = SpinButtonBaseState & { + root: { + 'data-disabled'?: string; + 'data-spin-state'?: string; + 'data-at-bound'?: string; + }; +}; + +// @public +export const Spinner: ForwardRefComponent; + +// @public +export type SpinnerProps = SpinnerBaseProps; + +// @public +export type SpinnerSlots = SpinnerSlots_2; + +// @public +export type SpinnerState = SpinnerBaseState & { + root: { + 'data-label-position'?: 'before' | 'after' | 'above' | 'below'; + }; +}; + +// @public +export const Switch: ForwardRefComponent; + +// @public +export type SwitchProps = SwitchBaseProps; + +// @public +export type SwitchSlots = SwitchSlots_2; + +// @public +export type SwitchState = SwitchBaseState & { + root: { + 'data-disabled'?: string; + 'data-disabled-focusable'?: string; + 'data-checked'?: string; + }; +}; + +// @public +export const Tab: ForwardRefComponent; + +// @public +export const TabList: ForwardRefComponent; + +// @public +export type TabListProps = TabListBaseProps; + +// @public +export type TabListSlots = TabListSlots_2; + +// @public +export type TabListState = TabListBaseState & { + root: { + focusgroup?: string; + 'data-orientation'?: 'vertical' | 'horizontal'; + }; +}; + +// @public (undocumented) +export type TabProps = TabBaseProps; + +export { TabSlots } + +// @public (undocumented) +export type TabState = TabBaseState & { + root: { + focusgroupstart?: string; + 'data-icon-only'?: string; + 'data-selected'?: string; + }; +}; + +export { TabValue } + +// @public +export const Textarea: ForwardRefComponent; + +// @public +export type TextareaProps = TextareaBaseProps; + +// @public +export type TextareaSlots = TextareaSlots_2; + +// @public +export type TextareaState = TextareaBaseState; + +// @public +export const ToggleButton: ForwardRefComponent; + +// @public +export type ToggleButtonProps = ToggleButtonBaseProps; + +// @public +export type ToggleButtonSlots = ButtonSlots; + +// @public +export type ToggleButtonState = ToggleButtonBaseState & { + root: { + 'data-disabled'?: string; + 'data-disabled-focusable'?: string; + 'data-icon-only'?: string; + 'data-checked'?: string; + }; +}; + // @public export const useAccordion: (props: AccordionProps, ref: React_2.Ref) => AccordionState; @@ -142,12 +774,102 @@ export const useAccordionItem: (props: AccordionItemProps, ref: React_2.Ref) => AccordionPanelState; +// @public +export const useAvatar: (props: AvatarProps, ref: React_2.Ref) => AvatarState; + +// @public +export const useBadge: (props: BadgeProps, ref: React_2.Ref) => BadgeState; + +// @public +export const useBreadcrumb: (props: BreadcrumbProps, ref: React_2.Ref) => BreadcrumbState; + +// @public +export const useBreadcrumbButton: (props: BreadcrumbButtonProps, ref: React_2.Ref) => BreadcrumbButtonState; + +// @public +export const useBreadcrumbContext: () => BreadcrumbContextValues_2; + +// @public +export const useBreadcrumbContextValues: (state: BreadcrumbState) => BreadcrumbContextValues; + +// @public +export const useBreadcrumbDivider: (props: BreadcrumbDividerProps, ref: React_2.Ref) => BreadcrumbDividerState; + +// @public +export const useBreadcrumbItem: (props: BreadcrumbItemProps, ref: React_2.Ref) => BreadcrumbItemState; + // @public export const useButton: (props: ButtonProps, ref: React_2.Ref) => ButtonState; +// @public +export const useCheckbox: (props: CheckboxProps, ref: React_2.Ref) => CheckboxState; + // @public export const useDivider: (props: DividerProps, ref: React_2.Ref) => DividerState; +// @public +export const useField: (props: FieldProps, ref: React_2.Ref) => FieldState; + +// @public +export const useInput: (props: InputProps, ref: React_2.Ref) => InputState; + +// @public +export const useLink: (props: LinkProps, ref: React_2.Ref) => LinkState; + +// @public +export const useProgressBar: (props: ProgressBarProps, ref: React_2.Ref) => ProgressBarState; + +// @public +export const useRadio: (props: RadioProps, ref: React_2.Ref) => RadioState; + +// @public +export const useRadioGroup: (props: RadioGroupProps, ref: React_2.Ref) => RadioGroupState; + +// @public +export const useRating: (props: RatingProps, ref: React_2.Ref) => RatingState; + +// @public +export const useRatingDisplay: (props: RatingDisplayProps, ref: React_2.Ref) => RatingDisplayState; + +// @public +export const useRatingItem: (props: RatingItemProps, ref: React_2.Ref) => RatingItemState; + +// @public +export const useSearchBox: (props: SearchBoxProps, ref: React_2.Ref) => SearchBoxState; + +// @public +export const useSelect: (props: SelectProps, ref: React_2.Ref) => SelectState; + +// @public +export const useSkeleton: (props: SkeletonProps, ref: React_2.Ref) => SkeletonState; + +// @public +export const useSkeletonItem: (props: SkeletonItemProps, ref: React_2.Ref) => SkeletonItemState; + +// @public +export const useSlider: (props: SliderProps, ref: React_2.Ref) => SliderState; + +// @public +export const useSpinButton: (props: SpinButtonProps, ref: React_2.Ref) => SpinButtonState; + +// @public +export const useSpinner: (props: SpinnerProps, ref: React_2.Ref) => SpinnerState; + +// @public +export const useSwitch: (props: SwitchProps, ref: React_2.Ref) => SwitchState; + +// @public +export const useTab: (props: TabProps, ref: React_2.Ref) => TabState; + +// @public +export const useTabList: (props: TabListProps, ref: React_2.Ref) => TabListState; + +// @public +export const useTextarea: (props: TextareaProps, ref: React_2.Ref) => TextareaState; + +// @public +export const useToggleButton: (props: ToggleButtonProps, ref: React_2.Ref) => ToggleButtonState; + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/react-components/react-headless-components-preview/library/package.json b/packages/react-components/react-headless-components-preview/library/package.json index 5de707fdcb733..c0c8e93077262 100644 --- a/packages/react-components/react-headless-components-preview/library/package.json +++ b/packages/react-components/react-headless-components-preview/library/package.json @@ -20,10 +20,33 @@ "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-checkbox": "^9.5.17", + "@fluentui/react-dialog": "^9.17.3", "@fluentui/react-divider": "^9.7.0", - "@fluentui/react-jsx-runtime": "^9.4.1", + "@fluentui/react-field": "^9.4.16", + "@fluentui/react-image": "^9.4.0", + "@fluentui/react-input": "^9.8.0", + "@fluentui/react-label": "^9.3.15", + "@fluentui/react-link": "^9.8.0", + "@fluentui/react-persona": "^9.7.1", + "@fluentui/react-progress": "^9.4.17", + "@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-switch": "^9.7.0", + "@fluentui/react-tabs": "^9.11.2", + "@fluentui/react-tags": "^9.7.19", + "@fluentui/react-textarea": "^9.7.0", "@fluentui/react-utilities": "^9.26.2", "@swc/helpers": "^0.5.1" }, diff --git a/packages/react-components/react-headless-components-preview/library/src/Avatar.ts b/packages/react-components/react-headless-components-preview/library/src/Avatar.ts new file mode 100644 index 0000000000000..b18247893808b --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/Avatar.ts @@ -0,0 +1,2 @@ +export { Avatar, renderAvatar, useAvatar } from './components/Avatar/index'; +export type { AvatarSlots, AvatarProps, AvatarState } from './components/Avatar/index'; diff --git a/packages/react-components/react-headless-components-preview/library/src/Badge.ts b/packages/react-components/react-headless-components-preview/library/src/Badge.ts new file mode 100644 index 0000000000000..13eee87fab714 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/Badge.ts @@ -0,0 +1,2 @@ +export { Badge, renderBadge, useBadge } from './components/Badge/index'; +export type { BadgeProps, BadgeState } from './components/Badge/index'; diff --git a/packages/react-components/react-headless-components-preview/library/src/Breadcrumb.ts b/packages/react-components/react-headless-components-preview/library/src/Breadcrumb.ts new file mode 100644 index 0000000000000..85cce42643f7c --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/Breadcrumb.ts @@ -0,0 +1,22 @@ +export { + Breadcrumb, + renderBreadcrumb, + useBreadcrumb, + useBreadcrumbContext, + useBreadcrumbContextValues, +} from './components/Breadcrumb'; +export type { + BreadcrumbSlots, + BreadcrumbProps, + BreadcrumbState, + BreadcrumbContextValues, +} from './components/Breadcrumb'; + +export { BreadcrumbDivider, renderBreadcrumbDivider, useBreadcrumbDivider } from './components/Breadcrumb'; +export type { BreadcrumbDividerSlots, BreadcrumbDividerProps, BreadcrumbDividerState } from './components/Breadcrumb'; + +export { BreadcrumbItem, renderBreadcrumbItem, useBreadcrumbItem } from './components/Breadcrumb'; +export type { BreadcrumbItemSlots, BreadcrumbItemProps, BreadcrumbItemState } from './components/Breadcrumb'; + +export { BreadcrumbButton, renderBreadcrumbButton, useBreadcrumbButton } from './components/Breadcrumb'; +export type { BreadcrumbButtonSlots, BreadcrumbButtonProps, BreadcrumbButtonState } from './components/Breadcrumb'; diff --git a/packages/react-components/react-headless-components-preview/library/src/Checkbox.ts b/packages/react-components/react-headless-components-preview/library/src/Checkbox.ts new file mode 100644 index 0000000000000..a6fe15d94c5fc --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/Checkbox.ts @@ -0,0 +1,2 @@ +export { Checkbox, renderCheckbox, useCheckbox } from './components/Checkbox/index'; +export type { CheckboxSlots, CheckboxProps, CheckboxState } from './components/Checkbox/index'; diff --git a/packages/react-components/react-headless-components-preview/library/src/Field.ts b/packages/react-components/react-headless-components-preview/library/src/Field.ts new file mode 100644 index 0000000000000..86afafa149ff1 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/Field.ts @@ -0,0 +1,2 @@ +export { Field, renderField, useField } from './components/Field/index'; +export type { FieldSlots, FieldProps, FieldState } from './components/Field/index'; diff --git a/packages/react-components/react-headless-components-preview/library/src/Input.ts b/packages/react-components/react-headless-components-preview/library/src/Input.ts new file mode 100644 index 0000000000000..89764f4bc2620 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/Input.ts @@ -0,0 +1,2 @@ +export { Input, renderInput, useInput } from './components/Input/index'; +export type { InputSlots, InputProps, InputState } from './components/Input/index'; diff --git a/packages/react-components/react-headless-components-preview/library/src/Link.ts b/packages/react-components/react-headless-components-preview/library/src/Link.ts new file mode 100644 index 0000000000000..5a1056fe6576d --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/Link.ts @@ -0,0 +1,2 @@ +export { Link, renderLink, useLink } from './components/Link/index'; +export type { LinkSlots, LinkProps, LinkState } from './components/Link/index'; diff --git a/packages/react-components/react-headless-components-preview/library/src/ProgressBar.ts b/packages/react-components/react-headless-components-preview/library/src/ProgressBar.ts new file mode 100644 index 0000000000000..43a14f1ad2f57 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/ProgressBar.ts @@ -0,0 +1,2 @@ +export { ProgressBar, renderProgressBar, useProgressBar } from './components/ProgressBar/index'; +export type { ProgressBarSlots, ProgressBarProps, ProgressBarState } from './components/ProgressBar/index'; diff --git a/packages/react-components/react-headless-components-preview/library/src/RadioGroup.ts b/packages/react-components/react-headless-components-preview/library/src/RadioGroup.ts new file mode 100644 index 0000000000000..6c3ea10ba1e14 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/RadioGroup.ts @@ -0,0 +1,16 @@ +export { + Radio, + renderRadio, + useRadio, + RadioGroup, + renderRadioGroup, + useRadioGroup, +} from './components/RadioGroup/index'; +export type { + RadioSlots, + RadioProps, + RadioState, + RadioGroupSlots, + RadioGroupProps, + RadioGroupState, +} from './components/RadioGroup/index'; diff --git a/packages/react-components/react-headless-components-preview/library/src/Rating.ts b/packages/react-components/react-headless-components-preview/library/src/Rating.ts new file mode 100644 index 0000000000000..658430ad97cc2 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/Rating.ts @@ -0,0 +1,16 @@ +export { + RatingItem, + renderRatingItem, + useRatingItem, + Rating, + renderRating, + useRating, +} from './components/Rating/index'; +export type { + RatingItemSlots, + RatingItemProps, + RatingItemState, + RatingSlots, + RatingProps, + RatingState, +} from './components/Rating/index'; diff --git a/packages/react-components/react-headless-components-preview/library/src/RatingDisplay.ts b/packages/react-components/react-headless-components-preview/library/src/RatingDisplay.ts new file mode 100644 index 0000000000000..292fa79f46558 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/RatingDisplay.ts @@ -0,0 +1,2 @@ +export { RatingDisplay, renderRatingDisplay, useRatingDisplay } from './components/RatingDisplay'; +export type { RatingDisplaySlots, RatingDisplayProps, RatingDisplayState } from './components/RatingDisplay'; diff --git a/packages/react-components/react-headless-components-preview/library/src/SearchBox.ts b/packages/react-components/react-headless-components-preview/library/src/SearchBox.ts new file mode 100644 index 0000000000000..6a8dbcf6c258a --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/SearchBox.ts @@ -0,0 +1,2 @@ +export { SearchBox, renderSearchBox, useSearchBox } from './components/SearchBox/index'; +export type { SearchBoxSlots, SearchBoxProps, SearchBoxState } from './components/SearchBox/index'; diff --git a/packages/react-components/react-headless-components-preview/library/src/Select.ts b/packages/react-components/react-headless-components-preview/library/src/Select.ts new file mode 100644 index 0000000000000..77a248d43add6 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/Select.ts @@ -0,0 +1,2 @@ +export { Select, renderSelect, useSelect } from './components/Select/index'; +export type { SelectSlots, SelectProps, SelectState } from './components/Select/index'; diff --git a/packages/react-components/react-headless-components-preview/library/src/Skeleton.ts b/packages/react-components/react-headless-components-preview/library/src/Skeleton.ts new file mode 100644 index 0000000000000..c29e0a37c8fdd --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/Skeleton.ts @@ -0,0 +1,16 @@ +export { + SkeletonItem, + renderSkeletonItem, + useSkeletonItem, + Skeleton, + renderSkeleton, + useSkeleton, +} from './components/Skeleton/index'; +export type { + SkeletonItemSlots, + SkeletonItemProps, + SkeletonItemState, + SkeletonSlots, + SkeletonProps, + SkeletonState, +} from './components/Skeleton/index'; diff --git a/packages/react-components/react-headless-components-preview/library/src/Slider.ts b/packages/react-components/react-headless-components-preview/library/src/Slider.ts new file mode 100644 index 0000000000000..c82dca29698d3 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/Slider.ts @@ -0,0 +1,2 @@ +export { Slider, renderSlider, useSlider } from './components/Slider/index'; +export type { SliderSlots, SliderProps, SliderState } from './components/Slider/index'; diff --git a/packages/react-components/react-headless-components-preview/library/src/SpinButton.ts b/packages/react-components/react-headless-components-preview/library/src/SpinButton.ts new file mode 100644 index 0000000000000..5166e6c1f4fe4 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/SpinButton.ts @@ -0,0 +1,2 @@ +export { SpinButton, renderSpinButton, useSpinButton } from './components/SpinButton/index'; +export type { SpinButtonSlots, SpinButtonProps, SpinButtonState } from './components/SpinButton/index'; diff --git a/packages/react-components/react-headless-components-preview/library/src/Spinner.ts b/packages/react-components/react-headless-components-preview/library/src/Spinner.ts new file mode 100644 index 0000000000000..eaf7c7c0d9554 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/Spinner.ts @@ -0,0 +1,2 @@ +export { Spinner, renderSpinner, useSpinner } from './components/Spinner/index'; +export type { SpinnerSlots, SpinnerProps, SpinnerState } from './components/Spinner/index'; diff --git a/packages/react-components/react-headless-components-preview/library/src/Switch.ts b/packages/react-components/react-headless-components-preview/library/src/Switch.ts new file mode 100644 index 0000000000000..23349fb31f717 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/Switch.ts @@ -0,0 +1,2 @@ +export { Switch, renderSwitch, useSwitch } from './components/Switch/index'; +export type { SwitchSlots, SwitchProps, SwitchState } from './components/Switch/index'; diff --git a/packages/react-components/react-headless-components-preview/library/src/TabList.ts b/packages/react-components/react-headless-components-preview/library/src/TabList.ts new file mode 100644 index 0000000000000..e8339b88f812b --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/TabList.ts @@ -0,0 +1,10 @@ +export { Tab, renderTab, useTab, TabList, renderTabList, useTabList } from './components/TabList/index'; +export type { + TabSlots, + TabValue, + TabProps, + TabState, + TabListSlots, + TabListProps, + TabListState, +} from './components/TabList/index'; diff --git a/packages/react-components/react-headless-components-preview/library/src/Textarea.ts b/packages/react-components/react-headless-components-preview/library/src/Textarea.ts new file mode 100644 index 0000000000000..e1ee150910003 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/Textarea.ts @@ -0,0 +1,2 @@ +export { Textarea, renderTextarea, useTextarea } from './components/Textarea/index'; +export type { TextareaSlots, TextareaProps, TextareaState } from './components/Textarea/index'; diff --git a/packages/react-components/react-headless-components-preview/library/src/ToggleButton.ts b/packages/react-components/react-headless-components-preview/library/src/ToggleButton.ts new file mode 100644 index 0000000000000..7faf241ce3633 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/ToggleButton.ts @@ -0,0 +1,2 @@ +export { ToggleButton, renderToggleButton, useToggleButton } from './components/ToggleButton'; +export type { ToggleButtonSlots, ToggleButtonProps, ToggleButtonState } from './components/ToggleButton'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/Accordion.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/Accordion.test.tsx index a97ab0ac256a3..e7799a273b0ba 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/Accordion.test.tsx +++ b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/Accordion.test.tsx @@ -26,7 +26,7 @@ describe('Accordion', () => { ]; it('renders a default state', () => { - const { container } = render( + const { getAllByRole, getByText } = render( {items.map(item => ( @@ -37,57 +37,17 @@ describe('Accordion', () => { , ); - expect(container.firstChild).toMatchInlineSnapshot(` -
-
-
- -
-
- Item #1 Panel -
-
-
-
- -
-
- Item #2 Panel -
-
-
- `); + const buttons = getAllByRole('button'); + expect(buttons).toHaveLength(2); + + // First item is open + expect(buttons[0]).toHaveAttribute('aria-expanded', 'true'); + expect(getByText('Item #1 header')).toBeInTheDocument(); + expect(getByText('Item #1 Panel')).toBeInTheDocument(); + + // Second item is closed + expect(buttons[1]).toHaveAttribute('aria-expanded', 'false'); + expect(getByText('Item #2 header')).toBeInTheDocument(); + expect(getByText('Item #2 Panel')).toBeInTheDocument(); }); }); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/Accordion.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/Accordion.types.ts index f70377ba16201..39f3108e03562 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/Accordion.types.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/Accordion.types.ts @@ -9,6 +9,17 @@ export type AccordionSlots = AccordionBaseSlots; export type AccordionProps = AccordionBaseProps; -export type AccordionState = AccordionBaseState; +export type AccordionState = AccordionBaseState & { + root: { + /** + * Data attribute set to indicate whether the accordion allows multiple items to be expanded at once. + */ + 'data-collapsible'?: string; + /** + * Data attribute set to indicate whether the accordion allows multiple items to be expanded at once. + */ + 'data-multiple'?: string; + }; +}; export type AccordionContextValues = AccordionBaseContextValues; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionHeader/AccordionHeader.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionHeader/AccordionHeader.types.ts index 0b945771795df..5fc52a150552c 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionHeader/AccordionHeader.types.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionHeader/AccordionHeader.types.ts @@ -9,6 +9,23 @@ export type AccordionHeaderSlots = AccordionHeaderBaseSlots; export type AccordionHeaderProps = AccordionHeaderBaseProps; -export type AccordionHeaderState = AccordionHeaderBaseState; +export type AccordionHeaderState = AccordionHeaderBaseState & { + root: { + /** + * Data attribute set when the accordion item is open. + */ + 'data-open'?: string; + + /** + * Data attribute set when the accordion header is disabled. + */ + 'data-disabled'?: string; + + /** + * Data attribute reflecting the expand icon position. Value is 'start' or 'end'. + */ + 'data-expand-icon-position'?: string; + }; +}; export type AccordionHeaderContextValues = AccordionHeaderBaseContextValues; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionHeader/useAccordionHeader.ts b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionHeader/useAccordionHeader.ts index f1964035d97bb..93ece6da7404e 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionHeader/useAccordionHeader.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionHeader/useAccordionHeader.ts @@ -8,13 +8,21 @@ import { } from '@fluentui/react-accordion'; import type { AccordionHeaderProps, AccordionHeaderState, AccordionHeaderContextValues } from './AccordionHeader.types'; +import { stringifyDataAttribute } from '../../../utils'; /** * Returns the state for an AccordionHeader component, given its props and ref. * The returned state can be modified with hooks before being passed to `renderAccordionHeader`. */ export const useAccordionHeader = (props: AccordionHeaderProps, ref: React.Ref): AccordionHeaderState => { - const state = useAccordionHeaderBase_unstable(props, ref); + 'use no memo'; + + const state: AccordionHeaderState = useAccordionHeaderBase_unstable(props, ref); + + // Set data attributes for open, disabled, and expand icon position states to simplify styling. + state.root['data-open'] = stringifyDataAttribute(state.open); + state.root['data-disabled'] = stringifyDataAttribute(state.disabled); + state.root['data-expand-icon-position'] = state.expandIconPosition; return state; }; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionItem/AccordionItem.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionItem/AccordionItem.types.ts index a5b064e93f589..d898c1a990177 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionItem/AccordionItem.types.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionItem/AccordionItem.types.ts @@ -9,6 +9,17 @@ export type AccordionItemSlots = AccordionItemBaseSlots; export type AccordionItemProps = AccordionItemBaseProps; -export type AccordionItemState = AccordionItemBaseState; +export type AccordionItemState = AccordionItemBaseState & { + root: { + /** + * Data attribute set to indicate whether the accordion item is disabled. + */ + 'data-disabled'?: string; + /** + * Data attribute set to indicate whether the accordion item is open. + */ + 'data-open'?: string; + }; +}; export type AccordionItemContextValues = AccordionItemBaseContextValues; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionItem/useAccordionItem.ts b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionItem/useAccordionItem.ts index 58010b1a5b490..87a9efbaf680e 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionItem/useAccordionItem.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionItem/useAccordionItem.ts @@ -15,12 +15,13 @@ import { stringifyDataAttribute } from '../../../utils'; * The returned state can be modified with hooks before being passed to `renderAccordionItem`. */ export const useAccordionItem = (props: AccordionItemProps, ref: React.Ref): AccordionItemState => { - const state = useAccordionItem_unstable(props, ref); + 'use no memo'; - Object.assign(state.root, { - 'data-disabled': stringifyDataAttribute(state.disabled), - 'data-open': stringifyDataAttribute(state.open), - }); + const state: AccordionItemState = useAccordionItem_unstable(props, ref); + + // Set data attributes for open and disabled states to simplify styling of these states. + state.root['data-disabled'] = stringifyDataAttribute(state.disabled); + state.root['data-open'] = stringifyDataAttribute(state.open); return state; }; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionPanel/AccordionPanel.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionPanel/AccordionPanel.types.ts index 311319fc5847f..318a6e803a683 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionPanel/AccordionPanel.types.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionPanel/AccordionPanel.types.ts @@ -8,4 +8,11 @@ export type AccordionPanelSlots = AccordionPanelBaseSlots; export type AccordionPanelProps = AccordionPanelBaseProps; -export type AccordionPanelState = AccordionPanelBaseState; +export type AccordionPanelState = AccordionPanelBaseState & { + root: { + /** + * Data attribute set when the accordion panel is open. + */ + 'data-open'?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionPanel/useAccordionPanel.ts b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionPanel/useAccordionPanel.ts index 1f664cd2a8521..ebdd7aea2ae80 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionPanel/useAccordionPanel.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/AccordionPanel/useAccordionPanel.ts @@ -4,13 +4,19 @@ import type * as React from 'react'; import { useAccordionPanelBase_unstable } from '@fluentui/react-accordion'; import type { AccordionPanelProps, AccordionPanelState } from './AccordionPanel.types'; +import { stringifyDataAttribute } from '../../../utils'; /** * Returns the state for an AccordionPanel component, given its props and ref. * The returned state can be modified with hooks before being passed to `renderAccordionPanel`. */ export const useAccordionPanel = (props: AccordionPanelProps, ref: React.Ref): AccordionPanelState => { - const state = useAccordionPanelBase_unstable(props, ref); + 'use no memo'; + + const state: AccordionPanelState = useAccordionPanelBase_unstable(props, ref); + + // Set data attribute for open state to simplify styling. + state.root['data-open'] = stringifyDataAttribute(state.open); return state; }; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/useAccordion.ts b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/useAccordion.ts index 8c734230b7f00..1421e18fc8047 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Accordion/useAccordion.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Accordion/useAccordion.ts @@ -15,12 +15,13 @@ import { stringifyDataAttribute } from '../../utils'; * The returned state can be modified with hooks before being passed to `renderAccordion`. */ export const useAccordion = (props: AccordionProps, ref: React.Ref): AccordionState => { - const state = useAccordionBase_unstable(props, ref); + 'use no memo'; - Object.assign(state.root, { - 'data-collapsible': stringifyDataAttribute(state.collapsible), - 'data-multiple': stringifyDataAttribute(state.multiple), - }); + const state: AccordionState = useAccordionBase_unstable(props, ref); + + // Set data attributes for collapsible and multiple states to simplify styling of these states. + state.root['data-collapsible'] = stringifyDataAttribute(state.collapsible); + state.root['data-multiple'] = stringifyDataAttribute(state.multiple); return state; }; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Avatar/Avatar.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Avatar/Avatar.test.tsx new file mode 100644 index 0000000000000..7e5b1a5e64428 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Avatar/Avatar.test.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { Avatar } from './Avatar'; + +describe('Avatar', () => { + isConformant({ + Component: Avatar, + displayName: 'Avatar', + }); + + it('renders a default state', () => { + const { getByRole } = render(); + const avatar = getByRole('img'); + + expect(avatar).toBeInTheDocument(); + expect(avatar).toHaveAttribute('aria-label', 'John Doe'); + expect(avatar).toHaveTextContent('JD'); + }); +}); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Avatar/Avatar.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Avatar/Avatar.tsx new file mode 100644 index 0000000000000..2441a688f8349 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Avatar/Avatar.tsx @@ -0,0 +1,19 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; + +import type { AvatarProps } from './Avatar.types'; +import { useAvatar } from './useAvatar'; +import { renderAvatar } from './renderAvatar'; + +/** + * An avatar component that displays an image or icon. + */ +export const Avatar: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useAvatar(props, ref); + + return renderAvatar(state); +}); + +Avatar.displayName = 'Avatar'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Avatar/Avatar.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Avatar/Avatar.types.ts new file mode 100644 index 0000000000000..111fcac1500c2 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Avatar/Avatar.types.ts @@ -0,0 +1,16 @@ +import type { AvatarSlots as AvatarBaseSlots, AvatarBaseProps, AvatarBaseState } from '@fluentui/react-avatar'; + +/** + * Avatar component slots + */ +export type AvatarSlots = AvatarBaseSlots; + +/** + * Avatar component props + */ +export type AvatarProps = AvatarBaseProps; + +/** + * Avatar component state + */ +export type AvatarState = AvatarBaseState; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Avatar/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Avatar/index.ts new file mode 100644 index 0000000000000..58da5abff54df --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Avatar/index.ts @@ -0,0 +1,4 @@ +export { Avatar } from './Avatar'; +export { renderAvatar } from './renderAvatar'; +export { useAvatar } from './useAvatar'; +export type { AvatarSlots, AvatarProps, AvatarState } from './Avatar.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Avatar/renderAvatar.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Avatar/renderAvatar.tsx new file mode 100644 index 0000000000000..a7dbd90ee6bce --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Avatar/renderAvatar.tsx @@ -0,0 +1,6 @@ +import { renderAvatar_unstable } from '@fluentui/react-avatar'; + +/** + * Renders the final JSX of the Avatar component, given the state. + */ +export const renderAvatar = renderAvatar_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Avatar/useAvatar.ts b/packages/react-components/react-headless-components-preview/library/src/components/Avatar/useAvatar.ts new file mode 100644 index 0000000000000..dc195103a6840 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Avatar/useAvatar.ts @@ -0,0 +1,16 @@ +'use client'; + +import type * as React from 'react'; +import { useAvatarBase_unstable } from '@fluentui/react-avatar'; + +import type { AvatarProps, AvatarState } from './Avatar.types'; + +/** + * Returns the state for an Avatar component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderAvatar`. + */ +export const useAvatar = (props: AvatarProps, ref: React.Ref): AvatarState => { + const state: AvatarState = useAvatarBase_unstable(props, ref); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Badge/Badge.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Badge/Badge.test.tsx new file mode 100644 index 0000000000000..f58eb382f8070 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Badge/Badge.test.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { Badge } from './Badge'; + +describe('Badge', () => { + isConformant({ + Component: Badge, + displayName: 'Badge', + }); + + it('renders a default state', () => { + const { getByText } = render(Default Badge); + + expect(getByText('Default Badge')).toBeInTheDocument(); + }); + + it('renders with data-icon-position when icon is provided', () => { + const { container } = render(★}>With Icon); + const root = container.firstElementChild!; + + expect(root).toHaveAttribute('data-icon-position', 'before'); + }); + + it('does not render data-icon-position without icon', () => { + const { container } = render(No Icon); + const root = container.firstElementChild!; + + expect(root).not.toHaveAttribute('data-icon-position'); + }); +}); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Badge/Badge.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Badge/Badge.tsx new file mode 100644 index 0000000000000..88d9da04d8ef2 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Badge/Badge.tsx @@ -0,0 +1,19 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; + +import type { BadgeProps } from './Badge.types'; +import { useBadge } from './useBadge'; +import { renderBadge } from './renderBadge'; + +/** + * A badge component for displaying counts or labels. + */ +export const Badge: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useBadge(props, ref); + + return renderBadge(state); +}); + +Badge.displayName = 'Badge'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Badge/Badge.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Badge/Badge.types.ts new file mode 100644 index 0000000000000..c1c4dcd6bf5d7 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Badge/Badge.types.ts @@ -0,0 +1,23 @@ +import type { BadgeSlots as BadgeBaseSlots, BadgeBaseProps, BadgeBaseState } from '@fluentui/react-badge'; + +/** + * Badge component slots + */ +export type BadgeSlots = BadgeBaseSlots; + +/** + * Badge component props + */ +export type BadgeProps = BadgeBaseProps; + +/** + * Badge component state + */ +export type BadgeState = BadgeBaseState & { + root: { + /** + * Data attribute reflecting the icon position when an icon slot is present. Value is 'before' or 'after'. + */ + 'data-icon-position'?: 'before' | 'after'; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Badge/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Badge/index.ts new file mode 100644 index 0000000000000..1f2787bfe1772 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Badge/index.ts @@ -0,0 +1,4 @@ +export { Badge } from './Badge'; +export { renderBadge } from './renderBadge'; +export { useBadge } from './useBadge'; +export type { BadgeSlots, BadgeProps, BadgeState } from './Badge.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Badge/renderBadge.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Badge/renderBadge.tsx new file mode 100644 index 0000000000000..e47c727459cce --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Badge/renderBadge.tsx @@ -0,0 +1,6 @@ +import { renderBadge_unstable } from '@fluentui/react-badge'; + +/** + * Renders the final JSX of the Badge component, given the state. + */ +export const renderBadge = renderBadge_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Badge/useBadge.ts b/packages/react-components/react-headless-components-preview/library/src/components/Badge/useBadge.ts new file mode 100644 index 0000000000000..3c8c51971a88d --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Badge/useBadge.ts @@ -0,0 +1,21 @@ +'use client'; + +import type * as React from 'react'; +import { useBadgeBase_unstable } from '@fluentui/react-badge'; + +import type { BadgeProps, BadgeState } from './Badge.types'; + +/** + * Returns the state for a Badge component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderBadge`. + */ +export const useBadge = (props: BadgeProps, ref: React.Ref): BadgeState => { + 'use no memo'; + + const state: BadgeState = useBadgeBase_unstable(props, ref); + + // Set data-icon-position only when an icon slot is present. + state.root['data-icon-position'] = state.icon ? state.iconPosition : undefined; + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/Breadcrumb.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/Breadcrumb.test.tsx new file mode 100644 index 0000000000000..10f28197cc8af --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/Breadcrumb.test.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { Breadcrumb } from './Breadcrumb'; + +describe('Breadcrumb', () => { + isConformant({ + Component: Breadcrumb, + displayName: 'Breadcrumb', + }); + + it('renders a default state', () => { + const { getByRole, getByText } = render(Default Breadcrumb); + const nav = getByRole('navigation'); + + expect(nav).toBeInTheDocument(); + expect(nav).toHaveAttribute('aria-label', 'breadcrumb'); + expect(nav.querySelector('ol')).toBeInTheDocument(); + expect(getByText('Default Breadcrumb')).toBeInTheDocument(); + }); +}); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/Breadcrumb.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/Breadcrumb.tsx new file mode 100644 index 0000000000000..2faaf67edce84 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/Breadcrumb.tsx @@ -0,0 +1,20 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; + +import type { BreadcrumbProps } from './Breadcrumb.types'; +import { useBreadcrumb, useBreadcrumbContextValues } from './useBreadcrumb'; +import { renderBreadcrumb } from './renderBreadcrumb'; + +/** + * A breadcrumb component for displaying navigation hierarchy. + */ +export const Breadcrumb: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useBreadcrumb(props, ref); + const contextValues = useBreadcrumbContextValues(state); + + return renderBreadcrumb(state, contextValues); +}); + +Breadcrumb.displayName = 'Breadcrumb'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/Breadcrumb.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/Breadcrumb.types.ts new file mode 100644 index 0000000000000..15e237ade2150 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/Breadcrumb.types.ts @@ -0,0 +1,26 @@ +import type { + BreadcrumbSlots as BreadcrumbBaseSlots, + BreadcrumbBaseProps, + BreadcrumbBaseState, + BreadcrumbContextValues as BreadcrumbBaseContextValues, +} from '@fluentui/react-breadcrumb'; + +/** + * Breadcrumb component slots + */ +export type BreadcrumbSlots = BreadcrumbBaseSlots; + +/** + * Breadcrumb component props + */ +export type BreadcrumbProps = BreadcrumbBaseProps; + +/** + * Breadcrumb component state + */ +export type BreadcrumbState = BreadcrumbBaseState; + +/** + * Context values provided by Breadcrumb to its sub-components. + */ +export type BreadcrumbContextValues = BreadcrumbBaseContextValues; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbButton/BreadcrumbButton.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbButton/BreadcrumbButton.tsx new file mode 100644 index 0000000000000..b14ce4082efb0 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbButton/BreadcrumbButton.tsx @@ -0,0 +1,20 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; + +import type { BreadcrumbButtonProps } from './BreadcrumbButton.types'; +import { useBreadcrumbButton } from './useBreadcrumbButton'; +import { renderBreadcrumbButton } from './renderBreadcrumbButton'; + +/** + * An interactive button representing a navigation step in a breadcrumb trail. + * Set the `current` prop to mark the active page. + */ +export const BreadcrumbButton: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useBreadcrumbButton(props, ref); + + return renderBreadcrumbButton(state); +}); + +BreadcrumbButton.displayName = 'BreadcrumbButton'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbButton/BreadcrumbButton.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbButton/BreadcrumbButton.types.ts new file mode 100644 index 0000000000000..dd3cb0093b542 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbButton/BreadcrumbButton.types.ts @@ -0,0 +1,27 @@ +import type { + BreadcrumbButtonSlots as BreadcrumbButtonBaseSlots, + BreadcrumbButtonBaseProps, + BreadcrumbButtonBaseState, +} from '@fluentui/react-breadcrumb'; + +/** + * BreadcrumbButton component slots + */ +export type BreadcrumbButtonSlots = BreadcrumbButtonBaseSlots; + +/** + * BreadcrumbButton component props + */ +export type BreadcrumbButtonProps = BreadcrumbButtonBaseProps; + +/** + * BreadcrumbButton component state + */ +export type BreadcrumbButtonState = BreadcrumbButtonBaseState & { + root: { + /** + * Data attribute set to indicate that this button represents the current page in the breadcrumb. + */ + 'data-current'?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbButton/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbButton/index.ts new file mode 100644 index 0000000000000..2e5582099aa1a --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbButton/index.ts @@ -0,0 +1,4 @@ +export { BreadcrumbButton } from './BreadcrumbButton'; +export { renderBreadcrumbButton } from './renderBreadcrumbButton'; +export { useBreadcrumbButton } from './useBreadcrumbButton'; +export type { BreadcrumbButtonSlots, BreadcrumbButtonProps, BreadcrumbButtonState } from './BreadcrumbButton.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbButton/renderBreadcrumbButton.ts b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbButton/renderBreadcrumbButton.ts new file mode 100644 index 0000000000000..a709686a8d220 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbButton/renderBreadcrumbButton.ts @@ -0,0 +1,12 @@ +import { renderBreadcrumbButton_unstable } from '@fluentui/react-breadcrumb'; +import type { BreadcrumbButtonBaseState } from '@fluentui/react-breadcrumb'; +import type { JSXElement } from '@fluentui/react-utilities'; + +import type { BreadcrumbButtonState } from './BreadcrumbButton.types'; + +/** + * Renders the final JSX of the BreadcrumbButton component, given the state. + */ +export const renderBreadcrumbButton = (state: BreadcrumbButtonState): JSXElement => { + return renderBreadcrumbButton_unstable(state as BreadcrumbButtonBaseState); +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbButton/useBreadcrumbButton.ts b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbButton/useBreadcrumbButton.ts new file mode 100644 index 0000000000000..d260c4ac69228 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbButton/useBreadcrumbButton.ts @@ -0,0 +1,25 @@ +'use client'; + +import type * as React from 'react'; +import { useBreadcrumbButtonBase_unstable } from '@fluentui/react-breadcrumb'; + +import type { BreadcrumbButtonProps, BreadcrumbButtonState } from './BreadcrumbButton.types'; +import { stringifyDataAttribute } from '../../../utils'; + +/** + * Returns the state for a BreadcrumbButton component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderBreadcrumbButton`. + */ +export const useBreadcrumbButton = ( + props: BreadcrumbButtonProps, + ref: React.Ref, +): BreadcrumbButtonState => { + 'use no memo'; + + const state: BreadcrumbButtonState = useBreadcrumbButtonBase_unstable(props, ref); + + // Set data attribute for current state to simplify styling of the active breadcrumb item. + state.root['data-current'] = stringifyDataAttribute(state.current); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbDivider/BreadcrumbDivider.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbDivider/BreadcrumbDivider.tsx new file mode 100644 index 0000000000000..db73976b7e775 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbDivider/BreadcrumbDivider.tsx @@ -0,0 +1,19 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; + +import type { BreadcrumbDividerProps } from './BreadcrumbDivider.types'; +import { useBreadcrumbDivider } from './useBreadcrumbDivider'; +import { renderBreadcrumbDivider } from './renderBreadcrumbDivider'; + +/** + * A visual separator between breadcrumb items. + */ +export const BreadcrumbDivider: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useBreadcrumbDivider(props, ref); + + return renderBreadcrumbDivider(state); +}); + +BreadcrumbDivider.displayName = 'BreadcrumbDivider'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbDivider/BreadcrumbDivider.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbDivider/BreadcrumbDivider.types.ts new file mode 100644 index 0000000000000..ceb907e7ad0ca --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbDivider/BreadcrumbDivider.types.ts @@ -0,0 +1,20 @@ +import type { + BreadcrumbDividerSlots as BreadcrumbDividerBaseSlots, + BreadcrumbDividerBaseProps, + BreadcrumbDividerBaseState, +} from '@fluentui/react-breadcrumb'; + +/** + * BreadcrumbDivider component slots + */ +export type BreadcrumbDividerSlots = BreadcrumbDividerBaseSlots; + +/** + * BreadcrumbDivider component props + */ +export type BreadcrumbDividerProps = BreadcrumbDividerBaseProps; + +/** + * BreadcrumbDivider component state + */ +export type BreadcrumbDividerState = BreadcrumbDividerBaseState; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbDivider/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbDivider/index.ts new file mode 100644 index 0000000000000..9a24109e5a136 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbDivider/index.ts @@ -0,0 +1,4 @@ +export { BreadcrumbDivider } from './BreadcrumbDivider'; +export { renderBreadcrumbDivider } from './renderBreadcrumbDivider'; +export { useBreadcrumbDivider } from './useBreadcrumbDivider'; +export type { BreadcrumbDividerSlots, BreadcrumbDividerProps, BreadcrumbDividerState } from './BreadcrumbDivider.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbDivider/renderBreadcrumbDivider.ts b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbDivider/renderBreadcrumbDivider.ts new file mode 100644 index 0000000000000..b6fc2aca033e2 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbDivider/renderBreadcrumbDivider.ts @@ -0,0 +1,6 @@ +import { renderBreadcrumbDivider_unstable } from '@fluentui/react-breadcrumb'; + +/** + * Renders the final JSX of the BreadcrumbDivider component, given the state. + */ +export const renderBreadcrumbDivider = renderBreadcrumbDivider_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbDivider/useBreadcrumbDivider.ts b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbDivider/useBreadcrumbDivider.ts new file mode 100644 index 0000000000000..fcbc02361f132 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbDivider/useBreadcrumbDivider.ts @@ -0,0 +1,19 @@ +'use client'; + +import type * as React from 'react'; +import { useBreadcrumbDividerBase_unstable } from '@fluentui/react-breadcrumb'; + +import type { BreadcrumbDividerProps, BreadcrumbDividerState } from './BreadcrumbDivider.types'; + +/** + * Returns the state for a BreadcrumbDivider component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderBreadcrumbDivider`. + */ +export const useBreadcrumbDivider = ( + props: BreadcrumbDividerProps, + ref: React.Ref, +): BreadcrumbDividerState => { + const state: BreadcrumbDividerState = useBreadcrumbDividerBase_unstable(props, ref); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbItem/BreadcrumbItem.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbItem/BreadcrumbItem.tsx new file mode 100644 index 0000000000000..395346e93cd84 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbItem/BreadcrumbItem.tsx @@ -0,0 +1,19 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; + +import type { BreadcrumbItemProps } from './BreadcrumbItem.types'; +import { useBreadcrumbItem } from './useBreadcrumbItem'; +import { renderBreadcrumbItem } from './renderBreadcrumbItem'; + +/** + * A list item that wraps a breadcrumb entry, such as a BreadcrumbButton or plain text. + */ +export const BreadcrumbItem: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useBreadcrumbItem(props, ref); + + return renderBreadcrumbItem(state); +}); + +BreadcrumbItem.displayName = 'BreadcrumbItem'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbItem/BreadcrumbItem.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbItem/BreadcrumbItem.types.ts new file mode 100644 index 0000000000000..51f6d92f49b09 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbItem/BreadcrumbItem.types.ts @@ -0,0 +1,20 @@ +import type { + BreadcrumbItemSlots as BreadcrumbItemBaseSlots, + BreadcrumbItemBaseProps, + BreadcrumbItemBaseState, +} from '@fluentui/react-breadcrumb'; + +/** + * BreadcrumbItem component slots + */ +export type BreadcrumbItemSlots = BreadcrumbItemBaseSlots; + +/** + * BreadcrumbItem component props + */ +export type BreadcrumbItemProps = BreadcrumbItemBaseProps; + +/** + * BreadcrumbItem component state + */ +export type BreadcrumbItemState = BreadcrumbItemBaseState; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbItem/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbItem/index.ts new file mode 100644 index 0000000000000..58d83098be85a --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbItem/index.ts @@ -0,0 +1,4 @@ +export { BreadcrumbItem } from './BreadcrumbItem'; +export { renderBreadcrumbItem } from './renderBreadcrumbItem'; +export { useBreadcrumbItem } from './useBreadcrumbItem'; +export type { BreadcrumbItemSlots, BreadcrumbItemProps, BreadcrumbItemState } from './BreadcrumbItem.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbItem/renderBreadcrumbItem.ts b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbItem/renderBreadcrumbItem.ts new file mode 100644 index 0000000000000..d0178cf53a6f4 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbItem/renderBreadcrumbItem.ts @@ -0,0 +1,6 @@ +import { renderBreadcrumbItem_unstable } from '@fluentui/react-breadcrumb'; + +/** + * Renders the final JSX of the BreadcrumbItem component, given the state. + */ +export const renderBreadcrumbItem = renderBreadcrumbItem_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbItem/useBreadcrumbItem.ts b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbItem/useBreadcrumbItem.ts new file mode 100644 index 0000000000000..b8b353f6fc75c --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/BreadcrumbItem/useBreadcrumbItem.ts @@ -0,0 +1,16 @@ +'use client'; + +import type * as React from 'react'; +import { useBreadcrumbItemBase_unstable } from '@fluentui/react-breadcrumb'; + +import type { BreadcrumbItemProps, BreadcrumbItemState } from './BreadcrumbItem.types'; + +/** + * Returns the state for a BreadcrumbItem component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderBreadcrumbItem`. + */ +export const useBreadcrumbItem = (props: BreadcrumbItemProps, ref: React.Ref): BreadcrumbItemState => { + const state: BreadcrumbItemState = useBreadcrumbItemBase_unstable(props, ref); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/index.ts new file mode 100644 index 0000000000000..23c1c9f1e956b --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/index.ts @@ -0,0 +1,13 @@ +export { Breadcrumb } from './Breadcrumb'; +export { renderBreadcrumb } from './renderBreadcrumb'; +export { useBreadcrumb, useBreadcrumbContext, useBreadcrumbContextValues } from './useBreadcrumb'; +export type { BreadcrumbSlots, BreadcrumbProps, BreadcrumbState, BreadcrumbContextValues } from './Breadcrumb.types'; + +export { BreadcrumbDivider, renderBreadcrumbDivider, useBreadcrumbDivider } from './BreadcrumbDivider'; +export type { BreadcrumbDividerSlots, BreadcrumbDividerProps, BreadcrumbDividerState } from './BreadcrumbDivider'; + +export { BreadcrumbItem, renderBreadcrumbItem, useBreadcrumbItem } from './BreadcrumbItem'; +export type { BreadcrumbItemSlots, BreadcrumbItemProps, BreadcrumbItemState } from './BreadcrumbItem'; + +export { BreadcrumbButton, renderBreadcrumbButton, useBreadcrumbButton } from './BreadcrumbButton'; +export type { BreadcrumbButtonSlots, BreadcrumbButtonProps, BreadcrumbButtonState } from './BreadcrumbButton'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/renderBreadcrumb.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/renderBreadcrumb.tsx new file mode 100644 index 0000000000000..21372820fb7a9 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/renderBreadcrumb.tsx @@ -0,0 +1,6 @@ +import { renderBreadcrumb_unstable } from '@fluentui/react-breadcrumb'; + +/** + * Renders the final JSX of the Breadcrumb component, given the state and context values. + */ +export const renderBreadcrumb = renderBreadcrumb_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/useBreadcrumb.ts b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/useBreadcrumb.ts new file mode 100644 index 0000000000000..64dd5eede1e7e --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Breadcrumb/useBreadcrumb.ts @@ -0,0 +1,33 @@ +'use client'; + +import type * as React from 'react'; +import { + useBreadcrumbBase_unstable, + useBreadcrumbContext_unstable, + useBreadcrumbContextValues_unstable, +} from '@fluentui/react-breadcrumb'; + +import type { BreadcrumbProps, BreadcrumbState, BreadcrumbContextValues } from './Breadcrumb.types'; + +/** + * Returns the state for a Breadcrumb component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderBreadcrumb`. + */ +export const useBreadcrumb = (props: BreadcrumbProps, ref: React.Ref): BreadcrumbState => { + const state: BreadcrumbState = useBreadcrumbBase_unstable(props, ref); + + return state; +}; + +/** + * Returns the context values provided by the nearest Breadcrumb, enabling child components to + * read breadcrumb-level state such as the current size. + */ +export const useBreadcrumbContext = useBreadcrumbContext_unstable; + +/** + * Maps Breadcrumb state to the context values passed down to child components. + */ +export const useBreadcrumbContextValues = useBreadcrumbContextValues_unstable as ( + state: BreadcrumbState, +) => BreadcrumbContextValues; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Button/Button.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Button/Button.test.tsx index 35e2562c89dd8..f4e9898ca2dd3 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Button/Button.test.tsx +++ b/packages/react-components/react-headless-components-preview/library/src/components/Button/Button.test.tsx @@ -13,13 +13,7 @@ describe('Button', () => { const result = render(); const button = result.getByRole('button', { name: 'Default Button' }); expect(button).toBeInTheDocument(); - expect(button).toMatchInlineSnapshot(` - - `); + expect(button).toHaveAttribute('type', 'button'); }); it('renders an anchor when "as" prop is set to "a"', () => { @@ -32,13 +26,7 @@ describe('Button', () => { const link = result.getByRole('link', { name: 'Link Button' }); expect(link).toBeInTheDocument(); - expect(link).toMatchInlineSnapshot(` - - Link Button - - `); + expect(link).toHaveAttribute('href', 'https://www.microsoft.com'); }); it('renders with state data attributes', () => { diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Button/Button.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Button/Button.types.ts index 81bbfb69d11b5..6080ab66927fa 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Button/Button.types.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Button/Button.types.ts @@ -13,4 +13,21 @@ export type ButtonProps = ButtonBaseProps; /** * Button component state */ -export type ButtonState = ButtonBaseState; +export type ButtonState = ButtonBaseState & { + root: { + /** + * Data attribute set when the button is disabled. + */ + 'data-disabled'?: string; + + /** + * Data attribute set when the button is disabled but still focusable. + */ + 'data-disabled-focusable'?: string; + + /** + * Data attribute set when the button renders only an icon. + */ + 'data-icon-only'?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Button/useButton.ts b/packages/react-components/react-headless-components-preview/library/src/components/Button/useButton.ts index 8fa1537184d18..e3619da1d76e9 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Button/useButton.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Button/useButton.ts @@ -11,13 +11,14 @@ import { stringifyDataAttribute } from '../../utils'; * The returned state can be modified with hooks before being passed to `renderButton`. */ export const useButton = (props: ButtonProps, ref: React.Ref): ButtonState => { - const state = useButtonBase_unstable(props, ref); + 'use no memo'; - Object.assign(state.root, { - 'data-disabled': stringifyDataAttribute(state.disabled), - 'data-disabled-focusable': stringifyDataAttribute(state.disabledFocusable), - 'data-icon-only': stringifyDataAttribute(state.iconOnly), - }); + const state: ButtonState = useButtonBase_unstable(props, ref); + + // Set data attributes for disabled, disabledFocusable, and iconOnly states to simplify styling of these states. + state.root['data-disabled'] = stringifyDataAttribute(state.disabled); + state.root['data-disabled-focusable'] = stringifyDataAttribute(state.disabledFocusable); + state.root['data-icon-only'] = stringifyDataAttribute(state.iconOnly); return state; }; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Checkbox/Checkbox.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Checkbox/Checkbox.test.tsx new file mode 100644 index 0000000000000..c00a1358b6544 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Checkbox/Checkbox.test.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { Checkbox } from './Checkbox'; + +describe('Checkbox', () => { + isConformant({ + Component: Checkbox, + displayName: 'Checkbox', + primarySlot: 'input', + }); + + it('renders a default state', () => { + const { getByRole, getByText } = render(); + const checkbox = getByRole('checkbox'); + + expect(checkbox).toBeInTheDocument(); + expect(checkbox).toBeChecked(); + expect(getByText('Default Checkbox')).toBeInTheDocument(); + }); + + it('renders with data-checked attribute when checked', () => { + const { container } = render(); + const root = container.firstElementChild!; + + expect(root).toHaveAttribute('data-checked'); + }); + + it('renders with data-disabled attribute when disabled', () => { + const { container } = render(); + const root = container.firstElementChild!; + + expect(root).toHaveAttribute('data-disabled'); + }); +}); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Checkbox/Checkbox.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Checkbox/Checkbox.tsx new file mode 100644 index 0000000000000..05f551879b70e --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Checkbox/Checkbox.tsx @@ -0,0 +1,18 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { useCheckbox } from './useCheckbox'; +import { renderCheckbox } from './renderCheckbox'; +import type { CheckboxProps } from './Checkbox.types'; + +/** + * Checkbox component - TODO: add more docs + */ +export const Checkbox: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useCheckbox(props, ref); + + return renderCheckbox(state); +}); + +Checkbox.displayName = 'Checkbox'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Checkbox/Checkbox.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Checkbox/Checkbox.types.ts new file mode 100644 index 0000000000000..fab0a8076f3ac --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Checkbox/Checkbox.types.ts @@ -0,0 +1,29 @@ +import type { + CheckboxSlots as CheckboxBaseSlots, + CheckboxBaseState, + CheckboxBaseProps, +} from '@fluentui/react-checkbox'; + +export type CheckboxSlots = CheckboxBaseSlots; + +/** + * Checkbox Props + */ +export type CheckboxProps = CheckboxBaseProps; + +/** + * State used in rendering Checkbox + */ +export type CheckboxState = CheckboxBaseState & { + root: { + /** + * Data attribute set when the checkbox is disabled. + */ + 'data-disabled'?: string; + + /** + * Data attribute set when the checkbox is checked. Value is 'mixed' when in the indeterminate state. + */ + 'data-checked'?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Checkbox/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Checkbox/index.ts new file mode 100644 index 0000000000000..6c2ba7bed9e16 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Checkbox/index.ts @@ -0,0 +1,4 @@ +export { Checkbox } from './Checkbox'; +export type { CheckboxSlots, CheckboxProps, CheckboxState } from './Checkbox.types'; +export { renderCheckbox } from './renderCheckbox'; +export { useCheckbox } from './useCheckbox'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Checkbox/renderCheckbox.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Checkbox/renderCheckbox.tsx new file mode 100644 index 0000000000000..b24c9b2ed209d --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Checkbox/renderCheckbox.tsx @@ -0,0 +1,6 @@ +import { renderCheckbox_unstable } from '@fluentui/react-checkbox'; + +/** + * Render the final JSX of Checkbox + */ +export const renderCheckbox = renderCheckbox_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Checkbox/useCheckbox.ts b/packages/react-components/react-headless-components-preview/library/src/components/Checkbox/useCheckbox.ts new file mode 100644 index 0000000000000..7e3af6f0c502b --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Checkbox/useCheckbox.ts @@ -0,0 +1,27 @@ +'use client'; + +import type * as React from 'react'; +import { useCheckboxBase_unstable } from '@fluentui/react-checkbox'; +import type { CheckboxProps, CheckboxState } from './Checkbox.types'; +import { stringifyDataAttribute } from '../../utils'; + +/** + * Create the state required to render Checkbox. + * + * The returned state can be modified with hooks, + * before being passed to renderCheckbox_unstable. + * + * @param props - props from this instance of Checkbox + * @param ref - reference to root HTMLInputElement of Checkbox + */ +export const useCheckbox = (props: CheckboxProps, ref: React.Ref): CheckboxState => { + 'use no memo'; + + const state: CheckboxState = useCheckboxBase_unstable(props, ref); + + // Set data attributes for disabled and checked states to simplify styling of these states. + state.root['data-disabled'] = stringifyDataAttribute(state.disabled); + state.root['data-checked'] = stringifyDataAttribute(state.checked); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Divider/Divider.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Divider/Divider.types.ts index c228163738bb6..3947f556a0c38 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Divider/Divider.types.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Divider/Divider.types.ts @@ -4,4 +4,11 @@ export type DividerSlots = DividerBaseSlots; export type DividerProps = DividerBaseProps; -export type DividerState = DividerBaseState; +export type DividerState = DividerBaseState & { + root: { + /** + * Data attribute set to indicate the orientation of the divider. + */ + 'data-orientation'?: 'vertical' | 'horizontal'; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Divider/useDivider.ts b/packages/react-components/react-headless-components-preview/library/src/components/Divider/useDivider.ts index b0f17acfcabae..f312873e608eb 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Divider/useDivider.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Divider/useDivider.ts @@ -10,11 +10,12 @@ import type { DividerProps, DividerState } from './Divider.types'; * The returned state can be modified with hooks before being passed to `renderDivider`. */ export const useDivider = (props: DividerProps, ref: React.Ref): DividerState => { - const state = useDividerBase_unstable(props, ref); + 'use no memo'; - Object.assign(state.root, { - 'data-orientation': props.vertical ? 'vertical' : 'horizontal', - }); + const state: DividerState = useDividerBase_unstable(props, ref); + + // Set data attribute for orientation to simplify styling of vertical vs horizontal dividers. + state.root['data-orientation'] = props.vertical ? 'vertical' : 'horizontal'; return state; }; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Field/Field.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Field/Field.test.tsx new file mode 100644 index 0000000000000..cf4e0ddff6657 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Field/Field.test.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { Field } from './Field'; + +describe('Field', () => { + isConformant({ + Component: Field, + displayName: 'Field', + }); + + it('renders a default state', () => { + const { getByText, container } = render(Default Field); + const root = container.firstElementChild!; + + expect(getByText('Default Field')).toBeInTheDocument(); + expect(root).toHaveAttribute('data-validate-state', 'none'); + }); + + it('renders with error validation state', () => { + const { container } = render(Error Field); + const root = container.firstElementChild!; + + expect(root).toHaveAttribute('data-validate-state', 'error'); + }); + + it('renders with success validation state', () => { + const { container } = render(Success Field); + const root = container.firstElementChild!; + + expect(root).toHaveAttribute('data-validate-state', 'success'); + }); +}); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Field/Field.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Field/Field.tsx new file mode 100644 index 0000000000000..c3250989039c7 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Field/Field.tsx @@ -0,0 +1,20 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { FieldProps } from './Field.types'; +import { useField } from './useField'; +import { renderField } from './renderField'; +import { useFieldContextValues } from './useFieldContextValues'; + +/** + * A field component for form input grouping. + */ +export const Field: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useField(props, ref); + const contextValues = useFieldContextValues(state); + + return renderField(state, contextValues); +}); + +Field.displayName = 'Field'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Field/Field.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Field/Field.types.ts new file mode 100644 index 0000000000000..f2890329cb4d6 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Field/Field.types.ts @@ -0,0 +1,30 @@ +import type { + FieldSlots as FieldBaseSlots, + FieldBaseProps, + FieldBaseState, + FieldContextValues as FieldContextValuesBase, +} from '@fluentui/react-field'; + +/** + * Field component slots + */ +export type FieldSlots = FieldBaseSlots; + +/** + * Field component props + */ +export type FieldProps = FieldBaseProps; + +/** + * Field component state + */ +export type FieldState = FieldBaseState & { + root: { + 'data-validate-state'?: FieldBaseState['validationState']; + }; +}; + +/** + * Field component context values + */ +export type FieldContextValues = FieldContextValuesBase; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Field/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Field/index.ts new file mode 100644 index 0000000000000..7d165fd1d0e39 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Field/index.ts @@ -0,0 +1,4 @@ +export { Field } from './Field'; +export { renderField } from './renderField'; +export { useField } from './useField'; +export type { FieldSlots, FieldProps, FieldState } from './Field.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Field/renderField.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Field/renderField.tsx new file mode 100644 index 0000000000000..5223ac169fe8e --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Field/renderField.tsx @@ -0,0 +1,6 @@ +import { renderField_unstable } from '@fluentui/react-field'; + +/** + * Renders the final JSX of the Field component, given the state. + */ +export const renderField = renderField_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Field/useField.ts b/packages/react-components/react-headless-components-preview/library/src/components/Field/useField.ts new file mode 100644 index 0000000000000..7a22ff90d060f --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Field/useField.ts @@ -0,0 +1,20 @@ +'use client'; + +import type * as React from 'react'; +import { useFieldBase_unstable } from '@fluentui/react-field'; + +import type { FieldProps, FieldState } from './Field.types'; + +/** + * Returns the state for a Field component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderField`. + */ +export const useField = (props: FieldProps, ref: React.Ref): FieldState => { + 'use no memo'; + + const state: FieldState = useFieldBase_unstable(props, ref); + + state.root['data-validate-state'] = state.validationState; + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Field/useFieldContextValues.ts b/packages/react-components/react-headless-components-preview/library/src/components/Field/useFieldContextValues.ts new file mode 100644 index 0000000000000..e942fb9ba3fea --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Field/useFieldContextValues.ts @@ -0,0 +1,10 @@ +'use client'; + +import { useFieldContextValues_unstable } from '@fluentui/react-field'; + +import type { FieldState, FieldContextValues } from './Field.types'; + +/** + * Returns the context values for a Field component, given its state. + */ +export const useFieldContextValues = useFieldContextValues_unstable as (state: FieldState) => FieldContextValues; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Input/Input.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Input/Input.test.tsx new file mode 100644 index 0000000000000..7789788220240 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Input/Input.test.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { Input } from './Input'; + +describe('Input', () => { + isConformant({ + Component: Input, + displayName: 'Input', + primarySlot: 'input', + }); + + it('renders a default state', () => { + const { getByRole } = render(); + const input = getByRole('textbox'); + + expect(input).toBeInTheDocument(); + expect(input).toHaveAttribute('placeholder', 'Input your text'); + expect(input).toHaveAttribute('type', 'text'); + }); + + it('renders with data-disabled when disabled', () => { + const { container } = render(); + const root = container.firstElementChild!; + + expect(root).toHaveAttribute('data-disabled'); + }); +}); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Input/Input.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Input/Input.tsx new file mode 100644 index 0000000000000..8f0a0618c4da8 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Input/Input.tsx @@ -0,0 +1,18 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { InputProps } from './Input.types'; +import { useInput } from './useInput'; +import { renderInput } from './renderInput'; + +/** + * An input component for text and other input types. + */ +export const Input: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useInput(props, ref); + + return renderInput(state); +}); + +Input.displayName = 'Input'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Input/Input.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Input/Input.types.ts new file mode 100644 index 0000000000000..17af79929b52c --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Input/Input.types.ts @@ -0,0 +1,23 @@ +import type { InputSlots as InputBaseSlots, InputBaseProps, InputBaseState } from '@fluentui/react-input'; + +/** + * Input component slots + */ +export type InputSlots = InputBaseSlots; + +/** + * Input component props + */ +export type InputProps = InputBaseProps; + +/** + * Input component state + */ +export type InputState = InputBaseState & { + root: { + /** + * Data attribute set when the input is disabled. + */ + 'data-disabled'?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Input/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Input/index.ts new file mode 100644 index 0000000000000..5c7dc655e38a7 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Input/index.ts @@ -0,0 +1,4 @@ +export { Input } from './Input'; +export { renderInput } from './renderInput'; +export { useInput } from './useInput'; +export type { InputSlots, InputProps, InputState } from './Input.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Input/renderInput.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Input/renderInput.tsx new file mode 100644 index 0000000000000..7e1891f1a33a9 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Input/renderInput.tsx @@ -0,0 +1,6 @@ +import { renderInput_unstable } from '@fluentui/react-input'; + +/** + * Renders the final JSX of the Input component, given the state. + */ +export const renderInput = renderInput_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Input/useInput.ts b/packages/react-components/react-headless-components-preview/library/src/components/Input/useInput.ts new file mode 100644 index 0000000000000..d353493b2d585 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Input/useInput.ts @@ -0,0 +1,22 @@ +'use client'; + +import type * as React from 'react'; +import { useInputBase_unstable } from '@fluentui/react-input'; + +import type { InputProps, InputState } from './Input.types'; +import { stringifyDataAttribute } from '../../utils'; + +/** + * Returns the state for an Input component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderInput`. + */ +export const useInput = (props: InputProps, ref: React.Ref): InputState => { + 'use no memo'; + + const state: InputState = useInputBase_unstable(props, ref); + + // Set data attribute for disabled state to simplify styling. + state.root['data-disabled'] = stringifyDataAttribute(state.input.disabled); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Label/Label.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Label/Label.tsx new file mode 100644 index 0000000000000..68baf453df1a2 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Label/Label.tsx @@ -0,0 +1,18 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { LabelProps } from './Label.types'; +import { useLabel } from './useLabel'; +import { renderLabel } from './renderLabel'; + +/** + * A label component that associates text with form elements. + */ +export const Label: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useLabel(props, ref); + + return renderLabel(state); +}); + +Label.displayName = 'Label'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Label/Label.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Label/Label.types.ts new file mode 100644 index 0000000000000..8d6b57e232b8c --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Label/Label.types.ts @@ -0,0 +1,23 @@ +import type { LabelSlots as LabelBaseSlots, LabelBaseProps, LabelBaseState } from '@fluentui/react-label'; + +/** + * Label component slots + */ +export type LabelSlots = LabelBaseSlots; + +/** + * Label component props + */ +export type LabelProps = LabelBaseProps; + +/** + * Label component state + */ +export type LabelState = LabelBaseState & { + root: { + /** + * Data attribute set when the label is disabled. + */ + 'data-disabled'?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Label/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Label/index.ts new file mode 100644 index 0000000000000..6679803721ab1 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Label/index.ts @@ -0,0 +1,4 @@ +export { Label } from './Label'; +export { renderLabel } from './renderLabel'; +export { useLabel } from './useLabel'; +export type { LabelSlots, LabelProps, LabelState } from './Label.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Label/renderLabel.ts b/packages/react-components/react-headless-components-preview/library/src/components/Label/renderLabel.ts new file mode 100644 index 0000000000000..ae856b597bfd4 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Label/renderLabel.ts @@ -0,0 +1,6 @@ +import { renderLabel_unstable } from '@fluentui/react-label'; + +/** + * Renders the final JSX of the Label component, given the state. + */ +export const renderLabel = renderLabel_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Label/useLabel.ts b/packages/react-components/react-headless-components-preview/library/src/components/Label/useLabel.ts new file mode 100644 index 0000000000000..27f1437c01a92 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Label/useLabel.ts @@ -0,0 +1,22 @@ +'use client'; + +import type * as React from 'react'; +import { useLabelBase_unstable } from '@fluentui/react-label'; + +import type { LabelProps, LabelState } from './Label.types'; +import { stringifyDataAttribute } from '../../utils'; + +/** + * Returns the state for a Label component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderLabel`. + */ +export const useLabel = (props: LabelProps, ref: React.Ref): LabelState => { + 'use no memo'; + + const state: LabelState = useLabelBase_unstable(props, ref); + + // Set data attribute for disabled state to simplify styling. + state.root['data-disabled'] = stringifyDataAttribute(state.disabled); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Link/Link.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Link/Link.test.tsx new file mode 100644 index 0000000000000..cdea20fcb54d3 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Link/Link.test.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { Link } from './Link'; + +describe('Link', () => { + isConformant({ + Component: Link, + displayName: 'Link', + }); + + it('renders as a button when no href is provided', () => { + const { getByRole } = render(Default Link); + const button = getByRole('button'); + + expect(button).toBeInTheDocument(); + expect(button).toHaveTextContent('Default Link'); + }); + + it('renders as an anchor when href is provided', () => { + const { getByRole } = render(Anchor Link); + const link = getByRole('link'); + + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href', 'https://example.com'); + expect(link).toHaveTextContent('Anchor Link'); + }); + + it('renders with data-disabled when disabled', () => { + const { container } = render(Disabled Link); + const root = container.firstElementChild!; + + expect(root).toHaveAttribute('data-disabled'); + }); + + it('renders with data-disabled-focusable when disabledFocusable', () => { + const { container } = render(Disabled Focusable Link); + const root = container.firstElementChild!; + + expect(root).toHaveAttribute('data-disabled'); + expect(root).toHaveAttribute('data-disabled-focusable'); + }); +}); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Link/Link.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Link/Link.tsx new file mode 100644 index 0000000000000..b72231440020c --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Link/Link.tsx @@ -0,0 +1,19 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; + +import type { LinkProps } from './Link.types'; +import { useLink } from './useLink'; +import { renderLink } from './renderLink'; + +/** + * A link component for navigation. + */ +export const Link: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useLink(props, ref); + + return renderLink(state); +}); + +Link.displayName = 'Link'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Link/Link.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Link/Link.types.ts new file mode 100644 index 0000000000000..9b88ec57c8035 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Link/Link.types.ts @@ -0,0 +1,28 @@ +import type { LinkSlots as LinkBaseSlots, LinkBaseProps, LinkBaseState } from '@fluentui/react-link'; + +/** + * Link component slots + */ +export type LinkSlots = LinkBaseSlots; + +/** + * Link component props + */ +export type LinkProps = LinkBaseProps; + +/** + * Link component state + */ +export type LinkState = LinkBaseState & { + root: { + /** + * Data attribute set when the link is disabled. + */ + 'data-disabled'?: string; + + /** + * Data attribute set when the link is disabled but still focusable. + */ + 'data-disabled-focusable'?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Link/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Link/index.ts new file mode 100644 index 0000000000000..f27464593eec2 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Link/index.ts @@ -0,0 +1,4 @@ +export { Link } from './Link'; +export { renderLink } from './renderLink'; +export { useLink } from './useLink'; +export type { LinkSlots, LinkProps, LinkState } from './Link.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Link/renderLink.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Link/renderLink.tsx new file mode 100644 index 0000000000000..0fda28a3a1366 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Link/renderLink.tsx @@ -0,0 +1,6 @@ +import { renderLink_unstable } from '@fluentui/react-link'; + +/** + * Renders the final JSX of the Link component, given the state. + */ +export const renderLink = renderLink_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Link/useLink.ts b/packages/react-components/react-headless-components-preview/library/src/components/Link/useLink.ts new file mode 100644 index 0000000000000..e2bff81db26b9 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Link/useLink.ts @@ -0,0 +1,22 @@ +'use client'; + +import type * as React from 'react'; +import { useLinkBase_unstable } from '@fluentui/react-link'; + +import type { LinkProps, LinkState } from './Link.types'; +import { stringifyDataAttribute } from '../../utils'; + +/** + * Returns the state for a Link component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderLink`. + */ +export const useLink = (props: LinkProps, ref: React.Ref): LinkState => { + 'use no memo'; + + const state: LinkState = useLinkBase_unstable(props, ref); + + state.root['data-disabled'] = stringifyDataAttribute(state.disabled); + state.root['data-disabled-focusable'] = stringifyDataAttribute(state.disabledFocusable); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/ProgressBar/ProgressBar.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/ProgressBar/ProgressBar.test.tsx new file mode 100644 index 0000000000000..c9d555d8639fa --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/ProgressBar/ProgressBar.test.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { ProgressBar } from './ProgressBar'; + +describe('ProgressBar', () => { + isConformant({ + Component: ProgressBar, + displayName: 'ProgressBar', + }); + + it('renders a default state', () => { + const { getByRole } = render(Default ProgressBar); + const progressbar = getByRole('progressbar'); + + expect(progressbar).toBeInTheDocument(); + }); + + it('renders the bar with correct width when value is provided', () => { + const { getByRole } = render(); + const progressbar = getByRole('progressbar'); + const bar = progressbar.firstElementChild!; + + expect(bar).toHaveStyle({ width: '50%' }); + }); +}); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/ProgressBar/ProgressBar.tsx b/packages/react-components/react-headless-components-preview/library/src/components/ProgressBar/ProgressBar.tsx new file mode 100644 index 0000000000000..17c00f567e1db --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/ProgressBar/ProgressBar.tsx @@ -0,0 +1,18 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import { useProgressBar } from './useProgressBar'; +import { renderProgressBar } from './renderProgressBar'; +import type { ProgressBarProps } from './ProgressBar.types'; + +/** + * ProgressBar component - TODO: add more docs + */ +export const ProgressBar: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useProgressBar(props, ref); + + return renderProgressBar(state); +}); + +ProgressBar.displayName = 'ProgressBar'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/ProgressBar/ProgressBar.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/ProgressBar/ProgressBar.types.ts new file mode 100644 index 0000000000000..00d2f37d74e24 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/ProgressBar/ProgressBar.types.ts @@ -0,0 +1,17 @@ +import type { + ProgressBarSlots as ProgressBarBaseSlots, + ProgressBarBaseProps, + ProgressBarBaseState, +} from '@fluentui/react-progress'; + +export type ProgressBarSlots = ProgressBarBaseSlots; + +/** + * ProgressBar Props + */ +export type ProgressBarProps = ProgressBarBaseProps; + +/** + * State used in rendering ProgressBar + */ +export type ProgressBarState = ProgressBarBaseState; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/ProgressBar/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/ProgressBar/index.ts new file mode 100644 index 0000000000000..7c8181a5c39c7 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/ProgressBar/index.ts @@ -0,0 +1,4 @@ +export { ProgressBar } from './ProgressBar'; +export type { ProgressBarSlots, ProgressBarProps, ProgressBarState } from './ProgressBar.types'; +export { renderProgressBar } from './renderProgressBar'; +export { useProgressBar } from './useProgressBar'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/ProgressBar/renderProgressBar.tsx b/packages/react-components/react-headless-components-preview/library/src/components/ProgressBar/renderProgressBar.tsx new file mode 100644 index 0000000000000..905b5243737ce --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/ProgressBar/renderProgressBar.tsx @@ -0,0 +1,9 @@ +import { renderProgressBar_unstable } from '@fluentui/react-progress'; +import type { JSXElement } from '@fluentui/react-utilities'; + +import type { ProgressBarState } from './ProgressBar.types'; + +/** + * Render the final JSX of ProgressBar + */ +export const renderProgressBar = renderProgressBar_unstable as (state: ProgressBarState) => JSXElement; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/ProgressBar/useProgressBar.ts b/packages/react-components/react-headless-components-preview/library/src/components/ProgressBar/useProgressBar.ts new file mode 100644 index 0000000000000..a348500951a4e --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/ProgressBar/useProgressBar.ts @@ -0,0 +1,30 @@ +'use client'; + +import type * as React from 'react'; +import { useProgressBarBase_unstable } from '@fluentui/react-progress'; + +import type { ProgressBarProps, ProgressBarState } from './ProgressBar.types'; + +/** + * Create the state required to render ProgressBar. + * + * The returned state can be modified with hooks, + * before being passed to renderProgressBar_unstable. + * + * @param props - props from this instance of ProgressBar + * @param ref - reference to root HTMLDivElement of ProgressBar + */ +export const useProgressBar = (props: ProgressBarProps, ref: React.Ref): ProgressBarState => { + 'use no memo'; + + const state = useProgressBarBase_unstable(props, ref); + + if (state.bar && state.value !== undefined) { + state.bar.style = { + width: Math.min(100, Math.max(0, (state.value / state.max) * 100)) + '%', + ...state.bar.style, + }; + } + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/Radio/Radio.tsx b/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/Radio/Radio.tsx new file mode 100644 index 0000000000000..37c67fff8d9a2 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/Radio/Radio.tsx @@ -0,0 +1,17 @@ +'use client'; + +import * as React from 'react'; +import type { RadioProps } from './Radio.types'; +import { useRadio } from './useRadio'; +import { renderRadio } from './renderRadio'; + +/** + * A Radio component for use inside a RadioGroup. + */ +export const Radio = React.forwardRef((props, ref) => { + const state = useRadio(props, ref); + + return renderRadio(state); +}); + +Radio.displayName = 'Radio'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/Radio/Radio.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/Radio/Radio.types.ts new file mode 100644 index 0000000000000..3a70c4aeec9d7 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/Radio/Radio.types.ts @@ -0,0 +1,23 @@ +import type { RadioSlots as RadioBaseSlots, RadioBaseProps, RadioBaseState } from '@fluentui/react-radio'; + +/** + * Radio component slots + */ +export type RadioSlots = RadioBaseSlots; + +/** + * Radio component props + */ +export type RadioProps = RadioBaseProps; + +/** + * Radio component state + */ +export type RadioState = RadioBaseState & { + root: { + /** + * Data attribute set when the radio is disabled. + */ + 'data-disabled'?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/Radio/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/Radio/index.ts new file mode 100644 index 0000000000000..d5771a3974c95 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/Radio/index.ts @@ -0,0 +1,4 @@ +export { Radio } from './Radio'; +export { renderRadio } from './renderRadio'; +export { useRadio } from './useRadio'; +export type { RadioSlots, RadioProps, RadioState } from './Radio.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/Radio/renderRadio.tsx b/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/Radio/renderRadio.tsx new file mode 100644 index 0000000000000..755b32abbb7d6 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/Radio/renderRadio.tsx @@ -0,0 +1,6 @@ +import { renderRadio_unstable } from '@fluentui/react-radio'; + +/** + * Renders the final JSX of the Radio component, given the state. + */ +export const renderRadio = renderRadio_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/Radio/useRadio.ts b/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/Radio/useRadio.ts new file mode 100644 index 0000000000000..0d570bcbd7dec --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/Radio/useRadio.ts @@ -0,0 +1,22 @@ +'use client'; + +import type * as React from 'react'; +import { useRadioBase_unstable } from '@fluentui/react-radio'; + +import type { RadioProps, RadioState } from './Radio.types'; +import { stringifyDataAttribute } from '../../../utils'; + +/** + * Returns the state for a Radio component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderRadio`. + */ +export const useRadio = (props: RadioProps, ref: React.Ref): RadioState => { + 'use no memo'; + + const state: RadioState = useRadioBase_unstable(props, ref); + + // Set data attribute for disabled state to simplify styling. + state.root['data-disabled'] = stringifyDataAttribute(state.input.disabled); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/RadioGroup.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/RadioGroup.test.tsx new file mode 100644 index 0000000000000..629dd58a90892 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/RadioGroup.test.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { RadioGroup } from './RadioGroup'; + +describe('RadioGroup', () => { + isConformant({ + Component: RadioGroup, + displayName: 'RadioGroup', + }); + + it('renders a default state', () => { + const { getByRole, getByText } = render(Default RadioGroup); + const radiogroup = getByRole('radiogroup'); + + expect(radiogroup).toBeInTheDocument(); + expect(getByText('Default RadioGroup')).toBeInTheDocument(); + }); +}); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/RadioGroup.tsx b/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/RadioGroup.tsx new file mode 100644 index 0000000000000..18ee525fb7c58 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/RadioGroup.tsx @@ -0,0 +1,21 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { RadioGroupProps } from './RadioGroup.types'; + +import { useRadioGroup } from './useRadioGroup'; +import { renderRadioGroup } from './renderRadioGroup'; +import { useRadioGroupContextValues } from './useRadioGroupContextValues'; + +/** + * A radio group component for selecting one option. + */ +export const RadioGroup: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useRadioGroup(props, ref); + const contextValues = useRadioGroupContextValues(state); + + return renderRadioGroup(state, contextValues); +}); + +RadioGroup.displayName = 'RadioGroup'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/RadioGroup.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/RadioGroup.types.ts new file mode 100644 index 0000000000000..23d7cb05ab802 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/RadioGroup.types.ts @@ -0,0 +1,26 @@ +import type { + RadioGroupSlots as RadioGroupBaseSlots, + RadioGroupBaseProps, + RadioGroupBaseState, + RadioGroupContextValues as RadioGroupContextValuesBase, +} from '@fluentui/react-radio'; + +/** + * RadioGroup component slots + */ +export type RadioGroupSlots = RadioGroupBaseSlots; + +/** + * RadioGroup component props + */ +export type RadioGroupProps = RadioGroupBaseProps; + +/** + * RadioGroup component state + */ +export type RadioGroupState = RadioGroupBaseState; + +/** + * RadioGroup component context values + */ +export type RadioGroupContextValues = RadioGroupContextValuesBase; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/index.ts new file mode 100644 index 0000000000000..0227525cd8440 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/index.ts @@ -0,0 +1,7 @@ +export { RadioGroup } from './RadioGroup'; +export { renderRadioGroup } from './renderRadioGroup'; +export { useRadioGroup } from './useRadioGroup'; +export type { RadioGroupSlots, RadioGroupProps, RadioGroupState } from './RadioGroup.types'; + +export { Radio, renderRadio, useRadio } from './Radio'; +export type { RadioSlots, RadioProps, RadioState } from './Radio'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/renderRadioGroup.tsx b/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/renderRadioGroup.tsx new file mode 100644 index 0000000000000..9a8edbf465385 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/renderRadioGroup.tsx @@ -0,0 +1,6 @@ +import { renderRadioGroup_unstable } from '@fluentui/react-radio'; + +/** + * Renders the final JSX of the RadioGroup component, given the state. + */ +export const renderRadioGroup = renderRadioGroup_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/useRadioGroup.ts b/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/useRadioGroup.ts new file mode 100644 index 0000000000000..343a57345f9dc --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/useRadioGroup.ts @@ -0,0 +1,16 @@ +'use client'; + +import type * as React from 'react'; +import { useRadioGroupBase_unstable } from '@fluentui/react-radio'; + +import type { RadioGroupProps, RadioGroupState } from './RadioGroup.types'; + +/** + * Returns the state for a RadioGroup component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderRadioGroup`. + */ +export const useRadioGroup = (props: RadioGroupProps, ref: React.Ref): RadioGroupState => { + const state = useRadioGroupBase_unstable(props, ref); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/useRadioGroupContextValues.ts b/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/useRadioGroupContextValues.ts new file mode 100644 index 0000000000000..a35741e42159e --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/RadioGroup/useRadioGroupContextValues.ts @@ -0,0 +1,8 @@ +'use client'; + +import { useRadioGroupContextValues as useRadioGroupContextValues_unstable } from '@fluentui/react-radio'; +import type { RadioGroupContextValues, RadioGroupState } from './RadioGroup.types'; + +export const useRadioGroupContextValues = useRadioGroupContextValues_unstable as ( + state: RadioGroupState, +) => RadioGroupContextValues; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Rating/Rating.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Rating/Rating.test.tsx new file mode 100644 index 0000000000000..7aa6171cbee45 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Rating/Rating.test.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { Rating } from './Rating'; +import { RatingItem } from './RatingItem'; + +describe('Rating', () => { + isConformant({ + Component: Rating, + displayName: 'Rating', + }); + + it('renders a default state', () => { + const { getByRole, getAllByRole } = render( + + + + + + + , + ); + const radiogroup = getByRole('radiogroup'); + const radios = getAllByRole('radio'); + + expect(radiogroup).toBeInTheDocument(); + expect(radios).toHaveLength(5); + }); + + it('checks the correct radio based on defaultValue', () => { + const { getAllByRole } = render( + + + + + + + , + ); + const radios = getAllByRole('radio') as HTMLInputElement[]; + + expect(radios[2]).toBeChecked(); + expect(radios[0]).not.toBeChecked(); + expect(radios[4]).not.toBeChecked(); + }); + + it('renders filled appearance for items up to the selected value', () => { + const { container } = render( + + + + + + + , + ); + const items = container.querySelectorAll('[data-appearance]'); + const appearances = Array.from(items).map(item => item.getAttribute('data-appearance')); + + expect(appearances).toEqual(['filled', 'filled', 'filled', 'outline', 'outline']); + }); +}); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Rating/Rating.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Rating/Rating.tsx new file mode 100644 index 0000000000000..b6acc6635bcb6 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Rating/Rating.tsx @@ -0,0 +1,21 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; + +import type { RatingProps } from './Rating.types'; +import { useRating } from './useRating'; +import { renderRating } from './renderRating'; +import { useRatingContextValues } from './useRatingContextValues'; + +/** + * A rating component for displaying star ratings. + */ +export const Rating: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useRating(props, ref); + const contextValues = useRatingContextValues(state); + + return renderRating(state, contextValues); +}); + +Rating.displayName = 'Rating'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Rating/Rating.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Rating/Rating.types.ts new file mode 100644 index 0000000000000..b5c63ce69007e --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Rating/Rating.types.ts @@ -0,0 +1,26 @@ +import type { + RatingSlots as RatingBaseSlots, + RatingBaseProps, + RatingBaseState, + RatingContextValues as RatingContextValuesBase, +} from '@fluentui/react-rating'; + +/** + * Rating component slots + */ +export type RatingSlots = RatingBaseSlots; + +/** + * Rating component props + */ +export type RatingProps = RatingBaseProps; + +/** + * Rating component state + */ +export type RatingState = RatingBaseState; + +/** + * Rating component context values + */ +export type RatingContextValues = RatingContextValuesBase; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Rating/RatingItem/RatingItem.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Rating/RatingItem/RatingItem.tsx new file mode 100644 index 0000000000000..c1df766bcaf8a --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Rating/RatingItem/RatingItem.tsx @@ -0,0 +1,21 @@ +'use client'; + +import * as React from 'react'; +import type { RatingItemProps } from './RatingItem.types'; +import { useRatingItem } from './useRatingItem'; +import { renderRatingItem } from './renderRatingItem'; + +/** + * A RatingItem component representing a single star/icon within a Rating. + */ +export const RatingItem = React.forwardRef((props, ref) => { + const state = useRatingItem(props, ref); + + Object.assign(state.root, { + 'data-appearance': state.iconFillWidth === 1 ? 'filled' : state.iconFillWidth > 0 ? 'filled-half' : 'outline', + }); + + return renderRatingItem(state); +}); + +RatingItem.displayName = 'RatingItem'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Rating/RatingItem/RatingItem.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Rating/RatingItem/RatingItem.types.ts new file mode 100644 index 0000000000000..57e5361304610 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Rating/RatingItem/RatingItem.types.ts @@ -0,0 +1,27 @@ +import type { + RatingItemSlots as RatingItemBaseSlots, + RatingItemBaseProps, + RatingItemBaseState, +} from '@fluentui/react-rating'; + +/** + * RatingItem component slots + */ +export type RatingItemSlots = RatingItemBaseSlots; + +/** + * RatingItem component props + */ +export type RatingItemProps = RatingItemBaseProps; + +/** + * RatingItem component state + */ +export type RatingItemState = RatingItemBaseState & { + root: { + /** + * Data attribute reflecting the appearance of the rating item. Value is 'filled', 'filled-half', or 'outline'. + */ + 'data-appearance'?: 'filled' | 'filled-half' | 'outline'; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Rating/RatingItem/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Rating/RatingItem/index.ts new file mode 100644 index 0000000000000..871ca14f654d4 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Rating/RatingItem/index.ts @@ -0,0 +1,4 @@ +export { RatingItem } from './RatingItem'; +export { renderRatingItem } from './renderRatingItem'; +export { useRatingItem } from './useRatingItem'; +export type { RatingItemSlots, RatingItemProps, RatingItemState } from './RatingItem.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Rating/RatingItem/renderRatingItem.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Rating/RatingItem/renderRatingItem.tsx new file mode 100644 index 0000000000000..c85d3c3fabc25 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Rating/RatingItem/renderRatingItem.tsx @@ -0,0 +1,6 @@ +import { renderRatingItem_unstable } from '@fluentui/react-rating'; + +/** + * Renders the final JSX of the RatingItem component, given the state. + */ +export const renderRatingItem = renderRatingItem_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Rating/RatingItem/useRatingItem.ts b/packages/react-components/react-headless-components-preview/library/src/components/Rating/RatingItem/useRatingItem.ts new file mode 100644 index 0000000000000..9e0d12e28a357 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Rating/RatingItem/useRatingItem.ts @@ -0,0 +1,16 @@ +'use client'; + +import type * as React from 'react'; +import { useRatingItemBase_unstable } from '@fluentui/react-rating'; + +import type { RatingItemProps, RatingItemState } from './RatingItem.types'; + +/** + * Returns the state for a RatingItem component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderRatingItem`. + */ +export const useRatingItem = (props: RatingItemProps, ref: React.Ref): RatingItemState => { + const state = useRatingItemBase_unstable(props, ref); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Rating/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Rating/index.ts new file mode 100644 index 0000000000000..d4b1d142dde41 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Rating/index.ts @@ -0,0 +1,7 @@ +export { RatingItem, renderRatingItem, useRatingItem } from './RatingItem'; +export type { RatingItemSlots, RatingItemProps, RatingItemState } from './RatingItem/RatingItem.types'; + +export { Rating } from './Rating'; +export { renderRating } from './renderRating'; +export { useRating } from './useRating'; +export type { RatingSlots, RatingProps, RatingState } from './Rating.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Rating/renderRating.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Rating/renderRating.tsx new file mode 100644 index 0000000000000..6fdd0e4ba9bf8 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Rating/renderRating.tsx @@ -0,0 +1,6 @@ +import { renderRating_unstable } from '@fluentui/react-rating'; + +/** + * Renders the final JSX of the Rating component, given the state. + */ +export const renderRating = renderRating_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Rating/useRating.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Rating/useRating.tsx new file mode 100644 index 0000000000000..dc4efb6bf5358 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Rating/useRating.tsx @@ -0,0 +1,16 @@ +'use client'; + +import type * as React from 'react'; +import { useRatingBase_unstable } from '@fluentui/react-rating'; + +import type { RatingProps, RatingState } from './Rating.types'; + +/** + * Returns the state for a Rating component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderRating`. + */ +export const useRating = (props: RatingProps, ref: React.Ref): RatingState => { + const state = useRatingBase_unstable(props, ref); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Rating/useRatingContextValues.ts b/packages/react-components/react-headless-components-preview/library/src/components/Rating/useRatingContextValues.ts new file mode 100644 index 0000000000000..e571d10ec4b37 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Rating/useRatingContextValues.ts @@ -0,0 +1,6 @@ +'use client'; + +import { useRatingContextValues as useFieldContextValuesBase_unstable } from '@fluentui/react-rating'; +import type { RatingContextValues, RatingState } from './Rating.types'; + +export const useRatingContextValues = useFieldContextValuesBase_unstable as (state: RatingState) => RatingContextValues; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/RatingDisplay/RatingDisplay.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/RatingDisplay/RatingDisplay.test.tsx new file mode 100644 index 0000000000000..7cbae13514590 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/RatingDisplay/RatingDisplay.test.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { RatingDisplay } from './RatingDisplay'; +import { RatingItem } from '../Rating/RatingItem/RatingItem'; + +describe('RatingDisplay', () => { + isConformant({ + Component: RatingDisplay, + displayName: 'RatingDisplay', + }); + + it('renders a default state', () => { + const { getByRole, getByText } = render( + + + , + ); + const ratingDisplay = getByRole('img'); + + expect(ratingDisplay).toBeInTheDocument(); + expect(getByText('3')).toBeInTheDocument(); + }); +}); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/RatingDisplay/RatingDisplay.tsx b/packages/react-components/react-headless-components-preview/library/src/components/RatingDisplay/RatingDisplay.tsx new file mode 100644 index 0000000000000..fcefbb63fe1b1 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/RatingDisplay/RatingDisplay.tsx @@ -0,0 +1,21 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; + +import type { RatingDisplayProps } from './RatingDisplay.types'; +import { useRatingDisplay } from './useRatingDisplay'; +import { renderRatingDisplay } from './renderRatingDisplay'; +import { useRatingDisplayContextValues } from './useRatingDisplayContextValues'; + +/** + * A rating component for displaying star ratings. + */ +export const RatingDisplay: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useRatingDisplay(props, ref); + const contextValues = useRatingDisplayContextValues(state); + + return renderRatingDisplay(state, contextValues); +}); + +RatingDisplay.displayName = 'RatingDisplay'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/RatingDisplay/RatingDisplay.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/RatingDisplay/RatingDisplay.types.ts new file mode 100644 index 0000000000000..b2cbb50342808 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/RatingDisplay/RatingDisplay.types.ts @@ -0,0 +1,26 @@ +import type { + RatingDisplaySlots as RatingDisplayBaseSlots, + RatingDisplayBaseProps, + RatingDisplayBaseState, + RatingDisplayContextValues as RatingDisplayContextValuesBase, +} from '@fluentui/react-rating'; + +/** + * RatingDisplay component slots + */ +export type RatingDisplaySlots = RatingDisplayBaseSlots; + +/** + * RatingDisplay component props + */ +export type RatingDisplayProps = RatingDisplayBaseProps; + +/** + * RatingDisplay component state + */ +export type RatingDisplayState = RatingDisplayBaseState; + +/** + * RatingDisplay component context values + */ +export type RatingDisplayContextValues = RatingDisplayContextValuesBase; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/RatingDisplay/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/RatingDisplay/index.ts new file mode 100644 index 0000000000000..c9e6c7f33836a --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/RatingDisplay/index.ts @@ -0,0 +1,4 @@ +export { RatingDisplay } from './RatingDisplay'; +export { renderRatingDisplay } from './renderRatingDisplay'; +export { useRatingDisplay } from './useRatingDisplay'; +export type { RatingDisplaySlots, RatingDisplayProps, RatingDisplayState } from './RatingDisplay.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/RatingDisplay/renderRatingDisplay.tsx b/packages/react-components/react-headless-components-preview/library/src/components/RatingDisplay/renderRatingDisplay.tsx new file mode 100644 index 0000000000000..c1962edd399eb --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/RatingDisplay/renderRatingDisplay.tsx @@ -0,0 +1,6 @@ +import { renderRatingDisplay_unstable } from '@fluentui/react-rating'; + +/** + * Renders the final JSX of the RatingDisplay component, given the state. + */ +export const renderRatingDisplay = renderRatingDisplay_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/RatingDisplay/useRatingDisplay.tsx b/packages/react-components/react-headless-components-preview/library/src/components/RatingDisplay/useRatingDisplay.tsx new file mode 100644 index 0000000000000..aad0daa7d71ba --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/RatingDisplay/useRatingDisplay.tsx @@ -0,0 +1,37 @@ +'use client'; + +import * as React from 'react'; +import { useRatingDisplayBase_unstable } from '@fluentui/react-rating'; + +import { RatingItem } from '../Rating'; +import type { RatingDisplayProps, RatingDisplayState } from './RatingDisplay.types'; + +/** + * Returns the state for a RatingDisplay component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderRatingDisplay`. + */ +export const useRatingDisplay = (props: RatingDisplayProps, ref: React.Ref): RatingDisplayState => { + 'use no memo'; + + const state = useRatingDisplayBase_unstable( + { + icon: 'span', + ...props, + }, + ref, + ); + + const { compact, max } = state; + + const rootChildren = React.useMemo(() => { + return compact ? ( + + ) : ( + Array.from(Array(max), (_, i) => ) + ); + }, [compact, max]); + + state.root.children ??= rootChildren; + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/RatingDisplay/useRatingDisplayContextValues.ts b/packages/react-components/react-headless-components-preview/library/src/components/RatingDisplay/useRatingDisplayContextValues.ts new file mode 100644 index 0000000000000..1e9704b8ea892 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/RatingDisplay/useRatingDisplayContextValues.ts @@ -0,0 +1,8 @@ +'use client'; + +import { useRatingDisplayContextValues as useFieldContextValuesBase_unstable } from '@fluentui/react-rating'; +import type { RatingDisplayContextValues, RatingDisplayState } from './RatingDisplay.types'; + +export const useRatingDisplayContextValues = useFieldContextValuesBase_unstable as ( + state: RatingDisplayState, +) => RatingDisplayContextValues; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SearchBox/Search.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/SearchBox/Search.types.ts new file mode 100644 index 0000000000000..dc3b908aaebd0 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SearchBox/Search.types.ts @@ -0,0 +1,32 @@ +import type { + SearchBoxSlots as SearchBoxBaseSlots, + SearchBoxBaseProps, + SearchBoxBaseState, +} from '@fluentui/react-search'; + +/** + * Search component slots + */ +export type SearchBoxSlots = SearchBoxBaseSlots; + +/** + * Search component props + */ +export type SearchBoxProps = SearchBoxBaseProps; + +/** + * Search component state + */ +export type SearchBoxState = SearchBoxBaseState & { + root: { + /** + * Data attribute set when the search box is disabled. + */ + 'data-disabled'?: string; + + /** + * Data attribute set when the search box has focus within. + */ + 'data-focused'?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SearchBox/SearchBox.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/SearchBox/SearchBox.test.tsx new file mode 100644 index 0000000000000..c3cf9ceb39f5b --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SearchBox/SearchBox.test.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { SearchBox } from './SearchBox'; + +describe('SearchBox', () => { + isConformant({ + Component: SearchBox, + displayName: 'SearchBox', + primarySlot: 'input', + }); + + it('renders a default state', () => { + const { getByRole } = render(); + const input = getByRole('searchbox'); + + expect(input).toBeInTheDocument(); + expect(input).toHaveAttribute('placeholder', 'Search...'); + expect(input).toHaveAttribute('type', 'search'); + }); + + it('renders a clear button', () => { + const { getByRole } = render(); + const clearButton = getByRole('button', { name: 'clear' }); + + expect(clearButton).toBeInTheDocument(); + }); + + it('renders with data-disabled when disabled', () => { + const { container } = render(); + const root = container.firstElementChild!; + + expect(root).toHaveAttribute('data-disabled'); + }); +}); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SearchBox/SearchBox.tsx b/packages/react-components/react-headless-components-preview/library/src/components/SearchBox/SearchBox.tsx new file mode 100644 index 0000000000000..611daf7f7a3ee --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SearchBox/SearchBox.tsx @@ -0,0 +1,19 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; + +import type { SearchBoxProps } from './Search.types'; +import { useSearchBox } from './useSearchBox'; +import { renderSearchBox } from './renderSearchBox'; + +/** + * A search box component for search input. + */ +export const SearchBox: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useSearchBox(props, ref); + + return renderSearchBox(state); +}); + +SearchBox.displayName = 'SearchBox'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SearchBox/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/SearchBox/index.ts new file mode 100644 index 0000000000000..dde1ce3b058d2 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SearchBox/index.ts @@ -0,0 +1,4 @@ +export { SearchBox } from './SearchBox'; +export { renderSearchBox } from './renderSearchBox'; +export { useSearchBox as useSearchBox } from './useSearchBox'; +export type { SearchBoxSlots, SearchBoxProps, SearchBoxState } from './Search.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SearchBox/renderSearchBox.tsx b/packages/react-components/react-headless-components-preview/library/src/components/SearchBox/renderSearchBox.tsx new file mode 100644 index 0000000000000..6c17d54d4a6dd --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SearchBox/renderSearchBox.tsx @@ -0,0 +1,6 @@ +import { renderSearchBox_unstable } from '@fluentui/react-search'; + +/** + * Renders the final JSX of the SearchBox component, given the state. + */ +export const renderSearchBox = renderSearchBox_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SearchBox/useSearchBox.ts b/packages/react-components/react-headless-components-preview/library/src/components/SearchBox/useSearchBox.ts new file mode 100644 index 0000000000000..e439502468fbe --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SearchBox/useSearchBox.ts @@ -0,0 +1,23 @@ +'use client'; + +import type * as React from 'react'; +import { useSearchBoxBase_unstable } from '@fluentui/react-search'; + +import type { SearchBoxProps, SearchBoxState } from './Search.types'; +import { stringifyDataAttribute } from '../../utils'; + +/** + * Returns the state for a SearchBox component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderSearchBox`. + */ +export const useSearchBox = (props: SearchBoxProps, ref: React.Ref): SearchBoxState => { + 'use no memo'; + + const state: SearchBoxState = useSearchBoxBase_unstable(props, ref); + + // Set data attributes for disabled and focused states to simplify styling of these states. + state.root['data-disabled'] = stringifyDataAttribute(state.disabled); + state.root['data-focused'] = stringifyDataAttribute(state.focused); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Select/Select.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Select/Select.test.tsx new file mode 100644 index 0000000000000..6b9ba7d9dc39f --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Select/Select.test.tsx @@ -0,0 +1,40 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { Select } from './Select'; + +describe('Select', () => { + isConformant({ + Component: Select, + displayName: 'Select', + primarySlot: 'select', + }); + + it('renders a default state', () => { + const { getByRole } = render( + , + ); + const select = getByRole('combobox') as HTMLSelectElement; + + expect(select).toBeInTheDocument(); + expect(select.options).toHaveLength(3); + expect(select.options[0]).toHaveTextContent('Option 1'); + expect(select.options[1]).toHaveTextContent('Option 2'); + expect(select.options[2]).toHaveTextContent('Option 3'); + }); + + it('renders with data-disabled when disabled', () => { + const { container } = render( + , + ); + const root = container.firstElementChild!; + + expect(root).toHaveAttribute('data-disabled'); + }); +}); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Select/Select.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Select/Select.tsx new file mode 100644 index 0000000000000..ba45cf76cd4d6 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Select/Select.tsx @@ -0,0 +1,19 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; + +import { useSelect } from './useSelect'; +import { renderSelect } from './renderSelect'; +import type { SelectProps } from './Select.types'; + +/** + * Select component - TODO: add more docs + */ +export const Select: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useSelect(props, ref); + + return renderSelect(state); +}); + +Select.displayName = 'Select'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Select/Select.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Select/Select.types.ts new file mode 100644 index 0000000000000..b7d8d09113168 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Select/Select.types.ts @@ -0,0 +1,20 @@ +import type { SelectSlots as SelectBaseSlots, SelectBaseProps, SelectBaseState } from '@fluentui/react-select'; + +export type SelectSlots = SelectBaseSlots; + +/** + * Select Props + */ +export type SelectProps = SelectBaseProps; + +/** + * State used in rendering Select + */ +export type SelectState = SelectBaseState & { + root: { + /** + * Data attribute set when the select is disabled. + */ + 'data-disabled'?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Select/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Select/index.ts new file mode 100644 index 0000000000000..b82efc3cd2c4d --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Select/index.ts @@ -0,0 +1,4 @@ +export { Select } from './Select'; +export type { SelectSlots, SelectProps, SelectState } from './Select.types'; +export { renderSelect } from './renderSelect'; +export { useSelect } from './useSelect'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Select/renderSelect.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Select/renderSelect.tsx new file mode 100644 index 0000000000000..b6de75286f3ec --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Select/renderSelect.tsx @@ -0,0 +1,6 @@ +import { renderSelect_unstable } from '@fluentui/react-select'; + +/** + * Render the final JSX of Select + */ +export const renderSelect = renderSelect_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Select/useSelect.ts b/packages/react-components/react-headless-components-preview/library/src/components/Select/useSelect.ts new file mode 100644 index 0000000000000..15c07b7d072e2 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Select/useSelect.ts @@ -0,0 +1,26 @@ +'use client'; + +import type * as React from 'react'; +import { useSelectBase_unstable } from '@fluentui/react-select'; +import type { SelectProps, SelectState } from './Select.types'; +import { stringifyDataAttribute } from '../../utils'; + +/** + * Create the state required to render Select. + * + * The returned state can be modified with hooks, + * before being passed to renderSelect. + * + * @param props - props from this instance of Select + * @param ref - reference to root HTMLSelectElement + */ +export const useSelect = (props: SelectProps, ref: React.Ref): SelectState => { + 'use no memo'; + + const state: SelectState = useSelectBase_unstable(props, ref); + + // Set data attribute for disabled state to simplify styling. + state.root['data-disabled'] = stringifyDataAttribute(state.select.disabled); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/Skeleton.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/Skeleton.test.tsx new file mode 100644 index 0000000000000..4b98b6c6958bb --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/Skeleton.test.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { Skeleton } from './Skeleton'; + +describe('Skeleton', () => { + isConformant({ + Component: Skeleton, + displayName: 'Skeleton', + }); + + it('renders a default state', () => { + const { getByRole } = render(Default Skeleton); + const skeleton = getByRole('progressbar'); + + expect(skeleton).toBeInTheDocument(); + expect(skeleton).toHaveAttribute('aria-busy', 'true'); + expect(skeleton).toHaveTextContent('Default Skeleton'); + }); +}); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/Skeleton.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/Skeleton.tsx new file mode 100644 index 0000000000000..fbbb2ee923f2a --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/Skeleton.tsx @@ -0,0 +1,18 @@ +'use client'; + +import * as React from 'react'; +import type { SkeletonProps } from './Skeleton.types'; +import { useSkeleton, useSkeletonContextValues } from './useSkeleton'; +import { renderSkeleton } from './renderSkeleton'; + +/** + * A skeleton component for loading placeholders. + */ +export const Skeleton = React.forwardRef((props, ref) => { + const state = useSkeleton(props, ref); + const contextValues = useSkeletonContextValues(state); + + return renderSkeleton(state, contextValues); +}); + +Skeleton.displayName = 'Skeleton'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/Skeleton.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/Skeleton.types.ts new file mode 100644 index 0000000000000..df885847b1ba7 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/Skeleton.types.ts @@ -0,0 +1,26 @@ +import type { + SkeletonSlots as SkeletonBaseSlots, + SkeletonBaseProps, + SkeletonBaseState, + SkeletonContextValues as SkeletonContextValuesBase, +} from '@fluentui/react-skeleton'; + +/** + * Skeleton component slots + */ +export type SkeletonSlots = SkeletonBaseSlots; + +/** + * Skeleton component props + */ +export type SkeletonProps = SkeletonBaseProps; + +/** + * Skeleton component state + */ +export type SkeletonState = SkeletonBaseState; + +/** + * Skeleton component context values + */ +export type SkeletonContextValues = SkeletonContextValuesBase; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/SkeletonItem/SkeletonItem.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/SkeletonItem/SkeletonItem.tsx new file mode 100644 index 0000000000000..33aef85417544 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/SkeletonItem/SkeletonItem.tsx @@ -0,0 +1,17 @@ +'use client'; + +import * as React from 'react'; +import type { SkeletonItemProps } from './SkeletonItem.types'; +import { useSkeletonItem } from './useSkeletonItem'; +import { renderSkeletonItem } from './renderSkeletonItem'; + +/** + * A SkeletonItem component for loading placeholders. + */ +export const SkeletonItem = React.forwardRef((props, ref) => { + const state = useSkeletonItem(props, ref); + + return renderSkeletonItem(state); +}); + +SkeletonItem.displayName = 'SkeletonItem'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/SkeletonItem/SkeletonItem.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/SkeletonItem/SkeletonItem.types.ts new file mode 100644 index 0000000000000..cf31015dff683 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/SkeletonItem/SkeletonItem.types.ts @@ -0,0 +1,20 @@ +import type { + SkeletonItemSlots as SkeletonItemBaseSlots, + SkeletonItemBaseProps, + SkeletonItemBaseState, +} from '@fluentui/react-skeleton'; + +/** + * SkeletonItem component slots + */ +export type SkeletonItemSlots = SkeletonItemBaseSlots; + +/** + * SkeletonItem component props + */ +export type SkeletonItemProps = SkeletonItemBaseProps; + +/** + * SkeletonItem component state + */ +export type SkeletonItemState = SkeletonItemBaseState; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/SkeletonItem/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/SkeletonItem/index.ts new file mode 100644 index 0000000000000..c1f1988e3a8aa --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/SkeletonItem/index.ts @@ -0,0 +1,4 @@ +export { SkeletonItem } from './SkeletonItem'; +export { renderSkeletonItem } from './renderSkeletonItem'; +export { useSkeletonItem } from './useSkeletonItem'; +export type { SkeletonItemSlots, SkeletonItemProps, SkeletonItemState } from './SkeletonItem.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/SkeletonItem/renderSkeletonItem.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/SkeletonItem/renderSkeletonItem.tsx new file mode 100644 index 0000000000000..f99403783efd2 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/SkeletonItem/renderSkeletonItem.tsx @@ -0,0 +1,6 @@ +import { renderSkeletonItem_unstable } from '@fluentui/react-skeleton'; + +/** + * Renders the final JSX of the SkeletonItem component, given the state. + */ +export const renderSkeletonItem = renderSkeletonItem_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/SkeletonItem/useSkeletonItem.ts b/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/SkeletonItem/useSkeletonItem.ts new file mode 100644 index 0000000000000..28822bc8af645 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/SkeletonItem/useSkeletonItem.ts @@ -0,0 +1,16 @@ +'use client'; + +import type * as React from 'react'; +import { useSkeletonItemBase_unstable } from '@fluentui/react-skeleton'; + +import type { SkeletonItemProps, SkeletonItemState } from './SkeletonItem.types'; + +/** + * Returns the state for a SkeletonItem component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderSkeletonItem`. + */ +export const useSkeletonItem = (props: SkeletonItemProps, ref: React.Ref): SkeletonItemState => { + const state = useSkeletonItemBase_unstable(props, ref); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/index.ts new file mode 100644 index 0000000000000..93eb84a1fa39d --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/index.ts @@ -0,0 +1,7 @@ +export { SkeletonItem, renderSkeletonItem, useSkeletonItem } from './SkeletonItem'; +export type { SkeletonItemSlots, SkeletonItemProps, SkeletonItemState } from './SkeletonItem/SkeletonItem.types'; + +export { Skeleton } from './Skeleton'; +export { renderSkeleton } from './renderSkeleton'; +export { useSkeleton } from './useSkeleton'; +export type { SkeletonSlots, SkeletonProps, SkeletonState } from './Skeleton.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/renderSkeleton.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/renderSkeleton.tsx new file mode 100644 index 0000000000000..2bc31745425dc --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/renderSkeleton.tsx @@ -0,0 +1,6 @@ +import { renderSkeleton_unstable } from '@fluentui/react-skeleton'; + +/** + * Renders the final JSX of the Skeleton component, given the state. + */ +export const renderSkeleton = renderSkeleton_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/useSkeleton.ts b/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/useSkeleton.ts new file mode 100644 index 0000000000000..332f9df9c5b70 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Skeleton/useSkeleton.ts @@ -0,0 +1,23 @@ +'use client'; + +import type * as React from 'react'; +import { + useSkeletonBase_unstable, + useSkeletonContextValues as useSkeletonContextValues_unstable, +} from '@fluentui/react-skeleton'; + +import type { SkeletonProps, SkeletonState, SkeletonContextValues } from './Skeleton.types'; + +/** + * Returns the state for a Skeleton component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderSkeleton`. + */ +export const useSkeleton = (props: SkeletonProps, ref: React.Ref): SkeletonState => { + const state = useSkeletonBase_unstable(props, ref); + + return state; +}; + +export const useSkeletonContextValues = useSkeletonContextValues_unstable as ( + state: SkeletonState, +) => SkeletonContextValues; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Slider/Slider.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Slider/Slider.test.tsx new file mode 100644 index 0000000000000..0bab0fdbc4a5b --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Slider/Slider.test.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { Slider } from './Slider'; + +describe('Slider', () => { + isConformant({ + Component: Slider, + displayName: 'Slider', + primarySlot: 'input', + }); + + it('renders a default state', () => { + const { getByRole } = render(); + const slider = getByRole('slider'); + + expect(slider).toBeInTheDocument(); + expect(slider).toHaveAttribute('type', 'range'); + expect(slider).toHaveValue('0.5'); + }); + + it('renders with data-disabled when disabled', () => { + const { container } = render(); + const root = container.firstElementChild!; + + expect(root).toHaveAttribute('data-disabled'); + }); + + it('renders with data-vertical when vertical', () => { + const { container } = render(); + const root = container.firstElementChild!; + + expect(root).toHaveAttribute('data-vertical'); + }); +}); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Slider/Slider.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Slider/Slider.tsx new file mode 100644 index 0000000000000..9f937ac1dc517 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Slider/Slider.tsx @@ -0,0 +1,18 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { SliderProps } from './Slider.types'; +import { useSlider } from './useSlider'; +import { renderSlider } from './renderSlider'; + +/** + * A slider component for selecting a value in a range. + */ +export const Slider: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useSlider(props, ref); + + return renderSlider(state); +}); + +Slider.displayName = 'Slider'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Slider/Slider.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Slider/Slider.types.ts new file mode 100644 index 0000000000000..eb7b063f2c12a --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Slider/Slider.types.ts @@ -0,0 +1,28 @@ +import type { SliderSlots as SliderBaseSlots, SliderBaseProps, SliderBaseState } from '@fluentui/react-slider'; + +/** + * Slider component slots + */ +export type SliderSlots = SliderBaseSlots; + +/** + * Slider component props + */ +export type SliderProps = SliderBaseProps; + +/** + * Slider component state + */ +export type SliderState = SliderBaseState & { + root: { + /** + * Data attribute set when the slider is disabled. + */ + 'data-disabled'?: string; + + /** + * Data attribute set when the slider is oriented vertically. + */ + 'data-vertical'?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Slider/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Slider/index.ts new file mode 100644 index 0000000000000..679a67e73c73f --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Slider/index.ts @@ -0,0 +1,4 @@ +export { Slider } from './Slider'; +export { renderSlider } from './renderSlider'; +export { useSlider } from './useSlider'; +export type { SliderSlots, SliderProps, SliderState } from './Slider.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Slider/renderSlider.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Slider/renderSlider.tsx new file mode 100644 index 0000000000000..93f9cf3a20971 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Slider/renderSlider.tsx @@ -0,0 +1,6 @@ +import { renderSlider_unstable } from '@fluentui/react-slider'; + +/** + * Renders the final JSX of the Slider component, given the state. + */ +export const renderSlider = renderSlider_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Slider/useSlider.ts b/packages/react-components/react-headless-components-preview/library/src/components/Slider/useSlider.ts new file mode 100644 index 0000000000000..3f71e8809f418 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Slider/useSlider.ts @@ -0,0 +1,23 @@ +'use client'; + +import type * as React from 'react'; +import { useSliderBase_unstable } from '@fluentui/react-slider'; + +import type { SliderProps, SliderState } from './Slider.types'; +import { stringifyDataAttribute } from '../../utils'; + +/** + * Returns the state for a Slider component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderSlider`. + */ +export const useSlider = (props: SliderProps, ref: React.Ref): SliderState => { + 'use no memo'; + + const state: SliderState = useSliderBase_unstable(props, ref); + + // Set data attributes for disabled and vertical states to simplify styling of these states. + state.root['data-disabled'] = stringifyDataAttribute(state.disabled); + state.root['data-vertical'] = stringifyDataAttribute(state.vertical); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SpinButton/SpinButton.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/SpinButton/SpinButton.test.tsx new file mode 100644 index 0000000000000..fbc499f008a3a --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SpinButton/SpinButton.test.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { SpinButton } from './SpinButton'; + +describe('SpinButton', () => { + isConformant({ + Component: SpinButton, + displayName: 'SpinButton', + primarySlot: 'input', + }); + + it('renders a default state', () => { + const { getByRole, getByLabelText } = render(); + const spinbutton = getByRole('spinbutton'); + + expect(spinbutton).toBeInTheDocument(); + expect(spinbutton).toHaveAttribute('aria-valuemin', '0'); + expect(spinbutton).toHaveAttribute('aria-valuemax', '10'); + expect(spinbutton).toHaveAttribute('aria-valuenow', '1'); + expect(spinbutton).toHaveValue('1'); + expect(getByLabelText('Increment value')).toBeInTheDocument(); + expect(getByLabelText('Decrement value')).toBeInTheDocument(); + }); + + it('renders with data-at-bound when at min value', () => { + const { container } = render(); + const root = container.firstElementChild!; + + expect(root).toHaveAttribute('data-at-bound', 'min'); + }); + + it('renders with data-at-bound when at max value', () => { + const { container } = render(); + const root = container.firstElementChild!; + + expect(root).toHaveAttribute('data-at-bound', 'max'); + }); +}); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SpinButton/SpinButton.tsx b/packages/react-components/react-headless-components-preview/library/src/components/SpinButton/SpinButton.tsx new file mode 100644 index 0000000000000..47b9c280b601c --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SpinButton/SpinButton.tsx @@ -0,0 +1,18 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { SpinButtonProps } from './SpinButton.types'; +import { useSpinButton } from './useSpinButton'; +import { renderSpinButton } from './renderSpinButton'; + +/** + * A spin button component for incrementing/decrementing values. + */ +export const SpinButton: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useSpinButton(props, ref); + + return renderSpinButton(state); +}); + +SpinButton.displayName = 'SpinButton'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SpinButton/SpinButton.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/SpinButton/SpinButton.types.ts new file mode 100644 index 0000000000000..e5e05a7e88bff --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SpinButton/SpinButton.types.ts @@ -0,0 +1,37 @@ +import type { + SpinButtonSlots as SpinButtonBaseSlots, + SpinButtonBaseProps, + SpinButtonBaseState, +} from '@fluentui/react-spinbutton'; + +/** + * SpinButton component slots + */ +export type SpinButtonSlots = SpinButtonBaseSlots; + +/** + * SpinButton component props + */ +export type SpinButtonProps = SpinButtonBaseProps; + +/** + * SpinButton component state + */ +export type SpinButtonState = SpinButtonBaseState & { + root: { + /** + * Data attribute set when the spin button is disabled. + */ + 'data-disabled'?: string; + + /** + * Data attribute set when the spin button is actively spinning. Value is 'up' or 'down'. + */ + 'data-spin-state'?: string; + + /** + * Data attribute set when the value is at a range boundary. Value is 'min', 'max', or 'both'. + */ + 'data-at-bound'?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SpinButton/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/SpinButton/index.ts new file mode 100644 index 0000000000000..c6acde55db924 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SpinButton/index.ts @@ -0,0 +1,4 @@ +export { SpinButton } from './SpinButton'; +export { renderSpinButton } from './renderSpinButton'; +export { useSpinButton } from './useSpinButton'; +export type { SpinButtonSlots, SpinButtonProps, SpinButtonState } from './SpinButton.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SpinButton/renderSpinButton.tsx b/packages/react-components/react-headless-components-preview/library/src/components/SpinButton/renderSpinButton.tsx new file mode 100644 index 0000000000000..e20dc034670f9 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SpinButton/renderSpinButton.tsx @@ -0,0 +1,6 @@ +import { renderSpinButton_unstable } from '@fluentui/react-spinbutton'; + +/** + * Renders the final JSX of the SpinButton component, given the state. + */ +export const renderSpinButton = renderSpinButton_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/SpinButton/useSpinButton.ts b/packages/react-components/react-headless-components-preview/library/src/components/SpinButton/useSpinButton.ts new file mode 100644 index 0000000000000..7ef42b11f8177 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/SpinButton/useSpinButton.ts @@ -0,0 +1,24 @@ +'use client'; + +import type * as React from 'react'; +import { useSpinButtonBase_unstable } from '@fluentui/react-spinbutton'; + +import type { SpinButtonProps, SpinButtonState } from './SpinButton.types'; +import { stringifyDataAttribute } from '../../utils'; + +/** + * Returns the state for a SpinButton component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderSpinButton`. + */ +export const useSpinButton = (props: SpinButtonProps, ref: React.Ref): SpinButtonState => { + 'use no memo'; + + const state: SpinButtonState = useSpinButtonBase_unstable(props, ref); + + // Set data attributes for disabled, spin direction, and bound states to simplify styling. + state.root['data-disabled'] = stringifyDataAttribute(state.input.disabled); + state.root['data-spin-state'] = state.spinState !== 'rest' ? state.spinState : undefined; + state.root['data-at-bound'] = state.atBound !== 'none' ? state.atBound : undefined; + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Spinner/Spinner.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Spinner/Spinner.test.tsx new file mode 100644 index 0000000000000..7e15dfb3cc437 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Spinner/Spinner.test.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { Spinner } from './Spinner'; + +describe('Spinner', () => { + isConformant({ + Component: Spinner, + displayName: 'Spinner', + }); + + it('renders a default state', () => { + const { getByRole } = render(Default Spinner); + const spinner = getByRole('progressbar'); + + expect(spinner).toBeInTheDocument(); + expect(spinner).toHaveAttribute('data-label-position', 'after'); + }); + + it('renders with label position "before"', () => { + const { getByRole } = render(Loading); + const spinner = getByRole('progressbar'); + + expect(spinner).toHaveAttribute('data-label-position', 'before'); + }); +}); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Spinner/Spinner.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Spinner/Spinner.tsx new file mode 100644 index 0000000000000..1d44449ade26e --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Spinner/Spinner.tsx @@ -0,0 +1,18 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { SpinnerProps } from './Spinner.types'; +import { useSpinner } from './useSpinner'; +import { renderSpinner } from './renderSpinner'; + +/** + * A spinner component for loading indicators. + */ +export const Spinner: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useSpinner(props, ref); + + return renderSpinner(state); +}); + +Spinner.displayName = 'Spinner'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Spinner/Spinner.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Spinner/Spinner.types.ts new file mode 100644 index 0000000000000..fd206d46aa7b1 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Spinner/Spinner.types.ts @@ -0,0 +1,23 @@ +import type { SpinnerSlots as SpinnerBaseSlots, SpinnerBaseProps, SpinnerBaseState } from '@fluentui/react-spinner'; + +/** + * Spinner component slots + */ +export type SpinnerSlots = SpinnerBaseSlots; + +/** + * Spinner component props + */ +export type SpinnerProps = SpinnerBaseProps; + +/** + * Spinner component state + */ +export type SpinnerState = SpinnerBaseState & { + root: { + /** + * Data attribute reflecting the position of the label when a label slot is present. Value is 'before', 'after', 'above', or 'below'. + */ + 'data-label-position'?: 'before' | 'after' | 'above' | 'below'; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Spinner/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Spinner/index.ts new file mode 100644 index 0000000000000..7c45ff5910c02 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Spinner/index.ts @@ -0,0 +1,4 @@ +export { Spinner } from './Spinner'; +export { renderSpinner } from './renderSpinner'; +export { useSpinner } from './useSpinner'; +export type { SpinnerSlots, SpinnerProps, SpinnerState } from './Spinner.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Spinner/renderSpinner.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Spinner/renderSpinner.tsx new file mode 100644 index 0000000000000..d0a7b1ecf8615 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Spinner/renderSpinner.tsx @@ -0,0 +1,6 @@ +import { renderSpinner_unstable } from '@fluentui/react-spinner'; + +/** + * Renders the final JSX of the Spinner component, given the state. + */ +export const renderSpinner = renderSpinner_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Spinner/useSpinner.ts b/packages/react-components/react-headless-components-preview/library/src/components/Spinner/useSpinner.ts new file mode 100644 index 0000000000000..a4ff2859b3598 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Spinner/useSpinner.ts @@ -0,0 +1,20 @@ +'use client'; + +import type * as React from 'react'; +import { useSpinnerBase_unstable } from '@fluentui/react-spinner'; + +import type { SpinnerProps, SpinnerState } from './Spinner.types'; + +/** + * Returns the state for a Spinner component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderSpinner`. + */ +export const useSpinner = (props: SpinnerProps, ref: React.Ref): SpinnerState => { + 'use no memo'; + + const state: SpinnerState = useSpinnerBase_unstable(props, ref); + + state.root['data-label-position'] = state.labelPosition; + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Switch/Switch.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Switch/Switch.test.tsx new file mode 100644 index 0000000000000..2553afa0002a4 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Switch/Switch.test.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { Switch } from './Switch'; + +describe('Switch', () => { + isConformant({ + Component: Switch, + displayName: 'Switch', + primarySlot: 'input', + }); + + it('renders a default state', () => { + const { getByRole, getByText } = render(); + const switchInput = getByRole('switch'); + + expect(switchInput).toBeInTheDocument(); + expect(switchInput).toBeChecked(); + expect(getByText('Default Switch')).toBeInTheDocument(); + }); + + it('renders with data-disabled when disabled', () => { + const { container } = render(); + const root = container.firstElementChild!; + + expect(root).toHaveAttribute('data-disabled'); + }); + + it('renders with data-disabled-focusable when disabledFocusable', () => { + const { container } = render(); + const root = container.firstElementChild!; + + expect(root).toHaveAttribute('data-disabled'); + expect(root).toHaveAttribute('data-disabled-focusable'); + }); + + it('renders with data-checked when checked', () => { + const { container } = render(); + const root = container.firstElementChild!; + + expect(root).toHaveAttribute('data-checked'); + }); +}); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Switch/Switch.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Switch/Switch.tsx new file mode 100644 index 0000000000000..4bf8930f48ee9 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Switch/Switch.tsx @@ -0,0 +1,18 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { SwitchProps } from './Switch.types'; +import { useSwitch } from './useSwitch'; +import { renderSwitch } from './renderSwitch'; + +/** + * A switch component for toggling values. + */ +export const Switch: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useSwitch(props, ref); + + return renderSwitch(state); +}); + +Switch.displayName = 'Switch'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Switch/Switch.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Switch/Switch.types.ts new file mode 100644 index 0000000000000..da658aea50416 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Switch/Switch.types.ts @@ -0,0 +1,33 @@ +import type { SwitchSlots as SwitchBaseSlots, SwitchBaseProps, SwitchBaseState } from '@fluentui/react-switch'; + +/** + * Switch component slots + */ +export type SwitchSlots = SwitchBaseSlots; + +/** + * Switch component props + */ +export type SwitchProps = SwitchBaseProps; + +/** + * Switch component state + */ +export type SwitchState = SwitchBaseState & { + root: { + /** + * Data attribute set when the switch is disabled. + */ + 'data-disabled'?: string; + + /** + * Data attribute set when the switch is disabled but still focusable. + */ + 'data-disabled-focusable'?: string; + + /** + * Data attribute set when the switch is checked (controlled mode only). + */ + 'data-checked'?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Switch/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Switch/index.ts new file mode 100644 index 0000000000000..5204770be9dd6 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Switch/index.ts @@ -0,0 +1,4 @@ +export { Switch } from './Switch'; +export { renderSwitch } from './renderSwitch'; +export { useSwitch } from './useSwitch'; +export type { SwitchSlots, SwitchProps, SwitchState } from './Switch.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Switch/renderSwitch.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Switch/renderSwitch.tsx new file mode 100644 index 0000000000000..9fd8c9e984fa8 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Switch/renderSwitch.tsx @@ -0,0 +1,6 @@ +import { renderSwitch_unstable } from '@fluentui/react-switch'; + +/** + * Renders the final JSX of the Switch component, given the state. + */ +export const renderSwitch = renderSwitch_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Switch/useSwitch.ts b/packages/react-components/react-headless-components-preview/library/src/components/Switch/useSwitch.ts new file mode 100644 index 0000000000000..3c824c4b51fe0 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Switch/useSwitch.ts @@ -0,0 +1,24 @@ +'use client'; + +import type * as React from 'react'; +import { useSwitchBase_unstable } from '@fluentui/react-switch'; + +import type { SwitchProps, SwitchState } from './Switch.types'; +import { stringifyDataAttribute } from '../../utils'; + +/** + * Returns the state for a Switch component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderSwitch`. + */ +export const useSwitch = (props: SwitchProps, ref: React.Ref): SwitchState => { + 'use no memo'; + + const state: SwitchState = useSwitchBase_unstable(props, ref); + + // Set data attributes for disabled, disabledFocusable, and checked states to simplify styling. + state.root['data-disabled'] = stringifyDataAttribute(state.input.disabled || state.disabledFocusable); + state.root['data-disabled-focusable'] = stringifyDataAttribute(state.disabledFocusable); + state.root['data-checked'] = stringifyDataAttribute(state.input.checked); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/TabList/Tab/Tab.tsx b/packages/react-components/react-headless-components-preview/library/src/components/TabList/Tab/Tab.tsx new file mode 100644 index 0000000000000..fa044bbfff0b2 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/TabList/Tab/Tab.tsx @@ -0,0 +1,18 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { TabProps } from './Tab.types'; +import { useTab } from './useTab'; +import { renderTab } from './renderTab'; + +/** + * A tab component for organizing content. + */ +export const Tab: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useTab(props, ref); + + return renderTab(state); +}); + +Tab.displayName = 'Tab'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/TabList/Tab/Tab.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/TabList/Tab/Tab.types.ts new file mode 100644 index 0000000000000..72b41ffc51e2a --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/TabList/Tab/Tab.types.ts @@ -0,0 +1,13 @@ +export type { TabSlots, TabValue } from '@fluentui/react-tabs'; + +import type { TabBaseProps, TabBaseState } from '@fluentui/react-tabs'; + +export type TabProps = TabBaseProps; + +export type TabState = TabBaseState & { + root: { + focusgroupstart?: string; + 'data-icon-only'?: string; + 'data-selected'?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/TabList/Tab/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/TabList/Tab/index.ts new file mode 100644 index 0000000000000..08ead275c3335 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/TabList/Tab/index.ts @@ -0,0 +1,4 @@ +export type { TabSlots, TabValue, TabProps, TabState } from './Tab.types'; +export { Tab } from './Tab'; +export { renderTab } from './renderTab'; +export { useTab } from './useTab'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/TabList/Tab/renderTab.tsx b/packages/react-components/react-headless-components-preview/library/src/components/TabList/Tab/renderTab.tsx new file mode 100644 index 0000000000000..68322a8d4542e --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/TabList/Tab/renderTab.tsx @@ -0,0 +1,6 @@ +import { renderTab_unstable } from '@fluentui/react-tabs'; + +/** + * Renders the final JSX of the Tab component, given the state. + */ +export const renderTab = renderTab_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/TabList/Tab/useTab.ts b/packages/react-components/react-headless-components-preview/library/src/components/TabList/Tab/useTab.ts new file mode 100644 index 0000000000000..0b19cb6433db3 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/TabList/Tab/useTab.ts @@ -0,0 +1,23 @@ +'use client'; + +import type * as React from 'react'; +import { useTabBase_unstable } from '@fluentui/react-tabs'; + +import type { TabProps, TabState } from './Tab.types'; +import { stringifyDataAttribute } from '../../../utils'; + +/** + * Returns the state for a Tab component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderTab`. + */ +export const useTab = (props: TabProps, ref: React.Ref): TabState => { + 'use no memo'; + + const state: TabState = useTabBase_unstable(props, ref); + + state.root.focusgroupstart = stringifyDataAttribute(state.selected); + state.root['data-icon-only'] = stringifyDataAttribute(state.iconOnly); + state.root['data-selected'] = stringifyDataAttribute(state.selected); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/TabList/TabList.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/TabList/TabList.test.tsx new file mode 100644 index 0000000000000..dadc54e7f2fbc --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/TabList/TabList.test.tsx @@ -0,0 +1,46 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { TabList } from './TabList'; +import { Tab } from './Tab'; + +describe('TabList', () => { + isConformant({ + Component: TabList, + displayName: 'TabList', + }); + + it('renders a default state', () => { + const { getByRole, getAllByRole } = render( + + Tab 1 + Tab 2 + Tab 3 + , + ); + const tablist = getByRole('tablist'); + + expect(tablist).toBeInTheDocument(); + expect(tablist).toHaveAttribute('data-orientation', 'horizontal'); + expect(tablist).toHaveAttribute('focusgroup', 'tablist inline wrap no-memory'); + + const tabs = getAllByRole('tab'); + + expect(tabs).toHaveLength(3); + expect(tabs[0]).toHaveAttribute('data-selected'); + }); + + it('renders with vertical orientation', () => { + const { getByRole } = render( + + Tab 1 + Tab 2 + Tab 3 + , + ); + const tablist = getByRole('tablist'); + + expect(tablist).toHaveAttribute('data-orientation', 'vertical'); + expect(tablist).toHaveAttribute('focusgroup', 'tablist block wrap no-memory'); + }); +}); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/TabList/TabList.tsx b/packages/react-components/react-headless-components-preview/library/src/components/TabList/TabList.tsx new file mode 100644 index 0000000000000..d5fbd91a6b810 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/TabList/TabList.tsx @@ -0,0 +1,20 @@ +'use client'; + +import * as React from 'react'; +import type { ForwardRefComponent } from '@fluentui/react-utilities'; +import type { TabListProps } from './TabList.types'; +import { useTabList } from './useTabList'; +import { useTabListContextValues } from './useTabListContextValues'; +import { renderTabList } from './renderTabList'; + +/** + * A tab list component for organizing content. + */ +export const TabList: ForwardRefComponent = React.forwardRef((props, ref) => { + const state = useTabList(props, ref); + const contextValues = useTabListContextValues(state); + + return renderTabList(state, contextValues); +}); + +TabList.displayName = 'TabList'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/TabList/TabList.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/TabList/TabList.types.ts new file mode 100644 index 0000000000000..d02dcc4c429ad --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/TabList/TabList.types.ts @@ -0,0 +1,34 @@ +import type { + TabListSlots as TabListBaseSlots, + TabListBaseProps, + TabListBaseState, + TabListContextValues as TabListContextValuesBase, +} from '@fluentui/react-tabs'; + +/** + * TabList component slots + */ +export type TabListSlots = TabListBaseSlots; + +/** + * TabList component props + */ +export type TabListProps = TabListBaseProps; + +/** + * TabList component state + */ +export type TabListState = TabListBaseState & { + root: { + focusgroup?: string; + /** + * Data attribute set to reflect the orientation of the tab list. Value is 'vertical' or 'horizontal'. + */ + 'data-orientation'?: 'vertical' | 'horizontal'; + }; +}; + +/** + * TabList component context values + */ +export type TabListContextValues = TabListContextValuesBase; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/TabList/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/TabList/index.ts new file mode 100644 index 0000000000000..6b35fd5593fd0 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/TabList/index.ts @@ -0,0 +1,7 @@ +export type { TabProps, TabValue, TabSlots, TabState } from './Tab'; +export { Tab, renderTab, useTab } from './Tab'; + +export { TabList } from './TabList'; +export { renderTabList } from './renderTabList'; +export { useTabList } from './useTabList'; +export type { TabListSlots, TabListProps, TabListState } from './TabList.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/TabList/renderTabList.tsx b/packages/react-components/react-headless-components-preview/library/src/components/TabList/renderTabList.tsx new file mode 100644 index 0000000000000..9d601f590a86d --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/TabList/renderTabList.tsx @@ -0,0 +1,6 @@ +import { renderTabList_unstable } from '@fluentui/react-tabs'; + +/** + * Renders the final JSX of the TabList component, given the state. + */ +export const renderTabList = renderTabList_unstable; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/TabList/useTabList.ts b/packages/react-components/react-headless-components-preview/library/src/components/TabList/useTabList.ts new file mode 100644 index 0000000000000..73faa95eaebd7 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/TabList/useTabList.ts @@ -0,0 +1,21 @@ +'use client'; + +import type * as React from 'react'; +import { useTabListBase_unstable } from '@fluentui/react-tabs'; + +import type { TabListProps, TabListState } from './TabList.types'; + +/** + * Returns the state for a TabList component, given its props and ref. + * The returned state can be modified with hooks before being passed to `renderTabList`. + */ +export const useTabList = (props: TabListProps, ref: React.Ref): TabListState => { + 'use no memo'; + + const state: TabListState = useTabListBase_unstable(props, ref); + + state.root.focusgroup = state.vertical ? 'tablist block wrap no-memory' : 'tablist inline wrap no-memory'; + state.root['data-orientation'] = state.vertical ? 'vertical' : 'horizontal'; + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/TabList/useTabListContextValues.ts b/packages/react-components/react-headless-components-preview/library/src/components/TabList/useTabListContextValues.ts new file mode 100644 index 0000000000000..f896b23be18a8 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/TabList/useTabListContextValues.ts @@ -0,0 +1,9 @@ +'use client'; + +import { useTabListContextValues_unstable } from '@fluentui/react-tabs'; + +import type { TabListContextValues, TabListState } from './TabList.types'; + +export const useTabListContextValues = useTabListContextValues_unstable as ( + state: TabListState, +) => TabListContextValues; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Textarea/Textarea.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Textarea/Textarea.test.tsx new file mode 100644 index 0000000000000..98060c0f028ab --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Textarea/Textarea.test.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { isConformant } from '../../testing/isConformant'; +import { Textarea } from './Textarea'; + +describe('Textarea', () => { + isConformant({ + Component: Textarea, + displayName: 'Textarea', + primarySlot: 'textarea', + }); + + it('renders a default state', () => { + const { getByRole } = render(