UNPKG

@clayui/shared

Version:
253 lines (238 loc) 9.76 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FOCUSABLE_ELEMENTS = void 0; exports.isFocusable = isFocusable; exports.useFocusManagement = useFocusManagement; var _react = _interopRequireDefault(require("react")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } /** * SPDX-FileCopyrightText: © 2019 Liferay, Inc. <https://liferay.com> * SPDX-License-Identifier: BSD-3-Clause */ // https://github.com/facebook/react/blob/master/packages/shared/ReactWorkTags.js#L39 var HostComponent = 5; var minimalTabIndex = 0; function isFocusable(_ref) { var _tagName; var contentEditable = _ref.contentEditable, disabled = _ref.disabled, href = _ref.href, offsetParent = _ref.offsetParent, rel = _ref.rel, tabIndex = _ref.tabIndex, tagName = _ref.tagName, type = _ref.type; // Normalize casing tagName = (_tagName = tagName) === null || _tagName === void 0 ? void 0 : _tagName.toLowerCase(); // Hack to check if element is visible if (!offsetParent) { return false; } if (disabled) { return false; } if (tabIndex != null && tabIndex < minimalTabIndex) { return false; } if (tabIndex != null && tabIndex >= minimalTabIndex || contentEditable === true || contentEditable === 'true') { return true; } if (tagName === 'a' || tagName === 'area') { return !!href && rel !== 'ignore'; } if (tagName === 'input') { return type !== 'file' && type !== 'hidden'; } return tagName === 'button' || tagName === 'embed' || tagName === 'iframe' || tagName === 'object' || tagName === 'select' || tagName === 'textarea'; } var FOCUSABLE_ELEMENTS = exports.FOCUSABLE_ELEMENTS = ['a[href]', '[contenteditable]', '[tabindex]:not([tabindex^="-"])', 'area[href]', 'button:not([disabled])', 'embed', 'iframe', 'input:not([disabled]):not([type="hidden"])', 'object', 'select:not([disabled]):not([aria-hidden])', 'textarea:not([disabled]):not([aria-hidden])']; // A switcher that helps define which fiber to use to navigate, the // component's current fiber or the fiber in progress. var hasSibling = false; function collectDocumentFocusableElements() { return Array.from(document.querySelectorAll(FOCUSABLE_ELEMENTS.join(','))).filter(function (element) { if (isFocusable(element)) { return window.getComputedStyle(element).visibility !== 'hidden'; } return false; }); } // https://github.com/facebook/react/pull/15849#diff-39a673d38713257d5fe7d90aac2acb5aR107 var isFiberHostComponentFocusable = function isFiberHostComponentFocusable(fiber) { if (fiber.tag !== HostComponent) { return false; } var memoizedProps = fiber.memoizedProps, stateNode = fiber.stateNode, type = fiber.type; // The element may be having an update in progress. if (memoizedProps === null) { return false; } return isFocusable({ contentEditable: memoizedProps.contentEditable, disabled: memoizedProps.disabled, href: memoizedProps.href, offsetParent: stateNode.offsetParent, rel: memoizedProps.rel, tabIndex: memoizedProps.tabIndex, tagName: type, type: memoizedProps.type }); }; var collectFocusableElements = function collectFocusableElements(node, focusableElements) { if (isFiberHostComponentFocusable(node)) { focusableElements.push(node.stateNode); } var child = node.child; if (child !== null) { collectFocusableElements(child, focusableElements); } var sibling = node.sibling; if (sibling) { hasSibling = true; collectFocusableElements(sibling, focusableElements); } }; var getFiber = function getFiber(scope) { if (!scope.current) { return null; } var internalKey = Object.keys(scope.current).find(function (key) { return key.indexOf('__reactInternalInstance') === 0 || key.indexOf('__reactFiber') === 0; }); if (internalKey) { return scope.current[internalKey]; } return null; }; var getFocusableElementsInScope = function getFocusableElementsInScope(fiberNode) { var focusableElements = []; var child = fiberNode.child; if (child !== null) { collectFocusableElements(child, focusableElements); } return focusableElements; }; function useFocusManagement(scope) { var nextFocusInDocRef = _react.default.useRef(null); var prevFocusInDocRef = _react.default.useRef(null); var moveFocusInScope = function moveFocusInScope(scope) { var _scope$alternate; var backwards = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; var persistOnScope = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; var fiberFocusElements = getFocusableElementsInScope((_scope$alternate = scope.alternate) !== null && _scope$alternate !== void 0 ? _scope$alternate : scope); // When browsing the alternate/in progress fiber if don't find sibling // elements that might correspond to a React.Portal try searching for // focusable elements using the current fiber. if (!hasSibling) { fiberFocusElements = getFocusableElementsInScope(scope); } else { // Just resets the value for the next focus iteration. hasSibling = false; } if (fiberFocusElements.length === 0) { return null; } var activeElement = document.activeElement; if (!activeElement) { return; } var docFocusElements = collectDocumentFocusableElements(); var docPosition = docFocusElements.indexOf(activeElement); var reactFiberPosition = fiberFocusElements.indexOf(activeElement); var startFocusTrap = fiberFocusElements.find(function (element) { return element.getAttribute('data-focus-scope-start') === 'true'; }); var endFocusTrap = fiberFocusElements.find(function (element) { return element.getAttribute('data-focus-scope-end') === 'true'; }); var nextFocusInDoc = docFocusElements[docPosition + 1]; var prevFocusInDoc = docFocusElements[docPosition - 1]; // Ignore when the active element is not in the scope. if (reactFiberPosition < 0 && !prevFocusInDocRef.current && !nextFocusInDocRef.current && nextFocusInDoc !== endFocusTrap && prevFocusInDoc !== startFocusTrap) { return null; } var nextFocusInFiber = fiberFocusElements[reactFiberPosition + 1]; var prevFocusInFiber = fiberFocusElements[reactFiberPosition - 1]; // If the focus is moving within the focus trap, let the browser handle // navigation and focus order. if (startFocusTrap && endFocusTrap && startFocusTrap !== prevFocusInDoc && endFocusTrap !== nextFocusInDoc) { return null; } // Checks if the focus has reached the end of the scope and should // go back to the beginning. if (endFocusTrap && endFocusTrap === nextFocusInDoc) { nextFocusInFiber = docFocusElements.find(function (_, index, array) { return array[index - 1] === startFocusTrap; }); } // Checks if the focus has arrived at the beginning of the scope and is // returning moves the focus to the end of the scope. if (startFocusTrap && startFocusTrap === prevFocusInDoc) { prevFocusInFiber = docFocusElements.find(function (_, index, array) { return array[index + 1] === endFocusTrap; }); } // Only moves to the next element if it is in scope. if (persistOnScope && (!nextFocusInFiber || backwards && !prevFocusInFiber)) { return null; } // If these two nodes are not equal, that means React is likely using // a portal to render the node in a different part of the DOM. When // this happens, we want to track where the next node is in case we // reach the end of the list of focusable nodes. if (nextFocusInFiber !== nextFocusInDoc) { nextFocusInDocRef.current = nextFocusInDoc; } // Same as above, except we track the previous node for tabbing backwards. if (prevFocusInFiber !== prevFocusInDoc) { prevFocusInDocRef.current = prevFocusInDoc; } var nextActive = backwards ? prevFocusInFiber : nextFocusInFiber; // We track the previous and next elements in the document flow due to React portals. // // Consider the following structure of dom elements and where a React Tree is: // // <HtmlFocusElement1> // <ReactTree> // <HtmlFocusElement2> // // When our focus gets to the end of the React Tree, we want to focus HtmlFocusElement2 // When we are at the beginning of the React Tree and want to go backwards with // SHIFT + TAB, we want to focus HtmlFocusElement1. This allows the React Tree to // render nodes whereever it would like in the document. // // If there is no `nextActive`, that means we are either at the beginning or end of the // list of focusable elements in the React Tree. So we go back to the flow of the // document instead of the flow of the React Tree. if (!nextActive) { nextActive = backwards ? prevFocusInDocRef.current : nextFocusInDocRef.current; } if (nextActive) { nextActive.focus(); if (nextActive === prevFocusInDocRef.current || nextActive === nextFocusInDocRef.current) { nextFocusInDocRef.current = null; prevFocusInDocRef.current = null; } return nextActive; } return null; }; return { focusFirst: function focusFirst() { minimalTabIndex = -1; var next = moveFocusInScope(getFiber(scope), false, true); minimalTabIndex = 0; return next; }, focusNext: function focusNext(persistOnScope) { return moveFocusInScope(getFiber(scope), false, persistOnScope); }, focusPrevious: function focusPrevious(persistOnScope) { return moveFocusInScope(getFiber(scope), true, persistOnScope); } }; }