@zendeskgarden/container-focusjail
Version:
Containers relating to focusjail in the Garden Design System
142 lines (135 loc) • 4.1 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.
*/
import React, { createRef, useRef, useState, useEffect, useCallback } from 'react';
import { composeEventHandlers, KEYS } from '@zendeskgarden/container-utilities';
import { tabbable } from 'tabbable';
import PropTypes from '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 = function (_temp) {
let {
focusOnMount = true,
restoreFocus = true,
environment,
focusElem,
containerRef
} = _temp === void 0 ? {
containerRef: createRef()
} : _temp;
const restoreFocusElement = useRef(null);
const [currentRef, setCurrentRef] = useState(containerRef.current);
useEffect(() => {
if (containerRef.current !== currentRef) {
setCurrentRef(containerRef.current);
}
});
const focusElement = 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(currentRef);
return {
firstItem: elements[0] || getInitialFocusNode(),
lastItem: elements[elements.length - 1] || getInitialFocusNode()
};
};
const getContainerProps = function (_temp2) {
let {
onKeyDown,
...other
} = _temp2 === void 0 ? {} : _temp2;
return {
onKeyDown: composeEventHandlers(onKeyDown, event => {
if (event.key !== 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.19',
...other
};
};
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 = _ref => {
let {
children,
render = children,
...options
} = _ref;
return 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
};
export { FocusJailContainer, useFocusJail };