UNPKG

@carbon/ibm-products-web-components

Version:
533 lines (527 loc) 20.3 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 pconsole_default from "../../globals/internal/pconsole.js"; import { __decorate } from "../../_virtual/_@oxc-project_runtime@0.127.0/helpers/decorate.js"; import tearsheet_default$1 from "./tearsheet.scss.js"; import { TEARSHEET_INFLUENCER_PLACEMENT, TEARSHEET_INFLUENCER_WIDTH, TEARSHEET_WIDTH } from "./defs.js"; import { LitElement, html } from "lit"; import { property, query, queryAssignedElements, state } from "lit/decorators.js"; import '@carbon/web-components/es-custom/components/button/index.js'; import HostListener from "@carbon/web-components/es/globals/decorators/host-listener.js"; import HostListenerMixin from "@carbon/web-components/es/globals/mixins/host-listener.js"; import { selectorTabbable } from "@carbon/web-components/es/globals/settings.js"; import { carbonElement } from "@carbon/web-components/es/globals/decorators/carbon-element.js"; import '@carbon/web-components/es-custom/components/layer/index.js'; import '@carbon/web-components/es-custom/components/button/button-set-base.js'; import '@carbon/web-components/es-custom/components/modal/index.js'; //#region src/components/tearsheet/tearsheet.ts /** * @license * * Copyright IBM Corp. 2023, 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 _CDSTearsheet; const maxStackDepth = 3; const blockClass = `c4p--tearsheet`; const blockClassModalHeader = `cds-custom--modal-header`; const blockClassActionSet = `c4p--action-set`; /** * Tearsheet. * * @element c4p-tearsheet * @csspart dialog The dialog. * @fires c4p-tearsheet-beingclosed * The custom event fired before this tearsheet is being closed upon a user gesture. * Cancellation of this event stops the user-initiated action of closing this tearsheet. * @fires c4p-tearsheet-closed - The custom event fired after this tearsheet is closed upon a user gesture. */ let CDSTearsheet = class CDSTearsheet extends HostListenerMixin(LitElement) { static { _CDSTearsheet = this; } constructor(..._args) { super(..._args); this._launcher = null; this._actionsCount = 0; this._hasHeaderActions = false; this._hasLabel = false; this._hasSlug = false; this._hasDecorator = false; this._hasAILabel = false; this._hasTitle = false; this._hasDescription = false; this._hasInfluencerLeft = false; this._hasInfluencerRight = false; this._isOpen = false; this._hasHeaderNavigation = false; this._handleClick = (event) => { if (event.composedPath().indexOf(this.shadowRoot) < 0 && !this.preventCloseOnClickOutside) this._handleUserInitiatedClose(event.target); }; this._handleKeydown = ({ key, target }) => { if ((key === "Esc" || key === "Escape") && this._topOfStack()) this._handleUserInitiatedClose(target); }; this._handleHostKeydown = (event) => { if (event.key === "Tab") { const { first: _firstElement, last: _lastElement } = this.getFocusable(); if (event.shiftKey && (this.shadowRoot?.activeElement === _firstElement || document.activeElement === _firstElement)) { event.preventDefault(); _lastElement?.focus(); } else if (!event.shiftKey && document.activeElement === _lastElement) { event.preventDefault(); _firstElement?.focus(); } } }; this.ariaLabel = ""; this.closeIconDescription = "Close"; this.hasCloseIcon = false; this.influencerPlacement = "right"; this.influencerWidth = "narrow"; this.open = false; this.preventCloseOnClickOutside = false; this.width = "narrow"; this._checkUpdateActionSizes = () => { if (this._actions) for (let i = 0; i < this._actions.length; i++) this._actions[i].setAttribute("size", this.width === "wide" ? "2xl" : "xl"); }; this._maxActions = 4; this._stackDepth = -1; this._stackPosition = -1; this._topOfStack = () => { return this._stackDepth === this._stackPosition; }; this._notifyStack = () => { _CDSTearsheet._stack.all.forEach((handler) => { handler(Math.min(_CDSTearsheet._stack.open.length, maxStackDepth), _CDSTearsheet._stack.open.indexOf(handler) + 1); }); }; this._handleStackChange = (newDepth, newPosition) => { this._stackDepth = newDepth; this._stackPosition = newPosition; if (this._stackDepth > 1 && this._stackPosition > 0) { this.setAttribute("stack-position", `${newPosition}`); this.setAttribute("stack-depth", `${this._stackDepth}`); } else { this.removeAttribute("stack-position"); this.removeAttribute("stack-depth"); } }; this._updateStack = () => { if (this.open) _CDSTearsheet._stack.open.push(this._handleStackChange); else { const indexOpen = _CDSTearsheet._stack.open.indexOf(this._handleStackChange); if (indexOpen >= 0) _CDSTearsheet._stack.open.splice(indexOpen, 1); } this._notifyStack(); }; this.actionsMultiple = [ "", "single", "double", "triple" ][this._actionsCount]; this._checkSetOpen = () => { const { _tearsheet: tearsheet } = this; if (tearsheet && this._isOpen) tearsheet.addEventListener("transitionend", () => { this._isOpen = false; }); else window.requestAnimationFrame(() => { this._isOpen = this.open; }); }; } /** * Get focusable elements. * * Querying all tabbable items. * * @returns {{first: HTMLElement, last: HTMLElement, all: HTMLElement[]}} Returns an object with various elements. */ getFocusable() { const elements = []; if (this._hasSlug) elements.push(this.querySelectorAll(`cds-custom-slug`)); if (this.hasCloseIcon) { const closeButtons = this.shadowRoot?.querySelectorAll(`cds-custom-modal-close-button`); if (closeButtons) elements.push(closeButtons); } const _tabbableItems = this.querySelectorAll(selectorTabbable); if (_tabbableItems) elements.push(_tabbableItems); const all = elements?.flatMap((nodeList) => Array.from(nodeList))?.filter((el) => typeof el?.focus === "function"); return { first: all[0], last: all[all.length - 1], all }; } _checkSetHasSlot(e) { const t = e.target; const dataPostfix = t.getAttribute("data-postfix"); const postfix = dataPostfix ? `-${dataPostfix}` : ""; const hasName = `_has-${t.name}${postfix}`.replace(/-./g, (c) => c[1].toUpperCase()); this[hasName] = (t?.assignedElements()?.length ?? 0) > 0; } /** * Handles `click` event on the modal container. * * @param event The event. */ _handleClickContainer(event) { if (event.target.matches(this.constructor.selectorCloseButton)) this._handleUserInitiatedClose(event.target); } /** * Handles user-initiated close request of this tearsheet. * * @param triggeredBy The element that triggered this close request. */ _handleUserInitiatedClose(triggeredBy) { if (this.open) { const init = { bubbles: true, cancelable: true, composed: true, detail: { triggeredBy } }; if (this.dispatchEvent(new CustomEvent(this.constructor.eventBeforeClose, init))) { this.open = false; this.dispatchEvent(new CustomEvent(this.constructor.eventClose, init)); } } } _handleSlugChange(e) { const childItems = e.target.assignedElements(); this._hasSlug = childItems.length > 0; if (this._hasSlug) { childItems[0].setAttribute("size", "sm"); this.setAttribute("slug", ""); } else this.removeAttribute("slug"); } _handleDecoratorChange(e) { this._hasAILabel = false; const childItems = e.target.assignedElements(); this._hasDecorator = childItems.length > 0; if (this._hasDecorator) { for (const item of childItems) if (item.tagName.toLowerCase() === "cds-custom-ai-label") { this._hasAILabel = true; break; } } if (this._hasAILabel || this._hasDecorator) { childItems[0].setAttribute("size", "sm"); this.setAttribute(this._hasAILabel ? "ai-label" : "decorator", ""); this.removeAttribute(this._hasAILabel ? "decorator" : "ai-label"); } else { this.removeAttribute("decorator"); this.removeAttribute("ai-label"); } } _handleActionsChange(e) { const actions = e.target?.assignedElements(); const actionsCount = actions?.length ?? 0; if (actionsCount > this._maxActions) { this._actionsCount = this._maxActions; console.error(`Too many tearsheet actions, max ${this._maxActions}.`); } else this._actionsCount = actionsCount; for (let i = 0; i < actions?.length; i++) if (i + 1 > this._maxActions) { actions[i].setAttribute("hidden", "true"); actions[i].setAttribute(`data-actions-limit-${this._maxActions}-exceeded`, `${actions.length}`); } else actions[i].classList.add(`${blockClassActionSet}__action-button`); this._checkUpdateActionSizes(); } static { this._stack = { open: [], all: [] }; } connectedCallback() { super.connectedCallback(); _CDSTearsheet._stack.all.push(this._handleStackChange); } disconnectedCallback() { super.disconnectedCallback(); const indexAll = _CDSTearsheet._stack.all.indexOf(this._handleStackChange); _CDSTearsheet._stack.all.splice(indexAll, 1); const indexOpen = _CDSTearsheet._stack.all.indexOf(this._handleStackChange); _CDSTearsheet._stack.open.splice(indexOpen, 1); } render() { const { closeIconDescription, influencerPlacement, influencerWidth, open, width } = this; const actionsMultiple = [ "", "single", "double", "triple" ][this._actionsCount]; const headerFieldsTemplate = html`<div class=${`${blockClass}__header-fields`} > <h2 class=${`${blockClassModalHeader}__label`} ?hidden=${!this._hasLabel}> <slot name="label" @slotchange=${this._checkSetHasSlot}></slot> </h2> <h3 class=${`${blockClassModalHeader}__heading ${blockClass}__heading`} ?hidden=${!this._hasTitle} > <slot name="title" @slotchange=${this._checkSetHasSlot}></slot> </h3> <div class=${`${blockClass}__header-description`} ?hidden=${!this._hasDescription} > <slot name="description" @slotchange=${this._checkSetHasSlot}></slot> </div> </div>`; const headerActionsTemplate = html` <div class=${`${blockClass}__header-actions`} ?hidden=${!this._hasHeaderActions || this.width === "narrow"} > <slot name="header-actions" @slotchange=${this._checkSetHasSlot}></slot> </div>`; const headerTemplate = html` <cds-custom-modal-header class=${`${blockClass}__header`} ?has-close-icon=${this.hasCloseIcon || this?._actionsCount === 0} ?has-navigation=${this._hasHeaderNavigation && this.width === "wide"} ?has-header-actions=${this._hasHeaderActions && this.width === "wide"} ?has-actions=${this?._actionsCount > 0} ?has-slug=${this?._hasSlug} ?has-decorator=${this?._hasDecorator} width=${width} > ${this.width === "wide" ? html`<cds-custom-layer level="1" class=${`${blockClass}__header-content`} >${headerFieldsTemplate}${headerActionsTemplate}</cds-custom-layer >` : html`<div>${headerFieldsTemplate}${headerActionsTemplate}</div>`} <div class=${`${blockClass}__header-navigation`} ?hidden=${!this._hasHeaderNavigation || this.width === "narrow"} > <slot name="header-navigation" @slotchange=${this._checkSetHasSlot} ></slot> </div> <slot name="decorator" slot="ai-label" @slotchange=${this._handleDecoratorChange} ></slot> <slot name="slug" @slotchange=${this._handleSlugChange}></slot> ${this.hasCloseIcon || this?._actionsCount === 0 ? html`<cds-custom-modal-close-button close-button-label=${closeIconDescription} @click=${this._handleUserInitiatedClose} ></cds-custom-modal-close-button>` : ""} </cds-custom-modal-header>`; if (this._stackPosition <= this._stackDepth) return html` <div aria-label=${this.ariaLabel} class=${`${blockClass}__container cds-custom--modal-container cds-custom--modal-container--sm`} part="dialog" role="complementary" ?open=${this._isOpen} ?opening=${open && !this._isOpen} ?closing=${!open && this._isOpen} width=${width} stack-position=${this._stackPosition} stack-depth=${this._stackDepth} @click=${this._handleClickContainer} > <!-- Header --> ${headerTemplate} <!-- Body --> <cds-custom-modal-body class=${`${blockClass}__body`} width=${width}> <!-- Influencer when on left --> ${influencerPlacement !== "right" ? html`<div class=${`${blockClass}__influencer`} ?wide=${influencerWidth === "wide"} ?hidden=${!this._hasInfluencerLeft || this.width === "narrow"} > <slot name="influencer" data-postfix="left" @slotchange=${this._checkSetHasSlot} ></slot> </div>` : ""} <div class=${`${blockClass}__right`}> <div class=${`${blockClass}__main`}> <div class=${`${blockClass}__content`}> <cds-custom-layer level="${this.width === "narrow" ? 1 : 0}" > <slot></slot> </cds-custom-layer> </div> <!-- Influencer when on right --> ${influencerPlacement === "right" ? html`<div class=${`${blockClass}__influencer`} ?wide=${influencerWidth} ?hidden=${!this._hasInfluencerRight || this.width === "narrow"} > <slot name="influencer" data-postfix="right" @slotchange=${this._checkSetHasSlot} ></slot> </div>` : ""} </div> <!-- Action buttons --> <cds-custom-button-set-base class=${`${blockClass}__buttons ${blockClass}__button-container`} actions-multiple=${actionsMultiple} ?tearsheet-wide=${width === "wide"} ?hidden=${this._actionsCount === 0} > <slot name="actions" @slotchange=${this._handleActionsChange} ></slot> </cds-custom-button-set-base> </div> </cds-custom-modal-body> </div> `; else { pconsole_default.warn("Tearsheet not rendered: maximum stacking depth exceeded."); return null; } } async updated(changedProperties) { if (changedProperties.has("width")) this._checkUpdateActionSizes(); if (changedProperties.has("width") || changedProperties.has("_hasHeaderNavigation") || changedProperties.has("_hasInfluencerLeft") || changedProperties.has("_hasInfluencerRight") || changedProperties.has("_hasHeaderActions")) { if (this.width === "narrow") { if (this._hasHeaderNavigation) console.error(`Header navigation is not permitted in narrow Tearsheet.`); if (this._hasInfluencerLeft || this._hasInfluencerRight) console.error(`Influencer is not permitted in narrow Tearsheet.`); if (this._hasHeaderActions) console.error(`Header actions are not permitted in narrow Tearsheet.`); } } if (changedProperties.has("open")) { this._updateStack(); this._checkSetOpen(); if (this.open) { this._launcher = this.ownerDocument.activeElement; await this.constructor._delay(); if (this.selectorInitialFocus?.trim()?.length) this.querySelector(this.selectorInitialFocus)?.focus(); else { const { first: _firstElement } = this.getFocusable(); _firstElement?.focus(); } } else if (this._launcher && typeof this._launcher.focus === "function") { this._launcher.focus(); this._launcher = null; } } } /** * @param ms The number of milliseconds. * @returns A promise that is resolves after the given milliseconds. */ static _delay(ms = 0) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } /** * A selector selecting buttons that should close this modal. */ static get selectorCloseButton() { return `[data-modal-close],cds-custom-modal-close-button`; } /** * A selector selecting tabbable nodes. */ static get selectorTabbable() { return selectorTabbable; } /** * The name of the custom event fired before this tearsheet is being closed upon a user gesture. * Cancellation of this event stops the user-initiated action of closing this tearsheet. */ static get eventBeforeClose() { return `c4p-tearsheet-beingclosed`; } /** * The name of the custom event fired after this tearsheet is closed upon a user gesture. */ static get eventClose() { return `c4p-tearsheet-closed`; } /** * The name of the custom event fired on clicking the navigate back button */ static get eventNavigateBack() { return `c4p-tearsheet-header-navigate-back`; } static { this.styles = tearsheet_default$1; } }; __decorate([query(`.${blockClass}__container`)], CDSTearsheet.prototype, "_tearsheet", void 0); __decorate([queryAssignedElements({ slot: "actions", selector: `cds-custom-button` })], CDSTearsheet.prototype, "_actions", void 0); __decorate([state()], CDSTearsheet.prototype, "_actionsCount", void 0); __decorate([state()], CDSTearsheet.prototype, "_hasHeaderActions", void 0); __decorate([state()], CDSTearsheet.prototype, "_hasLabel", void 0); __decorate([state()], CDSTearsheet.prototype, "_hasSlug", void 0); __decorate([state()], CDSTearsheet.prototype, "_hasDecorator", void 0); __decorate([state()], CDSTearsheet.prototype, "_hasAILabel", void 0); __decorate([state()], CDSTearsheet.prototype, "_hasTitle", void 0); __decorate([state()], CDSTearsheet.prototype, "_hasDescription", void 0); __decorate([state()], CDSTearsheet.prototype, "_hasInfluencerLeft", void 0); __decorate([state()], CDSTearsheet.prototype, "_hasInfluencerRight", void 0); __decorate([state()], CDSTearsheet.prototype, "_isOpen", void 0); __decorate([state()], CDSTearsheet.prototype, "_hasHeaderNavigation", void 0); __decorate([HostListener("click")], CDSTearsheet.prototype, "_handleClick", void 0); __decorate([HostListener("document:keydown")], CDSTearsheet.prototype, "_handleKeydown", void 0); __decorate([HostListener("keydown")], CDSTearsheet.prototype, "_handleHostKeydown", void 0); __decorate([property({ reflect: true, attribute: "aria-label" })], CDSTearsheet.prototype, "ariaLabel", void 0); __decorate([property({ reflect: true, attribute: "close-icon-description" })], CDSTearsheet.prototype, "closeIconDescription", void 0); __decorate([property({ reflect: true, type: Boolean, attribute: "has-close-icon" })], CDSTearsheet.prototype, "hasCloseIcon", void 0); __decorate([property({ reflect: true, attribute: "influencer-placement" })], CDSTearsheet.prototype, "influencerPlacement", void 0); __decorate([property({ reflect: true, attribute: "influencer-width" })], CDSTearsheet.prototype, "influencerWidth", void 0); __decorate([property({ type: Boolean, reflect: true })], CDSTearsheet.prototype, "open", void 0); __decorate([property({ type: Boolean, attribute: "prevent-close-on-click-outside" })], CDSTearsheet.prototype, "preventCloseOnClickOutside", void 0); __decorate([property({ reflect: true, attribute: "selector-initial-focus", type: String })], CDSTearsheet.prototype, "selectorInitialFocus", void 0); __decorate([property({ reflect: true, attribute: "width" })], CDSTearsheet.prototype, "width", void 0); __decorate([state()], CDSTearsheet.prototype, "_stackDepth", void 0); __decorate([state()], CDSTearsheet.prototype, "_stackPosition", void 0); CDSTearsheet = _CDSTearsheet = __decorate([carbonElement(`c4p-tearsheet`)], CDSTearsheet); var tearsheet_default = CDSTearsheet; //#endregion export { TEARSHEET_INFLUENCER_PLACEMENT, TEARSHEET_INFLUENCER_WIDTH, TEARSHEET_WIDTH, tearsheet_default as default }; //# sourceMappingURL=tearsheet.js.map