@zendeskgarden/container-focusjail
Version:
Containers relating to focusjail in the Garden Design System
140 lines (132 loc) • 4.02 kB
JavaScript
/**
* 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.
*/
;
var React = require('react');
var containerUtilities = require('@zendeskgarden/container-utilities');
var tabbable = require('tabbable');
var PropTypes = require('prop-types');
function ownerDocument(node) {
return node && node.ownerDocument || document;
}
function activeElement(doc) {
if (doc === void 0) {
doc = ownerDocument();
}
try {
var active = doc.activeElement;
if (!active || !active.nodeName) return null;
return active;
} catch (e) {
return doc.body;
}
}
const useFocusJail = ({
focusOnMount = true,
restoreFocus = true,
environment,
focusElem,
containerRef
} = {
containerRef: React.createRef()
}) => {
const restoreFocusElement = React.useRef(null);
const [currentRef, setCurrentRef] = React.useState(containerRef.current);
React.useEffect(() => {
if (containerRef.current !== currentRef) {
setCurrentRef(containerRef.current);
}
});
const focusElement = React.useCallback(element => {
const htmlElement = element;
if (focusElem) {
focusElem(htmlElement);
} else {
htmlElement && htmlElement.focus();
}
}, [focusElem]);
const validateContainerRef = () => {
if (!currentRef) {
throw new Error('Accessibility Error: You must apply the ref prop to your containing element.');
}
};
const getInitialFocusNode = () => {
const doc = environment ? environment : document;
const activeElem = activeElement(doc);
const containerElem = currentRef;
return containerElem.contains(activeElem) ? activeElem : containerElem;
};
const getTabbableNodes = () => {
const elements = tabbable.tabbable(currentRef);
return {
firstItem: elements[0] || getInitialFocusNode(),
lastItem: elements[elements.length - 1] || getInitialFocusNode()
};
};
const getContainerProps = ({
onKeyDown,
...other
} = {}) => {
return {
onKeyDown: containerUtilities.composeEventHandlers(onKeyDown, event => {
if (event.key !== containerUtilities.KEYS.TAB) {
return;
}
validateContainerRef();
const tabbableNodes = getTabbableNodes();
if (event.shiftKey && (event.target === tabbableNodes.firstItem || event.target === currentRef)) {
focusElement(tabbableNodes.lastItem);
event.preventDefault();
}
if (!event.shiftKey && event.target === tabbableNodes.lastItem) {
focusElement(tabbableNodes.firstItem);
event.preventDefault();
}
}),
'data-garden-container-id': 'containers.focusjail',
'data-garden-container-version': '2.0.22',
...other
};
};
React.useEffect(() => {
const doc = environment || document;
restoreFocusElement.current = activeElement(doc);
if (focusOnMount) {
focusElement(currentRef);
}
return () => {
const isBodyInactive = restoreFocusElement.current !== doc.body;
const hasActiveElement = restoreFocusElement.current !== null;
if (isBodyInactive && hasActiveElement && restoreFocus) {
focusElement(restoreFocusElement.current);
}
};
}, [focusOnMount, restoreFocus, environment, focusElement, currentRef]);
return {
getContainerProps,
focusElement
};
};
const FocusJailContainer = ({
children,
render = children,
...options
}) => React.createElement(React.Fragment, null, render(useFocusJail(options)));
FocusJailContainer.propTypes = {
children: PropTypes.func,
render: PropTypes.func,
focusOnMount: PropTypes.bool,
restoreFocus: PropTypes.bool,
environment: PropTypes.any,
containerRef: PropTypes.any.isRequired,
focusElem: PropTypes.func
};
FocusJailContainer.defaultProps = {
focusOnMount: true,
restoreFocus: true
};
exports.FocusJailContainer = FocusJailContainer;
exports.useFocusJail = useFocusJail;