Skip to content

Commit 6f24bd2

Browse files
asynclizcopybara-github
authored andcommitted
fix(tab): move aria to host
PiperOrigin-RevId: 563200891
1 parent acd40a2 commit 6f24bd2

File tree

4 files changed

+29
-56
lines changed

4 files changed

+29
-56
lines changed

tabs/harness.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ import {Tabs} from './internal/tabs.js';
1515
export class TabHarness extends Harness<Tab> {
1616
override async getInteractiveElement() {
1717
await this.element.updateComplete;
18-
return this.element.renderRoot
19-
.querySelector<HTMLButtonElement|HTMLLinkElement>('.button')!;
18+
return this.element as HTMLElement;
2019
}
2120

2221
private async completeIndicatorAnimation() {

tabs/internal/_tab.scss

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
outline: none;
2222
-webkit-tap-highlight-color: transparent;
2323
vertical-align: middle;
24+
user-select: none;
2425

2526
@include ripple.theme(
2627
(
@@ -45,28 +46,18 @@
4546
}
4647

4748
.button {
48-
appearance: none;
49+
box-sizing: border-box;
4950
display: inline-flex;
5051
align-items: center;
5152
justify-content: center;
52-
border: none;
53-
outline: none;
54-
user-select: none;
5553
vertical-align: middle;
56-
background: none;
57-
text-decoration: none;
5854
width: 100%;
5955
position: relative;
6056
padding: 0 16px;
6157
margin: 0;
6258
z-index: 0; // Ensure this is a stacking context so the indicator displays
6359
font: var(--_label-text-type);
6460
color: var(--_label-text-color);
65-
66-
&::-moz-focus-inner {
67-
padding: 0;
68-
border: 0;
69-
}
7061
}
7162

7263
.button::before {

tabs/internal/tab.ts

Lines changed: 25 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@ import {html, isServer, LitElement, nothing, PropertyValues} from 'lit';
1212
import {property, query, queryAssignedElements, queryAssignedNodes, state} from 'lit/decorators.js';
1313
import {classMap} from 'lit/directives/class-map.js';
1414

15-
import {ARIAMixinStrict} from '../../internal/aria/aria.js';
16-
import {requestUpdateOnAriaChange} from '../../internal/aria/delegate.js';
17-
import {dispatchActivationClick, isActivationClick} from '../../internal/controller/events.js';
15+
import {polyfillElementInternalsAria, setupHostAria} from '../../internal/aria/aria.js';
1816
import {EASING} from '../../internal/motion/animation.js';
1917

2018
interface Tabs extends HTMLElement {
@@ -28,23 +26,14 @@ interface Tabs extends HTMLElement {
2826
*/
2927
export class Tab extends LitElement {
3028
static {
31-
requestUpdateOnAriaChange(Tab);
29+
setupHostAria(Tab);
3230
}
3331

34-
/** @nocollapse */
35-
static override shadowRootOptions:
36-
ShadowRootInit = {mode: 'open', delegatesFocus: true};
37-
3832
/**
3933
* Whether or not the tab is `selected`.
4034
**/
4135
@property({type: Boolean, reflect: true}) selected = false;
4236

43-
/**
44-
* Whether or not the tab is `focusable`.
45-
*/
46-
@property({type: Boolean}) focusable = false;
47-
4837
/**
4938
* In SSR, set this to true when an icon is present.
5039
*/
@@ -55,8 +44,6 @@ export class Tab extends LitElement {
5544
*/
5645
@property({type: Boolean, attribute: 'icon-only'}) iconOnly = false;
5746

58-
@query('.button') private readonly button!: HTMLElement|null;
59-
6047
// note, this is public so it can participate in selection animation.
6148
/** @private */
6249
@query('.indicator') readonly indicator!: HTMLElement;
@@ -65,44 +52,33 @@ export class Tab extends LitElement {
6552
private readonly assignedDefaultNodes!: Node[];
6653
@queryAssignedElements({slot: 'icon', flatten: true})
6754
private readonly assignedIcons!: HTMLElement[];
55+
private readonly internals = polyfillElementInternalsAria(
56+
this, (this as HTMLElement /* needed for closure */).attachInternals());
6857

6958
constructor() {
7059
super();
7160
if (!isServer) {
72-
this.addEventListener('click', this.handleActivationClick);
61+
this.internals.role = 'tab';
62+
this.addEventListener('keydown', this.handleKeydown.bind(this));
7363
}
7464
}
7565

76-
override focus() {
77-
this.button?.focus();
78-
}
79-
80-
override blur() {
81-
this.button?.blur();
82-
}
83-
8466
protected override render() {
8567
const indicator = html`<div class="indicator"></div>`;
86-
// Needed for closure conformance
87-
const {ariaLabel} = this as ARIAMixinStrict;
8868
return html`
89-
<button
90-
class="button"
91-
role="tab"
92-
.tabIndex=${this.focusable ? 0 : -1}
93-
aria-selected=${this.selected ? 'true' : 'false'}
94-
aria-label=${ariaLabel || nothing}
95-
>
96-
<md-focus-ring part="focus-ring" inward></md-focus-ring>
69+
<div class="button" role="presentation">
70+
<md-focus-ring part="focus-ring" inward
71+
.control=${this}></md-focus-ring>
9772
<md-elevation></md-elevation>
98-
<md-ripple></md-ripple>
99-
<div class="content ${classMap(this.getContentClasses())}">
73+
<md-ripple .control=${this}></md-ripple>
74+
<div class="content ${classMap(this.getContentClasses())}"
75+
role="presentation">
10076
<slot name="icon" @slotchange=${this.handleIconSlotChange}></slot>
10177
<slot @slotchange=${this.handleSlotChange}></slot>
10278
${this.fullWidthIndicator ? nothing : indicator}
10379
</div>
10480
${this.fullWidthIndicator ? indicator : nothing}
105-
</button>`;
81+
</div>`;
10682
}
10783

10884
protected getContentClasses() {
@@ -114,17 +90,24 @@ export class Tab extends LitElement {
11490

11591
protected override updated(changed: PropertyValues) {
11692
if (changed.has('selected')) {
93+
this.internals.ariaSelected = String(this.selected);
11794
this.animateSelected();
11895
}
11996
}
12097

121-
private readonly handleActivationClick = (event: MouseEvent) => {
122-
if (!isActivationClick((event)) || !this.button) {
98+
private async handleKeydown(event: KeyboardEvent) {
99+
// Allow event to bubble.
100+
await 0;
101+
if (event.defaultPrevented) {
123102
return;
124103
}
125-
this.focus();
126-
dispatchActivationClick(this.button);
127-
};
104+
105+
if (event.key === 'Enter' || event.key === ' ') {
106+
// Prevent default behavior such as scrolling when pressing spacebar.
107+
event.preventDefault();
108+
this.click();
109+
}
110+
}
128111

129112
private animateSelected() {
130113
this.indicator.getAnimations().forEach(a => {

tabs/internal/tabs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ export class Tabs extends LitElement {
227227

228228
private updateFocusableItem(focusableItem: HTMLElement|null) {
229229
for (const item of this.items) {
230-
item.focusable = item === focusableItem;
230+
item.tabIndex = item === focusableItem ? 0 : -1;
231231
}
232232
}
233233

0 commit comments

Comments
 (0)