@fluentui/react
Version:
Reusable React components for building web experiences.
144 lines • 7.82 kB
JavaScript
import { __assign } from "tslib";
import * as React from 'react';
import { KeyCodes, divProperties, doesElementContainFocus, getDocument, getNativeProps, getWindow, } from '../../Utilities';
import { useMergedRefs, useAsync, useOnEvent } from '@fluentui/react-hooks';
import { useWindow } from '@fluentui/react-window-provider';
function useScrollbarAsync(props, root) {
var async = useAsync();
var _a = React.useState(false), needsVerticalScrollBarState = _a[0], setNeedsVerticalScrollBar = _a[1];
React.useEffect(function () {
async.requestAnimationFrame(function () {
var _a;
// If overflowY is overridden, don't waste time calculating whether the scrollbar is necessary.
if (props.style && props.style.overflowY) {
return;
}
var needsVerticalScrollBar = false;
if (root && root.current && ((_a = root.current) === null || _a === void 0 ? void 0 : _a.firstElementChild)) {
// ClientHeight returns the client height of an element rounded to an
// integer. On some browsers at different zoom levels this rounding
// can generate different results for the root container and child even
// though they are the same height. This causes us to show a scroll bar
// when not needed. Ideally we would use BoundingClientRect().height
// instead however seems that the API is 90% slower than using ClientHeight.
// Therefore instead we will calculate the difference between heights and
// allow for a 1px difference to still be considered ok and not show the
// scroll bar.
var rootHeight = root.current.clientHeight;
var firstChildHeight = root.current.firstElementChild.clientHeight;
if (rootHeight > 0 && firstChildHeight > rootHeight) {
needsVerticalScrollBar = firstChildHeight - rootHeight > 1;
}
}
if (needsVerticalScrollBarState !== needsVerticalScrollBar) {
setNeedsVerticalScrollBar(needsVerticalScrollBar);
}
});
return function () { return async.dispose(); };
});
return needsVerticalScrollBarState;
}
function defaultFocusRestorer(options) {
var originalElement = options.originalElement, containsFocus = options.containsFocus;
if (originalElement && containsFocus && originalElement !== getWindow()) {
// Make sure that the focus method actually exists
// In some cases the object might exist but not be a real element.
// This is primarily for IE 11 and should be removed once IE 11 is no longer in use.
// This is wrapped in a setTimeout because of a React 16 bug that is resolved in 17.
// Once we move to 17, the setTimeout should be removed (ref: https://github.com/facebook/react/issues/17894#issuecomment-656094405)
setTimeout(function () {
var _a;
(_a = originalElement.focus) === null || _a === void 0 ? void 0 : _a.call(originalElement);
}, 0);
}
}
function useRestoreFocus(props, root) {
var _a = props.onRestoreFocus, onRestoreFocus = _a === void 0 ? defaultFocusRestorer : _a;
var originalFocusedElement = React.useRef();
var containsFocus = React.useRef(false);
React.useEffect(function () {
originalFocusedElement.current = getDocument().activeElement;
if (doesElementContainFocus(root.current)) {
containsFocus.current = true;
}
return function () {
var _a;
onRestoreFocus === null || onRestoreFocus === void 0 ? void 0 : onRestoreFocus({
originalElement: originalFocusedElement.current,
containsFocus: containsFocus.current,
documentContainsFocus: ((_a = getDocument()) === null || _a === void 0 ? void 0 : _a.hasFocus()) || false,
});
// De-reference DOM Node to avoid retainment via transpiled closure of _onKeyDown
originalFocusedElement.current = undefined;
};
// eslint-disable-next-line react-hooks/exhaustive-deps -- should only run on first render
}, []);
useOnEvent(root, 'focus', React.useCallback(function () {
containsFocus.current = true;
}, []), true);
useOnEvent(root, 'blur', React.useCallback(function (ev) {
/** The popup should update this._containsFocus when:
* relatedTarget exists AND
* the relatedTarget is not contained within the popup.
* If the relatedTarget is within the popup, that means the popup still has focus
* and focused moved from one element to another within the popup.
* If relatedTarget is undefined or null that usually means that a
* keyboard event occurred and focus didn't change
*/
if (root.current && ev.relatedTarget && !root.current.contains(ev.relatedTarget)) {
containsFocus.current = false;
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- should only run on first render
}, []), true);
}
function useHideSiblingNodes(props) {
var isModalOrPanel = props['aria-modal'];
React.useEffect(function () {
var targetDocument = getDocument();
if (isModalOrPanel && targetDocument) {
var children = targetDocument.body.children;
var nodesToHide_1 = [];
for (var i = 0; i < children.length - 1; i++) {
nodesToHide_1.push(children[i]);
}
nodesToHide_1 = nodesToHide_1.filter(function (child) {
return child.tagName !== 'TEMPLATE' &&
child.tagName !== 'SCRIPT' &&
child.tagName !== 'STYLE' &&
!child.hasAttribute('aria-hidden');
});
nodesToHide_1.forEach(function (node) { return node.setAttribute('aria-hidden', 'true'); });
return function () { return nodesToHide_1.forEach(function (child) { return child.removeAttribute('aria-hidden'); }); };
}
}, [isModalOrPanel]);
}
/**
* This adds accessibility to Dialog and Panel controls
*/
export var Popup = React.forwardRef(function (props, forwardedRef) {
// Default props
// eslint-disable-next-line deprecation/deprecation
props = __assign({ shouldRestoreFocus: true }, props);
var root = React.useRef();
var mergedRootRef = useMergedRefs(root, forwardedRef);
useHideSiblingNodes(props);
useRestoreFocus(props, root);
var role = props.role, className = props.className, ariaLabel = props.ariaLabel, ariaLabelledBy = props.ariaLabelledBy, ariaDescribedBy = props.ariaDescribedBy, style = props.style, children = props.children, onDismiss = props.onDismiss;
var needsVerticalScrollBar = useScrollbarAsync(props, root);
var onKeyDown = React.useCallback(function (ev) {
// eslint-disable-next-line deprecation/deprecation
switch (ev.which) {
case KeyCodes.escape:
if (onDismiss) {
onDismiss(ev);
ev.preventDefault();
ev.stopPropagation();
}
break;
}
}, [onDismiss]);
var win = useWindow();
useOnEvent(win, 'keydown', onKeyDown);
return (React.createElement("div", __assign({ ref: mergedRootRef }, getNativeProps(props, divProperties), { className: className, role: role, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, onKeyDown: onKeyDown, style: __assign({ overflowY: needsVerticalScrollBar ? 'scroll' : undefined, outline: 'none' }, style) }), children));
});
//# sourceMappingURL=Popup.js.map