UNPKG

@carbon/ibm-products-web-components

Version:
369 lines (367 loc) 16 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. */ import "../../globals/settings.js"; import { __decorate } from "../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorate.js"; import tearsheet_default$1 from "./tearsheet.scss.js"; import { MatchMediaController } from "../../globals/js/utils/match-media-controller.js"; import { blockClass, updateTearsheetSignals } from "./tearsheet-signal.js"; import { stackManager } from "./stack-signal.js"; import { clearFocusableContainers, trapFocus } from "../../utilities/manageFocusTrap/manageFocusTrap.js"; import { LitElement, html } from "lit"; import { property, query, state } from "lit/decorators.js"; import { carbonElement } from "@carbon/web-components/es/globals/decorators/carbon-element.js"; import { classMap } from "lit-html/directives/class-map.js"; import { breakpoints } from "@carbon/layout"; import { SignalWatcher } from "@lit-labs/signals"; import HostListenerMixin from "@carbon/web-components/es/globals/mixins/host-listener"; import { ifDefined } from "lit/directives/if-defined.js"; //#region src/components/tearsheet-preview/tearsheet.ts /** * @license * * Copyright IBM Corp. 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. */ /** * Tearsheet component - A slide-out panel for displaying detailed content. * * @element c4p-preview-tearsheet * @slot header - The header content of the tearsheet * @slot influencer - Optional left sidebar content (wide variant only) * @slot body - Main body content * @slot footer - Footer content with actions * @fires c4p-preview-tearsheet-beingclosed - Fired when the tearsheet is about to close * @fires c4p-preview-tearsheet-closed - Fired after the tearsheet has closed * @fires c4p-preview-tearsheet-collapse-change - Fired when the header collapse state changes. * `event.detail.collapsed` is `true` when collapsing, `false` when expanding. */ let CDSTearsheet = class CDSTearsheet extends SignalWatcher(HostListenerMixin(LitElement)) { constructor(..._args) { super(..._args); this.open = false; this.containerClassName = ""; this.influencerWidth = ""; this.summaryContentWidth = ""; this.verticalGap = ""; this.variant = "wide"; this.selectorsFloatingMenus = ""; this.selectorPrimaryFocus = ""; this.preventCloseOnClickOutside = false; this.uniqueId = `tearsheet-${Math.random().toString(36).substr(2, 9)}`; this._stackingEnabled = false; this.isSm = false; this._trapFocusAPI = null; this._wasOpen = false; this.smMediaQuery = `(max-width: ${breakpoints.md.width})`; this.isSmallDevice = new MatchMediaController(this, this.smMediaQuery, false); this.handleStackConnected = (event) => { event.stopPropagation(); this._stackingEnabled = true; }; this.handleStackStepSizeChanged = (event) => { event.stopPropagation(); if (this._stackingEnabled && this.open) this.updateStackProperties(); }; this.handleInfluencerSlotChange = (e) => { const slot = e.target; this.updateInfluencerVisibility(slot); }; this.handleHeaderCloseButtonClick = (event) => { event.stopPropagation(); this.closeTearsheet(); }; this.handleHeaderCollapseChange = (event) => { event.stopPropagation(); const { collapsed } = event.detail; this.dispatchEvent(new CustomEvent(this.constructor.eventCollapseChange, { bubbles: true, composed: true, detail: { collapsed } })); }; this.handleClose = (event) => { this.closeTearsheet(event, true); }; } /** * Checks if the tearsheet has a decorator (AI label or other) */ get hasDecorator() { const headerElement = this.querySelector(`c4p-tearsheet-header`); if (!headerElement) return false; return !!headerElement.querySelector("[slot=\"decorator\"]"); } /** * Checks if the tearsheet has an AI label decorator */ get hasAILabel() { const headerElement = this.querySelector(`c4p-tearsheet-header`); if (!headerElement) return false; const decorator = headerElement.querySelector("[slot=\"decorator\"]"); if (!decorator) return false; const tagName = decorator.tagName.toLowerCase(); return tagName === "cds-custom-ai-label" || tagName === `c4p-ai-label`; } connectedCallback() { super.connectedCallback(); this.addEventListener(`c4p-tearsheet-stack-connected`, this.handleStackConnected); this.addEventListener(`c4p-tearsheet-stack-step-size-changed`, this.handleStackStepSizeChanged); this._checkForStackWrapper(); if (this.open) this.classList.add("is-visible"); else this.classList.remove("is-visible"); this.addEventListener(`c4p-tearsheet-header-close-button-clicked`, this.handleHeaderCloseButtonClick); this.addEventListener(`c4p-tearsheet-header-collapse-change`, this.handleHeaderCollapseChange); } firstUpdated(_changedProperties) { this.updateCSSCustomProperties(); this.isSm = this.isSmallDevice?.matches || this.variant === "narrow"; updateTearsheetSignals({ variant: this.variant, isSm: this.isSm, open: this.open, hasAILabel: this.hasAILabel, uniqueId: this.uniqueId, ...this._readHeaderProps(), onClose: () => this.closeTearsheet() }); } /** Read close-button props from the slotted c4p-tearsheet-header element */ _readHeaderProps() { const header = this.querySelector(`c4p-tearsheet-header`); return { closeIconDescription: header?.closeIconDescription ?? header?.getAttribute("close-icon-description") ?? "Close", hideCloseButton: header?.hideCloseButton ?? header?.hasAttribute("hide-close-button") ?? false }; } updated(_changedProperties) { this.updateIsSmState(_changedProperties); this.handleOpenPropertyChange(_changedProperties); this.updateCSSPropertiesIfNeeded(_changedProperties); if (_changedProperties.has("variant")) updateTearsheetSignals({ variant: this.variant }); if (_changedProperties.has("isSm")) this.updateInfluencerVisibility(); this.updateStackPropertiesIfNeeded(); } updateIsSmState(_changedProperties) { const previousIsSm = this.isSm; this.isSm = this.isSmallDevice?.matches || this.variant === "narrow"; if (this.isSm !== previousIsSm) updateTearsheetSignals({ isSm: this.isSm }); } handleOpenPropertyChange(_changedProperties) { if (!_changedProperties.has("open")) return; const wasOpen = this._wasOpen; const isOpen = this.open; updateTearsheetSignals({ open: this.open }); if (this._stackingEnabled && this.modalBodyElement) stackManager.notifyStack(this.uniqueId, this.open, this.modalBodyElement); this.classList.toggle("is-visible", this.open); if (this._stackingEnabled) this.updateStackProperties(); if (!wasOpen && isOpen) { updateTearsheetSignals({ uniqueId: this.uniqueId }); requestAnimationFrame(() => { this._trapFocusAPI = trapFocus(this, this.uniqueId, () => this._getFirstFocusable()); }); } this._wasOpen = isOpen; if (!this.open && this.launcherButtonRef) setTimeout(() => { if (this.launcherButtonRef instanceof HTMLElement) { const headerActionItem = this.launcherButtonRef.closest(`.${blockClass}__header-action-item`); if (headerActionItem) { const menuButton = headerActionItem.closest(`.${blockClass}__content__header-actions`)?.querySelector(`.${blockClass}__header-actions-menuButton:not(.${blockClass}__header-actions-menuButton--hidden) button`); if (menuButton instanceof HTMLElement) menuButton.focus(); else this.launcherButtonRef.focus(); } else this.launcherButtonRef.focus(); } }, 100); } updateCSSPropertiesIfNeeded(_changedProperties) { if (_changedProperties.has("influencerWidth") || _changedProperties.has("summaryContentWidth") || _changedProperties.has("verticalGap")) this.updateCSSCustomProperties(); } updateStackPropertiesIfNeeded() { if (!this._stackingEnabled) return; if (stackManager.state.stack.length > 0) this.updateStackProperties(); } /** * Update CSS custom properties for stacking */ updateStackProperties() { const stackState = stackManager.state; const depth = stackManager.getDepth(this.uniqueId); const scaleFactor = stackManager.getScaleFactor(this.uniqueId); const blockSizeChange = stackManager.getBlockSizeChange(this.uniqueId); if (stackState.stack.length > 1) this.classList.add(`${blockClass}--stack-activated`); else this.classList.remove(`${blockClass}--stack-activated`); if (depth !== -1) { this.style.setProperty("--stack-depth", depth.toString()); this.style.setProperty("--scale-factor", scaleFactor.toString()); this.style.setProperty("--block-size-change", blockSizeChange); } } /** * Delegates to `c4p-tearsheet-header-content.getFirstFocusable()`. * All priority logic lives in the component that owns the relevant DOM. */ _getFirstFocusable() { return this.querySelector(`c4p-tearsheet-header-content`)?.getFirstFocusable() ?? null; } disconnectedCallback() { super.disconnectedCallback(); this._trapFocusAPI?.cleanup(); clearFocusableContainers(); this.removeEventListener(`c4p-tearsheet-header-close-button-clicked`, this.handleHeaderCloseButtonClick); this.removeEventListener(`c4p-tearsheet-stack-connected`, this.handleStackConnected); this.removeEventListener(`c4p-tearsheet-stack-step-size-changed`, this.handleStackStepSizeChanged); if (this._stackingEnabled) stackManager.notifyStack(this.uniqueId, false, null); if (this.influencerWidth) document.documentElement.style.removeProperty("--tearsheet-influencer-width"); if (this.summaryContentWidth) document.documentElement.style.removeProperty("--tearsheet-summary-content-width"); if (this.verticalGap) document.documentElement.style.removeProperty("--tearsheet-vertical-gap"); } /** * Update CSS custom properties for dynamic styling */ updateCSSCustomProperties() { if (this.influencerWidth) document.documentElement.style.setProperty("--tearsheet-influencer-width", this.influencerWidth); if (this.summaryContentWidth) document.documentElement.style.setProperty("--tearsheet-summary-content-width", this.summaryContentWidth); if (this.verticalGap) document.documentElement.style.setProperty("--tearsheet-vertical-gap", this.verticalGap); } /** * Update influencer visibility based on slot content and screen size * Handles both slot changes and screen size changes */ updateInfluencerVisibility(slot) { const influencerSlot = slot || this.shadowRoot?.querySelector("slot[name=\"influencer\"]"); if (!influencerSlot) return; const shouldShow = influencerSlot.assignedNodes({ flatten: true }).length > 0 && !this.isSm; if (this.modalBodyElement) if (shouldShow) this.modalBodyElement.classList.add(`${blockClass}__body-layout--has-influencer`); else this.modalBodyElement.classList.remove(`${blockClass}__body-layout--has-influencer`); } /** * Check if this tearsheet is wrapped in a stack provider */ _checkForStackWrapper() { let parent = this.parentElement; while (parent) { if (parent.tagName.toLowerCase() === `c4p-tearsheet-stack`) { this._stackingEnabled = true; return; } parent = parent.parentElement; } this._stackingEnabled = false; } /** * Common method to handle tearsheet close with proper event dispatching * @param originalEvent - The original event that triggered the close (optional) * @param useAsync - Whether to dispatch closed event asynchronously */ closeTearsheet(originalEvent, useAsync = false) { const beforeCloseEvent = new CustomEvent(`c4p-preview-tearsheet-beingclosed`, { bubbles: true, cancelable: true, composed: true, detail: {} }); if (!this.dispatchEvent(beforeCloseEvent)) { if (originalEvent) originalEvent.preventDefault(); return; } this.open = false; const dispatchClosedEvent = () => { this.dispatchEvent(new CustomEvent(`c4p-preview-tearsheet-closed`, { bubbles: true, composed: true, detail: {} })); }; if (useAsync) Promise.resolve().then(dispatchClosedEvent); else dispatchClosedEvent(); } /** * Parse floating menu selectors from comma-separated string */ getFloatingMenuSelectors() { const defaultSelectors = [ `.c4p--overflow-menu-options`, `.c4p--tooltip`, ".flatpickr-calendar", `.${blockClass}__container`, `.c4p--menu` ]; const customSelectors = this.selectorsFloatingMenus ? this.selectorsFloatingMenus.split(",").map((s) => s.trim()) : []; return [...defaultSelectors, ...customSelectors].join(","); } render() { const classes = classMap({ [blockClass]: true, [`${blockClass}--wide`]: this.variant === "wide", [`${blockClass}--narrow`]: this.variant === "narrow", [`${blockClass}--has-ai-label`]: this.hasAILabel, [`${blockClass}--has-decorator`]: this.hasDecorator && !this.hasAILabel }); const containerClasses = `${blockClass}__container ${this.containerClassName}`; const computedAriaLabelledby = !this.ariaLabel && this.headerContentElement?.titleId ? this.headerContentElement.titleId : void 0; return html`<cds-custom-modal class=${classes} size=${this.variant === "narrow" ? "sm" : "lg"} ?open="${this.open}" container-class="${containerClasses}" ?prevent-close-on-click-outside="${this.preventCloseOnClickOutside}" aria-label="${ifDefined(this.ariaLabel || void 0)}" aria-labelledby="${ifDefined(computedAriaLabelledby)}" selector-primary-focus="${ifDefined(this.selectorPrimaryFocus || void 0)}" selectors-floating-menus="${this.getFloatingMenuSelectors()}" @cds-custom-modal-beingclosed="${this.handleClose}" @cds-custom-modal-closed="${this.handleClose}" ?full-width="${true}" ai-label="${ifDefined(this.hasAILabel || void 0)}" > <slot name="header"></slot> <cds-custom-modal-body class="${blockClass}__body-layout"> <slot name="influencer" @slotchange=${this.handleInfluencerSlotChange} ></slot> <slot name="body"></slot> <slot name="footer"></slot> </cds-custom-modal-body> </cds-custom-modal>`; } static { this.styles = tearsheet_default$1; } /** * Public event fired when the header collapse state changes. * `event.detail.collapsed` is `true` when collapsing, `false` when expanding. */ static get eventCollapseChange() { return `c4p-preview-tearsheet-collapse-change`; } }; __decorate([property({ type: Boolean, reflect: true })], CDSTearsheet.prototype, "open", void 0); __decorate([property({ attribute: "container-class-name" })], CDSTearsheet.prototype, "containerClassName", void 0); __decorate([property({ attribute: "influencer-width" })], CDSTearsheet.prototype, "influencerWidth", void 0); __decorate([property({ attribute: "summary-content-width" })], CDSTearsheet.prototype, "summaryContentWidth", void 0); __decorate([property({ attribute: "vertical-gap" })], CDSTearsheet.prototype, "verticalGap", void 0); __decorate([property({ reflect: true })], CDSTearsheet.prototype, "variant", void 0); __decorate([property({ attribute: "selectors-floating-menus" })], CDSTearsheet.prototype, "selectorsFloatingMenus", void 0); __decorate([property({ attribute: "selector-primary-focus" })], CDSTearsheet.prototype, "selectorPrimaryFocus", void 0); __decorate([property({ type: Boolean, attribute: "prevent-close-on-click-outside" })], CDSTearsheet.prototype, "preventCloseOnClickOutside", void 0); __decorate([property({ attribute: false })], CDSTearsheet.prototype, "launcherButtonRef", void 0); __decorate([state()], CDSTearsheet.prototype, "isSm", void 0); __decorate([query("cds-custom-modal-body")], CDSTearsheet.prototype, "modalBodyElement", void 0); __decorate([query(`c4p-tearsheet-header-content`)], CDSTearsheet.prototype, "headerContentElement", void 0); CDSTearsheet = __decorate([carbonElement(`c4p-preview-tearsheet`)], CDSTearsheet); var tearsheet_default = CDSTearsheet; //#endregion export { tearsheet_default as default }; //# sourceMappingURL=tearsheet.js.map