diff --git a/change/@fluentui-web-components-8407ff54-9881-408f-86cd-9bf5bb6f5e83.json b/change/@fluentui-web-components-8407ff54-9881-408f-86cd-9bf5bb6f5e83.json new file mode 100644 index 0000000000000..dd3587187eea3 --- /dev/null +++ b/change/@fluentui-web-components-8407ff54-9881-408f-86cd-9bf5bb6f5e83.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "fix: make sure autofocus works on all focusable custom elements during initial page load", + "packageName": "@fluentui/web-components", + "email": "machi@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/web-components/docs/web-components.api.md b/packages/web-components/docs/web-components.api.md index bc2e50a46bb25..fef7fb412a672 100644 --- a/packages/web-components/docs/web-components.api.md +++ b/packages/web-components/docs/web-components.api.md @@ -1029,7 +1029,6 @@ export class BaseTextInput extends FASTElement { disabled?: boolean; // @internal elementInternals: ElementInternals; - focusinHandler(e: FocusEvent): boolean | void; get form(): HTMLFormElement | null; static readonly formAssociated = true; formAttribute?: string; diff --git a/packages/web-components/src/button/button.base.ts b/packages/web-components/src/button/button.base.ts index e42c2a858fedc..891e46f5b59c3 100644 --- a/packages/web-components/src/button/button.base.ts +++ b/packages/web-components/src/button/button.base.ts @@ -1,4 +1,5 @@ import { attr, FASTElement, observable } from '@microsoft/fast-element'; +import { maybeSetAutoFocus } from '../utils/autofocus.js'; import { type ButtonFormTarget, ButtonType } from './button.options.js'; /** @@ -248,6 +249,7 @@ export class BaseButton extends FASTElement { super.connectedCallback(); this.elementInternals.ariaDisabled = `${!!this.disabledFocusable}`; this.setTabIndex(); + maybeSetAutoFocus(this); } constructor() { diff --git a/packages/web-components/src/checkbox/checkbox.base.ts b/packages/web-components/src/checkbox/checkbox.base.ts index 77cbb38abef38..b9660fe1911cd 100644 --- a/packages/web-components/src/checkbox/checkbox.base.ts +++ b/packages/web-components/src/checkbox/checkbox.base.ts @@ -1,5 +1,6 @@ import { attr, FASTElement, Observable, observable } from '@microsoft/fast-element'; import { toggleState } from '../utils/element-internals.js'; +import { maybeSetAutoFocus } from '../utils/autofocus.js'; /** * The base class for a component with a toggleable checked state. @@ -348,6 +349,7 @@ export class BaseCheckbox extends FASTElement { this.disabled = !!this.disabledAttribute; this.setAriaChecked(); this.setValidity(); + maybeSetAutoFocus(this); } /** diff --git a/packages/web-components/src/combobox/combobox.stories.ts b/packages/web-components/src/combobox/combobox.stories.ts index e79f9eb5f6d55..a0fba980aa835 100644 --- a/packages/web-components/src/combobox/combobox.stories.ts +++ b/packages/web-components/src/combobox/combobox.stories.ts @@ -49,8 +49,10 @@ export default { parameters: { docs: { description: { - component: dedent`hello - world + component: dedent` + The Combobox component is a variant of the Dropdown component. + + To use a combobox, use <fluent-dropdown type="combobox">. `, }, }, diff --git a/packages/web-components/src/dropdown/dropdown.base.ts b/packages/web-components/src/dropdown/dropdown.base.ts index 82f683b366746..a28225207c723 100644 --- a/packages/web-components/src/dropdown/dropdown.base.ts +++ b/packages/web-components/src/dropdown/dropdown.base.ts @@ -9,6 +9,7 @@ import { getLanguage } from '../utils/language.js'; import { waitForConnectedDescendants } from '../utils/request-idle-callback.js'; import { AnchorPositioningCSSSupported } from '../utils/support.js'; import { uniqueId } from '../utils/unique-id.js'; +import { maybeSetAutoFocus } from '../utils/autofocus.js'; import { DropdownType } from './dropdown.options.js'; import { dropdownButtonTemplate, dropdownInputTemplate } from './dropdown.template.js'; @@ -858,7 +859,7 @@ export class BaseDropdown extends FASTElement { private searchTimeout?: ReturnType; /** - * Handles printable character input by moving {@link activeIndex} to the next option whose label matches the + * Handles printable character input by moving {@link Dropdown#activeIndex} to the next option whose label matches the * accumulated search string. When the string is a single character (or the same character repeated), matching * options are cycled through; otherwise the string is treated as a prefix match. * @@ -1107,6 +1108,8 @@ export class BaseDropdown extends FASTElement { Updates.enqueue(() => { this.insertControl(); }); + + maybeSetAutoFocus(this); } disconnectedCallback(): void { diff --git a/packages/web-components/src/menu-item/menu-item.ts b/packages/web-components/src/menu-item/menu-item.ts index 7bf1b5be923a3..4d10eda2c9ad2 100644 --- a/packages/web-components/src/menu-item/menu-item.ts +++ b/packages/web-components/src/menu-item/menu-item.ts @@ -4,6 +4,7 @@ import { StartEnd } from '../patterns/start-end.js'; import { applyMixins } from '../utils/apply-mixins.js'; import { toggleState } from '../utils/element-internals.js'; import type { StaticallyComposableHTML } from '../utils/template-helpers.js'; +import { maybeSetAutoFocus } from '../utils/autofocus.js'; import { MenuItemRole, roleForMenuItem } from './menu-item.options.js'; export type MenuItemColumnCount = 0 | 1 | 2; @@ -160,6 +161,8 @@ export class MenuItem extends FASTElement { this.elementInternals.role = this.role ?? MenuItemRole.menuitem; this.elementInternals.ariaChecked = this.role !== MenuItemRole.menuitem ? `${!!this.checked}` : null; + + maybeSetAutoFocus(this); } /** diff --git a/packages/web-components/src/slider/slider.ts b/packages/web-components/src/slider/slider.ts index 469d4420eb9b8..40f2f212d7f69 100644 --- a/packages/web-components/src/slider/slider.ts +++ b/packages/web-components/src/slider/slider.ts @@ -5,6 +5,7 @@ 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 { maybeSetAutoFocus } from '../utils/autofocus.js'; import { convertPixelToPercent } from './slider-utilities.js'; import { type SliderConfiguration, SliderMode, SliderOrientation, SliderSize } from './slider.options.js'; @@ -552,6 +553,8 @@ export class Slider extends FASTElement implements SliderConfiguration { notifier.subscribe(this, 'min'); notifier.subscribe(this, 'step'); }); + + maybeSetAutoFocus(this); } /** diff --git a/packages/web-components/src/tab/tab.ts b/packages/web-components/src/tab/tab.ts index 42929cc963bae..25cf4683b7f90 100644 --- a/packages/web-components/src/tab/tab.ts +++ b/packages/web-components/src/tab/tab.ts @@ -2,6 +2,7 @@ import { attr, css, type ElementStyles, FASTElement } from '@microsoft/fast-elem import type { StartEndOptions } from '../patterns/start-end.js'; import { StartEnd } from '../patterns/start-end.js'; import { applyMixins } from '../utils/apply-mixins.js'; +import { maybeSetAutoFocus } from '../utils/autofocus.js'; /** * Tab configuration options @@ -65,6 +66,8 @@ export class Tab extends FASTElement { `; this.$fastController.addStyles(this.styles); + + maybeSetAutoFocus(this); } private setDisabledSideEffect(disabled: boolean) { diff --git a/packages/web-components/src/text-input/text-input.base.ts b/packages/web-components/src/text-input/text-input.base.ts index fa1e96095c795..4a2f08be10eb9 100644 --- a/packages/web-components/src/text-input/text-input.base.ts +++ b/packages/web-components/src/text-input/text-input.base.ts @@ -1,12 +1,5 @@ -import { - attr, - FASTElement, - nullableNumberConverter, - Observable, - observable, - type Subscriber, - Updates, -} from '@microsoft/fast-element'; +import { attr, FASTElement, nullableNumberConverter, Observable, observable, Updates } from '@microsoft/fast-element'; +import { maybeSetAutoFocus } from '../utils/autofocus.js'; import { ImplicitSubmissionBlockingTypes, TextInputType } from './text-input.options.js'; /** @@ -456,24 +449,10 @@ export class BaseTextInput extends FASTElement { public connectedCallback(): void { super.connectedCallback(); - this.tabIndex = Number(this.getAttribute('tabindex') ?? 0) < 0 ? -1 : 0; - this.setFormValue(this.value); this.setValidity(); - } - /** - * Focuses the inner control when the component is focused. - * - * @param e - the event object - * @public - */ - public focusinHandler(e: FocusEvent): boolean | void { - if (e.target === this) { - this.control?.focus(); - } - - return true; + maybeSetAutoFocus(this); } /** diff --git a/packages/web-components/src/text-input/text-input.spec.ts b/packages/web-components/src/text-input/text-input.spec.ts index 43c131a9e03ff..190f4966a9465 100644 --- a/packages/web-components/src/text-input/text-input.spec.ts +++ b/packages/web-components/src/text-input/text-input.spec.ts @@ -29,12 +29,6 @@ test.describe('TextInput', () => { const attributes: InitialTemplateAttributes = { autofocus: true }; - if (ssr) { - // the host element needs to be focusable for autofocus to work on the server, - // so we need to set tabindex="0" - attributes.tabindex = '0'; - } - await fastPage.setTemplate({ attributes }); await expect(element).toBeFocused(); diff --git a/packages/web-components/src/text-input/text-input.template.ts b/packages/web-components/src/text-input/text-input.template.ts index f19fe9cfe2def..be19023a99857 100644 --- a/packages/web-components/src/text-input/text-input.template.ts +++ b/packages/web-components/src/text-input/text-input.template.ts @@ -10,10 +10,7 @@ import type { TextInputOptions } from './text-input.options.js'; */ export function textInputTemplate(options: TextInputOptions = {}): ElementViewTemplate { return html` -