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 @@