UNPKG

@fluentui/react

Version:

Reusable React components for building web experiences.

290 lines 15.5 kB
"use strict"; var _a; Object.defineProperty(exports, "__esModule", { value: true }); exports.PositioningContainer = void 0; exports.useHeightOffset = useHeightOffset; var tslib_1 = require("tslib"); var React = require("react"); var PositioningContainer_styles_1 = require("./PositioningContainer.styles"); var Styling_1 = require("../../../Styling"); var Layer_1 = require("../../../Layer"); // Utilites/Helpers var DirectionalHint_1 = require("../../../common/DirectionalHint"); var Utilities_1 = require("../../../Utilities"); var Positioning_1 = require("../../../Positioning"); var Styling_2 = require("../../../Styling"); var react_hooks_1 = require("@fluentui/react-hooks"); var dom_1 = require("../../../utilities/dom"); var OFF_SCREEN_STYLE = { opacity: 0 }; // In order for some of the max height logic to work properly we need to set the border. // The value is arbitrary. var BORDER_WIDTH = 1; var SLIDE_ANIMATIONS = (_a = {}, _a[Positioning_1.RectangleEdge.top] = 'slideUpIn20', _a[Positioning_1.RectangleEdge.bottom] = 'slideDownIn20', _a[Positioning_1.RectangleEdge.left] = 'slideLeftIn20', _a[Positioning_1.RectangleEdge.right] = 'slideRightIn20', _a); var DEFAULT_PROPS = { preventDismissOnScroll: false, offsetFromTarget: 0, minPagePadding: 8, directionalHint: DirectionalHint_1.DirectionalHint.bottomAutoEdge, }; function useBounds(props, targetWindow) { /** The bounds used when determining if and where the PositioningContainer should be placed. */ var getBounds = function () { var currentBounds = props.bounds; if (!currentBounds) { currentBounds = { top: 0 + props.minPagePadding, left: 0 + props.minPagePadding, right: targetWindow.innerWidth - props.minPagePadding, bottom: targetWindow.innerHeight - props.minPagePadding, width: targetWindow.innerWidth - props.minPagePadding * 2, height: targetWindow.innerHeight - props.minPagePadding * 2, }; } return currentBounds; }; return getBounds; } function usePositionState(props, positionedHost, contentHost, targetRef, getCachedBounds) { var async = (0, react_hooks_1.useAsync)(); var doc = (0, dom_1.useDocumentEx)(); var win = (0, dom_1.useWindowEx)(); /** * Current set of calculated positions for the outermost parent container. */ var _a = React.useState(), positions = _a[0], setPositions = _a[1]; var positionAttempts = React.useRef(0); var updateAsyncPosition = function () { async.requestAnimationFrame(function () { return updatePosition(); }); }; var updatePosition = function () { var offsetFromTarget = props.offsetFromTarget, onPositioned = props.onPositioned; var hostElement = positionedHost.current; var positioningContainerElement = contentHost.current; if (hostElement && positioningContainerElement) { var currentProps = tslib_1.__assign({}, props); currentProps.bounds = getCachedBounds(); currentProps.target = targetRef.current; var target = currentProps.target; if (target) { // Check if the target is an Element or a MouseEvent and the document contains it // or don't check anything else if the target is a Point or Rectangle if ((!target.getBoundingClientRect && !target.preventDefault) || (doc === null || doc === void 0 ? void 0 : doc.body.contains(target))) { currentProps.gapSpace = offsetFromTarget; var newPositions = (0, Positioning_1.positionElement)(currentProps, hostElement, positioningContainerElement, undefined, win); // Set the new position only when the positions are not exists or one of the new positioningContainer // positions are different. The position should not change if the position is within 2 decimal places. if ((!positions && newPositions) || (positions && newPositions && !arePositionsEqual(positions, newPositions) && positionAttempts.current < 5)) { // We should not reposition the positioningContainer more than a few times, if it is then the content is // likely resizing and we should stop trying to reposition to prevent a stack overflow. positionAttempts.current++; setPositions(newPositions); onPositioned === null || onPositioned === void 0 ? void 0 : onPositioned(newPositions); } else { positionAttempts.current = 0; onPositioned === null || onPositioned === void 0 ? void 0 : onPositioned(newPositions); } } else if (positions !== undefined) { setPositions(undefined); } } else if (positions !== undefined) { setPositions(undefined); } } }; React.useEffect(updateAsyncPosition); return [positions, updateAsyncPosition]; } function useSetInitialFocus(_a, contentHost, positions) { var setInitialFocus = _a.setInitialFocus; var didSetInitialFocus = React.useRef(false); React.useEffect(function () { if (!didSetInitialFocus.current && contentHost.current && setInitialFocus && positions) { didSetInitialFocus.current = true; (0, Utilities_1.focusFirstChild)(contentHost.current); } }); } function useMaxHeight(_a, targetRef, getCachedBounds) { var directionalHintFixed = _a.directionalHintFixed, offsetFromTarget = _a.offsetFromTarget, directionalHint = _a.directionalHint, target = _a.target; /** * The maximum height the PositioningContainer can grow to * without going beyond the window or target bounds */ var maxHeight = React.useRef(undefined); var win = (0, dom_1.useWindowEx)(); // If the target element changed, reset the max height. If we are tracking // target with class name, always reset because we do not know if // fabric has rendered a new element and disposed the old element. if (typeof target === 'string') { maxHeight.current = undefined; } React.useEffect(function () { maxHeight.current = undefined; }, [target, offsetFromTarget]); /** * Return the maximum height the container can grow to * without going out of the specified bounds */ var getCachedMaxHeight = function () { if (!maxHeight.current) { if (directionalHintFixed && targetRef.current) { var gapSpace = offsetFromTarget ? offsetFromTarget : 0; maxHeight.current = (0, Positioning_1.getMaxHeight)(targetRef.current, directionalHint, gapSpace, getCachedBounds(), undefined, win); } else { maxHeight.current = getCachedBounds().height - BORDER_WIDTH * 2; } } return maxHeight.current; }; return getCachedMaxHeight; } function useAutoDismissEvents(_a, positionedHost, targetWindow, targetRef, positions, updateAsyncPosition) { var onDismiss = _a.onDismiss, preventDismissOnScroll = _a.preventDismissOnScroll; var async = (0, react_hooks_1.useAsync)(); var onResize = React.useCallback(function (ev) { if (onDismiss) { onDismiss(ev); } else { updateAsyncPosition(); } }, [onDismiss, updateAsyncPosition]); var dismissOnLostFocus = React.useCallback(function (ev) { var target = ev.target; var clickedOutsideCallout = positionedHost.current && !(0, Utilities_1.elementContains)(positionedHost.current, target); if ((!targetRef.current && clickedOutsideCallout) || (ev.target !== targetWindow && clickedOutsideCallout && (targetRef.current.stopPropagation || !targetRef.current || (target !== targetRef.current && !(0, Utilities_1.elementContains)(targetRef.current, target))))) { onResize(ev); } }, [onResize, positionedHost, targetRef, targetWindow]); var dismissOnScroll = React.useCallback(function (ev) { if (positions && !preventDismissOnScroll) { dismissOnLostFocus(ev); } }, [dismissOnLostFocus, positions, preventDismissOnScroll]); React.useEffect(function () { var events = new Utilities_1.EventGroup({}); // This is added so the positioningContainer will dismiss when the window is scrolled // but not when something inside the positioningContainer is scrolled. The delay seems // to be required to avoid React firing an async focus event in IE from // the target changing focus quickly prior to rendering the positioningContainer. async.setTimeout(function () { var _a, _b; events.on(targetWindow, 'scroll', async.throttle(dismissOnScroll, 10), true); events.on(targetWindow, 'resize', async.throttle(onResize, 10), true); events.on((_a = targetWindow === null || targetWindow === void 0 ? void 0 : targetWindow.document) === null || _a === void 0 ? void 0 : _a.body, 'focus', dismissOnLostFocus, true); events.on((_b = targetWindow === null || targetWindow === void 0 ? void 0 : targetWindow.document) === null || _b === void 0 ? void 0 : _b.body, 'click', dismissOnLostFocus, true); }, 0); return function () { return events.dispose(); }; // eslint-disable-next-line react-hooks/exhaustive-deps -- should only run on mount }, [dismissOnScroll]); } function useHeightOffset(_a, contentHost) { var finalHeight = _a.finalHeight; /** * Tracks the current height offset and updates during * the height animation when props.finalHeight is specified. * State stored as object to ensure re-render even if the value does not change. * See https://github.com/microsoft/fluentui/issues/23545 */ var _b = React.useState({ value: 0 }), heightOffset = _b[0], setHeightOffset = _b[1]; var async = (0, react_hooks_1.useAsync)(); var setHeightOffsetTimer = React.useRef(0); /** Animates the height if finalHeight was given. */ var setHeightOffsetEveryFrame = function () { if (contentHost && finalHeight) { setHeightOffsetTimer.current = async.requestAnimationFrame(function () { if (!contentHost.current) { return; } var positioningContainerMainElem = contentHost.current.lastChild; var cardScrollHeight = positioningContainerMainElem.scrollHeight; var cardCurrHeight = positioningContainerMainElem.offsetHeight; var scrollDiff = cardScrollHeight - cardCurrHeight; setHeightOffset({ value: heightOffset.value + scrollDiff }); if (positioningContainerMainElem.offsetHeight < finalHeight) { setHeightOffsetEveryFrame(); } else { async.cancelAnimationFrame(setHeightOffsetTimer.current); } }); } }; // eslint-disable-next-line react-hooks/exhaustive-deps -- should only re-run if finalHeight changes React.useEffect(setHeightOffsetEveryFrame, [finalHeight]); return heightOffset.value; } exports.PositioningContainer = React.forwardRef(function (propsWithoutDefaults, forwardedRef) { var props = (0, Utilities_1.getPropsWithDefaults)(DEFAULT_PROPS, propsWithoutDefaults); // @TODO rename to reflect the name of this class var contentHost = React.useRef(null); /** * The primary positioned div. */ var positionedHost = React.useRef(null); var rootRef = (0, react_hooks_1.useMergedRefs)(forwardedRef, positionedHost); var _a = (0, react_hooks_1.useTarget)(props.target, positionedHost), targetRef = _a[0], targetWindow = _a[1]; var getCachedBounds = useBounds(props, targetWindow); var _b = usePositionState(props, positionedHost, contentHost, targetRef, getCachedBounds), positions = _b[0], updateAsyncPosition = _b[1]; var getCachedMaxHeight = useMaxHeight(props, targetRef, getCachedBounds); var heightOffset = useHeightOffset(props, contentHost); useSetInitialFocus(props, contentHost, positions); useAutoDismissEvents(props, positionedHost, targetWindow, targetRef, positions, updateAsyncPosition); // eslint-disable-next-line react-hooks/exhaustive-deps -- should only run on initial render React.useEffect(function () { var _a; return (_a = props.onLayerMounted) === null || _a === void 0 ? void 0 : _a.call(props); }, []); // If there is no target window then we are likely in server side rendering and we should not render anything. if (!targetWindow) { return null; } var className = props.className, doNotLayer = props.doNotLayer, positioningContainerWidth = props.positioningContainerWidth, positioningContainerMaxHeight = props.positioningContainerMaxHeight, children = props.children; var styles = (0, PositioningContainer_styles_1.getClassNames)(); var directionalClassName = positions && positions.targetEdge ? Styling_2.AnimationClassNames[SLIDE_ANIMATIONS[positions.targetEdge]] : ''; var getContentMaxHeight = getCachedMaxHeight() + heightOffset; var contentMaxHeight = positioningContainerMaxHeight && positioningContainerMaxHeight > getContentMaxHeight ? getContentMaxHeight : positioningContainerMaxHeight; var content = (React.createElement("div", { ref: rootRef, className: (0, Utilities_1.css)('ms-PositioningContainer', styles.container) }, React.createElement("div", { className: (0, Styling_2.mergeStyles)('ms-PositioningContainer-layerHost', styles.root, className, directionalClassName, !!positioningContainerWidth && { width: positioningContainerWidth }, doNotLayer && { zIndex: Styling_1.ZIndexes.Layer }), style: positions ? positions.elementPosition : OFF_SCREEN_STYLE, // Safari and Firefox on Mac OS requires this to back-stop click events so focus remains in the Callout. // See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus tabIndex: -1, ref: contentHost }, children, // @TODO apply to the content container contentMaxHeight))); return doNotLayer ? content : React.createElement(Layer_1.Layer, tslib_1.__assign({}, props.layerProps), content); }); exports.PositioningContainer.displayName = 'PositioningContainer'; function arePositionsEqual(positions, newPosition) { return comparePositions(positions.elementPosition, newPosition.elementPosition); } function comparePositions(oldPositions, newPositions) { for (var key in newPositions) { if (newPositions.hasOwnProperty(key)) { var oldPositionEdge = oldPositions[key]; var newPositionEdge = newPositions[key]; if (oldPositionEdge && newPositionEdge) { if (oldPositionEdge.toFixed(2) !== newPositionEdge.toFixed(2)) { return false; } } } } return true; } //# sourceMappingURL=PositioningContainer.js.map