diff --git a/change/@fluentui-web-components-b9ad2d8f-37a9-4a6d-b6fb-9d498fa34fb5.json b/change/@fluentui-web-components-b9ad2d8f-37a9-4a6d-b6fb-9d498fa34fb5.json new file mode 100644 index 0000000000000..e7ddd4bfa239e --- /dev/null +++ b/change/@fluentui-web-components-b9ad2d8f-37a9-4a6d-b6fb-9d498fa34fb5.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "adopt focusgroup and polyfill", + "packageName": "@fluentui/web-components", + "email": "machi@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-web-components-ce5d6bec-0682-4483-9813-9fd1c1275adb.json b/change/@fluentui-web-components-ce5d6bec-0682-4483-9813-9fd1c1275adb.json new file mode 100644 index 0000000000000..21cfab3010083 --- /dev/null +++ b/change/@fluentui-web-components-ce5d6bec-0682-4483-9813-9fd1c1275adb.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "fix components for native focusgroup implementation", + "packageName": "@fluentui/web-components", + "email": "machi@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/package.json b/package.json index 586e301ed9513..6da693a8bdd19 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.2.1", + "@microsoft/focusgroup-polyfill": "^1.3.0", "@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/docs/web-components.api.md b/packages/web-components/docs/web-components.api.md index 3d4298702c839..ee7caf3dacb89 100644 --- a/packages/web-components/docs/web-components.api.md +++ b/packages/web-components/docs/web-components.api.md @@ -739,11 +739,7 @@ export class BaseMenuList extends FASTElement { elementInternals: ElementInternals; focus(): void; handleChange(source: any, propertyName: string): void; - // @internal - handleFocusOut: (e: FocusEvent) => void; - // @internal (undocumented) - handleMenuKeyDown(e: KeyboardEvent): void | boolean; - protected isMenuItemElement: (el: Element) => el is HTMLElement; + protected isMenuItemElement(el: Element): el is MenuItem; // @internal (undocumented) readonly isNestedMenu: () => boolean; // @internal (undocumented) @@ -751,7 +747,9 @@ export class BaseMenuList extends FASTElement { // (undocumented) protected itemsChanged(oldValue: HTMLElement[], newValue: HTMLElement[]): void; // (undocumented) - protected menuItems: Element[] | undefined; + protected menuChildren: HTMLElement[] | undefined; + // (undocumented) + protected menuItems: MenuItem[] | undefined; // (undocumented) protected setItems(): void; } @@ -808,8 +806,6 @@ export class BaseRadioGroup extends FASTElement { focus(): void; // @internal focusinHandler(e: FocusEvent): boolean | void; - // @internal - focusoutHandler(e: FocusEvent): boolean | void; static formAssociated: boolean; // (undocumented) formResetCallback(): void; @@ -893,22 +889,26 @@ export class BaseTablist extends FASTElement { // @internal (undocumented) protected activeidChanged(oldValue: string, newValue: string): void; activetab: Tab; - adjust(adjustment: number): void; // @internal (undocumented) connectedCallback(): void; disabled: boolean; - // @internal + // @internal (undocumented) protected disabledChanged(prev: boolean, next: boolean): void; // @internal elementInternals: ElementInternals; - orientation: TablistOrientation; // @internal (undocumented) + handleFocusIn(event: FocusEvent): void; + orientation: TablistOrientation; + // (undocumented) protected orientationChanged(prev: TablistOrientation, next: TablistOrientation): void; - protected setTabs(): void; + protected setTabs({ connectToPanel, forceDisabled }?: { + connectToPanel?: boolean | undefined; + forceDisabled?: boolean | undefined; + }): void; // @internal slottedTabs: Node[]; - // @internal - slottedTabsChanged(prev: Node[] | undefined, next: Node[] | undefined): void; + // @internal (undocumented) + protected slottedTabsChanged(prev: Node[] | undefined, next: Node[] | undefined): void; // @internal (undocumented) tabs: Tab[]; // @internal (undocumented) @@ -1076,8 +1076,6 @@ export class BaseTextInput extends FASTElement { export class BaseTree extends FASTElement { constructor(); // @internal - blurHandler(e: FocusEvent): void; - // @internal changeHandler(e: Event): boolean | void; // Warning: (ae-forgotten-export) The symbol "BaseTreeItem" needs to be exported by the entry point index.d.ts // @@ -1087,17 +1085,14 @@ export class BaseTree extends FASTElement { childTreeItemsChanged(): void; // @internal clickHandler(e: Event): boolean | void; - // (undocumented) - connectedCallback(): void; currentSelected: HTMLElement | null; // @internal (undocumented) defaultSlot: HTMLSlotElement; // @internal defaultSlotChanged(): void; + protected get descendantTreeItems(): BaseTreeItem[]; // @internal elementInternals: ElementInternals; - // @internal - focusHandler(e: FocusEvent): void; // @internal (undocumented) handleDefaultSlotChange(): void; // @internal @@ -3287,6 +3282,10 @@ export class MenuItem extends FASTElement { handleMouseOut: (e: MouseEvent) => boolean; // @internal (undocumented) handleMouseOver: (e: MouseEvent) => boolean; + // @internal (undocumented) + handleSubmenuFocusOut: (e: FocusEvent) => void; + // @internal + handleToggle: (e: Event) => void; hidden: boolean; role: MenuItemRole; roleChanged(prev: MenuItemRole | undefined, next: MenuItemRole | undefined): void; @@ -3298,8 +3297,6 @@ export class MenuItem extends FASTElement { protected slottedSubmenuChanged(prev: HTMLElement[] | undefined, next: HTMLElement[]): void; // @internal (undocumented) submenu: HTMLElement | undefined; - // @internal - toggleHandler: (e: Event) => void; } // @internal @@ -3340,6 +3337,10 @@ export const MenuItemTemplate: ElementViewTemplate; // @public export class MenuList extends BaseMenuList { + // (undocumented) + disconnectedCallback(): void; + // (undocumented) + setItems(): void; } // @public (undocumented) @@ -3486,6 +3487,10 @@ export const RadioDefinition: FASTElementDefinition; // @public export class RadioGroup extends BaseRadioGroup { + // (undocumented) + disconnectedCallback(): void; + // (undocumented) + radiosChanged(prev: Radio[] | undefined, next: Radio[] | undefined): void; } // @public @@ -3955,6 +3960,8 @@ export class Tab extends FASTElement { // (undocumented) connectedCallback(): void; disabled: boolean; + // (undocumented) + protected disabledChanged(prev: boolean, next: boolean): void; // @internal elementInternals: ElementInternals; } @@ -3970,9 +3977,11 @@ export const TabDefinition: FASTElementDefinition; // @public export class Tablist extends BaseTablist { - activeidChanged(oldValue: string, newValue: string): void; appearance?: TablistAppearance; + // (undocumented) + disconnectedCallback(): void; size?: TablistSize; + // (undocumented) tabsChanged(prev: Tab[] | undefined, next: Tab[] | undefined): void; } @@ -4369,6 +4378,10 @@ export class Tree extends BaseTree { protected appearanceChanged(): void; // @internal childTreeItemsChanged(): void; + // (undocumented) + disconnectedCallback(): void; + // @internal (undocumented) + itemToggleHandler(): void; size: TreeItemSize; // (undocumented) protected sizeChanged(): void; diff --git a/packages/web-components/package.json b/packages/web-components/package.json index d6c3726f90a2f..cc1b9b7c7df83 100644 --- a/packages/web-components/package.json +++ b/packages/web-components/package.json @@ -77,6 +77,7 @@ "devDependencies": { "@custom-elements-manifest/analyzer": "0.10.10", "@microsoft/fast-element": "2.0.0", + "@microsoft/focusgroup-polyfill": "^1.3.0", "@tensile-perf/web-components": "~0.2.2", "@storybook/html": "9.1.17", "@storybook/html-vite": "9.1.17", @@ -93,7 +94,8 @@ "tslib": "^2.1.0" }, "peerDependencies": { - "@microsoft/fast-element": "^2.0.0" + "@microsoft/fast-element": "^2.0.0", + "@microsoft/focusgroup-polyfill": "^1.3.0" }, "beachball": { "disallowedChangeTypes": [ diff --git a/packages/web-components/src/_docs/developer/polyfilling.mdx b/packages/web-components/src/_docs/developer/polyfilling.mdx index 70e19d36cad9b..2e24491fcb8d5 100644 --- a/packages/web-components/src/_docs/developer/polyfilling.mdx +++ b/packages/web-components/src/_docs/developer/polyfilling.mdx @@ -94,3 +94,17 @@ if (!CSS.supports('anchor-name: --foo')) { import { default as applyPolyfill } from '@oddbird/css-anchor-positioning/fn'; window.CSS_ANCHOR_POLYFILL = applyPolyfill; ``` + +## HTML Focusgroup + +For components that require directional navigations (moving focus between focusable elements within a component with arrow keys instead of tab), Fluent Web Components have adopted [HTML Focusgroup](https://open-ui.org/components/scoped-focusgroup.explainer/). The components that currently use focusgroup include MenuList/MenuItem, RadioGroup/Radio, Tablist/Tab, and Tree/TreeItem. + +For browsers that don’t yet support focusgroup, we use [`@microsoft/focusgroup-polyfill`](https://github.com/microsoft/polyfills/tree/main/packages/focusgroup) and automatically polyfill the components when they are connected to the DOM. + +If you want to opt out of the polyfill, you can use the base classes of these components, e.g. + +```js +import { BaseTablist } from '@fluentui/web-components/tablist/base.js'; + +export class MyTablist extends BaseTablist {} +``` diff --git a/packages/web-components/src/menu-item/menu-item.template.ts b/packages/web-components/src/menu-item/menu-item.template.ts index 9fc26092f8bb3..71f7537acab80 100644 --- a/packages/web-components/src/menu-item/menu-item.template.ts +++ b/packages/web-components/src/menu-item/menu-item.template.ts @@ -14,11 +14,12 @@ const chevronRight16Filled = html.partial( export function menuItemTemplate(options: MenuItemOptions = {}): ElementViewTemplate { return html`