UNPKG

@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
// 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