UNPKG

@carbon/ibm-products-web-components

Version:

Carbon for IBM Products Web Components

440 lines (436 loc) 17.3 kB
/** * 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" @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=${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