UNPKG

@carbon/react

Version:

React components for the Carbon Design System

259 lines (257 loc) 9.62 kB
/** * Copyright IBM Corp. 2016, 2026 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ const require_runtime = require("../_virtual/_rolldown/runtime.js"); const require_usePrefix = require("./usePrefix.js"); const require_keys = require("./keyboard/keys.js"); const require_match = require("./keyboard/match.js"); const require_navigation = require("./keyboard/navigation.js"); const require_warning = require("./warning.js"); const require_wrapFocus = require("./wrapFocus.js"); const require_OptimizedResize = require("./OptimizedResize.js"); let _carbon_feature_flags = require("@carbon/feature-flags"); let react = require("react"); react = require_runtime.__toESM(react); let react_jsx_runtime = require("react/jsx-runtime"); let react_dom = require("react-dom"); react_dom = require_runtime.__toESM(react_dom); //#region src/internal/FloatingMenu.tsx /** * Copyright IBM Corp. 2016, 2026 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ const DIRECTION_LEFT = "left"; const DIRECTION_RIGHT = "right"; const DIRECTION_BOTTOM = "bottom"; /** * Computes the floating menu's position based on the menu size, trigger element * position, offset, direction, and container. */ const getFloatingPosition = ({ menuSize, refPosition, offset, direction, scrollX, scrollY, container }) => { const { left: refLeft = 0, top: refTop = 0, right: refRight = 0, bottom: refBottom = 0 } = refPosition; const effectiveScrollX = container.position !== "static" ? 0 : scrollX; const effectiveScrollY = container.position !== "static" ? 0 : scrollY; 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 + effectiveScrollX - left - relativeDiff.left, top: refCenterVertical - height / 2 + effectiveScrollY + top - 9 - relativeDiff.top }), ["top"]: () => ({ left: refCenterHorizontal - width / 2 + effectiveScrollX + left - relativeDiff.left, top: refTop - height + effectiveScrollY - top - relativeDiff.top }), [DIRECTION_RIGHT]: () => ({ left: refRight + effectiveScrollX + left - relativeDiff.left, top: refCenterVertical - height / 2 + effectiveScrollY + top + 3 - relativeDiff.top }), [DIRECTION_BOTTOM]: () => ({ left: refCenterHorizontal - width / 2 + effectiveScrollX + left - relativeDiff.left, top: refBottom + effectiveScrollY + top - relativeDiff.top }) }[direction](); }; const FloatingMenu = ({ children, flipped, focusTrap, menuDirection = DIRECTION_BOTTOM, menuOffset = { top: 0, left: 0 }, menuRef: externalMenuRef, onPlace, selectorPrimaryFocus, styles, target = () => document.body, triggerRef, updateOrientation }) => { const prefix = (0, react.useContext)(require_usePrefix.PrefixContext); const [floatingPosition, setFloatingPosition] = (0, react.useState)(void 0); const menuBodyRef = (0, react.useRef)(null); const startSentinelRef = (0, react.useRef)(null); const endSentinelRef = (0, react.useRef)(null); const placeInProgressRef = (0, react.useRef)(false); const updateMenuPosition = (0, react.useCallback)((isAdjustment) => { const menuBody = menuBodyRef.current; if (!menuBody) { require_warning.warning(!!menuBody, "The DOM node for menu body for calculating its position is not available. Skipping..."); return; } const triggerEl = triggerRef.current; const menuSize = menuBody.getBoundingClientRect(); const refPosition = triggerEl ? triggerEl.getBoundingClientRect() : void 0; const offsetValue = typeof menuOffset === "function" ? menuOffset(menuBody, menuDirection, triggerEl, flipped) : menuOffset; const scrollX = globalThis.scrollX ?? 0; const scrollY = globalThis.scrollY ?? 0; if (updateOrientation) updateOrientation({ menuSize, refPosition, direction: menuDirection, offset: offsetValue, scrollX, scrollY, container: { rect: target().getBoundingClientRect(), position: getComputedStyle(target()).position } }); if (menuSize.width > 0 && menuSize.height > 0 || !offsetValue) { const newFloatingPosition = getFloatingPosition({ menuSize, refPosition: refPosition ?? { left: 0, top: 0, right: 0, bottom: 0 }, offset: offsetValue, direction: menuDirection, scrollX, scrollY, container: { rect: target().getBoundingClientRect(), position: getComputedStyle(target()).position } }); if (!floatingPosition || floatingPosition.left !== newFloatingPosition.left || floatingPosition.top !== newFloatingPosition.top) setFloatingPosition(newFloatingPosition); if (!isAdjustment) { const newMenuSize = menuBody.getBoundingClientRect(); if (newMenuSize.width !== menuSize.width || newMenuSize.height !== menuSize.height) updateMenuPosition(true); } } }, [ triggerRef, menuOffset, menuDirection, flipped, target, updateOrientation, floatingPosition ]); const focusMenuContent = (menuBody) => { const primaryFocusNode = selectorPrimaryFocus ? menuBody.querySelector(selectorPrimaryFocus) : null; const tabbableNode = menuBody.querySelector(require_navigation.selectorTabbable); const focusableNode = menuBody.querySelector(require_navigation.selectorFocusable); const focusTarget = primaryFocusNode || tabbableNode || focusableNode || menuBody; focusTarget.focus(); if (focusTarget === menuBody) require_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."); }; const handleMenuRef = (node) => { menuBodyRef.current = node; placeInProgressRef.current = !!node; if (externalMenuRef) externalMenuRef(node); if (node) updateMenuPosition(); }; (0, react.useEffect)(() => { if (placeInProgressRef.current && floatingPosition && menuBodyRef.current) { if (!menuBodyRef.current.contains(document.activeElement)) focusMenuContent(menuBodyRef.current); if (typeof onPlace === "function") onPlace(menuBodyRef.current); placeInProgressRef.current = false; } }, [floatingPosition, onPlace]); (0, react.useEffect)(() => { const resizeHandler = require_OptimizedResize.OptimizedResize.add(() => { updateMenuPosition(); }); return () => { resizeHandler.remove(); }; }, [ triggerRef, menuOffset, menuDirection, flipped, target, updateOrientation ]); (0, react.useEffect)(() => { updateMenuPosition(); }, [ menuOffset, menuDirection, flipped, triggerRef, target, updateOrientation ]); /** * Clones the child element to add a `ref` and positioning styles. */ const getChildrenWithProps = () => { const pos = floatingPosition; const positioningStyle = pos ? { left: `${pos.left}px`, top: `${pos.top}px`, right: "auto" } : { visibility: "hidden", top: "0px" }; return (0, react.cloneElement)(children, { ref: handleMenuRef, style: { ...styles, ...positioningStyle, position: "absolute", opacity: 1 } }); }; /** * Blur handler used when focus trapping is enabled. */ const handleBlur = (event) => { const { target, relatedTarget } = event; if (menuBodyRef.current && startSentinelRef.current && endSentinelRef.current && target instanceof HTMLElement && relatedTarget instanceof HTMLElement) require_wrapFocus.wrapFocus({ bodyNode: menuBodyRef.current, startTrapNode: startSentinelRef.current, endTrapNode: endSentinelRef.current, currentActiveNode: relatedTarget, oldActiveNode: target, prefix }); }; /** * Keydown handler for focus wrapping when experimental focus trap is enabled. */ const handleKeyDown = (event) => { if (require_match.match(event, require_keys.Tab) && menuBodyRef.current && event.target instanceof HTMLElement) require_wrapFocus.wrapFocusWithoutSentinels({ containerNode: menuBodyRef.current, currentActiveNode: event.target, event }); }; const deprecatedFlag = (0, _carbon_feature_flags.enabled)("enable-experimental-focus-wrap-without-sentinels"); const focusTrapWithoutSentinelsFlag = (0, _carbon_feature_flags.enabled)("enable-focus-wrap-without-sentinels"); const focusTrapWithoutSentinels = deprecatedFlag || focusTrapWithoutSentinelsFlag; if (typeof document !== "undefined") { const portalTarget = target ? target() : document.body; return react_dom.default.createPortal(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { onBlur: focusTrap && !focusTrapWithoutSentinels ? handleBlur : void 0, onKeyDown: focusTrapWithoutSentinels ? handleKeyDown : void 0, children: [ !focusTrapWithoutSentinels && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { ref: startSentinelRef, tabIndex: 0, role: "link", className: `${prefix}--visually-hidden`, children: "Focus sentinel" }), getChildrenWithProps(), !focusTrapWithoutSentinels && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { ref: endSentinelRef, tabIndex: 0, role: "link", className: `${prefix}--visually-hidden`, children: "Focus sentinel" }) ] }), portalTarget); } return null; }; //#endregion exports.DIRECTION_BOTTOM = DIRECTION_BOTTOM; exports.FloatingMenu = FloatingMenu;