-
Notifications
You must be signed in to change notification settings - Fork 2.9k
feat(react-button): introduce headless style hooks for button components #35491
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
53c3830
896aea8
373fddf
169a630
75020f7
6cbe729
08e2f05
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,8 @@ const config = { | |
| tableName: 'fluentuilatest', | ||
| }), | ||
| bundler: webpackBundler(config => { | ||
| config.resolve ??= {}; | ||
| config.resolve.extensions = ['.headless.js', '.js', '.jsx', '.ts', '.tsx']; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can revert this, used it for bundle size report generation |
||
| return config; | ||
| }), | ||
| reportResolvers: { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| 'use client'; | ||
|
|
||
| import type { SlotClassNames } from '@fluentui/react-utilities'; | ||
| import type { ButtonSlots, ButtonState } from './Button.types'; | ||
|
|
||
| export const buttonClassNames: SlotClassNames<ButtonSlots> = { | ||
| 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) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not totally sure if we should just merge classes by hand, set up a shared utility, or maybe use mergeClasses from Griffel. Open to any ideas - what do you all think?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As I understand it, the main value of Seems like this won't be useful for headless so we should use something else like clsx. Would be great if we had a way for users to inject their own utility here but I also think something like
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the main benefit of using clsx or classnames over just concatenating the strings together?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding another dependency could get a bit confusing, since people might not know when to use
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the main benefit of |
||
| .join(' '); | ||
|
|
||
| if (state.icon) { | ||
| state.icon.className = [buttonClassNames.icon, state.icon.className].filter(Boolean).join(' '); | ||
| } | ||
|
|
||
| return state; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<CompoundButtonSlots> = { | ||
| 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; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| 'use client'; | ||
|
|
||
| import type { SlotClassNames } from '@fluentui/react-utilities'; | ||
| import type { MenuButtonSlots, MenuButtonState } from './MenuButton.types'; | ||
|
|
||
| export const menuButtonClassNames: SlotClassNames<MenuButtonSlots> = { | ||
| 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; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| 'use client'; | ||
|
|
||
| import type { SlotClassNames } from '@fluentui/react-utilities'; | ||
| import type { SplitButtonSlots, SplitButtonState } from './SplitButton.types'; | ||
|
|
||
| export const splitButtonClassNames: SlotClassNames<SplitButtonSlots> = { | ||
| 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; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<ButtonSlots> = { | ||
| 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; | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🕵🏾♀️ visual changes to review in the Visual Change Report
vr-tests-react-components/Charts-DonutChart 3 screenshots
vr-tests-react-components/Positioning 2 screenshots
vr-tests-react-components/TagPicker 3 screenshots
vr-tests/Callout 6 screenshots
vr-tests/react-charting-GaugeChart 1 screenshots
vr-tests/react-charting-LineChart 4 screenshots
vr-tests/react-charting-MultiStackBarChart 2 screenshots
vr-tests/react-charting-VerticalBarChart 1 screenshots
There were 4 duplicate changes discarded. Check the build logs for more information.