diff --git a/apps/playground/src/app/components/tabs/vertical-tabset/vertical-tabset.component.html b/apps/playground/src/app/components/tabs/vertical-tabset/vertical-tabset.component.html index 6e295ed6ea..88efe081ca 100644 --- a/apps/playground/src/app/components/tabs/vertical-tabset/vertical-tabset.component.html +++ b/apps/playground/src/app/components/tabs/vertical-tabset/vertical-tabset.component.html @@ -54,6 +54,9 @@

Vertical tabset

Group 3 Tab 2 content + + Ungrouped tab content + diff --git a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tab.component.html b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tab.component.html index 357a938222..962e288d59 100644 --- a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tab.component.html +++ b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tab.component.html @@ -1,18 +1,22 @@
diff --git a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tab.component.scss b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tab.component.scss index 27aeeacef1..6069640300 100644 --- a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tab.component.scss +++ b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tab.component.scss @@ -50,7 +50,7 @@ margin-left: $sky-margin-half; } -.sky-vertical-tab-disabled { +.sky-vertical-tabset-button-disabled { cursor: not-allowed; pointer-events: none; } @@ -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; diff --git a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tab.component.ts b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tab.component.ts index 260e54faa9..a6dc67d1af 100644 --- a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tab.component.ts +++ b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tab.component.ts @@ -9,6 +9,7 @@ import { OnInit, Optional, ViewChild, + inject, } from '@angular/core'; import { SkyMediaQueryService } from '@skyux/core'; @@ -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; @@ -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; @@ -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; @@ -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(); } } diff --git a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset-adapter.service.ts b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset-adapter.service.ts index 35355977e2..ec1420cf27 100644 --- a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset-adapter.service.ts +++ b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset-adapter.service.ts @@ -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); @@ -56,4 +62,79 @@ export class SkyVerticalTabsetAdapterService { this.#renderer.addClass(nativeEl, newClass); } + + public focusButton(elRef: ElementRef | undefined): boolean { + return this.#focusButtonEl(elRef?.nativeElement); + } + + public focusFirstButton( + tabGroups: ElementRef | 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 | 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; + } } diff --git a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset-group-message.ts b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset-group-message.ts new file mode 100644 index 0000000000..1344420c5c --- /dev/null +++ b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset-group-message.ts @@ -0,0 +1,3 @@ +export interface SkyVerticalTabsetGroupMessage { + messageType: 'focus'; +} diff --git a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset-group.component.html b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset-group.component.html index ec0df45281..6ea9a45abb 100644 --- a/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset-group.component.html +++ b/libs/components/tabs/src/lib/modules/vertical-tabset/vertical-tabset-group.component.html @@ -3,21 +3,28 @@