UNPKG

@mskcc/carbon-react

Version:

Carbon react components for the MSKCC DSM

478 lines (462 loc) 17.7 kB
/** * MSKCC 2021, 2024 */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _rollupPluginBabelHelpers = require('../_virtual/_rollupPluginBabelHelpers.js'); var PropTypes = require('prop-types'); var React = require('react'); var ReactDOM = require('react-dom'); var window = require('window-or-global'); var OptimizedResize = require('./OptimizedResize.js'); var navigation = require('./keyboard/navigation.js'); var warning = require('./warning.js'); var wrapFocus = require('./wrapFocus.js'); var usePrefix = require('./usePrefix.js'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var PropTypes__default = /*#__PURE__*/_interopDefaultLegacy(PropTypes); var React__default = /*#__PURE__*/_interopDefaultLegacy(React); var ReactDOM__default = /*#__PURE__*/_interopDefaultLegacy(ReactDOM); var window__default = /*#__PURE__*/_interopDefaultLegacy(window); /** * The structure for the position of floating menu. * @typedef {object} FloatingMenu~position * @property {number} left The left position. * @property {number} top The top position. * @property {number} right The right position. * @property {number} bottom The bottom position. */ /** * The structure for the size of floating menu. * @typedef {object} FloatingMenu~size * @property {number} width The width. * @property {number} height The height. */ /** * The structure for the position offset of floating menu. * @typedef {object} FloatingMenu~offset * @property {number} top The top position. * @property {number} left The left position. */ /** * The structure for the target container. * @typedef {object} FloatingMenu~container * @property {DOMRect} rect Return of element.getBoundingClientRect() * @property {string} position Position style (static, absolute, relative...) */ const DIRECTION_LEFT = 'left'; const DIRECTION_TOP = 'top'; const DIRECTION_RIGHT = 'right'; const DIRECTION_BOTTOM = 'bottom'; /** * @param {FloatingMenu~offset} [oldMenuOffset={}] The old value. * @param {FloatingMenu~offset} [menuOffset={}] The new value. * @returns `true` if the parent component wants to change in the adjustment of the floating menu position. * @private */ const hasChangeInOffset = function () { let oldMenuOffset = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; let menuOffset = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; if (typeof oldMenuOffset !== typeof menuOffset) { return true; } if (Object(menuOffset) === menuOffset && typeof menuOffset !== 'function') { return oldMenuOffset.top !== menuOffset.top || oldMenuOffset.left !== menuOffset.left; } return oldMenuOffset !== menuOffset; }; /** * @param {object} params The parameters. * @param {FloatingMenu~size} params.menuSize The size of the menu. * @param {FloatingMenu~position} params.refPosition The position of the triggering element. * @param {FloatingMenu~offset} [params.offset={ left: 0, top: 0 }] The position offset of the menu. * @param {string} [params.direction=bottom] The menu direction. * @param {number} [params.scrollX=0] The scroll position of the viewport. * @param {number} [params.scrollY=0] The scroll position of the viewport. * @param {FloatingMenu~container} [params.container] The size and position type of target element. * @returns {FloatingMenu~offset} The position of the menu, relative to the top-left corner of the viewport. * @private */ const getFloatingPosition = _ref => { let { menuSize, refPosition = {}, offset = {}, direction = DIRECTION_BOTTOM, scrollX: pageXOffset = 0, scrollY: pageYOffset = 0, container } = _ref; const { left: refLeft = 0, top: refTop = 0, right: refRight = 0, bottom: refBottom = 0 } = refPosition; const scrollX = container.position !== 'static' ? 0 : pageXOffset; const scrollY = container.position !== 'static' ? 0 : pageYOffset; const relativeDiff = { top: container.position !== 'static' ? container.rect.top : 0, left: container.position !== 'static' ? container.rect.left : 0 }; const { width, height } = menuSize; const { top = 0, left = 0 } = offset; const refCenterHorizontal = (refLeft + refRight) / 2; const refCenterVertical = (refTop + refBottom) / 2; return { [DIRECTION_LEFT]: () => ({ left: refLeft - width + scrollX - left - relativeDiff.left, top: refCenterVertical - height / 2 + scrollY + top - 9 - relativeDiff.top }), [DIRECTION_TOP]: () => ({ left: refCenterHorizontal - width / 2 + scrollX + left - relativeDiff.left, top: refTop - height + scrollY - top - relativeDiff.top }), [DIRECTION_RIGHT]: () => ({ left: refRight + scrollX + left - relativeDiff.left, top: refCenterVertical - height / 2 + scrollY + top + 3 - relativeDiff.top }), [DIRECTION_BOTTOM]: () => ({ left: refCenterHorizontal - width / 2 + scrollX + left - relativeDiff.left, top: refBottom + scrollY + top - relativeDiff.top }) }[direction](); }; /** * A menu that is detached from the triggering element. * Useful when the container of the triggering element cannot have `overflow:visible` style, etc. */ class FloatingMenu extends React__default["default"].Component { constructor() { var _this; super(...arguments); _this = this; // `true` if the menu body is mounted and calculation of the position is in progress. _rollupPluginBabelHelpers.defineProperty(this, "_placeInProgress", false); _rollupPluginBabelHelpers.defineProperty(this, "state", { /** * The position of the menu, relative to the top-left corner of the viewport. * @type {FloatingMenu~offset} */ floatingPosition: undefined }); /** * The cached reference to the menu container. * Only used if React portal API is not available. * @type {Element} * @private */ _rollupPluginBabelHelpers.defineProperty(this, "_menuContainer", null); /** * The cached reference to the menu body. * The reference is set via callback ref instead of object ref, * in order to hook the event when the element ref gets available, * which can be at a different timing from `cDM()`, presumably with SSR scenario. * @type {Element} * @private */ _rollupPluginBabelHelpers.defineProperty(this, "_menuBody", null); /** * Focus sentinel refs for focus trap behavior */ _rollupPluginBabelHelpers.defineProperty(this, "startSentinel", /*#__PURE__*/React__default["default"].createRef()); _rollupPluginBabelHelpers.defineProperty(this, "endSentinel", /*#__PURE__*/React__default["default"].createRef()); /** * Calculates the position in the viewport of floating menu, * once this component is mounted or updated upon change in the following props: * * * `menuOffset` (The adjustment that should be applied to the calculated floating menu's position) * * `menuDirection` (Where the floating menu menu should be placed relative to the trigger button) * * @private */ _rollupPluginBabelHelpers.defineProperty(this, "_updateMenuSize", function () { let prevProps = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; let isAdjustment = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; const menuBody = _this._menuBody; process.env.NODE_ENV !== "production" ? warning.warning(menuBody, 'The DOM node for menu body for calculating its position is not available. Skipping...') : void 0; if (!menuBody) { return; } const { menuOffset: oldMenuOffset = {}, menuDirection: oldMenuDirection } = prevProps; const { menuOffset = {}, menuDirection } = _this.props; if (hasChangeInOffset(oldMenuOffset, menuOffset) || oldMenuDirection !== menuDirection || isAdjustment) { const { flipped, triggerRef, updateOrientation } = _this.props; const { current: triggerEl } = triggerRef; const menuSize = menuBody.getBoundingClientRect(); const refPosition = triggerEl && triggerEl.getBoundingClientRect(); const offset = typeof menuOffset !== 'function' ? menuOffset : menuOffset(menuBody, menuDirection, triggerEl, flipped); // Optional function to allow parent component to check // if the orientation needs to be changed based on params if (updateOrientation) { updateOrientation({ menuSize, refPosition, direction: menuDirection, offset, scrollX: window__default["default"].pageXOffset, scrollY: window__default["default"].pageYOffset, container: { rect: _this.props.target().getBoundingClientRect(), position: getComputedStyle(_this.props.target()).position } }); } // Skips if either in the following condition: // a) Menu body has `display:none` // b) `menuOffset` as a callback returns `undefined` (The callback saw that it couldn't calculate the value) if (menuSize.width > 0 && menuSize.height > 0 || !offset) { _this.setState({ floatingPosition: getFloatingPosition({ menuSize, refPosition, direction: menuDirection, offset, scrollX: window__default["default"].pageXOffset, scrollY: window__default["default"].pageYOffset, container: { rect: _this.props.target().getBoundingClientRect(), position: getComputedStyle(_this.props.target()).position } }) }, () => { if (!isAdjustment) { const newMenuSize = menuBody.getBoundingClientRect(); if (newMenuSize !== menuSize) { _this._updateMenuSize(_this.props, true); } } }); } } }); /** * Set focus on floating menu content after menu placement. * @param {Element} menuBody The DOM element of the menu body. * @private */ _rollupPluginBabelHelpers.defineProperty(this, "_focusMenuContent", menuBody => { const primaryFocusNode = menuBody.querySelector(this.props.selectorPrimaryFocus || null); const tabbableNode = menuBody.querySelector(navigation.selectorTabbable); const focusableNode = menuBody.querySelector(navigation.selectorFocusable); const focusTarget = primaryFocusNode || // User defined focusable node tabbableNode || // First sequentially focusable node focusableNode || // First programmatic focusable node menuBody; focusTarget.focus(); if (focusTarget === menuBody && process.env.NODE_ENV !== "production") { process.env.NODE_ENV !== "production" ? warning.warning(focusableNode === null, 'Floating Menus must have at least a programmatically focusable child. ' + 'This can be accomplished by adding tabIndex="-1" to the content element.') : void 0; } }); /** * A callback for called when menu body is mounted or unmounted. * @param {Element} menuBody The menu body being mounted. `null` if the menu body is being unmounted. */ _rollupPluginBabelHelpers.defineProperty(this, "_menuRef", menuBody => { const { menuRef } = this.props; this._placeInProgress = !!menuBody; menuRef && menuRef(this._menuBody = menuBody); if (menuBody) { this._updateMenuSize(); } }); /** * @returns The child nodes, with styles containing the floating menu position. * @private */ _rollupPluginBabelHelpers.defineProperty(this, "_getChildrenWithProps", () => { const { styles, children } = this.props; const { floatingPosition: pos } = this.state; // If no pos available, we need to hide the element (offscreen to the left) // This is done so we can measure the content before positioning it correctly. const positioningStyle = pos ? { left: `${pos.left}px`, top: `${pos.top}px`, right: 'auto' } : { visibility: 'hidden', top: '0px' }; return /*#__PURE__*/React__default["default"].cloneElement(children, { ref: this._menuRef, style: { ...styles, ...positioningStyle, position: 'absolute', opacity: 1 } }); }); /** * Blur handler for when focus wrap behavior is enabled * @param {Event} event * @param {Element} event.target previously focused node * @param {Element} event.relatedTarget current focused node */ _rollupPluginBabelHelpers.defineProperty(this, "handleBlur", _ref2 => { let { target: oldActiveNode, relatedTarget: currentActiveNode } = _ref2; if (currentActiveNode && oldActiveNode) { const { current: startSentinelNode } = this.startSentinel; const { current: endSentinelNode } = this.endSentinel; wrapFocus["default"]({ bodyNode: this._menuBody, startSentinelNode, endSentinelNode, currentActiveNode, oldActiveNode }); } }); } componentWillUnmount() { this.hResize.release(); } componentDidMount() { this.hResize = OptimizedResize["default"].add(() => { this._updateMenuSize(); }); } componentDidUpdate(prevProps) { this._updateMenuSize(prevProps); const { onPlace } = this.props; if (this._placeInProgress && this.state.floatingPosition) { if (this._menuBody && !this._menuBody.contains(document.activeElement)) { this._focusMenuContent(this._menuBody); } if (typeof onPlace === 'function') { onPlace(this._menuBody); this._placeInProgress = false; } } } render() { const { context: prefix } = this; if (typeof document !== 'undefined') { const { focusTrap, target } = this.props; return /*#__PURE__*/ReactDOM__default["default"].createPortal( /*#__PURE__*/React__default["default"].createElement("div", { onBlur: focusTrap ? this.handleBlur : null }, /*#__PURE__*/React__default["default"].createElement("span", { ref: this.startSentinel, tabIndex: "0", role: "link", className: `${prefix}--visually-hidden` }, "Focus sentinel"), this._getChildrenWithProps(), /*#__PURE__*/React__default["default"].createElement("span", { ref: this.endSentinel, tabIndex: "0", role: "link", className: `${prefix}--visually-hidden` }, "Focus sentinel")), !target ? document.body : target()); } return null; } } _rollupPluginBabelHelpers.defineProperty(FloatingMenu, "contextType", usePrefix.PrefixContext); _rollupPluginBabelHelpers.defineProperty(FloatingMenu, "propTypes", { /** * Contents to put into the floating menu. */ children: PropTypes__default["default"].object, /** * `true` if the menu alignment should be flipped. */ flipped: PropTypes__default["default"].bool, /** * Enable or disable focus trap behavior */ focusTrap: PropTypes__default["default"].bool, /** * Where to put the tooltip, relative to the trigger button. */ menuDirection: PropTypes__default["default"].oneOf([DIRECTION_LEFT, DIRECTION_TOP, DIRECTION_RIGHT, DIRECTION_BOTTOM]), /** * The adjustment of the floating menu position, considering the position of dropdown arrow, etc. */ menuOffset: PropTypes__default["default"].oneOfType([PropTypes__default["default"].shape({ top: PropTypes__default["default"].number, left: PropTypes__default["default"].number }), PropTypes__default["default"].func]), /** * The callback called when the menu body has been mounted to/will be unmounted from the DOM. */ menuRef: PropTypes__default["default"].func, /** * The callback called when the menu body has been mounted and positioned. */ onPlace: PropTypes__default["default"].func, /** * Specify a CSS selector that matches the DOM element that should * be focused when the Modal opens */ selectorPrimaryFocus: PropTypes__default["default"].string, /** * The additional styles to put to the floating menu. */ styles: PropTypes__default["default"].object, /** * The query selector indicating where the floating menu body should be placed. */ target: PropTypes__default["default"].func, /** * The element ref of the tooltip's trigger button. */ triggerRef: PropTypes__default["default"].oneOfType([PropTypes__default["default"].func, PropTypes__default["default"].shape({ current: PropTypes__default["default"].any })]), /** * Optional function to change orientation of tooltip based on parent */ updateOrientation: PropTypes__default["default"].func }); _rollupPluginBabelHelpers.defineProperty(FloatingMenu, "defaultProps", { menuOffset: {}, menuDirection: DIRECTION_BOTTOM, updateOrientation: null }); var FloatingMenu$1 = FloatingMenu; exports.DIRECTION_BOTTOM = DIRECTION_BOTTOM; exports.DIRECTION_LEFT = DIRECTION_LEFT; exports.DIRECTION_RIGHT = DIRECTION_RIGHT; exports.DIRECTION_TOP = DIRECTION_TOP; exports["default"] = FloatingMenu$1;