mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-15 01:59:57 +00:00
* Initial swing
* Finish up <snack-bar> implementation and integrate it
* Add missing types
* Use shift() since we dont care about referential equality
* Use `_` for private fields
* Remove rogue handler
* Remove impossible fallback value
* Make `<snack-bar>` actually contain its children
* will-change for the button ripple
* Guard against mutliple button action clicks
* `onhide()` -> `onremove()`
* remove transitionend
* Replace inline ref callback with linkRef
* showError only accepts strings
* Remove undefined initialization
* Throw on error
* Add missing error type.
* `SnackBar` ▶️ `Snack`
* Avoid child retaining a reference to parent, make show() return a Promise.
* async/await and avoid processing the stack if it is already being processed
* Add a meaningful return value to showSnackbar()
116 lines
3.3 KiB
TypeScript
116 lines
3.3 KiB
TypeScript
import './styles.css';
|
|
|
|
const DEFAULT_TIMEOUT = 2750;
|
|
|
|
export interface SnackOptions {
|
|
message: string;
|
|
timeout?: number;
|
|
actionText?: string;
|
|
actionHandler?: () => boolean | null;
|
|
}
|
|
|
|
export interface SnackShowResult {
|
|
action: boolean;
|
|
}
|
|
|
|
class Snack {
|
|
private _onremove: ((result: SnackShowResult) => void)[] = [];
|
|
private _options: SnackOptions;
|
|
private _element: Element = document.createElement('div');
|
|
private _text: Element = document.createElement('div');
|
|
private _button: Element = document.createElement('button');
|
|
private _showing = false;
|
|
private _closeTimer?: number;
|
|
private _result: SnackShowResult = {
|
|
action: false,
|
|
};
|
|
|
|
constructor (options: SnackOptions, callback?: (result: SnackShowResult) => void) {
|
|
this._options = options;
|
|
|
|
this._element.className = 'snackbar';
|
|
this._element.setAttribute('aria-live', 'assertive');
|
|
this._element.setAttribute('aria-atomic', 'true');
|
|
this._element.setAttribute('aria-hidden', 'true');
|
|
|
|
this._text.className = 'snackbar--text';
|
|
this._text.textContent = options.message;
|
|
this._element.appendChild(this._text);
|
|
|
|
if (options.actionText) {
|
|
this._button.className = 'snackbar--button';
|
|
this._button.textContent = options.actionText;
|
|
this._button.addEventListener('click', () => {
|
|
if (this._showing) {
|
|
if (options.actionHandler && options.actionHandler() === false) return;
|
|
this._result.action = true;
|
|
}
|
|
this.hide();
|
|
});
|
|
this._element.appendChild(this._button);
|
|
}
|
|
|
|
if (callback) {
|
|
this._onremove.push(callback);
|
|
}
|
|
}
|
|
|
|
cancelTimer () {
|
|
if (this._closeTimer != null) clearTimeout(this._closeTimer);
|
|
}
|
|
|
|
show (parent: Element): Promise<SnackShowResult> {
|
|
if (this._showing) return Promise.resolve(this._result);
|
|
this._showing = true;
|
|
this.cancelTimer();
|
|
if (parent !== this._element.parentNode) {
|
|
parent.appendChild(this._element);
|
|
}
|
|
this._element.removeAttribute('aria-hidden');
|
|
this._closeTimer = setTimeout(this.hide.bind(this), this._options.timeout || DEFAULT_TIMEOUT);
|
|
return new Promise((resolve) => {
|
|
this._onremove.push(resolve);
|
|
});
|
|
}
|
|
|
|
hide () {
|
|
if (!this._showing) return;
|
|
this._showing = false;
|
|
this.cancelTimer();
|
|
this._element.addEventListener('animationend', this.remove.bind(this));
|
|
this._element.setAttribute('aria-hidden', 'true');
|
|
}
|
|
|
|
remove () {
|
|
this.cancelTimer();
|
|
const parent = this._element.parentNode;
|
|
if (parent) parent.removeChild(this._element);
|
|
this._onremove.forEach(f => f(this._result));
|
|
this._onremove = [];
|
|
}
|
|
}
|
|
|
|
export default class SnackBarElement extends HTMLElement {
|
|
private _snackbars: Snack[] = [];
|
|
private _processingStack = false;
|
|
|
|
showSnackbar (options: SnackOptions): Promise<SnackShowResult> {
|
|
return new Promise((resolve) => {
|
|
const snack = new Snack(options, resolve);
|
|
this._snackbars.push(snack);
|
|
this._processStack();
|
|
});
|
|
}
|
|
|
|
private async _processStack () {
|
|
if (this._processingStack === true || this._snackbars.length === 0) return;
|
|
this._processingStack = true;
|
|
await this._snackbars[0].show(this);
|
|
this._snackbars.shift();
|
|
this._processingStack = false;
|
|
this._processStack();
|
|
}
|
|
}
|
|
|
|
customElements.define('snack-bar', SnackBarElement);
|