@fluentui/react
Version:
Reusable React components for building web experiences.
313 lines (312 loc) • 21.2 kB
JavaScript
define(["require", "exports", "tslib", "react", "../../Utilities", "../../FocusTrapZone", "./Modal.styles", "../../Overlay", "../../Layer", "../../Popup", "../../ResponsiveMode", "../../common/DirectionalHint", "../../Icon", "../../utilities/DraggableZone/index", "@fluentui/react-window-provider", "@fluentui/react-hooks"], function (require, exports, tslib_1, React, Utilities_1, FocusTrapZone_1, Modal_styles_1, Overlay_1, Layer_1, Popup_1, ResponsiveMode_1, DirectionalHint_1, Icon_1, index_1, react_window_provider_1, react_hooks_1) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ModalBase = void 0;
var ZERO = { x: 0, y: 0 };
var DEFAULT_PROPS = {
isOpen: false,
isDarkOverlay: true,
className: '',
containerClassName: '',
enableAriaHiddenSiblings: true,
};
var getClassNames = (0, Utilities_1.classNamesFunction)();
var getMoveDelta = function (ev) {
var delta = 10;
if (ev.shiftKey) {
if (!ev.ctrlKey) {
delta = 50;
}
}
else if (ev.ctrlKey) {
delta = 1;
}
return delta;
};
var useComponentRef = function (props, focusTrapZone) {
React.useImperativeHandle(props.componentRef, function () { return ({
focus: function () {
if (focusTrapZone.current) {
focusTrapZone.current.focus();
}
},
}); }, [focusTrapZone]);
};
exports.ModalBase = React.forwardRef(function (propsWithoutDefaults, ref) {
var _a, _b, _c, _d, _e;
var props = (0, Utilities_1.getPropsWithDefaults)(DEFAULT_PROPS, propsWithoutDefaults);
var allowTouchBodyScroll = props.allowTouchBodyScroll, className = props.className, children = props.children, containerClassName = props.containerClassName, scrollableContentClassName = props.scrollableContentClassName, elementToFocusOnDismiss = props.elementToFocusOnDismiss, firstFocusableSelector = props.firstFocusableSelector, focusTrapZoneProps = props.focusTrapZoneProps, forceFocusInsideTrap = props.forceFocusInsideTrap,
// eslint-disable-next-line @typescript-eslint/no-deprecated
_f = props.disableRestoreFocus,
// eslint-disable-next-line @typescript-eslint/no-deprecated
disableRestoreFocus = _f === void 0 ? props.ignoreExternalFocusing : _f, isBlocking = props.isBlocking, isAlert = props.isAlert, isClickableOutsideFocusTrap = props.isClickableOutsideFocusTrap, isDarkOverlay = props.isDarkOverlay, onDismiss = props.onDismiss, layerProps = props.layerProps, overlay = props.overlay, isOpen = props.isOpen, titleAriaId = props.titleAriaId, styles = props.styles, subtitleAriaId = props.subtitleAriaId, theme = props.theme, topOffsetFixed = props.topOffsetFixed, responsiveMode = props.responsiveMode,
// eslint-disable-next-line @typescript-eslint/no-deprecated
onLayerDidMount = props.onLayerDidMount, isModeless = props.isModeless, dragOptions = props.dragOptions, onDismissed = props.onDismissed,
// eslint-disable-next-line @typescript-eslint/no-deprecated
enableAriaHiddenSiblings = props.enableAriaHiddenSiblings, popupProps = props.popupProps;
var rootRef = React.useRef(null);
var focusTrapZone = React.useRef(null);
var focusTrapZoneRef = (0, react_hooks_1.useMergedRefs)(focusTrapZone, focusTrapZoneProps === null || focusTrapZoneProps === void 0 ? void 0 : focusTrapZoneProps.componentRef);
var focusTrapZoneElm = React.useRef(null);
var mergedRef = (0, react_hooks_1.useMergedRefs)(rootRef, ref);
var modalResponsiveMode = (0, ResponsiveMode_1.useResponsiveMode)(mergedRef);
var focusTrapZoneId = (0, react_hooks_1.useId)('ModalFocusTrapZone', focusTrapZoneProps === null || focusTrapZoneProps === void 0 ? void 0 : focusTrapZoneProps.id);
var win = (0, react_window_provider_1.useWindow)();
var _g = (0, react_hooks_1.useSetTimeout)(), setTimeout = _g.setTimeout, clearTimeout = _g.clearTimeout;
var _h = React.useState(isOpen), isModalOpen = _h[0], setIsModalOpen = _h[1];
var _j = React.useState(isOpen), isVisible = _j[0], setIsVisible = _j[1];
var _k = React.useState(ZERO), coordinates = _k[0], setCoordinates = _k[1];
var _l = React.useState(), modalRectangleTop = _l[0], setModalRectangleTop = _l[1];
var _m = (0, react_hooks_1.useBoolean)(false), isModalMenuOpen = _m[0], _o = _m[1], toggleModalMenuOpen = _o.toggle, setModalMenuClose = _o.setFalse;
var internalState = (0, react_hooks_1.useConst)(function () { return ({
onModalCloseTimer: 0,
allowTouchBodyScroll: allowTouchBodyScroll,
scrollableContent: null,
lastSetCoordinates: ZERO,
events: new Utilities_1.EventGroup({}),
}); });
var keepInBounds = (dragOptions || {}).keepInBounds;
var isAlertRole = isAlert !== null && isAlert !== void 0 ? isAlert : (isBlocking && !isModeless);
var layerClassName = layerProps === undefined ? '' : layerProps.className;
var classNames = getClassNames(styles, {
theme: theme,
className: className,
containerClassName: containerClassName,
scrollableContentClassName: scrollableContentClassName,
isOpen: isOpen,
isVisible: isVisible,
hasBeenOpened: internalState.hasBeenOpened,
modalRectangleTop: modalRectangleTop,
topOffsetFixed: topOffsetFixed,
isModeless: isModeless,
layerClassName: layerClassName,
windowInnerHeight: win === null || win === void 0 ? void 0 : win.innerHeight,
isDefaultDragHandle: dragOptions && !dragOptions.dragHandleSelector,
});
var mergedLayerProps = tslib_1.__assign(tslib_1.__assign({ eventBubblingEnabled: false }, layerProps), { onLayerDidMount: layerProps && layerProps.onLayerDidMount ? layerProps.onLayerDidMount : onLayerDidMount, insertFirst: (layerProps === null || layerProps === void 0 ? void 0 : layerProps.insertFirst) || isModeless, className: classNames.layer });
// Allow the user to scroll within the modal but not on the body
var allowScrollOnModal = React.useCallback(function (elt) {
if (elt) {
if (internalState.allowTouchBodyScroll) {
(0, Utilities_1.allowOverscrollOnElement)(elt, internalState.events);
}
else {
(0, Utilities_1.allowScrollOnElement)(elt, internalState.events);
}
}
else {
internalState.events.off(internalState.scrollableContent);
}
internalState.scrollableContent = elt;
}, [internalState]);
var registerInitialModalPosition = function () {
var dialogMain = focusTrapZoneElm.current;
var modalRectangle = dialogMain === null || dialogMain === void 0 ? void 0 : dialogMain.getBoundingClientRect();
if (modalRectangle) {
if (topOffsetFixed) {
setModalRectangleTop(modalRectangle.top);
}
if (keepInBounds) {
// x/y are unavailable in IE, so use the equivalent left/top
internalState.minPosition = { x: -modalRectangle.left, y: -modalRectangle.top };
internalState.maxPosition = { x: modalRectangle.left, y: modalRectangle.top };
}
}
};
/**
* Clamps an axis to a specified min and max position.
*
* @param axis A string that represents the axis (x/y).
* @param position The position on the axis.
*/
var getClampedAxis = React.useCallback(function (axis, position) {
var minPosition = internalState.minPosition, maxPosition = internalState.maxPosition;
if (keepInBounds && minPosition && maxPosition) {
position = Math.max(minPosition[axis], position);
position = Math.min(maxPosition[axis], position);
}
return position;
}, [keepInBounds, internalState]);
var handleModalClose = function () {
var _a;
internalState.lastSetCoordinates = ZERO;
setModalMenuClose();
internalState.isInKeyboardMoveMode = false;
setIsModalOpen(false);
setCoordinates(ZERO);
(_a = internalState.disposeOnKeyUp) === null || _a === void 0 ? void 0 : _a.call(internalState);
onDismissed === null || onDismissed === void 0 ? void 0 : onDismissed();
};
var handleDragStart = React.useCallback(function () {
setModalMenuClose();
internalState.isInKeyboardMoveMode = false;
}, [internalState, setModalMenuClose]);
var handleDrag = React.useCallback(function (ev, dragData) {
setCoordinates(function (prevValue) { return ({
x: getClampedAxis('x', prevValue.x + dragData.delta.x),
y: getClampedAxis('y', prevValue.y + dragData.delta.y),
}); });
}, [getClampedAxis]);
var handleDragStop = React.useCallback(function () {
if (focusTrapZone.current) {
focusTrapZone.current.focus();
}
}, []);
var handleEnterKeyboardMoveMode = function () {
// We need a global handleKeyDown event when we are in the move mode so that we can
// handle the key presses and the components inside the modal do not get the events
var handleKeyDown = function (ev) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
if (ev.altKey && ev.ctrlKey && ev.keyCode === Utilities_1.KeyCodes.space) {
// CTRL + ALT + SPACE is handled during keyUp
ev.preventDefault();
ev.stopPropagation();
return;
}
// eslint-disable-next-line @typescript-eslint/no-deprecated
var newLocal = ev.altKey || ev.keyCode === Utilities_1.KeyCodes.escape;
if (isModalMenuOpen && newLocal) {
setModalMenuClose();
}
// eslint-disable-next-line @typescript-eslint/no-deprecated
if (internalState.isInKeyboardMoveMode && (ev.keyCode === Utilities_1.KeyCodes.escape || ev.keyCode === Utilities_1.KeyCodes.enter)) {
internalState.isInKeyboardMoveMode = false;
ev.preventDefault();
ev.stopPropagation();
}
if (internalState.isInKeyboardMoveMode) {
var handledEvent = true;
var delta_1 = getMoveDelta(ev);
// eslint-disable-next-line @typescript-eslint/no-deprecated
switch (ev.keyCode) {
/* eslint-disable no-fallthrough */
case Utilities_1.KeyCodes.escape:
setCoordinates(internalState.lastSetCoordinates);
case Utilities_1.KeyCodes.enter: {
// TODO: determine if fallthrough was intentional
/* eslint-enable no-fallthrough */
internalState.lastSetCoordinates = ZERO;
// setIsInKeyboardMoveMode(false);
break;
}
case Utilities_1.KeyCodes.up: {
setCoordinates(function (prevValue) { return ({ x: prevValue.x, y: getClampedAxis('y', prevValue.y - delta_1) }); });
break;
}
case Utilities_1.KeyCodes.down: {
setCoordinates(function (prevValue) { return ({ x: prevValue.x, y: getClampedAxis('y', prevValue.y + delta_1) }); });
break;
}
case Utilities_1.KeyCodes.left: {
setCoordinates(function (prevValue) { return ({ x: getClampedAxis('x', prevValue.x - delta_1), y: prevValue.y }); });
break;
}
case Utilities_1.KeyCodes.right: {
setCoordinates(function (prevValue) { return ({ x: getClampedAxis('x', prevValue.x + delta_1), y: prevValue.y }); });
break;
}
default: {
handledEvent = false;
}
}
if (handledEvent) {
ev.preventDefault();
ev.stopPropagation();
}
}
};
internalState.lastSetCoordinates = coordinates;
setModalMenuClose();
internalState.isInKeyboardMoveMode = true;
internalState.events.on(win, 'keydown', handleKeyDown, true /* useCapture */);
internalState.disposeOnKeyDown = function () {
internalState.events.off(win, 'keydown', handleKeyDown, true /* useCapture */);
internalState.disposeOnKeyDown = undefined;
};
};
var handleExitKeyboardMoveMode = function (ev) {
var _a, _b;
(_a = focusTrapZoneProps === null || focusTrapZoneProps === void 0 ? void 0 : focusTrapZoneProps.onBlur) === null || _a === void 0 ? void 0 : _a.call(focusTrapZoneProps, ev);
internalState.lastSetCoordinates = ZERO;
internalState.isInKeyboardMoveMode = false;
(_b = internalState.disposeOnKeyDown) === null || _b === void 0 ? void 0 : _b.call(internalState);
};
var registerForKeyUp = function () {
var handleKeyUp = function (ev) {
// Needs to handle the CTRL + ALT + SPACE key during keyup due to FireFox bug:
// https://bugzilla.mozilla.org/show_bug.cgi?id=1220143
// eslint-disable-next-line @typescript-eslint/no-deprecated
if (ev.altKey && ev.ctrlKey && ev.keyCode === Utilities_1.KeyCodes.space) {
if ((0, Utilities_1.elementContains)(internalState.scrollableContent, ev.target)) {
toggleModalMenuOpen();
ev.preventDefault();
ev.stopPropagation();
}
}
};
if (!internalState.disposeOnKeyUp) {
internalState.events.on(win, 'keyup', handleKeyUp, true /* useCapture */);
internalState.disposeOnKeyUp = function () {
internalState.events.off(win, 'keyup', handleKeyUp, true /* useCapture */);
internalState.disposeOnKeyUp = undefined;
};
}
};
React.useEffect(function () {
clearTimeout(internalState.onModalCloseTimer);
// Opening the dialog
if (isOpen) {
// This must be done after the modal content has rendered
requestAnimationFrame(function () { return setTimeout(registerInitialModalPosition, 0); });
setIsModalOpen(true);
// Add a keyUp handler for all key up events once the dialog is open.
if (dragOptions) {
registerForKeyUp();
}
internalState.hasBeenOpened = true;
setIsVisible(true);
}
// Closing the dialog
if (!isOpen && isModalOpen) {
internalState.onModalCloseTimer = setTimeout(handleModalClose, parseFloat(Modal_styles_1.animationDuration) * 1000);
setIsVisible(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- should only run if isModalOpen or isOpen mutates.
}, [isModalOpen, isOpen]);
(0, react_hooks_1.useUnmount)(function () {
internalState.events.dispose();
if (internalState.onModalCloseTimer) {
clearTimeout(internalState.onModalCloseTimer);
internalState.onModalCloseTimer = 0;
}
});
useComponentRef(props, focusTrapZone);
useDebugWarnings(props);
var modalContent = (React.createElement(FocusTrapZone_1.FocusTrapZone, tslib_1.__assign({}, focusTrapZoneProps, { id: focusTrapZoneId, ref: focusTrapZoneElm, componentRef: focusTrapZoneRef, className: (0, Utilities_1.css)(classNames.main, focusTrapZoneProps === null || focusTrapZoneProps === void 0 ? void 0 : focusTrapZoneProps.className), elementToFocusOnDismiss: (_a = focusTrapZoneProps === null || focusTrapZoneProps === void 0 ? void 0 : focusTrapZoneProps.elementToFocusOnDismiss) !== null && _a !== void 0 ? _a : elementToFocusOnDismiss, isClickableOutsideFocusTrap: (_b = focusTrapZoneProps === null || focusTrapZoneProps === void 0 ? void 0 : focusTrapZoneProps.isClickableOutsideFocusTrap) !== null && _b !== void 0 ? _b : (isModeless || isClickableOutsideFocusTrap || !isBlocking), disableRestoreFocus: (_c = focusTrapZoneProps === null || focusTrapZoneProps === void 0 ? void 0 : focusTrapZoneProps.disableRestoreFocus) !== null && _c !== void 0 ? _c : disableRestoreFocus, forceFocusInsideTrap: ((_d = focusTrapZoneProps === null || focusTrapZoneProps === void 0 ? void 0 : focusTrapZoneProps.forceFocusInsideTrap) !== null && _d !== void 0 ? _d : forceFocusInsideTrap) && !isModeless,
// eslint-disable-next-line @typescript-eslint/no-deprecated
firstFocusableSelector: (focusTrapZoneProps === null || focusTrapZoneProps === void 0 ? void 0 : focusTrapZoneProps.firstFocusableSelector) || firstFocusableSelector, focusPreviouslyFocusedInnerElement: (_e = focusTrapZoneProps === null || focusTrapZoneProps === void 0 ? void 0 : focusTrapZoneProps.focusPreviouslyFocusedInnerElement) !== null && _e !== void 0 ? _e : true, onBlur: internalState.isInKeyboardMoveMode ? handleExitKeyboardMoveMode : undefined }),
dragOptions && internalState.isInKeyboardMoveMode && (React.createElement("div", { className: classNames.keyboardMoveIconContainer }, dragOptions.keyboardMoveIconProps ? (React.createElement(Icon_1.Icon, tslib_1.__assign({}, dragOptions.keyboardMoveIconProps))) : (React.createElement(Icon_1.Icon, { iconName: "move", className: classNames.keyboardMoveIcon })))),
React.createElement("div", { ref: allowScrollOnModal, className: classNames.scrollableContent, "data-is-scrollable": true },
dragOptions && isModalMenuOpen && (React.createElement(dragOptions.menu, { items: [
{ key: 'move', text: dragOptions.moveMenuItemText, onClick: handleEnterKeyboardMoveMode },
{ key: 'close', text: dragOptions.closeMenuItemText, onClick: handleModalClose },
], onDismiss: setModalMenuClose, alignTargetEdge: true, coverTarget: true, directionalHint: DirectionalHint_1.DirectionalHint.topLeftEdge, directionalHintFixed: true, shouldFocusOnMount: true, target: internalState.scrollableContent })),
children)));
return ((isModalOpen && modalResponsiveMode >= (responsiveMode || ResponsiveMode_1.ResponsiveMode.small) && (React.createElement(Layer_1.Layer, tslib_1.__assign({ ref: mergedRef }, mergedLayerProps),
React.createElement(Popup_1.Popup, tslib_1.__assign({ role: isAlertRole ? 'alertdialog' : 'dialog', ariaLabelledBy: titleAriaId, ariaDescribedBy: subtitleAriaId, onDismiss: onDismiss,
// eslint-disable-next-line @typescript-eslint/no-deprecated
shouldRestoreFocus: !disableRestoreFocus,
// Modeless modals shouldn't hide siblings.
// Popup will automatically handle this based on the aria-modal setting.
// eslint-disable-next-line @typescript-eslint/no-deprecated
enableAriaHiddenSiblings: enableAriaHiddenSiblings, "aria-modal": !isModeless }, popupProps),
React.createElement("div", { className: classNames.root, role: !isModeless ? 'document' : undefined },
!isModeless && (React.createElement(Overlay_1.Overlay, tslib_1.__assign({ "aria-hidden": true, isDarkThemed: isDarkOverlay, onClick: isBlocking ? undefined : onDismiss, allowTouchBodyScroll: allowTouchBodyScroll }, overlay))),
dragOptions ? (React.createElement(index_1.DraggableZone, { handleSelector: dragOptions.dragHandleSelector || "#".concat(focusTrapZoneId), preventDragSelector: "button", onStart: handleDragStart, onDragChange: handleDrag, onStop: handleDragStop, position: coordinates }, modalContent)) : (modalContent)))))) ||
null);
});
exports.ModalBase.displayName = 'Modal';
function useDebugWarnings(props) {
}
});
//# sourceMappingURL=Modal.base.js.map