UNPKG

@zendeskgarden/container-focusvisible

Version:

Containers relating to the :focus-visible polyfill hook in the Garden Design System

210 lines (204 loc) 7.27 kB
/** * Copyright Zendesk, Inc. * * Use of this source code is governed under the Apache License, Version 2.0 * found at http://www.apache.org/licenses/LICENSE-2.0. */ 'use strict'; var React = require('react'); var PropTypes = require('prop-types'); const INPUT_TYPES_WHITE_LIST = { text: true, search: true, url: true, tel: true, email: true, password: true, number: true, date: true, month: true, week: true, time: true, datetime: true, 'datetime-local': true }; function useFocusVisible(_temp) { let { scope, relativeDocument, className = 'garden-focus-visible', dataAttribute = 'data-garden-focus-visible' } = _temp === void 0 ? {} : _temp; if (!scope) { throw new Error('Error: the useFocusVisible() hook requires a "scope" property'); } const hadKeyboardEvent = React.useRef(false); const hadFocusVisibleRecently = React.useRef(false); const hadFocusVisibleRecentlyTimeout = React.useRef(); React.useEffect(() => { let environment = relativeDocument; if (!environment) { environment = document; } const isValidFocusTarget = el => { if (el && el !== scope.current && el.nodeName !== 'HTML' && el.nodeName !== 'BODY' && 'classList' in el && 'contains' in el.classList) { return true; } return false; }; const focusTriggersKeyboardModality = el => { const type = el.type; const tagName = el.tagName; if (tagName === 'INPUT' && INPUT_TYPES_WHITE_LIST[type] && !el.readOnly) { return true; } if (tagName === 'TEXTAREA' && !el.readOnly) { return true; } if (el.isContentEditable) { return true; } return false; }; const isFocused = el => { if (el && (el.classList.contains(className) || el.hasAttribute(dataAttribute))) { return true; } return false; }; const addFocusVisibleClass = el => { if (isFocused(el)) { return; } el && el.classList.add(className); el && el.setAttribute(dataAttribute, 'true'); }; const removeFocusVisibleClass = el => { el.classList.remove(className); el.removeAttribute(dataAttribute); }; const onKeyDown = e => { if (e.metaKey || e.altKey || e.ctrlKey) { return; } if (isValidFocusTarget(environment.activeElement)) { addFocusVisibleClass(environment.activeElement); } hadKeyboardEvent.current = true; }; const onPointerDown = () => { hadKeyboardEvent.current = false; }; const onFocus = e => { if (!isValidFocusTarget(e.target)) { return; } if (hadKeyboardEvent.current || focusTriggersKeyboardModality(e.target)) { addFocusVisibleClass(e.target); } }; const onBlur = e => { if (!isValidFocusTarget(e.target)) { return; } if (isFocused(e.target)) { hadFocusVisibleRecently.current = true; clearTimeout(hadFocusVisibleRecentlyTimeout.current); const timeoutId = setTimeout(() => { hadFocusVisibleRecently.current = false; clearTimeout(hadFocusVisibleRecentlyTimeout.current); }, 100); hadFocusVisibleRecentlyTimeout.current = Number(timeoutId); removeFocusVisibleClass(e.target); } }; const onInitialPointerMove = e => { const nodeName = e.target.nodeName; if (nodeName && nodeName.toLowerCase() === 'html') { return; } hadKeyboardEvent.current = false; removeInitialPointerMoveListeners(); }; const addInitialPointerMoveListeners = () => { environment.addEventListener('mousemove', onInitialPointerMove); environment.addEventListener('mousedown', onInitialPointerMove); environment.addEventListener('mouseup', onInitialPointerMove); environment.addEventListener('pointermove', onInitialPointerMove); environment.addEventListener('pointerdown', onInitialPointerMove); environment.addEventListener('pointerup', onInitialPointerMove); environment.addEventListener('touchmove', onInitialPointerMove); environment.addEventListener('touchstart', onInitialPointerMove); environment.addEventListener('touchend', onInitialPointerMove); }; const removeInitialPointerMoveListeners = () => { environment.removeEventListener('mousemove', onInitialPointerMove); environment.removeEventListener('mousedown', onInitialPointerMove); environment.removeEventListener('mouseup', onInitialPointerMove); environment.removeEventListener('pointermove', onInitialPointerMove); environment.removeEventListener('pointerdown', onInitialPointerMove); environment.removeEventListener('pointerup', onInitialPointerMove); environment.removeEventListener('touchmove', onInitialPointerMove); environment.removeEventListener('touchstart', onInitialPointerMove); environment.removeEventListener('touchend', onInitialPointerMove); }; const onVisibilityChange = () => { if (environment.visibilityState === 'hidden') { if (hadFocusVisibleRecently.current) { hadKeyboardEvent.current = true; } } }; const currentScope = scope.current; if (!environment || !currentScope) { return; } environment.addEventListener('keydown', onKeyDown, true); environment.addEventListener('mousedown', onPointerDown, true); environment.addEventListener('pointerdown', onPointerDown, true); environment.addEventListener('touchstart', onPointerDown, true); environment.addEventListener('visibilitychange', onVisibilityChange, true); addInitialPointerMoveListeners(); currentScope && currentScope.addEventListener('focus', onFocus, true); currentScope && currentScope.addEventListener('blur', onBlur, true); return () => { environment.removeEventListener('keydown', onKeyDown); environment.removeEventListener('mousedown', onPointerDown); environment.removeEventListener('pointerdown', onPointerDown); environment.removeEventListener('touchstart', onPointerDown); environment.removeEventListener('visibilityChange', onVisibilityChange); removeInitialPointerMoveListeners(); currentScope && currentScope.removeEventListener('focus', onFocus); currentScope && currentScope.removeEventListener('blur', onBlur); clearTimeout(hadFocusVisibleRecentlyTimeout.current); }; }, [relativeDocument, scope, className, dataAttribute]); } const FocusVisibleContainer = _ref => { let { children, render = children, ...options } = _ref; const scopeRef = React.useRef(null); useFocusVisible({ scope: scopeRef, ...options }); return React.createElement(React.Fragment, null, render({ ref: scopeRef })); }; FocusVisibleContainer.defaultProps = { className: 'garden-focus-visible', dataAttribute: 'data-garden-focus-visible' }; FocusVisibleContainer.propTypes = { children: PropTypes.func, render: PropTypes.func, relativeDocument: PropTypes.object, className: PropTypes.string, dataAttribute: PropTypes.string }; exports.FocusVisibleContainer = FocusVisibleContainer; exports.useFocusVisible = useFocusVisible;