UNPKG

chrome-devtools-frontend

Version:
257 lines (224 loc) • 8.04 kB
// Copyright 2025 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /* eslint-disable @devtools/no-lit-render-outside-of-view, @devtools/enforce-custom-element-definitions-location */ import * as i18n from '../../../core/i18n/i18n.js'; import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js'; import * as UI from '../../legacy/legacy.js'; import * as Lit from '../../lit/lit.js'; import * as Buttons from '../buttons/buttons.js'; import snackbarStyles from './snackbar.css.js'; const {html} = Lit; const UIStrings = { /** * @description Title for close button */ dismiss: 'Dismiss', } as const; const str_ = i18n.i18n.registerUIStrings('ui/components/snackbars/Snackbar.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export interface ActionProperties { label: string; title?: string; onClick: () => void; } export interface SnackbarProperties { message: string; closable?: boolean; actionProperties?: ActionProperties; } export const DEFAULT_AUTO_DISMISS_MS = 5000; const LONG_ACTION_THRESHOLD = 15; /** * @property actionButtonClickHandler - Function to be triggered when action button is clicked. * @property dismissTimeout - reflects the `"dismiss-timeout"` attribute. * @property message - reflects the `"message"` attribute. * @property closable - reflects the `"closable"` attribute. * @property actionButtonLabel - reflects the `"action-button-label"` attribute. * @property actionButtonTitle - reflects the `"action-button-title"` attribute. * @attribute dismiss-timeout - Timeout in ms after which the snackbar is dismissed (if closable is false). * @attribute message - The message to display in the snackbar. * @attribute closable - If true, the snackbar will have a dismiss button. This cancels the auto dismiss behavior. * @attribute action-button-label - The text for the action button. * @attribute action-button-title - The title for the action button. * */ export class Snackbar extends HTMLElement { readonly #shadow = this.attachShadow({mode: 'open'}); #container: HTMLElement; #timeout: number|null = null; #isLongAction = false; #actionButtonClickHandler?: () => void; static snackbarQueue: Snackbar[] = []; /** * Returns the timeout (in ms) after which the snackbar is dismissed. */ get dismissTimeout(): number { return this.hasAttribute('dismiss-timeout') ? Number(this.getAttribute('dismiss-timeout')) : DEFAULT_AUTO_DISMISS_MS; } /** * Sets the value of the `"dismiss-timeout"` attribute for the snackbar. */ set dismissTimeout(dismissMs: number) { this.setAttribute('dismiss-timeout', dismissMs.toString()); } /** * Returns the message displayed in the snackbar. */ get message(): string|null { return this.getAttribute('message'); } /** * Sets the `"message"` attribute for the snackbar. */ set message(message: string) { this.setAttribute('message', message); } /** * Returns whether the snackbar is closable. If true, the snackbar will have a dismiss button. * @default false */ get closable(): boolean { return this.hasAttribute('closable'); } /** * Sets the `"closable"` attribute for the snackbar. */ set closable(closable: boolean) { this.toggleAttribute('closable', closable); } /** * Returns the text for the action button. */ get actionButtonLabel(): string|null { return this.getAttribute('action-button-label'); } /** * Sets the `"action-button-label"` attribute for the snackbar. */ set actionButtonLabel(actionButtonLabel: string) { this.setAttribute('action-button-label', actionButtonLabel); } /** * Returns the title for the action button. */ get actionButtonTitle(): string|null { return this.getAttribute('action-button-title'); } /** * Sets the `"action-button-title"` attribute for the snackbar. */ set actionButtonTitle(actionButtonTitle: string) { this.setAttribute('action-button-title', actionButtonTitle); } /** * Sets the function to be triggered when the action button is clicked. * @param actionButtonClickHandler */ set actionButtonClickHandler(actionButtonClickHandler: () => void) { this.#actionButtonClickHandler = actionButtonClickHandler; } constructor(properties: SnackbarProperties, container?: HTMLElement) { super(); this.message = properties.message; this.#container = container || UI.InspectorView.InspectorView.instance().element; if (properties.closable) { this.closable = properties.closable; } if (properties.actionProperties) { this.actionButtonLabel = properties.actionProperties.label; this.#actionButtonClickHandler = properties.actionProperties.onClick; if (properties.actionProperties.title) { this.actionButtonTitle = properties.actionProperties.title; } } } static show(properties: SnackbarProperties, container?: HTMLElement): Snackbar { const snackbar = new Snackbar(properties, container); Snackbar.snackbarQueue.push(snackbar); if (Snackbar.snackbarQueue.length === 1) { snackbar.#show(); } return snackbar; } #show(): void { this.#container.appendChild(this); if (this.#timeout) { window.clearTimeout(this.#timeout); } if (!this.closable) { this.#timeout = window.setTimeout(() => { this.#close(); }, this.dismissTimeout); } } #close(): void { if (this.#timeout) { window.clearTimeout(this.#timeout); } this.remove(); Snackbar.snackbarQueue.shift(); if (Snackbar.snackbarQueue.length > 0) { const nextSnackbar = Snackbar.snackbarQueue[0]; if (nextSnackbar) { nextSnackbar.#show(); } } } #onActionButtonClickHandler(event: Event): void { if (this.#actionButtonClickHandler) { event.preventDefault(); this.#actionButtonClickHandler(); this.#close(); } } connectedCallback(): void { if (this.actionButtonLabel) { this.#isLongAction = this.actionButtonLabel.length > LONG_ACTION_THRESHOLD; } this.role = 'alert'; const containerCls = Lit.Directives.classMap({ container: true, 'long-action': Boolean(this.#isLongAction), closable: Boolean(this.closable), }); // clang-format off const actionButton = this.actionButtonLabel ? html`<devtools-button class="snackbar-button" @click=${this.#onActionButtonClickHandler} jslog=${VisualLogging.action('snackbar.action').track({click: true})} .variant=${Buttons.Button.Variant.TEXT} .title=${this.actionButtonTitle ?? ''} .inverseColorTheme=${true} >${this.actionButtonLabel}</devtools-button>`:Lit.nothing; const crossButton = this.closable ? html`<devtools-button class="dismiss snackbar-button" @click=${this.#close} jslog=${VisualLogging.action('snackbar.dismiss').track({click: true})} aria-label=${i18nString(UIStrings.dismiss)} .iconName=${'cross'} .variant=${Buttons.Button.Variant.ICON} .title=${i18nString(UIStrings.dismiss)} .inverseColorTheme=${true} ></devtools-button>`:Lit.nothing; Lit.render(html` <style>${snackbarStyles}</style> <div class=${containerCls}> <div class="label-container"> <div class="message">${this.message}</div> ${!this.#isLongAction ? actionButton : Lit.nothing} ${crossButton} </div> ${this.#isLongAction ? html`<div class="long-action-container">${actionButton}</div>` : Lit.nothing} </div> `, this.#shadow, {host: this}); // clang-format on } } customElements.define('devtools-snackbar', Snackbar); declare global { interface HTMLElementTagNameMap { 'devtools-snackbar': Snackbar; } }