Skip to content

Commit

Permalink
feat(components/tabs): add keyboard navigation to vertical tabset (#1978
Browse files Browse the repository at this point in the history
)
  • Loading branch information
Blackbaud-PaulCrowder committed Feb 2, 2024
1 parent f27b176 commit 2ceaefe
Show file tree
Hide file tree
Showing 15 changed files with 623 additions and 113 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ <h2>Vertical tabset</h2>
Group 3 Tab 2 content
</sky-vertical-tab>
</sky-vertical-tabset-group>
<sky-vertical-tab tabHeading="Ungrouped Tab" tabHeaderCount="2">
Ungrouped tab content
</sky-vertical-tab>
</sky-vertical-tabset>
</div>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
<a
class="sky-vertical-tab"
class="sky-vertical-tab sky-vertical-tabset-button"
[attr.aria-controls]="isMobile ? null : ariaControls || tabContentPane.id"
[attr.aria-disabled]="disabled || undefined"
[attr.aria-selected]="active"
[attr.id]="tabId"
[attr.role]="isMobile ? undefined : ariaRole"
[ngClass]="{
'sky-vertical-tab-active': active,
'sky-vertical-tab-disabled': disabled,
'sky-vertical-tabset-button-disabled': disabled,
'sky-font-deemphasized': disabled,
'sky-deemphasized': disabled
}"
[tabIndex]="tabIndex()"
[tabIndex]="-1"
(click)="activateTab()"
(keyup)="onTabButtonKeyUp($event)"
(keyup.arrowleft)="tabButtonArrowLeft($event)"
(keyup.enter)="tabButtonActivate($event)"
(keyup.space)="tabButtonActivate($event)"
#tabButton
>
<div class="sky-vertical-tab-display">
<div class="sky-vertical-tab-heading">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
margin-left: $sky-margin-half;
}

.sky-vertical-tab-disabled {
.sky-vertical-tabset-button-disabled {
cursor: not-allowed;
pointer-events: none;
}
Expand All @@ -60,6 +60,13 @@
color: $sky-text-color-icon-borderless;
}

.sky-vertical-tab-content-pane {
&:focus-visible {
outline: none;
border: none;
}
}

@include mixins.sky-theme-modern {
.sky-vertical-tab {
color: $sky-text-color-deemphasized;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
OnInit,
Optional,
ViewChild,
inject,
} from '@angular/core';
import { SkyMediaQueryService } from '@skyux/core';

Expand All @@ -19,6 +20,7 @@ import { SkyTabIdService } from '../shared/tab-id.service';

import { SkyVerticalTabMediaQueryService } from './vertical-tab-media-query.service';
import { SkyVerticalTabsetAdapterService } from './vertical-tabset-adapter.service';
import { SkyVerticalTabsetGroupService } from './vertical-tabset-group.service';
import { SkyVerticalTabsetService } from './vertical-tabset.service';

let nextId = 0;
Expand Down Expand Up @@ -140,9 +142,16 @@ export class SkyVerticalTabComponent implements OnInit, OnDestroy {

public isMobile = false;

@ViewChild('tabButton')
public tabButton: ElementRef | undefined;

@ViewChild('tabContentWrapper')
public tabContent: ElementRef | undefined;

public groupService = inject(SkyVerticalTabsetGroupService, {
optional: true,
});

#_ariaRole = 'tab';

#_contentRendered = false;
Expand Down Expand Up @@ -210,14 +219,6 @@ export class SkyVerticalTabComponent implements OnInit, OnDestroy {
this.#tabsetService.destroyTab(this);
}

public tabIndex(): number {
if (!this.disabled) {
return 0;
} else {
return -1;
}
}

public activateTab(): void {
if (!this.disabled) {
this.active = true;
Expand All @@ -227,19 +228,22 @@ export class SkyVerticalTabComponent implements OnInit, OnDestroy {
}
}

public onTabButtonKeyUp(event: KeyboardEvent): void {
/*istanbul ignore else */
if (event.key) {
switch (event.key.toUpperCase()) {
case ' ':
case 'ENTER':
this.activateTab();
event.stopPropagation();
break;
/* istanbul ignore next */
default:
break;
}
public focusButton(): void {
this.#adapterService.focusButton(this.tabButton);
}

public tabButtonActivate(event: Event): void {
this.activateTab();
event.stopPropagation();
}

public tabButtonArrowLeft(event: Event): void {
if (this.groupService) {
this.groupService.messageStream.next({
messageType: 'focus',
});

event.preventDefault();
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { DOCUMENT } from '@angular/common';
import {
ElementRef,
Injectable,
Renderer2,
RendererFactory2,
inject,
} from '@angular/core';
import { SkyMediaBreakpoints } from '@skyux/core';

const VERTICAL_TABSET_BUTTON_SELECTOR = '.sky-vertical-tabset-button';
const VERTICAL_TABSET_BUTTON_DISABLED_SELECTOR = `${VERTICAL_TABSET_BUTTON_SELECTOR}-disabled`;

@Injectable()
export class SkyVerticalTabsetAdapterService {
#renderer: Renderer2;
#document = inject(DOCUMENT);

constructor(rendererFactory: RendererFactory2) {
this.#renderer = rendererFactory.createRenderer(undefined, null);
Expand Down Expand Up @@ -56,4 +62,79 @@ export class SkyVerticalTabsetAdapterService {

this.#renderer.addClass(nativeEl, newClass);
}

public focusButton(elRef: ElementRef<HTMLElement> | undefined): boolean {
return this.#focusButtonEl(elRef?.nativeElement);
}

public focusFirstButton(
tabGroups: ElementRef<HTMLElement> | undefined,
): void {
const tabGroupsEl = tabGroups?.nativeElement;

if (tabGroupsEl) {
const firstButtonEl = tabGroupsEl.querySelector(
VERTICAL_TABSET_BUTTON_SELECTOR,
) as HTMLElement | null;

if (firstButtonEl) {
firstButtonEl.focus();
}
}
}

public focusNextButton(tabGroups: ElementRef | undefined): void {
this.#focusSiblingButton(tabGroups);
}

public focusPreviousButton(tabGroups: ElementRef | undefined): void {
this.#focusSiblingButton(tabGroups, true);
}

#focusSiblingButton(
tabGroups: ElementRef<HTMLElement> | undefined,
previous?: boolean,
): void {
if (tabGroups) {
const focusedEl = this.#document.activeElement as HTMLElement | null;

if (focusedEl?.matches(VERTICAL_TABSET_BUTTON_SELECTOR)) {
const tabGroupsEl = tabGroups.nativeElement;

const buttonEls = Array.from(
tabGroupsEl.querySelectorAll(VERTICAL_TABSET_BUTTON_SELECTOR),
) as HTMLElement[];

if (previous) {
buttonEls.reverse();
}

const focusedIndex = buttonEls.indexOf(focusedEl);

for (let i = 1, n = buttonEls.length; i < n; i++) {
// Offset the index of the next button from the index of the
// currently focused button, circling back to the first button
// when the end of the list is reached.
const offset = (i + focusedIndex) % n;

if (this.#focusButtonEl(buttonEls[offset])) {
break;
}
}
}
}
}

#focusButtonEl(el: HTMLElement | null | undefined): boolean {
if (el && !el.matches(VERTICAL_TABSET_BUTTON_DISABLED_SELECTOR)) {
el.focus();
return this.#elHasFocus(el);
}

return false;
}

#elHasFocus(el: HTMLElement | null | undefined): boolean {
return !!el && el === this.#document.activeElement;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface SkyVerticalTabsetGroupMessage {
messageType: 'focus';
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,28 @@
<div
class="sky-vertical-tabset-group-header"
[ngClass]="{
'sky-vertical-tabset-group-header-sub-open sky-font-emphasized':
subMenuOpen(),
'sky-vertical-tabset-group-header-active sky-font-emphasized':
isActive(),
'sky-vertical-tabset-group-header-sub-open': open,
'sky-font-deemphasized': disabled,
'sky-deemphasized': disabled,
'sky-vertical-tabset-disabled': disabled
}"
[tabIndex]="-1"
>
<button
class="sky-padding-even-default"
class="sky-padding-even-default sky-vertical-tabset-button"
role="tab"
type="button"
skyId
[attr.aria-controls]="groupContent.id"
[attr.aria-disabled]="disabled || undefined"
[attr.aria-expanded]="!disabled && open"
[id]="groupId"
[ngClass]="{
'sky-vertical-tabset-button-disabled': disabled
}"
[tabIndex]="-1"
(click)="toggleMenuOpen()"
(keyup.arrowleft)="groupButtonArrowLeft($event)"
(keyup.arrowright)="groupButtonArrowRight($event)"
#groupHeadingButton
>
{{ groupHeading }}
Expand All @@ -31,7 +38,7 @@
<div
class="sky-vertical-tabset-group-content"
skyId
[attr.labelledby]="groupHeadingButton.id"
[attr.labelledby]="groupId"
[@.disabled]="animationDisabled"
[@skyAnimationSlide]="slideDirection"
(@skyAnimationSlide.done)="updateSlideDirection($event)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
font-size: 14px !important;
}

.sky-vertical-tabset-disabled {
.sky-vertical-tabset-button-disabled {
cursor: not-allowed;
pointer-events: none;
}
Expand Down
Loading

0 comments on commit 2ceaefe

Please sign in to comment.