Skip to content

Commit 49c7872

Browse files
feat(components/tabs): wizard keyboard nav and roles (#558)
1 parent 0183930 commit 49c7872

File tree

11 files changed

+553
-76
lines changed

11 files changed

+553
-76
lines changed

libs/components/modals/src/lib/modules/modal/modal.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
<div
6060
class="sky-modal-content sky-padding-even-large"
6161
role="region"
62-
tabindex="0"
62+
tabindex="-1"
6363
[attr.aria-labelledby]="modalHeaderId"
6464
[attr.id]="modalContentId"
6565
(skyModalScrollShadow)="scrollShadowChange($event)"

libs/components/tabs/src/lib/modules/tabs/fixtures/tabset.component.fixture.html

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
<div [ngStyle]="{'max-width.px': tabMaxWidth}">
2-
<sky-tabset [ariaLabel]="ariaLabel" [ariaLabelledBy]="ariaLabelledBy">
2+
<sky-tabset
3+
[ariaLabel]="ariaLabel"
4+
[ariaLabelledBy]="ariaLabelledBy"
5+
tabStyle="tabs"
6+
>
37
<sky-tab [tabHeading]="tab1Heading" [active]="activeTab === 0">
48
<div class="tabset-test-content-1">{{tab1Content}}</div>
59
</sky-tab>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { ElementRef, Injectable } from '@angular/core';
2+
3+
/**
4+
* @internal
5+
*/
6+
@Injectable()
7+
export class SkyTabButtonAdapterService {
8+
public focusButtonLink(buttonEl: ElementRef): void {
9+
buttonEl.nativeElement.querySelector('a').focus();
10+
}
11+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { SkyTabIndex } from './tab-index';
2+
3+
/**
4+
* @internal
5+
*/
6+
export type TabButtonViewModel = {
7+
active: boolean;
8+
ariaControls: string;
9+
buttonHref: string;
10+
buttonId: string;
11+
buttonText: string;
12+
buttonTextCount: string;
13+
closeable: boolean;
14+
disabled: boolean;
15+
tabIndex: SkyTabIndex;
16+
};

libs/components/tabs/src/lib/modules/tabs/tab-button.component.html

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
}"
77
>
88
<a
9+
#tabButtonEl
910
class="sky-btn-tab"
10-
role="tab"
11+
[attr.role]="elementRole"
1112
[attr.aria-controls]="ariaControls"
1213
[attr.aria-disabled]="disabled"
1314
[attr.aria-selected]="active"
@@ -19,9 +20,10 @@
1920
'sky-btn-tab-disabled': disabled,
2021
'sky-tab-btn-closeable': closeable
2122
}"
22-
[tabindex]="disabled ? '-1' : '0'"
23+
[tabindex]="active ? '0' : '-1'"
2324
(click)="onButtonClick($event)"
2425
(keydown)="onTabButtonKeyDown($event)"
26+
(focus)="onFocus()"
2527
>
2628
<span class="sky-tab-heading">
2729
{{ buttonText }}
@@ -34,6 +36,7 @@
3436
<button
3537
*ngIf="closeable"
3638
class="sky-btn-tab-close"
39+
[tabindex]="closeBtnTabIndex"
3740
type="button"
3841
[attr.aria-label]="'skyux_tab_close' | skyLibResources: buttonText"
3942
[disabled]="disabled"

libs/components/tabs/src/lib/modules/tabs/tab-button.component.ts

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,25 @@
11
import {
2+
AfterViewInit,
23
ChangeDetectionStrategy,
4+
ChangeDetectorRef,
35
Component,
6+
ElementRef,
47
EventEmitter,
58
Input,
9+
OnDestroy,
610
Output,
711
ViewEncapsulation,
812
} from '@angular/core';
913

14+
import { Subject } from 'rxjs';
15+
import { distinctUntilChanged, takeUntil } from 'rxjs/operators';
16+
17+
import { SkyTabButtonAdapterService } from './tab-button-adapter.service';
18+
import { SkyTabIndex } from './tab-index';
1019
import { SkyTabsetStyle } from './tabset-style';
20+
import { SkyTabsetService } from './tabset.service';
21+
22+
const DEFAULT_ELEMENT_ROLE = 'tab';
1123

1224
/**
1325
* @internal
@@ -16,10 +28,11 @@ import { SkyTabsetStyle } from './tabset-style';
1628
selector: 'sky-tab-button',
1729
templateUrl: './tab-button.component.html',
1830
styleUrls: ['./tab-button.component.scss'],
31+
providers: [SkyTabButtonAdapterService],
1932
changeDetection: ChangeDetectionStrategy.OnPush,
2033
encapsulation: ViewEncapsulation.None,
2134
})
22-
export class SkyTabButtonComponent {
35+
export class SkyTabButtonComponent implements AfterViewInit, OnDestroy {
2336
@Input()
2437
public active: boolean;
2538

@@ -45,14 +58,65 @@ export class SkyTabButtonComponent {
4558
public disabled: boolean;
4659

4760
@Input()
48-
public tabStyle: SkyTabsetStyle;
61+
public tabIndex: SkyTabIndex;
62+
63+
@Input()
64+
public get tabStyle(): SkyTabsetStyle {
65+
return this.#_tabStyle;
66+
}
67+
68+
public set tabStyle(style: SkyTabsetStyle | undefined) {
69+
this.#_tabStyle = style;
70+
this.elementRole = style === 'tabs' ? DEFAULT_ELEMENT_ROLE : undefined;
71+
}
4972

5073
@Output()
5174
public buttonClick = new EventEmitter<void>();
5275

5376
@Output()
5477
public closeClick = new EventEmitter<void>();
5578

79+
constructor(
80+
elementRef: ElementRef,
81+
adapterService: SkyTabButtonAdapterService,
82+
changeDetectorRef: ChangeDetectorRef,
83+
tabsetService: SkyTabsetService
84+
) {
85+
this.#adapterService = adapterService;
86+
this.#changeDetectorRef = changeDetectorRef;
87+
this.#elementRef = elementRef;
88+
this.#tabsetService = tabsetService;
89+
}
90+
91+
public elementRole: string | undefined = DEFAULT_ELEMENT_ROLE;
92+
public closeBtnTabIndex = '-1';
93+
94+
#_tabStyle: SkyTabsetStyle;
95+
#adapterService: SkyTabButtonAdapterService;
96+
#changeDetectorRef: ChangeDetectorRef;
97+
#elementRef: ElementRef;
98+
#tabsetService: SkyTabsetService;
99+
#ngUnsubscribe = new Subject<void>();
100+
101+
public ngAfterViewInit(): void {
102+
this.#tabsetService.focusedTabBtnIndex
103+
.pipe(distinctUntilChanged(), takeUntil(this.#ngUnsubscribe))
104+
.subscribe((focusedIndex) => {
105+
if (focusedIndex === this.tabIndex) {
106+
this.closeBtnTabIndex = '0';
107+
this.focusBtn();
108+
} else {
109+
this.closeBtnTabIndex = '-1';
110+
}
111+
this.#changeDetectorRef.markForCheck();
112+
});
113+
}
114+
115+
public ngOnDestroy(): void {
116+
this.#ngUnsubscribe.next();
117+
this.#ngUnsubscribe.complete();
118+
}
119+
56120
public onButtonClick(event: any): void {
57121
if (!this.disabled) {
58122
this.buttonClick.emit();
@@ -83,4 +147,12 @@ export class SkyTabButtonComponent {
83147
event.stopPropagation();
84148
event.preventDefault();
85149
}
150+
151+
public focusBtn(): void {
152+
this.#adapterService.focusButtonLink(this.#elementRef);
153+
}
154+
155+
public onFocus(): void {
156+
this.#tabsetService.setFocusedTabBtnIndex(this.tabIndex);
157+
}
86158
}

libs/components/tabs/src/lib/modules/tabs/tabset-nav-button.component.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { TestBed, async, fakeAsync, tick } from '@angular/core/testing';
1+
import { TestBed, fakeAsync, tick } from '@angular/core/testing';
22
import { expect, expectAsync } from '@skyux-sdk/testing';
33
import { SkyLogService } from '@skyux/core';
44

@@ -64,13 +64,13 @@ describe('Tabset navigation button', () => {
6464

6565
describe('wizard style', () => {
6666
describe('without finish button', () => {
67-
it('should be accessible', async(async () => {
67+
it('should be accessible', async () => {
6868
const fixture = TestBed.createComponent(SkyWizardTestFormComponent);
6969
fixture.detectChanges();
7070
await fixture.whenStable();
7171
fixture.detectChanges();
7272
await expectAsync(fixture.nativeElement).toBeAccessible();
73-
}));
73+
});
7474

7575
describe('previous button', () => {
7676
it('should navigate to the previous tab when clicked', fakeAsync(() => {

libs/components/tabs/src/lib/modules/tabs/tabset.component.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
<div
22
class="sky-tabset"
3-
role="tablist"
3+
[attr.role]="elementRole"
44
[attr.aria-label]="ariaLabel"
55
[attr.aria-labelledby]="ariaLabelledBy"
66
[ngClass]="
77
'sky-tabset-mode-' + tabDisplayMode + ' sky-tabset-style-' + tabStyle
88
"
99
(window:resize)="onWindowResize()"
10+
(keydown.arrowright)="onRightKeyDown()"
11+
(keydown.arrowleft)="onLeftKeyDown()"
12+
(keydown.home)="onHomeKeyDown()"
13+
(keydown.end)="onEndKeyDown()"
1014
>
1115
<span class="sky-tabset-dropdown">
1216
<sky-dropdown *ngIf="tabDisplayMode === 'dropdown'" buttonType="tab">
@@ -88,6 +92,7 @@
8892
[buttonTextCount]="tabButton.buttonTextCount"
8993
[closeable]="tabButton.closeable"
9094
[disabled]="tabButton.disabled"
95+
[tabIndex]="tabButton.tabIndex"
9196
[tabStyle]="tabStyle"
9297
(buttonClick)="onTabButtonClick(tabButton)"
9398
(closeClick)="onTabCloseClick(tabButton)"

0 commit comments

Comments
 (0)