/** * Adapts focus styles based on the user's active input modality (i.e., how * they are interacting with the UI right now). * * Focus styles are only relevant when using the keyboard to interact with the * page. If we only show the focus ring when relevant, we can avoid user * confusion without compromising accessibility. * * The script uses two heuristics to determine whether the keyboard is being used: * * 1. a keydown event occurred immediately before a focus event; * 2. a focus event happened on an element which requires keyboard interaction (e.g., a text field); * * This software or document includes material copied from or derived from https://github.com/WICG/focus-visible. * Copyright © 2018 W3C® (MIT, ERCIM, Keio, Beihang). * W3C Software Notice and License: https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document * * */ import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment'; var focusVisibleAttributeName = 'data-focusvisible-polyfill'; var rule = ":focus:not([" + focusVisibleAttributeName + "]){outline: none;}"; var modality = function modality(insertRule) { insertRule(rule); if (!canUseDOM) { return; } var hadKeyboardEvent = true; var hadFocusVisibleRecently = false; var hadFocusVisibleRecentlyTimeout = null; var inputTypesWhitelist = { 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 }; /** * Helper function for legacy browsers and iframes which sometimes focus * elements like document, body, and non-interactive SVG. */ function isValidFocusTarget(el) { if (el && el !== document && el.nodeName !== 'HTML' && el.nodeName !== 'BODY' && 'classList' in el && 'contains' in el.classList) { return true; } return false; } /** * Computes whether the given element should automatically trigger the * `focus-visible` attribute being added, i.e. whether it should always match * `:focus-visible` when focused. */ function focusTriggersKeyboardModality(el) { var type = el.type; var tagName = el.tagName; var isReadOnly = el.readOnly; if (tagName === 'INPUT' && inputTypesWhitelist[type] && !isReadOnly) { return true; } if (tagName === 'TEXTAREA' && !isReadOnly) { return true; } if (el.isContentEditable) { return true; } return false; } /** * Add the `focus-visible` attribute to the given element if it was not added by * the author. */ function addFocusVisibleAttribute(el) { if (el.hasAttribute(focusVisibleAttributeName)) { return; } el.setAttribute(focusVisibleAttributeName, true); } /** * Remove the `focus-visible` attribute from the given element if it was not * originally added by the author. */ function removeFocusVisibleAttribute(el) { el.removeAttribute(focusVisibleAttributeName); } /** * Remove the `focus-visible` attribute from all elements in the document. */ function removeAllFocusVisibleAttributes() { var list = document.querySelectorAll("[" + focusVisibleAttributeName + "]"); for (var i = 0; i < list.length; i += 1) { removeFocusVisibleAttribute(list[i]); } } /** * Treat `keydown` as a signal that the user is in keyboard modality. * Apply `focus-visible` to any current active element and keep track * of our keyboard modality state with `hadKeyboardEvent`. */ function onKeyDown(e) { if (e.key !== 'Tab' && (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)) { return; } if (isValidFocusTarget(document.activeElement)) { addFocusVisibleAttribute(document.activeElement); } hadKeyboardEvent = true; } /** * If at any point a user clicks with a pointing device, ensure that we change * the modality away from keyboard. * This avoids the situation where a user presses a key on an already focused * element, and then clicks on a different element, focusing it with a * pointing device, while we still think we're in keyboard modality. * It also avoids the situation where a user presses on an element within a * previously keyboard-focused element (i.e., `e.target` is not the previously * focused element, but one of its descendants) and we need to remove the * focus ring because a `blur` event doesn't occur. */ function onPointerDown(e) { if (hadKeyboardEvent === true) { removeAllFocusVisibleAttributes(); } hadKeyboardEvent = false; } /** * On `focus`, add the `focus-visible` attribute to the target if: * - the target received focus as a result of keyboard navigation, or * - the event target is an element that will likely require interaction * via the keyboard (e.g. a text box) */ function onFocus(e) { // Prevent IE from focusing the document or HTML element. if (!isValidFocusTarget(e.target)) { return; } if (hadKeyboardEvent || focusTriggersKeyboardModality(e.target)) { addFocusVisibleAttribute(e.target); } } /** * On `blur`, remove the `focus-visible` attribute from the target. */ function onBlur(e) { if (!isValidFocusTarget(e.target)) { return; } if (e.target.hasAttribute(focusVisibleAttributeName)) { // To detect a tab/window switch, we look for a blur event followed // rapidly by a visibility change. // If we don't see a visibility change within 100ms, it's probably a // regular focus change. hadFocusVisibleRecently = true; window.clearTimeout(hadFocusVisibleRecentlyTimeout); hadFocusVisibleRecentlyTimeout = window.setTimeout(function () { hadFocusVisibleRecently = false; window.clearTimeout(hadFocusVisibleRecentlyTimeout); }, 100); removeFocusVisibleAttribute(e.target); } } /** * If the user changes tabs, keep track of whether or not the previously * focused element had the focus-visible attribute. */ function onVisibilityChange(e) { if (document.visibilityState === 'hidden') { // If the tab becomes active again, the browser will handle calling focus // on the element (Safari actually calls it twice). // If this tab change caused a blur on an element with focus-visible, // re-apply the attribute when the user switches back to the tab. if (hadFocusVisibleRecently) { hadKeyboardEvent = true; } addInitialPointerMoveListeners(); } } /** * Add a group of listeners to detect usage of any pointing devices. * These listeners will be added when the polyfill first loads, and anytime * the window is blurred, so that they are active when the window regains * focus. */ function addInitialPointerMoveListeners() { document.addEventListener('mousemove', onInitialPointerMove); document.addEventListener('mousedown', onInitialPointerMove); document.addEventListener('mouseup', onInitialPointerMove); document.addEventListener('pointermove', onInitialPointerMove); document.addEventListener('pointerdown', onInitialPointerMove); document.addEventListener('pointerup', onInitialPointerMove); document.addEventListener('touchmove', onInitialPointerMove); document.addEventListener('touchstart', onInitialPointerMove); document.addEventListener('touchend', onInitialPointerMove); } function removeInitialPointerMoveListeners() { document.removeEventListener('mousemove', onInitialPointerMove); document.removeEventListener('mousedown', onInitialPointerMove); document.removeEventListener('mouseup', onInitialPointerMove); document.removeEventListener('pointermove', onInitialPointerMove); document.removeEventListener('pointerdown', onInitialPointerMove); document.removeEventListener('pointerup', onInitialPointerMove); document.removeEventListener('touchmove', onInitialPointerMove); document.removeEventListener('touchstart', onInitialPointerMove); document.removeEventListener('touchend', onInitialPointerMove); } /** * When the polfyill first loads, assume the user is in keyboard modality. * If any event is received from a pointing device (e.g. mouse, pointer, * touch), turn off keyboard modality. * This accounts for situations where focus enters the page from the URL bar. */ function onInitialPointerMove(e) { // Work around a Safari quirk that fires a mousemove on whenever the // window blurs, even if you're tabbing out of the page. ¯\_(ツ)_/¯ if (e.target.nodeName === 'HTML') { return; } hadKeyboardEvent = false; removeInitialPointerMoveListeners(); } document.addEventListener('keydown', onKeyDown, true); document.addEventListener('mousedown', onPointerDown, true); document.addEventListener('pointerdown', onPointerDown, true); document.addEventListener('touchstart', onPointerDown, true); document.addEventListener('focus', onFocus, true); document.addEventListener('blur', onBlur, true); document.addEventListener('visibilitychange', onVisibilityChange, true); addInitialPointerMoveListeners(); }; export default modality;