@carbon/ibm-products-web-components
Version:
Carbon for IBM Products Web Components
440 lines (436 loc) • 17.3 kB
JavaScript
/**
* Copyright IBM Corp. 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.
*/
import { __decorate } from 'tslib';
import { LitElement, html } from 'lit';
import { property, query, state, queryAssignedElements } from 'lit/decorators.js';
import { provide } from '@lit/context';
import { prefix } from '../../globals/settings.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 styles from './notification-panel.scss.js';
import { selectorTabbable } from '@carbon/web-components/es/globals/settings.js';
import { carbonElement } from '@carbon/web-components/es/globals/decorators/carbon-element.js';
import { classMap } from 'lit/directives/class-map.js';
import { dateTimeLocaleContext } from './date-time-context.js';
import '@carbon/web-components/es/components/button/index.js';
import '@carbon/web-components/es/components/toggle/index.js';
import '@carbon/web-components/es/components/icon-button/index.js';
import '@carbon-labs/wc-empty-state/es/index.js';
/**
* @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 = `${prefix}--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() {
super(...arguments);
/**
* Determines whether the notifications panel should render or not
*/
this.open = false;
this._hasTodayContent = false;
this._hasPreviousContent = false;
this._handleKeydown = ({ key, target }) => {
var _a;
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));
(_a = this.triggerButtonRef) === null || _a === void 0 ? void 0 : _a.focus();
}
};
// Use @HostListener for global document click events
this._handleClick = (event) => {
var _a, _b;
const target = event.target;
if (!this.open || this.contains(target)) {
return;
}
// Check if the target is an actionable element
const actionableSelectors = [
'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',
];
const isActionable = actionableSelectors.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 && ((_a = this.triggerButtonRef) === null || _a === void 0 ? void 0 : _a.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) {
(_b = this.triggerButtonRef) === null || _b === void 0 ? void 0 : _b.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() {
var _a, _b, _c;
(_a = this.todaySlot) === null || _a === void 0 ? void 0 : _a.addEventListener('slotchange', () => this._handleSlotChange('today'));
(_b = this.previousSlot) === null || _b === void 0 ? void 0 : _b.addEventListener('slotchange', () => this._handleSlotChange('previous'));
this._markFirstNotification();
const slot = (_c = this.shadowRoot) === null || _c === void 0 ? void 0 : _c.querySelector('slot[name="previous"]');
const slottedElements = (slot === null || slot === void 0 ? void 0 : slot.assignedElements({ flatten: true })) || [];
for (const el of slottedElements) {
el.addEventListener('mouseenter', () => {
this._handleMouseEnter(el);
});
el.addEventListener('mouseleave', () => {
this._handleMouseLeave(el);
});
}
}
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;
const classes = classMap({
[`${blockClass}__container`]: true,
[`${blockClass}__entrance`]: open,
[`${blockClass}__exit`]: !open,
});
const mainSectionClasses = classMap({
[`${blockClass}__main-section`]: true,
[`${blockClass}__main-section-empty`]: !_hasTodayContent && !_hasPreviousContent,
});
return html `
<div role="dialog" tabindex="0" class=${classes}>
<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"
=${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}
-toggle-changed=${handleToggle}
></cds-toggle>
</div>
<div class=${mainSectionClasses}>
${_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() {
var _a;
super.disconnectedCallback();
(_a = this._mutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
}
_handleMouseEnter(el) {
const next = el.nextElementSibling;
if ((next === null || next === void 0 ? void 0 : next.tagName.toLowerCase()) === 'c4p-notification') {
next.classList.add(`${blockClass}__notification--next`);
}
}
_handleMouseLeave(el) {
const next = el.nextElementSibling;
if ((next === null || next === void 0 ? void 0 : next.tagName.toLowerCase()) === 'c4p-notification') {
next.classList.remove(`${blockClass}__notification--next`);
}
}
_markFirstNotification() {
const notifications = this.querySelectorAll(`${prefix}-notification`);
notifications.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() {
var _a;
const button = this.renderRoot.querySelector(`.${blockClass}__dismiss-button`);
if (button) {
button.focus();
}
else {
(_a = this._mutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
this._mutationObserver = new MutationObserver(() => {
var _a;
const btn = this.renderRoot.querySelector(`.${blockClass}__dismiss-button`);
if (btn) {
btn.focus();
(_a = this._mutationObserver) === null || _a === void 0 ? void 0 : _a.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 triggeredBy = event.target;
const init = {
bubbles: true,
cancelable: true,
composed: true,
detail: {
triggeredBy,
},
};
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 `${prefix}-notification-panel-beingclosed`;
}
/**
* The name of the custom event fired after this notification-panel is closed upon a user gesture.
*/
static get eventClickOutside() {
return `${prefix}-notification-click-outside`;
}
/**
* The name of the custom event fired after dismiss all button click
*/
static get eventDismissAll() {
return `${prefix}-notification-dismiss-all`;
}
/**
* The name of the custom event fired after this do not disturb button toggled.
*/
static get eventDonotDisturbChange() {
return `${prefix}-notification-donot-disturb-change`;
}
};
CDSNotificationPanel.styles = styles; // `styles` here is a `CSSResult` generated by custom WebPack loader
__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 })
// @ts-ignore
], 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() // Use @state decorator
], CDSNotificationPanel.prototype, "_hasTodayContent", void 0);
__decorate([
state() // Use @state decorator
], 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')
// @ts-ignore: The decorator refers to this method but TS thinks this method is not referred to
], CDSNotificationPanel.prototype, "_handleKeydown", void 0);
__decorate([
HostListener('document:click')
// @ts-ignore: The decorator refers to this method but TS thinks this method is not referred to
], CDSNotificationPanel.prototype, "_handleClick", void 0);
CDSNotificationPanel = __decorate([
carbonElement(`${prefix}-notification-panel`)
], CDSNotificationPanel);
var CDSNotificationPanel$1 = CDSNotificationPanel;
export { CDSNotificationPanel$1 as default };
//# sourceMappingURL=notification-panel.js.map