280 lines
16 KiB
JavaScript
280 lines
16 KiB
JavaScript
define(["require", "exports", "tslib", "react", "./PositioningContainer.styles", "../../../Styling", "../../../Layer", "../../../common/DirectionalHint", "../../../Utilities", "../../../Positioning", "../../../Styling", "@fluentui/react-hooks", "../../../utilities/dom"], function (require, exports, tslib_1, React, PositioningContainer_styles_1, Styling_1, Layer_1, DirectionalHint_1, Utilities_1, Positioning_1, Styling_2, react_hooks_1, dom_1) {
|
|
"use strict";
|
|
var _a;
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.PositioningContainer = void 0;
|
|
exports.useHeightOffset = useHeightOffset;
|
|
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
|