Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions src/angular/tabs/paginated-tab-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,9 @@ export abstract class SbbPaginatedTabHeader
this._keyManager = new FocusKeyManager<SbbPaginatedTabHeaderItem>(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);

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/angular/tabs/tab-group.html
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand Down
9 changes: 4 additions & 5 deletions src/angular/tabs/tab-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
49 changes: 32 additions & 17 deletions src/angular/tabs/tab-header.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,18 +77,19 @@ 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);

// 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);
Expand All @@ -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', () => {
Expand All @@ -115,31 +115,47 @@ 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', () => {
appComponent.tabHeader.focusIndex = 0;
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);
Expand Down Expand Up @@ -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();
Expand All @@ -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', () => {
Expand All @@ -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();
Expand All @@ -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', () => {
Expand Down