diff --git a/src/angular/tabs/paginated-tab-header.ts b/src/angular/tabs/paginated-tab-header.ts index cd18e43fc2..3c717ab340 100644 --- a/src/angular/tabs/paginated-tab-header.ts +++ b/src/angular/tabs/paginated-tab-header.ts @@ -216,7 +216,9 @@ export abstract class SbbPaginatedTabHeader this._keyManager = new FocusKeyManager(this._items) .withHorizontalOrientation('ltr') .withHomeAndEnd() - .withWrap(); + .withWrap() + // Allow focus to land on disabled tabs, as per https://w3c.github.io/aria-practices/#kbd_disabled_controls + .skipPredicate(() => false); this._keyManager.updateActiveItem(this._selectedIndex); @@ -333,8 +335,12 @@ export abstract class SbbPaginatedTabHeader case ENTER: case SPACE: if (this.focusIndex !== this.selectedIndex) { - this.selectFocusedIndex.emit(this.focusIndex); - this._itemSelected(event); + const item = this._items.get(this.focusIndex); + + if (item && !item.disabled) { + this.selectFocusedIndex.emit(this.focusIndex); + this._itemSelected(event); + } } break; default: @@ -395,12 +401,7 @@ export abstract class SbbPaginatedTabHeader * providing a valid index and return true. */ _isValidIndex(index: number): boolean { - if (!this._items) { - return true; - } - - const tab = this._items ? this._items.toArray()[index] : null; - return !!tab && !tab.disabled; + return this._items ? !!this._items.toArray()[index] : true; } /** diff --git a/src/angular/tabs/tab-group.html b/src/angular/tabs/tab-group.html index 5e54491918..6ab33ebef9 100644 --- a/src/angular/tabs/tab-group.html +++ b/src/angular/tabs/tab-group.html @@ -12,7 +12,7 @@ cdkMonitorElementFocus *ngFor="let tab of _tabs; let i = index" [id]="_getTabLabelId(i)" - [attr.tabIndex]="_getTabIndex(tab, i)" + [attr.tabIndex]="_getTabIndex(i)" [attr.aria-posinset]="i + 1" [attr.aria-setsize]="_tabs.length" [attr.aria-controls]="_getTabContentId(i)" diff --git a/src/angular/tabs/tab-group.ts b/src/angular/tabs/tab-group.ts index b589a73325..db10a85f5a 100644 --- a/src/angular/tabs/tab-group.ts +++ b/src/angular/tabs/tab-group.ts @@ -403,16 +403,15 @@ export abstract class SbbTabGroupBase implements AfterContentInit, AfterContentC /** Handle click events, setting new selected index if appropriate. */ _handleClick(tab: SbbTab, tabHeader: SbbTabGroupBaseHeader, index: number) { + tabHeader.focusIndex = index; + if (!tab.disabled) { - this.selectedIndex = tabHeader.focusIndex = index; + this.selectedIndex = index; } } /** Retrieves the tabindex for the tab. */ - _getTabIndex(tab: SbbTab, index: number): number | null { - if (tab.disabled) { - return null; - } + _getTabIndex(index: number): number | null { const targetIndex = this._lastFocusedTabIndex ?? this.selectedIndex; return index === targetIndex ? 0 : -1; } diff --git a/src/angular/tabs/tab-header.spec.ts b/src/angular/tabs/tab-header.spec.ts index 2ed47e3d42..25e071aa66 100644 --- a/src/angular/tabs/tab-header.spec.ts +++ b/src/angular/tabs/tab-header.spec.ts @@ -77,7 +77,7 @@ describe('SbbTabHeader', () => { expect(appComponent.tabHeader.focusIndex).toBe(2); }); - it('should not set focus a disabled tab', () => { + it('should be able to focus a disabled tab', () => { appComponent.tabHeader.focusIndex = 0; fixture.detectChanges(); expect(appComponent.tabHeader.focusIndex).toBe(0); @@ -85,10 +85,11 @@ describe('SbbTabHeader', () => { // Set focus on the disabled tab, but focus should remain 0 appComponent.tabHeader.focusIndex = appComponent.disabledTabIndex; fixture.detectChanges(); - expect(appComponent.tabHeader.focusIndex).toBe(0); + + expect(appComponent.tabHeader.focusIndex).toBe(appComponent.disabledTabIndex); }); - it('should move focus right and skip disabled tabs', () => { + it('should move focus right including over disabled tabs', () => { appComponent.tabHeader.focusIndex = 0; fixture.detectChanges(); expect(appComponent.tabHeader.focusIndex).toBe(0); @@ -97,12 +98,11 @@ describe('SbbTabHeader', () => { expect(appComponent.disabledTabIndex).toBe(1); dispatchKeyboardEvent(tabListContainer, 'keydown', RIGHT_ARROW); fixture.detectChanges(); - expect(appComponent.tabHeader.focusIndex).toBe(2); + expect(appComponent.tabHeader.focusIndex).toBe(1); - // Move focus right to index 3 dispatchKeyboardEvent(tabListContainer, 'keydown', RIGHT_ARROW); fixture.detectChanges(); - expect(appComponent.tabHeader.focusIndex).toBe(3); + expect(appComponent.tabHeader.focusIndex).toBe(2); }); it('should move focus left and skip disabled tabs', () => { @@ -115,11 +115,10 @@ describe('SbbTabHeader', () => { fixture.detectChanges(); expect(appComponent.tabHeader.focusIndex).toBe(2); - // Move focus left, verify that the disabled tab is 1 and should be skipped expect(appComponent.disabledTabIndex).toBe(1); dispatchKeyboardEvent(tabListContainer, 'keydown', LEFT_ARROW); fixture.detectChanges(); - expect(appComponent.tabHeader.focusIndex).toBe(0); + expect(appComponent.tabHeader.focusIndex).toBe(1); }); it('should support key down events to move and select focus', () => { @@ -127,19 +126,36 @@ describe('SbbTabHeader', () => { fixture.detectChanges(); expect(appComponent.tabHeader.focusIndex).toBe(0); + // Move focus right to 1 + dispatchKeyboardEvent(tabListContainer, 'keydown', RIGHT_ARROW); + fixture.detectChanges(); + expect(appComponent.tabHeader.focusIndex).toBe(1); + + // Try to select 1. Should not work since it's disabled. + expect(appComponent.selectedIndex).toBe(0); + const firstEnterEvent = dispatchKeyboardEvent(tabListContainer, 'keydown', ENTER); + fixture.detectChanges(); + expect(appComponent.selectedIndex).toBe(0); + expect(firstEnterEvent.defaultPrevented).toBe(false); + // Move focus right to 2 dispatchKeyboardEvent(tabListContainer, 'keydown', RIGHT_ARROW); fixture.detectChanges(); expect(appComponent.tabHeader.focusIndex).toBe(2); - // Select the focused index 2 + // Select 2 which is enabled. expect(appComponent.selectedIndex).toBe(0); - const enterEvent = dispatchKeyboardEvent(tabListContainer, 'keydown', ENTER); + const secondEnterEvent = dispatchKeyboardEvent(tabListContainer, 'keydown', ENTER); fixture.detectChanges(); expect(appComponent.selectedIndex).toBe(2); - expect(enterEvent.defaultPrevented).toBe(true); + expect(secondEnterEvent.defaultPrevented).toBe(true); + + // Move focus left to 1 + dispatchKeyboardEvent(tabListContainer, 'keydown', LEFT_ARROW); + fixture.detectChanges(); + expect(appComponent.tabHeader.focusIndex).toBe(1); - // Move focus right to 0 + // Move again to 0 dispatchKeyboardEvent(tabListContainer, 'keydown', LEFT_ARROW); fixture.detectChanges(); expect(appComponent.tabHeader.focusIndex).toBe(0); @@ -177,7 +193,7 @@ describe('SbbTabHeader', () => { expect(event.defaultPrevented).toBe(true); }); - it('should skip disabled items when moving focus using HOME', () => { + it('should focus disabled items when moving focus using HOME', () => { appComponent.tabHeader.focusIndex = 3; appComponent.tabs[0].disabled = true; fixture.detectChanges(); @@ -186,8 +202,7 @@ describe('SbbTabHeader', () => { dispatchKeyboardEvent(tabListContainer, 'keydown', HOME); fixture.detectChanges(); - // Note that the second tab is disabled by default already. - expect(appComponent.tabHeader.focusIndex).toBe(2); + expect(appComponent.tabHeader.focusIndex).toBe(0); }); it('should move focus to the last tab when pressing END', () => { @@ -202,7 +217,7 @@ describe('SbbTabHeader', () => { expect(event.defaultPrevented).toBe(true); }); - it('should skip disabled items when moving focus using END', () => { + it('should focus disabled items when moving focus using END', () => { appComponent.tabHeader.focusIndex = 0; appComponent.tabs[3].disabled = true; fixture.detectChanges(); @@ -211,7 +226,7 @@ describe('SbbTabHeader', () => { dispatchKeyboardEvent(tabListContainer, 'keydown', END); fixture.detectChanges(); - expect(appComponent.tabHeader.focusIndex).toBe(2); + expect(appComponent.tabHeader.focusIndex).toBe(3); }); it('should not do anything if a modifier key is pressed', () => {