Skip to content

Commit

Permalink
fix(tabs): basic keyboard navigation (carbon-design-system#9811)
Browse files Browse the repository at this point in the history
### Related Ticket(s)
carbon-design-system/carbon-web-components#970
carbon-design-system/carbon-web-components#707

### Description
This PR ensures that the Tabs and Content Switcher components are able to be work with tab navigation.

### Changelog

**New**

- added tab-index in the tab shadow dom
- added home/end key functionality

**Changed**

- triggering the content switching upon pressing the enter key

**Removed**

- remove unneeded inFocus property in the tab item component

<!-- React and Web Component deploy previews are enabled by default. -->
<!-- To enable additional available deploy previews, apply the following -->
<!-- labels for the corresponding package: -->
<!-- *** "test: e2e": Codesandbox examples and e2e integration tests -->
<!-- *** "package: services": Services -->
<!-- *** "package: utilities": Utilities -->
<!-- *** "RTL": React / Web Components (RTL) -->
<!-- *** "feature flag": React / Web Components (experimental) -->
  • Loading branch information
IgnacioBecerra authored Dec 9, 2022
1 parent 1b6e383 commit 2d0365f
Show file tree
Hide file tree
Showing 6 changed files with 51 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ class BXContentSwitcherItem extends FocusMixin(LitElement) {
role="tab"
class="${className}"
?disabled="${disabled}"
tabindex="${selected ? '0' : '-1'}"
aria-controls="${ifNonNull(target)}"
aria-selected="${Boolean(selected)}">
<span class="${prefix}--content-switcher__label"><slot></slot></span>
Expand Down
17 changes: 16 additions & 1 deletion packages/carbon-web-components/src/components/tabs/defs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* @license
*
* Copyright IBM Corp. 2020, 2021
* Copyright IBM Corp. 2020, 2022
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
Expand Down Expand Up @@ -42,6 +42,21 @@ export enum TABS_KEYBOARD_ACTION {
* Keyboard action to open/close menu on the trigger button or select/deselect a menu item.
*/
TRIGGERING = 'triggering',

/**
* Keyboard action to trigger tab selection using enter key
*/
SELECTING = 'selecting',

/**
* Keyboard action to navigate to first tab using home key
*/
HOME = 'home',

/**
* Keyboard action to navigate to last tab using end key
*/
END = 'end',
}

/**
Expand Down
9 changes: 1 addition & 8 deletions packages/carbon-web-components/src/components/tabs/tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,6 @@ class BXTab extends BXContentSwitcherItem {
@property({ type: Boolean, reflect: true })
highlighted = false;

/**
* `true` if this tab is in a focused `<bx-tabs>`.
*
* @private
*/
@property({ type: Boolean, reflect: true, attribute: 'in-focus' })
inFocus = false;

/**
* Tab type.
*/
Expand All @@ -59,6 +51,7 @@ class BXTab extends BXContentSwitcherItem {
<a
class="${prefix}--tabs__nav-link"
role="tab"
tabindex="${disabled ? -1 : 0}"
?disabled="${disabled}"
aria-selected="${Boolean(selected)}">
<slot></slot>
Expand Down
14 changes: 0 additions & 14 deletions packages/carbon-web-components/src/components/tabs/tabs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -211,20 +211,6 @@
}
}

:host(#{$prefix}-tab[in-focus][selected]),
:host(#{$prefix}-tab[in-focus][selected]:hover) {
@include carbon--breakpoint(md) {
@include focus-outline('outline');

// `[role]` is only for specificity.
// We have `:not()` usage in the corresponding Carbon core style
// which puts specificity of "specific" scenario though the style is for "regular" scenario
a.#{$prefix}--tabs__nav-link[role] {
border-bottom-width: 2px;
}
}
}

:host(#{$prefix}-tab[highlighted][selected]),
:host(#{$prefix}-tab[highlighted][selected]:hover) {
background-color: $selected-ui;
Expand Down
51 changes: 34 additions & 17 deletions packages/carbon-web-components/src/components/tabs/tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,28 +57,26 @@ class BXTabs extends HostListenerMixin(BXContentSwitcher) {
*/
private _open = false;

/**
* The currently selected index
*/
private _currentIndex: number = 0;

/**
* The content of the selected item, used in the narrow mode.
*/
private _selectedItemContent: DocumentFragment | null = null;

/**
* The DOM element for the trigger button in narrow mode.
* Total number of tabs in the component
*/
@query('#trigger')
private _triggerNode!: HTMLDivElement;
private _totalTabs: number = 0;

/**
* Handles `focus` event handler on this element.
* The DOM element for the trigger button in narrow mode.
*/
@HostListener('focusin')
// @ts-ignore: The decorator refers to this method but TS thinks this method is not referred to
private _handleFocusIn() {
const { selectorItem } = this.constructor as typeof BXTabs;
forEach(this.querySelectorAll(selectorItem), (item) => {
(item as BXTab).inFocus = true;
});
}
@query('#trigger')
private _triggerNode!: HTMLDivElement;

/**
* Handles `blur` event handler on this element.
Expand All @@ -89,10 +87,6 @@ class BXTabs extends HostListenerMixin(BXContentSwitcher) {
// @ts-ignore: The decorator refers to this method but TS thinks this method is not referred to
private _handleFocusOut({ relatedTarget }: FocusEvent) {
if (!this.contains(relatedTarget as Node)) {
const { selectorItem } = this.constructor as typeof BXTabs;
forEach(this.querySelectorAll(selectorItem), (item) => {
(item as BXTab).inFocus = false;
});
this._handleUserInitiatedToggle(false);
}
}
Expand Down Expand Up @@ -183,6 +177,7 @@ class BXTabs extends HostListenerMixin(BXContentSwitcher) {
if (nextItemText) {
this._assistiveStatusText = nextItemText;
}
this._currentIndex += direction;
this.requestUpdate();
}

Expand All @@ -202,8 +197,9 @@ class BXTabs extends HostListenerMixin(BXContentSwitcher) {
}

@HostListener('keydown')
protected _handleKeydown({ key }: KeyboardEvent) {
protected _handleKeydown(event: KeyboardEvent) {
const { _open: open, _triggerNode: triggerNode } = this;
const { key, target } = event;
const narrowMode = Boolean(triggerNode.offsetParent);
const action = (this.constructor as typeof BXTabs).getAction(key, {
narrowMode,
Expand All @@ -227,6 +223,17 @@ class BXTabs extends HostListenerMixin(BXContentSwitcher) {
case TABS_KEYBOARD_ACTION.CLOSING:
this._handleUserInitiatedToggle(false);
break;
case TABS_KEYBOARD_ACTION.SELECTING:
this._handleUserInitiatedSelectItem(target as BXTab);
break;
case TABS_KEYBOARD_ACTION.HOME:
this._navigate(this._currentIndex + this._totalTabs, {
immediate: !narrowMode,
});
break;
case TABS_KEYBOARD_ACTION.END:
this._navigate(-this._currentIndex, { immediate: !narrowMode });
break;
case TABS_KEYBOARD_ACTION.NAVIGATING:
{
const direction = narrowMode
Expand Down Expand Up @@ -306,6 +313,7 @@ class BXTabs extends HostListenerMixin(BXContentSwitcher) {
const { selectorItem } = this.constructor as typeof BXTabs;
if (changedProperties.has('type')) {
forEach(this.querySelectorAll(selectorItem), (elem) => {
this._totalTabs++;
(elem as BXTab).type = this.type;
});
}
Expand Down Expand Up @@ -428,6 +436,15 @@ class BXTabs extends HostListenerMixin(BXContentSwitcher) {
if (key === 'Escape') {
return TABS_KEYBOARD_ACTION.CLOSING;
}
if (key === 'Enter') {
return TABS_KEYBOARD_ACTION.SELECTING;
}
if (key === 'Home') {
return TABS_KEYBOARD_ACTION.HOME;
}
if (key === 'End') {
return TABS_KEYBOARD_ACTION.END;
}
if (
key in (narrowMode ? NAVIGATION_DIRECTION_NARROW : NAVIGATION_DIRECTION)
) {
Expand Down
30 changes: 0 additions & 30 deletions packages/carbon-web-components/tests/spec/tabs_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

import { render } from 'lit-html';
import EventManager from '../utils/event-manager';
import { forEach } from '../../src/globals/internal/collection-helpers';
import BXTabs from '../../src/components/tabs/tabs';
import BXTab from '../../src/components/tabs/tab';
import { Default } from '../../src/components/tabs/tabs-story';
Expand Down Expand Up @@ -138,35 +137,6 @@ describe('bx-tabs', function () {
});
});

describe('Focus style', function () {
it('should support setting focus style to child tabs', async function () {
render(template(), document.body);
await Promise.resolve();
const elem = document.body.querySelector('bx-tabs');
const itemNodes = document.body.querySelectorAll('bx-tab');
const event = new CustomEvent('focusin', { bubbles: true });
(elem as HTMLElement).dispatchEvent(event);
expect(
Array.prototype.every.call(itemNodes, (item) => (item as BXTab).inFocus)
).toBe(true);
});

it('should support unsetting focus style to child tabs', async function () {
render(template(), document.body);
await Promise.resolve();
const elem = document.body.querySelector('bx-tabs');
const itemNodes = document.body.querySelectorAll('bx-tab');
forEach(itemNodes, (item) => {
(item as BXTab).inFocus = true;
});
const event = new CustomEvent('focusout', { bubbles: true });
(elem as HTMLElement).dispatchEvent(event);
expect(
Array.prototype.every.call(itemNodes, (item) => (item as BXTab).inFocus)
).toBe(false);
});
});

describe('Keyboard navigation', function () {
it('should support closing narrow mode dropdown by ESC key', async function () {
render(template(), document.body);
Expand Down

0 comments on commit 2d0365f

Please sign in to comment.