@carbon/ibm-products-web-components
Version:
Carbon for IBM Products Web Components
298 lines (290 loc) • 12.6 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 { 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