diff --git a/change/@fluentui-react-button-5368ba44-0210-43a8-9960-27fb5a61f347.json b/change/@fluentui-react-button-5368ba44-0210-43a8-9960-27fb5a61f347.json new file mode 100644 index 0000000000000..efa4bc34f8bdf --- /dev/null +++ b/change/@fluentui-react-button-5368ba44-0210-43a8-9960-27fb5a61f347.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: introduce headless style hooks for button components", + "packageName": "@fluentui/react-button", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-divider-dfaf2632-108c-4558-8643-a154b097f22e.json b/change/@fluentui-react-divider-dfaf2632-108c-4558-8643-a154b097f22e.json new file mode 100644 index 0000000000000..7424af9bffb60 --- /dev/null +++ b/change/@fluentui-react-divider-dfaf2632-108c-4558-8643-a154b097f22e.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: implement headless style hook for Divider component", + "packageName": "@fluentui/react-divider", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.headless.ts b/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.headless.ts new file mode 100644 index 0000000000000..621ced016cd5c --- /dev/null +++ b/packages/react-components/react-button/library/src/components/Button/useButtonStyles.styles.headless.ts @@ -0,0 +1,51 @@ +'use client'; + +import type { SlotClassNames } from '@fluentui/react-utilities'; +import type { ButtonSlots, ButtonState } from './Button.types'; + +export const buttonClassNames: SlotClassNames = { + root: 'fui-Button', + icon: 'fui-Button__icon', +}; + +/** + * Attaches only semantic slot class names and state modifiers + */ +export const useButtonStyles_unstable = (state: ButtonState): ButtonState => { + 'use no memo'; + + const { appearance, disabled, disabledFocusable, icon, iconOnly, iconPosition, shape, size } = state; + + state.root.className = [ + buttonClassNames.root, + + // Appearance + appearance && `${buttonClassNames.root}--${appearance}`, + + // Size + `${buttonClassNames.root}--${size}`, + + // Shape + `${buttonClassNames.root}--${shape}`, + + // Disabled styles + disabled && `${buttonClassNames.root}--disabled`, + disabledFocusable && `${buttonClassNames.root}--disabledFocusable`, + + // Icon styles + icon && iconPosition === 'before' && `${buttonClassNames.root}--iconBefore`, + icon && iconPosition === 'after' && `${buttonClassNames.root}--iconAfter`, + iconOnly && `${buttonClassNames.root}--iconOnly`, + + // User provided class name + state.root.className, + ] + .filter(Boolean) + .join(' '); + + if (state.icon) { + state.icon.className = [buttonClassNames.icon, state.icon.className].filter(Boolean).join(' '); + } + + return state; +}; diff --git a/packages/react-components/react-button/library/src/components/CompoundButton/useCompoundButtonStyles.styles.headless.ts b/packages/react-components/react-button/library/src/components/CompoundButton/useCompoundButtonStyles.styles.headless.ts new file mode 100644 index 0000000000000..7ad9383b509ba --- /dev/null +++ b/packages/react-components/react-button/library/src/components/CompoundButton/useCompoundButtonStyles.styles.headless.ts @@ -0,0 +1,64 @@ +'use client'; + +import type { SlotClassNames } from '@fluentui/react-utilities'; +import type { CompoundButtonSlots, CompoundButtonState } from './CompoundButton.types'; + +// Re-export the same slot class names mapping used by the griffel styles file +export const compoundButtonClassNames: SlotClassNames = { + root: 'fui-CompoundButton', + icon: 'fui-CompoundButton__icon', + contentContainer: 'fui-CompoundButton__contentContainer', + secondaryContent: 'fui-CompoundButton__secondaryContent', +}; + +/** + * Attaches only semantic slot class names and state modifiers + */ +export const useCompoundButtonStyles_unstable = (state: CompoundButtonState): CompoundButtonState => { + 'use no memo'; + + const { appearance, disabled, disabledFocusable, icon, iconOnly, iconPosition, shape, size } = state; + + state.root.className = [ + compoundButtonClassNames.root, + + // Appearance + appearance && `${compoundButtonClassNames.root}--${appearance}`, + + // Size + size && `${compoundButtonClassNames.root}--${size}`, + + // Shape + shape && `${compoundButtonClassNames.root}--${shape}`, + + // Disabled styles + disabled && `${compoundButtonClassNames.root}--disabled`, + disabledFocusable && `${compoundButtonClassNames.root}--disabledFocusable`, + + // Icon styles + icon && iconPosition === 'before' && `${compoundButtonClassNames.root}--iconBefore`, + icon && iconPosition === 'after' && `${compoundButtonClassNames.root}--iconAfter`, + icon && iconOnly && `${compoundButtonClassNames.root}--iconOnly`, + + // User provided class name + state.root.className, + ] + .filter(Boolean) + .join(' '); + + if (state.icon) { + state.icon.className = [compoundButtonClassNames.icon, state.icon.className].filter(Boolean).join(' '); + } + + state.contentContainer.className = [compoundButtonClassNames.contentContainer, state.contentContainer.className] + .filter(Boolean) + .join(' '); + + if (state.secondaryContent) { + state.secondaryContent.className = [compoundButtonClassNames.secondaryContent, state.secondaryContent.className] + .filter(Boolean) + .join(' '); + } + + return state; +}; diff --git a/packages/react-components/react-button/library/src/components/MenuButton/useMenuButtonStyles.styles.headless.ts b/packages/react-components/react-button/library/src/components/MenuButton/useMenuButtonStyles.styles.headless.ts new file mode 100644 index 0000000000000..735d2024ca0a2 --- /dev/null +++ b/packages/react-components/react-button/library/src/components/MenuButton/useMenuButtonStyles.styles.headless.ts @@ -0,0 +1,58 @@ +'use client'; + +import type { SlotClassNames } from '@fluentui/react-utilities'; +import type { MenuButtonSlots, MenuButtonState } from './MenuButton.types'; + +export const menuButtonClassNames: SlotClassNames = { + root: 'fui-MenuButton', + icon: 'fui-MenuButton__icon', + menuIcon: 'fui-MenuButton__menuIcon', +}; + +/** + * Attaches only semantic slot class names and state modifiers + */ +export const useMenuButtonStyles_unstable = (state: MenuButtonState): MenuButtonState => { + 'use no memo'; + + const { appearance, disabled, disabledFocusable, shape, size, icon, iconOnly } = state; + const expanded = !!state.root['aria-expanded']; + + state.root.className = [ + menuButtonClassNames.root, + + // Appearance + appearance && `${menuButtonClassNames.root}--${appearance}`, + + // Size + size && `${menuButtonClassNames.root}--${size}`, + + // Shape + shape && `${menuButtonClassNames.root}--${shape}`, + + // Disabled styles + disabled && `${menuButtonClassNames.root}--disabled`, + disabledFocusable && `${menuButtonClassNames.root}--disabledFocusable`, + + // Expanded + expanded && `${menuButtonClassNames.root}--expanded`, + + // Icons + icon && iconOnly && `${menuButtonClassNames.root}--iconOnly`, + + // User provided class name + state.root.className, + ] + .filter(Boolean) + .join(' '); + + if (state.icon) { + state.icon.className = [menuButtonClassNames.icon, state.icon.className].filter(Boolean).join(' '); + } + + if (state.menuIcon) { + state.menuIcon.className = [menuButtonClassNames.menuIcon, state.menuIcon.className].filter(Boolean).join(' '); + } + + return state; +}; diff --git a/packages/react-components/react-button/library/src/components/SplitButton/useSplitButtonStyles.styles.headless.ts b/packages/react-components/react-button/library/src/components/SplitButton/useSplitButtonStyles.styles.headless.ts new file mode 100644 index 0000000000000..2822d6f7df43a --- /dev/null +++ b/packages/react-components/react-button/library/src/components/SplitButton/useSplitButtonStyles.styles.headless.ts @@ -0,0 +1,58 @@ +'use client'; + +import type { SlotClassNames } from '@fluentui/react-utilities'; +import type { SplitButtonSlots, SplitButtonState } from './SplitButton.types'; + +export const splitButtonClassNames: SlotClassNames = { + root: 'fui-SplitButton', + menuButton: 'fui-SplitButton__menuButton', + primaryActionButton: 'fui-SplitButton__primaryActionButton', +}; + +/** + * Attaches only semantic slot class names and state modifiers + */ +export const useSplitButtonStyles_unstable = (state: SplitButtonState): SplitButtonState => { + 'use no memo'; + + const { appearance, disabled, disabledFocusable, shape, size } = state; + + state.root.className = [ + splitButtonClassNames.root, + + // Appearance + appearance && `${splitButtonClassNames.root}--${appearance}`, + + // Size + size && `${splitButtonClassNames.root}--${size}`, + + // Shape + shape && `${splitButtonClassNames.root}--${shape}`, + + // Disabled styles + disabled && `${splitButtonClassNames.root}--disabled`, + disabledFocusable && !disabled && `${splitButtonClassNames.root}--disabledFocusable`, + + // User provided class name + state.root.className, + ] + .filter(Boolean) + .join(' '); + + if (state.primaryActionButton) { + state.primaryActionButton.className = [ + splitButtonClassNames.primaryActionButton, + state.primaryActionButton.className, + ] + .filter(Boolean) + .join(' '); + } + + if (state.menuButton) { + state.menuButton.className = [splitButtonClassNames.menuButton, state.menuButton.className] + .filter(Boolean) + .join(' '); + } + + return state; +}; diff --git a/packages/react-components/react-button/library/src/components/ToggleButton/useToggleButtonStyles.styles.headless.ts b/packages/react-components/react-button/library/src/components/ToggleButton/useToggleButtonStyles.styles.headless.ts new file mode 100644 index 0000000000000..20500ab961eeb --- /dev/null +++ b/packages/react-components/react-button/library/src/components/ToggleButton/useToggleButtonStyles.styles.headless.ts @@ -0,0 +1,53 @@ +'use client'; + +import type { SlotClassNames } from '@fluentui/react-utilities'; +import type { ButtonSlots } from '../Button/Button.types'; +import type { ToggleButtonState } from './ToggleButton.types'; + +export const toggleButtonClassNames: SlotClassNames = { + root: 'fui-ToggleButton', + icon: 'fui-ToggleButton__icon', +}; + +/** + * Attaches only semantic slot class names and state modifiers + */ +export const useToggleButtonStyles_unstable = (state: ToggleButtonState): ToggleButtonState => { + 'use no memo'; + + const { appearance, disabled, disabledFocusable, shape, size, checked, iconOnly } = state; + + state.root.className = [ + toggleButtonClassNames.root, + + // Appearance + appearance && `${toggleButtonClassNames.root}--${appearance}`, + + // Size + size && `${toggleButtonClassNames.root}--${size}`, + + // Shape + shape && `${toggleButtonClassNames.root}--${shape}`, + + // Checked + checked && `${toggleButtonClassNames.root}--checked`, + + // Icons + iconOnly && `${toggleButtonClassNames.root}--iconOnly`, + + // Disabled + disabled && `${toggleButtonClassNames.root}--disabled`, + disabledFocusable && `${toggleButtonClassNames.root}--disabledFocusable`, + + // User provided class name + state.root.className, + ] + .filter(Boolean) + .join(' '); + + if (state.icon) { + state.icon.className = [toggleButtonClassNames.icon, state.icon.className].filter(Boolean).join(' '); + } + + return state; +}; diff --git a/packages/react-components/react-divider/library/src/components/Divider/useDividerStyles.styles.headless.ts b/packages/react-components/react-divider/library/src/components/Divider/useDividerStyles.styles.headless.ts new file mode 100644 index 0000000000000..528c532f762e9 --- /dev/null +++ b/packages/react-components/react-divider/library/src/components/Divider/useDividerStyles.styles.headless.ts @@ -0,0 +1,45 @@ +'use client'; + +import { DividerSlots, DividerState } from './Divider.types'; +import type { SlotClassNames } from '@fluentui/react-utilities'; + +export const dividerClassNames: SlotClassNames = { + root: 'fui-Divider', + wrapper: 'fui-Divider__wrapper', +}; + +/** + * Attaches only semantic slot class names and state modifiers + */ +export const useDividerStyles_unstable = (state: DividerState): DividerState => { + 'use no memo'; + + const { alignContent, appearance, inset, vertical } = state; + + state.root.className = [ + dividerClassNames.root, + + // Alignment + `${dividerClassNames}--align-${alignContent}`, + + // Appearance + `${dividerClassNames}--${appearance}`, + + // Orientation + vertical ? `${dividerClassNames}--vertical` : `${dividerClassNames}--horizontal`, + + // Inset + inset && `${dividerClassNames}--inset`, + + // User provided class name + state.root.className, + ] + .filter(Boolean) + .join(' '); + + if (state.wrapper) { + state.wrapper.className = [dividerClassNames.wrapper, state.wrapper.className].filter(Boolean).join(' '); + } + + return state; +};