From 3d24f1499aa6dbb31f031c7995217993be2f5edb Mon Sep 17 00:00:00 2001 From: Chris Holt <13071055+chrisdholt@users.noreply.github.com> Date: Wed, 6 May 2026 09:41:30 -0700 Subject: [PATCH 1/5] fix(web-components): remove references to fast-web-utilities (#36106) --- ...-d6a01ea1-26a1-4a60-9c84-2d82313c59d8.json | 7 +++++ .../web-components/docs/web-components.api.md | 20 ++++++++++++-- packages/web-components/package.json | 2 -- .../src/anchor-button/anchor-button.base.ts | 3 +-- .../web-components/src/button/button.base.ts | 3 +-- .../src/divider/divider.options.ts | 2 +- .../web-components/src/field/field.base.ts | 2 +- packages/web-components/src/index.ts | 3 ++- .../web-components/src/menu-item/menu-item.ts | 9 +++---- .../src/menu-list/menu-list.base.ts | 2 +- packages/web-components/src/menu/menu.ts | 9 +++---- .../src/radio-group/radio-group.base.ts | 26 ++++++------------- .../src/radio-group/radio-group.options.ts | 2 +- .../src/slider/slider-utilities.ts | 3 ++- .../src/slider/slider.options.ts | 4 +-- packages/web-components/src/slider/slider.ts | 26 +++++++------------ .../src/tablist/tablist.base.ts | 2 +- .../src/tablist/tablist.options.ts | 2 +- .../web-components/src/theme/set-theme.ts | 2 +- .../src/tooltip/tooltip.stories.ts | 2 +- .../web-components/src/tooltip/tooltip.ts | 2 +- packages/web-components/src/tree/tree.base.ts | 9 +++---- .../web-components/src/utils/direction.ts | 15 ++++++++++- packages/web-components/src/utils/index.ts | 2 ++ packages/web-components/src/utils/numbers.ts | 9 +++++++ .../web-components/src/utils/orientation.ts | 16 ++++++++++++ packages/web-components/src/utils/typings.ts | 7 +++++ packages/web-components/tensile.config.js | 1 - packages/web-components/tsconfig.json | 1 + packages/web-components/tsconfig.lib.json | 1 - 30 files changed, 120 insertions(+), 74 deletions(-) create mode 100644 change/@fluentui-web-components-d6a01ea1-26a1-4a60-9c84-2d82313c59d8.json create mode 100644 packages/web-components/src/utils/numbers.ts create mode 100644 packages/web-components/src/utils/orientation.ts diff --git a/change/@fluentui-web-components-d6a01ea1-26a1-4a60-9c84-2d82313c59d8.json b/change/@fluentui-web-components-d6a01ea1-26a1-4a60-9c84-2d82313c59d8.json new file mode 100644 index 00000000000000..731dd67fb661db --- /dev/null +++ b/change/@fluentui-web-components-d6a01ea1-26a1-4a60-9c84-2d82313c59d8.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "fix: remove deprecated fast-web-utilities references", + "packageName": "@fluentui/web-components", + "email": "13071055+chrisdholt@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/web-components/docs/web-components.api.md b/packages/web-components/docs/web-components.api.md index ee7caf3dacb89b..7eb64f59ffdf12 100644 --- a/packages/web-components/docs/web-components.api.md +++ b/packages/web-components/docs/web-components.api.md @@ -6,13 +6,11 @@ import { CaptureType } from '@microsoft/fast-element'; import { CSSDirective } from '@microsoft/fast-element'; -import { Direction } from '@microsoft/fast-web-utilities'; import { ElementStyles } from '@microsoft/fast-element'; import { ElementViewTemplate } from '@microsoft/fast-element'; import { FASTElement } from '@microsoft/fast-element'; import { FASTElementDefinition } from '@microsoft/fast-element'; import { HTMLDirective } from '@microsoft/fast-element'; -import { Orientation } from '@microsoft/fast-web-utilities'; import { SyntheticViewTemplate } from '@microsoft/fast-element'; import { ViewTemplate } from '@microsoft/fast-element'; @@ -2566,6 +2564,15 @@ export const DialogType: { // @public (undocumented) export type DialogType = ValuesOf; +// @public +export const Direction: { + readonly ltr: "ltr"; + readonly rtl: "rtl"; +}; + +// @public +export type Direction = (typeof Direction)[keyof typeof Direction]; + // Warning: (ae-forgotten-export) The symbol "CSSDisplayPropertyValue" needs to be exported by the entry point index.d.ts // // @public @@ -3419,6 +3426,15 @@ export const MessageBarStyles: ElementStyles; // @public export const MessageBarTemplate: ElementViewTemplate; +// @public +export const Orientation: { + readonly horizontal: "horizontal"; + readonly vertical: "vertical"; +}; + +// @public +export type Orientation = (typeof Orientation)[keyof typeof Orientation]; + // @public export class ProgressBar extends BaseProgressBar { shape?: ProgressBarShape; diff --git a/packages/web-components/package.json b/packages/web-components/package.json index 8a8b20a93454c8..60955bca734eef 100644 --- a/packages/web-components/package.json +++ b/packages/web-components/package.json @@ -88,9 +88,7 @@ "chromedriver": "^125.0.0" }, "dependencies": { - "@microsoft/fast-web-utilities": "^6.0.0", "@fluentui/tokens": "1.0.0-alpha.23", - "tabbable": "^6.2.0", "tslib": "^2.1.0" }, "peerDependencies": { diff --git a/packages/web-components/src/anchor-button/anchor-button.base.ts b/packages/web-components/src/anchor-button/anchor-button.base.ts index 400f9bf26acfeb..9681f2a382299a 100644 --- a/packages/web-components/src/anchor-button/anchor-button.base.ts +++ b/packages/web-components/src/anchor-button/anchor-button.base.ts @@ -1,5 +1,4 @@ import { attr, FASTElement, Observable } from '@microsoft/fast-element'; -import { keyEnter } from '@microsoft/fast-web-utilities'; import { AnchorAttributes, type AnchorTarget } from './anchor-button.options.js'; /** @@ -193,7 +192,7 @@ export class BaseAnchor extends FASTElement { */ public keydownHandler(e: KeyboardEvent): boolean | void { if (this.href) { - if (e.key === keyEnter) { + if (e.key === 'Enter') { const newTab = !this.isMac ? e.ctrlKey : e.metaKey || e.ctrlKey; this.handleNavigation(newTab); return; diff --git a/packages/web-components/src/button/button.base.ts b/packages/web-components/src/button/button.base.ts index e741408848a339..510c6ba31ec3fb 100644 --- a/packages/web-components/src/button/button.base.ts +++ b/packages/web-components/src/button/button.base.ts @@ -1,5 +1,4 @@ import { attr, FASTElement, observable } from '@microsoft/fast-element'; -import { keyEnter, keySpace } from '@microsoft/fast-web-utilities'; import { type ButtonFormTarget, ButtonType } from './button.options.js'; /** @@ -347,7 +346,7 @@ export class BaseButton extends FASTElement { return; } - if (e.key === keyEnter || e.key === keySpace) { + if (e.key === 'Enter' || e.key === ' ') { this.click(); return; } diff --git a/packages/web-components/src/divider/divider.options.ts b/packages/web-components/src/divider/divider.options.ts index 2c7c97eaf2c0ed..fd98d0f2a982e6 100644 --- a/packages/web-components/src/divider/divider.options.ts +++ b/packages/web-components/src/divider/divider.options.ts @@ -1,4 +1,4 @@ -import { Orientation } from '@microsoft/fast-web-utilities'; +import { Orientation } from '../utils/orientation.js'; import type { ValuesOf } from '../utils/typings.js'; import { FluentDesignSystem } from '../fluent-design-system.js'; diff --git a/packages/web-components/src/field/field.base.ts b/packages/web-components/src/field/field.base.ts index c92c33a312fd84..08cde269cf472e 100644 --- a/packages/web-components/src/field/field.base.ts +++ b/packages/web-components/src/field/field.base.ts @@ -1,5 +1,5 @@ import { FASTElement, observable } from '@microsoft/fast-element'; -import { uniqueId } from '@microsoft/fast-web-utilities'; +import { uniqueId } from '../utils/unique-id.js'; import { toggleState } from '../utils/element-internals.js'; import { type SlottableInput, ValidationFlags } from './field.options.js'; diff --git a/packages/web-components/src/index.ts b/packages/web-components/src/index.ts index 314e4b7f441440..4acba253a84633 100644 --- a/packages/web-components/src/index.ts +++ b/packages/web-components/src/index.ts @@ -331,5 +331,6 @@ export { export { BaseTree, Tree, TreeDefinition, TreeTemplate, TreeStyles } from './tree/index.js'; export { TreeItem, TreeItemDefinition, TreeItemTemplate, TreeItemStyles } from './tree-item/index.js'; export type { isTreeItem, TreeItemAppearance, TreeItemSize } from './tree-item/index.js'; -export { getDirection } from './utils/direction.js'; +export { Direction, getDirection } from './utils/direction.js'; +export { Orientation } from './utils/orientation.js'; export { display } from './utils/display.js'; diff --git a/packages/web-components/src/menu-item/menu-item.ts b/packages/web-components/src/menu-item/menu-item.ts index b6ea94d1f3bdd0..533f1cfaee9cdc 100644 --- a/packages/web-components/src/menu-item/menu-item.ts +++ b/packages/web-components/src/menu-item/menu-item.ts @@ -1,5 +1,4 @@ import { attr, FASTElement, observable } from '@microsoft/fast-element'; -import { keyArrowLeft, keyArrowRight, keyEnter, keySpace } from '@microsoft/fast-web-utilities'; import type { StartEndOptions } from '../patterns/start-end.js'; import { StartEnd } from '../patterns/start-end.js'; import { applyMixins } from '../utils/apply-mixins.js'; @@ -172,12 +171,12 @@ export class MenuItem extends FASTElement { } switch (e.key) { - case keyEnter: - case keySpace: + case 'Enter': + case ' ': this.invoke(); return false; - case keyArrowRight: + case 'ArrowRight': //open/focus on submenu if (!this.disabled) { this.submenu?.togglePopover(true); @@ -186,7 +185,7 @@ export class MenuItem extends FASTElement { return false; - case keyArrowLeft: + case 'ArrowLeft': //close submenu if (this.parentElement?.hasAttribute('popover')) { this.parentElement.togglePopover(false); diff --git a/packages/web-components/src/menu-list/menu-list.base.ts b/packages/web-components/src/menu-list/menu-list.base.ts index 404189e9668103..d08aaa46d3b2ae 100644 --- a/packages/web-components/src/menu-list/menu-list.base.ts +++ b/packages/web-components/src/menu-list/menu-list.base.ts @@ -1,5 +1,5 @@ import { FASTElement, observable, Updates } from '@microsoft/fast-element'; -import { isHTMLElement } from '@microsoft/fast-web-utilities'; +import { isHTMLElement } from '../utils/typings.js'; import type { MenuItemColumnCount } from '../menu-item/menu-item.js'; import type { MenuItem } from '../menu-item/menu-item.js'; import { isMenuItem, MenuItemRole } from '../menu-item/menu-item.options.js'; diff --git a/packages/web-components/src/menu/menu.ts b/packages/web-components/src/menu/menu.ts index 3d48840d8ae2e9..e61ea0279305f8 100644 --- a/packages/web-components/src/menu/menu.ts +++ b/packages/web-components/src/menu/menu.ts @@ -1,5 +1,4 @@ import { attr, FASTElement, observable, Updates } from '@microsoft/fast-element'; -import { keyEnter, keyEscape, keySpace, keyTab } from '@microsoft/fast-web-utilities'; import { MenuItem } from '../menu-item/menu-item.js'; import { MenuItemRole } from '../menu-item/menu-item.options.js'; @@ -402,14 +401,14 @@ export class Menu extends FASTElement { const key = e.key; switch (key) { - case keyEscape: + case 'Escape': e.preventDefault(); if (this._open) { this.closeMenu(); this.focusTrigger(); } break; - case keyTab: + case 'Tab': if (this._open) this.closeMenu(); if ( e.shiftKey && @@ -438,8 +437,8 @@ export class Menu extends FASTElement { } const key = e.key; switch (key) { - case keySpace: - case keyEnter: + case ' ': + case 'Enter': e.preventDefault(); this.toggleMenu(); break; diff --git a/packages/web-components/src/radio-group/radio-group.base.ts b/packages/web-components/src/radio-group/radio-group.base.ts index 45070ad7c42ccf..48161953041618 100644 --- a/packages/web-components/src/radio-group/radio-group.base.ts +++ b/packages/web-components/src/radio-group/radio-group.base.ts @@ -1,14 +1,4 @@ import { attr, FASTElement, Observable, observable } from '@microsoft/fast-element'; -import { - findLastIndex, - keyArrowDown, - keyArrowLeft, - keyArrowRight, - keyArrowUp, - keyEnd, - keyHome, - keySpace, -} from '@microsoft/fast-web-utilities'; import type { Radio } from '../radio/radio.js'; import { isRadio } from '../radio/radio.options.js'; import { RadioGroupOrientation } from './radio-group.options.js'; @@ -164,7 +154,7 @@ export class BaseRadioGroup extends FASTElement { this.name = next[0].name; } - const checkedIndex = findLastIndex(this.enabledRadios, x => x.initialChecked); + const checkedIndex = this.enabledRadios.findLastIndex(x => x.initialChecked); next.forEach((radio, index) => { radio.ariaPosInSet = `${index + 1}`; @@ -461,15 +451,15 @@ export class BaseRadioGroup extends FASTElement { */ public keydownHandler(e: KeyboardEvent): boolean | void { switch (e.key) { - case keyArrowUp: - case keyArrowDown: - case keyArrowLeft: - case keyArrowRight: - case keyHome: - case keyEnd: + case 'ArrowUp': + case 'ArrowDown': + case 'ArrowLeft': + case 'ArrowRight': + case 'Home': + case 'End': this.isNavigating = true; break; - case keySpace: + case ' ': this.checkRadio(); break; } diff --git a/packages/web-components/src/radio-group/radio-group.options.ts b/packages/web-components/src/radio-group/radio-group.options.ts index b94fa7771b6899..a933411b18b202 100644 --- a/packages/web-components/src/radio-group/radio-group.options.ts +++ b/packages/web-components/src/radio-group/radio-group.options.ts @@ -1,4 +1,4 @@ -import { Orientation } from '@microsoft/fast-web-utilities'; +import { Orientation } from '../utils/orientation.js'; import type { ValuesOf } from '../utils/typings.js'; import { FluentDesignSystem } from '../fluent-design-system.js'; diff --git a/packages/web-components/src/slider/slider-utilities.ts b/packages/web-components/src/slider/slider-utilities.ts index c797169cd9bc25..fe853b0ae0aaea 100644 --- a/packages/web-components/src/slider/slider-utilities.ts +++ b/packages/web-components/src/slider/slider-utilities.ts @@ -1,4 +1,5 @@ -import { Direction, limit } from '@microsoft/fast-web-utilities'; +import { Direction } from '../utils/direction.js'; +import { limit } from '../utils/numbers.js'; /** * Converts a pixel coordinate on the track to a percent of the track's range diff --git a/packages/web-components/src/slider/slider.options.ts b/packages/web-components/src/slider/slider.options.ts index c0090370222fee..3b685c15fbe947 100644 --- a/packages/web-components/src/slider/slider.options.ts +++ b/packages/web-components/src/slider/slider.options.ts @@ -1,5 +1,5 @@ -import type { Direction } from '@microsoft/fast-web-utilities'; -import { Orientation } from '@microsoft/fast-web-utilities'; +import type { Direction } from '../utils/direction.js'; +import { Orientation } from '../utils/orientation.js'; import type { StaticallyComposableHTML } from '../utils/template-helpers.js'; import type { ValuesOf } from '../utils/typings.js'; import { FluentDesignSystem } from '../fluent-design-system.js'; diff --git a/packages/web-components/src/slider/slider.ts b/packages/web-components/src/slider/slider.ts index a49f59a2ddde37..4fe92baff364bd 100644 --- a/packages/web-components/src/slider/slider.ts +++ b/packages/web-components/src/slider/slider.ts @@ -1,16 +1,8 @@ import type { ElementStyles } from '@microsoft/fast-element'; import { attr, css, FASTElement, observable, Observable, Updates } from '@microsoft/fast-element'; -import { - Direction, - keyArrowDown, - keyArrowLeft, - keyArrowRight, - keyArrowUp, - keyEnd, - keyHome, - limit, - Orientation, -} from '@microsoft/fast-web-utilities'; +import { Direction } from '../utils/direction.js'; +import { limit } from '../utils/numbers.js'; +import { Orientation } from '../utils/orientation.js'; import { numberLikeStringConverter } from '../utils/converters.js'; import { getDirection } from '../utils/direction.js'; import { convertPixelToPercent } from './slider-utilities.js'; @@ -612,29 +604,29 @@ export class Slider extends FASTElement implements SliderConfiguration { } switch (event.key) { - case keyHome: + case 'Home': event.preventDefault(); this.value = this.direction !== Direction.rtl && this.orientation !== Orientation.vertical ? `${this.minAsNumber}` : `${this.maxAsNumber}`; break; - case keyEnd: + case 'End': event.preventDefault(); this.value = this.direction !== Direction.rtl && this.orientation !== Orientation.vertical ? `${this.maxAsNumber}` : `${this.minAsNumber}`; break; - case keyArrowRight: - case keyArrowUp: + case 'ArrowRight': + case 'ArrowUp': if (!event.shiftKey) { event.preventDefault(); this.increment(); } break; - case keyArrowLeft: - case keyArrowDown: + case 'ArrowLeft': + case 'ArrowDown': if (!event.shiftKey) { event.preventDefault(); this.decrement(); diff --git a/packages/web-components/src/tablist/tablist.base.ts b/packages/web-components/src/tablist/tablist.base.ts index fad8d3d5508a47..325c99496b1984 100644 --- a/packages/web-components/src/tablist/tablist.base.ts +++ b/packages/web-components/src/tablist/tablist.base.ts @@ -1,5 +1,5 @@ import { attr, FASTElement, observable } from '@microsoft/fast-element'; -import { uniqueId } from '@microsoft/fast-web-utilities'; +import { uniqueId } from '../utils/unique-id.js'; import type { Tab } from '../tab/tab.js'; import { isTab } from '../tab/tab.options.js'; import { swapStates, toggleState } from '../utils/element-internals.js'; diff --git a/packages/web-components/src/tablist/tablist.options.ts b/packages/web-components/src/tablist/tablist.options.ts index 400539a81f5a2e..58abd3dd36c100 100644 --- a/packages/web-components/src/tablist/tablist.options.ts +++ b/packages/web-components/src/tablist/tablist.options.ts @@ -1,4 +1,4 @@ -import { Orientation } from '@microsoft/fast-web-utilities'; +import { Orientation } from '../utils/orientation.js'; import type { ValuesOf } from '../utils/typings.js'; import { FluentDesignSystem } from '../fluent-design-system.js'; diff --git a/packages/web-components/src/theme/set-theme.ts b/packages/web-components/src/theme/set-theme.ts index eb41323e431f07..91acd4743e00d4 100644 --- a/packages/web-components/src/theme/set-theme.ts +++ b/packages/web-components/src/theme/set-theme.ts @@ -1,5 +1,5 @@ import { Updates } from '@microsoft/fast-element'; -import { uniqueId } from '@microsoft/fast-web-utilities'; +import { uniqueId } from '../utils/unique-id.js'; /** * Not using the `Theme` type from `@fluentui/tokens` package to allow custom diff --git a/packages/web-components/src/tooltip/tooltip.stories.ts b/packages/web-components/src/tooltip/tooltip.stories.ts index 9a1de32d562e76..bb6ef4d4aef304 100644 --- a/packages/web-components/src/tooltip/tooltip.stories.ts +++ b/packages/web-components/src/tooltip/tooltip.stories.ts @@ -1,5 +1,5 @@ import { css, html, repeat } from '@microsoft/fast-element'; -import { uniqueId } from '@microsoft/fast-web-utilities'; +import { uniqueId } from '../utils/unique-id.js'; import { type Meta, renderComponent, type StoryArgs, type StoryObj } from '../helpers.stories.js'; import { definition } from './tooltip.definition.js'; import type { Tooltip as FluentTooltip } from './tooltip.js'; diff --git a/packages/web-components/src/tooltip/tooltip.ts b/packages/web-components/src/tooltip/tooltip.ts index c9a21f40e1b54e..9ff69650d7f0d9 100644 --- a/packages/web-components/src/tooltip/tooltip.ts +++ b/packages/web-components/src/tooltip/tooltip.ts @@ -1,5 +1,5 @@ import { attr, FASTElement, nullableNumberConverter, Updates } from '@microsoft/fast-element'; -import { uniqueId } from '@microsoft/fast-web-utilities'; +import { uniqueId } from '../utils/unique-id.js'; import { AnchorPositioningCSSSupported, AnchorPositioningHTMLSupported } from '../utils/support.js'; import type { TooltipPositioningOption } from './tooltip.options.js'; diff --git a/packages/web-components/src/tree/tree.base.ts b/packages/web-components/src/tree/tree.base.ts index 5ae9a950f0b651..aa8114ce468e98 100644 --- a/packages/web-components/src/tree/tree.base.ts +++ b/packages/web-components/src/tree/tree.base.ts @@ -1,5 +1,4 @@ import { FASTElement, observable } from '@microsoft/fast-element'; -import { keyArrowLeft, keyArrowRight, keyEnter, keySpace } from '@microsoft/fast-web-utilities'; import type { BaseTreeItem } from '../tree-item/tree-item.base.js'; import { isTreeItem } from '../tree-item/tree-item.options.js'; @@ -72,7 +71,7 @@ export class BaseTree extends FASTElement { } switch (e.key) { - case keyArrowLeft: { + case 'ArrowLeft': { if (item?.childTreeItems?.length && item.expanded) { item.expanded = false; } else if (isTreeItem(item.parentElement)) { @@ -80,7 +79,7 @@ export class BaseTree extends FASTElement { } return; } - case keyArrowRight: { + case 'ArrowRight': { if (item?.childTreeItems?.length) { if (!item.expanded) { item.expanded = true; @@ -88,13 +87,13 @@ export class BaseTree extends FASTElement { } return; } - case keyEnter: { + case 'Enter': { // In single-select trees where selection does not follow focus (see note below), // the default action is typically to select the focused node. this.clickHandler(e as Event); return; } - case keySpace: { + case ' ': { item.selected = true; return; } diff --git a/packages/web-components/src/utils/direction.ts b/packages/web-components/src/utils/direction.ts index b68d3d5755aab4..98854839bde388 100644 --- a/packages/web-components/src/utils/direction.ts +++ b/packages/web-components/src/utils/direction.ts @@ -1,6 +1,19 @@ //Copied from @microsoft/fast-foundation -import { Direction } from '@microsoft/fast-web-utilities'; +/** + * Expose ltr and rtl strings + * @public + */ +export const Direction = { + ltr: 'ltr', + rtl: 'rtl', +} as const; + +/** + * The direction type + * @public + */ +export type Direction = (typeof Direction)[keyof typeof Direction]; /** * Determines the current localization direction of an element. diff --git a/packages/web-components/src/utils/index.ts b/packages/web-components/src/utils/index.ts index 28802ac6dd4306..fbbf3a18fafe7e 100644 --- a/packages/web-components/src/utils/index.ts +++ b/packages/web-components/src/utils/index.ts @@ -1,5 +1,7 @@ export * from './converters.js'; export * from './direction.js'; +export * from './numbers.js'; +export * from './orientation.js'; export * from './typings.js'; export * from './template-helpers.js'; export * from './whitespace-filter.js'; diff --git a/packages/web-components/src/utils/numbers.ts b/packages/web-components/src/utils/numbers.ts new file mode 100644 index 00000000000000..95efcd47a26035 --- /dev/null +++ b/packages/web-components/src/utils/numbers.ts @@ -0,0 +1,9 @@ +// Copied from @microsoft/fast-web-utilities + +/** + * Ensures that a value is between a min and max value. If value is lower than min, min will be returned. + * If value is greater than max, max will be returned. + */ +export function limit(min: number, max: number, value: number): number { + return Math.min(Math.max(value, min), max); +} diff --git a/packages/web-components/src/utils/orientation.ts b/packages/web-components/src/utils/orientation.ts new file mode 100644 index 00000000000000..497cceb2b30d90 --- /dev/null +++ b/packages/web-components/src/utils/orientation.ts @@ -0,0 +1,16 @@ +// Copied from @microsoft/fast-web-utilities + +/** + * Standard orientation values + * @public + */ +export const Orientation = { + horizontal: 'horizontal', + vertical: 'vertical', +} as const; + +/** + * The orientation type + * @public + */ +export type Orientation = (typeof Orientation)[keyof typeof Orientation]; diff --git a/packages/web-components/src/utils/typings.ts b/packages/web-components/src/utils/typings.ts index 283e433f1c1624..fef60f6bd31f43 100644 --- a/packages/web-components/src/utils/typings.ts +++ b/packages/web-components/src/utils/typings.ts @@ -23,3 +23,10 @@ export function isCustomElement(tagSuffix: string): (elem return (element as Element).tagName.toLowerCase().endsWith(tagSuffix); }; } + +/** + * A test that ensures that all arguments are HTML Elements + */ +export function isHTMLElement(...args: any[]): boolean { + return args.every((arg: any) => arg instanceof HTMLElement); +} diff --git a/packages/web-components/tensile.config.js b/packages/web-components/tensile.config.js index d2531e5ea64879..06d172d742cebe 100644 --- a/packages/web-components/tensile.config.js +++ b/packages/web-components/tensile.config.js @@ -12,7 +12,6 @@ const config = { '@fluentui/tokens': '/tensile-assets/benchmark-dependencies/tokens.js', '@fluentui/web-components': '/node_modules/@fluentui/web-components/dist/esm/index.js', 'exenv-es6': '/node_modules/exenv-es6/dist/index.js', - tabbable: '/node_modules/tabbable/dist/index.esm.js', tslib: '/node_modules/tslib/tslib.es6.js', }, }; diff --git a/packages/web-components/tsconfig.json b/packages/web-components/tsconfig.json index 4484e77ae366b6..2391e39c3e0471 100644 --- a/packages/web-components/tsconfig.json +++ b/packages/web-components/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.base.wc.json", "compilerOptions": { + "lib": ["ESNext", "DOM", "DOM.Iterable"], "moduleResolution": "bundler", "experimentalDecorators": true, "resolveJsonModule": true, diff --git a/packages/web-components/tsconfig.lib.json b/packages/web-components/tsconfig.lib.json index d657492231492a..d9e0d5a6e11f8d 100644 --- a/packages/web-components/tsconfig.lib.json +++ b/packages/web-components/tsconfig.lib.json @@ -1,7 +1,6 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "lib": ["ESNext", "DOM", "DOM.Iterable"], "declaration": true, "outDir": "dist/esm", "importHelpers": true, From d57f87e85e38fe4b70ff74a775065cc3e21e8dd3 Mon Sep 17 00:00:00 2001 From: Zacky Ma Date: Wed, 6 May 2026 11:47:47 -0700 Subject: [PATCH 2/5] fix(web-components): menu not closing after menu-list loses keyboard focus (#36111) --- ...nts-2efee0b6-f5be-4ba7-bd0a-383d2acdc86a.json | 7 +++++++ package.json | 2 +- packages/web-components/package.json | 4 ++-- packages/web-components/src/menu/menu.spec.ts | 16 ++++++++++++++++ yarn.lock | 8 ++++---- 5 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 change/@fluentui-web-components-2efee0b6-f5be-4ba7-bd0a-383d2acdc86a.json diff --git a/change/@fluentui-web-components-2efee0b6-f5be-4ba7-bd0a-383d2acdc86a.json b/change/@fluentui-web-components-2efee0b6-f5be-4ba7-bd0a-383d2acdc86a.json new file mode 100644 index 00000000000000..f3d4cc551d445f --- /dev/null +++ b/change/@fluentui-web-components-2efee0b6-f5be-4ba7-bd0a-383d2acdc86a.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "fix menu not closing when menu list loses keyboard focus", + "packageName": "@fluentui/web-components", + "email": "machi@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/package.json b/package.json index 6da693a8bdd197..756753194acc26 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "@microsoft/api-extractor": "7.51.0", "@microsoft/api-extractor-model": "7.31.2", "@microsoft/eslint-plugin-sdl": "1.0.1", - "@microsoft/focusgroup-polyfill": "^1.3.0", + "@microsoft/focusgroup-polyfill": "^1.4.1", "@microsoft/load-themed-styles": "1.10.26", "@microsoft/loader-load-themed-styles": "2.0.17", "@microsoft/tsdoc": "0.15.1", diff --git a/packages/web-components/package.json b/packages/web-components/package.json index 60955bca734eef..7eeb69b390c03e 100644 --- a/packages/web-components/package.json +++ b/packages/web-components/package.json @@ -77,7 +77,7 @@ "devDependencies": { "@custom-elements-manifest/analyzer": "0.10.10", "@microsoft/fast-element": "2.0.0", - "@microsoft/focusgroup-polyfill": "^1.3.0", + "@microsoft/focusgroup-polyfill": "^1.4.1", "@tensile-perf/web-components": "~0.2.2", "@storybook/html": "9.1.17", "@storybook/html-vite": "9.1.17", @@ -93,7 +93,7 @@ }, "peerDependencies": { "@microsoft/fast-element": "^2.0.0", - "@microsoft/focusgroup-polyfill": "^1.3.0" + "@microsoft/focusgroup-polyfill": "^1.4.1" }, "beachball": { "disallowedChangeTypes": [ diff --git a/packages/web-components/src/menu/menu.spec.ts b/packages/web-components/src/menu/menu.spec.ts index 5de1fb4bf7e786..90aa168ed5d2de 100644 --- a/packages/web-components/src/menu/menu.spec.ts +++ b/packages/web-components/src/menu/menu.spec.ts @@ -139,6 +139,22 @@ test.describe('Menu', () => { await expect(menuList).toBeHidden(); }); + test('should close when the menu list loses keyboard focus', async ({ fastPage, page }) => { + const { element } = fastPage; + const menuButton = element.locator('fluent-menu-button'); + const menuList = element.locator('fluent-menu-list'); + const menuItems = element.locator('fluent-menu-item'); + + await menuButton.click(); + + await expect(menuList).toBeVisible(); + await expect(menuItems.nth(0)).toBeFocused(); + + await page.keyboard.press('Tab'); + + await expect(menuList).toBeHidden(); + }); + test('should NOT open on hover when the `openOnHover` property is false', async ({ fastPage }) => { const { element } = fastPage; const menuButton = element.locator('fluent-menu-button'); diff --git a/yarn.lock b/yarn.lock index c268a50e5ecbb5..6548596d4b514a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2699,10 +2699,10 @@ dependencies: exenv-es6 "^1.1.1" -"@microsoft/focusgroup-polyfill@^1.3.0": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@microsoft/focusgroup-polyfill/-/focusgroup-polyfill-1.3.0.tgz#12a4e9abac1b7736ca7c4f59c923a45713a8c274" - integrity sha512-rlhfgqEsC6jR36/LSrl4dd0xQha05gGv5q7cfz+dnGKGVcsCo6CI6C0GYiq+jSja+VzVY97fRQnMiBJR1EgOeg== +"@microsoft/focusgroup-polyfill@^1.4.1": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@microsoft/focusgroup-polyfill/-/focusgroup-polyfill-1.4.1.tgz#0d3feee675775f7a692a3b1e974eea107503c78e" + integrity sha512-nXn/kJ5ZnzpR+TWYGV+nU2yaHPJORjWf2lSQJ90VT6Ydsmmkaz/yOYW3CizDGPPsA7ouAu9K8khCUeyfBcz7UA== "@microsoft/load-themed-styles@1.10.26", "@microsoft/load-themed-styles@^1.10.26": version "1.10.26" From f54d65c5514ad28e064c151a637021bc2841c506 Mon Sep 17 00:00:00 2001 From: Fluent UI Build Date: Wed, 6 May 2026 19:08:49 +0000 Subject: [PATCH 3/5] release: applying package updates - web-components --- ...-2efee0b6-f5be-4ba7-bd0a-383d2acdc86a.json | 7 ------- ...-d6a01ea1-26a1-4a60-9c84-2d82313c59d8.json | 7 ------- .../chart-web-components/CHANGELOG.json | 15 +++++++++++++ .../charts/chart-web-components/CHANGELOG.md | 11 +++++++++- .../charts/chart-web-components/package.json | 4 ++-- packages/web-components/CHANGELOG.json | 21 +++++++++++++++++++ packages/web-components/CHANGELOG.md | 12 ++++++++++- packages/web-components/package.json | 2 +- 8 files changed, 60 insertions(+), 19 deletions(-) delete mode 100644 change/@fluentui-web-components-2efee0b6-f5be-4ba7-bd0a-383d2acdc86a.json delete mode 100644 change/@fluentui-web-components-d6a01ea1-26a1-4a60-9c84-2d82313c59d8.json diff --git a/change/@fluentui-web-components-2efee0b6-f5be-4ba7-bd0a-383d2acdc86a.json b/change/@fluentui-web-components-2efee0b6-f5be-4ba7-bd0a-383d2acdc86a.json deleted file mode 100644 index f3d4cc551d445f..00000000000000 --- a/change/@fluentui-web-components-2efee0b6-f5be-4ba7-bd0a-383d2acdc86a.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "prerelease", - "comment": "fix menu not closing when menu list loses keyboard focus", - "packageName": "@fluentui/web-components", - "email": "machi@microsoft.com", - "dependentChangeType": "patch" -} diff --git a/change/@fluentui-web-components-d6a01ea1-26a1-4a60-9c84-2d82313c59d8.json b/change/@fluentui-web-components-d6a01ea1-26a1-4a60-9c84-2d82313c59d8.json deleted file mode 100644 index 731dd67fb661db..00000000000000 --- a/change/@fluentui-web-components-d6a01ea1-26a1-4a60-9c84-2d82313c59d8.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "prerelease", - "comment": "fix: remove deprecated fast-web-utilities references", - "packageName": "@fluentui/web-components", - "email": "13071055+chrisdholt@users.noreply.github.com", - "dependentChangeType": "patch" -} diff --git a/packages/charts/chart-web-components/CHANGELOG.json b/packages/charts/chart-web-components/CHANGELOG.json index 7649a3b86e4922..5c9dc80df7c8af 100644 --- a/packages/charts/chart-web-components/CHANGELOG.json +++ b/packages/charts/chart-web-components/CHANGELOG.json @@ -1,6 +1,21 @@ { "name": "@fluentui/chart-web-components", "entries": [ + { + "date": "Wed, 06 May 2026 19:08:43 GMT", + "tag": "@fluentui/chart-web-components_v0.0.75", + "version": "0.0.75", + "comments": { + "patch": [ + { + "author": "beachball", + "package": "@fluentui/chart-web-components", + "comment": "Bump @fluentui/web-components to v3.0.0-rc.17", + "commit": "d57f87e85e38fe4b70ff74a775065cc3e21e8dd3" + } + ] + } + }, { "date": "Wed, 06 May 2026 04:07:36 GMT", "tag": "@fluentui/chart-web-components_v0.0.74", diff --git a/packages/charts/chart-web-components/CHANGELOG.md b/packages/charts/chart-web-components/CHANGELOG.md index 4da4924f1aa258..76471b9929c52a 100644 --- a/packages/charts/chart-web-components/CHANGELOG.md +++ b/packages/charts/chart-web-components/CHANGELOG.md @@ -1,9 +1,18 @@ # Change Log - @fluentui/chart-web-components -This log was last generated on Wed, 06 May 2026 04:07:36 GMT and should not be manually modified. +This log was last generated on Wed, 06 May 2026 19:08:43 GMT and should not be manually modified. +## [0.0.75](https://github.com/microsoft/fluentui/tree/@fluentui/chart-web-components_v0.0.75) + +Wed, 06 May 2026 19:08:43 GMT +[Compare changes](https://github.com/microsoft/fluentui/compare/@fluentui/chart-web-components_v0.0.74..@fluentui/chart-web-components_v0.0.75) + +### Patches + +- Bump @fluentui/web-components to v3.0.0-rc.17 ([PR #36111](https://github.com/microsoft/fluentui/pull/36111) by beachball) + ## [0.0.74](https://github.com/microsoft/fluentui/tree/@fluentui/chart-web-components_v0.0.74) Wed, 06 May 2026 04:07:36 GMT diff --git a/packages/charts/chart-web-components/package.json b/packages/charts/chart-web-components/package.json index c8aa8aabb24fed..5c10d3e9f10310 100644 --- a/packages/charts/chart-web-components/package.json +++ b/packages/charts/chart-web-components/package.json @@ -1,7 +1,7 @@ { "name": "@fluentui/chart-web-components", "description": "A library of Fluent Chart Web Components", - "version": "0.0.74", + "version": "0.0.75", "author": { "name": "Microsoft" }, @@ -70,7 +70,7 @@ "dependencies": { "@microsoft/fast-web-utilities": "^6.0.0", "@fluentui/tokens": "^1.0.0-alpha.23", - "@fluentui/web-components": "^3.0.0-rc.16", + "@fluentui/web-components": "^3.0.0-rc.17", "@types/d3-selection": "^3.0.0", "@types/d3-shape": "^3.0.0", "d3-selection": "^3.0.0", diff --git a/packages/web-components/CHANGELOG.json b/packages/web-components/CHANGELOG.json index cf804e76a11b4a..9d63740a6390ca 100644 --- a/packages/web-components/CHANGELOG.json +++ b/packages/web-components/CHANGELOG.json @@ -1,6 +1,27 @@ { "name": "@fluentui/web-components", "entries": [ + { + "date": "Wed, 06 May 2026 19:08:42 GMT", + "tag": "@fluentui/web-components_v3.0.0-rc.17", + "version": "3.0.0-rc.17", + "comments": { + "prerelease": [ + { + "author": "machi@microsoft.com", + "package": "@fluentui/web-components", + "commit": "d57f87e85e38fe4b70ff74a775065cc3e21e8dd3", + "comment": "fix menu not closing when menu list loses keyboard focus" + }, + { + "author": "13071055+chrisdholt@users.noreply.github.com", + "package": "@fluentui/web-components", + "commit": "3d24f1499aa6dbb31f031c7995217993be2f5edb", + "comment": "fix: remove deprecated fast-web-utilities references" + } + ] + } + }, { "date": "Wed, 06 May 2026 04:07:34 GMT", "tag": "@fluentui/web-components_v3.0.0-rc.16", diff --git a/packages/web-components/CHANGELOG.md b/packages/web-components/CHANGELOG.md index c2367c91d0f9a8..950bfecdabbe0c 100644 --- a/packages/web-components/CHANGELOG.md +++ b/packages/web-components/CHANGELOG.md @@ -1,9 +1,19 @@ # Change Log - @fluentui/web-components -This log was last generated on Wed, 06 May 2026 04:07:34 GMT and should not be manually modified. +This log was last generated on Wed, 06 May 2026 19:08:42 GMT and should not be manually modified. +## [3.0.0-rc.17](https://github.com/microsoft/fluentui/tree/@fluentui/web-components_v3.0.0-rc.17) + +Wed, 06 May 2026 19:08:42 GMT +[Compare changes](https://github.com/microsoft/fluentui/compare/@fluentui/web-components_v3.0.0-rc.16..@fluentui/web-components_v3.0.0-rc.17) + +### Changes + +- fix menu not closing when menu list loses keyboard focus ([PR #36111](https://github.com/microsoft/fluentui/pull/36111) by machi@microsoft.com) +- fix: remove deprecated fast-web-utilities references ([PR #36106](https://github.com/microsoft/fluentui/pull/36106) by 13071055+chrisdholt@users.noreply.github.com) + ## [3.0.0-rc.16](https://github.com/microsoft/fluentui/tree/@fluentui/web-components_v3.0.0-rc.16) Wed, 06 May 2026 04:07:34 GMT diff --git a/packages/web-components/package.json b/packages/web-components/package.json index 7eeb69b390c03e..cbed415018f1e3 100644 --- a/packages/web-components/package.json +++ b/packages/web-components/package.json @@ -1,7 +1,7 @@ { "name": "@fluentui/web-components", "description": "A library of Fluent Web Components", - "version": "3.0.0-rc.16", + "version": "3.0.0-rc.17", "author": { "name": "Microsoft", "url": "https://discord.gg/FcSNfg4" From 4fa64db43f604aa07abfd56f81a8ce90b9b97d19 Mon Sep 17 00:00:00 2001 From: Robert Penner Date: Wed, 6 May 2026 16:40:56 -0400 Subject: [PATCH 4/5] docs(motion): home pages for motion system & motion components (#35737) --- ...-44d5d8fd-6710-49df-8937-48b7e73075a4.json | 7 + ...-643db992-5d5b-4a0a-adbb-b8bf361f41c3.json | 7 + .../library/README.md | 92 ++- .../stories/package.json | 10 +- .../stories/src/Atoms/AtomsDemo.styles.ts | 68 ++ .../stories/src/Atoms/AtomsDemo.tsx | 115 ++++ .../src/Atoms/ComposingAtomsDemo.styles.ts | 83 +++ .../stories/src/Atoms/ComposingAtomsDemo.tsx | 259 +++++++ .../stories/src/Atoms/index.mdx | 183 +++++ .../src/Introduction/ComponentsGrid.styles.ts | 48 ++ .../src/Introduction/ComponentsGrid.tsx | 90 +++ .../src/Introduction/InOutDemo.styles.ts | 69 ++ .../stories/src/Introduction/InOutDemo.tsx | 63 ++ .../src/Introduction/VariantsDemo.styles.ts | 66 ++ .../stories/src/Introduction/VariantsDemo.tsx | 70 ++ .../stories/src/Introduction/index.mdx | 289 ++++++++ .../react-motion/library/README.md | 62 +- .../Introduction/MotionIntroDemo.styles.ts | 19 + .../src/Introduction/MotionIntroDemo.tsx | 28 + .../MotionVsPresenceDemo.styles.ts | 78 +++ .../src/Introduction/MotionVsPresenceDemo.tsx | 112 +++ .../stories/src/Introduction/index.mdx | 238 +++++++ .../src/Introduction/index.stories.tsx | 9 + .../stories/src/Migration/index.mdx | 643 ++++++++++++++++++ 24 files changed, 2703 insertions(+), 5 deletions(-) create mode 100644 change/@fluentui-react-motion-44d5d8fd-6710-49df-8937-48b7e73075a4.json create mode 100644 change/@fluentui-react-motion-components-preview-643db992-5d5b-4a0a-adbb-b8bf361f41c3.json create mode 100644 packages/react-components/react-motion-components-preview/stories/src/Atoms/AtomsDemo.styles.ts create mode 100644 packages/react-components/react-motion-components-preview/stories/src/Atoms/AtomsDemo.tsx create mode 100644 packages/react-components/react-motion-components-preview/stories/src/Atoms/ComposingAtomsDemo.styles.ts create mode 100644 packages/react-components/react-motion-components-preview/stories/src/Atoms/ComposingAtomsDemo.tsx create mode 100644 packages/react-components/react-motion-components-preview/stories/src/Atoms/index.mdx create mode 100644 packages/react-components/react-motion-components-preview/stories/src/Introduction/ComponentsGrid.styles.ts create mode 100644 packages/react-components/react-motion-components-preview/stories/src/Introduction/ComponentsGrid.tsx create mode 100644 packages/react-components/react-motion-components-preview/stories/src/Introduction/InOutDemo.styles.ts create mode 100644 packages/react-components/react-motion-components-preview/stories/src/Introduction/InOutDemo.tsx create mode 100644 packages/react-components/react-motion-components-preview/stories/src/Introduction/VariantsDemo.styles.ts create mode 100644 packages/react-components/react-motion-components-preview/stories/src/Introduction/VariantsDemo.tsx create mode 100644 packages/react-components/react-motion-components-preview/stories/src/Introduction/index.mdx create mode 100644 packages/react-components/react-motion/stories/src/Introduction/MotionIntroDemo.styles.ts create mode 100644 packages/react-components/react-motion/stories/src/Introduction/MotionIntroDemo.tsx create mode 100644 packages/react-components/react-motion/stories/src/Introduction/MotionVsPresenceDemo.styles.ts create mode 100644 packages/react-components/react-motion/stories/src/Introduction/MotionVsPresenceDemo.tsx create mode 100644 packages/react-components/react-motion/stories/src/Introduction/index.mdx create mode 100644 packages/react-components/react-motion/stories/src/Introduction/index.stories.tsx create mode 100644 packages/react-components/react-motion/stories/src/Migration/index.mdx diff --git a/change/@fluentui-react-motion-44d5d8fd-6710-49df-8937-48b7e73075a4.json b/change/@fluentui-react-motion-44d5d8fd-6710-49df-8937-48b7e73075a4.json new file mode 100644 index 00000000000000..64b131f566d882 --- /dev/null +++ b/change/@fluentui-react-motion-44d5d8fd-6710-49df-8937-48b7e73075a4.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "docs(motion): add motion system docs", + "packageName": "@fluentui/react-motion", + "email": "robertpenner@microsoft.com", + "dependentChangeType": "none" +} diff --git a/change/@fluentui-react-motion-components-preview-643db992-5d5b-4a0a-adbb-b8bf361f41c3.json b/change/@fluentui-react-motion-components-preview-643db992-5d5b-4a0a-adbb-b8bf361f41c3.json new file mode 100644 index 00000000000000..fb1b21d7ef7db3 --- /dev/null +++ b/change/@fluentui-react-motion-components-preview-643db992-5d5b-4a0a-adbb-b8bf361f41c3.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "docs(motion): add motion components docs", + "packageName": "@fluentui/react-motion-components-preview", + "email": "robertpenner@microsoft.com", + "dependentChangeType": "none" +} diff --git a/packages/react-components/react-motion-components-preview/library/README.md b/packages/react-components/react-motion-components-preview/library/README.md index 986aafe2c27e38..4df0ec3995f013 100644 --- a/packages/react-components/react-motion-components-preview/library/README.md +++ b/packages/react-components/react-motion-components-preview/library/README.md @@ -1,5 +1,93 @@ # @fluentui/react-motion-components-preview -**React Motion Components for [Fluent UI React](https://react.fluentui.dev/)** +**Pre-built Motion Components for [Fluent UI React](https://react.fluentui.dev/)** -These are not production-ready components and **should never be used in product**. This space is useful for testing new components whose APIs might change before final release. +> ⚠️ **Preview Package**: These components are in beta and APIs may change before stable release. + +Ready-to-use presence components for common UI animation patterns, built on top of `@fluentui/react-motion`. + +## Components + +| Component | Description | +| ------------ | ---------------------------------------------------------- | +| **Fade** | Opacity transitions for tooltips, notifications, overlays | +| **Scale** | Size animations for popovers, menus, emphasis | +| **Collapse** | Height/width expansion for accordions, expandable sections | +| **Slide** | Directional movement for drawers, panels, carousels | +| **Blur** | Focus/defocus effects for backgrounds, depth | +| **Rotate** | 3D rotation for card flips, reveals | +| **Stagger** | Choreography for sequential list animations | + +Each component (except Blur and Rotate) comes with **Snappy** (150ms) and **Relaxed** (250ms) timing variants. + +## Installation + +```bash +npm install @fluentui/react-motion-components-preview +# or +yarn add @fluentui/react-motion-components-preview +``` + +## Quick Start + +```tsx +import { Fade, Scale, Slide, Collapse } from '@fluentui/react-motion-components-preview'; + +// Simple fade +function Tooltip({ visible, children }) { + return ( + + {children} + + ); +} + +// Slide from the right +function Drawer({ open, children }) { + return ( + + {children} + + ); +} + +// Use timing variants +import { FadeSnappy, ScaleRelaxed } from '@fluentui/react-motion-components-preview'; + +Quick feedback +Smooth entrance +``` + +### The `.In` and `.Out` Pattern + +Every presence component includes one-way sub-components: + +```tsx +// One-way enter animation (plays on mount) + +
Fades in once
+
+ +// One-way exit animation (plays on mount) + +
Fades out once
+
+``` + +## Documentation + +πŸ“š **[Full documentation](https://react.fluentui.dev/?path=/docs/motion-components-preview-introduction--docs)** + +- [Introduction](https://react.fluentui.dev/?path=/docs/motion-components-preview-introduction--docs) β€” Overview of all components +- [Fade](https://react.fluentui.dev/?path=/docs/motion-components-preview-fade--docs) +- [Scale](https://react.fluentui.dev/?path=/docs/motion-components-preview-scale--docs) +- [Collapse](https://react.fluentui.dev/?path=/docs/motion-components-preview-collapse--docs) +- [Slide](https://react.fluentui.dev/?path=/docs/motion-components-preview-slide--docs) +- [Blur](https://react.fluentui.dev/?path=/docs/motion-components-preview-blur--docs) +- [Rotate](https://react.fluentui.dev/?path=/docs/motion-components-preview-rotate--docs) +- [Stagger](https://react.fluentui.dev/?path=/docs/motion-choreography-preview-stagger--docs) +- [Motion Atoms](https://react.fluentui.dev/?path=/docs/motion-components-preview-atoms--docs) β€” Building blocks for custom components + +## Related + +- **[@fluentui/react-motion](https://www.npmjs.com/package/@fluentui/react-motion)** β€” Core motion APIs for creating custom animations diff --git a/packages/react-components/react-motion-components-preview/stories/package.json b/packages/react-components/react-motion-components-preview/stories/package.json index d9f61427c4985f..a9cc291db98dfc 100644 --- a/packages/react-components/react-motion-components-preview/stories/package.json +++ b/packages/react-components/react-motion-components-preview/stories/package.json @@ -1,5 +1,13 @@ { "name": "@fluentui/react-motion-components-preview-stories", "version": "0.0.0", - "private": true + "private": true, + "dependencies": { + "@fluentui/react-components": "*", + "@fluentui/react-icons": "^2.0.245", + "@fluentui/react-motion": "*", + "@fluentui/react-motion-components-preview": "*", + "@fluentui/react-shared-contexts": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } } diff --git a/packages/react-components/react-motion-components-preview/stories/src/Atoms/AtomsDemo.styles.ts b/packages/react-components/react-motion-components-preview/stories/src/Atoms/AtomsDemo.styles.ts new file mode 100644 index 00000000000000..4de86b5898d282 --- /dev/null +++ b/packages/react-components/react-motion-components-preview/stories/src/Atoms/AtomsDemo.styles.ts @@ -0,0 +1,68 @@ +'use client'; + +import { makeStyles, tokens } from '@fluentui/react-components'; + +export const useClasses = makeStyles({ + container: { + display: 'flex', + flexDirection: 'column', + border: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke1}`, + borderRadius: tokens.borderRadiusMedium, + marginTop: tokens.spacingVerticalXXL, + marginBottom: tokens.spacingVerticalXXL, + overflow: 'hidden', + }, + controls: { + display: 'flex', + alignItems: 'center', + gap: tokens.spacingHorizontalL, + flexWrap: 'wrap', + padding: `${tokens.spacingVerticalL} ${tokens.spacingHorizontalXL}`, + backgroundColor: tokens.colorNeutralBackground2, + borderBottom: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke1}`, + }, + body: { + display: 'flex', + '@media (max-width: 600px)': { + flexDirection: 'column', + }, + }, + demoArea: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + minHeight: '120px', + flex: '0 0 200px', + backgroundColor: tokens.colorNeutralBackground1, + padding: tokens.spacingVerticalXL, + }, + demoBox: { + width: '100px', + height: '80px', + backgroundColor: tokens.colorNeutralBackground3, + border: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke1}`, + borderRadius: tokens.borderRadiusMedium, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + color: tokens.colorNeutralForeground1, + fontWeight: tokens.fontWeightSemibold, + }, + codeArea: { + flex: '1 1 auto', + minWidth: 0, + margin: 0, + padding: `${tokens.spacingVerticalL} ${tokens.spacingHorizontalXL}`, + backgroundColor: tokens.colorNeutralBackground3, + borderLeft: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke1}`, + overflow: 'auto', + fontFamily: tokens.fontFamilyMonospace, + fontSize: tokens.fontSizeBase100, + lineHeight: tokens.lineHeightBase200, + color: tokens.colorNeutralForeground1, + '@media (max-width: 600px)': { + borderLeft: 'none', + borderTop: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke1}`, + }, + }, +}); diff --git a/packages/react-components/react-motion-components-preview/stories/src/Atoms/AtomsDemo.tsx b/packages/react-components/react-motion-components-preview/stories/src/Atoms/AtomsDemo.tsx new file mode 100644 index 00000000000000..203bf7e3d182cb --- /dev/null +++ b/packages/react-components/react-motion-components-preview/stories/src/Atoms/AtomsDemo.tsx @@ -0,0 +1,115 @@ +'use client'; + +import * as React from 'react'; +import { Button, Select } from '@fluentui/react-components'; +import { createMotionComponent, motionTokens } from '@fluentui/react-motion'; +import { fadeAtom, scaleAtom, slideAtom, rotateAtom, blurAtom } from '@fluentui/react-motion-components-preview'; +import { useClasses } from './AtomsDemo.styles'; + +type AtomType = 'fade' | 'scale' | 'slide' | 'rotate' | 'blur'; + +const demoDuration = 600; + +const createAtomMotion = (type: AtomType) => { + const easing = motionTokens.curveDecelerateMid; + + switch (type) { + case 'fade': + return createMotionComponent(fadeAtom({ direction: 'enter', duration: demoDuration, easing })); + case 'scale': + return createMotionComponent(scaleAtom({ direction: 'enter', duration: demoDuration, easing, outScale: 0.5 })); + case 'slide': + return createMotionComponent(slideAtom({ direction: 'enter', duration: demoDuration, easing, outY: '30px' })); + case 'rotate': + return createMotionComponent( + rotateAtom({ direction: 'enter', duration: demoDuration, easing, axis: 'z', outAngle: -90 }), + ); + case 'blur': + return createMotionComponent(blurAtom({ direction: 'enter', duration: demoDuration, easing, outRadius: '10px' })); + default: { + const _exhaustive: never = type; + throw new Error(`Unhandled atom type: ${_exhaustive}`); + } + } +}; + +const atomLabels: Record = { + fade: 'fadeAtom', + scale: 'scaleAtom', + slide: 'slideAtom', + rotate: 'rotateAtom', + blur: 'blurAtom', +}; + +const atomCodeSnippets: Record = { + fade: `fadeAtom({ + direction: 'enter', + duration: 600, + outOpacity: 0, + inOpacity: 1, +})`, + scale: `scaleAtom({ + direction: 'enter', + duration: 600, + outScale: 0.5, + inScale: 1, +})`, + slide: `slideAtom({ + direction: 'enter', + duration: 600, + outY: '30px', + inY: '0px', +})`, + rotate: `rotateAtom({ + direction: 'enter', + duration: 600, + axis: 'z', + outAngle: -90, + inAngle: 0, +})`, + blur: `blurAtom({ + direction: 'enter', + duration: 600, + outRadius: '10px', + inRadius: '0px', +})`, +}; + +export const AtomsDemo: React.FC = () => { + const classes = useClasses(); + const [atomType, setAtomType] = React.useState('fade'); + const [key, setKey] = React.useState(0); + + const MotionComponent = React.useMemo(() => createAtomMotion(atomType), [atomType]); + + const handleAtomChange = React.useCallback((event: React.ChangeEvent) => { + setAtomType(event.target.value as AtomType); + setKey(k => k + 1); + }, []); + const handleReplay = React.useCallback(() => setKey(k => k + 1), []); + + return ( +
+
+ + +
+
+
+ +
{atomLabels[atomType]}
+
+
+
{atomCodeSnippets[atomType]}
+
+
+ ); +}; diff --git a/packages/react-components/react-motion-components-preview/stories/src/Atoms/ComposingAtomsDemo.styles.ts b/packages/react-components/react-motion-components-preview/stories/src/Atoms/ComposingAtomsDemo.styles.ts new file mode 100644 index 00000000000000..2ce48718464d39 --- /dev/null +++ b/packages/react-components/react-motion-components-preview/stories/src/Atoms/ComposingAtomsDemo.styles.ts @@ -0,0 +1,83 @@ +'use client'; + +import { makeStyles, tokens } from '@fluentui/react-components'; + +export const useClasses = makeStyles({ + wrapper: { + border: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke1}`, + borderRadius: tokens.borderRadiusMedium, + overflow: 'hidden', + marginTop: tokens.spacingVerticalXXL, + marginBottom: tokens.spacingVerticalXXL, + }, + header: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: `${tokens.spacingVerticalM} ${tokens.spacingHorizontalXL}`, + backgroundColor: tokens.colorNeutralBackground2, + borderBottom: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke1}`, + }, + title: { + margin: 0, + fontWeight: tokens.fontWeightSemibold, + fontSize: tokens.fontSizeBase300, + }, + body: { + display: 'flex', + '@media (max-width: 600px)': { + flexDirection: 'column', + }, + }, + demoArea: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flex: '1 1 auto', + minHeight: '120px', + padding: tokens.spacingVerticalXL, + backgroundColor: tokens.colorNeutralBackground1, + }, + demoPane: { + display: 'flex', + flexDirection: 'column', + flex: '0 0 220px', + }, + buttonRow: { + display: 'flex', + justifyContent: 'center', + padding: `${tokens.spacingVerticalM} ${tokens.spacingHorizontalXL} ${tokens.spacingVerticalL}`, + backgroundColor: tokens.colorNeutralBackground1, + }, + demoBox: { + padding: `${tokens.spacingVerticalL} ${tokens.spacingHorizontalXXL}`, + backgroundColor: tokens.colorNeutralBackground3, + border: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke1}`, + borderRadius: tokens.borderRadiusMedium, + color: tokens.colorNeutralForeground1, + fontSize: tokens.fontSizeBase300, + fontWeight: tokens.fontWeightSemibold, + textAlign: 'center', + }, + codeArea: { + flex: '1 1 auto', + minWidth: 0, + padding: `${tokens.spacingVerticalL} ${tokens.spacingHorizontalXL}`, + backgroundColor: tokens.colorNeutralBackground1, + borderLeft: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke1}`, + overflow: 'auto', + '@media (max-width: 600px)': { + borderLeft: 'none', + borderTop: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke1}`, + }, + }, + code: { + fontFamily: tokens.fontFamilyMonospace, + fontSize: tokens.fontSizeBase100, + lineHeight: tokens.lineHeightBase200, + color: tokens.colorNeutralForeground1, + whiteSpace: 'pre', + margin: 0, + display: 'block', + }, +}); diff --git a/packages/react-components/react-motion-components-preview/stories/src/Atoms/ComposingAtomsDemo.tsx b/packages/react-components/react-motion-components-preview/stories/src/Atoms/ComposingAtomsDemo.tsx new file mode 100644 index 00000000000000..aaa0c58b1d7808 --- /dev/null +++ b/packages/react-components/react-motion-components-preview/stories/src/Atoms/ComposingAtomsDemo.tsx @@ -0,0 +1,259 @@ +'use client'; + +import * as React from 'react'; +import { Button } from '@fluentui/react-components'; +import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts'; +import { OpenRegular } from '@fluentui/react-icons'; +import { createPresenceComponent, motionTokens } from '@fluentui/react-motion'; +import { rotateAtom, blurAtom, scaleAtom } from '@fluentui/react-motion-components-preview'; +import { useClasses } from './ComposingAtomsDemo.styles'; + +const duration = 1000; +const exitDuration = 1000; +const easing = motionTokens.curveDecelerateMid; + +// Custom "SpinBlur" β€” combines rotate + blur + scale +const SpinBlur = createPresenceComponent({ + enter: [ + rotateAtom({ direction: 'enter', duration, easing, axis: 'z', outAngle: -20, inAngle: 0 }), + blurAtom({ direction: 'enter', duration, easing, outRadius: '8px', inRadius: '0px' }), + scaleAtom({ direction: 'enter', duration, easing, outScale: 2 }), + ], + exit: [ + rotateAtom({ + direction: 'exit', + duration: exitDuration, + easing: motionTokens.curveAccelerateMid, + axis: 'z', + outAngle: 90, + inAngle: 0, + }), + blurAtom({ + direction: 'exit', + duration: exitDuration, + easing: motionTokens.curveLinear, + outRadius: '8px', + inRadius: '0px', + }), + scaleAtom({ direction: 'exit', duration: exitDuration, easing: motionTokens.curveLinear, outScale: 0 }), + ], +}); + +const codeSnippet = `const SpinBlur = createPresenceComponent({ + enter: [ + rotateAtom({ direction: 'enter', duration: 800, axis: 'z', outAngle: -20 }), + blurAtom({ direction: 'enter', duration: 800, outRadius: '8px' }), + scaleAtom({ direction: 'enter', duration: 800, outScale: 0.8 }), + ], + exit: [ + rotateAtom({ direction: 'exit', duration: 600, axis: 'z', outAngle: -20 }), + blurAtom({ direction: 'exit', duration: 600, outRadius: '8px' }), + scaleAtom({ direction: 'exit', duration: 600, outScale: 0.8 }), + ], +}); + +// Usage β€” just like any presence component + +
Your content
+
`; + +const stackblitzExampleCode = `import * as React from 'react'; +import { makeStyles, tokens, Button, FluentProvider, webLightTheme } from '@fluentui/react-components'; +import { createPresenceComponent } from '@fluentui/react-motion'; +import { rotateAtom, blurAtom, scaleAtom } from '@fluentui/react-motion-components-preview'; + +const useClasses = makeStyles({ + container: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: '16px', + padding: '40px', + }, + box: { + padding: '24px 32px', + backgroundColor: tokens.colorNeutralBackground3, + border: \`1px solid \${tokens.colorNeutralStroke1}\`, + borderRadius: '8px', + fontSize: '18px', + fontWeight: 600, + textAlign: 'center', + }, +}); + +const SpinBlur = createPresenceComponent({ + enter: [ + rotateAtom({ direction: 'enter', duration: 800, axis: 'z', outAngle: -20 }), + blurAtom({ direction: 'enter', duration: 800, outRadius: '8px' }), + scaleAtom({ direction: 'enter', duration: 800, outScale: 0.8 }), + ], + exit: [ + rotateAtom({ direction: 'exit', duration: 600, axis: 'z', outAngle: -20 }), + blurAtom({ direction: 'exit', duration: 600, outRadius: '8px' }), + scaleAtom({ direction: 'exit', duration: 600, outScale: 0.8 }), + ], +}); + +export default function Example() { + const classes = useClasses(); + const [visible, setVisible] = React.useState(true); + + return ( +
+ +
SpinBlur ✨
+
+ +
+ ); +}`; + +const stackblitzFiles: Record = { + '.stackblitzrc': JSON.stringify({}), + 'index.html': ` + + + + + SpinBlur β€” Composing Motion Atoms + + +
+ + +`, + 'src/index.tsx': `import * as React from 'react'; +import { createRoot } from 'react-dom/client'; +import { FluentProvider, webLightTheme } from '@fluentui/react-components'; +import Example from './example'; + +createRoot(document.getElementById('root')!).render( + + + + + , +);`, + 'src/example.tsx': stackblitzExampleCode, + 'tsconfig.json': JSON.stringify( + { + compilerOptions: { + target: 'ES2020', + useDefineForClassFields: true, + lib: ['ES2020', 'DOM', 'DOM.Iterable'], + module: 'ESNext', + skipLibCheck: true, + moduleResolution: 'node', + resolveJsonModule: true, + isolatedModules: true, + noEmit: true, + jsx: 'react-jsx', + strict: true, + }, + include: ['src'], + }, + null, + 2, + ), + 'vite.config.ts': `import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +export default defineConfig({ plugins: [react()] })`, + 'package.json': JSON.stringify( + { + name: 'fluent-motion-spinblur-example', + private: true, + version: '0.0.0', + type: 'module', + scripts: { dev: 'vite', build: 'tsc && vite build', preview: 'vite preview' }, + dependencies: { + react: '^18', + 'react-dom': '^18', + '@fluentui/react-components': '^9.0.0', + '@fluentui/react-motion': 'latest', + '@fluentui/react-motion-components-preview': 'latest', + }, + devDependencies: { + '@types/react': '^18', + '@types/react-dom': '^18', + '@vitejs/plugin-react': '^4.2.0', + typescript: '~5.0.0', + vite: '^5.0.0', + }, + }, + null, + 2, + ), +}; + +function openInStackBlitz(doc: Document) { + const form = doc.createElement('form'); + form.method = 'post'; + form.target = '_blank'; + form.action = `https://stackblitz.com/run?file=${encodeURIComponent('src/example.tsx')}`; + + const addField = (name: string, value: string) => { + const input = doc.createElement('input'); + input.type = 'hidden'; + input.name = name; + input.value = value; + form.appendChild(input); + }; + + addField('project[template]', 'node'); + addField('project[title]', 'SpinBlur β€” Composing Motion Atoms'); + addField('project[description]', '# Custom presence component composed from rotateAtom + blurAtom + scaleAtom'); + + Object.entries(stackblitzFiles).forEach(([path, content]) => { + addField(`project[files][${path}]`, content); + }); + + doc.body.appendChild(form); + form.submit(); + doc.body.removeChild(form); +} + +export const ComposingAtomsDemo: React.FC = () => { + const classes = useClasses(); + const { targetDocument } = useFluent(); + const [visible, setVisible] = React.useState(true); + + const handleToggle = React.useCallback(() => { + setVisible(v => !v); + }, []); + + const handleOpenStackBlitz = React.useCallback(() => { + if (targetDocument) { + openInStackBlitz(targetDocument); + } + }, [targetDocument]); + + return ( +
+
+

Custom "SpinBlur" β€” rotate + blur + scale

+ +
+
+
+
+ +
SpinBlur
+
+
+
+ +
+
+
+ {codeSnippet} +
+
+
+ ); +}; diff --git a/packages/react-components/react-motion-components-preview/stories/src/Atoms/index.mdx b/packages/react-components/react-motion-components-preview/stories/src/Atoms/index.mdx new file mode 100644 index 00000000000000..c5fab44755eb47 --- /dev/null +++ b/packages/react-components/react-motion-components-preview/stories/src/Atoms/index.mdx @@ -0,0 +1,183 @@ +import { Meta } from '@storybook/addon-docs/blocks'; +import { tokens } from '@fluentui/react-components'; +import { AtomsDemo } from './AtomsDemo'; +import { ComposingAtomsDemo } from './ComposingAtomsDemo'; + + + + + +
+

Motion Atoms

+
+ The smallest building blocks in the Fluent Motion System. Each + atom produces a single animation effect that can be composed into richer motion. +
+
+ +
+ ⚠️ Advanced API: Atoms are for developers creating custom motion components. For most use cases, use + the pre-built Motion Components like{' '} + Fade, Scale, etc. +
+ +## What are Motion Atoms? + +In the Fluent Motion System's atomic model, **atoms** are single one-way animations β€” keyframes changing one property over time. They're the foundation that Motion Components (molecules) are built from. + +Each atom is a factory function that returns an `AtomMotion` object β€” a plain description of keyframes, duration, and easing that the browser can execute directly. + +```ts +import { fadeAtom } from '@fluentui/react-motion-components-preview'; + +const fadeIn = fadeAtom({ + direction: 'enter', + duration: 500, + outOpacity: 0, + inOpacity: 1, +}); +// Returns: { keyframes: [{ opacity: 0 }, { opacity: 1 }], duration: 500, easing: '...', ... } +``` + +## Available Atoms + +| Atom | Effect | CSS Property | Key Parameters | +| ------------ | -------------------- | ------------------------ | ----------------------------- | +| `fadeAtom` | Opacity transition | `opacity` | `outOpacity`, `inOpacity` | +| `scaleAtom` | Size scaling | `transform: scale()` | `outScale`, `inScale` | +| `slideAtom` | Position translation | `transform: translate()` | `outX`, `outY`, `inX`, `inY` | +| `rotateAtom` | 3D rotation | `transform: rotate3d()` | `axis`, `outAngle`, `inAngle` | +| `blurAtom` | Blur filter | `filter: blur()` | `outRadius`, `inRadius` | + + + +## Atom API Reference + +### Common Parameters + +All atoms accept these base parameters: + +| Parameter | Type | Default | Description | +| ----------- | ------------------- | ------------- | --------------------------------------- | +| `direction` | `'enter' \| 'exit'` | β€” | **Required.** Determines keyframe order | +| `duration` | `number` | β€” | **Required.** Animation duration in ms | +| `easing` | `string` | `curveLinear` | CSS easing function | +| `delay` | `number` | `0` | Delay before animation starts | + +### Per-Atom Parameters + +In addition to the common parameters above, each atom has specific properties: + +```ts +import { fadeAtom, scaleAtom, slideAtom, rotateAtom, blurAtom } from '@fluentui/react-motion-components-preview'; + +// fadeAtom β€” opacity transition +fadeAtom({ direction: 'enter', duration: 500, outOpacity: 0, inOpacity: 1 }); + +// scaleAtom β€” size scaling +scaleAtom({ direction: 'enter', duration: 500, outScale: 0.9, inScale: 1 }); + +// slideAtom β€” position translation +slideAtom({ direction: 'enter', duration: 500, outX: '0px', outY: '20px', inX: '0px', inY: '0px' }); + +// rotateAtom β€” rotation (axis: 'x' | 'y' | 'z') +rotateAtom({ direction: 'enter', duration: 600, axis: 'z', outAngle: -90, inAngle: 0 }); + +// blurAtom β€” blur filter +blurAtom({ direction: 'enter', duration: 500, outRadius: '10px', inRadius: '0px' }); +``` + +## Creating Custom Presence Components + +The real power of atoms is **composing** them. Pass an array of atoms to `createPresenceComponent` and they run in parallel β€” creating richer effects from simple building blocks: + + + +Multiple atoms in an array animate simultaneously. Use the same duration for a cohesive feel, or intentionally vary them for more complex choreography. + +### Dynamic Parameters + +Use the function form to access runtime values like element dimensions: + +```tsx +const DynamicSlide = createPresenceComponent(({ element }) => { + const width = element.offsetWidth; + return { + enter: slideAtom({ direction: 'enter', duration: 500, outX: `-${width}px` }), + exit: slideAtom({ direction: 'exit', duration: 500, outX: `-${width}px` }), + }; +}); +``` + +## When to Use Atoms + +| Use Case | Recommendation | +| ---------------------------------------------- | --------------------------------------------------------------- | +| Standard fade, scale, slide | Use pre-built components (`Fade`, `Scale`, etc.) | +| Custom combination of effects | Compose atoms into a new presence component | +| Unique animation not covered by existing atoms | Create custom keyframes directly with `createPresenceComponent` | +| Dynamic animations based on element size | Use atoms with the function form | + +## See Also + +- [Motion Components](?path=/docs/motion-components-preview-introduction--docs) β€” Pre-built presence animations (molecules) +- [createPresenceComponent](?path=/docs/motion-apis-createpresencecomponent--docs) β€” Factory for presence components +- [createMotionComponent](?path=/docs/motion-apis-createmotioncomponent--docs) β€” Factory for one-way motion +- [Motion Tokens](?path=/docs/motion-tokens--docs) β€” Duration and easing values diff --git a/packages/react-components/react-motion-components-preview/stories/src/Introduction/ComponentsGrid.styles.ts b/packages/react-components/react-motion-components-preview/stories/src/Introduction/ComponentsGrid.styles.ts new file mode 100644 index 00000000000000..43d61eb7052c37 --- /dev/null +++ b/packages/react-components/react-motion-components-preview/stories/src/Introduction/ComponentsGrid.styles.ts @@ -0,0 +1,48 @@ +'use client'; + +import { makeStyles, tokens } from '@fluentui/react-components'; + +export const useClasses = makeStyles({ + grid: { + display: 'grid', + gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', + gap: tokens.spacingHorizontalL, + marginTop: tokens.spacingVerticalXXL, + marginBottom: tokens.spacingVerticalXXL, + }, + card: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: tokens.spacingVerticalM, + padding: tokens.spacingVerticalXL, + border: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke1}`, + borderRadius: tokens.borderRadiusMedium, + backgroundColor: tokens.colorNeutralBackground1, + }, + name: { + margin: 0, + fontWeight: tokens.fontWeightSemibold, + fontSize: tokens.fontSizeBase400, + }, + demoArea: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: '100%', + height: '60px', + }, + demoBox: { + width: '50px', + height: '50px', + backgroundColor: tokens.colorNeutralBackground3, + border: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke1}`, + borderRadius: tokens.borderRadiusSmall, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + color: tokens.colorNeutralForeground1, + fontSize: tokens.fontSizeBase200, + fontWeight: tokens.fontWeightSemibold, + }, +}); diff --git a/packages/react-components/react-motion-components-preview/stories/src/Introduction/ComponentsGrid.tsx b/packages/react-components/react-motion-components-preview/stories/src/Introduction/ComponentsGrid.tsx new file mode 100644 index 00000000000000..9d1a5093ff062d --- /dev/null +++ b/packages/react-components/react-motion-components-preview/stories/src/Introduction/ComponentsGrid.tsx @@ -0,0 +1,90 @@ +'use client'; + +import * as React from 'react'; +import { Button } from '@fluentui/react-components'; +import { + FadeRelaxed, + ScaleRelaxed, + CollapseRelaxed, + SlideRelaxed, + Blur, + Rotate, +} from '@fluentui/react-motion-components-preview'; +import { useClasses } from './ComponentsGrid.styles'; + +interface ComponentCardProps { + name: string; + children: (visible: boolean) => React.ReactNode; +} + +const ComponentCard: React.FC = ({ name, children }) => { + const classes = useClasses(); + const [visible, setVisible] = React.useState(true); + const toggleVisible = React.useCallback(() => setVisible(v => !v), []); + + return ( +
+

{name}

+
{children(visible)}
+ +
+ ); +}; + +export const ComponentsGrid: React.FC = () => { + const classes = useClasses(); + + return ( +
+ + {visible => ( + +
Fade
+
+ )} +
+ + + {visible => ( + +
Scale
+
+ )} +
+ + + {visible => ( + +
Collapse
+
+ )} +
+ + + {visible => ( + +
Slide
+
+ )} +
+ + + {visible => ( + +
Blur
+
+ )} +
+ + + {visible => ( + +
Rotate
+
+ )} +
+
+ ); +}; diff --git a/packages/react-components/react-motion-components-preview/stories/src/Introduction/InOutDemo.styles.ts b/packages/react-components/react-motion-components-preview/stories/src/Introduction/InOutDemo.styles.ts new file mode 100644 index 00000000000000..0ec1bcd096a770 --- /dev/null +++ b/packages/react-components/react-motion-components-preview/stories/src/Introduction/InOutDemo.styles.ts @@ -0,0 +1,69 @@ +'use client'; + +import { makeStyles, tokens } from '@fluentui/react-components'; + +export const useClasses = makeStyles({ + container: { + display: 'grid', + gridTemplateColumns: 'repeat(3, 1fr)', + gap: tokens.spacingHorizontalL, + marginTop: tokens.spacingVerticalXXL, + marginBottom: tokens.spacingVerticalXXL, + + '@media (max-width: 600px)': { + gridTemplateColumns: '1fr', + }, + }, + panel: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: tokens.spacingVerticalM, + padding: tokens.spacingVerticalXL, + border: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke1}`, + borderRadius: tokens.borderRadiusMedium, + backgroundColor: tokens.colorNeutralBackground1, + }, + title: { + margin: 0, + fontWeight: tokens.fontWeightSemibold, + fontSize: tokens.fontSizeBase300, + }, + subtitle: { + margin: 0, + fontSize: tokens.fontSizeBase200, + color: tokens.colorNeutralForeground3, + textAlign: 'center', + }, + code: { + fontFamily: tokens.fontFamilyMonospace, + fontSize: tokens.fontSizeBase200, + backgroundColor: tokens.colorNeutralBackground4, + padding: `${tokens.spacingVerticalXXS} ${tokens.spacingHorizontalSNudge}`, + borderRadius: tokens.borderRadiusSmall, + }, + demoArea: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: '100%', + height: '60px', + }, + demoBox: { + width: '80px', + height: '50px', + backgroundColor: tokens.colorBrandBackground, + borderRadius: tokens.borderRadiusSmall, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + color: tokens.colorNeutralForegroundOnBrand, + fontSize: tokens.fontSizeBase200, + fontWeight: tokens.fontWeightSemibold, + }, + button: { + minWidth: 'auto', + padding: `${tokens.spacingVerticalXS} ${tokens.spacingHorizontalM}`, + fontSize: tokens.fontSizeBase200, + }, +}); diff --git a/packages/react-components/react-motion-components-preview/stories/src/Introduction/InOutDemo.tsx b/packages/react-components/react-motion-components-preview/stories/src/Introduction/InOutDemo.tsx new file mode 100644 index 00000000000000..7862474e3ef93f --- /dev/null +++ b/packages/react-components/react-motion-components-preview/stories/src/Introduction/InOutDemo.tsx @@ -0,0 +1,63 @@ +'use client'; + +import * as React from 'react'; +import { Button } from '@fluentui/react-components'; +import { Fade } from '@fluentui/react-motion-components-preview'; +import { useClasses } from './InOutDemo.styles'; + +export const InOutDemo: React.FC = () => { + const classes = useClasses(); + const [visible, setVisible] = React.useState(true); + const [inKey, setInKey] = React.useState(0); + const [outKey, setOutKey] = React.useState(0); + + const replayIn = React.useCallback(() => setInKey(k => k + 1), []); + const toggleVisible = React.useCallback(() => setVisible(v => !v), []); + const replayOut = React.useCallback(() => setOutKey(k => k + 1), []); + + return ( +
+
+

Fade.In

+ <Fade.In> +

One-way enter

+
+ +
Enter
+
+
+ +
+ +
+

Fade

+ <Fade visible> +

Two-way presence

+
+ +
Toggle
+
+
+ +
+ +
+

Fade.Out

+ <Fade.Out> +

One-way exit

+
+ +
Exit
+
+
+ +
+
+ ); +}; diff --git a/packages/react-components/react-motion-components-preview/stories/src/Introduction/VariantsDemo.styles.ts b/packages/react-components/react-motion-components-preview/stories/src/Introduction/VariantsDemo.styles.ts new file mode 100644 index 00000000000000..da009657df0c39 --- /dev/null +++ b/packages/react-components/react-motion-components-preview/stories/src/Introduction/VariantsDemo.styles.ts @@ -0,0 +1,66 @@ +'use client'; + +import { makeStyles, tokens } from '@fluentui/react-components'; + +export const useClasses = makeStyles({ + toolbar: { + textAlign: 'center', + marginBottom: tokens.spacingVerticalL, + }, + container: { + display: 'grid', + gridTemplateColumns: 'repeat(3, 1fr)', + gap: tokens.spacingHorizontalL, + marginTop: tokens.spacingVerticalXXL, + marginBottom: tokens.spacingVerticalXXL, + + '@media (max-width: 600px)': { + gridTemplateColumns: '1fr', + }, + }, + panel: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: tokens.spacingVerticalM, + padding: tokens.spacingVerticalXL, + border: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke1}`, + borderRadius: tokens.borderRadiusMedium, + backgroundColor: tokens.colorNeutralBackground1, + }, + title: { + margin: 0, + fontWeight: tokens.fontWeightSemibold, + fontSize: tokens.fontSizeBase300, + }, + duration: { + margin: 0, + fontSize: tokens.fontSizeBase200, + color: tokens.colorNeutralForeground3, + fontFamily: tokens.fontFamilyMonospace, + }, + demoArea: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: '100%', + height: '60px', + }, + demoBox: { + width: '80px', + height: '50px', + backgroundColor: tokens.colorBrandBackground, + borderRadius: tokens.borderRadiusSmall, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + color: tokens.colorNeutralForegroundOnBrand, + fontSize: tokens.fontSizeBase200, + fontWeight: tokens.fontWeightSemibold, + }, + button: { + minWidth: 'auto', + padding: `${tokens.spacingVerticalXS} ${tokens.spacingHorizontalM}`, + fontSize: tokens.fontSizeBase200, + }, +}); diff --git a/packages/react-components/react-motion-components-preview/stories/src/Introduction/VariantsDemo.tsx b/packages/react-components/react-motion-components-preview/stories/src/Introduction/VariantsDemo.tsx new file mode 100644 index 00000000000000..bac88b78b2dcdc --- /dev/null +++ b/packages/react-components/react-motion-components-preview/stories/src/Introduction/VariantsDemo.tsx @@ -0,0 +1,70 @@ +'use client'; + +import * as React from 'react'; +import { Button } from '@fluentui/react-components'; +import { Fade, FadeSnappy, FadeRelaxed } from '@fluentui/react-motion-components-preview'; +import { useClasses } from './VariantsDemo.styles'; + +export const VariantsDemo: React.FC = () => { + const classes = useClasses(); + const [visibleSnappy, setVisibleSnappy] = React.useState(true); + const [visibleDefault, setVisibleDefault] = React.useState(true); + const [visibleRelaxed, setVisibleRelaxed] = React.useState(true); + + const toggleAll = React.useCallback(() => { + setVisibleSnappy(v => !v); + setVisibleDefault(v => !v); + setVisibleRelaxed(v => !v); + }, []); + const toggleSnappy = React.useCallback(() => setVisibleSnappy(v => !v), []); + const toggleDefault = React.useCallback(() => setVisibleDefault(v => !v), []); + const toggleRelaxed = React.useCallback(() => setVisibleRelaxed(v => !v), []); + + return ( + <> +
+ +
+
+
+

FadeSnappy

+

150ms

+
+ +
Snappy
+
+
+ +
+ +
+

Fade

+

200ms

+
+ +
Default
+
+
+ +
+ +
+

FadeRelaxed

+

250ms

+
+ +
Relaxed
+
+
+ +
+
+ + ); +}; diff --git a/packages/react-components/react-motion-components-preview/stories/src/Introduction/index.mdx b/packages/react-components/react-motion-components-preview/stories/src/Introduction/index.mdx new file mode 100644 index 00000000000000..f95c8ecddee4e6 --- /dev/null +++ b/packages/react-components/react-motion-components-preview/stories/src/Introduction/index.mdx @@ -0,0 +1,289 @@ +import { Meta } from '@storybook/addon-docs/blocks'; +import { tokens } from '@fluentui/react-components'; +import { ComponentsGrid } from './ComponentsGrid'; +import { InOutDemo } from './InOutDemo'; +import { VariantsDemo } from './VariantsDemo'; + + + + + +
+

Motion Components

+
+ Pre-built presence animations for common UI patterns β€” ready to use with intelligent defaults. Part of the{' '} + Fluent Motion System. +
+
+ +
+ ⚠️ Preview Package: These components are in beta and APIs may change before stable release. Import + from @fluentui/react-motion-components-preview +
+ +## Overview + +The Motion Components package provides six **presence components** that handle the most common UI animation needs. Each component manages visibility state and plays appropriate enter/exit animations. + + + +--- + +
+ +## For Motion Designers + +### Available Components + +| Component | Animation | Best For | +| ------------ | ---------------------- | ----------------------------------------- | +| **Fade** | Opacity transition | Tooltips, notifications, overlays, modals | +| **Scale** | Size grow/shrink | Popovers, menus, buttons, emphasis | +| **Collapse** | Height/width expansion | Accordions, expandable sections | +| **Slide** | Directional movement | Drawers, panels, carousels | +| **Blur** | Focus/defocus effect | Background dimming, depth | +| **Rotate** | 3D rotation | Card flips, reveals, playful interactions | + +### Timing Variants + +Each component (except Blur and Rotate) comes with pre-configured timing variants: + +| Variant | Duration | Feel | Use Case | +| ----------- | ------------------------ | ----------------- | ---------------------------------- | +| **Default** | 200ms (`durationNormal`) | Balanced | General purpose | +| **Snappy** | 150ms (`durationFast`) | Quick, responsive | Small elements, micro-interactions | +| **Relaxed** | 250ms (`durationGentle`) | Smooth, calm | Large elements, important content | + +**Collapse** also has a **Delayed** variant that adds a delay before the collapse begins, useful for accordions where you want the content to be readable briefly before collapsing. + +### Direction Support + +**Slide** can move content in any direction β€” left, right, up, or down. This makes it versatile for drawers that slide in from the edge of the screen, panels that reveal from a specific side, or page transitions that move content horizontally. + +### Choreography with Stagger + +**Stagger** coordinates multiple animations in sequence, creating a cascading ripple effect β€” like items in a list appearing one after another instead of all at once. It's ideal for lists, grids, card layouts, and any group of elements that should animate in sequence. + +
+ +--- + +
+ +## For Developers + +### Installation + +```bash +npm install @fluentui/react-motion-components-preview +# or +yarn add @fluentui/react-motion-components-preview +``` + +### Basic Usage + +```tsx +import { Fade, Scale, Slide, Collapse } from '@fluentui/react-motion-components-preview'; + +function MyComponent() { + const [showTooltip, setShowTooltip] = useState(false); + + return ( + +
Hello!
+
+ ); +} +``` + +### The `.In` and `.Out` Pattern + +Every presence component includes **one-way motion sub-components** that play just the enter or exit animation: + + + +```tsx +import { Fade, Scale, Slide } from '@fluentui/react-motion-components-preview'; + +// Full presence (two-way, controlled by visible prop) + + + + +// Just the enter animation (one-way, plays on mount) + + + + +// Just the exit animation (one-way, plays on mount) + + + +``` + +**Use cases for `.In` and `.Out`:** + +- Page load animations (use `.In` for entrance-only) +- Exit transitions before navigation (use `.Out`) +- Choreographing different enter/exit from different components +- Looping or staggered animations + +### Using Variants + +Variants are separate exports with pre-configured timing: + +```tsx +import { + Fade, FadeSnappy, FadeRelaxed, + Scale, ScaleSnappy, ScaleRelaxed, + Collapse, CollapseSnappy, CollapseRelaxed, CollapseDelayed, + Slide, SlideSnappy, SlideRelaxed +} from '@fluentui/react-motion-components-preview'; + +// Quick feedback for small UI + + + + +// Relaxed feel for important content + + + +``` + + + +### Customization with Parameters + +All components accept parameters to customize their behavior: + +```tsx +// Custom opacity range (e.g. fade to semi-transparent instead of invisible) + + + + +// Slide from a specific direction + + + + +// Horizontal collapse (instead of default vertical) + + + +``` + +See the Component Reference table below for all available parameters. + +### Choreography with Stagger + +Animate lists of items in sequence: + +```tsx +import { Stagger, Fade } from '@fluentui/react-motion-components-preview'; + +function List({ items, visible }) { + return ( + + {items.map(item => ( + + {item.name} + + ))} + + ); +} +``` + +### Component Reference + +| Component | Variants | Key Parameters | +| ---------- | ------------------------------------------------------ | --------------------------------------------------- | +| `Fade` | `FadeSnappy`, `FadeRelaxed` | `outOpacity`, `inOpacity`, `duration`, `easing` | +| `Scale` | `ScaleSnappy`, `ScaleRelaxed` | `outScale`, `inScale`, `duration`, `easing` | +| `Collapse` | `CollapseSnappy`, `CollapseRelaxed`, `CollapseDelayed` | `orientation`, `outSize`, `duration`, `easing` | +| `Slide` | `SlideSnappy`, `SlideRelaxed` | `outX`, `outY`, `inX`, `inY`, `duration`, `easing` | +| `Blur` | β€” | `outRadius`, `inRadius`, `duration`, `easing` | +| `Rotate` | β€” | `axis`, `outAngle`, `inAngle`, `duration`, `easing` | +| `Stagger` | β€” | `itemDelay`, `itemDuration`, `reversed`, `hideMode` | + +
+ +--- + +## Next Steps + +- **[Fade](?path=/docs/motion-components-preview-fade--docs)** β€” Simple opacity transitions +- **[Scale](?path=/docs/motion-components-preview-scale--docs)** β€” Grow and shrink animations +- **[Collapse](?path=/docs/motion-components-preview-collapse--docs)** β€” Expandable height/width +- **[Slide](?path=/docs/motion-components-preview-slide--docs)** β€” Directional slide animations +- **[Blur](?path=/docs/motion-components-preview-blur--docs)** β€” Focus/blur effects +- **[Rotate](?path=/docs/motion-components-preview-rotate--docs)** β€” 3D rotation effects +- **[Stagger](?path=/docs/motion-choreography-preview-stagger--docs)** β€” Sequential animation choreography +- **[Motion Atoms](?path=/docs/motion-components-preview-atoms--docs)** β€” Building blocks for custom components diff --git a/packages/react-components/react-motion/library/README.md b/packages/react-components/react-motion/library/README.md index 048d4cbb332b60..54c364e57abad4 100644 --- a/packages/react-components/react-motion/library/README.md +++ b/packages/react-components/react-motion/library/README.md @@ -1,5 +1,63 @@ # @fluentui/react-motion -**React Motions components for [Fluent UI React](https://react.fluentui.dev/)** +**React Motion components for [Fluent UI React](https://react.fluentui.dev/)** -These are not production-ready components and **should never be used in product**. This space is useful for testing new components whose APIs might change before final release. +A lightweight, performant animation library for React that brings Fluent UI experiences to life using the Web Animations API (WAAPI). + +## Features + +- ⚑ **Performance** β€” Animations run on the compositor thread for smooth 60fps motion +- πŸ“¦ **Lightweight** β€” ~3KB gzipped, leverages native browser capabilities +- 🎯 **Simple by default** β€” Common UI animations with minimal code +- πŸ”§ **Powerful on demand** β€” Full customization with keyframes, timing, and callbacks + +## Installation + +```bash +npm install @fluentui/react-motion +# or +yarn add @fluentui/react-motion +``` + +## Quick Start + +```tsx +import { createPresenceComponent, motionTokens } from '@fluentui/react-motion'; + +// Create a custom fade presence component +const Fade = createPresenceComponent({ + enter: { + keyframes: [{ opacity: 0 }, { opacity: 1 }], + duration: motionTokens.durationNormal, + }, + exit: { + keyframes: [{ opacity: 1 }, { opacity: 0 }], + duration: motionTokens.durationFast, + }, +}); + +// Use it in your app +function App() { + const [visible, setVisible] = useState(true); + + return ( + +
Animated content
+
+ ); +} +``` + +## Documentation + +πŸ“š **[Full documentation](https://react.fluentui.dev/?path=/docs/motion-introduction--docs)** + +- [Introduction](https://react.fluentui.dev/?path=/docs/motion-introduction--docs) β€” Overview and key concepts +- [createPresenceComponent](https://react.fluentui.dev/?path=/docs/motion-apis-createpresencecomponent--docs) β€” Two-way enter/exit animations +- [createMotionComponent](https://react.fluentui.dev/?path=/docs/motion-apis-createmotioncomponent--docs) β€” One-way animations +- [Motion Tokens](https://react.fluentui.dev/?path=/docs/motion-tokens--docs) β€” Duration and easing values +- [Migration Guide](https://react.fluentui.dev/?path=/docs/motion-migration--docs) β€” Coming from Framer Motion, GSAP, etc. + +## Pre-built Components + +For ready-to-use motion components (Fade, Scale, Slide, Collapse, etc.), see **[@fluentui/react-motion-components-preview](https://www.npmjs.com/package/@fluentui/react-motion-components-preview)**. diff --git a/packages/react-components/react-motion/stories/src/Introduction/MotionIntroDemo.styles.ts b/packages/react-components/react-motion/stories/src/Introduction/MotionIntroDemo.styles.ts new file mode 100644 index 00000000000000..ced72a3b26b20e --- /dev/null +++ b/packages/react-components/react-motion/stories/src/Introduction/MotionIntroDemo.styles.ts @@ -0,0 +1,19 @@ +'use client'; + +import { makeStyles, tokens } from '@fluentui/react-components'; + +export const useClasses = makeStyles({ + demo: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: tokens.spacingVerticalM, + padding: tokens.spacingVerticalXL, + }, + card: { + padding: `${tokens.spacingVerticalL} ${tokens.spacingHorizontalXXL}`, + fontWeight: tokens.fontWeightSemibold, + fontSize: tokens.fontSizeBase300, + textAlign: 'center', + }, +}); diff --git a/packages/react-components/react-motion/stories/src/Introduction/MotionIntroDemo.tsx b/packages/react-components/react-motion/stories/src/Introduction/MotionIntroDemo.tsx new file mode 100644 index 00000000000000..bbb779cd678788 --- /dev/null +++ b/packages/react-components/react-motion/stories/src/Introduction/MotionIntroDemo.tsx @@ -0,0 +1,28 @@ +'use client'; + +import * as React from 'react'; +import { Button, Card } from '@fluentui/react-components'; +import { Fade } from '@fluentui/react-motion-components-preview'; +import { useClasses } from './MotionIntroDemo.styles'; + +export const MotionIntroDemo: React.FC = () => { + const classes = useClasses(); + const [visible, setVisible] = React.useState(true); + + const handleToggle = React.useCallback(() => { + setVisible(v => !v); + }, []); + + return ( +
+ + + Hello, Motion! ✨ + + + +
+ ); +}; diff --git a/packages/react-components/react-motion/stories/src/Introduction/MotionVsPresenceDemo.styles.ts b/packages/react-components/react-motion/stories/src/Introduction/MotionVsPresenceDemo.styles.ts new file mode 100644 index 00000000000000..36c9c42bb3f127 --- /dev/null +++ b/packages/react-components/react-motion/stories/src/Introduction/MotionVsPresenceDemo.styles.ts @@ -0,0 +1,78 @@ +'use client'; + +import { makeStyles, tokens } from '@fluentui/react-components'; + +export const useClasses = makeStyles({ + container: { + display: 'grid', + gridTemplateColumns: '1fr 1fr', + gap: tokens.spacingHorizontalXXL, + marginTop: tokens.spacingVerticalXXL, + marginBottom: tokens.spacingVerticalXXL, + + '@media (max-width: 600px)': { + gridTemplateColumns: '1fr', + }, + }, + panel: { + display: 'flex', + flexDirection: 'column', + border: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke1}`, + borderRadius: tokens.borderRadiusMedium, + overflow: 'hidden', + }, + panelHeader: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: tokens.spacingVerticalXS, + padding: `${tokens.spacingVerticalL} ${tokens.spacingHorizontalXL} ${tokens.spacingVerticalM}`, + backgroundColor: tokens.colorNeutralBackground2, + }, + title: { + margin: 0, + fontSize: tokens.fontSizeBase400, + fontWeight: tokens.fontWeightSemibold, + }, + subtitle: { + margin: 0, + fontSize: tokens.fontSizeBase200, + color: tokens.colorNeutralForeground3, + textAlign: 'center', + }, + demoArea: { + display: 'grid', + alignItems: 'center', + justifyItems: 'center', + height: '80px', + padding: tokens.spacingVerticalL, + backgroundColor: tokens.colorNeutralBackground1, + }, + motionWrapper: { + width: 'fit-content', + }, + card: { + alignItems: 'center', + justifyContent: 'center', + padding: `${tokens.spacingVerticalM} ${tokens.spacingHorizontalXL}`, + fontWeight: tokens.fontWeightSemibold, + fontSize: tokens.fontSizeBase300, + }, + codeArea: { + margin: 0, + padding: `${tokens.spacingVerticalM} ${tokens.spacingHorizontalL}`, + backgroundColor: tokens.colorNeutralBackground3, + borderTop: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke1}`, + overflow: 'auto', + fontFamily: tokens.fontFamilyMonospace, + fontSize: tokens.fontSizeBase100, + lineHeight: tokens.lineHeightBase200, + color: tokens.colorNeutralForeground1, + }, + buttonRow: { + display: 'flex', + justifyContent: 'center', + padding: `${tokens.spacingVerticalM} ${tokens.spacingHorizontalXL} ${tokens.spacingVerticalL}`, + backgroundColor: tokens.colorNeutralBackground2, + }, +}); diff --git a/packages/react-components/react-motion/stories/src/Introduction/MotionVsPresenceDemo.tsx b/packages/react-components/react-motion/stories/src/Introduction/MotionVsPresenceDemo.tsx new file mode 100644 index 00000000000000..3fd05162ccdf53 --- /dev/null +++ b/packages/react-components/react-motion/stories/src/Introduction/MotionVsPresenceDemo.tsx @@ -0,0 +1,112 @@ +'use client'; + +import * as React from 'react'; +import { Button, Card } from '@fluentui/react-components'; +import { createMotionComponent, createPresenceComponent, motionTokens } from '@fluentui/react-motion'; +import { useClasses } from './MotionVsPresenceDemo.styles'; + +// One-way motion: plays on mount +const SlideIn = createMotionComponent({ + keyframes: [ + { transform: 'translateX(-50px)', opacity: 0 }, + { transform: 'translateX(0)', opacity: 1 }, + ], + duration: motionTokens.durationUltraSlow, + easing: motionTokens.curveDecelerateMin, +}); + +// Two-way presence: controlled by visible prop +const FadePresence = createPresenceComponent({ + enter: { + keyframes: [{ opacity: 0 }, { opacity: 1 }], + duration: motionTokens.durationUltraSlow, + easing: motionTokens.curveDecelerateMid, + }, + exit: { + keyframes: [{ opacity: 1 }, { opacity: 0 }], + duration: motionTokens.durationSlow, + easing: motionTokens.curveAccelerateMid, + }, +}); + +const motionCode = `const SlideIn = createMotionComponent({ + keyframes: [ + { transform: 'translateX(-50px)', opacity: 0 }, + { transform: 'translateX(0)', opacity: 1 }, + ], + duration: motionTokens.durationUltraSlow, + easing: motionTokens.curveDecelerateMin, +});`; + +const presenceCode = `const Fade = createPresenceComponent({ + enter: { + keyframes: [{ opacity: 0 }, { opacity: 1 }], + duration: 500, + }, + exit: { + keyframes: [{ opacity: 1 }, { opacity: 0 }], + duration: 300, + }, +});`; + +export const MotionVsPresenceDemo: React.FC = () => { + const classes = useClasses(); + const [motionKey, setMotionKey] = React.useState(0); + const [presenceVisible, setPresenceVisible] = React.useState(true); + + const handleReplay = React.useCallback(() => { + setMotionKey(k => k + 1); + }, []); + + const handleTogglePresence = React.useCallback(() => { + setPresenceVisible(v => !v); + }, []); + + return ( +
+
+
+

One-Way Motion

+

Plays once on mount

+
+
+
+ + + Slide In + + +
+
+
+ +
+
{motionCode}
+
+ +
+
+

Two-Way Presence

+

Toggles with enter/exit

+
+
+
+ + + Fade + + +
+
+
+ +
+
{presenceCode}
+
+
+ ); +}; diff --git a/packages/react-components/react-motion/stories/src/Introduction/index.mdx b/packages/react-components/react-motion/stories/src/Introduction/index.mdx new file mode 100644 index 00000000000000..6a6b614991ef98 --- /dev/null +++ b/packages/react-components/react-motion/stories/src/Introduction/index.mdx @@ -0,0 +1,238 @@ +import { Meta } from '@storybook/addon-docs/blocks'; +import { Card, CardHeader, tokens } from '@fluentui/react-components'; +import { MotionIntroDemo } from './MotionIntroDemo'; +import { MotionVsPresenceDemo } from './MotionVsPresenceDemo'; + + + + + +
+

Fluent Motion System

+
+ Add smooth, polished animations to your UI with just a few lines of code. Built on the browser's native animation + engine for great performance out of the box. +
+
+ +## What is Fluent Motion? + +Fluent Motion is a set of React components that make it easy to animate elements in your UI β€” showing, hiding, sliding, fading, and more. You describe _what_ you want to animate, and the system handles the _how_. + +Under the hood it uses the browser's [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API), so animations are fast and smooth without extra JavaScript overhead. + + + +## An Atomic Motion System + +Inspired by [atomic design](https://bradfrost.com/blog/post/atomic-web-design/), Fluent Motion is built in composable layers: + +| Layer | What it is | Example | +| ------------- | -------------------------------------------------------------------------------------------------- | ------------------------------ | +| **Atoms** | A single one-way animation β€” keyframes changing one property over time | `fadeAtom()`, `scaleAtom()` | +| **Molecules** | Combinations of atoms, e.g. pairing an enter atom with an exit atom to create a presence animation | ``, ``, `` | +| **Organisms** | Higher-order choreography that coordinates multiple molecules | ``, `` | + +**Factories** (`createMotionComponent`, `createPresenceComponent`) are the tools you use to assemble atoms into molecules β€” they turn raw keyframe definitions into React components. + +**Most users start with Molecules** β€” the pre-built components like `` and `` that work out of the box. When you need a custom effect, you can compose atoms together using the factories. See Motion Atoms for the full API. + +## Key Principles + +
+ + ⚑ Performance} /> +
+ Animations run on the browser's compositor thread where possible, ensuring smooth motion at 60fps, 120fps β€” + whatever your device can handle. +
+
+ + πŸ“¦ Lightweight} /> +
+ Small bundle size β€” the browser does the heavy lifting. No JavaScript animation engine is shipped to your users. +
+
+ + 🎯 Simple by Default} /> +
+ Common animations work with minimal code. Pre-built components like Fade, Scale, and{' '} + Slide work out of the box. +
+
+ + πŸ”§ Powerful on Demand} /> +
+ Full customization available when needed β€” custom keyframes, timing, easing curves, and lifecycle callbacks. +
+
+
+ +
+ +### β™Ώ Built-in Accessibility + +Fluent Motion automatically respects the user's **prefers-reduced-motion** operating system setting. +When a user has requested reduced motion, animations are scaled down or disabled β€” you don't need to write any extra code. This ensures your UI is inclusive by default. + +
+ + + +## For Motion Designers + +The Fluent Motion System uses **design tokens** β€” named values that keep animation timing consistent across all Microsoft products. Instead of picking arbitrary millisecond values, you choose a token that communicates the _intent_ of the motion. + +### Duration Tokens (examples) + +A few commonly used durations to give you a feel for the scale: + +| Token | Duration | When to Use | +| ------------------- | -------- | ------------------------------ | +| `durationFaster` | 100ms | Button presses, toggles | +| `durationNormal` | 200ms | Default for most UI animations | +| `durationSlow` | 300ms | Large elements, emphasis | +| `durationUltraSlow` | 500ms | Dramatic reveals, onboarding | + +The full set of duration and easing tokens is available on the Motion Tokens page. + +### Easing Curves (examples) + +Easing curves control how an animation accelerates and decelerates. Fluent defines three families: + +| Curve Family | Feel | When to Use | +| -------------- | --------------------------- | ------------------------------------- | +| **Decelerate** | Starts fast, settles gently | Elements **entering** the screen | +| **Accelerate** | Starts slow, speeds away | Elements **leaving** the screen | +| **Easy-ease** | Smooth start and end | Elements **moving** within the screen | + +Each family has variants (e.g. `curveDecelerateMid`, `curveDecelerateMax`) for different intensities. +See all curves with interactive previews on the Motion Tokens page. + +### Pre-built Components + +Six presence components β€” **Fade**, **Scale**, **Collapse**, **Slide**, **Blur**, and **Rotate** β€” handle the most common UI motion patterns. Each comes in **Snappy** and **Relaxed** timing variants. + +See all components with interactive demos on the Motion Components page. + + + + + +## For Developers + +### Two Types of Motion + +The Fluent Motion System distinguishes between two fundamental patterns: + + + +**One-Way Motion** β€” Created with `createMotionComponent()`, plays a single animation on mount. Good for page-load entrances or one-off effects. + +**Two-Way Presence** β€” Created with `createPresenceComponent()`, manages enter/exit animations controlled by a `visible` prop. This is what the pre-built components (`Fade`, `Scale`, etc.) use under the hood. + +For most use cases, you don't need the factories β€” use the pre-built Motion Components instead. + +### Two Packages to Know + +| Package | Status | What's Inside | +| ------------------------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------- | +| `@fluentui/react-motion` | Stable | Core factories (`createMotionComponent`, `createPresenceComponent`), `PresenceGroup`, `motionTokens`, `presenceMotionSlot` | +| `@fluentui/react-motion-components-preview` | Beta | Ready-to-use components: `Fade`, `Scale`, `Collapse`, `Slide`, `Blur`, `Rotate`, `Stagger`, plus Snappy/Relaxed variants | + + + +## Next Steps + +
+ + πŸ“š Learn the APIs} /> +
+ Dive into createPresenceComponent and{' '} + createMotionComponent for full API + documentation. +
+
+ + 🧩 Use Pre-built Components} /> +
+ Explore the Motion Components for + ready-to-use animations. +
+
+ + 🎨 Explore Tokens} /> +
+ See all available Motion Tokens for durations and easing curves. +
+
+ + πŸ”„ Migrate from Other Libraries} /> +
+ Coming from Framer Motion or GSAP? Check our Migration Guide. +
+
+ + βš›οΈ Motion Atoms} /> +
+ Build custom animations by composing{' '} + low-level atoms like fadeAtom and{' '} + scaleAtom. +
+
+
diff --git a/packages/react-components/react-motion/stories/src/Introduction/index.stories.tsx b/packages/react-components/react-motion/stories/src/Introduction/index.stories.tsx new file mode 100644 index 00000000000000..2864b60fb3b99d --- /dev/null +++ b/packages/react-components/react-motion/stories/src/Introduction/index.stories.tsx @@ -0,0 +1,9 @@ +import type { Meta } from '@storybook/react-webpack5'; +import { MotionIntroDemo } from './MotionIntroDemo'; + +const meta: Meta = { + title: 'Motion/Introduction', + component: MotionIntroDemo, +}; + +export default meta; diff --git a/packages/react-components/react-motion/stories/src/Migration/index.mdx b/packages/react-components/react-motion/stories/src/Migration/index.mdx new file mode 100644 index 00000000000000..6d8c2eb63df59a --- /dev/null +++ b/packages/react-components/react-motion/stories/src/Migration/index.mdx @@ -0,0 +1,643 @@ +import { Meta } from '@storybook/addon-docs/blocks'; +import { tokens } from '@fluentui/react-components'; + + + + + +
+

Migration & Comparison

+

+ Coming from another animation library? This guide helps you understand how Fluent Motion compares and how to + migrate. +

+
+ +## Why Fluent Motion? + +Fluent Motion is purpose-built for Microsoft's design system with specific goals: + +
+
+

βœ… Strengths

+
    +
  • Tiny bundle size β€” ~3KB gzipped
  • +
  • Native performance β€” Uses browser's WAAPI
  • +
  • Design token integration β€” Fluent durations & curves
  • +
  • Simple API β€” Declarative React components
  • +
  • Reduced motion support β€” Built-in accessibility
  • +
+
+
+

⚠️ Limitations

+
    +
  • No spring physics (use CSS/keyframes instead)
  • +
  • No scroll-driven animations (yet)
  • +
  • No gesture-based animations
  • +
  • No complex orchestration (use Stagger for basic needs)
  • +
  • No shared element transitions
  • +
+
+
+ +--- + +## Library Comparison + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FeatureFluent MotionFramer MotionReact SpringGSAP
+ Bundle Size + ~3KB~50KB~25KB~60KB
+ Animation Engine + WAAPI (native)JS runtimeJS runtime (springs)JS runtime
+ Spring Physics + ❌ Noβœ… Yesβœ… Yes (primary)βœ… Yes (plugin)
+ Keyframe Animations + βœ… Yes (primary)βœ… Yes⚠️ Limitedβœ… Yes
+ Enter/Exit (Presence) + βœ… Yesβœ… Yes (AnimatePresence)βœ… Yes (useTransition)⚠️ Manual
+ Layout Animations + ❌ Noβœ… Yes❌ No⚠️ Plugin
+ Gesture Support + ❌ Noβœ… Yes⚠️ Limited⚠️ Plugin
+ Scroll Animations + ❌ Noβœ… Yes❌ Noβœ… Yes
+ SSR Support + βœ… Yesβœ… Yesβœ… Yes⚠️ Requires setup
+ Design Tokens + βœ… Fluent integrated❌ Manual❌ Manual❌ Manual
+ +--- + +
+ +## Migrating from Framer Motion + +Framer Motion is a popular choice for React animations. Here's how common patterns translate to Fluent Motion. + +### Basic Animation + +
+
+

Framer Motion

+ +```tsx +import { motion } from 'framer-motion'; + + + Content +; +``` + +
+
+

Fluent Motion

+ +```tsx +import { Fade } from '@fluentui/react-motion-components-preview'; + + +
Content
+
; +``` + +
+
+ +### Enter/Exit Presence + +
+
+

Framer Motion

+ +```tsx +import { AnimatePresence, motion } from 'framer-motion'; + + + {isVisible && ( + + Content + + )} +; +``` + +
+
+

Fluent Motion

+ +```tsx +import { Fade } from '@fluentui/react-motion-components-preview'; + + +
Content
+
; +``` + +
+
+ +### Scale + Fade Combo + +
+
+

Framer Motion

+ +```tsx + + Content + +``` + +
+
+

Fluent Motion

+ +```tsx +import { createPresenceComponent, motionTokens } from '@fluentui/react-motion'; +import { fadeAtom, scaleAtom } from '@fluentui/react-motion-components-preview'; + +const FadeScale = createPresenceComponent({ + enter: [ + fadeAtom({ direction: 'enter', duration: motionTokens.durationNormal }), + scaleAtom({ + direction: 'enter', + duration: motionTokens.durationNormal, + outScale: 0.9, + }), + ], + exit: [ + fadeAtom({ direction: 'exit', duration: motionTokens.durationNormal }), + scaleAtom({ + direction: 'exit', + duration: motionTokens.durationNormal, + outScale: 0.9, + }), + ], +}); + + +
Content
+
; +``` + +
+
+ +### List Stagger + +
+
+

Framer Motion

+ +```tsx + + {items.map(item => ( + + {item.name} + + ))} + +``` + +
+
+

Fluent Motion

+ +```tsx +import { Stagger, Fade } from '@fluentui/react-motion-components-preview'; + + + {items.map(item => ( + +
  • {item.name}
  • +
    + ))} +
    ; +``` + +
    +
    + +### What Doesn't Migrate Directly + +| Framer Motion Feature | Fluent Motion Alternative | +| --------------------- | ---------------------------------------------------- | +| `spring` physics | Use CSS easing curves (`curveDecelerateMid`, etc.) | +| `layout` animations | Not supported; consider CSS transitions | +| `drag` gestures | Use native drag APIs or a gesture library | +| `useScroll` | Not supported; use Intersection Observer | +| `useMotionValue` | Not supported; use CSS custom properties | +| Variants system | Use component variants (`FadeSnappy`, `FadeRelaxed`) | + +
    + +--- + +
    + +## Migrating from React Spring + +React Spring focuses on spring physics. Fluent Motion uses keyframe-based animations instead. + +### Basic Spring β†’ Keyframe + +
    +
    +

    React Spring

    + +```tsx +import { useSpring, animated } from '@react-spring/web'; + +const props = useSpring({ + opacity: isVisible ? 1 : 0, + config: { tension: 300, friction: 20 }, +}); + +Content; +``` + +
    +
    +

    Fluent Motion

    + +```tsx +import { Fade } from '@fluentui/react-motion-components-preview'; + +// Note: Spring physics are replaced with easing curves + +
    Content
    +
    ; +``` + +
    +
    + +### useTransition β†’ Presence + +
    +
    +

    React Spring

    + +```tsx +import { useTransition, animated } from '@react-spring/web'; + +const transitions = useTransition(isVisible, { + from: { opacity: 0 }, + enter: { opacity: 1 }, + leave: { opacity: 0 }, +}); + +{ + transitions((style, item) => item && Content); +} +``` + +
    +
    +

    Fluent Motion

    + +```tsx +import { Fade } from '@fluentui/react-motion-components-preview'; + + +
    Content
    +
    ; +``` + +
    +
    + +### Key Differences + +- **No spring physics**: Fluent Motion uses CSS easing curves. For a spring-like feel, use `curveDecelerateMid` or `curveDecelerateMax`. +- **Simpler API**: No hooks required for basic animationsβ€”just components with props. +- **Predictable timing**: Keyframe animations have fixed durations, making them easier to coordinate. + +
    + +--- + +
    + +## Migrating from GSAP + +GSAP is a powerful timeline-based animation library. Fluent Motion is simpler but covers most UI animation needs. + +### Basic Tween β†’ Presence + +
    +
    +

    GSAP

    + +```tsx +import { gsap } from 'gsap'; +import { useRef, useEffect } from 'react'; + +const ref = useRef(); + +useEffect(() => { + if (isVisible) { + gsap.to(ref.current, { opacity: 1, duration: 0.2 }); + } else { + gsap.to(ref.current, { opacity: 0, duration: 0.2 }); + } +}, [isVisible]); + +
    + Content +
    ; +``` + +
    +
    +

    Fluent Motion

    + +```tsx +import { Fade } from '@fluentui/react-motion-components-preview'; + + +
    Content
    +
    ; +``` + +
    +
    + +### Timeline β†’ Stagger + +
    +
    +

    GSAP

    + +```tsx +const tl = gsap.timeline(); +tl.to('.item', { + opacity: 1, + stagger: 0.1, + duration: 0.3, +}); +``` + +
    +
    +

    Fluent Motion

    + +```tsx +import { Stagger, Fade } from '@fluentui/react-motion-components-preview'; + + + {items.map(item => ( + +
    {item}
    +
    + ))} +
    ; +``` + +
    +
    + +### What GSAP Does That Fluent Motion Doesn't + +- Complex timelines with labels and control +- ScrollTrigger for scroll-based animations +- MorphSVG for shape morphing +- Physics-based motion +- Draggable plugin + +For these advanced use cases, GSAP may still be the right choice. Fluent Motion is optimized for common UI transitions. + +
    + +--- + +## Concept Mapping + +| Concept | Framer Motion | React Spring | GSAP | Fluent Motion | +| ------------------ | --------------------------- | ----------------- | ---------------- | ------------------------------------------------ | +| One-shot animation | `motion.div` with `animate` | `useSpring` | `gsap.to()` | `createMotionComponent` or `.In`/`.Out` | +| Enter/exit | `AnimatePresence` | `useTransition` | Manual | `createPresenceComponent` or built-in components | +| Staggered list | `staggerChildren` variant | `useTrail` | `stagger` option | `` component | +| Duration | `transition.duration` | `config.duration` | `duration` | `duration` prop or tokens | +| Easing | `transition.ease` | `config.easing` | `ease` | `easing` prop or tokens | + +--- + +## When to Use What + +| Use Case | Recommendation | +| ------------------------------------- | ----------------- | +| Fluent UI app, simple transitions | **Fluent Motion** | +| Complex gesture interactions | Framer Motion | +| Spring physics feel | React Spring | +| Complex timelines, scroll-driven | GSAP | +| Performance-critical, minimal bundle | **Fluent Motion** | +| Design system consistency (Microsoft) | **Fluent Motion** | From 6411342ccfa1a14f414d13af9881c61788bafd7e Mon Sep 17 00:00:00 2001 From: Juraj Kapsiar Date: Wed, 6 May 2026 22:44:56 +0200 Subject: [PATCH 5/5] fix(Menu): Highlight expanded menuitem (#36098) Co-authored-by: Juraj Kapsiar --- ...-4b0904b6-f1ef-4b82-ace6-d2809363522d.json | 7 ++++++ .../react-menu/library/etc/react-menu.api.md | 4 +++- .../src/components/MenuItem/MenuItem.types.ts | 4 +++- .../src/components/MenuItem/useMenuItem.tsx | 2 ++ .../MenuItem/useMenuItemStyles.styles.ts | 24 +++++++++++++++++++ .../useMenuItemSwitchStyles.styles.ts | 1 + 6 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 change/@fluentui-react-menu-4b0904b6-f1ef-4b82-ace6-d2809363522d.json diff --git a/change/@fluentui-react-menu-4b0904b6-f1ef-4b82-ace6-d2809363522d.json b/change/@fluentui-react-menu-4b0904b6-f1ef-4b82-ace6-d2809363522d.json new file mode 100644 index 00000000000000..c7264f9077a065 --- /dev/null +++ b/change/@fluentui-react-menu-4b0904b6-f1ef-4b82-ace6-d2809363522d.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix(Menu): Menu item with expanded submenu should be highlighted", + "packageName": "@fluentui/react-menu", + "email": "jukapsia@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-menu/library/etc/react-menu.api.md b/packages/react-components/react-menu/library/etc/react-menu.api.md index 9814f2ee71551c..1b66b4025ded1d 100644 --- a/packages/react-components/react-menu/library/etc/react-menu.api.md +++ b/packages/react-components/react-menu/library/etc/react-menu.api.md @@ -199,7 +199,9 @@ export type MenuItemSlots = { }; // @public (undocumented) -export type MenuItemState = ComponentState & Required>; +export type MenuItemState = ComponentState & Required> & { + submenuOpen: boolean; +}; // @public (undocumented) export const MenuItemSwitch: ForwardRefComponent; diff --git a/packages/react-components/react-menu/library/src/components/MenuItem/MenuItem.types.ts b/packages/react-components/react-menu/library/src/components/MenuItem/MenuItem.types.ts index f8e0576690f440..0ed169826a7ea6 100644 --- a/packages/react-components/react-menu/library/src/components/MenuItem/MenuItem.types.ts +++ b/packages/react-components/react-menu/library/src/components/MenuItem/MenuItem.types.ts @@ -61,4 +61,6 @@ export type MenuItemProps = Omit>, 'conten }; export type MenuItemState = ComponentState & - Required>; + Required> & { + submenuOpen: boolean; + }; diff --git a/packages/react-components/react-menu/library/src/components/MenuItem/useMenuItem.tsx b/packages/react-components/react-menu/library/src/components/MenuItem/useMenuItem.tsx index 0b4a63b7180ec3..0b677c82a4f9c8 100644 --- a/packages/react-components/react-menu/library/src/components/MenuItem/useMenuItem.tsx +++ b/packages/react-components/react-menu/library/src/components/MenuItem/useMenuItem.tsx @@ -67,6 +67,7 @@ export const useMenuItemBase_unstable = ( } = props; const { hasIcons, hasCheckmarks } = useIconAndCheckmarkAlignment({ hasSubmenu }); const setOpen = useMenuContext_unstable(context => context.setOpen); + const open = useMenuContext_unstable(context => context.open); useNotifySplitItemMultiline({ multiline: !!props.subText, hasSubmenu }); const innerRef = React.useRef>(null); @@ -76,6 +77,7 @@ export const useMenuItemBase_unstable = ( const state: MenuItemState = { hasSubmenu, + submenuOpen: hasSubmenu && open, disabled, persistOnClick, components: { diff --git a/packages/react-components/react-menu/library/src/components/MenuItem/useMenuItemStyles.styles.ts b/packages/react-components/react-menu/library/src/components/MenuItem/useMenuItemStyles.styles.ts index 66c0f0bf8ed646..005b5762777f59 100644 --- a/packages/react-components/react-menu/library/src/components/MenuItem/useMenuItemStyles.styles.ts +++ b/packages/react-components/react-menu/library/src/components/MenuItem/useMenuItemStyles.styles.ts @@ -147,6 +147,29 @@ const useStyles = makeStyles({ backgroundColor: tokens.colorNeutralStroke1, }, }, + submenuOpen: { + backgroundColor: tokens.colorNeutralBackground1Hover, + color: tokens.colorNeutralForeground2Hover, + + [`& .${iconFilledClassName}`]: { + display: 'inline', + }, + [`& .${iconRegularClassName}`]: { + display: 'none', + }, + [`& .${menuItemClassNames.icon}`]: { + color: tokens.colorNeutralForeground2BrandSelected, + }, + + [`& .${menuItemClassNames.subText}`]: { + color: tokens.colorNeutralForeground3Hover, + }, + + '@media (forced-colors: active)': { + backgroundColor: 'Canvas', + color: 'Highlight', + }, + }, disabled: { color: tokens.colorNeutralForegroundDisabled, ':hover': { @@ -250,6 +273,7 @@ export const useMenuItemStyles_unstable = (state: MenuItemState): MenuItemState state.root.className = mergeClasses( menuItemClassNames.root, rootBaseStyles, + state.submenuOpen && styles.submenuOpen, state.disabled && styles.disabled, state.root.className, ); diff --git a/packages/react-components/react-menu/library/src/components/MenuItemSwitch/useMenuItemSwitchStyles.styles.ts b/packages/react-components/react-menu/library/src/components/MenuItemSwitch/useMenuItemSwitchStyles.styles.ts index 17b4430701d616..5d674ba776b821 100644 --- a/packages/react-components/react-menu/library/src/components/MenuItemSwitch/useMenuItemSwitchStyles.styles.ts +++ b/packages/react-components/react-menu/library/src/components/MenuItemSwitch/useMenuItemSwitchStyles.styles.ts @@ -145,6 +145,7 @@ export const useMenuItemSwitchStyles_unstable = (state: MenuItemSwitchState): Me checkmark: undefined, submenuIndicator: undefined, hasSubmenu: false, + submenuOpen: false, persistOnClick: true, });