diff --git a/change/@fluentui-react-headless-components-preview-c80196b7-476b-4a8e-8501-c7ddf249a46c.json b/change/@fluentui-react-headless-components-preview-c80196b7-476b-4a8e-8501-c7ddf249a46c.json new file mode 100644 index 0000000000000..8376c244e3499 --- /dev/null +++ b/change/@fluentui-react-headless-components-preview-c80196b7-476b-4a8e-8501-c7ddf249a46c.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "feat: expose missing resolved state through additive data attributes", + "packageName": "@fluentui/react-headless-components-preview", + "email": "vgenaev@gmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-headless-components-preview/library/README.md b/packages/react-components/react-headless-components-preview/library/README.md index b7b4ec1271421..147516f56750c 100644 --- a/packages/react-components/react-headless-components-preview/library/README.md +++ b/packages/react-components/react-headless-components-preview/library/README.md @@ -5,3 +5,9 @@ > [!WARNING] > **This package is in preview and not production-ready.** APIs may change without notice before final release. **Do not use in production.** > > This package exposes unstyled, headless Fluent UI v9 primitives for teams building custom design systems. For most teams, [`@fluentui/react-components`](https://www.npmjs.com/package/@fluentui/react-components) remains the recommended default. + +## State selector contract + +Headless components expose resolved interaction and structural state through `data-*` attributes. Consumers that wrap these components, including visual-system packages, must preserve the attributes on the slot where headless emits them. + +Existing attributes retain their current names and value semantics. Visual axes such as appearance, size, and shape are intentionally not part of the headless contract. diff --git a/packages/react-components/react-headless-components-preview/library/etc/avatar-group.api.md b/packages/react-components/react-headless-components-preview/library/etc/avatar-group.api.md index 810e9414dd8a6..aca2977469329 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/avatar-group.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/avatar-group.api.md @@ -4,6 +4,7 @@ ```ts +import type { AvatarGroupBaseState } from '@fluentui/react-avatar'; import { AvatarGroupContextValue } from '@fluentui/react-avatar'; import { AvatarGroupContextValues } from '@fluentui/react-avatar'; import type { AvatarGroupItemBaseProps } from '@fluentui/react-avatar'; @@ -12,7 +13,6 @@ import { AvatarGroupItemBaseState as AvatarGroupItemState } from '@fluentui/reac import { AvatarGroupBaseProps as AvatarGroupProps } from '@fluentui/react-avatar'; import type { AvatarGroupProps as AvatarGroupProps_2 } from '@fluentui/react-avatar'; import { AvatarGroupSlots } from '@fluentui/react-avatar'; -import { AvatarGroupBaseState as AvatarGroupState } from '@fluentui/react-avatar'; import type { ComponentProps } from '@fluentui/react-utilities'; import type { ComponentState } from '@fluentui/react-utilities'; import type { EventData } from '@fluentui/react-utilities'; @@ -81,7 +81,12 @@ export { AvatarGroupProps } export { AvatarGroupSlots } -export { AvatarGroupState } +// @public (undocumented) +export type AvatarGroupState = AvatarGroupBaseState & { + root: { + 'data-layout'?: AvatarGroupBaseState['layout']; + }; +}; export { PartitionAvatarGroupItems } @@ -97,7 +102,7 @@ export { renderAvatarGroupItem } export const renderAvatarGroupPopover: (state: AvatarGroupPopoverState, contextValues: AvatarGroupContextValues) => JSXElement; // @public -export const useAvatarGroup: (props: AvatarGroupProps, ref: React_2.Ref) => AvatarGroupState; +export const useAvatarGroup: (props: AvatarGroupProps, ref: React_2.Ref) => AvatarGroupState; // @public export const useAvatarGroupContextValues: (state: AvatarGroupState) => AvatarGroupContextValues; diff --git a/packages/react-components/react-headless-components-preview/library/etc/button.api.md b/packages/react-components/react-headless-components-preview/library/etc/button.api.md index 0d755b0928738..712b72bf72526 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/button.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/button.api.md @@ -26,6 +26,7 @@ export type ButtonState = ButtonBaseState & { 'data-disabled'?: string; 'data-disabled-focusable'?: string; 'data-icon-only'?: string; + 'data-icon-position'?: ButtonBaseState['iconPosition']; }; }; diff --git a/packages/react-components/react-headless-components-preview/library/etc/card.api.md b/packages/react-components/react-headless-components-preview/library/etc/card.api.md index a5336b417936e..5afeed5312bd8 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/card.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/card.api.md @@ -78,6 +78,7 @@ export type CardSlots = CardSlots_2; export type CardState = CardBaseState & { root: { 'data-selected'?: string; + 'data-disabled'?: string; }; }; diff --git a/packages/react-components/react-headless-components-preview/library/etc/checkbox.api.md b/packages/react-components/react-headless-components-preview/library/etc/checkbox.api.md index eece743a9e8e8..5f89b3ac84a7e 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/checkbox.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/checkbox.api.md @@ -25,6 +25,7 @@ export type CheckboxState = CheckboxBaseState & { root: { 'data-disabled'?: string; 'data-checked'?: string; + 'data-label-position'?: CheckboxBaseState['labelPosition']; }; }; diff --git a/packages/react-components/react-headless-components-preview/library/etc/combobox.api.md b/packages/react-components/react-headless-components-preview/library/etc/combobox.api.md index 0779a00228edb..18ccb1784a0c8 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/combobox.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/combobox.api.md @@ -84,6 +84,7 @@ export type OptionState = OptionState_2 & { root: { 'data-disabled'?: string; 'data-selected'?: string; + 'data-multiselect'?: string; }; }; diff --git a/packages/react-components/react-headless-components-preview/library/etc/dropdown.api.md b/packages/react-components/react-headless-components-preview/library/etc/dropdown.api.md index b3a68344f16cf..18c863047f9a3 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/dropdown.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/dropdown.api.md @@ -81,6 +81,7 @@ export type OptionState = OptionState_2 & { root: { 'data-disabled'?: string; 'data-selected'?: string; + 'data-multiselect'?: string; }; }; diff --git a/packages/react-components/react-headless-components-preview/library/etc/info-label.api.md b/packages/react-components/react-headless-components-preview/library/etc/info-label.api.md index 681765d08fd2a..17ee3d84079d3 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/info-label.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/info-label.api.md @@ -31,7 +31,11 @@ export type InfoButtonSlots = { }; // @public -export type InfoButtonState = ComponentState; +export type InfoButtonState = ComponentState & { + root: { + 'data-open'?: string; + }; +}; // @public export const InfoLabel: ForwardRefComponent; diff --git a/packages/react-components/react-headless-components-preview/library/etc/input.api.md b/packages/react-components/react-headless-components-preview/library/etc/input.api.md index 7bd9fccb68ca8..c4401b2c2b1ac 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/input.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/input.api.md @@ -24,6 +24,7 @@ export type InputSlots = InputSlots_2; export type InputState = InputBaseState & { root: { 'data-disabled'?: string; + 'data-invalid'?: string; }; }; diff --git a/packages/react-components/react-headless-components-preview/library/etc/label.api.md b/packages/react-components/react-headless-components-preview/library/etc/label.api.md index 19c334aa2a65c..079daca808ba5 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/label.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/label.api.md @@ -24,6 +24,7 @@ export type LabelSlots = LabelSlots_2; export type LabelState = LabelBaseState & { root: { 'data-disabled'?: string; + 'data-required'?: string; }; }; diff --git a/packages/react-components/react-headless-components-preview/library/etc/menu-button.api.md b/packages/react-components/react-headless-components-preview/library/etc/menu-button.api.md index 458f8dcc0c3ce..e4af1304f3939 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/menu-button.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/menu-button.api.md @@ -24,6 +24,7 @@ export type MenuButtonState = MenuButtonBaseState & { 'data-disabled'?: string; 'data-disabled-focusable'?: string; 'data-icon-only'?: string; + 'data-open'?: string; }; }; diff --git a/packages/react-components/react-headless-components-preview/library/etc/menu.api.md b/packages/react-components/react-headless-components-preview/library/etc/menu.api.md index c2ea9c8cf1b68..0035a143129f4 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/menu.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/menu.api.md @@ -24,8 +24,8 @@ import { MenuItemLinkProps } from '@fluentui/react-menu'; import { MenuItemLinkSlots } from '@fluentui/react-menu'; import { MenuItemLinkState } from '@fluentui/react-menu'; import type { MenuItemProps as MenuItemProps_2 } from '@fluentui/react-menu'; -import { MenuItemRadioBaseProps as MenuItemRadioProps } from '@fluentui/react-menu'; -import { MenuItemRadioBaseState as MenuItemRadioState } from '@fluentui/react-menu'; +import type { MenuItemRadioBaseProps } from '@fluentui/react-menu'; +import type { MenuItemRadioBaseState } from '@fluentui/react-menu'; import { MenuItemSlots } from '@fluentui/react-menu'; import type { MenuItemState as MenuItemState_2 } from '@fluentui/react-menu'; import { MenuItemSwitchProps } from '@fluentui/react-menu'; @@ -64,10 +64,6 @@ import { useMenuDivider_unstable as useMenuDivider } from '@fluentui/react-menu' import { useMenuGroup_unstable as useMenuGroup } from '@fluentui/react-menu'; import { useMenuGroupContextValues_unstable as useMenuGroupContextValues } from '@fluentui/react-menu'; import { useMenuGroupHeader_unstable as useMenuGroupHeader } from '@fluentui/react-menu'; -import { useMenuItemCheckboxBase_unstable as useMenuItemCheckbox } from '@fluentui/react-menu'; -import { useMenuItemLinkBase_unstable as useMenuItemLink } from '@fluentui/react-menu'; -import { useMenuItemRadioBase_unstable as useMenuItemRadio } from '@fluentui/react-menu'; -import { useMenuItemSwitchBase_unstable as useMenuItemSwitch } from '@fluentui/react-menu'; import { useMenuListContextValues_unstable as useMenuListContextValues } from '@fluentui/react-menu'; // @public @@ -113,14 +109,14 @@ export { MenuGroupState } export const MenuItem: ForwardRefComponent; // @public -export const MenuItemCheckbox: ForwardRefComponent; +export const MenuItemCheckbox: ForwardRefComponent; export { MenuItemCheckboxProps } export { MenuItemCheckboxState } // @public -export const MenuItemLink: ForwardRefComponent; +export const MenuItemLink: ForwardRefComponent; export { MenuItemLinkProps } @@ -134,9 +130,18 @@ export type MenuItemProps = MenuItemProps_2; // @public export const MenuItemRadio: ForwardRefComponent; -export { MenuItemRadioProps } +// @public (undocumented) +export type MenuItemRadioProps = MenuItemRadioBaseProps; -export { MenuItemRadioState } +// @public (undocumented) +export type MenuItemRadioState = MenuItemRadioBaseState & { + root: { + 'data-disabled'?: string; + 'data-has-submenu'?: string; + 'data-submenu-open'?: string; + 'data-checked'?: string; + }; +}; export { MenuItemSlots } @@ -144,11 +149,14 @@ export { MenuItemSlots } export type MenuItemState = MenuItemState_2 & { root: { focusgroupstart?: string; + 'data-disabled'?: string; + 'data-has-submenu'?: string; + 'data-submenu-open'?: string; }; }; // @public -export const MenuItemSwitch: ForwardRefComponent; +export const MenuItemSwitch: ForwardRefComponent; export { MenuItemSwitchProps } @@ -254,13 +262,17 @@ export { useMenuGroupHeader } // @public export const useMenuItem: (props: MenuItemProps, ref: React_2.Ref>) => MenuItemState; -export { useMenuItemCheckbox } +// @public (undocumented) +export const useMenuItemCheckbox: (props: MenuItemCheckboxProps_2, ref: React_2.Ref>) => MenuItemCheckboxState_2; -export { useMenuItemLink } +// @public (undocumented) +export const useMenuItemLink: (props: MenuItemLinkProps_2, ref: React_2.Ref) => MenuItemLinkState_2; -export { useMenuItemRadio } +// @public (undocumented) +export const useMenuItemRadio: (props: MenuItemRadioProps, ref: React_2.Ref>) => MenuItemRadioState; -export { useMenuItemSwitch } +// @public (undocumented) +export const useMenuItemSwitch: (props: MenuItemSwitchProps_2, ref: React_2.Ref) => MenuItemSwitchState_2; // @public export const useMenuList: (props: MenuListProps, ref: React_2.Ref) => MenuListState; diff --git a/packages/react-components/react-headless-components-preview/library/etc/nav.api.md b/packages/react-components/react-headless-components-preview/library/etc/nav.api.md index c9d2b5be8c16b..c5b42faefa493 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/nav.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/nav.api.md @@ -31,17 +31,17 @@ import { DrawerFooterProps as NavDrawerFooterProps } from '@fluentui/react-drawe import { DrawerFooterSlots as NavDrawerFooterSlots } from '@fluentui/react-drawer'; import { DrawerHeaderProps as NavDrawerHeaderProps } from '@fluentui/react-drawer'; import { DrawerHeaderSlots as NavDrawerHeaderSlots } from '@fluentui/react-drawer'; -import { NavItemBaseProps as NavItemProps } from '@fluentui/react-nav'; +import type { NavItemBaseProps } from '@fluentui/react-nav'; +import type { NavItemBaseState } from '@fluentui/react-nav'; import { NavItemRegisterData } from '@fluentui/react-nav'; import { NavItemSlots } from '@fluentui/react-nav'; -import { NavItemBaseState as NavItemState } from '@fluentui/react-nav'; import { NavItemValue } from '@fluentui/react-nav'; import { NavBaseProps as NavProps } from '@fluentui/react-nav'; import { NavProvider } from '@fluentui/react-nav'; import { NavSlots } from '@fluentui/react-nav'; +import type { NavSubItemBaseState } from '@fluentui/react-nav'; import { NavSubItemBaseProps as NavSubItemProps } from '@fluentui/react-nav'; import { NavSubItemSlots } from '@fluentui/react-nav'; -import { NavSubItemBaseState as NavSubItemState } from '@fluentui/react-nav'; import { OnNavItemSelectData } from '@fluentui/react-nav'; import type { OverlayDrawerProps as OverlayDrawerProps_2 } from '@fluentui/react-drawer'; import * as React_2 from 'react'; @@ -53,8 +53,6 @@ import { useNavCategoryContext_unstable as useNavCategoryContext } from '@fluent import { useNavCategoryContextValues_unstable as useNavCategoryContextValues } from '@fluentui/react-nav'; import { useNavCategoryItemContext_unstable as useNavCategoryItemContext } from '@fluentui/react-nav'; import { useNavContext_unstable as useNavContext } from '@fluentui/react-nav'; -import { useNavItemBase_unstable as useNavItem } from '@fluentui/react-nav'; -import { useNavSubItemBase_unstable as useNavSubItem } from '@fluentui/react-nav'; // @public export const Nav: ForwardRefComponent; @@ -99,6 +97,10 @@ export type NavCategoryItemSlots = { // @public export type NavCategoryItemState = ComponentState & NavCategoryItemContextValue & { selected: boolean; + root: { + 'data-open'?: string; + 'data-selected'?: string; + }; }; export { NavCategoryProps } @@ -177,13 +179,19 @@ export type NavDrawerState = ComponentState & Omit; -export { NavItemProps } +// @public (undocumented) +export type NavItemProps = NavItemBaseProps; export { NavItemRegisterData } export { NavItemSlots } -export { NavItemState } +// @public (undocumented) +export type NavItemState = NavItemBaseState & { + root: { + 'data-selected'?: string; + }; +}; export { NavItemValue } @@ -237,7 +245,12 @@ export { NavSubItemProps } export { NavSubItemSlots } -export { NavSubItemState } +// @public (undocumented) +export type NavSubItemState = NavSubItemBaseState & { + root: { + 'data-selected'?: string; + }; +}; export { OnNavItemSelectData } @@ -315,12 +328,14 @@ export const useNavDrawerFooter: (props: NavDrawerFooterProps, ref: React_2.Ref< // @public export const useNavDrawerHeader: (props: NavDrawerHeaderProps, ref: React_2.Ref) => NavDrawerHeaderState; -export { useNavItem } +// @public (undocumented) +export const useNavItem: (props: NavItemProps, ref: React_2.Ref) => NavItemState; // @public export const useNavSectionHeader: (props: NavSectionHeaderProps, ref: React_2.Ref) => NavSectionHeaderState; -export { useNavSubItem } +// @public (undocumented) +export const useNavSubItem: (props: NavSubItemProps, ref: React_2.Ref) => NavSubItemState; // @public export const useNavSubItemGroup: (props: NavSubItemGroupProps, ref: React_2.Ref) => NavSubItemGroupState; diff --git a/packages/react-components/react-headless-components-preview/library/etc/persona.api.md b/packages/react-components/react-headless-components-preview/library/etc/persona.api.md index d1b5428dd4e99..951534908fd1f 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/persona.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/persona.api.md @@ -27,7 +27,11 @@ export type PersonaSlots = Omit & { }; // @public -export type PersonaState = ComponentState & Pick; +export type PersonaState = ComponentState & Pick & { + root: { + 'data-text-position'?: PersonaState_2['textPosition']; + }; +}; // @public (undocumented) export const renderPersona: (state: PersonaState) => JSXElement; diff --git a/packages/react-components/react-headless-components-preview/library/etc/progress-bar.api.md b/packages/react-components/react-headless-components-preview/library/etc/progress-bar.api.md index 91ff008669f65..49c6d5853b839 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/progress-bar.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/progress-bar.api.md @@ -21,7 +21,11 @@ export type ProgressBarProps = ProgressBarBaseProps; export type ProgressBarSlots = ProgressBarSlots_2; // @public -export type ProgressBarState = ProgressBarBaseState; +export type ProgressBarState = ProgressBarBaseState & { + root: { + 'data-indeterminate'?: string; + }; +}; // @public export const renderProgressBar: (state: ProgressBarState) => JSXElement; diff --git a/packages/react-components/react-headless-components-preview/library/etc/radio-group.api.md b/packages/react-components/react-headless-components-preview/library/etc/radio-group.api.md index 24a690735d85e..7f3347fb6009c 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/radio-group.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/radio-group.api.md @@ -48,6 +48,7 @@ export type RadioSlots = RadioSlots_2; export type RadioState = RadioBaseState & { root: { 'data-disabled'?: string; + 'data-label-position'?: RadioBaseState['labelPosition']; }; }; diff --git a/packages/react-components/react-headless-components-preview/library/etc/select.api.md b/packages/react-components/react-headless-components-preview/library/etc/select.api.md index b5a5db212b478..36db7a3565e33 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/select.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/select.api.md @@ -26,6 +26,7 @@ export type SelectSlots = SelectSlots_2; export type SelectState = SelectBaseState & { root: { 'data-disabled'?: string; + 'data-invalid'?: string; }; }; diff --git a/packages/react-components/react-headless-components-preview/library/etc/spin-button.api.md b/packages/react-components/react-headless-components-preview/library/etc/spin-button.api.md index 7b96afa295161..90fc42fff1558 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/spin-button.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/spin-button.api.md @@ -28,6 +28,7 @@ export type SpinButtonState = SpinButtonBaseState & { 'data-disabled'?: string; 'data-spin-state'?: string; 'data-at-bound'?: string; + 'data-invalid'?: string; }; }; diff --git a/packages/react-components/react-headless-components-preview/library/etc/switch.api.md b/packages/react-components/react-headless-components-preview/library/etc/switch.api.md index 3a2e29ce58f3a..b5588213eb7f2 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/switch.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/switch.api.md @@ -28,6 +28,7 @@ export type SwitchState = SwitchBaseState & { 'data-disabled'?: string; 'data-disabled-focusable'?: string; 'data-checked'?: string; + 'data-label-position'?: SwitchBaseState['labelPosition']; }; }; diff --git a/packages/react-components/react-headless-components-preview/library/etc/tab-list.api.md b/packages/react-components/react-headless-components-preview/library/etc/tab-list.api.md index 5307f28be8e0c..2564195bcfb3a 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/tab-list.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/tab-list.api.md @@ -52,6 +52,7 @@ export type TabState = TabBaseState & { focusgroupstart?: string; 'data-icon-only'?: string; 'data-selected'?: string; + 'data-disabled'?: string; }; }; diff --git a/packages/react-components/react-headless-components-preview/library/etc/teaching-popover.api.md b/packages/react-components/react-headless-components-preview/library/etc/teaching-popover.api.md index acff453f9affc..1749a6bcc6641 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/teaching-popover.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/teaching-popover.api.md @@ -29,9 +29,9 @@ import { TeachingPopoverCarouselCardState } from '@fluentui/react-teaching-popov import { TeachingPopoverCarouselFooterButtonBaseProps as TeachingPopoverCarouselFooterButtonProps } from '@fluentui/react-teaching-popover'; import { TeachingPopoverCarouselFooterButtonSlots } from '@fluentui/react-teaching-popover'; import { TeachingPopoverCarouselFooterButtonBaseState as TeachingPopoverCarouselFooterButtonState } from '@fluentui/react-teaching-popover'; -import { TeachingPopoverCarouselNavButtonBaseProps as TeachingPopoverCarouselNavButtonProps } from '@fluentui/react-teaching-popover'; +import type { TeachingPopoverCarouselNavButtonBaseProps } from '@fluentui/react-teaching-popover'; +import type { TeachingPopoverCarouselNavButtonBaseState } from '@fluentui/react-teaching-popover'; import { TeachingPopoverCarouselNavButtonSlots } from '@fluentui/react-teaching-popover'; -import { TeachingPopoverCarouselNavButtonBaseState as TeachingPopoverCarouselNavButtonState } from '@fluentui/react-teaching-popover'; import { TeachingPopoverCarouselNavBaseProps as TeachingPopoverCarouselNavProps } from '@fluentui/react-teaching-popover'; import { TeachingPopoverCarouselNavSlots } from '@fluentui/react-teaching-popover'; import { TeachingPopoverCarouselNavBaseState as TeachingPopoverCarouselNavState } from '@fluentui/react-teaching-popover'; @@ -57,7 +57,6 @@ import { useTeachingPopoverCarouselCard_unstable as useTeachingPopoverCarouselCa import type { useTeachingPopoverCarouselContextValues_unstable } from '@fluentui/react-teaching-popover'; import { useTeachingPopoverCarouselFooterButtonBase_unstable as useTeachingPopoverCarouselFooterButton } from '@fluentui/react-teaching-popover'; import { useTeachingPopoverCarouselNavBase_unstable as useTeachingPopoverCarouselNav } from '@fluentui/react-teaching-popover'; -import { useTeachingPopoverCarouselNavButtonBase_unstable as useTeachingPopoverCarouselNavButton } from '@fluentui/react-teaching-popover'; import { useTeachingPopoverCarouselPageCount_unstable as useTeachingPopoverCarouselPageCount } from '@fluentui/react-teaching-popover'; import { useTeachingPopoverFooterBase_unstable as useTeachingPopoverFooter } from '@fluentui/react-teaching-popover'; import { useTeachingPopoverHeaderBase_unstable as useTeachingPopoverHeader } from '@fluentui/react-teaching-popover'; @@ -168,11 +167,17 @@ export const TeachingPopoverCarouselNav: ForwardRefComponent; -export { TeachingPopoverCarouselNavButtonProps } +// @public (undocumented) +export type TeachingPopoverCarouselNavButtonProps = TeachingPopoverCarouselNavButtonBaseProps; export { TeachingPopoverCarouselNavButtonSlots } -export { TeachingPopoverCarouselNavButtonState } +// @public (undocumented) +export type TeachingPopoverCarouselNavButtonState = TeachingPopoverCarouselNavButtonBaseState & { + root: { + 'data-selected'?: string; + }; +}; export { TeachingPopoverCarouselNavProps } @@ -288,7 +293,8 @@ export { useTeachingPopoverCarouselFooterButton } export { useTeachingPopoverCarouselNav } -export { useTeachingPopoverCarouselNavButton } +// @public (undocumented) +export const useTeachingPopoverCarouselNavButton: (props: TeachingPopoverCarouselNavButtonProps, ref: React_2.Ref) => TeachingPopoverCarouselNavButtonState; export { useTeachingPopoverCarouselPageCount } diff --git a/packages/react-components/react-headless-components-preview/library/etc/textarea.api.md b/packages/react-components/react-headless-components-preview/library/etc/textarea.api.md index a9afe00471d9f..60643cea4d171 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/textarea.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/textarea.api.md @@ -23,7 +23,13 @@ export type TextareaProps = TextareaBaseProps; export type TextareaSlots = TextareaSlots_2; // @public -export type TextareaState = TextareaBaseState; +export type TextareaState = TextareaBaseState & { + root: { + 'data-disabled'?: string; + 'data-invalid'?: string; + 'data-resize'?: TextareaBaseState['resize']; + }; +}; // @public export const useTextarea: (props: TextareaProps, ref: React_2.Ref) => TextareaState; diff --git a/packages/react-components/react-headless-components-preview/library/etc/toggle-button.api.md b/packages/react-components/react-headless-components-preview/library/etc/toggle-button.api.md index f6bd1000f5a99..cfee4ffae8e97 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/toggle-button.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/toggle-button.api.md @@ -31,6 +31,7 @@ export type ToggleButtonState = ToggleButtonBaseState & { 'data-disabled-focusable'?: string; 'data-icon-only'?: string; 'data-checked'?: string; + 'data-icon-position'?: ToggleButtonBaseState['iconPosition']; }; }; diff --git a/packages/react-components/react-headless-components-preview/library/etc/tooltip.api.md b/packages/react-components/react-headless-components-preview/library/etc/tooltip.api.md index d62197f93471c..7d3778965604a 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/tooltip.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/tooltip.api.md @@ -28,7 +28,11 @@ export type TooltipProps = Omit; export { TooltipSlots } // @public -export type TooltipState = Omit; +export type TooltipState = Omit & { + content: { + 'data-open'?: string; + }; +}; export { TooltipTriggerProps } diff --git a/packages/react-components/react-headless-components-preview/library/src/components/AvatarGroup/AvatarGroup.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/AvatarGroup/AvatarGroup.types.ts index bbb90b7ecc839..679edb8a29849 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/AvatarGroup/AvatarGroup.types.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/AvatarGroup/AvatarGroup.types.ts @@ -1,7 +1,14 @@ +import type { AvatarGroupBaseState } from '@fluentui/react-avatar'; + export type { - AvatarGroupBaseProps as AvatarGroupProps, - AvatarGroupBaseState as AvatarGroupState, AvatarGroupContextValue, AvatarGroupContextValues, AvatarGroupSlots, + AvatarGroupBaseProps as AvatarGroupProps, } from '@fluentui/react-avatar'; + +export type AvatarGroupState = AvatarGroupBaseState & { + root: { + 'data-layout'?: AvatarGroupBaseState['layout']; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/AvatarGroup/useAvatarGroup.ts b/packages/react-components/react-headless-components-preview/library/src/components/AvatarGroup/useAvatarGroup.ts index 21a6b50e37003..d392dd0c49e1a 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/AvatarGroup/useAvatarGroup.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/AvatarGroup/useAvatarGroup.ts @@ -9,7 +9,11 @@ import type { AvatarGroupProps, AvatarGroupState } from './AvatarGroup.types'; * Returns the state for an AvatarGroup component, given its props and ref. * The returned state can be modified with hooks before being passed to `renderAvatarGroup`. */ -export const useAvatarGroup = useAvatarGroupBase_unstable as ( - props: AvatarGroupProps, - ref: React.Ref, -) => AvatarGroupState; +export const useAvatarGroup = (props: AvatarGroupProps, ref: React.Ref): AvatarGroupState => { + const state: AvatarGroupState = useAvatarGroupBase_unstable(props, ref); + + // eslint-disable-next-line react-hooks/immutability + state.root['data-layout'] = state.layout; + + return state; +}; 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 6080ab66927fa..4027c0245605d 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 @@ -29,5 +29,10 @@ export type ButtonState = ButtonBaseState & { * Data attribute set when the button renders only an icon. */ 'data-icon-only'?: string; + + /** + * Data attribute reflecting the icon position when an icon slot is present. + */ + 'data-icon-position'?: ButtonBaseState['iconPosition']; }; }; 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 1a80907b1db8c..61b6c4bb22707 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 @@ -20,6 +20,8 @@ export const useButton = (props: ButtonProps, ref: React.Ref; export type CardState = CardBaseState & { root: { 'data-selected'?: string; + 'data-disabled'?: string; }; }; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Card/useCard.ts b/packages/react-components/react-headless-components-preview/library/src/components/Card/useCard.ts index b35ce2969eb00..a2d7c3463f9d8 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Card/useCard.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Card/useCard.ts @@ -15,6 +15,8 @@ export const useCard = (props: CardProps, ref: React.Ref): CardS // eslint-disable-next-line react-hooks/immutability state.root['data-selected'] = stringifyDataAttribute(state.selected); + // eslint-disable-next-line react-hooks/immutability + state.root['data-disabled'] = stringifyDataAttribute(state.disabled); return state; }; 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 index fab0a8076f3ac..87b4c2862352e 100644 --- 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 @@ -25,5 +25,10 @@ export type CheckboxState = CheckboxBaseState & { * Data attribute set when the checkbox is checked. Value is 'mixed' when in the indeterminate state. */ 'data-checked'?: string; + + /** + * Data attribute reflecting the label position. + */ + 'data-label-position'?: CheckboxBaseState['labelPosition']; }; }; 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 index 6fe64344ca647..04d1b8790ec54 100644 --- 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 @@ -22,6 +22,8 @@ export const useCheckbox = (props: CheckboxProps, ref: React.Ref): Opti state.root['data-disabled'] = stringifyDataAttribute(state.disabled); // eslint-disable-next-line react-hooks/immutability state.root['data-selected'] = stringifyDataAttribute(state.selected); + // eslint-disable-next-line react-hooks/immutability + state.root['data-multiselect'] = stringifyDataAttribute(state.multiselect); return state; }; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/InfoLabel/InfoButton/InfoButton.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/InfoLabel/InfoButton/InfoButton.test.tsx index b4451c9356171..f492381346914 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/InfoLabel/InfoButton/InfoButton.test.tsx +++ b/packages/react-components/react-headless-components-preview/library/src/components/InfoLabel/InfoButton/InfoButton.test.tsx @@ -1,5 +1,7 @@ +import * as React from 'react'; import { InfoButton } from './InfoButton'; import { isConformant } from '../../../testing/isConformant'; +import { render } from '@testing-library/react'; describe('InfoButton', () => { isConformant({ @@ -10,4 +12,21 @@ describe('InfoButton', () => { }, disabledTests: ['component-has-static-classnames-object', 'exported-top-level', 'has-top-level-file-extra'], }); + + it('exposes open state on the trigger', () => { + const { getByRole, rerender } = render(); + const trigger = getByRole('button', { name: 'information' }); + + expect(trigger).toHaveAttribute('data-open', ''); + + rerender(); + + expect(trigger).not.toHaveAttribute('data-open'); + }); + + it('does not expose open state by default', () => { + const { getByRole } = render(); + + expect(getByRole('button', { name: 'information' })).not.toHaveAttribute('data-open'); + }); }); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/InfoLabel/InfoButton/InfoButton.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/InfoLabel/InfoButton/InfoButton.types.ts index 5f1ba3704107e..940a89868246f 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/InfoLabel/InfoButton/InfoButton.types.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/InfoLabel/InfoButton/InfoButton.types.ts @@ -28,4 +28,8 @@ export type InfoButtonProps = Omit>, 'di /** * State used in rendering InfoButton */ -export type InfoButtonState = ComponentState; +export type InfoButtonState = ComponentState & { + root: { + 'data-open'?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/InfoLabel/InfoButton/useInfoButton.tsx b/packages/react-components/react-headless-components-preview/library/src/components/InfoLabel/InfoButton/useInfoButton.tsx index 4b9c935dbf3cc..431f8063d0cc5 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/InfoLabel/InfoButton/useInfoButton.tsx +++ b/packages/react-components/react-headless-components-preview/library/src/components/InfoLabel/InfoButton/useInfoButton.tsx @@ -14,6 +14,7 @@ import { import type { InfoButtonProps, InfoButtonState } from './InfoButton.types'; import { Popover, PopoverSurface } from '../../Popover'; import type { PopoverProps } from '../../Popover'; +import { stringifyDataAttribute } from '../../../utils'; /** * Create the state required to render InfoButton. @@ -66,6 +67,7 @@ export const useInfoButton = (props: InfoButtonProps, ref: React.Ref setPopoverOpen(data.open)); const infoRef = useMergedRefs(state.info.ref); 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 index 7789788220240..b430dd4b53004 100644 --- 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 @@ -11,14 +11,25 @@ describe('Input', () => { }); it('renders a default state', () => { - const { getByRole } = render(); + const { container, getByRole } = render(); + const root = container.firstElementChild!; const input = getByRole('textbox'); + expect(root).not.toHaveAttribute('data-invalid'); expect(input).toBeInTheDocument(); expect(input).toHaveAttribute('placeholder', 'Input your text'); expect(input).toHaveAttribute('type', 'text'); }); + it('exposes invalid state on the root while preserving aria-invalid on the input', () => { + const { container, getByRole } = render(); + const root = container.firstElementChild!; + const input = getByRole('textbox'); + + expect(root).toHaveAttribute('data-invalid', ''); + expect(input).toHaveAttribute('aria-invalid', 'true'); + }); + it('renders with data-disabled when disabled', () => { const { container } = render(); const root = container.firstElementChild!; 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 index 17af79929b52c..7811a1bdb082a 100644 --- 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 @@ -19,5 +19,10 @@ export type InputState = InputBaseState & { * Data attribute set when the input is disabled. */ 'data-disabled'?: string; + + /** + * Data attribute set when the input is invalid. + */ + 'data-invalid'?: string; }; }; 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 index eec5361127c04..97ea19896f385 100644 --- 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 @@ -16,6 +16,10 @@ export const useInput = (props: InputProps, ref: React.Ref): I // Set data attribute for disabled state to simplify styling. // eslint-disable-next-line react-hooks/immutability state.root['data-disabled'] = stringifyDataAttribute(state.input.disabled); + // eslint-disable-next-line react-hooks/immutability + state.root['data-invalid'] = stringifyDataAttribute( + state.input['aria-invalid'] === true || state.input['aria-invalid'] === 'true', + ); return state; }; 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 index 8d6b57e232b8c..f40fe37ab33e6 100644 --- 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 @@ -19,5 +19,10 @@ export type LabelState = LabelBaseState & { * Data attribute set when the label is disabled. */ 'data-disabled'?: string; + + /** + * Data attribute set when the required indicator is rendered. + */ + 'data-required'?: string; }; }; 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 index 87d2d13ef9691..b2ce1be3734eb 100644 --- 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 @@ -16,6 +16,8 @@ export const useLabel = (props: LabelProps, ref: React.Ref): L // Set data attribute for disabled state to simplify styling. // eslint-disable-next-line react-hooks/immutability state.root['data-disabled'] = stringifyDataAttribute(state.disabled); + // eslint-disable-next-line react-hooks/immutability + state.root['data-required'] = stringifyDataAttribute(Boolean(state.required)); return state; }; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Menu/Menu.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Menu/Menu.test.tsx index ed87095fb1869..d5dc4f9b44fed 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Menu/Menu.test.tsx +++ b/packages/react-components/react-headless-components-preview/library/src/components/Menu/Menu.test.tsx @@ -195,6 +195,40 @@ describe('Menu', () => { expect(getAllByRole('menuitem')).toHaveLength(2); }); + it('exposes MenuItem interaction state through data attributes', () => { + const { getByText, getByTestId } = render( + + + + + + + + Disabled item + + + + + Submenu item + + + + + Nested item + + + + + + , + ); + + expect(getByTestId('disabled-item')).toHaveAttribute('data-disabled'); + expect(getByTestId('submenu-item')).toHaveAttribute('data-has-submenu'); + userEvent.click(getByText('Submenu item')); + expect(getByTestId('submenu-item')).toHaveAttribute('data-submenu-open'); + }); + it('renders MenuDivider with role="presentation" and aria-hidden', () => { const { container } = render( @@ -294,6 +328,7 @@ describe('Menu', () => { ); expect(getByRole('menuitemcheckbox')).toBeInTheDocument(); + expect(getByRole('menuitemcheckbox')).not.toHaveAttribute('data-checked'); }); it('toggles aria-checked on click via controlled checkedValues', () => { @@ -338,6 +373,7 @@ describe('Menu', () => { ); expect(getByRole('menuitemcheckbox')).toHaveAttribute('aria-checked', 'true'); + expect(getByRole('menuitemcheckbox')).toHaveAttribute('data-checked'); }); }); @@ -385,6 +421,7 @@ describe('Menu', () => { const [name, size] = getAllByRole('menuitemradio'); expect(name).toHaveAttribute('aria-checked', 'true'); + expect(name).toHaveAttribute('data-checked'); expect(size).toHaveAttribute('aria-checked', 'false'); userEvent.click(size); @@ -404,7 +441,9 @@ describe('Menu', () => { - Docs + + Docs + , @@ -413,13 +452,14 @@ describe('Menu', () => { const link = getByRole('menuitem'); expect(link.tagName).toBe('A'); expect(link).toHaveAttribute('href', 'https://example.com'); + expect(link).toHaveAttribute('data-disabled'); }); }); describe('MenuItemSwitch', () => { it('renders an item with switchIndicator slot', () => { const { getByText, container } = render( - + @@ -435,6 +475,7 @@ describe('Menu', () => { expect(getByText('Grid view')).toBeInTheDocument(); expect(container.querySelector('[data-testid="switch"]')).toBeInTheDocument(); + expect(getByText('Grid view').closest('[role="menuitemcheckbox"]')).toHaveAttribute('data-checked'); }); }); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItem/MenuItem.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItem/MenuItem.types.ts index 6d0ab9a962a92..7cbb970d1d47e 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItem/MenuItem.types.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItem/MenuItem.types.ts @@ -7,5 +7,8 @@ export type MenuItemProps = MenuItemBaseProps; export type MenuItemState = MenuItemBaseState & { root: { focusgroupstart?: string; + 'data-disabled'?: string; + 'data-has-submenu'?: string; + 'data-submenu-open'?: string; }; }; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItem/useMenuItem.ts b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItem/useMenuItem.ts index b93b500c42e4d..96b6ddac35ebd 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItem/useMenuItem.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItem/useMenuItem.ts @@ -5,6 +5,7 @@ import { useMenuItemBase_unstable } from '@fluentui/react-menu'; import type { ARIAButtonElement } from '@fluentui/react-aria'; import type { MenuItemProps, MenuItemState } from './MenuItem.types'; +import { stringifyDataAttribute } from '../../../utils'; /** Returns the state for a MenuItem; adds `focusgroupstart` so the focusgroup polyfill anchors the initial tab stop on the first item. */ export const useMenuItem = (props: MenuItemProps, ref: React.Ref>): MenuItemState => { @@ -12,6 +13,12 @@ export const useMenuItem = (props: MenuItemProps, ref: React.Ref>, +): MenuItemCheckboxState => { + const state: MenuItemCheckboxState = useMenuItemCheckboxBase_unstable(props, ref); + + // eslint-disable-next-line react-hooks/immutability + state.root['data-disabled'] = stringifyDataAttribute(state.disabled); + // eslint-disable-next-line react-hooks/immutability + state.root['data-has-submenu'] = stringifyDataAttribute(state.hasSubmenu); + // eslint-disable-next-line react-hooks/immutability + state.root['data-submenu-open'] = stringifyDataAttribute(state.submenuOpen); + // eslint-disable-next-line react-hooks/immutability + state.root['data-checked'] = stringifyDataAttribute(state.checked); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemLink/MenuItemLink.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemLink/MenuItemLink.tsx index f7b6cd8f159ab..4ba4e1f38b2ce 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemLink/MenuItemLink.tsx +++ b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemLink/MenuItemLink.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; import { useMenuItemLink } from './useMenuItemLink'; import { renderMenuItemLink } from './renderMenuItemLink'; -import type { MenuItemLinkProps } from '@fluentui/react-menu'; +import type { MenuItemLinkProps } from './MenuItemLink.types'; /** * Headless MenuItemLink component. diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemLink/MenuItemLink.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemLink/MenuItemLink.types.ts new file mode 100644 index 0000000000000..933041b60dd7b --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemLink/MenuItemLink.types.ts @@ -0,0 +1,15 @@ +import type { + MenuItemLinkProps as MenuItemLinkBaseProps, + MenuItemLinkState as MenuItemLinkBaseState, + MenuItemLinkSlots, +} from '@fluentui/react-menu'; + +export type { MenuItemLinkSlots }; + +export type MenuItemLinkProps = MenuItemLinkBaseProps; + +export type MenuItemLinkState = MenuItemLinkBaseState & { + root: { + 'data-disabled'?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemLink/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemLink/index.ts index da31000500bf1..fed47417f0653 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemLink/index.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemLink/index.ts @@ -1,4 +1,4 @@ export { MenuItemLink } from './MenuItemLink'; export { useMenuItemLink } from './useMenuItemLink'; export { renderMenuItemLink } from './renderMenuItemLink'; -export type { MenuItemLinkProps, MenuItemLinkSlots, MenuItemLinkState } from '@fluentui/react-menu'; +export type { MenuItemLinkProps, MenuItemLinkSlots, MenuItemLinkState } from './MenuItemLink.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemLink/useMenuItemLink.ts b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemLink/useMenuItemLink.ts index edb98cb376707..d9bb3c5685c49 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemLink/useMenuItemLink.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemLink/useMenuItemLink.ts @@ -1 +1,15 @@ -export { useMenuItemLinkBase_unstable as useMenuItemLink } from '@fluentui/react-menu'; +'use client'; + +import type * as React from 'react'; +import { useMenuItemLinkBase_unstable } from '@fluentui/react-menu'; +import { stringifyDataAttribute } from '../../../utils'; +import type { MenuItemLinkProps, MenuItemLinkState } from './MenuItemLink.types'; + +export const useMenuItemLink = (props: MenuItemLinkProps, ref: React.Ref): MenuItemLinkState => { + const state: MenuItemLinkState = useMenuItemLinkBase_unstable(props, ref); + + // eslint-disable-next-line react-hooks/immutability + state.root['data-disabled'] = stringifyDataAttribute(props.disabled); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemRadio/MenuItemRadio.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemRadio/MenuItemRadio.types.ts index c3773613f1bee..1ba35243c62dd 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemRadio/MenuItemRadio.types.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemRadio/MenuItemRadio.types.ts @@ -1,4 +1,12 @@ -export type { - MenuItemRadioBaseProps as MenuItemRadioProps, - MenuItemRadioBaseState as MenuItemRadioState, -} from '@fluentui/react-menu'; +import type { MenuItemRadioBaseProps, MenuItemRadioBaseState } from '@fluentui/react-menu'; + +export type MenuItemRadioProps = MenuItemRadioBaseProps; + +export type MenuItemRadioState = MenuItemRadioBaseState & { + root: { + 'data-disabled'?: string; + 'data-has-submenu'?: string; + 'data-submenu-open'?: string; + 'data-checked'?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemRadio/useMenuItemRadio.ts b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemRadio/useMenuItemRadio.ts index f2a828d3371d5..0170739e19113 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemRadio/useMenuItemRadio.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemRadio/useMenuItemRadio.ts @@ -1 +1,25 @@ -export { useMenuItemRadioBase_unstable as useMenuItemRadio } from '@fluentui/react-menu'; +'use client'; + +import type * as React from 'react'; +import type { ARIAButtonElement } from '@fluentui/react-aria'; +import { useMenuItemRadioBase_unstable } from '@fluentui/react-menu'; +import { stringifyDataAttribute } from '../../../utils'; +import type { MenuItemRadioProps, MenuItemRadioState } from './MenuItemRadio.types'; + +export const useMenuItemRadio = ( + props: MenuItemRadioProps, + ref: React.Ref>, +): MenuItemRadioState => { + const state: MenuItemRadioState = useMenuItemRadioBase_unstable(props, ref); + + // eslint-disable-next-line react-hooks/immutability + state.root['data-disabled'] = stringifyDataAttribute(state.disabled); + // eslint-disable-next-line react-hooks/immutability + state.root['data-has-submenu'] = stringifyDataAttribute(state.hasSubmenu); + // eslint-disable-next-line react-hooks/immutability + state.root['data-submenu-open'] = stringifyDataAttribute(state.submenuOpen); + // eslint-disable-next-line react-hooks/immutability + state.root['data-checked'] = stringifyDataAttribute(state.checked); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemSwitch/MenuItemSwitch.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemSwitch/MenuItemSwitch.tsx index 6f384f739e82f..5ed4b247be41b 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemSwitch/MenuItemSwitch.tsx +++ b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemSwitch/MenuItemSwitch.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; import { useMenuItemSwitch } from './useMenuItemSwitch'; import { renderMenuItemSwitch } from './renderMenuItemSwitch'; -import type { MenuItemSwitchProps } from '@fluentui/react-menu'; +import type { MenuItemSwitchProps } from './MenuItemSwitch.types'; /** * Headless MenuItemSwitch component. diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemSwitch/MenuItemSwitch.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemSwitch/MenuItemSwitch.types.ts new file mode 100644 index 0000000000000..12355ce3435b8 --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemSwitch/MenuItemSwitch.types.ts @@ -0,0 +1,16 @@ +import type { + MenuItemSwitchProps as MenuItemSwitchBaseProps, + MenuItemSwitchState as MenuItemSwitchBaseState, + MenuItemSwitchSlots, +} from '@fluentui/react-menu'; + +export type { MenuItemSwitchSlots }; + +export type MenuItemSwitchProps = MenuItemSwitchBaseProps; + +export type MenuItemSwitchState = MenuItemSwitchBaseState & { + root: { + 'data-disabled'?: string; + 'data-checked'?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemSwitch/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemSwitch/index.ts index 84af6c0332a4b..278937b65d602 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemSwitch/index.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemSwitch/index.ts @@ -1,4 +1,4 @@ export { MenuItemSwitch } from './MenuItemSwitch'; export { useMenuItemSwitch } from './useMenuItemSwitch'; export { renderMenuItemSwitch } from './renderMenuItemSwitch'; -export type { MenuItemSwitchProps, MenuItemSwitchSlots, MenuItemSwitchState } from '@fluentui/react-menu'; +export type { MenuItemSwitchProps, MenuItemSwitchSlots, MenuItemSwitchState } from './MenuItemSwitch.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemSwitch/useMenuItemSwitch.ts b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemSwitch/useMenuItemSwitch.ts index 2616a1c43706b..4bbcc0d966c7f 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemSwitch/useMenuItemSwitch.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuItemSwitch/useMenuItemSwitch.ts @@ -1 +1,17 @@ -export { useMenuItemSwitchBase_unstable as useMenuItemSwitch } from '@fluentui/react-menu'; +'use client'; + +import type * as React from 'react'; +import { useMenuItemSwitchBase_unstable } from '@fluentui/react-menu'; +import { stringifyDataAttribute } from '../../../utils'; +import type { MenuItemSwitchProps, MenuItemSwitchState } from './MenuItemSwitch.types'; + +export const useMenuItemSwitch = (props: MenuItemSwitchProps, ref: React.Ref): MenuItemSwitchState => { + const state: MenuItemSwitchState = useMenuItemSwitchBase_unstable(props, ref); + + // eslint-disable-next-line react-hooks/immutability + state.root['data-disabled'] = stringifyDataAttribute(state.disabled); + // eslint-disable-next-line react-hooks/immutability + state.root['data-checked'] = stringifyDataAttribute(state.checked); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/MenuButton/MenuButton.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/MenuButton/MenuButton.types.ts index 920f99e082a7c..cf6ac90fc2edd 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/MenuButton/MenuButton.types.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/MenuButton/MenuButton.types.ts @@ -21,5 +21,10 @@ export type MenuButtonState = MenuButtonBaseState & { * Data attribute set when the button renders only an icon. */ 'data-icon-only'?: string; + + /** + * Data attribute set when the menu is open. + */ + 'data-open'?: string; }; }; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/MenuButton/useMenuButton.ts b/packages/react-components/react-headless-components-preview/library/src/components/MenuButton/useMenuButton.ts index ed1744db489a9..be7d6028d29ff 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/MenuButton/useMenuButton.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/MenuButton/useMenuButton.ts @@ -23,6 +23,10 @@ export const useMenuButton = ( state.root['data-disabled-focusable'] = stringifyDataAttribute(state.disabledFocusable); // eslint-disable-next-line react-hooks/immutability state.root['data-icon-only'] = stringifyDataAttribute(state.iconOnly); + // eslint-disable-next-line react-hooks/immutability + state.root['data-open'] = stringifyDataAttribute( + state.root['aria-expanded'] === true || state.root['aria-expanded'] === 'true', + ); return state; }; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Nav/Nav.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Nav/Nav.test.tsx index 31cf43e459d1e..e1bd45c6110fc 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Nav/Nav.test.tsx +++ b/packages/react-components/react-headless-components-preview/library/src/components/Nav/Nav.test.tsx @@ -50,7 +50,9 @@ describe('Nav', () => { ); expect(result.getByText('Item 1')).toHaveAttribute('aria-current', 'page'); + expect(result.getByText('Item 1')).toHaveAttribute('data-selected'); expect(result.getByText('Item 2')).not.toHaveAttribute('aria-current', 'page'); + expect(result.getByText('Item 2')).not.toHaveAttribute('data-selected'); }); it('supports controlled selectedValue', () => { @@ -87,6 +89,7 @@ describe('Nav', () => { ); expect(result.getByText('Category 1')).toHaveAttribute('aria-expanded', 'true'); + expect(result.getByText('Category 1')).toHaveAttribute('data-open'); expect(result.getByText('Sub Item 1')).toBeInTheDocument(); }); @@ -109,6 +112,7 @@ describe('Nav', () => { // Click to open fireEvent.click(result.getByText('Category 1')); expect(result.getByText('Category 1')).toHaveAttribute('aria-expanded', 'true'); + expect(result.getByText('Category 1')).toHaveAttribute('data-open'); expect(result.getByText('Sub Item 1')).toBeInTheDocument(); // Click to close diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Nav/NavCategoryItem/NavCategoryItem.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Nav/NavCategoryItem/NavCategoryItem.types.ts index cfd53b021a45c..8fbda232dd5d1 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Nav/NavCategoryItem/NavCategoryItem.types.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Nav/NavCategoryItem/NavCategoryItem.types.ts @@ -40,4 +40,8 @@ export type NavCategoryItemState = ComponentState & * If this navCategoryItem is selected */ selected: boolean; + root: { + 'data-open'?: string; + 'data-selected'?: string; + }; }; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Nav/NavCategoryItem/useNavCategoryItem.ts b/packages/react-components/react-headless-components-preview/library/src/components/Nav/NavCategoryItem/useNavCategoryItem.ts index ab022eb1bf0d2..23c7039e30f0f 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Nav/NavCategoryItem/useNavCategoryItem.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Nav/NavCategoryItem/useNavCategoryItem.ts @@ -5,6 +5,7 @@ import { getIntrinsicElementProps, mergeCallbacks, slot, useEventCallback } from import { useNavContext_unstable, useNavCategoryContext_unstable, type OnNavItemSelectData } from '@fluentui/react-nav'; import type { NavCategoryItemProps, NavCategoryItemState } from './NavCategoryItem.types'; +import { stringifyDataAttribute } from '../../../utils'; /** * Create the state required to render NavCategoryItem. @@ -34,7 +35,7 @@ export const useNavCategoryItem = ( const selected = selectedCategoryValue === value && !open; const validAriaCurrent: 'page' | 'false' = selected ? 'page' : 'false'; - return { + const state: NavCategoryItemState = { open, value, selected, @@ -60,4 +61,9 @@ export const useNavCategoryItem = ( expandIcon: 'span', }, }; + + state.root['data-open'] = stringifyDataAttribute(state.open); + state.root['data-selected'] = stringifyDataAttribute(state.selected); + + return state; }; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Nav/NavItem/NavItem.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Nav/NavItem/NavItem.types.ts index 6cae353cd2f66..90dd84c67003a 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Nav/NavItem/NavItem.types.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Nav/NavItem/NavItem.types.ts @@ -1,5 +1,11 @@ -export type { - NavItemSlots, - NavItemBaseProps as NavItemProps, - NavItemBaseState as NavItemState, -} from '@fluentui/react-nav'; +import type { NavItemBaseProps, NavItemBaseState } from '@fluentui/react-nav'; + +export type { NavItemSlots } from '@fluentui/react-nav'; + +export type NavItemProps = NavItemBaseProps; + +export type NavItemState = NavItemBaseState & { + root: { + 'data-selected'?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Nav/NavItem/useNavItem.ts b/packages/react-components/react-headless-components-preview/library/src/components/Nav/NavItem/useNavItem.ts index fffb8710c6e29..521b20e062f3b 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Nav/NavItem/useNavItem.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Nav/NavItem/useNavItem.ts @@ -1 +1,18 @@ -export { useNavItemBase_unstable as useNavItem } from '@fluentui/react-nav'; +'use client'; + +import type * as React from 'react'; +import { useNavItemBase_unstable } from '@fluentui/react-nav'; +import { stringifyDataAttribute } from '../../../utils'; +import type { NavItemProps, NavItemState } from './NavItem.types'; + +export const useNavItem = ( + props: NavItemProps, + ref: React.Ref, +): NavItemState => { + const state: NavItemState = useNavItemBase_unstable(props, ref); + + // eslint-disable-next-line react-hooks/immutability + state.root['data-selected'] = stringifyDataAttribute(state.selected); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Nav/NavSubItem/NavSubItem.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Nav/NavSubItem/NavSubItem.types.ts index d411283b1aa23..d904318a3495f 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Nav/NavSubItem/NavSubItem.types.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Nav/NavSubItem/NavSubItem.types.ts @@ -1,5 +1,9 @@ -export type { - NavSubItemSlots, - NavSubItemBaseProps as NavSubItemProps, - NavSubItemBaseState as NavSubItemState, -} from '@fluentui/react-nav'; +import type { NavSubItemBaseState } from '@fluentui/react-nav'; + +export type { NavSubItemSlots, NavSubItemBaseProps as NavSubItemProps } from '@fluentui/react-nav'; + +export type NavSubItemState = NavSubItemBaseState & { + root: { + 'data-selected'?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Nav/NavSubItem/useNavSubItem.ts b/packages/react-components/react-headless-components-preview/library/src/components/Nav/NavSubItem/useNavSubItem.ts index 37609a0cbc5c5..4853d34cf4f45 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Nav/NavSubItem/useNavSubItem.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Nav/NavSubItem/useNavSubItem.ts @@ -1 +1,18 @@ -export { useNavSubItemBase_unstable as useNavSubItem } from '@fluentui/react-nav'; +'use client'; + +import type * as React from 'react'; +import { useNavSubItemBase_unstable } from '@fluentui/react-nav'; +import { stringifyDataAttribute } from '../../../utils'; +import type { NavSubItemProps, NavSubItemState } from './NavSubItem.types'; + +export const useNavSubItem = ( + props: NavSubItemProps, + ref: React.Ref, +): NavSubItemState => { + const state: NavSubItemState = useNavSubItemBase_unstable(props, ref); + + // eslint-disable-next-line react-hooks/immutability + state.root['data-selected'] = stringifyDataAttribute(state.selected); + + return state; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Persona/Persona.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/Persona/Persona.types.ts index 0e2b786135191..bf26e776c5050 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Persona/Persona.types.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Persona/Persona.types.ts @@ -18,4 +18,9 @@ export type PersonaProps = ComponentProps & Pick & Pick; +export type PersonaState = ComponentState & + Pick & { + root: { + 'data-text-position'?: PersonaBaseState['textPosition']; + }; + }; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Persona/usePersona.ts b/packages/react-components/react-headless-components-preview/library/src/components/Persona/usePersona.ts index af4f72ec262ef..7b8f711174ad7 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Persona/usePersona.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Persona/usePersona.ts @@ -19,7 +19,7 @@ export const usePersona = (props: PersonaProps, ref: React.Ref): const { textPosition = 'after', ...baseProps } = props; const baseState = usePersonaBase_unstable(baseProps, ref); - return { + const state: PersonaState = { ...baseState, textPosition, components: { @@ -35,4 +35,8 @@ export const usePersona = (props: PersonaProps, ref: React.Ref): elementType: Avatar, }), }; + + state.root['data-text-position'] = state.textPosition; + + return state; }; 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 index 00d2f37d74e24..befb9d8e4fbf8 100644 --- 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 @@ -14,4 +14,8 @@ export type ProgressBarProps = ProgressBarBaseProps; /** * State used in rendering ProgressBar */ -export type ProgressBarState = ProgressBarBaseState; +export type ProgressBarState = ProgressBarBaseState & { + root: { + 'data-indeterminate'?: string; + }; +}; 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 index 01dd4e5daab85..e79ba6c5ba8f5 100644 --- 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 @@ -4,6 +4,7 @@ import type * as React from 'react'; import { useProgressBarBase_unstable } from '@fluentui/react-progress'; import type { ProgressBarProps, ProgressBarState } from './ProgressBar.types'; +import { stringifyDataAttribute } from '../../utils'; /** * Create the state required to render ProgressBar. @@ -15,7 +16,10 @@ import type { ProgressBarProps, ProgressBarState } from './ProgressBar.types'; * @param ref - reference to root HTMLDivElement of ProgressBar */ export const useProgressBar = (props: ProgressBarProps, ref: React.Ref): ProgressBarState => { - const state = useProgressBarBase_unstable(props, ref); + const state: ProgressBarState = useProgressBarBase_unstable(props, ref); + + // eslint-disable-next-line react-hooks/immutability + state.root['data-indeterminate'] = stringifyDataAttribute(state.value === undefined); if (state.bar && state.value !== undefined) { // eslint-disable-next-line react-hooks/immutability 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 index 3a70c4aeec9d7..a0ecd397b2551 100644 --- 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 @@ -19,5 +19,10 @@ export type RadioState = RadioBaseState & { * Data attribute set when the radio is disabled. */ 'data-disabled'?: string; + + /** + * Data attribute reflecting the label position. + */ + 'data-label-position'?: RadioBaseState['labelPosition']; }; }; 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 index 17e772ed8eca1..0751c7c8fc63a 100644 --- 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 @@ -16,6 +16,8 @@ export const useRadio = (props: RadioProps, ref: React.Ref): R // Set data attribute for disabled state to simplify styling. // eslint-disable-next-line react-hooks/immutability state.root['data-disabled'] = stringifyDataAttribute(state.input.disabled); + // eslint-disable-next-line react-hooks/immutability + state.root['data-label-position'] = state.labelPosition; return state; }; 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 index b7d8d09113168..9aaaa20abb865 100644 --- 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 @@ -16,5 +16,10 @@ export type SelectState = SelectBaseState & { * Data attribute set when the select is disabled. */ 'data-disabled'?: string; + + /** + * Data attribute set when the select is invalid. + */ + 'data-invalid'?: string; }; }; 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 index 76464806d75ad..cdf12a4bf71a5 100644 --- 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 @@ -20,6 +20,10 @@ export const useSelect = (props: SelectProps, ref: React.Ref) // Set data attribute for disabled state to simplify styling. // eslint-disable-next-line react-hooks/immutability state.root['data-disabled'] = stringifyDataAttribute(state.select.disabled); + // eslint-disable-next-line react-hooks/immutability + state.root['data-invalid'] = stringifyDataAttribute( + state.select['aria-invalid'] === true || state.select['aria-invalid'] === 'true', + ); return state; }; 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 index e5e05a7e88bff..4dc5ba05483e0 100644 --- 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 @@ -33,5 +33,10 @@ export type SpinButtonState = SpinButtonBaseState & { * Data attribute set when the value is at a range boundary. Value is 'min', 'max', or 'both'. */ 'data-at-bound'?: string; + + /** + * Data attribute set when the spin button is invalid. + */ + 'data-invalid'?: string; }; }; 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 index 3f5eabce641f1..02e475fcd44c2 100644 --- 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 @@ -20,6 +20,10 @@ export const useSpinButton = (props: SpinButtonProps, ref: React.Ref): state.root['data-disabled-focusable'] = stringifyDataAttribute(state.disabledFocusable); // eslint-disable-next-line react-hooks/immutability state.root['data-checked'] = stringifyDataAttribute(state.input.checked); + // eslint-disable-next-line react-hooks/immutability + state.root['data-label-position'] = state.labelPosition; return state; }; 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 index 72b41ffc51e2a..5f0de1499e022 100644 --- 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 @@ -9,5 +9,6 @@ export type TabState = TabBaseState & { focusgroupstart?: string; 'data-icon-only'?: string; 'data-selected'?: string; + 'data-disabled'?: string; }; }; 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 index b79991577b02e..8ee23959743c3 100644 --- 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 @@ -19,6 +19,8 @@ export const useTab = (props: TabProps, ref: React.Ref): TabState = state.root['data-icon-only'] = stringifyDataAttribute(state.iconOnly); // eslint-disable-next-line react-hooks/immutability state.root['data-selected'] = stringifyDataAttribute(state.selected); + // eslint-disable-next-line react-hooks/immutability + state.root['data-disabled'] = stringifyDataAttribute(state.disabled); return state; }; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/TeachingPopover/TeachingPopoverCarouselNavButton/TeachingPopoverCarouselNavButton.types.ts b/packages/react-components/react-headless-components-preview/library/src/components/TeachingPopover/TeachingPopoverCarouselNavButton/TeachingPopoverCarouselNavButton.types.ts index 7dfec37bacd76..c067167061514 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/TeachingPopover/TeachingPopoverCarouselNavButton/TeachingPopoverCarouselNavButton.types.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/TeachingPopover/TeachingPopoverCarouselNavButton/TeachingPopoverCarouselNavButton.types.ts @@ -1,5 +1,14 @@ -export type { - TeachingPopoverCarouselNavButtonBaseProps as TeachingPopoverCarouselNavButtonProps, - TeachingPopoverCarouselNavButtonBaseState as TeachingPopoverCarouselNavButtonState, - TeachingPopoverCarouselNavButtonSlots, +import type { + TeachingPopoverCarouselNavButtonBaseProps, + TeachingPopoverCarouselNavButtonBaseState, } from '@fluentui/react-teaching-popover'; + +export type { TeachingPopoverCarouselNavButtonSlots } from '@fluentui/react-teaching-popover'; + +export type TeachingPopoverCarouselNavButtonProps = TeachingPopoverCarouselNavButtonBaseProps; + +export type TeachingPopoverCarouselNavButtonState = TeachingPopoverCarouselNavButtonBaseState & { + root: { + 'data-selected'?: string; + }; +}; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/TeachingPopover/TeachingPopoverCarouselNavButton/useTeachingPopoverCarouselNavButton.ts b/packages/react-components/react-headless-components-preview/library/src/components/TeachingPopover/TeachingPopoverCarouselNavButton/useTeachingPopoverCarouselNavButton.ts index bbeeaee8591be..58542c92ab259 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/TeachingPopover/TeachingPopoverCarouselNavButton/useTeachingPopoverCarouselNavButton.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/TeachingPopover/TeachingPopoverCarouselNavButton/useTeachingPopoverCarouselNavButton.ts @@ -1 +1,21 @@ -export { useTeachingPopoverCarouselNavButtonBase_unstable as useTeachingPopoverCarouselNavButton } from '@fluentui/react-teaching-popover'; +'use client'; + +import type * as React from 'react'; +import { useTeachingPopoverCarouselNavButtonBase_unstable } from '@fluentui/react-teaching-popover'; +import { stringifyDataAttribute } from '../../../utils'; +import type { + TeachingPopoverCarouselNavButtonProps, + TeachingPopoverCarouselNavButtonState, +} from './TeachingPopoverCarouselNavButton.types'; + +export const useTeachingPopoverCarouselNavButton = ( + props: TeachingPopoverCarouselNavButtonProps, + ref: React.Ref, +): TeachingPopoverCarouselNavButtonState => { + const state: TeachingPopoverCarouselNavButtonState = useTeachingPopoverCarouselNavButtonBase_unstable(props, ref); + + // eslint-disable-next-line react-hooks/immutability + state.root['data-selected'] = stringifyDataAttribute(state.isSelected); + + return state; +}; 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 index 98060c0f028ab..3e27d494a4bbe 100644 --- 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 @@ -11,11 +11,22 @@ describe('Textarea', () => { }); it('renders a default state', () => { - const { getByRole } = render(