From 6f24bd222a74534f298d5a6ce8c4ef202b853b0b Mon Sep 17 00:00:00 2001 From: Elizabeth Mitchell Date: Wed, 6 Sep 2023 13:32:40 -0700 Subject: [PATCH] fix(tab): move aria to host PiperOrigin-RevId: 563200891 --- tabs/harness.ts | 3 +- tabs/internal/_tab.scss | 13 ++------ tabs/internal/tab.ts | 67 +++++++++++++++-------------------------- tabs/internal/tabs.ts | 2 +- 4 files changed, 29 insertions(+), 56 deletions(-) diff --git a/tabs/harness.ts b/tabs/harness.ts index bf97d7c3a..dc8c25e14 100644 --- a/tabs/harness.ts +++ b/tabs/harness.ts @@ -15,8 +15,7 @@ import {Tabs} from './internal/tabs.js'; export class TabHarness extends Harness { override async getInteractiveElement() { await this.element.updateComplete; - return this.element.renderRoot - .querySelector('.button')!; + return this.element as HTMLElement; } private async completeIndicatorAnimation() { diff --git a/tabs/internal/_tab.scss b/tabs/internal/_tab.scss index 0dc54af5a..c99f3c303 100644 --- a/tabs/internal/_tab.scss +++ b/tabs/internal/_tab.scss @@ -21,6 +21,7 @@ outline: none; -webkit-tap-highlight-color: transparent; vertical-align: middle; + user-select: none; @include ripple.theme( ( @@ -45,16 +46,11 @@ } .button { - appearance: none; + box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; - border: none; - outline: none; - user-select: none; vertical-align: middle; - background: none; - text-decoration: none; width: 100%; position: relative; padding: 0 16px; @@ -62,11 +58,6 @@ z-index: 0; // Ensure this is a stacking context so the indicator displays font: var(--_label-text-type); color: var(--_label-text-color); - - &::-moz-focus-inner { - padding: 0; - border: 0; - } } .button::before { diff --git a/tabs/internal/tab.ts b/tabs/internal/tab.ts index 537f324de..5fcd4203f 100644 --- a/tabs/internal/tab.ts +++ b/tabs/internal/tab.ts @@ -12,9 +12,7 @@ import {html, isServer, LitElement, nothing, PropertyValues} from 'lit'; import {property, query, queryAssignedElements, queryAssignedNodes, state} from 'lit/decorators.js'; import {classMap} from 'lit/directives/class-map.js'; -import {ARIAMixinStrict} from '../../internal/aria/aria.js'; -import {requestUpdateOnAriaChange} from '../../internal/aria/delegate.js'; -import {dispatchActivationClick, isActivationClick} from '../../internal/controller/events.js'; +import {polyfillElementInternalsAria, setupHostAria} from '../../internal/aria/aria.js'; import {EASING} from '../../internal/motion/animation.js'; interface Tabs extends HTMLElement { @@ -28,23 +26,14 @@ interface Tabs extends HTMLElement { */ export class Tab extends LitElement { static { - requestUpdateOnAriaChange(Tab); + setupHostAria(Tab); } - /** @nocollapse */ - static override shadowRootOptions: - ShadowRootInit = {mode: 'open', delegatesFocus: true}; - /** * Whether or not the tab is `selected`. **/ @property({type: Boolean, reflect: true}) selected = false; - /** - * Whether or not the tab is `focusable`. - */ - @property({type: Boolean}) focusable = false; - /** * In SSR, set this to true when an icon is present. */ @@ -55,8 +44,6 @@ export class Tab extends LitElement { */ @property({type: Boolean, attribute: 'icon-only'}) iconOnly = false; - @query('.button') private readonly button!: HTMLElement|null; - // note, this is public so it can participate in selection animation. /** @private */ @query('.indicator') readonly indicator!: HTMLElement; @@ -65,44 +52,33 @@ export class Tab extends LitElement { private readonly assignedDefaultNodes!: Node[]; @queryAssignedElements({slot: 'icon', flatten: true}) private readonly assignedIcons!: HTMLElement[]; + private readonly internals = polyfillElementInternalsAria( + this, (this as HTMLElement /* needed for closure */).attachInternals()); constructor() { super(); if (!isServer) { - this.addEventListener('click', this.handleActivationClick); + this.internals.role = 'tab'; + this.addEventListener('keydown', this.handleKeydown.bind(this)); } } - override focus() { - this.button?.focus(); - } - - override blur() { - this.button?.blur(); - } - protected override render() { const indicator = html`
`; - // Needed for closure conformance - const {ariaLabel} = this as ARIAMixinStrict; return html` - `; + `; } protected getContentClasses() { @@ -114,17 +90,24 @@ export class Tab extends LitElement { protected override updated(changed: PropertyValues) { if (changed.has('selected')) { + this.internals.ariaSelected = String(this.selected); this.animateSelected(); } } - private readonly handleActivationClick = (event: MouseEvent) => { - if (!isActivationClick((event)) || !this.button) { + private async handleKeydown(event: KeyboardEvent) { + // Allow event to bubble. + await 0; + if (event.defaultPrevented) { return; } - this.focus(); - dispatchActivationClick(this.button); - }; + + if (event.key === 'Enter' || event.key === ' ') { + // Prevent default behavior such as scrolling when pressing spacebar. + event.preventDefault(); + this.click(); + } + } private animateSelected() { this.indicator.getAnimations().forEach(a => { diff --git a/tabs/internal/tabs.ts b/tabs/internal/tabs.ts index 46e159fd8..92443a5da 100644 --- a/tabs/internal/tabs.ts +++ b/tabs/internal/tabs.ts @@ -227,7 +227,7 @@ export class Tabs extends LitElement { private updateFocusableItem(focusableItem: HTMLElement|null) { for (const item of this.items) { - item.focusable = item === focusableItem; + item.tabIndex = item === focusableItem ? 0 : -1; } }