@fluentui/react
Version:
Reusable React components for building web experiences.
468 lines • 28.3 kB
JavaScript
define(["require", "exports", "tslib", "react", "../../common/DirectionalHint", "../../Utilities", "../../utilities/positioning/positioning", "../../Positioning", "../../Popup", "../../Utilities", "../../Styling", "@fluentui/react-hooks", "../../utilities/dom"], function (require, exports, tslib_1, React, DirectionalHint_1, Utilities_1, positioning_1, Positioning_1, Popup_1, Utilities_2, Styling_1, react_hooks_1, dom_1) {
"use strict";
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.CalloutContentBase = void 0;
var COMPONENT_NAME = 'CalloutContentBase';
var ANIMATIONS = (_a = {},
_a[Positioning_1.RectangleEdge.top] = Styling_1.AnimationClassNames.slideUpIn10,
_a[Positioning_1.RectangleEdge.bottom] = Styling_1.AnimationClassNames.slideDownIn10,
_a[Positioning_1.RectangleEdge.left] = Styling_1.AnimationClassNames.slideLeftIn10,
_a[Positioning_1.RectangleEdge.right] = Styling_1.AnimationClassNames.slideRightIn10,
_a);
var BEAK_ORIGIN_POSITION = { top: 0, left: 0 };
// Microsoft Edge will overwrite inline styles if there is an animation pertaining to that style.
// To help ensure that edge will respect the offscreen style opacity
// filter needs to be added as an additional way to set opacity.
// Also set pointer-events: none so that the callout will not occlude the element it is
// going to be positioned against
var OFF_SCREEN_STYLE = {
opacity: 0,
filter: 'opacity(0)',
pointerEvents: 'none',
};
// role and role description go hand-in-hand. Both would be included by spreading getNativeProps for a basic element
// This constant array can be used to filter these out of native props spread on callout root and apply them together on
// calloutMain (the Popup component within the callout)
var ARIA_ROLE_ATTRIBUTES = ['role', 'aria-roledescription'];
var DEFAULT_PROPS = {
preventDismissOnLostFocus: false,
preventDismissOnScroll: false,
preventDismissOnResize: false,
isBeakVisible: true,
beakWidth: 16,
gapSpace: 0,
minPagePadding: 8,
directionalHint: DirectionalHint_1.DirectionalHint.bottomAutoEdge,
};
var getClassNames = (0, Utilities_2.classNamesFunction)({
disableCaching: true, // disabling caching because stylesProp.position mutates often
});
/**
* (Hook) to return a function to lazily fetch the bounds of the target element for the callout.
*/
function useBounds(_a, targetRef, targetWindow) {
var bounds = _a.bounds, _b = _a.minPagePadding, minPagePadding = _b === void 0 ? DEFAULT_PROPS.minPagePadding : _b, target = _a.target;
var _c = React.useState(false), targetWindowResized = _c[0], setTargetWindowResized = _c[1];
var cachedBounds = React.useRef();
var getBounds = React.useCallback(function () {
if (!cachedBounds.current || targetWindowResized) {
var currentBounds = typeof bounds === 'function' ? (targetWindow ? bounds(target, targetWindow) : undefined) : bounds;
if (!currentBounds && targetWindow) {
currentBounds = (0, Positioning_1.getBoundsFromTargetWindow)(targetRef.current, targetWindow);
currentBounds = {
top: currentBounds.top + minPagePadding,
left: currentBounds.left + minPagePadding,
right: currentBounds.right - minPagePadding,
bottom: currentBounds.bottom - minPagePadding,
width: currentBounds.width - minPagePadding * 2,
height: currentBounds.height - minPagePadding * 2,
};
}
cachedBounds.current = currentBounds;
targetWindowResized && setTargetWindowResized(false);
}
return cachedBounds.current;
}, [bounds, minPagePadding, target, targetRef, targetWindow, targetWindowResized]);
var async = (0, react_hooks_1.useAsync)();
(0, react_hooks_1.useOnEvent)(targetWindow, 'resize', async.debounce(function () {
setTargetWindowResized(true);
}, 500, { leading: true }));
return getBounds;
}
/**
* (Hook) to return the maximum available height for the Callout to render into.
*/
function useMaxHeight(_a, getBounds, targetRef, positions) {
var _b;
var calloutMaxHeight = _a.calloutMaxHeight, finalHeight = _a.finalHeight, directionalHint = _a.directionalHint, directionalHintFixed = _a.directionalHintFixed, hidden = _a.hidden, gapSpace = _a.gapSpace, beakWidth = _a.beakWidth, isBeakVisible = _a.isBeakVisible, coverTarget = _a.coverTarget;
var _c = React.useState(), maxHeight = _c[0], setMaxHeight = _c[1];
var _d = (_b = positions === null || positions === void 0 ? void 0 : positions.elementPosition) !== null && _b !== void 0 ? _b : {}, top = _d.top, bottom = _d.bottom;
var targetRect = (targetRef === null || targetRef === void 0 ? void 0 : targetRef.current) ? (0, positioning_1.getRectangleFromTarget)(targetRef.current) : undefined;
React.useEffect(function () {
var _a;
var bounds = (_a = getBounds()) !== null && _a !== void 0 ? _a : {};
var topBounds = bounds.top;
var bottomBounds = bounds.bottom;
var calculatedHeight;
// If aligned to top edge of target and not covering target, update bottom bounds to the
// top of the target (accounting for gap space and beak)
if ((positions === null || positions === void 0 ? void 0 : positions.targetEdge) === Positioning_1.RectangleEdge.top && (targetRect === null || targetRect === void 0 ? void 0 : targetRect.top) && !coverTarget) {
bottomBounds = targetRect.top - (0, positioning_1.calculateGapSpace)(isBeakVisible, beakWidth, gapSpace);
}
if (typeof top === 'number' && bottomBounds) {
calculatedHeight = bottomBounds - top;
}
else if (typeof bottom === 'number' && typeof topBounds === 'number' && bottomBounds) {
calculatedHeight = bottomBounds - topBounds - bottom;
}
if ((!calloutMaxHeight && !hidden) ||
(calloutMaxHeight && calculatedHeight && calloutMaxHeight > calculatedHeight)) {
setMaxHeight(calculatedHeight);
}
else if (calloutMaxHeight) {
setMaxHeight(calloutMaxHeight);
}
else {
setMaxHeight(undefined);
}
}, [
bottom,
calloutMaxHeight,
finalHeight,
directionalHint,
directionalHintFixed,
getBounds,
hidden,
positions,
top,
gapSpace,
beakWidth,
isBeakVisible,
targetRect,
coverTarget,
]);
return maxHeight;
}
/**
* (Hook) to find the current position of Callout. If Callout is resized then a new position is calculated.
*/
function usePositions(props, hostElement, calloutElement, targetRef, getBounds, popupRef) {
var _a = React.useState(), positions = _a[0], setPositions = _a[1];
var positionAttempts = React.useRef(0);
var previousTarget = React.useRef();
var async = (0, react_hooks_1.useAsync)();
var hidden = props.hidden, target = props.target, finalHeight = props.finalHeight, calloutMaxHeight = props.calloutMaxHeight, onPositioned = props.onPositioned, directionalHint = props.directionalHint, hideOverflow = props.hideOverflow, preferScrollResizePositioning = props.preferScrollResizePositioning;
var win = (0, dom_1.useWindowEx)();
var localRef = React.useRef();
var popupStyles;
if (localRef.current !== popupRef.current) {
localRef.current = popupRef.current;
popupStyles = popupRef.current ? win === null || win === void 0 ? void 0 : win.getComputedStyle(popupRef.current) : undefined;
}
var popupOverflowY = popupStyles === null || popupStyles === void 0 ? void 0 : popupStyles.overflowY;
React.useEffect(function () {
if (!hidden) {
var timerId_1 = async.requestAnimationFrame(function () {
var _a, _b;
if (hostElement.current && calloutElement) {
var currentProps = tslib_1.__assign(tslib_1.__assign({}, props), { target: targetRef.current, bounds: getBounds() });
// duplicate calloutElement & remove useMaxHeight's maxHeight for position calc
var dupeCalloutElement = calloutElement.cloneNode(true);
dupeCalloutElement.style.maxHeight = calloutMaxHeight ? "".concat(calloutMaxHeight) : '';
dupeCalloutElement.style.visibility = 'hidden';
(_a = calloutElement.parentElement) === null || _a === void 0 ? void 0 : _a.appendChild(dupeCalloutElement);
var previousPositions = previousTarget.current === target ? positions : undefined;
// only account for scroll resizing if styles allow callout to scroll
// (popup styles determine if callout will scroll)
var isOverflowYHidden = hideOverflow || popupOverflowY === 'clip' || popupOverflowY === 'hidden';
var shouldScroll = preferScrollResizePositioning && !isOverflowYHidden;
// If there is a finalHeight given then we assume that the user knows and will handle
// additional positioning adjustments so we should call positionCard
var newPositions = finalHeight
? (0, Positioning_1.positionCard)(currentProps, hostElement.current, dupeCalloutElement, previousPositions, win)
: (0, Positioning_1.positionCallout)(currentProps, hostElement.current, dupeCalloutElement, previousPositions, shouldScroll, undefined, win);
// clean up duplicate calloutElement
(_b = calloutElement.parentElement) === null || _b === void 0 ? void 0 : _b.removeChild(dupeCalloutElement);
// Set the new position only when the positions do not exist or one of the new callout positions
// is 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 callout 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);
}
else if (positionAttempts.current > 0) {
// Only call the onPositioned callback if the callout has been re-positioned at least once.
positionAttempts.current = 0;
onPositioned === null || onPositioned === void 0 ? void 0 : onPositioned(positions);
}
}
}, calloutElement);
previousTarget.current = target;
return function () {
async.cancelAnimationFrame(timerId_1);
previousTarget.current = undefined;
};
}
else {
// When the callout is hidden, clear position state so that it is not accidentally used next render.
setPositions(undefined);
positionAttempts.current = 0;
}
}, [
hidden,
directionalHint,
async,
calloutElement,
calloutMaxHeight,
hostElement,
targetRef,
finalHeight,
getBounds,
onPositioned,
positions,
props,
target,
hideOverflow,
preferScrollResizePositioning,
popupOverflowY,
win,
]);
return positions;
}
/**
* (Hook) to set up behavior to automatically focus the callout when it appears, if indicated by props.
*/
function useAutoFocus(_a, positions, calloutElement) {
var hidden = _a.hidden, setInitialFocus = _a.setInitialFocus;
var async = (0, react_hooks_1.useAsync)();
var hasPositions = !!positions;
React.useEffect(function () {
if (!hidden && setInitialFocus && hasPositions && calloutElement) {
var timerId_2 = async.requestAnimationFrame(function () { return (0, Utilities_1.focusFirstChild)(calloutElement); }, calloutElement);
return function () { return async.cancelAnimationFrame(timerId_2); };
}
}, [hidden, hasPositions, async, calloutElement, setInitialFocus]);
}
/**
* (Hook) to set up various handlers to dismiss the popup when it loses focus or the window scrolls or similar cases.
*/
function useDismissHandlers(_a, positions, hostElement, targetRef, targetWindow) {
var hidden = _a.hidden, onDismiss = _a.onDismiss,
// eslint-disable-next-line deprecation/deprecation
preventDismissOnScroll = _a.preventDismissOnScroll,
// eslint-disable-next-line deprecation/deprecation
preventDismissOnResize = _a.preventDismissOnResize,
// eslint-disable-next-line deprecation/deprecation
preventDismissOnLostFocus = _a.preventDismissOnLostFocus, dismissOnTargetClick = _a.dismissOnTargetClick, shouldDismissOnWindowFocus = _a.shouldDismissOnWindowFocus, preventDismissOnEvent = _a.preventDismissOnEvent;
var isMouseDownOnPopup = React.useRef(false);
var async = (0, react_hooks_1.useAsync)();
var mouseDownHandlers = (0, react_hooks_1.useConst)([
function () {
isMouseDownOnPopup.current = true;
},
function () {
isMouseDownOnPopup.current = false;
},
]);
var positionsExists = !!positions;
React.useEffect(function () {
var dismissOnScroll = function (ev) {
if (positionsExists && !preventDismissOnScroll) {
dismissOnClickOrScroll(ev);
}
};
var dismissOnResize = function (ev) {
if (!preventDismissOnResize && !(preventDismissOnEvent && preventDismissOnEvent(ev))) {
onDismiss === null || onDismiss === void 0 ? void 0 : onDismiss(ev);
}
};
var dismissOnLostFocus = function (ev) {
if (!preventDismissOnLostFocus) {
dismissOnClickOrScroll(ev);
}
};
var dismissOnClickOrScroll = function (ev) {
var eventPaths = ev.composedPath ? ev.composedPath() : [];
var target = eventPaths.length > 0 ? eventPaths[0] : ev.target;
var isEventTargetOutsideCallout = hostElement.current && !(0, Utilities_1.elementContains)(hostElement.current, target);
// If mouse is pressed down on callout but moved outside then released, don't dismiss the callout.
if (isEventTargetOutsideCallout && isMouseDownOnPopup.current) {
isMouseDownOnPopup.current = false;
return;
}
if ((!targetRef.current && isEventTargetOutsideCallout) ||
(ev.target !== targetWindow &&
isEventTargetOutsideCallout &&
(!targetRef.current ||
'stopPropagation' in targetRef.current ||
dismissOnTargetClick ||
(target !== targetRef.current && !(0, Utilities_1.elementContains)(targetRef.current, target))))) {
if (preventDismissOnEvent && preventDismissOnEvent(ev)) {
return;
}
onDismiss === null || onDismiss === void 0 ? void 0 : onDismiss(ev);
}
};
var dismissOnTargetWindowBlur = function (ev) {
// Do nothing
if (!shouldDismissOnWindowFocus) {
return;
}
if (((preventDismissOnEvent && !preventDismissOnEvent(ev)) ||
(!preventDismissOnEvent && !preventDismissOnLostFocus)) &&
!(targetWindow === null || targetWindow === void 0 ? void 0 : targetWindow.document.hasFocus()) &&
ev.relatedTarget === null) {
onDismiss === null || onDismiss === void 0 ? void 0 : onDismiss(ev);
}
};
// This is added so the callout will dismiss when the window is scrolled
// but not when something inside the callout 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 callout.
var disposablesPromise = new Promise(function (resolve) {
async.setTimeout(function () {
if (!hidden && targetWindow) {
var disposables_1 = [
(0, Utilities_1.on)(targetWindow, 'scroll', dismissOnScroll, true),
(0, Utilities_1.on)(targetWindow, 'resize', dismissOnResize, true),
(0, Utilities_1.on)(targetWindow.document.documentElement, 'focus', dismissOnLostFocus, true),
(0, Utilities_1.on)(targetWindow.document.documentElement, 'click', dismissOnLostFocus, true),
(0, Utilities_1.on)(targetWindow, 'blur', dismissOnTargetWindowBlur, true),
];
resolve(function () {
disposables_1.forEach(function (dispose) { return dispose(); });
});
}
}, 0);
});
return function () {
disposablesPromise.then(function (dispose) { return dispose(); });
};
}, [
hidden,
async,
hostElement,
targetRef,
targetWindow,
onDismiss,
shouldDismissOnWindowFocus,
dismissOnTargetClick,
preventDismissOnLostFocus,
preventDismissOnResize,
preventDismissOnScroll,
positionsExists,
preventDismissOnEvent,
]);
return mouseDownHandlers;
}
exports.CalloutContentBase = React.memo(React.forwardRef(function (propsWithoutDefaults, forwardedRef) {
var props = (0, Utilities_1.getPropsWithDefaults)(DEFAULT_PROPS, propsWithoutDefaults);
var styles = props.styles, style = props.style, ariaLabel = props.ariaLabel, ariaDescribedBy = props.ariaDescribedBy, ariaLabelledBy = props.ariaLabelledBy, className = props.className, isBeakVisible = props.isBeakVisible, children = props.children, beakWidth = props.beakWidth, calloutWidth = props.calloutWidth, calloutMaxWidth = props.calloutMaxWidth, calloutMinWidth = props.calloutMinWidth, doNotLayer = props.doNotLayer, finalHeight = props.finalHeight, _a = props.hideOverflow, hideOverflow = _a === void 0 ? !!finalHeight : _a, backgroundColor = props.backgroundColor, calloutMaxHeight = props.calloutMaxHeight, onScroll = props.onScroll,
// eslint-disable-next-line deprecation/deprecation
_b = props.shouldRestoreFocus,
// eslint-disable-next-line deprecation/deprecation
shouldRestoreFocus = _b === void 0 ? true : _b, target = props.target, hidden = props.hidden, onLayerMounted = props.onLayerMounted, popupProps = props.popupProps;
var hostElement = React.useRef(null);
var popupRef = React.useRef(null);
var mergedPopupRefs = (0, react_hooks_1.useMergedRefs)(popupRef, popupProps === null || popupProps === void 0 ? void 0 : popupProps.ref);
var _c = React.useState(null), calloutElement = _c[0], setCalloutElement = _c[1];
var calloutCallback = React.useCallback(function (calloutEl) {
setCalloutElement(calloutEl);
}, []);
var rootRef = (0, react_hooks_1.useMergedRefs)(hostElement, forwardedRef);
var _d = (0, react_hooks_1.useTarget)(props.target, {
current: calloutElement,
}), targetRef = _d[0], targetWindow = _d[1];
var getBounds = useBounds(props, targetRef, targetWindow);
var positions = usePositions(props, hostElement, calloutElement, targetRef, getBounds, mergedPopupRefs);
var maxHeight = useMaxHeight(props, getBounds, targetRef, positions);
var _e = useDismissHandlers(props, positions, hostElement, targetRef, targetWindow), mouseDownOnPopup = _e[0], mouseUpOnPopup = _e[1];
// do not set both top and bottom css props from positions
// instead, use maxHeight
var isForcedInBounds = (positions === null || positions === void 0 ? void 0 : positions.elementPosition.top) && (positions === null || positions === void 0 ? void 0 : positions.elementPosition.bottom);
var cssPositions = tslib_1.__assign(tslib_1.__assign({}, positions === null || positions === void 0 ? void 0 : positions.elementPosition), { maxHeight: maxHeight });
if (isForcedInBounds) {
cssPositions.bottom = undefined;
}
useAutoFocus(props, positions, calloutElement);
React.useEffect(function () {
if (!hidden) {
onLayerMounted === null || onLayerMounted === void 0 ? void 0 : onLayerMounted();
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- should only run if hidden changes
}, [hidden]);
// 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 overflowYHidden = hideOverflow;
var beakVisible = isBeakVisible && !!target;
var classNames = getClassNames(styles, {
theme: props.theme,
className: className,
overflowYHidden: overflowYHidden,
calloutWidth: calloutWidth,
positions: positions,
beakWidth: beakWidth,
backgroundColor: backgroundColor,
calloutMaxWidth: calloutMaxWidth,
calloutMinWidth: calloutMinWidth,
doNotLayer: doNotLayer,
});
var overflowStyle = tslib_1.__assign(tslib_1.__assign({ maxHeight: calloutMaxHeight ? calloutMaxHeight : '100%' }, style), (overflowYHidden && { overflowY: 'hidden' }));
var visibilityStyle = props.hidden ? { visibility: 'hidden' } : undefined;
// React.CSSProperties does not understand IRawStyle, so the inline animations will need to be cast as any for now.
return (React.createElement("div", { ref: rootRef, className: classNames.container, style: visibilityStyle },
React.createElement("div", tslib_1.__assign({}, (0, Utilities_1.getNativeProps)(props, Utilities_1.divProperties, ARIA_ROLE_ATTRIBUTES), { className: (0, Utilities_1.css)(classNames.root, positions && positions.targetEdge && ANIMATIONS[positions.targetEdge]), style: positions ? tslib_1.__assign({}, cssPositions) : 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: calloutCallback }),
beakVisible && React.createElement("div", { className: classNames.beak, style: getBeakPosition(positions) }),
beakVisible && React.createElement("div", { className: classNames.beakCurtain }),
React.createElement(Popup_1.Popup
// don't use getNativeElementProps for role and roledescription because it will also
// pass through data-* props (resulting in them being used in two places)
, tslib_1.__assign({
// don't use getNativeElementProps for role and roledescription because it will also
// pass through data-* props (resulting in them being used in two places)
role: props.role, "aria-roledescription": props['aria-roledescription'], ariaDescribedBy: ariaDescribedBy, ariaLabel: ariaLabel, ariaLabelledBy: ariaLabelledBy, className: classNames.calloutMain, onDismiss: props.onDismiss, onMouseDown: mouseDownOnPopup, onMouseUp: mouseUpOnPopup, onRestoreFocus: props.onRestoreFocus, onScroll: onScroll, shouldRestoreFocus: shouldRestoreFocus, style: overflowStyle }, popupProps, { ref: mergedPopupRefs }), children))));
}), function (previousProps, nextProps) {
if (!nextProps.shouldUpdateWhenHidden && previousProps.hidden && nextProps.hidden) {
// Do not update when hidden.
return true;
}
return (0, Utilities_1.shallowCompare)(previousProps, nextProps);
});
/**
* (Utility) to find and return the current `Callout` Beak position.
*
* @param positions
*/
function getBeakPosition(positions) {
var _a, _b;
var beakPositionStyle = tslib_1.__assign(tslib_1.__assign({}, (_a = positions === null || positions === void 0 ? void 0 : positions.beakPosition) === null || _a === void 0 ? void 0 : _a.elementPosition), { display: ((_b = positions === null || positions === void 0 ? void 0 : positions.beakPosition) === null || _b === void 0 ? void 0 : _b.hideBeak) ? 'none' : undefined });
if (!beakPositionStyle.top && !beakPositionStyle.bottom && !beakPositionStyle.left && !beakPositionStyle.right) {
beakPositionStyle.left = BEAK_ORIGIN_POSITION.left;
beakPositionStyle.top = BEAK_ORIGIN_POSITION.top;
}
return beakPositionStyle;
}
/**
* (Utility) used to compare two different elementPositions to determine whether they are equal.
*
* @param prevElementPositions
* @param newElementPosition
*/
function arePositionsEqual(prevElementPositions, newElementPosition) {
return (comparePositions(prevElementPositions.elementPosition, newElementPosition.elementPosition) &&
comparePositions(prevElementPositions.beakPosition.elementPosition, newElementPosition.beakPosition.elementPosition));
}
/**
* (Utility) used in **arePositionsEqual** to compare two different elementPositions.
*
* @param prevElementPositions
* @param newElementPositions
*/
function comparePositions(prevElementPositions, newElementPositions) {
for (var key in newElementPositions) {
if (newElementPositions.hasOwnProperty(key)) {
var oldPositionEdge = prevElementPositions[key];
var newPositionEdge = newElementPositions[key];
if (oldPositionEdge !== undefined && newPositionEdge !== undefined) {
if (oldPositionEdge.toFixed(2) !== newPositionEdge.toFixed(2)) {
return false;
}
}
else {
return false;
}
}
}
return true;
}
exports.CalloutContentBase.displayName = COMPONENT_NAME;
});
//# sourceMappingURL=CalloutContent.base.js.map