Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/vr-tests-web-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"lint": "ESLINT_USE_FLAT_CONFIG=false eslint src --ext .ts,.tsx",
"start": "storybook dev",
"type-check": "tsc -p . --baseUrl . --noEmit",
"test-vr": "storywright --browsers chromium --url dist/storybook --destpath dist/screenshots --waitTimeScreenshot 500 --concurrency 4 --headless true --stepsApi parameters"
"test-vr": "storywright --browsers chromium --url dist/storybook --destpath dist/screenshots --waitTimeScreenshot 500 --concurrency 4 --headless true --stepsApi parameters --bailOnStoriesError"
},
"dependencies": {
"react": "19.2.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default {
steps: new Steps()
.snapshot('normal', { cropTo: '.testWrapper' })
.executeScript(
'document.getElementsByTagName("fluent-checkbox").forEach(checkbox => checkbox.indeterminate = true)',
'Array.from(document.getElementsByTagName("fluent-checkbox")).forEach(checkbox => checkbox.indeterminate = true)',
)
.snapshot('indeterminate', { cropTo: '.testWrapper' })
.end(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const WithLongText = () =>
</style>
<p class="max-width">
This paragraph contains a link which is very long.
<fluent-link href="#">Fluent links wrap correctly between lines when they are very long.</fluent-link> This is
<fluent-link id="${linkId}" href="#">Fluent links wrap correctly between lines when they are very long.</fluent-link> This is
because they are inline elements.
</p>
`);
Original file line number Diff line number Diff line change
Expand Up @@ -175,14 +175,14 @@ export const RadioWithIconsRTL = getStoryVariant(RadioWithIcons, RTL);
export const WithSubmenu = () =>
parse(`
<fluent-menu-list>
<fluent-menu-item>
<fluent-menu-item aria-haspopup="menu">
Item 1
<fluent-menu-list slot="submenu">
<fluent-menu-item> Subitem 1.1 </fluent-menu-item>
<fluent-menu-item> Subitem 1.2 </fluent-menu-item>
</fluent-menu-list>
</fluent-menu-item>
<fluent-menu-item>
<fluent-menu-item aria-haspopup="menu">
Item 2
<fluent-menu-list slot="submenu">
<fluent-menu-item> Subitem 2.1 </fluent-menu-item>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ export default {
storyWright: {
steps: new Steps()
.snapshot('normal', { cropTo: '.testWrapper' })
.click('[role="radio"]:first-child')
.click('fluent-radio:first-child')
.snapshot('1st selected', { cropTo: '.testWrapper' })
.click('[role="radio"]:nth-child(2)')
.click('fluent-radio:nth-child(2)')
.snapshot('2nd selected', { cropTo: '.testWrapper' })
.end(),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ export default {
storyWright: {
steps: new Steps()
.snapshot('normal', { cropTo: '.testWrapper' })
.focus('[role="slider"]')
.keys('[role="slider"]', Keys.rightArrow)
.focus('fluent-slider')
.keys('fluent-slider', Keys.rightArrow)
.snapshot('rightArrow', { cropTo: '.testWrapper' })
.end(),
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{

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
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Charts-DonutChart.Dynamic - Dark Mode.default.chromium.png 12638 Changed
vr-tests-react-components/Charts-DonutChart.Dynamic.default.chromium.png 27057 Changed
vr-tests-react-components/Charts-DonutChart.Dynamic - RTL.default.chromium.png 30791 Changed
vr-tests-react-components/Positioning 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Positioning.Positioning end.chromium.png 878 Changed
vr-tests-react-components/Positioning.Positioning end.updated 2 times.chromium.png 157 Changed
vr-tests-react-components/TagPicker 3 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/TagPicker.disabled - RTL.disabled input hover.chromium.png 635 Changed
vr-tests-react-components/TagPicker.disabled - High Contrast.chromium.png 1319 Changed
vr-tests-react-components/TagPicker.disabled.disabled input hover.chromium.png 677 Changed
vr-tests/Callout 6 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests/Callout.Bottom right edge - RTL.default.chromium.png 1124 Changed
vr-tests/Callout.Bottom left edge.default.chromium.png 2195 Changed
vr-tests/Callout.Gap space 25.default.chromium.png 2195 Changed
vr-tests/Callout.Left bottom edge.default.chromium.png 3182 Changed
vr-tests/Callout.Right bottom edge.default.chromium.png 3095 Changed
vr-tests/Callout.Top right edge.default.chromium.png 1146 Changed
vr-tests/react-charting-GaugeChart 1 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests/react-charting-GaugeChart.Basic.default.chromium.png 2 Changed
vr-tests/react-charting-LineChart 4 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests/react-charting-LineChart.Events - Dark Mode.default.chromium.png 16 Changed
vr-tests/react-charting-LineChart.Multiple - Dark Mode.default.chromium.png 181 Changed
vr-tests/react-charting-LineChart.Multiple - RTL.default.chromium.png 200 Changed
vr-tests/react-charting-LineChart.Multiple.default.chromium.png 192 Changed
vr-tests/react-charting-MultiStackBarChart 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests/react-charting-MultiStackBarChart.Basic_Absolute - RTL.default.chromium.png 343 Changed
vr-tests/react-charting-MultiStackBarChart.Basic_Absolute.default.chromium.png 359 Changed
vr-tests/react-charting-VerticalBarChart 1 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests/react-charting-VerticalBarChart.Basic - Secondary Y Axis.default.chromium.png 3 Changed

There were 4 duplicate changes discarded. Check the build logs for more information.

"type": "minor",
"comment": "feat: introduce headless style hooks for button components",
"packageName": "@fluentui/react-button",
"email": "dmytrokirpa@microsoft.com",
"dependentChangeType": "patch"
}
2 changes: 2 additions & 0 deletions monosize.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const config = {
tableName: 'fluentuilatest',
}),
bundler: webpackBundler(config => {
config.resolve ??= {};
config.resolve.extensions = ['.headless.js', '.js', '.jsx', '.ts', '.tsx'];
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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: {
Expand Down
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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I understand it, the main value of mergeClasses is that, for Griffel styles, it ensures the last style "wins" for any specificity conflicts.

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 clsx or classnames is small enough that it should be fine to just pick one and ship it.

Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 mergeClasses vs clsx/classnames. Same goes for creating our own classname utility. So, I'm also leaning toward just sticking with inline string concatenation for now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the main benefit of clsx/classnames over concatenation is getting rid of undefined/nullish values, which don't affect the runtime but stay in the generated HTML, so they still affect the amount of bits the clients are pulling in.

.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;
};