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
@@ -0,0 +1,4 @@
import * as React from 'react';
import type { IPositioningContainerProps } from './PositioningContainer.types';
export declare function useHeightOffset({ finalHeight }: IPositioningContainerProps, contentHost: React.RefObject<HTMLDivElement | null>): number;
export declare const PositioningContainer: React.FunctionComponent<IPositioningContainerProps>;
@@ -0,0 +1,290 @@
"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
File diff suppressed because one or more lines are too long
@@ -0,0 +1,19 @@
import type { IStyle } from '../../../Styling';
export interface IPositioningContainerStyles {
/**
* Style for the root element in the default enabled/unchecked state.
*/
root?: IStyle;
}
export interface IPositioningContainerNames {
/**
* Root html container for this component.
*/
root: string;
container: string;
main: string;
overFlowYHidden: string;
beak?: string;
beakCurtain?: string;
}
export declare const getClassNames: () => IPositioningContainerNames;
@@ -0,0 +1,35 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getClassNames = void 0;
var Utilities_1 = require("../../../Utilities");
var Styling_1 = require("../../../Styling");
exports.getClassNames = (0, Utilities_1.memoizeFunction)(function () {
var _a;
return (0, Styling_1.mergeStyleSets)({
root: [
{
position: 'absolute',
boxSizing: 'border-box',
selectors: (_a = {},
_a[Styling_1.HighContrastSelector] = {
border: '1px solid WindowText',
},
_a),
},
(0, Styling_1.focusClear)(),
],
container: {
position: 'relative',
},
main: {
backgroundColor: '#ffffff',
overflowX: 'hidden',
overflowY: 'hidden',
position: 'relative',
},
overFlowYHidden: {
overflowY: 'hidden',
},
});
});
//# sourceMappingURL=PositioningContainer.styles.js.map
@@ -0,0 +1 @@
{"version":3,"file":"PositioningContainer.styles.js","sourceRoot":"../src/","sources":["components/Coachmark/PositioningContainer/PositioningContainer.styles.ts"],"names":[],"mappings":";;;AAAA,gDAAqD;AACrD,4CAAoF;AAsBvE,QAAA,aAAa,GAAG,IAAA,2BAAe,EAAC;;IAC3C,OAAO,IAAA,wBAAc,EAAC;QACpB,IAAI,EAAE;YACJ;gBACE,QAAQ,EAAE,UAAU;gBACpB,SAAS,EAAE,YAAY;gBACvB,SAAS;oBACP,GAAC,8BAAoB,IAAG;wBACtB,MAAM,EAAE,sBAAsB;qBAC/B;uBACF;aACF;YACD,IAAA,oBAAU,GAAE;SACb;QACD,SAAS,EAAE;YACT,QAAQ,EAAE,UAAU;SACrB;QACD,IAAI,EAAE;YACJ,eAAe,EAAE,SAAS;YAC1B,SAAS,EAAE,QAAQ;YACnB,SAAS,EAAE,QAAQ;YACnB,QAAQ,EAAE,UAAU;SACrB;QACD,eAAe,EAAE;YACf,SAAS,EAAE,QAAQ;SACpB;KACF,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { memoizeFunction } from '../../../Utilities';\nimport { mergeStyleSets, focusClear, HighContrastSelector } from '../../../Styling';\nimport type { IStyle } from '../../../Styling';\n\nexport interface IPositioningContainerStyles {\n /**\n * Style for the root element in the default enabled/unchecked state.\n */\n root?: IStyle;\n}\n\nexport interface IPositioningContainerNames {\n /**\n * Root html container for this component.\n */\n root: string;\n container: string;\n main: string;\n overFlowYHidden: string;\n beak?: string;\n beakCurtain?: string;\n}\n\nexport const getClassNames = memoizeFunction((): IPositioningContainerNames => {\n return mergeStyleSets({\n root: [\n {\n position: 'absolute',\n boxSizing: 'border-box',\n selectors: {\n [HighContrastSelector]: {\n border: '1px solid WindowText',\n },\n },\n },\n focusClear(),\n ],\n container: {\n position: 'relative',\n },\n main: {\n backgroundColor: '#ffffff',\n overflowX: 'hidden',\n overflowY: 'hidden',\n position: 'relative',\n },\n overFlowYHidden: {\n overflowY: 'hidden',\n },\n });\n});\n"]}
@@ -0,0 +1,159 @@
import * as React from 'react';
import { DirectionalHint } from '../../../common/DirectionalHint';
import type { IRefObject, IBaseProps, Point, IRectangle } from '../../../Utilities';
import type { IPositionedData } from '../../../Positioning';
import type { Target } from '@fluentui/react-hooks';
import type { ILayerProps } from '../../../Layer';
/**
* {@docCategory Coachmark}
*/
export interface IPositioningContainer {
}
/**
* {@docCategory Coachmark}
*/
export interface IPositioningContainerProps extends IBaseProps<IPositioningContainer>, React.RefAttributes<HTMLDivElement> {
/**
* All props for your component are to be defined here.
*/
componentRef?: IRefObject<IPositioningContainer>;
/**
* The target that the positioningContainer should try to position itself based on.
* It can be either an HTMLElement a querySelector string of a valid HTMLElement
* or a MouseEvent. If MouseEvent is given then 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 `directionalHint` will be used instead
*/
directionalHintForRTL?: DirectionalHint;
/**
* The gap between the positioningContainer and the target
* @defaultvalue 0
*/
offsetFromTarget?: number;
/**
* Custom width for positioningContainer including borders. If value is 0, no width is applied.
* @defaultvalue 0
*/
positioningContainerWidth?: number;
/**
* The background color of the positioningContainer in hex format ie. #ffffff.
* @defaultvalue $ms-color-white
*/
backgroundColor?: string;
/**
* The bounding rectangle for which the contextual menu can appear in.
*/
bounds?: IRectangle;
/**
* The minimum distance the positioningContainer will be away from the edge of the screen.
* @defaultvalue 8
*/
minPagePadding?: number;
/**
* If true use a point rather than rectangle to position the positioningContainer.
* For example it can be used to position based on a click.
* @deprecated Do not use.
*/
useTargetPoint?: boolean;
/**
* Point used to position the positioningContainer.
* Deprecated, use `target` instead.
* @deprecated Use `target` instead.
*/
targetPoint?: Point;
/**
* If true then the onClose will not not dismiss on scroll
* @defaultvalue false
*/
preventDismissOnScroll?: boolean;
/**
* If true the position returned will have the menu element cover the target.
* If false then it will position next to the target;
* @defaultvalue false
*/
coverTarget?: boolean;
/**
* Aria role assigned to the positioningContainer (Eg. dialog, alertdialog).
*/
role?: string;
/**
* Accessible label text for positioningContainer.
*/
ariaLabel?: string;
/**
* Defines the element id referencing the element containing label text for positioningContainer.
*/
ariaLabelledBy?: string;
/**
* Defines the element id referencing the element containing the description for the positioningContainer.
*/
ariaDescribedBy?: string;
/**
* CSS class to apply to the positioningContainer.
* @defaultvalue null
*/
className?: string;
/**
* Defines an optional set of props to be passed through to Layer
*/
layerProps?: ILayerProps;
/**
* Optional callback when the layer content has mounted.
*/
onLayerMounted?: () => void;
/**
* Optional callback that is called once the positioningContainer has been correctly positioned.
* @param positions - gives the user information about how the container is positioned such
* as the element position, the target edge, and the alignment edge of the container.
*/
onPositioned?: (positions?: IPositionedData) => void;
/**
* Callback when the positioningContainer tries to close.
*/
onDismiss?: (ev?: any) => 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 positioningContainer 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 positioningContainer can adjust its position.
*/
finalHeight?: number;
/**
* If true then the positioningContainer will attempt to focus the first focusable element that it contains.
* If it doesn't find an element, no focus will be set and the method will return false.
* This means that it's the contents responsibility to either set focus or have
* focusable items.
* @returns True if focus was set, false if it was not.
*/
setInitialFocus?: boolean;
/**
* Set max height of positioningContainer
* When not set the positioningContainer will expand with contents up to the bottom of the screen
*/
positioningContainerMaxHeight?: number;
/**
* Child nodes to render
*/
children?: React.ReactNode;
}
/**
* @deprecated Use `IPositioningContainerProps`
* {@docCategory Coachmark}
*/
export type IPositioningContainerTypes = IPositioningContainerProps;
@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=PositioningContainer.types.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
export * from './PositioningContainer';
export * from './PositioningContainer.types';
@@ -0,0 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
tslib_1.__exportStar(require("./PositioningContainer"), exports);
tslib_1.__exportStar(require("./PositioningContainer.types"), exports);
//# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"../src/","sources":["components/Coachmark/PositioningContainer/index.ts"],"names":[],"mappings":";;;AAAA,iEAAuC;AACvC,uEAA6C","sourcesContent":["export * from './PositioningContainer';\nexport * from './PositioningContainer.types';\n"]}