UNPKG

@carbon/ibm-products-web-components

Version:
298 lines (290 loc) 12.6 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 { MatchMediaController } from "../../globals/js/utils/match-media-controller.js"; import { tearsheetSignal } from "./tearsheet-signal.js"; import { registerFocusableContainers } from "../../utilities/manageFocusTrap/manageFocusTrap.js"; import "../truncated-text/index.js"; import tearsheet_header_content_default$1 from "./tearsheet-header-content.scss.js"; import { LitElement, html } from "lit"; import { property, query, state } from "lit/decorators.js"; import HostListenerMixin from "@carbon/web-components/es/globals/mixins/host-listener.js"; import { carbonElement } from "@carbon/web-components/es/globals/decorators/carbon-element.js"; import { iconLoader } from "@carbon/web-components/es/globals/internal/icon-loader.js"; import '@carbon/web-components/es-custom/components/icon-button/index.js'; import '@carbon/web-components/es-custom/components/modal/index.js'; import { breakpoints } from "@carbon/layout"; import { SignalWatcher } from "@lit-labs/signals"; import Close20 from "@carbon/icons/es/close/20.js"; //#region src/components/tearsheet-preview/tearsheet-header-content.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. */ const blockClass = `c4p--tearsheet__next`; /** * Tearsheet Header Content component - Contains the title, description, and decorative elements. * * @element c4p-tearsheet-header-content * @slot label - Optional label text above the title * @slot title-start - Content before the title (e.g., icons) * @slot title-end - Content after the title (e.g., badges, tags) * @slot description - Custom description content below the title * @slot decorator - AI label or other decorative elements */ let CDSTearsheetHeaderContent = class CDSTearsheetHeaderContent extends SignalWatcher(HostListenerMixin(LitElement)) { constructor(..._args) { super(..._args); this.slot = "header-content"; this.title = ""; this._titleId = `${blockClass}__title-${Math.random().toString(36).substr(2, 9)}`; this._hasTitleStart = false; this._hasTitleEnd = false; this._hasDecorator = false; this._hasAILabel = false; this._isMobileOrNarrow = false; this.mdMediaQuery = `(max-width: ${breakpoints.md.width})`; this.isMobileDevice = new MatchMediaController(this, this.mdMediaQuery, false); } /** * Public getter for the title ID (used by parent tearsheet for aria-labelledby) */ get titleId() { return this._titleId; } get isNarrowVariant() { return this.closest(`c4p-preview-tearsheet`)?.getAttribute("variant") === "narrow"; } /** * Returns the first focusable element in the header for the focus trap. * Priority by screen size: * Large (desktop): header-actions button → close button → AI label * Small (mobile): AI label → close button → header-actions button */ getFirstFocusable() { const headerActionSlot = this.querySelector("[slot=\"header-actions\"]"); const headerActionBtn = headerActionSlot ? headerActionSlot.matches(`cds-custom-button:not([disabled]), button:not([disabled])`) ? headerActionSlot : headerActionSlot.querySelector(`cds-custom-button:not([disabled]), button:not([disabled])`) : null; const closeBtn = this.shadowRoot?.querySelector(`.${blockClass}__close-button cds-custom-icon-button:not([disabled])`); const decoratorSlot = this.querySelector("[slot=\"decorator\"]"); const aiLabel = decoratorSlot ? decoratorSlot.matches(`cds-custom-ai-label`) ? decoratorSlot : decoratorSlot.querySelector(`cds-custom-ai-label`) : null; return this._isMobileOrNarrow ? aiLabel ?? closeBtn ?? headerActionBtn ?? null : headerActionBtn ?? closeBtn ?? aiLabel ?? null; } firstUpdated(_changedProperties) { this._checkSlots(); this._isMobileOrNarrow = this.isMobileDevice?.matches || this.isNarrowVariant; const uniqueId = tearsheetSignal.get().uniqueId; if (uniqueId) { registerFocusableContainers(this, uniqueId); registerFocusableContainers(this.shadowRoot, uniqueId); } } updated(_changedProperties) { const previousIsMobileOrNarrow = this._isMobileOrNarrow; this._isMobileOrNarrow = this.isMobileDevice?.matches || this.isNarrowVariant; if (this._isMobileOrNarrow !== previousIsMobileOrNarrow) this.requestUpdate(); this._updateDecoratorSize(); this._updateHeaderOffset(); this._updateInertState(); } /** * Applies `inert` to collapsed header regions so they are removed from * the tab order and AT tree. CSS alone (opacity:0 / max-block-size:0) * does not prevent keyboard focus on hidden elements. * * Collapsed regions: * - Everything in header-content except the title wrapper * (description, label, extra slot content, "Read more" button) * - Header-actions on small/narrow screens */ _updateInertState() { const { fullyCollapsed } = tearsheetSignal.get(); const headerContent = this.shadowRoot?.querySelector(`.${blockClass}__header-content`); if (headerContent) headerContent.querySelectorAll(`:scope > *:not(.${blockClass}__content__title-wrapper)`).forEach((el) => { el.toggleAttribute("inert", fullyCollapsed); }); const headerActions = this.shadowRoot?.querySelector(`.${blockClass}__header-actions`); if (headerActions) { const shouldInert = fullyCollapsed && this._isMobileOrNarrow; headerActions.toggleAttribute("inert", shouldInert); } } _checkSlots() { if (this._titleStartSlot) { const assignedNodes = this._titleStartSlot.assignedElements(); this._hasTitleStart = assignedNodes.length > 0; } if (this._titleEndSlot) { const assignedNodes = this._titleEndSlot.assignedElements(); this._hasTitleEnd = assignedNodes.length > 0; } } _handleSlotChange() { this._checkSlots(); } _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; } const { fullyCollapsed } = tearsheetSignal.get(); childItems[0].setAttribute("size", fullyCollapsed ? "xs" : "sm"); const host = this.closest(`c4p-tearsheet-header`); if (host) { host.setAttribute(this._hasAILabel ? "ai-label" : "decorator", ""); host.removeAttribute(this._hasAILabel ? "decorator" : "ai-label"); } } else { const host = this.closest(`c4p-tearsheet-header`); if (host) { host.removeAttribute("decorator"); host.removeAttribute("ai-label"); } } this._updateHeaderOffset(); } _updateDecoratorSize() { const { fullyCollapsed } = tearsheetSignal.get(); const assigned = this._decoratorSlot?.assignedElements({ flatten: true }); if (assigned?.length) assigned[0].setAttribute("size", fullyCollapsed ? "xs" : "sm"); } _updateHeaderOffset() { const { open, isSm } = tearsheetSignal.get(); if (!open) return; const offset = (this.querySelector("[slot=\"decorator\"]")?.clientWidth ?? 0) + 24 + (isSm ? 8 : 0); document.documentElement.style.setProperty("--tearsheet-header-action-offset", `${offset}px`); } render() { const { fullyCollapsed, hideCloseButton, closeIconDescription, onClose } = tearsheetSignal.get(); const decoratorTemplate = html` <div class="${blockClass}__decorator" role="${this._hasDecorator ? "complementary" : void 0}" aria-label="${this._hasDecorator ? "Decorator" : void 0}" > <slot name="decorator" @slotchange=${this._handleDecoratorChange} ></slot> </div> `; const closeButtonTemplate = !hideCloseButton ? html` <div class="${blockClass}__close-button ${"cds-custom"}--modal-close-button" > <cds-custom-icon-button class="${"cds-custom"}--modal-close" kind="ghost" size="${fullyCollapsed ? "md" : "lg"}" align="left" aria-label="${closeIconDescription || "Close"}" @click="${() => { onClose?.(); this.dispatchEvent(new CustomEvent(`c4p-tearsheet-header-close-button-clicked`, { bubbles: true, composed: true })); }}" > ${iconLoader(Close20, { slot: "icon", class: `cds-custom--modal-close__icon` })} <span slot="tooltip-content" >${closeIconDescription || "Close"}</span > </cds-custom-icon-button> </div> ` : html``; const headerActionsTemplate = html` <div class="${blockClass}__header-actions"> <slot name="header-actions"></slot> </div> `; const headerContentTemplate = html` <div class="${blockClass}__header-content"> <!-- Label --> <div class="${blockClass}__header-label"> <slot name="label"></slot> </div> <div class="${blockClass}__content__title-wrapper"> <h2 class="${blockClass}__header-title" id="${this._titleId}"> <!-- Title Start --> ${this._hasTitleStart ? html` <span class="${blockClass}__title-start"> <slot name="title-start" @slotchange="${this._handleSlotChange}" ></slot> </span> ` : html`<slot name="title-start" @slotchange="${this._handleSlotChange}" ></slot>`} <!-- Title (main text) — limit to 1 line when collapsed so it fits in the reduced-height collapsed bar --> <c4p-truncated-text class="${blockClass}__content__title" id="${blockClass}__header-title__truncatedText" value="${this.title}" lines="${tearsheetSignal.get().fullyCollapsed ? 1 : 2}" ></c4p-truncated-text> <!-- Title End --> ${this._hasTitleEnd ? html` <span class="${blockClass}__title-end"> <slot name="title-end" @slotchange="${this._handleSlotChange}" ></slot> </span> ` : html`<slot name="title-end" @slotchange="${this._handleSlotChange}" ></slot>`} </h2> </div> <!-- Description --> <div class="${blockClass}__header-description"> <slot name="description"></slot> </div> <!-- Extra children --> <div class="${blockClass}__header-content--extra"> <slot></slot> </div> </div> `; return this._isMobileOrNarrow ? html`${decoratorTemplate} ${closeButtonTemplate} ${headerContentTemplate} ${headerActionsTemplate}` : html`${headerActionsTemplate} ${decoratorTemplate} ${closeButtonTemplate} ${headerContentTemplate}`; } static { this.styles = tearsheet_header_content_default$1; } }; __decorate([property({ reflect: true })], CDSTearsheetHeaderContent.prototype, "slot", void 0); __decorate([property({ reflect: true })], CDSTearsheetHeaderContent.prototype, "title", void 0); __decorate([query("slot[name=\"title-start\"]")], CDSTearsheetHeaderContent.prototype, "_titleStartSlot", void 0); __decorate([query("slot[name=\"title-end\"]")], CDSTearsheetHeaderContent.prototype, "_titleEndSlot", void 0); __decorate([query("slot[name=\"decorator\"]")], CDSTearsheetHeaderContent.prototype, "_decoratorSlot", void 0); __decorate([state()], CDSTearsheetHeaderContent.prototype, "_hasTitleStart", void 0); __decorate([state()], CDSTearsheetHeaderContent.prototype, "_hasTitleEnd", void 0); __decorate([state()], CDSTearsheetHeaderContent.prototype, "_hasDecorator", void 0); __decorate([state()], CDSTearsheetHeaderContent.prototype, "_hasAILabel", void 0); __decorate([state()], CDSTearsheetHeaderContent.prototype, "_isMobileOrNarrow", void 0); CDSTearsheetHeaderContent = __decorate([carbonElement(`c4p-tearsheet-header-content`)], CDSTearsheetHeaderContent); var tearsheet_header_content_default = CDSTearsheetHeaderContent; //#endregion export { tearsheet_header_content_default as default }; //# sourceMappingURL=tearsheet-header-content.js.map