UNPKG

@carbon/ibm-products

Version:
478 lines (476 loc) 20.4 kB
/** * Copyright IBM Corp. 2020, 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_index = require("../../node_modules/classnames/index.js"); const require_pconsole = require("../../global/js/utils/pconsole.js"); const require_settings = require("../../settings.js"); const require_useIsomorphicEffect = require("../../global/js/hooks/useIsomorphicEffect.js"); const require_usePreviousValue = require("../../global/js/hooks/usePreviousValue.js"); const require_useFocus = require("../../global/js/hooks/useFocus.js"); const require_props_helper = require("../../global/js/utils/props-helper.js"); const require_usePortalTarget = require("../../global/js/hooks/usePortalTarget.js"); const require_useResizeObserver = require("../../global/js/hooks/useResizeObserver.js"); const require_ActionSet = require("../ActionSet/ActionSet.js"); const require_getNodeTextContent = require("../../global/js/utils/getNodeTextContent.js"); const require_Wrap = require("../../global/js/utils/Wrap.js"); const require_useMergedRefs = require("../../global/js/hooks/useMergedRefs.js"); const require_useId = require("../../global/js/utils/useId.js"); const require_TearsheetPresence = require("./TearsheetPresence.js"); let react = require("react"); react = require_runtime.__toESM(react); let prop_types = require("prop-types"); prop_types = require_runtime.__toESM(prop_types); let _carbon_react = require("@carbon/react"); let _carbon_react_icons = require("@carbon/react/icons"); //#region src/components/Tearsheet/TearsheetShell.tsx /** * Copyright IBM Corp. 2020, 2024 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ var import_classnames = /* @__PURE__ */ require_runtime.__toESM(require_index.default); const bc = `${require_settings.pkg.prefix}--tearsheet`; const componentName = "TearsheetShell"; const maxDepth = 3; const stack = { open: [], all: [], sizes: [] }; const tearsheetShellWideProps = [ "headerActions", "influencer", "influencerPosition", "influencerWidth", "navigation" ]; const tearsheetIsPassive = (actions) => !actions || !actions?.length; const tearsheetHasCloseIcon = (actions, hasCloseIcon) => hasCloseIcon ?? tearsheetIsPassive(actions); /** * Since the Tearsheet has an H3 heading, any headings inside the Tearsheet should start at H4. * This is a helper to do that. */ const SectionLevel3 = ({ children, ...rest }) => /* @__PURE__ */ react.default.createElement(_carbon_react.Section, { level: 3, ...rest }, children); /** * TearSheetShell is used internally by TearSheet and TearSheetNarrow * * The component is not public. * * See the canvas tab for the component API details. * */ const TearsheetShell = react.default.forwardRef(({ open, ...props }, ref) => { const id = require_useId.useId(); const enablePresence = (0, _carbon_react.useFeatureFlag)("enable-presence"); const hasPresenceContext = Boolean((0, react.useContext)(require_TearsheetPresence.TearsheetPresenceContext)); const hasPresenceOptIn = enablePresence || hasPresenceContext; const exclusivePresenceContext = require_TearsheetPresence.useExclusiveTearsheetPresenceContext(id); if (hasPresenceOptIn && !exclusivePresenceContext) return /* @__PURE__ */ react.default.createElement(require_TearsheetPresence.TearsheetPresence, { open: open ?? false, _presenceId: id, _autoEnablePresence: hasPresenceContext }, /* @__PURE__ */ react.default.createElement(TearsheetShellDialog, { open: true, ref, ...props })); return /* @__PURE__ */ react.default.createElement(TearsheetShellDialog, { ref, open, ...props }); }); const TearsheetShellDialog = react.default.forwardRef(({ actions, decorator, ariaLabel, children, className, closeIconDescription = "Close", currentStep, description, hasCloseIcon, hasError, headerActions, influencer, influencerPosition, influencerWidth, label, navigation, onClose, open: externalOpen, portalTarget: portalTargetIn, selectorPrimaryFocus, selectorsFloatingMenus = [], size, slug: deprecated_slug, title, verticalPosition, launcherButtonRef, ...rest }, ref) => { const carbonPrefix = (0, _carbon_react.usePrefix)(); const bcModalHeader = `${carbonPrefix}--modal-header`; const renderPortalUse = require_usePortalTarget.usePortalTarget(portalTargetIn); const localRef = (0, react.useRef)(void 0); const resizer = (0, react.useRef)(null); const modalBodyRef = (0, react.useRef)(null); const modalRef = ref || localRef; const { width } = require_useResizeObserver.useResizeObserver(resizer); const { keyDownListener, claimFocus } = require_useFocus.useFocus(modalRef, selectorPrimaryFocus); modalRef.current; const wide = size === "wide"; const presenceContext = (0, react.useContext)(require_TearsheetPresence.TearsheetPresenceContext); const mergedRefs = require_useMergedRefs.useMergedRefs([modalRef, presenceContext?.presenceRef]); const enablePresence = (0, _carbon_react.useFeatureFlag)("enable-presence") || presenceContext?.autoEnablePresence; const open = externalOpen || enablePresence; const prevOpen = require_usePreviousValue.usePreviousValue(open); const [depth, setDepth] = (0, react.useState)(0); const [position, setPosition] = (0, react.useState)(0); const prevDepth = (0, react.useRef)(void 0); (0, react.useEffect)(() => { prevDepth.current = depth; }); const isPassive = tearsheetIsPassive(actions); const effectiveHasCloseIcon = tearsheetHasCloseIcon(actions, hasCloseIcon); function handleStackChange(newDepth, newPosition) { setDepth(newDepth); setPosition(newPosition); } (0, react.useEffect)(() => { if (open) claimFocus(); }, [ open, currentStep, effectiveHasCloseIcon, claimFocus ]); (0, react.useEffect)(() => { if (!enablePresence && prevOpen && !open && launcherButtonRef?.current) setTimeout(() => { launcherButtonRef?.current?.focus(); }, 10); }, [ enablePresence, open, prevOpen, launcherButtonRef ]); (0, react.useEffect)(() => { const launcherButton = launcherButtonRef?.current; if (!enablePresence || !launcherButton) return; return () => { setTimeout(() => { launcherButton.focus(); }, 10); }; }, [enablePresence, launcherButtonRef]); (0, react.useEffect)(() => { requestAnimationFrame(() => { if (open && depth === position && !modalRef?.current?.contains(document.activeElement)) claimFocus(); }); }, [ claimFocus, depth, modalRef, position ]); (0, react.useEffect)(() => { if (hasError && !modalRef?.current?.contains(document.activeElement)) claimFocus(); }, [ claimFocus, hasError, modalRef ]); (0, react.useEffect)(() => { const isPresent = open && !presenceContext?.isExiting; const notify = () => stack.all.forEach((handler) => { handler(Math.min(stack.open.length, maxDepth), stack.open.indexOf(handler) + 1); }); stack.all.push(handleStackChange); stack.sizes.push(size); if (isPresent) { stack.open.push(handleStackChange); notify(); } return function cleanup() { stack.all.splice(stack.all.indexOf(handleStackChange), 1); stack.sizes.splice(stack.sizes.indexOf(size), 1); const openIndex = stack.open.indexOf(handleStackChange); if (openIndex >= 0) { stack.open.splice(openIndex, 1); notify(); } }; }, [ open, presenceContext?.isExiting, size ]); const areAllSameSizeVariant = () => new Set(stack.sizes).size === 1; require_useIsomorphicEffect.useIsomorphicEffect(() => { const setScaleValues = () => { if (!areAllSameSizeVariant()) return { [`--${bc}--stacking-scale-factor-single`]: 1, [`--${bc}--stacking-scale-factor-double`]: 1 }; return { [`--${bc}--stacking-scale-factor-single`]: (width - 32) / width, [`--${bc}--stacking-scale-factor-double`]: (width - 64) / width }; }; if (modalRef.current) Object.entries(setScaleValues()).map(([key, value]) => { modalRef.current.style.setProperty(key, String(value)); }); }, [modalRef, width]); if (position <= depth) { const includeHeader = label || title || description || headerActions || navigation || effectiveHasCloseIcon; const includeActions = actions && actions?.length > 0; const areAllSameSizeVariant = () => new Set(stack.sizes).size === 1; const normalizedDecorator = (0, react.isValidElement)(decorator) && decorator.type === _carbon_react.AILabel ? (0, react.cloneElement)(decorator, { size: "sm" }) : decorator; return renderPortalUse(/* @__PURE__ */ react.default.createElement(_carbon_react.unstable_FeatureFlags, { enableExperimentalFocusWrapWithoutSentinels: true }, /* @__PURE__ */ react.default.createElement(_carbon_react.ComposedModal, { ...rest, "aria-label": ariaLabel || require_getNodeTextContent.getNodeTextContent(title), className: (0, import_classnames.default)(bc, className, { [`${bc}--stacked-${position}-of-${depth}`]: depth > 1 || depth === 1 && (prevDepth?.current ?? 0) > 1, [`${bc}--wide`]: wide, [`${bc}--narrow`]: !wide, [`${bc}--has-slug`]: deprecated_slug, [`${bc}--has-ai-label`]: !!decorator && decorator["type"]?.displayName === "AILabel", [`${bc}--has-decorator`]: !!decorator && decorator["type"]?.displayName !== "AILabel", [`${bc}--has-close`]: effectiveHasCloseIcon, ["is-visible"]: enablePresence, [`${bc}--tearsheet-enable-presence`]: presenceContext?.autoEnablePresence }), containerClassName: (0, import_classnames.default)(`${bc}__container`, { [`${bc}__container--lower`]: verticalPosition === "lower", [`${bc}__container--mixed-size-stacking`]: !areAllSameSizeVariant() }), onClose, open, selectorPrimaryFocus, onKeyDown: keyDownListener, preventCloseOnClickOutside: !isPassive, ref: mergedRefs, selectorsFloatingMenus: [ `.${carbonPrefix}--overflow-menu-options`, `.${carbonPrefix}--tooltip`, ".flatpickr-calendar", `.${bc}__container`, `.${carbonPrefix}--menu`, ...selectorsFloatingMenus ], size: "sm", "data-tearsheet-exiting": presenceContext?.isExiting || void 0 }, includeHeader && /* @__PURE__ */ react.default.createElement(_carbon_react.ModalHeader, { className: (0, import_classnames.default)(`${bc}__header`, { [`${bc}__header--with-close-icon`]: effectiveHasCloseIcon, [`${bc}__header--with-nav`]: navigation }), closeClassName: (0, import_classnames.default)(`${bc}__header--no-close-icon`) }, headerActions && /* @__PURE__ */ react.default.createElement("div", { className: `${bc}__header-actions` }, headerActions), (decorator || deprecated_slug) && /* @__PURE__ */ react.default.createElement("div", { className: `${bc}__decorator` }, normalizedDecorator || deprecated_slug), effectiveHasCloseIcon && /* @__PURE__ */ react.default.createElement("div", { className: `${bc}__close-button ${carbonPrefix}--modal-close-button` }, /* @__PURE__ */ react.default.createElement(_carbon_react.IconButton, { className: `${carbonPrefix}--modal-close`, label: closeIconDescription, onClick: onClose, align: "left" }, /* @__PURE__ */ react.default.createElement(_carbon_react_icons.Close, { size: 20, "aria-hidden": "true", tabIndex: "-1", className: `${carbonPrefix}--modal-close__icon` }))), /* @__PURE__ */ react.default.createElement(require_Wrap.Wrap, { className: `${bc}__header-content`, element: wide ? _carbon_react.Layer : void 0 }, /* @__PURE__ */ react.default.createElement(require_Wrap.Wrap, { className: `${bc}__header-fields` }, /* @__PURE__ */ react.default.createElement(require_Wrap.Wrap, { className: `${bcModalHeader}__label` }, label), /* @__PURE__ */ react.default.createElement(require_Wrap.Wrap, { element: "h3", className: (0, import_classnames.default)(`${bcModalHeader}__heading`, `${bc}__heading`) }, title), /* @__PURE__ */ react.default.createElement(require_Wrap.Wrap, { className: `${bc}__header-description` }, description))), /* @__PURE__ */ react.default.createElement(require_Wrap.Wrap, { className: `${bc}__header-navigation` }, navigation)), /* @__PURE__ */ react.default.createElement(require_Wrap.Wrap, { ref: modalBodyRef, className: `${carbonPrefix}--modal-content ${bc}__body` }, /* @__PURE__ */ react.default.createElement(require_Wrap.Wrap, { className: (0, import_classnames.default)({ [`${bc}__influencer`]: true, [`${bc}__influencer--wide`]: influencerWidth === "wide" }), neverRender: influencerPosition === "right", element: SectionLevel3 }, /* @__PURE__ */ react.default.createElement(require_Wrap.Wrap, { element: _carbon_react.Layer, className: `${bc}__layer` }, influencer)), /* @__PURE__ */ react.default.createElement(require_Wrap.Wrap, { className: `${bc}__right` }, /* @__PURE__ */ react.default.createElement(require_Wrap.Wrap, { className: `${bc}__main`, alwaysRender: includeActions }, /* @__PURE__ */ react.default.createElement(require_Wrap.Wrap, { className: `${bc}__content`, alwaysRender: !!(influencer && influencerPosition === "right"), element: SectionLevel3 }, wide ? children : /* @__PURE__ */ react.default.createElement(require_Wrap.Wrap, { element: _carbon_react.Layer, className: `${bc}__layer` }, children)), /* @__PURE__ */ react.default.createElement(require_Wrap.Wrap, { className: (0, import_classnames.default)({ [`${bc}__influencer`]: true, [`${bc}__influencer--wide`]: influencerWidth === "wide" }), neverRender: influencerPosition !== "right", element: SectionLevel3 }, /* @__PURE__ */ react.default.createElement(require_Wrap.Wrap, { element: _carbon_react.Layer, className: `${bc}__layer` }, influencer))), includeActions && /* @__PURE__ */ react.default.createElement(require_Wrap.Wrap, { className: `${bc}__button-container` }, /* @__PURE__ */ react.default.createElement(require_ActionSet.ActionSet, { actions, buttonSize: wide ? "2xl" : void 0, className: `${bc}__buttons`, size: wide ? "2xl" : "lg", "aria-hidden": !open })))), /* @__PURE__ */ react.default.createElement("div", { className: `${bc}__resize-detector`, ref: resizer })))); } else { require_pconsole.default.warn("Tearsheet not rendered: maximum stacking depth exceeded."); return null; } }); TearsheetShell.displayName = componentName; const portalType = typeof Element === "undefined" ? prop_types.default.object : prop_types.default.instanceOf(Element); const deprecatedProps = { /** * @deprecated Property replaced by `decorator` */ slug: require_props_helper.deprecateProp(prop_types.default.node, "Property replaced by `decorator`"), /** * **Deprecated** * * The position of the top of tearsheet in the viewport. The 'normal' * position is a short distance down from the top of the * viewport, leaving room at the top for a global header bar to show through * from below. The 'lower' position (the default) provides a little extra room at the top * to allow an action bar navigation or breadcrumbs to also show through. */ verticalPosition: prop_types.default.oneOf(["normal", "lower"]) }; TearsheetShell.propTypes = { /** * The actions to be shown as buttons in the action area at the bottom of the * tearsheet. Each action is specified as an object with optional fields * 'label' to supply the button label, 'kind' to select the button kind (must * be 'primary', 'secondary' or 'ghost'), 'loading' to display a loading * indicator, and 'onClick' to receive notifications when the button is * clicked. Additional fields in the object will be passed to the Button * component, and these can include 'disabled', 'ref', 'className', and any * other Button props. Any other fields in the object will be passed through * to the button element as HTML attributes. * * See https://react.carbondesignsystem.com/?path=/docs/components-button--default#component-api */ /**@ts-ignore*/ actions: prop_types.default.arrayOf(prop_types.default.shape({ /**@ts-ignore*/ ..._carbon_react.Button.propTypes, kind: prop_types.default.oneOf([ "ghost", "danger--ghost", "secondary", "danger", "primary" ]), label: prop_types.default.string, loading: prop_types.default.bool, /**@ts-ignore*/ onClick: _carbon_react.Button.propTypes.onClick })), /** * The main content of the tearsheet. */ children: prop_types.default.node, /** * An optional class or classes to be added to the outermost element. */ className: prop_types.default.string, /** * The accessibility title for the close icon (if shown). * * **Note:** This prop is only required if a close icon is shown, i.e. if * there are a no navigation actions and/or hasCloseIcon is true. */ /**@ts-ignore*/ closeIconDescription: prop_types.default.string, /** * Optional prop that allows you to pass any component. */ decorator: prop_types.default.oneOfType([prop_types.default.node, prop_types.default.bool]), /** * A description of the flow, displayed in the header area of the tearsheet. */ description: prop_types.default.node, /** * Enable a close icon ('x') in the header area of the tearsheet. By default, * (when this prop is omitted, or undefined or null) a tearsheet does not * display a close icon if there are navigation actions ("transactional * tearsheet") and displays one if there are no navigation actions ("passive * tearsheet"), and that behavior can be overridden if required by setting * this prop to either true or false. */ /**@ts-ignore*/ hasCloseIcon: prop_types.default.bool, /** * The content for the header actions area, displayed alongside the title in * the header area of the tearsheet. This is typically a drop-down, or a set * of small buttons, or similar. NB the headerActions is only applicable for * wide tearsheets. */ headerActions: prop_types.default.element, /** * The content for the influencer section of the tearsheet, displayed * alongside the main content. This is typically a menu, or filter, or * progress indicator, or similar. NB the influencer is only applicable for * wide tearsheets. */ influencer: prop_types.default.element, /** * The position of the influencer section, 'left' or 'right'. */ influencerPosition: prop_types.default.oneOf(["left", "right"]), /** * The width of the influencer: 'narrow' (the default) is 256px, and 'wide' * is 320px. */ influencerWidth: prop_types.default.oneOf(["narrow", "wide"]), /** * A label for the tearsheet, displayed in the header area of the tearsheet * to maintain context for the tearsheet (e.g. as the title changes from page * to page of a multi-page task). */ label: prop_types.default.node, /** * Provide a ref to return focus to once the tearsheet is closed. */ /**@ts-ignore */ launcherButtonRef: prop_types.default.any, /** * Navigation content, such as a set of tabs, to be displayed at the bottom * of the header area of the tearsheet. NB the navigation is only applicable * for wide tearsheets. */ navigation: prop_types.default.element, /** * An optional handler that is called when the user closes the tearsheet (by * clicking the close button, if enabled, or clicking outside, if enabled). * Returning `false` here prevents the modal from closing. */ onClose: prop_types.default.func, /** * Specifies whether the tearsheet is currently open. */ open: prop_types.default.bool, /** * The DOM element that the tearsheet should be rendered within. Defaults to document.body. */ /**@ts-ignore*/ portalTarget: portalType, /** * Specify a CSS selector that matches the DOM element that should be * focused when the Modal opens. */ selectorPrimaryFocus: prop_types.default.string, /** * Specify the CSS selectors that match the floating menus. * * See https://react.carbondesignsystem.com/?path=/docs/components-composedmodal--overview#focus-management */ /**@ts-ignore*/ selectorsFloatingMenus: prop_types.default.arrayOf(prop_types.default.string), /** * Specifies the width of the tearsheet, 'narrow' or 'wide'. */ /**@ts-ignore*/ size: prop_types.default.oneOf(["narrow", "wide"]).isRequired, /** * The main title of the tearsheet, displayed in the header area. */ title: prop_types.default.node, ...deprecatedProps }; //#endregion exports.TearsheetShell = TearsheetShell; exports.portalType = portalType; exports.tearsheetShellWideProps = tearsheetShellWideProps;