@awsui/components-react
Version:
On July 19th, 2022, we launched [Cloudscape Design System](https://cloudscape.design). Cloudscape is an evolution of AWS-UI. It consists of user interface guidelines, front-end components, design resources, and development tools for building intuitive, en
224 lines • 11.5 kB
JavaScript
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useRef } from 'react';
import { useEffect, useMemo } from 'react';
import { useStableCallback } from '@awsui/component-toolkit/internal';
import { SingleTabStopNavigationProvider, } from '@awsui/component-toolkit/internal';
import { getAllFocusables } from '../../internal/components/focus-lock/utils';
import { KeyCode } from '../../internal/keycode';
import handleKey, { isEventLike } from '../../internal/utils/handle-key';
import { nodeBelongs } from '../../internal/utils/node-belongs';
import { findTreeItemByIndex, findTreeItemContentById, getClosestTreeItem, getToggleButtonOfTreeItem, isElementDisabled, isTreeItemToggle, } from './utils';
import treeItemStyles from '../tree-item/styles.css.js';
export function KeyboardNavigationProvider({ getTreeView, children, }) {
const navigationAPI = useRef(null);
const keyboardNavigation = useMemo(() => new KeyboardNavigationProcessor(navigationAPI), []);
const getTreeViewStable = useStableCallback(getTreeView);
// Initialize the processor with the treeView container assuming it is mounted synchronously and only once.
useEffect(() => {
const treeView = getTreeViewStable();
if (treeView) {
keyboardNavigation.init(treeView);
return keyboardNavigation.cleanup;
}
}, [keyboardNavigation, getTreeViewStable]);
// Notify the processor of the new render.
useEffect(() => {
keyboardNavigation.refresh();
});
return (React.createElement(SingleTabStopNavigationProvider, { ref: navigationAPI, getNextFocusTarget: keyboardNavigation.getNextFocusTarget, onUnregisterActive: keyboardNavigation.onUnregisterActive, navigationActive: true }, children));
}
export class KeyboardNavigationProcessor {
constructor(navigationAPI) {
// Props
this._treeView = null;
// State
this.focusedTreeItem = null;
this.cleanup = () => {
// Do nothing before initialized.
};
this.onUnregisterActive = () => {
// If the focused tree-item or tree-item focusable appears to be no longer attached to the tree-view we need to re-apply
// focus to a tree-item focusable with the same position or to the tree-item toggle.
// istanbul ignore next - tested via integration tests
if (this.treeView && this.focusedTreeItem && !nodeBelongs(this.treeView, this.focusedTreeItem.element)) {
const nextFocusableElement = this.getNextFocusableTreeItemContent(this.treeView, this.focusedTreeItem, 0);
if (nextFocusableElement) {
nextFocusableElement === null || nextFocusableElement === void 0 ? void 0 : nextFocusableElement.focus();
}
else {
this.moveFocusBetweenTreeItems(this.treeView, this.focusedTreeItem, 0);
}
}
};
this.getNextFocusTarget = () => {
if (!this.treeView) {
return null;
}
const treeItem = this.focusedTreeItem;
const firstTreeItemToggle = this.treeView.querySelector(`.${treeItemStyles['tree-item-focus-target']}`);
let focusTarget = firstTreeItemToggle;
// Focus on the element that was focused before.
if (treeItem) {
focusTarget = this.getNextFocusableTreeItem(this.treeView, treeItem, 0);
}
return focusTarget;
};
this.onFocusin = (treeView, event) => {
var _a;
if (!(event.target instanceof HTMLElement)) {
return;
}
this.updateFocusedTreeItem(treeView, event.target);
if (!this.focusedTreeItem) {
return;
}
(_a = this._navigationAPI.current) === null || _a === void 0 ? void 0 : _a.updateFocusTarget();
};
this.onKeydown = (treeView, event) => {
const keys = [
KeyCode.up,
KeyCode.down,
KeyCode.left,
KeyCode.right,
KeyCode.pageUp,
KeyCode.pageDown,
KeyCode.home,
KeyCode.end,
];
if (!this.focusedTreeItem || !this.isRegistered(document.activeElement) || keys.indexOf(event.keyCode) === -1) {
return;
}
const from = this.focusedTreeItem;
if (isEventLike(event)) {
handleKey(event, {
onBlockStart: () => this.moveFocusBetweenTreeItems(treeView, from, -1, event),
onBlockEnd: () => this.moveFocusBetweenTreeItems(treeView, from, 1, event),
onInlineEnd: () => {
// If focus is on the toggle, move focus to the first element inside the tree-item
if (isTreeItemToggle(from.element)) {
return this.moveFocusInsideTreeItem(treeView, from, 0, event);
}
return this.moveFocusInsideTreeItem(treeView, from, 1, event);
},
onInlineStart: () => {
// If focus is on the toggle, move focus to the last element inside the tree-item
if (isTreeItemToggle(from.element)) {
return this.moveFocusToTheLastElementInsideTreeItem(treeView, from, event);
}
return this.moveFocusInsideTreeItem(treeView, from, -1, event);
},
onPageUp: () => this.moveFocusBetweenTreeItems(treeView, from, -10, event),
onPageDown: () => this.moveFocusBetweenTreeItems(treeView, from, 10, event),
onHome: () => this.moveFocusBetweenTreeItems(treeView, from, -Infinity, event),
onEnd: () => this.moveFocusBetweenTreeItems(treeView, from, Infinity, event),
});
}
};
this._navigationAPI = navigationAPI;
}
init(treeView) {
this._treeView = treeView;
const controller = new AbortController();
treeView.addEventListener('focusin', event => this.onFocusin(treeView, event), { signal: controller.signal });
treeView.addEventListener('keydown', event => this.onKeydown(treeView, event), { signal: controller.signal });
this.cleanup = () => {
controller.abort();
};
}
refresh() {
// Timeout ensures the newly rendered content elements are registered.
setTimeout(() => {
var _a, _b;
if (this.treeView) {
// Update focused tree-item in case tree-items change.
this.updateFocusedTreeItem(this.treeView, (_a = this.focusedTreeItem) === null || _a === void 0 ? void 0 : _a.element);
(_b = this._navigationAPI.current) === null || _b === void 0 ? void 0 : _b.updateFocusTarget();
}
}, 0);
}
get treeView() {
return this._treeView;
}
getFocusablesFrom(target) {
const isElementRegistered = (element) => { var _a; return (_a = this._navigationAPI.current) === null || _a === void 0 ? void 0 : _a.isRegistered(element); };
return getAllFocusables(target).filter(el => isElementRegistered(el) && !isElementDisabled(el));
}
isRegistered(element) {
var _a, _b;
return !element || ((_b = (_a = this._navigationAPI.current) === null || _a === void 0 ? void 0 : _a.isRegistered(element)) !== null && _b !== void 0 ? _b : false);
}
updateFocusedTreeItem(treeView, focusedElement) {
var _a;
if (!focusedElement) {
return;
}
const treeItem = getClosestTreeItem(focusedElement);
if (!treeItem) {
return;
}
const treeItemContent = findTreeItemContentById(treeView, treeItem.id);
this.focusedTreeItem = {
treeItemId: treeItem.id,
treeItemIndex: parseInt((_a = treeItem.getAttribute('data-awsui-tree-item-index')) !== null && _a !== void 0 ? _a : ''),
element: focusedElement,
elementIndex: treeItemContent ? this.getFocusablesFrom(treeItemContent).indexOf(focusedElement) : 0,
};
}
getNextFocusableTreeItem(treeView, from, by) {
const targetTreeItemIndex = from.treeItemIndex + by;
const targetTreeItem = findTreeItemByIndex(treeView, targetTreeItemIndex, by);
// Return the toggle of the tree-item
return getToggleButtonOfTreeItem(targetTreeItem);
}
moveFocusInsideTreeItem(treeView, from, by, event) {
const nextFocusableElement = this.getNextFocusableTreeItemContent(treeView, from, by);
if (nextFocusableElement) {
// Prevent default only if there are focusables inside
event === null || event === void 0 ? void 0 : event.preventDefault();
nextFocusableElement === null || nextFocusableElement === void 0 ? void 0 : nextFocusableElement.focus();
}
}
moveFocusBetweenTreeItems(treeView, from, by, event) {
event === null || event === void 0 ? void 0 : event.preventDefault();
const isToggleFocused = isTreeItemToggle(from.element);
// If toggle is not focused (focus is inside the tree-item),
// pressing up or down arrow keys should move focus to the toggle
const nextFocusableTreeItem = this.getNextFocusableTreeItem(treeView, from, isToggleFocused ? by : 0);
nextFocusableTreeItem === null || nextFocusableTreeItem === void 0 ? void 0 : nextFocusableTreeItem.focus();
}
moveFocusToTheLastElementInsideTreeItem(treeView, from, event) {
const treeItem = findTreeItemContentById(treeView, from.treeItemId);
if (!treeItem) {
return null;
}
const treeItemFocusables = this.getFocusablesFrom(treeItem);
const focusableElement = treeItemFocusables[treeItemFocusables.length - 1];
if (focusableElement) {
// Prevent default only if there are focusables inside
event === null || event === void 0 ? void 0 : event.preventDefault();
focusableElement === null || focusableElement === void 0 ? void 0 : focusableElement.focus();
}
}
getNextFocusableTreeItemContent(treeView, from, by) {
const treeItem = findTreeItemContentById(treeView, from.treeItemId);
if (!treeItem) {
return null;
}
const treeItemFocusables = this.getFocusablesFrom(treeItem);
const targetElementIndex = isTreeItemToggle(from.element) ? by : from.elementIndex + by;
// Move focus to the tree-item toggle if
// left arrow key is pressed while focused on the first element inside the tree-item, or
// right arrow key is pressed while focused on the last element inside the tree-item
const isTargetToggle = (from.elementIndex === 0 && by < 0) || (targetElementIndex === treeItemFocusables.length && by > 0);
if (isTargetToggle) {
return this.getNextFocusableTreeItem(treeView, from, 0);
}
const isValidIndex = targetElementIndex < treeItemFocusables.length;
if (isValidIndex) {
return treeItemFocusables[targetElementIndex];
}
return null;
}
}
//# sourceMappingURL=index.js.map