Skip to content
This repository has been archived by the owner on Mar 8, 2023. It is now read-only.

fix(tabs): basic keyboard navigation #992

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
3 changes: 1 addition & 2 deletions src/components/content-switcher/content-switcher-item.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* @license
*
* Copyright IBM Corp. 2019, 2021
* Copyright IBM Corp. 2019, 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 @@ -88,7 +88,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
7 changes: 6 additions & 1 deletion 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,11 @@ 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',
}

/**
Expand Down
16 changes: 7 additions & 9 deletions src/components/tabs/tab.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* @license
*
* Copyright IBM Corp. 2019, 2020
* Copyright IBM Corp. 2019, 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 @@ -29,13 +29,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 @@ -53,7 +46,12 @@ class BXTab extends BXContentSwitcherItem {
const { disabled, selected } = this;
// No `href`/`tabindex` to not to make this `<a>` click-focusable
return html`
<a class="${prefix}--tabs__nav-link" role="tab" ?disabled="${disabled}" aria-selected="${Boolean(selected)}">
<a
class="${prefix}--tabs__nav-link"
role="tab"
tabindex="${disabled ? -1 : 0}"
?disabled="${disabled}"
aria-selected="${Boolean(selected)}">
<slot></slot>
</a>
`;
Expand Down
16 changes: 1 addition & 15 deletions src/components/tabs/tabs.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright IBM Corp. 2019, 2021
// Copyright IBM Corp. 2019, 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 @@ -206,20 +206,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
27 changes: 9 additions & 18 deletions src/components/tabs/tabs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* @license
*
* Copyright IBM Corp. 2019, 2021
* Copyright IBM Corp. 2019, 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 @@ -54,18 +54,6 @@ class BXTabs extends HostListenerMixin(BXContentSwitcher) {
@query('#trigger')
private _triggerNode!: HTMLDivElement;

/**
* Handles `focus` event handler on this element.
*/
@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;
});
}

/**
* Handles `blur` event handler on this element.
* @param event The event.
Expand All @@ -74,10 +62,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 @@ -170,8 +154,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 });
if (!open && narrowMode) {
Expand All @@ -193,6 +178,9 @@ 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.NAVIGATING:
{
const direction = narrowMode ? NAVIGATION_DIRECTION_NARROW[key] : NAVIGATION_DIRECTION[key];
Expand Down Expand Up @@ -379,6 +367,9 @@ class BXTabs extends HostListenerMixin(BXContentSwitcher) {
if (key === 'Escape') {
return TABS_KEYBOARD_ACTION.CLOSING;
}
if (key === 'Enter') {
return TABS_KEYBOARD_ACTION.SELECTING;
}
if (key in (narrowMode ? NAVIGATION_DIRECTION_NARROW : NAVIGATION_DIRECTION)) {
return TABS_KEYBOARD_ACTION.NAVIGATING;
}
Expand Down
28 changes: 1 addition & 27 deletions tests/spec/tabs_spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
/**
* @license
*
* Copyright IBM Corp. 2019, 2021
* Copyright IBM Corp. 2019, 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.
*/

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 @@ -132,31 +131,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