@carbon/ibm-products-web-components
Version:
Carbon for IBM Products Web Components
391 lines (389 loc) • 14.2 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 { dateTimeLocaleContext } from "./date-time-context.js";
import notification_panel_default$1 from "./notification-panel.scss.js";
import { classMap } from "lit/directives/class-map.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/icon-button/index.js";
import { provide } from "@lit/context";
import "@carbon/web-components/es/components/toggle/index.js";
import "@carbon-labs/wc-empty-state/es/index.js";
//#region src/components/notification-panel/notification-panel.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.
*/
const blockClass = `c4p--notifications-panel`;
/**
* NotificationPanel.
*
* @element c4p-notification-panel
* @csspart dialog The dialog.
* @slot today - Today Section.
* @slot previous - Previous Section.
* @slot footer - Footer for the Panel.
* @fires c4p-notification-panel-beingclosed - The custom event before notification-panel is closed.
* @fires c4p-notification-dismiss-all - The custom event fired after notification-panel is closed upon a user gesture.
* @fires c4p-notification-donot-disturb-change - The custom event fired after notification-panel is closed upon a user gesture.
* @fires c4p-notification-click-outside - The custom event fired after user clicks outside the panel or Esc key is pressed.
*/
let CDSNotificationPanel = class CDSNotificationPanel extends HostListenerMixin(LitElement) {
constructor(..._args) {
super(..._args);
this.open = false;
this._hasTodayContent = false;
this._hasPreviousContent = false;
this._handleKeydown = ({ key, target }) => {
if (key === "Escape") {
this.open = false;
const init = {
bubbles: true,
cancelable: true,
composed: true,
detail: { triggeredBy: target }
};
this.dispatchEvent(new CustomEvent(this.constructor.eventClickOutside, init));
this.triggerButtonRef?.focus();
}
};
this._handleClick = (event) => {
const target = event.target;
if (!this.open || this.contains(target)) return;
const isActionable = [
"a",
"button",
"input",
"select",
"textarea",
"[role=\"button\"]",
"[role=\"link\"]",
"[tabindex]:not([tabindex=\"-1\"]",
"cds-button",
"cds-link",
"cds-accordion-item",
"cds-breadcrumb-item",
"cds-icon-button",
"cds-checkbox",
"cds-dropdown",
"cds-dropdown-item",
"cds-file-uploader",
"cds-file-uploader-button",
"cds-menu-button",
"cds-menu",
"cds-menu-item",
"cds-multi-select",
"cds-multi-select-item",
"cds-number-input",
"cds-overflow-menu",
"cds-overflow-menu-body",
"cds-overflow-menu-item",
"cds-pagination",
"cds-select-item",
"cds-radio-button",
"cds-search",
"cds-select",
"cds-slider",
"cds-slider-input",
"cds-tabs",
"cds-tab",
"cds-textarea",
"cds-text-input",
"cds-time-picker",
"cds-time-picker-select",
"cds-select-item",
"cds-toggle",
"cds-toggletip"
].some((selector) => {
if (!(target instanceof Element)) return false;
if (target.closest(selector)) return true;
const root = target.getRootNode();
return root instanceof ShadowRoot && root.host instanceof Element && root.host.matches(selector);
});
if (this.open && this.triggerButtonRef?.contains(target)) return;
const init = {
bubbles: true,
cancelable: true,
composed: true,
detail: { triggeredBy: target }
};
if (this.dispatchEvent(new CustomEvent(this.constructor.eventBeforeClose, init))) {
this.open = false;
this.dispatchEvent(new CustomEvent(this.constructor.eventClickOutside, init));
if (!isActionable) this.triggerButtonRef?.focus();
}
};
}
willUpdate(changedProperties) {
if (changedProperties.has("dateTimeLocale")) this._providedLocale = this.dateTimeLocale;
if (changedProperties.has("_todayElements") || changedProperties.has("_previousElements")) {
this._hasTodayContent = this._todayElements.length > 0;
this._hasPreviousContent = this._previousElements.length > 0;
}
}
firstUpdated() {
this.todaySlot?.addEventListener("slotchange", () => this._handleSlotChange("today"));
this.previousSlot?.addEventListener("slotchange", () => this._handleSlotChange("previous"));
this._markFirstNotification();
const addNotificationListeners = (slotName) => {
const slottedElements = (this.shadowRoot?.querySelector(`slot[name="${slotName}"]`))?.assignedElements({ flatten: true }) || [];
for (const el of slottedElements) {
el.addEventListener("mouseenter", () => {
this._handleMouseEnter(el);
});
el.addEventListener("mouseleave", () => {
this._handleMouseLeave(el);
});
el.addEventListener("focusin", () => {
this._handleFocusIn(el);
});
el.addEventListener("focusout", () => {
this._handleFocusOut(el);
});
}
};
addNotificationListeners("today");
addNotificationListeners("previous");
}
updated() {
this._markFirstNotification();
if (this.open) requestAnimationFrame(() => this._tryFocusDismissButton());
}
render() {
const { titleText, todayText, previousText, dismissAllLabel, emptyStateLabel, doNotDisturbLabel, open, _hasTodayContent, _hasPreviousContent, _onDismissAllNotifications: onDismissAllNotifications, _handleToggle: handleToggle } = this;
return html`
<div role="dialog" tabindex="0" class=${classMap({
[`${blockClass}__container`]: true,
[`${blockClass}__entrance`]: open,
[`${blockClass}__exit`]: !open
})}>
<div class="${blockClass}__header-container">
<div class="${blockClass}__header-flex">
<h2 class="${blockClass}__header">${titleText}</h2>
<cds-button
size="sm"
kind="ghost"
class="${blockClass}__dismiss-button"
@click=${onDismissAllNotifications}
>
${dismissAllLabel}
</cds-button>
</div>
<cds-toggle
size="sm"
class="${blockClass}__do-not-disturb-toggle"
id="${blockClass}__do-not-disturb-toggle-component"
label-a=${doNotDisturbLabel}
label-b=${doNotDisturbLabel}
aria-label=${doNotDisturbLabel}
@cds-toggle-changed=${handleToggle}
></cds-toggle>
</div>
<div class=${classMap({
[`${blockClass}__main-section`]: true,
[`${blockClass}__main-section-empty`]: !_hasTodayContent && !_hasPreviousContent
})}>
${_hasTodayContent ? html`
<h3
class="${blockClass}__time-section-label ${blockClass}__time-section-label--today"
>
${todayText}
</h3>
` : ""}
<slot name="today"></slot>
${_hasPreviousContent ? html`
<h3
class="${blockClass}__time-section-label ${blockClass}__time-section-label--previous"
>
${previousText}
</h3>
` : ""}
<slot name="previous"></slot>
${!_hasTodayContent && !_hasPreviousContent ? html` <slot name="empty-state">
<clabs-empty-state
subtitle="${emptyStateLabel}"
kind="notifications"
>
</clabs-empty-state>
</slot>` : ""}
</div>
${_hasTodayContent || _hasPreviousContent ? html`<div class="${blockClass}__bottom-actions-container">
<slot name="footer"></slot>
</div>` : ""}
</div>
`;
}
disconnectedCallback() {
super.disconnectedCallback();
this._mutationObserver?.disconnect();
}
_handleMouseEnter(el) {
const next = el.nextElementSibling;
if (next?.tagName.toLowerCase() === `c4p-notification`) next.classList.add(`${blockClass}__notification--next`);
}
_handleMouseLeave(el) {
const next = el.nextElementSibling;
if (next?.tagName.toLowerCase() === `c4p-notification`) next.classList.remove(`${blockClass}__notification--next`);
}
_handleFocusIn(el) {
const next = el.nextElementSibling;
if (next?.tagName.toLowerCase() === `c4p-notification`) next.classList.add(`${blockClass}__notification--next`);
}
_handleFocusOut(el) {
const next = el.nextElementSibling;
if (next?.tagName.toLowerCase() === `c4p-notification`) next.classList.remove(`${blockClass}__notification--next`);
}
_markFirstNotification() {
this.querySelectorAll(`c4p-notification`).forEach((el, i) => {
el.classList.toggle("first-notification", i === 0);
});
}
_handleSlotChange(slotName) {
slotName === "today" ? this._hasTodayContent = this._todayElements.length > 0 : this._hasPreviousContent = this._previousElements.length > 0;
}
_tryFocusDismissButton() {
const button = this.renderRoot.querySelector(`.${blockClass}__dismiss-button`);
if (button) button.focus();
else {
this._mutationObserver?.disconnect();
this._mutationObserver = new MutationObserver(() => {
const btn = this.renderRoot.querySelector(`.${blockClass}__dismiss-button`);
if (btn) {
btn.focus();
this._mutationObserver?.disconnect();
}
});
this._mutationObserver.observe(this.renderRoot, {
childList: true,
subtree: true
});
}
}
/**
* Handles user-initiated dismiss of all notifications.
*
* @param event The event that triggered the click.
*/
_onDismissAllNotifications(event) {
const triggeredBy = event.target;
event.stopPropagation();
const init = {
bubbles: true,
cancelable: true,
composed: true,
detail: { triggeredBy }
};
this.dispatchEvent(new CustomEvent(this.constructor.eventDismissAll, init));
}
_handleToggle(event) {
const init = {
bubbles: true,
cancelable: true,
composed: true,
detail: { triggeredBy: event.target }
};
this.dispatchEvent(new CustomEvent(this.constructor.eventDonotDisturbChange, init));
}
/**
* A selector selecting tabbable nodes.
*/
static get selectorTabbable() {
return selectorTabbable;
}
/**
* The name of the custom event fired before this notification-panel is being closed upon a user gesture.
* Cancellation of this event stops the user-initiated action of closing this notification-panel.
*/
static get eventBeforeClose() {
return `c4p-notification-panel-beingclosed`;
}
/**
* The name of the custom event fired after this notification-panel is closed upon a user gesture.
*/
static get eventClickOutside() {
return `c4p-notification-click-outside`;
}
/**
* The name of the custom event fired after dismiss all button click
*/
static get eventDismissAll() {
return `c4p-notification-dismiss-all`;
}
/**
* The name of the custom event fired after this do not disturb button toggled.
*/
static get eventDonotDisturbChange() {
return `c4p-notification-donot-disturb-change`;
}
static {
this.styles = notification_panel_default$1;
}
};
__decorate([property({
type: String,
attribute: "title-text"
})], CDSNotificationPanel.prototype, "titleText", void 0);
__decorate([property({
type: String,
attribute: "today-text"
})], CDSNotificationPanel.prototype, "todayText", void 0);
__decorate([property({
type: String,
attribute: "previous-text"
})], CDSNotificationPanel.prototype, "previousText", void 0);
__decorate([property({
reflect: true,
type: Boolean
})], CDSNotificationPanel.prototype, "open", void 0);
__decorate([property({
type: String,
attribute: "dismiss-all-label"
})], CDSNotificationPanel.prototype, "dismissAllLabel", void 0);
__decorate([property({
type: String,
attribute: "empty-state-label"
})], CDSNotificationPanel.prototype, "emptyStateLabel", void 0);
__decorate([property({
type: String,
attribute: "donot-disturb-label"
})], CDSNotificationPanel.prototype, "doNotDisturbLabel", void 0);
__decorate([property({ type: Object })], CDSNotificationPanel.prototype, "triggerButtonRef", void 0);
__decorate([property({
type: String,
attribute: "date-time-locale"
})], CDSNotificationPanel.prototype, "dateTimeLocale", void 0);
__decorate([provide({ context: dateTimeLocaleContext })], CDSNotificationPanel.prototype, "_providedLocale", void 0);
__decorate([query("slot[name=\"today\"]")], CDSNotificationPanel.prototype, "todaySlot", void 0);
__decorate([query("slot[name=\"previous\"]")], CDSNotificationPanel.prototype, "previousSlot", void 0);
__decorate([state()], CDSNotificationPanel.prototype, "_hasTodayContent", void 0);
__decorate([state()], CDSNotificationPanel.prototype, "_hasPreviousContent", void 0);
__decorate([queryAssignedElements({
slot: "today",
flatten: true
})], CDSNotificationPanel.prototype, "_todayElements", void 0);
__decorate([queryAssignedElements({
slot: "previous",
flatten: true
})], CDSNotificationPanel.prototype, "_previousElements", void 0);
__decorate([HostListener("keydown")], CDSNotificationPanel.prototype, "_handleKeydown", void 0);
__decorate([HostListener("document:click")], CDSNotificationPanel.prototype, "_handleClick", void 0);
CDSNotificationPanel = __decorate([carbonElement(`c4p-notification-panel`)], CDSNotificationPanel);
var notification_panel_default = CDSNotificationPanel;
//#endregion
export { notification_panel_default as default };
//# sourceMappingURL=notification-panel.js.map