Skip to content

Commit 9afbdec

Browse files
authored
refactor: move tabindex logic into JS class (#34741)
1 parent d59c32a commit 9afbdec

File tree

9 files changed

+40
-34
lines changed

9 files changed

+40
-34
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "refactor: move tabindex logic into JS class",
4+
"packageName": "@fluentui/web-components",
5+
"email": "machi@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}

packages/web-components/docs/web-components.api.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,9 @@ export class BaseButton extends FASTElement {
480480
// (undocumented)
481481
connectedCallback(): void;
482482
defaultSlottedContent: HTMLElement[];
483-
disabled?: boolean;
483+
disabled: boolean;
484+
// (undocumented)
485+
protected disabledChanged(): void;
484486
disabledFocusable: boolean;
485487
// @internal
486488
disabledFocusableChanged(previous: boolean, next: boolean): void;
@@ -501,7 +503,6 @@ export class BaseButton extends FASTElement {
501503
name?: string;
502504
protected press(): void;
503505
resetForm(): void;
504-
tabIndex: number;
505506
type: ButtonType;
506507
// @internal
507508
typeChanged(previous: ButtonType, next: ButtonType): void;
@@ -3284,8 +3285,6 @@ export type ProgressBarValidationState = ValuesOf<typeof ProgressBarValidationSt
32843285
// @public
32853286
export class Radio extends BaseCheckbox {
32863287
constructor();
3287-
// (undocumented)
3288-
connectedCallback(): void;
32893288
// @internal @override
32903289
protected disabledChanged(prev: boolean | undefined, next: boolean | undefined): void;
32913290
// @internal @override

packages/web-components/src/button/button.base.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { attr, FASTElement, nullableNumberConverter, observable } from '@microsoft/fast-element';
1+
import { attr, FASTElement, observable } from '@microsoft/fast-element';
22
import { keyEnter, keySpace } from '@microsoft/fast-web-utilities';
3-
import { ButtonFormTarget, ButtonType } from './button.options.js';
3+
import { type ButtonFormTarget, ButtonType } from './button.options.js';
44

55
/**
66
* A Button Custom HTML Element.
@@ -42,7 +42,19 @@ export class BaseButton extends FASTElement {
4242
* HTML Attribute: `disabled`
4343
*/
4444
@attr({ mode: 'boolean' })
45-
disabled?: boolean;
45+
disabled = false;
46+
47+
protected disabledChanged() {
48+
if (this.disabled) {
49+
this.removeAttribute('tabindex');
50+
} else {
51+
// If author sets tabindex to a non-positive value, the component should
52+
// respect it, otherwise set it to 0 to avoid the anti-pattern of setting
53+
// tabindex to a positive number. See details:
54+
// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/tabindex
55+
this.tabIndex = Number(this.getAttribute('tabindex') ?? 0) < 0 ? -1 : 0;
56+
}
57+
}
4658

4759
/**
4860
* Indicates that the button is focusable while disabled.
@@ -54,16 +66,6 @@ export class BaseButton extends FASTElement {
5466
@attr({ attribute: 'disabled-focusable', mode: 'boolean' })
5567
public disabledFocusable: boolean = false;
5668

57-
/**
58-
* Sets that the button tabindex attribute
59-
*
60-
* @public
61-
* @remarks
62-
* HTML Attribute: `tabindex`
63-
*/
64-
@attr({ attribute: 'tabindex', mode: 'fromView', converter: nullableNumberConverter })
65-
public override tabIndex: number = 0;
66-
6769
/**
6870
* Sets the element's internal disabled state when the element is focusable while disabled.
6971
*

packages/web-components/src/button/button.template.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import type { ButtonOptions } from './button.options.js';
1111
export function buttonTemplate<T extends Button>(options: ButtonOptions = {}): ElementViewTemplate<T> {
1212
return html<T>`
1313
<template
14-
tabindex="${x => (x.disabled ? null : x.tabIndex ?? 0)}"
1514
@click="${(x, c) => x.clickHandler(c.event as MouseEvent)}"
1615
@keypress="${(x, c) => x.keypressHandler(c.event as KeyboardEvent)}"
1716
>

packages/web-components/src/checkbox/checkbox.base.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@ export class BaseCheckbox extends FASTElement {
5151
* @internal
5252
*/
5353
protected disabledChanged(prev: boolean | undefined, next: boolean | undefined): void {
54+
if (this.disabled) {
55+
this.removeAttribute('tabindex');
56+
} else {
57+
// If author sets tabindex to a non-positive value, the component should
58+
// respect it, otherwise set it to 0 to avoid the anti-pattern of setting
59+
// tabindex to a positive number. See details:
60+
// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/tabindex
61+
this.tabIndex = Number(this.getAttribute('tabindex') ?? 0) < 0 ? -1 : 0;
62+
}
5463
this.elementInternals.ariaDisabled = this.disabled ? 'true' : 'false';
5564
toggleState(this.elementInternals, 'disabled', this.disabled);
5665
}
@@ -343,6 +352,7 @@ export class BaseCheckbox extends FASTElement {
343352
connectedCallback() {
344353
super.connectedCallback();
345354

355+
this.disabled = !!this.disabledAttribute;
346356
this.setAriaChecked();
347357
this.setValidity();
348358
}

packages/web-components/src/checkbox/checkbox.template.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ const indeterminateIndicator = html.partial(/* html */ `
2828
export function checkboxTemplate<T extends Checkbox>(options: CheckboxOptions = {}): ElementViewTemplate<T> {
2929
return html<T>`
3030
<template
31-
tabindex="${x => (!x.disabled ? 0 : void 0)}"
3231
@click="${(x, c) => x.clickHandler(c.event as MouseEvent)}"
3332
@input="${(x, c) => x.inputHandler(c.event as InputEvent)}"
3433
@keydown="${(x, c) => x.keydownHandler(c.event as KeyboardEvent)}"

packages/web-components/src/radio-group/radio-group.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export class RadioGroup extends FASTElement {
6565
if (this.$fastController.isConnected) {
6666
this.checkedIndex = -1;
6767
this.radios?.forEach(radio => {
68-
radio.disabled = radio.disabledAttribute || this.disabled;
68+
radio.disabled = !!radio.disabledAttribute || !!this.disabled;
6969
});
7070
this.restrictFocus();
7171
}
@@ -172,7 +172,7 @@ export class RadioGroup extends FASTElement {
172172
}
173173

174174
radio.name = this.name ?? radio.name;
175-
radio.disabled = this.disabled || radio.disabledAttribute;
175+
radio.disabled = !!this.disabled || !!radio.disabledAttribute;
176176
});
177177

178178
if (!this.dirtyState && this.initialValue) {

packages/web-components/src/radio/radio.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ test.describe('Radio', () => {
2626
});
2727

2828
await test.step('ariaDisabled', async () => {
29-
await expect(element).toHaveJSProperty('elementInternals.ariaDisabled', null);
29+
await expect(element).toHaveJSProperty('elementInternals.ariaDisabled', 'false');
3030

3131
await element.evaluate((node: Radio) => (node.disabled = true));
3232

@@ -44,12 +44,12 @@ test.describe('Radio', () => {
4444
await expect(element).toHaveAttribute('tabindex', '0');
4545
});
4646

47-
test('should set the tabindex to -1 when disabled is `true`', async ({ fastPage }) => {
47+
test('should not be focusable when disabled is `true`', async ({ fastPage }) => {
4848
const { element } = fastPage;
4949

5050
await fastPage.setTemplate({ attributes: { disabled: true } });
5151

52-
await expect(element).toHaveAttribute('tabindex', '-1');
52+
await expect(element).toHaveJSProperty('tabIndex', -1);
5353
});
5454

5555
test('should initialize to the initial value if no value property is set', async ({ fastPage }) => {

packages/web-components/src/radio/radio.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,6 @@ import { BaseCheckbox } from '../checkbox/checkbox.base.js';
1313
* @public
1414
*/
1515
export class Radio extends BaseCheckbox {
16-
connectedCallback() {
17-
super.connectedCallback();
18-
19-
this.tabIndex = this.disabled ? -1 : 0;
20-
}
21-
2216
constructor() {
2317
super();
2418
this.elementInternals.role = 'radio';
@@ -34,10 +28,6 @@ export class Radio extends BaseCheckbox {
3428
*/
3529
protected disabledChanged(prev: boolean | undefined, next: boolean | undefined): void {
3630
super.disabledChanged(prev, next);
37-
if (next) {
38-
this.tabIndex = -1;
39-
}
40-
4131
this.$emit('disabled', next, { bubbles: true });
4232
}
4333

0 commit comments

Comments
 (0)