first commit

This commit is contained in:
Stefan Hacker
2026-04-03 09:38:48 +02:00
commit 37ad745546
47450 changed files with 3120798 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
import * as React from 'react';
import type { ICalloutProps } from './Callout.types';
export declare const Callout: React.FunctionComponent<ICalloutProps>;
+11
View File
@@ -0,0 +1,11 @@
import { __assign, __rest } from "tslib";
import * as React from 'react';
import { CalloutContent } from './CalloutContent';
import { Layer } from '../../Layer';
export var Callout = React.forwardRef(function (_a, forwardedRef) {
var layerProps = _a.layerProps, doNotLayer = _a.doNotLayer, rest = __rest(_a, ["layerProps", "doNotLayer"]);
var content = React.createElement(CalloutContent, __assign({}, rest, { doNotLayer: doNotLayer, ref: forwardedRef }));
return doNotLayer ? content : React.createElement(Layer, __assign({}, layerProps), content);
});
Callout.displayName = 'Callout';
//# sourceMappingURL=Callout.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"Callout.js","sourceRoot":"../src/","sources":["components/Callout/Callout.tsx"],"names":[],"mappings":";AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAGpC,MAAM,CAAC,IAAM,OAAO,GAA2C,KAAK,CAAC,UAAU,CAC7E,UAAC,EAAmC,EAAE,YAAY;IAA/C,IAAA,UAAU,gBAAA,EAAE,UAAU,gBAAA,EAAK,IAAI,cAAjC,4BAAmC,CAAF;IAChC,IAAM,OAAO,GAAG,oBAAC,cAAc,eAAK,IAAI,IAAE,UAAU,EAAE,UAAU,EAAE,GAAG,EAAE,YAAY,IAAI,CAAC;IACxF,OAAO,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,oBAAC,KAAK,eAAK,UAAU,GAAG,OAAO,CAAS,CAAC;AACzE,CAAC,CACF,CAAC;AACF,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC","sourcesContent":["import * as React from 'react';\nimport { CalloutContent } from './CalloutContent';\nimport { Layer } from '../../Layer';\nimport type { ICalloutProps } from './Callout.types';\n\nexport const Callout: React.FunctionComponent<ICalloutProps> = React.forwardRef<HTMLDivElement, ICalloutProps>(\n ({ layerProps, doNotLayer, ...rest }, forwardedRef) => {\n const content = <CalloutContent {...rest} doNotLayer={doNotLayer} ref={forwardedRef} />;\n return doNotLayer ? content : <Layer {...layerProps}>{content}</Layer>;\n },\n);\nCallout.displayName = 'Callout';\n"]}
+347
View File
@@ -0,0 +1,347 @@
import * as React from 'react';
import { DirectionalHint } from '../../common/DirectionalHint';
import type { IStyle, ITheme } from '../../Styling';
import type { IRectangle, IStyleFunctionOrObject } from '../../Utilities';
import type { ICalloutPositionedInfo } from '../../Positioning';
import type { ILayerProps } from '../../Layer';
import type { IPopupProps } from '../../Popup';
import type { Target } from '@fluentui/react-hooks';
import type { IPopupRestoreFocusParams } from '../../Popup';
/**
* {@docCategory Callout}
*/
export interface ICalloutProps extends React.HTMLAttributes<HTMLDivElement>, React.RefAttributes<HTMLDivElement> {
/**
* The target that the Callout should try to position itself based on.
* It can be an element, a query selector string of a valid element,
* or a `MouseEvent`. If a `MouseEvent` is given, the origin point of the event will be used.
*/
target?: Target;
/**
* How the element should be positioned
* @defaultvalue DirectionalHint.BottomAutoEdge
*/
directionalHint?: DirectionalHint;
/**
* How the element should be positioned in RTL layouts.
* If not specified, a mirror of the `directionalHint` alignment edge will be used instead.
* This means that `DirectionalHint.BottomLeft` will change to `DirectionalHint.BottomRight` but
* `DirectionalHint.LeftAuto` will not change.
*/
directionalHintForRTL?: DirectionalHint;
/**
* The gap between the Callout and the target, specified as number of pixels
* @defaultvalue 0
*/
gapSpace?: number;
/**
* The width of the beak.
* @defaultvalue 16
*/
beakWidth?: number;
/**
* Custom width for callout including borders. If value is 0, no width is applied.
* @defaultvalue 0
*/
calloutWidth?: number;
/**
* Maximum width for callout including borders. If value is 0, no width is applied.
* @defaultvalue 0
*/
calloutMaxWidth?: number;
/**
* Minimum width for callout including borders. If value is 0, no width is applied.
* @defaultvalue 0
*/
calloutMinWidth?: number;
/**
* The background color of the Callout in hex format ie. #ffffff.
* @defaultvalue $ms-color-white
*/
backgroundColor?: string;
/**
* The bounding rectangle the callout can appear in (or callback that returns a rectangle).
*/
bounds?: IRectangle | ((target?: Target, targetWindow?: Window) => IRectangle | undefined);
/**
* The minimum distance the callout will be away from the edge of the screen.
* @defaultvalue 8
*/
minPagePadding?: number;
/**
* Whether the beak is visible.
* @defaultvalue true
*/
isBeakVisible?: boolean;
/**
* If true then the callout will not dismiss on scroll.
* *Note:* This property will be ignored if using `preventDismissOnEvent`.
* @defaultvalue false
*/
preventDismissOnScroll?: boolean;
/**
* If true then the callout will not dismiss on resize.
* *Note:* This property will be ignored if using `preventDismissOnEvent`.
* @defaultvalue false
*/
preventDismissOnResize?: boolean;
/**
* If true then the callout will not dismiss when it loses focus.
* *Note:* This property will be ignored if using `preventDismissOnEvent`.
* @defaultvalue false
*/
preventDismissOnLostFocus?: boolean;
/**
* If true then the callout will dismiss when the target element is clicked
* @defaultvalue false
*/
dismissOnTargetClick?: boolean;
/**
* If defined, then takes priority over `preventDismissOnLostFocus`, `preventDismissOnResize`,
* and `preventDismissOnScroll`.
* If it returns true, the callout will not dismiss for this event.
* If not defined or returns false, the callout can dismiss for this event.
*/
preventDismissOnEvent?: (ev: Event | React.FocusEvent | React.KeyboardEvent | React.MouseEvent) => boolean;
/**
* If true, callout will dismiss when the window gets focus.
* @defaultvalue false
*/
shouldDismissOnWindowFocus?: boolean;
/**
* If true, the callout element will be positioned to cover the target.
* If false, it will position next to the target.
* @defaultvalue false
*/
coverTarget?: boolean;
/**
* If true the positioning logic will prefer to flip edges rather than to nudge the rectangle to fit within bounds,
* thus making sure the element aligns perfectly with target's alignment edge.
*/
alignTargetEdge?: boolean;
/**
* Aria role assigned to the callout (e.g. `dialog`, `alertdialog`).
*/
role?: string;
/**
* Accessible label text for callout.
*/
ariaLabel?: string;
/**
* ID of the element which contains label text for the callout.
*/
ariaLabelledBy?: string;
/**
* ID of the element which contains the description for the callout.
*/
ariaDescribedBy?: string;
/**
* CSS class to apply to the callout.
* @defaultvalue null
*/
className?: string;
/**
* CSS style to apply to the callout.
*
* If you set `overflowY` in this object, it provides a performance optimization by preventing
* Popup (underlying component of Callout) from calculating whether it needs a scroll bar.
*/
style?: React.CSSProperties;
/**
* Optional callback when the layer content has mounted.
*/
onLayerMounted?: () => void;
/**
* Optional props to pass to the Layer component hosting the callout.
*/
layerProps?: ILayerProps;
/**
* Optional props to pass the Popup component that the callout uses.
*/
popupProps?: IPopupProps;
/**
* Optional callback that is called once the callout has been correctly positioned.
* @param positions - Gives the user information about how the callout is positioned such as the
* final edge of the target that it positioned against, the beak position, and the beak's relationship to the
* edges of the callout.
*/
onPositioned?: (positions?: ICalloutPositionedInfo) => void;
/**
* Callback when the Callout tries to close.
*/
onDismiss?: (ev?: Event | React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>) => void;
/**
* If true, do not render on a new layer. If false, render on a new layer.
*/
doNotLayer?: boolean;
/**
* If true the position will not change sides in an attempt to fit the callout within bounds.
* It will still attempt to align it to whatever bounds are given.
* @defaultvalue false
*/
directionalHintFixed?: boolean;
/**
* Specify the final height of the content.
* To be used when expanding the content dynamically so that callout can adjust its position.
*/
finalHeight?: number;
/**
* Manually set `overflowYHidden` style prop to true on `calloutMain` element.
* A variety of callout load animations will need this to hide the scollbar that can appear.
*/
hideOverflow?: boolean;
/**
* If true, then the callout will attempt to focus the first focusable element that it contains.
* If it doesn't find a focusable element, no focus will be set.
*/
setInitialFocus?: boolean;
/**
* Set max height of callout.
* When not set, the callout will expand with contents up to the bottom of the screen.
*/
calloutMaxHeight?: number;
/**
* Callback when the Callout body is scrolled.
*/
onScroll?: () => void;
/**
* Optional theme for component
*/
theme?: ITheme;
/**
* Optional styles for the component.
*/
styles?: IStyleFunctionOrObject<ICalloutContentStyleProps, ICalloutContentStyles>;
/**
* If specified, renders the Callout in a hidden state.
* Use this flag, rather than rendering a callout conditionally based on visibility,
* to improve rendering performance when it becomes visible.
* Note: When callout is hidden its content will not be rendered. It will only render
* once the callout is visible.
*/
hidden?: boolean;
/**
* If true, the component will be updated even when `hidden` is true.
* Note that this would consume resources to update even though nothing is being shown to the user.
* This might be helpful though if your updates are small and you want the
* callout to be revealed quickly to the user when `hidden` is set to false.
*/
shouldUpdateWhenHidden?: boolean;
/**
* If specified, determines whether the underlying {@link Popup} component should try to restore
* focus when it is dismissed. When set to false, the Popup won't try to restore focus to
* the last focused element.
* @defaultvalue true
* @deprecated use `onRestoreFocus` instead
*/
shouldRestoreFocus?: boolean;
/**
* Called when the component is unmounting, and focus needs to be restored. If this is provided,
* focus will not be restored automatically, and you'll need to call `params.originalElement.focus()`.
*/
onRestoreFocus?: (params: IPopupRestoreFocusParams) => void;
/**
* The minimum height, in pixels, that the callout will scroll-resize down to on top/bottom edges
* before repositioning to a new edge.
*
* Note: this prop has no effect if `directionalHintFixed=true`.
*
* Note: if `preferScrollResizing=false`, this prop will have no effect because the callout will not scroll-resize.
*
* Note: if `hideOverflow = true`, or if the computed callout style `overflowY` is `hidden` or `clip`,
* the callout will not scroll-resize.
*
* @defaultvalue 200
*/
minimumScrollResizeHeight?: number;
/**
* If true, the callout will scroll-resize when positioning on top / bottom edges,
* rather than repositioning to a new edge.
*
* Example: if `directionalHint=DirectionalHint.bottomAutoEdge`, and the callout content height exceeds
* the vertical space below the callout, the callout will position itself on the bottom edge of the target
* (rather than repositioning to a new edge with more available vertical space),
* and the callout will scroll-resize down to the available space.
*
* Use `minimumScrollResizeHeight` to change the minimum height the callout will resize down to
* before repositioning to another edge (default 200px).
*
* Note: this prop has no effect if `directionalHintFixed=true`.
*
* Note: if `hideOverflow = true`, or if the computed callout style `overflowY` is `hidden` or `clip`,
* the callout will not prefer scroll-resized positions (i.e. this prop will be ignored)
* @defaultvalue false
*/
preferScrollResizePositioning?: boolean;
}
/**
* {@docCategory Callout}
*/
export interface ICalloutContentStyleProps {
/**
* Theme to apply to the callout content.
*/
theme: ITheme;
/**
* Width for callout including borders.
*/
calloutWidth?: number;
/**
* CSS class to apply to the callout.
*/
className?: string;
/**
* Callout positioning data
*/
positions?: ICalloutPositionedInfo;
/**
* Whether or not to clip content of the callout, if it overflows vertically.
*/
overflowYHidden?: boolean;
/**
* Background color for the beak and callout.
*/
backgroundColor?: string;
/**
* Width of Callout beak
*/
beakWidth?: number;
/**
* Max width for callout including borders.
*/
calloutMaxWidth?: number;
/**
* Min width for callout including borders.
*/
calloutMinWidth?: number;
/**
* If true, a z-index should be set on the root element (since the Callout will not be rendered on a new layer).
*/
doNotLayer?: boolean;
}
/**
* {@docCategory Callout}
*/
export interface ICalloutContentStyles {
/**
* Style for wrapper of Callout component.
*/
container: IStyle;
/**
* Style for callout container root element.
*/
root: IStyle;
/**
* Style for callout beak.
*/
beak: IStyle;
/**
* Style for callout beak curtain.
*/
beakCurtain: IStyle;
/**
* Style for content component of the callout.
*/
calloutMain: IStyle;
}
export type { Target };
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=Callout.types.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
import * as React from 'react';
import type { ICalloutProps } from './Callout.types';
export declare const CalloutContentBase: React.FunctionComponent<ICalloutProps>;
@@ -0,0 +1,470 @@
var _a;
import { __assign } from "tslib";
import * as React from 'react';
import { DirectionalHint } from '../../common/DirectionalHint';
import { css, divProperties, elementContains, focusFirstChild, getNativeProps, on, shallowCompare, getPropsWithDefaults, } from '../../Utilities';
import { calculateGapSpace, getRectangleFromTarget } from '../../utilities/positioning/positioning';
import { positionCallout, RectangleEdge, positionCard, getBoundsFromTargetWindow } from '../../Positioning';
import { Popup } from '../../Popup';
import { classNamesFunction } from '../../Utilities';
import { AnimationClassNames } from '../../Styling';
import { useMergedRefs, useAsync, useConst, useTarget, useOnEvent } from '@fluentui/react-hooks';
import { useWindowEx } from '../../utilities/dom';
var COMPONENT_NAME = 'CalloutContentBase';
var ANIMATIONS = (_a = {},
_a[RectangleEdge.top] = AnimationClassNames.slideUpIn10,
_a[RectangleEdge.bottom] = AnimationClassNames.slideDownIn10,
_a[RectangleEdge.left] = AnimationClassNames.slideLeftIn10,
_a[RectangleEdge.right] = 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.bottomAutoEdge,
};
var getClassNames = 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(undefined);
var getBounds = React.useCallback(function () {
if (!cachedBounds.current || targetWindowResized) {
var currentBounds = typeof bounds === 'function' ? (targetWindow ? bounds(target, targetWindow) : undefined) : bounds;
if (!currentBounds && targetWindow) {
currentBounds = 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 = useAsync();
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) ? 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) === RectangleEdge.top && (targetRect === null || targetRect === void 0 ? void 0 : targetRect.top) && !coverTarget) {
bottomBounds = targetRect.top - 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(undefined);
var async = 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 = useWindowEx();
var localRef = React.useRef(undefined);
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 = __assign(__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
? positionCard(currentProps, hostElement.current, dupeCalloutElement, previousPositions, win)
: 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 = useAsync();
var hasPositions = !!positions;
React.useEffect(function () {
if (!hidden && setInitialFocus && hasPositions && calloutElement) {
var timerId_2 = async.requestAnimationFrame(function () { return 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, preventDismissOnScroll = _a.preventDismissOnScroll, preventDismissOnResize = _a.preventDismissOnResize, preventDismissOnLostFocus = _a.preventDismissOnLostFocus, dismissOnTargetClick = _a.dismissOnTargetClick, shouldDismissOnWindowFocus = _a.shouldDismissOnWindowFocus, preventDismissOnEvent = _a.preventDismissOnEvent;
var isMouseDownOnPopup = React.useRef(false);
var async = useAsync();
var mouseDownHandlers = 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 && !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 && !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 = [
on(targetWindow, 'scroll', dismissOnScroll, true),
on(targetWindow, 'resize', dismissOnResize, true),
on(targetWindow.document.documentElement, 'focus', dismissOnLostFocus, true),
on(targetWindow.document.documentElement, 'click', dismissOnLostFocus, true),
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;
}
export var CalloutContentBase = React.memo(React.forwardRef(function (propsWithoutDefaults, forwardedRef) {
var props = 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 @typescript-eslint/no-deprecated
_b = props.shouldRestoreFocus,
// eslint-disable-next-line @typescript-eslint/no-deprecated
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 = 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 = useMergedRefs(hostElement, forwardedRef);
var _d = 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 = __assign(__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 = __assign(__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", __assign({}, getNativeProps(props, divProperties, ARIA_ROLE_ATTRIBUTES), { className: css(classNames.root, positions && positions.targetEdge && ANIMATIONS[positions.targetEdge]), style: positions ? __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
// don't use getNativeElementProps for role and roledescription because it will also
// pass through data-* props (resulting in them being used in two places)
, __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,
// eslint-disable-next-line @typescript-eslint/no-deprecated
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 shallowCompare(previousProps, nextProps);
});
/**
* (Utility) to find and return the current `Callout` Beak position.
*
* @param positions
*/
function getBeakPosition(positions) {
var _a, _b;
var beakPositionStyle = __assign(__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;
}
CalloutContentBase.displayName = COMPONENT_NAME;
//# sourceMappingURL=CalloutContent.base.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
import * as React from 'react';
import type { ICalloutProps } from './Callout.types';
export declare const CalloutContent: React.FunctionComponent<ICalloutProps>;
@@ -0,0 +1,7 @@
import { styled } from '../../Utilities';
import { CalloutContentBase } from './CalloutContent.base';
import { getStyles } from './CalloutContent.styles';
export var CalloutContent = styled(CalloutContentBase, getStyles, undefined, {
scope: 'CalloutContent',
});
//# sourceMappingURL=CalloutContent.js.map
@@ -0,0 +1 @@
{"version":3,"file":"CalloutContent.js","sourceRoot":"../src/","sources":["components/Callout/CalloutContent.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAGpD,MAAM,CAAC,IAAM,cAAc,GAA2C,MAAM,CAAC,kBAAkB,EAAE,SAAS,EAAE,SAAS,EAAE;IACrH,KAAK,EAAE,gBAAgB;CACxB,CAAC,CAAC","sourcesContent":["import * as React from 'react';\nimport { styled } from '../../Utilities';\nimport { CalloutContentBase } from './CalloutContent.base';\nimport { getStyles } from './CalloutContent.styles';\nimport type { ICalloutProps } from './Callout.types';\n\nexport const CalloutContent: React.FunctionComponent<ICalloutProps> = styled(CalloutContentBase, getStyles, undefined, {\n scope: 'CalloutContent',\n});\n"]}
@@ -0,0 +1,2 @@
import type { ICalloutContentStyleProps, ICalloutContentStyles } from './Callout.types';
export declare const getStyles: (props: ICalloutContentStyleProps) => ICalloutContentStyles;
@@ -0,0 +1,97 @@
import { HighContrastSelector, focusClear, getGlobalClassNames, ZIndexes } from '../../Styling';
function getBeakStyle(beakWidth) {
return {
height: beakWidth,
width: beakWidth,
};
}
var GlobalClassNames = {
container: 'ms-Callout-container',
root: 'ms-Callout',
beak: 'ms-Callout-beak',
beakCurtain: 'ms-Callout-beakCurtain',
calloutMain: 'ms-Callout-main',
};
export var getStyles = function (props) {
var _a;
var theme = props.theme, className = props.className, overflowYHidden = props.overflowYHidden, calloutWidth = props.calloutWidth, beakWidth = props.beakWidth, backgroundColor = props.backgroundColor, calloutMaxWidth = props.calloutMaxWidth, calloutMinWidth = props.calloutMinWidth, doNotLayer = props.doNotLayer;
var classNames = getGlobalClassNames(GlobalClassNames, theme);
var semanticColors = theme.semanticColors, effects = theme.effects;
return {
container: [
classNames.container,
{
position: 'relative',
},
],
root: [
classNames.root,
theme.fonts.medium,
{
position: 'absolute',
display: 'flex',
zIndex: doNotLayer ? ZIndexes.Layer : undefined,
boxSizing: 'border-box',
borderRadius: effects.roundedCorner2,
boxShadow: effects.elevation16,
selectors: (_a = {},
_a[HighContrastSelector] = {
borderWidth: 1,
borderStyle: 'solid',
borderColor: 'WindowText',
},
_a),
},
focusClear(),
className,
!!calloutWidth && { width: calloutWidth },
!!calloutMaxWidth && { maxWidth: calloutMaxWidth },
!!calloutMinWidth && { minWidth: calloutMinWidth },
],
beak: [
classNames.beak,
{
position: 'absolute',
backgroundColor: semanticColors.menuBackground,
boxShadow: 'inherit',
border: 'inherit',
boxSizing: 'border-box',
transform: 'rotate(45deg)',
},
getBeakStyle(beakWidth),
backgroundColor && {
backgroundColor: backgroundColor,
},
],
beakCurtain: [
classNames.beakCurtain,
{
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
backgroundColor: semanticColors.menuBackground,
borderRadius: effects.roundedCorner2,
},
],
calloutMain: [
classNames.calloutMain,
{
backgroundColor: semanticColors.menuBackground,
overflowX: 'hidden',
overflowY: 'auto',
position: 'relative',
width: '100%',
borderRadius: effects.roundedCorner2,
},
overflowYHidden && {
overflowY: 'hidden',
},
backgroundColor && {
backgroundColor: backgroundColor,
},
],
};
};
//# sourceMappingURL=CalloutContent.styles.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
import * as React from 'react';
import type { IFocusTrapCalloutProps } from './FocusTrapCallout.types';
/**
* A special Callout that uses FocusTrapZone to trap focus
* @param props - Props for the component
*/
export declare const FocusTrapCallout: React.FunctionComponent<IFocusTrapCalloutProps>;
@@ -0,0 +1,13 @@
import { __assign } from "tslib";
import * as React from 'react';
import { Callout } from './Callout';
import { FocusTrapZone } from '../../FocusTrapZone';
/**
* A special Callout that uses FocusTrapZone to trap focus
* @param props - Props for the component
*/
export var FocusTrapCallout = function (props) {
return (React.createElement(Callout, __assign({}, props),
React.createElement(FocusTrapZone, __assign({ disabled: props.hidden }, props.focusTrapProps), props.children)));
};
//# sourceMappingURL=FocusTrapCallout.js.map
@@ -0,0 +1 @@
{"version":3,"file":"FocusTrapCallout.js","sourceRoot":"../src/","sources":["components/Callout/FocusTrapCallout.tsx"],"names":[],"mappings":";AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAKpD;;;GAGG;AACH,MAAM,CAAC,IAAM,gBAAgB,GAAoD,UAC/E,KAA6B;IAE7B,OAAO,CACL,oBAAC,OAAO,eAAK,KAAK;QAChB,oBAAC,aAAa,aAAC,QAAQ,EAAE,KAAK,CAAC,MAAM,IAAM,KAAK,CAAC,cAAc,GAC5D,KAAK,CAAC,QAAQ,CACD,CACR,CACX,CAAC;AACJ,CAAC,CAAC","sourcesContent":["import * as React from 'react';\n\nimport { Callout } from './Callout';\nimport { FocusTrapZone } from '../../FocusTrapZone';\nimport type { IFocusTrapCalloutProps } from './FocusTrapCallout.types';\n\nimport type { JSXElement } from '@fluentui/utilities';\n\n/**\n * A special Callout that uses FocusTrapZone to trap focus\n * @param props - Props for the component\n */\nexport const FocusTrapCallout: React.FunctionComponent<IFocusTrapCalloutProps> = (\n props: IFocusTrapCalloutProps,\n): JSXElement => {\n return (\n <Callout {...props}>\n <FocusTrapZone disabled={props.hidden} {...props.focusTrapProps}>\n {props.children}\n </FocusTrapZone>\n </Callout>\n );\n};\n"]}
@@ -0,0 +1,11 @@
import type { ICalloutProps } from './Callout.types';
import type { IFocusTrapZoneProps } from '../../FocusTrapZone';
/**
* {@docCategory Callout}
*/
export interface IFocusTrapCalloutProps extends ICalloutProps {
/**
* Optional props to be passed on to FocusTrapZone
*/
focusTrapProps?: IFocusTrapZoneProps;
}
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=FocusTrapCallout.types.js.map
@@ -0,0 +1 @@
{"version":3,"file":"FocusTrapCallout.types.js","sourceRoot":"../src/","sources":["components/Callout/FocusTrapCallout.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { ICalloutProps } from './Callout.types';\nimport type { IFocusTrapZoneProps } from '../../FocusTrapZone';\n\n/**\n * {@docCategory Callout}\n */\nexport interface IFocusTrapCalloutProps extends ICalloutProps {\n /**\n * Optional props to be passed on to FocusTrapZone\n */\n focusTrapProps?: IFocusTrapZoneProps;\n}\n"]}
+7
View File
@@ -0,0 +1,7 @@
export * from './Callout';
export * from './Callout.types';
export * from './CalloutContent';
export * from './CalloutContent.base';
export * from './FocusTrapCallout';
export * from './FocusTrapCallout.types';
export * from '../../common/DirectionalHint';
+8
View File
@@ -0,0 +1,8 @@
export * from './Callout';
export * from './Callout.types';
export * from './CalloutContent';
export * from './CalloutContent.base';
export * from './FocusTrapCallout';
export * from './FocusTrapCallout.types';
export * from '../../common/DirectionalHint';
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"../src/","sources":["components/Callout/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC","sourcesContent":["export * from './Callout';\nexport * from './Callout.types';\nexport * from './CalloutContent';\nexport * from './CalloutContent.base';\nexport * from './FocusTrapCallout';\nexport * from './FocusTrapCallout.types';\nexport * from '../../common/DirectionalHint';\n"]}