mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-14 09:39:15 +00:00
Snackbar (#99)
* 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()
This commit is contained in:
committed by
Jake Archibald
parent
ef4094885e
commit
c90db020b0
115
src/lib/SnackBar/index.ts
Normal file
115
src/lib/SnackBar/index.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user