@carbon/ibm-products-web-components
Version:
Carbon for IBM Products Web Components
533 lines (527 loc) • 20.1 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 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/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/components/layer/index.js";
import "@carbon/web-components/es/components/button/button-set-base.js";
import "@carbon/web-components/es/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--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-slug`));
if (this.hasCloseIcon) {
const closeButtons = this.shadowRoot?.querySelectorAll(`cds-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-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" =${this._checkSetHasSlot}></slot>
</h2>
<h3
class=${`${blockClassModalHeader}__heading ${blockClass}__heading`}
?hidden=${!this._hasTitle}
>
<slot name="title" =${this._checkSetHasSlot}></slot>
</h3>
<div
class=${`${blockClass}__header-description`}
?hidden=${!this._hasDescription}
>
<slot name="description" =${this._checkSetHasSlot}></slot>
</div>
</div>`;
const headerActionsTemplate = html` <div
class=${`${blockClass}__header-actions`}
?hidden=${!this._hasHeaderActions || this.width === "narrow"}
>
<slot name="header-actions" =${this._checkSetHasSlot}></slot>
</div>`;
const headerTemplate = html` <cds-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-layer level="1" class=${`${blockClass}__header-content`}
>${headerFieldsTemplate}${headerActionsTemplate}</cds-layer
>` : html`<div>${headerFieldsTemplate}${headerActionsTemplate}</div>`}
<div
class=${`${blockClass}__header-navigation`}
?hidden=${!this._hasHeaderNavigation || this.width === "narrow"}
>
<slot
name="header-navigation"
=${this._checkSetHasSlot}
></slot>
</div>
<slot
name="decorator"
slot="ai-label"
=${this._handleDecoratorChange}
></slot>
<slot name="slug" =${this._handleSlugChange}></slot>
${this.hasCloseIcon || this?._actionsCount === 0 ? html`<cds-modal-close-button
close-button-label=${closeIconDescription}
=${this._handleUserInitiatedClose}
></cds-modal-close-button>` : ""}
</cds-modal-header>`;
if (this._stackPosition <= this._stackDepth) return html`
<div
aria-label=${this.ariaLabel}
class=${`${blockClass}__container cds--modal-container cds--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}
=${this._handleClickContainer}
>
<!-- Header -->
${headerTemplate}
<!-- Body -->
<cds-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"
=${this._checkSetHasSlot}
></slot>
</div>` : ""}
<div class=${`${blockClass}__right`}>
<div class=${`${blockClass}__main`}>
<div class=${`${blockClass}__content`}>
<cds-layer
level="${this.width === "narrow" ? 1 : 0}"
>
<slot></slot>
</cds-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"
=${this._checkSetHasSlot}
></slot>
</div>` : ""}
</div>
<!-- Action buttons -->
<cds-button-set-base
class=${`${blockClass}__buttons ${blockClass}__button-container`}
actions-multiple=${actionsMultiple}
?tearsheet-wide=${width === "wide"}
?hidden=${this._actionsCount === 0}
>
<slot
name="actions"
=${this._handleActionsChange}
></slot>
</cds-button-set-base>
</div>
</cds-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-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-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