mirror of
https://github.com/owncast/owncast.git
synced 2024-10-10 19:16:02 +00:00
![dependabot[bot]](/assets/img/avatar_default.png)
* Commit updated Javascript packages * Bump preact from 10.5.4 to 10.5.5 in /build/javascript (#265) * Trying a new github workflow to install javascript packages * Bump tailwindcss from 1.9.2 to 1.9.4 in /build/javascript (#266) Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss) from 1.9.2 to 1.9.4. - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/master/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/compare/v1.9.2...v1.9.4) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Commit updated Javascript packages * Bump preact from 10.5.4 to 10.5.5 in /build/javascript Bumps [preact](https://github.com/preactjs/preact) from 10.5.4 to 10.5.5. - [Release notes](https://github.com/preactjs/preact/releases) - [Commits](https://github.com/preactjs/preact/compare/10.5.4...10.5.5) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Gabe Kangas <gabek@real-ity.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Owncast <owncast@owncast.online> * Bump @justinribeiro/lite-youtube in /build/javascript Bumps [@justinribeiro/lite-youtube](https://github.com/justinribeiro/lite-youtube) from 0.9.0 to 0.9.1. - [Release notes](https://github.com/justinribeiro/lite-youtube/releases) - [Commits](https://github.com/justinribeiro/lite-youtube/commits) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Owncast <owncast@owncast.online> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Gabe Kangas <gabek@real-ity.com>
338 lines
8.4 KiB
JavaScript
338 lines
8.4 KiB
JavaScript
var tabbable = require('tabbable');
|
|
var xtend = require('xtend');
|
|
|
|
var activeFocusDelay;
|
|
|
|
var activeFocusTraps = (function() {
|
|
var trapQueue = [];
|
|
return {
|
|
activateTrap: function(trap) {
|
|
if (trapQueue.length > 0) {
|
|
var activeTrap = trapQueue[trapQueue.length - 1];
|
|
if (activeTrap !== trap) {
|
|
activeTrap.pause();
|
|
}
|
|
}
|
|
|
|
var trapIndex = trapQueue.indexOf(trap);
|
|
if (trapIndex === -1) {
|
|
trapQueue.push(trap);
|
|
} else {
|
|
// move this existing trap to the front of the queue
|
|
trapQueue.splice(trapIndex, 1);
|
|
trapQueue.push(trap);
|
|
}
|
|
},
|
|
|
|
deactivateTrap: function(trap) {
|
|
var trapIndex = trapQueue.indexOf(trap);
|
|
if (trapIndex !== -1) {
|
|
trapQueue.splice(trapIndex, 1);
|
|
}
|
|
|
|
if (trapQueue.length > 0) {
|
|
trapQueue[trapQueue.length - 1].unpause();
|
|
}
|
|
}
|
|
};
|
|
})();
|
|
|
|
function focusTrap(element, userOptions) {
|
|
var doc = document;
|
|
var container =
|
|
typeof element === 'string' ? doc.querySelector(element) : element;
|
|
|
|
var config = xtend(
|
|
{
|
|
returnFocusOnDeactivate: true,
|
|
escapeDeactivates: true
|
|
},
|
|
userOptions
|
|
);
|
|
|
|
var state = {
|
|
firstTabbableNode: null,
|
|
lastTabbableNode: null,
|
|
nodeFocusedBeforeActivation: null,
|
|
mostRecentlyFocusedNode: null,
|
|
active: false,
|
|
paused: false
|
|
};
|
|
|
|
var trap = {
|
|
activate: activate,
|
|
deactivate: deactivate,
|
|
pause: pause,
|
|
unpause: unpause
|
|
};
|
|
|
|
return trap;
|
|
|
|
function activate(activateOptions) {
|
|
if (state.active) return;
|
|
|
|
updateTabbableNodes();
|
|
|
|
state.active = true;
|
|
state.paused = false;
|
|
state.nodeFocusedBeforeActivation = doc.activeElement;
|
|
|
|
var onActivate =
|
|
activateOptions && activateOptions.onActivate
|
|
? activateOptions.onActivate
|
|
: config.onActivate;
|
|
if (onActivate) {
|
|
onActivate();
|
|
}
|
|
|
|
addListeners();
|
|
return trap;
|
|
}
|
|
|
|
function deactivate(deactivateOptions) {
|
|
if (!state.active) return;
|
|
|
|
clearTimeout(activeFocusDelay);
|
|
|
|
removeListeners();
|
|
state.active = false;
|
|
state.paused = false;
|
|
|
|
activeFocusTraps.deactivateTrap(trap);
|
|
|
|
var onDeactivate =
|
|
deactivateOptions && deactivateOptions.onDeactivate !== undefined
|
|
? deactivateOptions.onDeactivate
|
|
: config.onDeactivate;
|
|
if (onDeactivate) {
|
|
onDeactivate();
|
|
}
|
|
|
|
var returnFocus =
|
|
deactivateOptions && deactivateOptions.returnFocus !== undefined
|
|
? deactivateOptions.returnFocus
|
|
: config.returnFocusOnDeactivate;
|
|
if (returnFocus) {
|
|
delay(function() {
|
|
tryFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation));
|
|
});
|
|
}
|
|
|
|
return trap;
|
|
}
|
|
|
|
function pause() {
|
|
if (state.paused || !state.active) return;
|
|
state.paused = true;
|
|
removeListeners();
|
|
}
|
|
|
|
function unpause() {
|
|
if (!state.paused || !state.active) return;
|
|
state.paused = false;
|
|
updateTabbableNodes();
|
|
addListeners();
|
|
}
|
|
|
|
function addListeners() {
|
|
if (!state.active) return;
|
|
|
|
// There can be only one listening focus trap at a time
|
|
activeFocusTraps.activateTrap(trap);
|
|
|
|
// Delay ensures that the focused element doesn't capture the event
|
|
// that caused the focus trap activation.
|
|
activeFocusDelay = delay(function() {
|
|
tryFocus(getInitialFocusNode());
|
|
});
|
|
|
|
doc.addEventListener('focusin', checkFocusIn, true);
|
|
doc.addEventListener('mousedown', checkPointerDown, {
|
|
capture: true,
|
|
passive: false
|
|
});
|
|
doc.addEventListener('touchstart', checkPointerDown, {
|
|
capture: true,
|
|
passive: false
|
|
});
|
|
doc.addEventListener('click', checkClick, {
|
|
capture: true,
|
|
passive: false
|
|
});
|
|
doc.addEventListener('keydown', checkKey, {
|
|
capture: true,
|
|
passive: false
|
|
});
|
|
|
|
return trap;
|
|
}
|
|
|
|
function removeListeners() {
|
|
if (!state.active) return;
|
|
|
|
doc.removeEventListener('focusin', checkFocusIn, true);
|
|
doc.removeEventListener('mousedown', checkPointerDown, true);
|
|
doc.removeEventListener('touchstart', checkPointerDown, true);
|
|
doc.removeEventListener('click', checkClick, true);
|
|
doc.removeEventListener('keydown', checkKey, true);
|
|
|
|
return trap;
|
|
}
|
|
|
|
function getNodeForOption(optionName) {
|
|
var optionValue = config[optionName];
|
|
var node = optionValue;
|
|
if (!optionValue) {
|
|
return null;
|
|
}
|
|
if (typeof optionValue === 'string') {
|
|
node = doc.querySelector(optionValue);
|
|
if (!node) {
|
|
throw new Error('`' + optionName + '` refers to no known node');
|
|
}
|
|
}
|
|
if (typeof optionValue === 'function') {
|
|
node = optionValue();
|
|
if (!node) {
|
|
throw new Error('`' + optionName + '` did not return a node');
|
|
}
|
|
}
|
|
return node;
|
|
}
|
|
|
|
function getInitialFocusNode() {
|
|
var node;
|
|
if (getNodeForOption('initialFocus') !== null) {
|
|
node = getNodeForOption('initialFocus');
|
|
} else if (container.contains(doc.activeElement)) {
|
|
node = doc.activeElement;
|
|
} else {
|
|
node = state.firstTabbableNode || getNodeForOption('fallbackFocus');
|
|
}
|
|
|
|
if (!node) {
|
|
throw new Error(
|
|
'Your focus-trap needs to have at least one focusable element'
|
|
);
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
function getReturnFocusNode(previousActiveElement) {
|
|
var node = getNodeForOption('setReturnFocus');
|
|
return node ? node : previousActiveElement;
|
|
}
|
|
|
|
// This needs to be done on mousedown and touchstart instead of click
|
|
// so that it precedes the focus event.
|
|
function checkPointerDown(e) {
|
|
if (container.contains(e.target)) return;
|
|
if (config.clickOutsideDeactivates) {
|
|
deactivate({
|
|
returnFocus: !tabbable.isFocusable(e.target)
|
|
});
|
|
return;
|
|
}
|
|
// This is needed for mobile devices.
|
|
// (If we'll only let `click` events through,
|
|
// then on mobile they will be blocked anyways if `touchstart` is blocked.)
|
|
if (config.allowOutsideClick && config.allowOutsideClick(e)) {
|
|
return;
|
|
}
|
|
e.preventDefault();
|
|
}
|
|
|
|
// In case focus escapes the trap for some strange reason, pull it back in.
|
|
function checkFocusIn(e) {
|
|
// In Firefox when you Tab out of an iframe the Document is briefly focused.
|
|
if (container.contains(e.target) || e.target instanceof Document) {
|
|
return;
|
|
}
|
|
e.stopImmediatePropagation();
|
|
tryFocus(state.mostRecentlyFocusedNode || getInitialFocusNode());
|
|
}
|
|
|
|
function checkKey(e) {
|
|
if (config.escapeDeactivates !== false && isEscapeEvent(e)) {
|
|
e.preventDefault();
|
|
deactivate();
|
|
return;
|
|
}
|
|
if (isTabEvent(e)) {
|
|
checkTab(e);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Hijack Tab events on the first and last focusable nodes of the trap,
|
|
// in order to prevent focus from escaping. If it escapes for even a
|
|
// moment it can end up scrolling the page and causing confusion so we
|
|
// kind of need to capture the action at the keydown phase.
|
|
function checkTab(e) {
|
|
updateTabbableNodes();
|
|
if (e.shiftKey && e.target === state.firstTabbableNode) {
|
|
e.preventDefault();
|
|
tryFocus(state.lastTabbableNode);
|
|
return;
|
|
}
|
|
if (!e.shiftKey && e.target === state.lastTabbableNode) {
|
|
e.preventDefault();
|
|
tryFocus(state.firstTabbableNode);
|
|
return;
|
|
}
|
|
}
|
|
|
|
function checkClick(e) {
|
|
if (config.clickOutsideDeactivates) return;
|
|
if (container.contains(e.target)) return;
|
|
if (config.allowOutsideClick && config.allowOutsideClick(e)) {
|
|
return;
|
|
}
|
|
e.preventDefault();
|
|
e.stopImmediatePropagation();
|
|
}
|
|
|
|
function updateTabbableNodes() {
|
|
var tabbableNodes = tabbable(container);
|
|
state.firstTabbableNode = tabbableNodes[0] || getInitialFocusNode();
|
|
state.lastTabbableNode =
|
|
tabbableNodes[tabbableNodes.length - 1] || getInitialFocusNode();
|
|
}
|
|
|
|
function tryFocus(node) {
|
|
if (node === doc.activeElement) return;
|
|
if (!node || !node.focus) {
|
|
tryFocus(getInitialFocusNode());
|
|
return;
|
|
}
|
|
node.focus();
|
|
state.mostRecentlyFocusedNode = node;
|
|
if (isSelectableInput(node)) {
|
|
node.select();
|
|
}
|
|
}
|
|
}
|
|
|
|
function isSelectableInput(node) {
|
|
return (
|
|
node.tagName &&
|
|
node.tagName.toLowerCase() === 'input' &&
|
|
typeof node.select === 'function'
|
|
);
|
|
}
|
|
|
|
function isEscapeEvent(e) {
|
|
return e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27;
|
|
}
|
|
|
|
function isTabEvent(e) {
|
|
return e.key === 'Tab' || e.keyCode === 9;
|
|
}
|
|
|
|
function delay(fn) {
|
|
return setTimeout(fn, 0);
|
|
}
|
|
|
|
module.exports = focusTrap;
|