mirror of
https://github.com/material-components/material-web.git
synced 2024-09-11 13:46:10 +03:00
chore: add polyfill for Firefox ElementInternals aria
PiperOrigin-RevId: 563194223
This commit is contained in:
parent
01a99a5cc3
commit
08acc413f6
@ -4,6 +4,8 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {isServer, ReactiveElement} from 'lit';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accessibility Object Model reflective aria property name types.
|
* Accessibility Object Model reflective aria property name types.
|
||||||
*/
|
*/
|
||||||
@ -178,3 +180,185 @@ export type ARIARole =
|
|||||||
'doc-glossary'|'doc-glossref'|'doc-index'|'doc-introduction'|'doc-noteref'|
|
'doc-glossary'|'doc-glossref'|'doc-index'|'doc-introduction'|'doc-noteref'|
|
||||||
'doc-notice'|'doc-pagebreak'|'doc-pagelist'|'doc-part'|'doc-preface'|
|
'doc-notice'|'doc-pagebreak'|'doc-pagelist'|'doc-part'|'doc-preface'|
|
||||||
'doc-prologue'|'doc-pullquote'|'doc-qna'|'doc-subtitle'|'doc-tip'|'doc-toc';
|
'doc-prologue'|'doc-pullquote'|'doc-qna'|'doc-subtitle'|'doc-tip'|'doc-toc';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables a host custom element to be the target for aria roles and attributes.
|
||||||
|
* Components should set the `elementInternals.role` property.
|
||||||
|
*
|
||||||
|
* By default, aria components are tab focusable. Provide a `focusable: false`
|
||||||
|
* option for components that should not be tab focusable, such as
|
||||||
|
* `role="listbox"`.
|
||||||
|
*
|
||||||
|
* This function will also polyfill aria `ElementInternals` properties for
|
||||||
|
* Firefox.
|
||||||
|
*
|
||||||
|
* @param ctor The `ReactiveElement` constructor to set up.
|
||||||
|
* @param options Options to configure the element's host aria.
|
||||||
|
*/
|
||||||
|
export function setupHostAria(
|
||||||
|
ctor: typeof ReactiveElement, {focusable}: SetupHostAriaOptions = {}) {
|
||||||
|
if (focusable !== false) {
|
||||||
|
ctor.addInitializer(host => {
|
||||||
|
host.addController({
|
||||||
|
hostConnected() {
|
||||||
|
if (host.hasAttribute('tabindex')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
host.tabIndex = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isServer || 'role' in Element.prototype) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Polyfill reflective aria properties for Firefox
|
||||||
|
for (const ariaProperty of ARIA_PROPERTIES) {
|
||||||
|
ctor.createProperty(ariaProperty, {
|
||||||
|
attribute: ariaPropertyToAttribute(ariaProperty),
|
||||||
|
reflect: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ctor.createProperty('role', {reflect: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for setting up a host element as an aria target.
|
||||||
|
*/
|
||||||
|
export interface SetupHostAriaOptions {
|
||||||
|
/**
|
||||||
|
* Whether or not the element can be focused with the tab key. Defaults to
|
||||||
|
* true.
|
||||||
|
*
|
||||||
|
* Set this to false for aria roles that should not be tab focusable, such as
|
||||||
|
* `role="listbox"`.
|
||||||
|
*/
|
||||||
|
focusable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Polyfills an element and its `ElementInternals` to support `ARIAMixin`
|
||||||
|
* properties on internals. This is needed for Firefox.
|
||||||
|
*
|
||||||
|
* `setupHostAria()` must be called for the element class.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* class XButton extends LitElement {
|
||||||
|
* static {
|
||||||
|
* setupHostAria(XButton);
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* private internals =
|
||||||
|
* polyfillElementInternalsAria(this, this.attachInternals());
|
||||||
|
*
|
||||||
|
* constructor() {
|
||||||
|
* super();
|
||||||
|
* this.internals.role = 'button';
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export function polyfillElementInternalsAria(
|
||||||
|
host: ReactiveElement, internals: ElementInternals) {
|
||||||
|
if (checkIfElementInternalsSupportsAria(internals)) {
|
||||||
|
return internals;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('role' in host)) {
|
||||||
|
throw new Error('Missing setupHostAria()');
|
||||||
|
}
|
||||||
|
|
||||||
|
let firstConnectedCallbacks: Array<() => void> = [];
|
||||||
|
let hasBeenConnected = false;
|
||||||
|
|
||||||
|
// Add support for Firefox, which has not yet implement ElementInternals aria
|
||||||
|
for (const ariaProperty of ARIA_PROPERTIES) {
|
||||||
|
let ariaValueBeforeConnected: string|null = null;
|
||||||
|
Object.defineProperty(internals, ariaProperty, {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
get() {
|
||||||
|
if (!hasBeenConnected) {
|
||||||
|
return ariaValueBeforeConnected;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic lookup rather than hardcoding all properties.
|
||||||
|
// tslint:disable-next-line:no-dict-access-on-struct-type
|
||||||
|
return host[ariaProperty];
|
||||||
|
},
|
||||||
|
set(value: string|null) {
|
||||||
|
const setValue = () => {
|
||||||
|
// Dynamic lookup rather than hardcoding all properties.
|
||||||
|
// tslint:disable-next-line:no-dict-access-on-struct-type
|
||||||
|
host[ariaProperty] = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!hasBeenConnected) {
|
||||||
|
ariaValueBeforeConnected = value;
|
||||||
|
firstConnectedCallbacks.push(setValue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let roleValueBeforeConnected: string|null = null;
|
||||||
|
Object.defineProperty(internals, 'role', {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
get() {
|
||||||
|
if (!hasBeenConnected) {
|
||||||
|
return roleValueBeforeConnected;
|
||||||
|
}
|
||||||
|
|
||||||
|
return host.getAttribute('role');
|
||||||
|
},
|
||||||
|
set(value: string|null) {
|
||||||
|
const setRole = () => {
|
||||||
|
if (value === null) {
|
||||||
|
host.removeAttribute('role');
|
||||||
|
} else {
|
||||||
|
host.setAttribute('role', value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!hasBeenConnected) {
|
||||||
|
roleValueBeforeConnected = value;
|
||||||
|
firstConnectedCallbacks.push(setRole);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRole();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
host.addController({
|
||||||
|
hostConnected() {
|
||||||
|
if (hasBeenConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasBeenConnected = true;
|
||||||
|
for (const callback of firstConnectedCallbacks) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove strong callback references
|
||||||
|
firstConnectedCallbacks = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return internals;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Separate function so that typescript doesn't complain about internals being
|
||||||
|
// "never".
|
||||||
|
function checkIfElementInternalsSupportsAria(internals: ElementInternals) {
|
||||||
|
return 'role' in internals;
|
||||||
|
}
|
||||||
|
@ -6,7 +6,10 @@
|
|||||||
|
|
||||||
// import 'jasmine'; (google3-only)
|
// import 'jasmine'; (google3-only)
|
||||||
|
|
||||||
import {ARIAProperty, ariaPropertyToAttribute, isAriaAttribute} from './aria.js';
|
import {html, LitElement} from 'lit';
|
||||||
|
import {customElement} from 'lit/decorators.js';
|
||||||
|
|
||||||
|
import {ARIAProperty, ariaPropertyToAttribute, isAriaAttribute, polyfillElementInternalsAria, setupHostAria} from './aria.js';
|
||||||
|
|
||||||
describe('aria', () => {
|
describe('aria', () => {
|
||||||
describe('isAriaAttribute()', () => {
|
describe('isAriaAttribute()', () => {
|
||||||
@ -41,4 +44,216 @@ describe('aria', () => {
|
|||||||
.toBe('aria-labelledby');
|
.toBe('aria-labelledby');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('setupHostAria()', () => {
|
||||||
|
@customElement('test-setup-aria-host')
|
||||||
|
class TestElement extends LitElement {
|
||||||
|
static {
|
||||||
|
setupHostAria(TestElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
override render() {
|
||||||
|
return html`<slot></slot>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should not hydrate tabindex attribute on creation', () => {
|
||||||
|
const element = new TestElement();
|
||||||
|
expect(element.hasAttribute('tabindex'))
|
||||||
|
.withContext('has tabindex attribute')
|
||||||
|
.toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set tabindex="0" on element once connected', () => {
|
||||||
|
const element = new TestElement();
|
||||||
|
document.body.appendChild(element);
|
||||||
|
expect(element.getAttribute('tabindex'))
|
||||||
|
.withContext('tabindex attribute value')
|
||||||
|
.toEqual('0');
|
||||||
|
|
||||||
|
element.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not set tabindex on connected if one already exists', () => {
|
||||||
|
const element = new TestElement();
|
||||||
|
element.tabIndex = -1;
|
||||||
|
document.body.appendChild(element);
|
||||||
|
expect(element.getAttribute('tabindex'))
|
||||||
|
.withContext('tabindex attribute value')
|
||||||
|
.toEqual('-1');
|
||||||
|
|
||||||
|
element.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not change tabindex if disconnected and reconnected', () => {
|
||||||
|
const element = new TestElement();
|
||||||
|
document.body.appendChild(element);
|
||||||
|
element.tabIndex = -1;
|
||||||
|
element.remove();
|
||||||
|
document.body.appendChild(element);
|
||||||
|
expect(element.getAttribute('tabindex'))
|
||||||
|
.withContext('tabindex attribute value')
|
||||||
|
.toEqual('-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!('role' in Element.prototype)) {
|
||||||
|
describe('polyfill', () => {
|
||||||
|
it('should hydrate aria attributes when ARIAMixin is not supported',
|
||||||
|
async () => {
|
||||||
|
const element = new TestElement();
|
||||||
|
document.body.appendChild(element);
|
||||||
|
element.role = 'button';
|
||||||
|
await element.updateComplete;
|
||||||
|
expect(element.getAttribute('role'))
|
||||||
|
.withContext('role attribute value')
|
||||||
|
.toEqual('button');
|
||||||
|
|
||||||
|
element.remove();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('polyfillElementInternalsAria()', () => {
|
||||||
|
@customElement('test-polyfill-element-internals-aria')
|
||||||
|
class TestElement extends LitElement {
|
||||||
|
static {
|
||||||
|
setupHostAria(TestElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
internals = polyfillElementInternalsAria(this, this.attachInternals());
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.internals.role = 'button';
|
||||||
|
}
|
||||||
|
|
||||||
|
override render() {
|
||||||
|
return html`<slot></slot>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('role' in ElementInternals.prototype) {
|
||||||
|
it('should not hydrate attributes when role set', () => {
|
||||||
|
const element = new TestElement();
|
||||||
|
document.body.appendChild(element);
|
||||||
|
expect(element.hasAttribute('role'))
|
||||||
|
.withContext('has role attribute')
|
||||||
|
.toBeFalse();
|
||||||
|
|
||||||
|
element.remove();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
it('should preserve role values when set before connected', () => {
|
||||||
|
const element = new TestElement();
|
||||||
|
// TestElement() sets role in constructor
|
||||||
|
expect(element.internals.role)
|
||||||
|
.withContext('ElementInternals.role')
|
||||||
|
.toEqual('button');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve aria values when set before connected', () => {
|
||||||
|
const element = new TestElement();
|
||||||
|
element.internals.ariaLabel = 'Foo';
|
||||||
|
expect(element.internals.ariaLabel)
|
||||||
|
.withContext('ElementInternals.ariaLabel')
|
||||||
|
.toEqual('Foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should hydrate role attributes when set before connection',
|
||||||
|
async () => {
|
||||||
|
const element = new TestElement();
|
||||||
|
// TestElement() sets role in constructor
|
||||||
|
document.body.appendChild(element);
|
||||||
|
await element.updateComplete;
|
||||||
|
expect(element.getAttribute('role'))
|
||||||
|
.withContext('role attribute value')
|
||||||
|
.toEqual('button');
|
||||||
|
|
||||||
|
element.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should hydrate aria attributes when set before connection',
|
||||||
|
async () => {
|
||||||
|
const element = new TestElement();
|
||||||
|
element.internals.ariaLabel = 'Foo';
|
||||||
|
document.body.appendChild(element);
|
||||||
|
await element.updateComplete;
|
||||||
|
expect(element.getAttribute('aria-label'))
|
||||||
|
.withContext('aria-label attribute value')
|
||||||
|
.toEqual('Foo');
|
||||||
|
|
||||||
|
element.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set aria attributes when set after connection', async () => {
|
||||||
|
const element = new TestElement();
|
||||||
|
document.body.appendChild(element);
|
||||||
|
element.internals.ariaLabel = 'Value after construction';
|
||||||
|
await element.updateComplete;
|
||||||
|
expect(element.getAttribute('aria-label'))
|
||||||
|
.withContext('aria-label attribute value')
|
||||||
|
.toEqual('Value after construction');
|
||||||
|
|
||||||
|
element.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle setting role multiple times before connection',
|
||||||
|
async () => {
|
||||||
|
const element = new TestElement();
|
||||||
|
element.internals.role = 'button';
|
||||||
|
element.internals.role = 'checkbox';
|
||||||
|
|
||||||
|
expect(element.internals.role)
|
||||||
|
.withContext('internals.role before connection')
|
||||||
|
.toEqual('checkbox');
|
||||||
|
document.body.appendChild(element);
|
||||||
|
await element.updateComplete;
|
||||||
|
expect(element.internals.role)
|
||||||
|
.withContext('internals.role after connection')
|
||||||
|
.toEqual('checkbox');
|
||||||
|
|
||||||
|
element.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle setting aria properties multiple times before connection',
|
||||||
|
async () => {
|
||||||
|
const element = new TestElement();
|
||||||
|
element.internals.ariaLabel = 'First';
|
||||||
|
element.internals.ariaLabel = 'Second';
|
||||||
|
|
||||||
|
expect(element.internals.ariaLabel)
|
||||||
|
.withContext('internals.ariaLabel before connection')
|
||||||
|
.toEqual('Second');
|
||||||
|
document.body.appendChild(element);
|
||||||
|
await element.updateComplete;
|
||||||
|
expect(element.internals.ariaLabel)
|
||||||
|
.withContext('internals.ariaLabel after connection')
|
||||||
|
.toEqual('Second');
|
||||||
|
|
||||||
|
element.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle setting role after first connection while disconnected',
|
||||||
|
async () => {
|
||||||
|
const element = new TestElement();
|
||||||
|
element.internals.role = 'button';
|
||||||
|
document.body.appendChild(element);
|
||||||
|
await element.updateComplete;
|
||||||
|
|
||||||
|
element.remove();
|
||||||
|
element.internals.role = 'checkbox';
|
||||||
|
expect(element.internals.role)
|
||||||
|
.withContext('internals.role after connected and disconnected')
|
||||||
|
.toEqual('checkbox');
|
||||||
|
document.body.appendChild(element);
|
||||||
|
await element.updateComplete;
|
||||||
|
expect(element.internals.role)
|
||||||
|
.withContext('internals.role after reconnected')
|
||||||
|
.toEqual('checkbox');
|
||||||
|
|
||||||
|
element.remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -11,6 +11,7 @@ import {html, isServer, LitElement} from 'lit';
|
|||||||
import {property} from 'lit/decorators.js';
|
import {property} from 'lit/decorators.js';
|
||||||
import {classMap} from 'lit/directives/class-map.js';
|
import {classMap} from 'lit/directives/class-map.js';
|
||||||
|
|
||||||
|
import {polyfillElementInternalsAria, setupHostAria} from '../../internal/aria/aria.js';
|
||||||
import {isActivationClick} from '../../internal/controller/events.js';
|
import {isActivationClick} from '../../internal/controller/events.js';
|
||||||
|
|
||||||
import {SingleSelectionController} from './single-selection-controller.js';
|
import {SingleSelectionController} from './single-selection-controller.js';
|
||||||
@ -22,6 +23,10 @@ let maskId = 0;
|
|||||||
* A radio component.
|
* A radio component.
|
||||||
*/
|
*/
|
||||||
export class Radio extends LitElement {
|
export class Radio extends LitElement {
|
||||||
|
static {
|
||||||
|
setupHostAria(Radio);
|
||||||
|
}
|
||||||
|
|
||||||
/** @nocollapse */
|
/** @nocollapse */
|
||||||
static readonly formAssociated = true;
|
static readonly formAssociated = true;
|
||||||
|
|
||||||
@ -86,30 +91,19 @@ export class Radio extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private readonly selectionController = new SingleSelectionController(this);
|
private readonly selectionController = new SingleSelectionController(this);
|
||||||
private readonly internals =
|
private readonly internals = polyfillElementInternalsAria(
|
||||||
(this as HTMLElement /* needed for closure */).attachInternals();
|
this, (this as HTMLElement /* needed for closure */).attachInternals());
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.addController(this.selectionController);
|
this.addController(this.selectionController);
|
||||||
if (!isServer) {
|
if (!isServer) {
|
||||||
|
this.internals.role = 'radio';
|
||||||
this.addEventListener('click', this.handleClick.bind(this));
|
this.addEventListener('click', this.handleClick.bind(this));
|
||||||
this.addEventListener('keydown', this.handleKeydown.bind(this));
|
this.addEventListener('keydown', this.handleKeydown.bind(this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
// Firefox does not support ElementInternals aria yet, so we need to hydrate
|
|
||||||
// an attribute.
|
|
||||||
if (!('role' in this.internals)) {
|
|
||||||
this.setAttribute('role', 'radio');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.internals.role = 'radio';
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override render() {
|
protected override render() {
|
||||||
const classes = {checked: this.checked};
|
const classes = {checked: this.checked};
|
||||||
return html`
|
return html`
|
||||||
@ -140,13 +134,6 @@ export class Radio extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected override updated() {
|
protected override updated() {
|
||||||
// Firefox does not support ElementInternals aria yet, so we need to hydrate
|
|
||||||
// an attribute.
|
|
||||||
if (!('ariaChecked' in this.internals)) {
|
|
||||||
this.setAttribute('aria-checked', String(this.checked));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.internals.ariaChecked = String(this.checked);
|
this.internals.ariaChecked = String(this.checked);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,8 @@ import '../../divider/divider.js';
|
|||||||
import {html, isServer, LitElement, PropertyValues} from 'lit';
|
import {html, isServer, LitElement, PropertyValues} from 'lit';
|
||||||
import {property, queryAssignedElements, state} from 'lit/decorators.js';
|
import {property, queryAssignedElements, state} from 'lit/decorators.js';
|
||||||
|
|
||||||
|
import {polyfillElementInternalsAria, setupHostAria} from '../../internal/aria/aria.js';
|
||||||
|
|
||||||
import {Tab} from './tab.js';
|
import {Tab} from './tab.js';
|
||||||
|
|
||||||
const NAVIGATION_KEYS = new Map([
|
const NAVIGATION_KEYS = new Map([
|
||||||
@ -41,6 +43,10 @@ const NAVIGATION_KEYS = new Map([
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export class Tabs extends LitElement {
|
export class Tabs extends LitElement {
|
||||||
|
static {
|
||||||
|
setupHostAria(Tabs, {focusable: false});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Index of the selected item.
|
* Index of the selected item.
|
||||||
*/
|
*/
|
||||||
@ -86,30 +92,19 @@ export class Tabs extends LitElement {
|
|||||||
return this.items.find((el: HTMLElement) => el.matches(':focus-within'));
|
return this.items.find((el: HTMLElement) => el.matches(':focus-within'));
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly internals =
|
private readonly internals = polyfillElementInternalsAria(
|
||||||
(this as HTMLElement /* needed for closure */).attachInternals();
|
this, (this as HTMLElement /* needed for closure */).attachInternals());
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
if (!isServer) {
|
if (!isServer) {
|
||||||
|
this.internals.role = 'tablist';
|
||||||
this.addEventListener('keydown', this.handleKeydown);
|
this.addEventListener('keydown', this.handleKeydown);
|
||||||
this.addEventListener('keyup', this.handleKeyup);
|
this.addEventListener('keyup', this.handleKeyup);
|
||||||
this.addEventListener('focusout', this.handleFocusout);
|
this.addEventListener('focusout', this.handleFocusout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
// Firefox does not support ElementInternals aria yet, so we need to hydrate
|
|
||||||
// an attribute.
|
|
||||||
if (!('role' in this.internals)) {
|
|
||||||
this.setAttribute('role', 'tablist');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.internals.role = 'tablist';
|
|
||||||
}
|
|
||||||
|
|
||||||
// focus item on keydown and optionally select it
|
// focus item on keydown and optionally select it
|
||||||
private readonly handleKeydown = async (event: KeyboardEvent) => {
|
private readonly handleKeydown = async (event: KeyboardEvent) => {
|
||||||
const {key} = event;
|
const {key} = event;
|
||||||
|
Loading…
Reference in New Issue
Block a user