Update 'modality' implementation

This commit is contained in:
Nicolas Gallagher
2017-04-13 15:57:58 -07:00
parent 170bab659d
commit cdca9e1e2b
+82 -62
View File
@@ -15,110 +15,130 @@ import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
* 1. a keydown event occurred immediately before a focus event; * 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); * 2. a focus event happened on an element which requires keyboard interaction (e.g., a text field);
* *
* Based on https://github.com/WICG/modality * Based on https://github.com/WICG/focus-ring
*/ */
const modality = () => { const modality = () => {
if (!canUseDOM) { if (!canUseDOM) {
return; return;
} }
/** let styleElement;
* Determine whether the keyboard is required when an element is focused let hadKeyboardEvent = false;
*/ let keyboardThrottleTimeoutID = 0;
const proto = window.Element.prototype; const proto = window.Element.prototype;
const matcher = proto.matches || const matches = proto.matches ||
proto.mozMatchesSelector || proto.mozMatchesSelector ||
proto.msMatchesSelector || proto.msMatchesSelector ||
proto.webkitMatchesSelector; proto.webkitMatchesSelector;
// These elements should always have a focus ring drawn, because they are
// associated with switching to a keyboard modality.
const keyboardModalityWhitelist = [ const keyboardModalityWhitelist = [
'input:not([type])', 'input:not([type])',
'input[type=text]', 'input[type=text]',
'input[type=search]',
'input[type=url]',
'input[type=tel]',
'input[type=email]',
'input[type=password]',
'input[type=number]', 'input[type=number]',
'input[type=date]', 'input[type=date]',
'input[type=month]',
'input[type=week]',
'input[type=time]', 'input[type=time]',
'input[type=datetime]', 'input[type=datetime]',
'input[type=datetime-local]',
'textarea', 'textarea',
'[role=textbox]', '[role=textbox]',
// indicates that a custom element supports the keyboard
'[supports-modality=keyboard]'
].join(','); ].join(',');
/**
* Disable the focus ring by default
*/
const initialize = () => {
// check if the style sheet needs to be created
const id = 'react-native-modality';
styleElement = document.getElementById(id);
if (!styleElement) {
// removes focus styles by default
const style = `<style id="${id}">:focus { outline: none; }</style>`;
document.head.insertAdjacentHTML('afterbegin', style);
styleElement = document.getElementById(id);
}
}
/**
* Computes whether the given element should automatically trigger the
* `focus-ring`.
*/
const focusTriggersKeyboardModality = el => { const focusTriggersKeyboardModality = el => {
if (matcher) { if (matches) {
return matcher.call(el, keyboardModalityWhitelist) && matcher.call(el, ':not([readonly])'); return matches.call(el, keyboardModalityWhitelist) && matches.call(el, ':not([readonly])');
} else { } else {
return false; return false;
} }
}; };
/** /**
* Disable the focus ring by default * Add the focus ring to the focused element
*/ */
const id = 'react-native-modality'; const addFocusRing = () => {
let styleElement = document.getElementById(id); if (styleElement) {
if (!styleElement) { styleElement.disabled = true;
const style = `<style id="${id}">:focus { outline: none; }</style>`; }
document.head.insertAdjacentHTML('afterbegin', style);
styleElement = document.getElementById(id);
} }
const disableFocus = () => { /**
* Remove the focus ring
*/
const removeFocusRing = () => {
if (styleElement) { if (styleElement) {
styleElement.disabled = false; styleElement.disabled = false;
} }
}; };
const enableFocus = () => { /**
if (styleElement) { * On `keydown`, set `hadKeyboardEvent`, to be removed 100ms later if there
styleElement.disabled = true; * are no further keyboard events. The 100ms throttle handles cases where
* focus is redirected programmatically after a keyboard event, such as
* opening a menu or dialog.
*/
const handleKeyDown = (e) => {
hadKeyboardEvent = true;
if (keyboardThrottleTimeoutID !== 0) {
clearTimeout(keyboardThrottleTimeoutID);
}
keyboardThrottleTimeoutID = setTimeout(() => {
hadKeyboardEvent = false;
keyboardThrottleTimeoutID = 0;
}, 100);
};
/**
* Display the focus-ring when the keyboard was used to focus
*/
const handleFocus = (e) => {
if (hadKeyboardEvent || focusTriggersKeyboardModality(e.target)) {
addFocusRing();
} }
}; };
/** /**
* Manage the modality focus state * Remove the focus-ring when the keyboard was used to focus
*/ */
let keyboardTimer; const handleBlur = () => {
let hadKeyboardEvent = false; if (!hadKeyboardEvent) {
removeFocusRing();
}
};
// track when the keyboard is in use if (document.body && document.body.addEventListener) {
document.body.addEventListener( initialize();
'keydown', document.body.addEventListener('keydown', handleKeyDown, true);
() => { document.body.addEventListener('focus', handleFocus, true);
hadKeyboardEvent = true; document.body.addEventListener('blur', handleBlur, true);
if (keyboardTimer) { }
clearTimeout(keyboardTimer);
}
keyboardTimer = setTimeout(
() => {
hadKeyboardEvent = false;
},
100
);
},
true
);
// disable focus style reset when the keyboard is in use
document.body.addEventListener(
'focus',
e => {
if (hadKeyboardEvent || focusTriggersKeyboardModality(e.target)) {
enableFocus();
}
},
true
);
// enable focus style reset when keyboard is no longer in use
document.body.addEventListener(
'blur',
() => {
if (!hadKeyboardEvent) {
disableFocus();
}
},
true
);
}; };
export default modality; export default modality;