@carbon/ibm-products-web-components
Version:
Carbon for IBM Products Web Components
369 lines (367 loc) • 16 kB
JavaScript
/**
* 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()}"
-custom-modal-beingclosed="${this.handleClose}"
-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"
=${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