mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 20:19:13 +00:00
Compare commits
7 Commits
event-list
...
konnorroge
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac4826a93a | ||
|
|
74d78df23c | ||
|
|
b8d84b7962 | ||
|
|
ac03167451 | ||
|
|
114b20ad98 | ||
|
|
c6222230e3 | ||
|
|
b4a09d0fa7 |
33
package-lock.json
generated
33
package-lock.json
generated
@@ -12,6 +12,7 @@
|
||||
"@ctrl/tinycolor": "^3.5.0",
|
||||
"@floating-ui/dom": "^1.2.1",
|
||||
"@lit-labs/react": "^1.1.1",
|
||||
"@open-wc/scoped-elements": "^2.2.0",
|
||||
"@shoelace-style/animations": "^1.1.0",
|
||||
"@shoelace-style/localize": "^3.1.1",
|
||||
"composed-offset-position": "^0.0.4",
|
||||
@@ -1658,19 +1659,17 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@open-wc/dedupe-mixin": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@open-wc/dedupe-mixin/-/dedupe-mixin-1.3.1.tgz",
|
||||
"integrity": "sha512-ukowSvzpZQDUH0Y3znJTsY88HkiGk3Khc0WGpIPhap1xlerieYi27QBg6wx/nTurpWfU6XXXsx9ocxDYCdtw0Q==",
|
||||
"dev": true
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@open-wc/dedupe-mixin/-/dedupe-mixin-1.4.0.tgz",
|
||||
"integrity": "sha512-Sj7gKl1TLcDbF7B6KUhtvr+1UCxdhMbNY5KxdU5IfMFWqL8oy1ZeAcCANjoB1TL0AJTcPmcCFsCbHf8X2jGDUA=="
|
||||
},
|
||||
"node_modules/@open-wc/scoped-elements": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@open-wc/scoped-elements/-/scoped-elements-2.1.3.tgz",
|
||||
"integrity": "sha512-WoQD5T8Me9obek+iyjgrAMw9wxZZg4ytIteIN1i9LXW2KohezUp0LTOlWgBajWJo0/bpjUKiODX73cMYL2i3hw==",
|
||||
"dev": true,
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@open-wc/scoped-elements/-/scoped-elements-2.2.0.tgz",
|
||||
"integrity": "sha512-Qe+vWsuVHFzUkdChwlmJGuQf9cA3I+QOsSHULV/6qf6wsqLM2/32svNRH+rbBIMwiPEwzZprZlkvkqQRucYnVA==",
|
||||
"dependencies": {
|
||||
"@lit/reactive-element": "^1.0.0",
|
||||
"@open-wc/dedupe-mixin": "^1.3.0"
|
||||
"@open-wc/dedupe-mixin": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@open-wc/semantic-dom-diff": {
|
||||
@@ -18379,19 +18378,17 @@
|
||||
}
|
||||
},
|
||||
"@open-wc/dedupe-mixin": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@open-wc/dedupe-mixin/-/dedupe-mixin-1.3.1.tgz",
|
||||
"integrity": "sha512-ukowSvzpZQDUH0Y3znJTsY88HkiGk3Khc0WGpIPhap1xlerieYi27QBg6wx/nTurpWfU6XXXsx9ocxDYCdtw0Q==",
|
||||
"dev": true
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@open-wc/dedupe-mixin/-/dedupe-mixin-1.4.0.tgz",
|
||||
"integrity": "sha512-Sj7gKl1TLcDbF7B6KUhtvr+1UCxdhMbNY5KxdU5IfMFWqL8oy1ZeAcCANjoB1TL0AJTcPmcCFsCbHf8X2jGDUA=="
|
||||
},
|
||||
"@open-wc/scoped-elements": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@open-wc/scoped-elements/-/scoped-elements-2.1.3.tgz",
|
||||
"integrity": "sha512-WoQD5T8Me9obek+iyjgrAMw9wxZZg4ytIteIN1i9LXW2KohezUp0LTOlWgBajWJo0/bpjUKiODX73cMYL2i3hw==",
|
||||
"dev": true,
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@open-wc/scoped-elements/-/scoped-elements-2.2.0.tgz",
|
||||
"integrity": "sha512-Qe+vWsuVHFzUkdChwlmJGuQf9cA3I+QOsSHULV/6qf6wsqLM2/32svNRH+rbBIMwiPEwzZprZlkvkqQRucYnVA==",
|
||||
"requires": {
|
||||
"@lit/reactive-element": "^1.0.0",
|
||||
"@open-wc/dedupe-mixin": "^1.3.0"
|
||||
"@open-wc/dedupe-mixin": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"@open-wc/semantic-dom-diff": {
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
"@ctrl/tinycolor": "^3.5.0",
|
||||
"@floating-ui/dom": "^1.2.1",
|
||||
"@lit-labs/react": "^1.1.1",
|
||||
"@open-wc/scoped-elements": "^2.2.0",
|
||||
"@shoelace-style/animations": "^1.1.0",
|
||||
"@shoelace-style/localize": "^3.1.1",
|
||||
"composed-offset-position": "^0.0.4",
|
||||
|
||||
248
src/components/alert/alert.component.ts
Normal file
248
src/components/alert/alert.component.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { html } from 'lit';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import SlIconButton from '../icon-button/icon-button';
|
||||
import styles from './alert.styles';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
const toastStack = Object.assign(document.createElement('div'), { className: 'sl-toast-stack' });
|
||||
|
||||
/**
|
||||
* @summary Alerts are used to display important messages inline or as toast notifications.
|
||||
* @documentation https://shoelace.style/components/alert
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @dependency sl-icon-button
|
||||
*
|
||||
* @slot - The alert's main content.
|
||||
* @slot icon - An icon to show in the alert. Works best with `<sl-icon>`.
|
||||
*
|
||||
* @event sl-show - Emitted when the alert opens.
|
||||
* @event sl-after-show - Emitted after the alert opens and all animations are complete.
|
||||
* @event sl-hide - Emitted when the alert closes.
|
||||
* @event sl-after-hide - Emitted after the alert closes and all animations are complete.
|
||||
*
|
||||
* @csspart base - The component's base wrapper.
|
||||
* @csspart icon - The container that wraps the optional icon.
|
||||
* @csspart message - The container that wraps the alert's main content.
|
||||
* @csspart close-button - The close button, an `<sl-icon-button>`.
|
||||
* @csspart close-button__base - The close button's exported `base` part.
|
||||
*
|
||||
* @animation alert.show - The animation to use when showing the alert.
|
||||
* @animation alert.hide - The animation to use when hiding the alert.
|
||||
*/
|
||||
export default class SlAlert extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
static get scopedElements() {
|
||||
return {
|
||||
'sl-icon-button': SlIconButton
|
||||
};
|
||||
}
|
||||
|
||||
private autoHideTimeout: number;
|
||||
private readonly hasSlotController = new HasSlotController(this, 'icon', 'suffix');
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
@query('[part~="base"]') base: HTMLElement;
|
||||
|
||||
/**
|
||||
* Indicates whether or not the alert is open. You can toggle this attribute to show and hide the alert, or you can
|
||||
* use the `show()` and `hide()` methods and this attribute will reflect the alert's open state.
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true }) open = false;
|
||||
|
||||
/** Enables a close button that allows the user to dismiss the alert. */
|
||||
@property({ type: Boolean, reflect: true }) closable = false;
|
||||
|
||||
/** The alert's theme variant. */
|
||||
@property({ reflect: true }) variant: 'primary' | 'success' | 'neutral' | 'warning' | 'danger' = 'primary';
|
||||
|
||||
/**
|
||||
* The length of time, in milliseconds, the alert will show before closing itself. If the user interacts with
|
||||
* the alert before it closes (e.g. moves the mouse over it), the timer will restart. Defaults to `Infinity`, meaning
|
||||
* the alert will not close on its own.
|
||||
*/
|
||||
@property({ type: Number }) duration = Infinity;
|
||||
|
||||
firstUpdated() {
|
||||
this.base.hidden = !this.open;
|
||||
}
|
||||
|
||||
private restartAutoHide() {
|
||||
clearTimeout(this.autoHideTimeout);
|
||||
if (this.open && this.duration < Infinity) {
|
||||
this.autoHideTimeout = window.setTimeout(() => this.hide(), this.duration);
|
||||
}
|
||||
}
|
||||
|
||||
private handleCloseClick() {
|
||||
this.hide();
|
||||
}
|
||||
|
||||
private handleMouseMove() {
|
||||
this.restartAutoHide();
|
||||
}
|
||||
|
||||
@watch('open', { waitUntilFirstUpdate: true })
|
||||
async handleOpenChange() {
|
||||
if (this.open) {
|
||||
// Show
|
||||
this.emit('sl-show');
|
||||
|
||||
if (this.duration < Infinity) {
|
||||
this.restartAutoHide();
|
||||
}
|
||||
|
||||
await stopAnimations(this.base);
|
||||
this.base.hidden = false;
|
||||
const { keyframes, options } = getAnimation(this, 'alert.show', { dir: this.localize.dir() });
|
||||
await animateTo(this.base, keyframes, options);
|
||||
|
||||
this.emit('sl-after-show');
|
||||
} else {
|
||||
// Hide
|
||||
this.emit('sl-hide');
|
||||
|
||||
clearTimeout(this.autoHideTimeout);
|
||||
|
||||
await stopAnimations(this.base);
|
||||
const { keyframes, options } = getAnimation(this, 'alert.hide', { dir: this.localize.dir() });
|
||||
await animateTo(this.base, keyframes, options);
|
||||
this.base.hidden = true;
|
||||
|
||||
this.emit('sl-after-hide');
|
||||
}
|
||||
}
|
||||
|
||||
@watch('duration')
|
||||
handleDurationChange() {
|
||||
this.restartAutoHide();
|
||||
}
|
||||
|
||||
/** Shows the alert. */
|
||||
async show() {
|
||||
if (this.open) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.open = true;
|
||||
return waitForEvent(this, 'sl-after-show');
|
||||
}
|
||||
|
||||
/** Hides the alert */
|
||||
async hide() {
|
||||
if (!this.open) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.open = false;
|
||||
return waitForEvent(this, 'sl-after-hide');
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the alert as a toast notification. This will move the alert out of its position in the DOM and, when
|
||||
* dismissed, it will be removed from the DOM completely. By storing a reference to the alert, you can reuse it by
|
||||
* calling this method again. The returned promise will resolve after the alert is hidden.
|
||||
*/
|
||||
async toast() {
|
||||
return new Promise<void>(resolve => {
|
||||
if (toastStack.parentElement === null) {
|
||||
document.body.append(toastStack);
|
||||
}
|
||||
|
||||
toastStack.appendChild(this);
|
||||
|
||||
// Wait for the toast stack to render
|
||||
requestAnimationFrame(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions -- force a reflow for the initial transition
|
||||
this.clientWidth;
|
||||
this.show();
|
||||
});
|
||||
|
||||
this.addEventListener(
|
||||
'sl-after-hide',
|
||||
() => {
|
||||
toastStack.removeChild(this);
|
||||
resolve();
|
||||
|
||||
// Remove the toast stack from the DOM when there are no more alerts
|
||||
if (toastStack.querySelector('sl-alert') === null) {
|
||||
toastStack.remove();
|
||||
}
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div
|
||||
part="base"
|
||||
class=${classMap({
|
||||
alert: true,
|
||||
'alert--open': this.open,
|
||||
'alert--closable': this.closable,
|
||||
'alert--has-icon': this.hasSlotController.test('icon'),
|
||||
'alert--primary': this.variant === 'primary',
|
||||
'alert--success': this.variant === 'success',
|
||||
'alert--neutral': this.variant === 'neutral',
|
||||
'alert--warning': this.variant === 'warning',
|
||||
'alert--danger': this.variant === 'danger'
|
||||
})}
|
||||
role="alert"
|
||||
aria-hidden=${this.open ? 'false' : 'true'}
|
||||
@mousemove=${this.handleMouseMove}
|
||||
>
|
||||
<slot name="icon" part="icon" class="alert__icon"></slot>
|
||||
|
||||
<slot part="message" class="alert__message" aria-live="polite"></slot>
|
||||
|
||||
${this.closable
|
||||
? html`
|
||||
<sl-icon-button
|
||||
part="close-button"
|
||||
exportparts="base:close-button__base"
|
||||
class="alert__close-button"
|
||||
name="x-lg"
|
||||
library="system"
|
||||
label=${this.localize.term('close')}
|
||||
@click=${this.handleCloseClick}
|
||||
></sl-icon-button>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
setDefaultAnimation('alert.show', {
|
||||
keyframes: [
|
||||
{ opacity: 0, scale: 0.8 },
|
||||
{ opacity: 1, scale: 1 }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
|
||||
setDefaultAnimation('alert.hide', {
|
||||
keyframes: [
|
||||
{ opacity: 1, scale: 1 },
|
||||
{ opacity: 0, scale: 0.8 }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sl-alert': SlAlert;
|
||||
}
|
||||
}
|
||||
@@ -1,244 +1,3 @@
|
||||
import '../icon-button/icon-button';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { html } from 'lit';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import styles from './alert.styles';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
const toastStack = Object.assign(document.createElement('div'), { className: 'sl-toast-stack' });
|
||||
|
||||
/**
|
||||
* @summary Alerts are used to display important messages inline or as toast notifications.
|
||||
* @documentation https://shoelace.style/components/alert
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @dependency sl-icon-button
|
||||
*
|
||||
* @slot - The alert's main content.
|
||||
* @slot icon - An icon to show in the alert. Works best with `<sl-icon>`.
|
||||
*
|
||||
* @event sl-show - Emitted when the alert opens.
|
||||
* @event sl-after-show - Emitted after the alert opens and all animations are complete.
|
||||
* @event sl-hide - Emitted when the alert closes.
|
||||
* @event sl-after-hide - Emitted after the alert closes and all animations are complete.
|
||||
*
|
||||
* @csspart base - The component's base wrapper.
|
||||
* @csspart icon - The container that wraps the optional icon.
|
||||
* @csspart message - The container that wraps the alert's main content.
|
||||
* @csspart close-button - The close button, an `<sl-icon-button>`.
|
||||
* @csspart close-button__base - The close button's exported `base` part.
|
||||
*
|
||||
* @animation alert.show - The animation to use when showing the alert.
|
||||
* @animation alert.hide - The animation to use when hiding the alert.
|
||||
*/
|
||||
|
||||
@customElement('sl-alert')
|
||||
export default class SlAlert extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
private autoHideTimeout: number;
|
||||
private readonly hasSlotController = new HasSlotController(this, 'icon', 'suffix');
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
@query('[part~="base"]') base: HTMLElement;
|
||||
|
||||
/**
|
||||
* Indicates whether or not the alert is open. You can toggle this attribute to show and hide the alert, or you can
|
||||
* use the `show()` and `hide()` methods and this attribute will reflect the alert's open state.
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true }) open = false;
|
||||
|
||||
/** Enables a close button that allows the user to dismiss the alert. */
|
||||
@property({ type: Boolean, reflect: true }) closable = false;
|
||||
|
||||
/** The alert's theme variant. */
|
||||
@property({ reflect: true }) variant: 'primary' | 'success' | 'neutral' | 'warning' | 'danger' = 'primary';
|
||||
|
||||
/**
|
||||
* The length of time, in milliseconds, the alert will show before closing itself. If the user interacts with
|
||||
* the alert before it closes (e.g. moves the mouse over it), the timer will restart. Defaults to `Infinity`, meaning
|
||||
* the alert will not close on its own.
|
||||
*/
|
||||
@property({ type: Number }) duration = Infinity;
|
||||
|
||||
firstUpdated() {
|
||||
this.base.hidden = !this.open;
|
||||
}
|
||||
|
||||
private restartAutoHide() {
|
||||
clearTimeout(this.autoHideTimeout);
|
||||
if (this.open && this.duration < Infinity) {
|
||||
this.autoHideTimeout = window.setTimeout(() => this.hide(), this.duration);
|
||||
}
|
||||
}
|
||||
|
||||
private handleCloseClick() {
|
||||
this.hide();
|
||||
}
|
||||
|
||||
private handleMouseMove() {
|
||||
this.restartAutoHide();
|
||||
}
|
||||
|
||||
@watch('open', { waitUntilFirstUpdate: true })
|
||||
async handleOpenChange() {
|
||||
if (this.open) {
|
||||
// Show
|
||||
this.emit('sl-show');
|
||||
|
||||
if (this.duration < Infinity) {
|
||||
this.restartAutoHide();
|
||||
}
|
||||
|
||||
await stopAnimations(this.base);
|
||||
this.base.hidden = false;
|
||||
const { keyframes, options } = getAnimation(this, 'alert.show', { dir: this.localize.dir() });
|
||||
await animateTo(this.base, keyframes, options);
|
||||
|
||||
this.emit('sl-after-show');
|
||||
} else {
|
||||
// Hide
|
||||
this.emit('sl-hide');
|
||||
|
||||
clearTimeout(this.autoHideTimeout);
|
||||
|
||||
await stopAnimations(this.base);
|
||||
const { keyframes, options } = getAnimation(this, 'alert.hide', { dir: this.localize.dir() });
|
||||
await animateTo(this.base, keyframes, options);
|
||||
this.base.hidden = true;
|
||||
|
||||
this.emit('sl-after-hide');
|
||||
}
|
||||
}
|
||||
|
||||
@watch('duration')
|
||||
handleDurationChange() {
|
||||
this.restartAutoHide();
|
||||
}
|
||||
|
||||
/** Shows the alert. */
|
||||
async show() {
|
||||
if (this.open) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.open = true;
|
||||
return waitForEvent(this, 'sl-after-show');
|
||||
}
|
||||
|
||||
/** Hides the alert */
|
||||
async hide() {
|
||||
if (!this.open) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.open = false;
|
||||
return waitForEvent(this, 'sl-after-hide');
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the alert as a toast notification. This will move the alert out of its position in the DOM and, when
|
||||
* dismissed, it will be removed from the DOM completely. By storing a reference to the alert, you can reuse it by
|
||||
* calling this method again. The returned promise will resolve after the alert is hidden.
|
||||
*/
|
||||
async toast() {
|
||||
return new Promise<void>(resolve => {
|
||||
if (toastStack.parentElement === null) {
|
||||
document.body.append(toastStack);
|
||||
}
|
||||
|
||||
toastStack.appendChild(this);
|
||||
|
||||
// Wait for the toast stack to render
|
||||
requestAnimationFrame(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions -- force a reflow for the initial transition
|
||||
this.clientWidth;
|
||||
this.show();
|
||||
});
|
||||
|
||||
this.addEventListener(
|
||||
'sl-after-hide',
|
||||
() => {
|
||||
toastStack.removeChild(this);
|
||||
resolve();
|
||||
|
||||
// Remove the toast stack from the DOM when there are no more alerts
|
||||
if (toastStack.querySelector('sl-alert') === null) {
|
||||
toastStack.remove();
|
||||
}
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div
|
||||
part="base"
|
||||
class=${classMap({
|
||||
alert: true,
|
||||
'alert--open': this.open,
|
||||
'alert--closable': this.closable,
|
||||
'alert--has-icon': this.hasSlotController.test('icon'),
|
||||
'alert--primary': this.variant === 'primary',
|
||||
'alert--success': this.variant === 'success',
|
||||
'alert--neutral': this.variant === 'neutral',
|
||||
'alert--warning': this.variant === 'warning',
|
||||
'alert--danger': this.variant === 'danger'
|
||||
})}
|
||||
role="alert"
|
||||
aria-hidden=${this.open ? 'false' : 'true'}
|
||||
@mousemove=${this.handleMouseMove}
|
||||
>
|
||||
<slot name="icon" part="icon" class="alert__icon"></slot>
|
||||
|
||||
<slot part="message" class="alert__message" aria-live="polite"></slot>
|
||||
|
||||
${this.closable
|
||||
? html`
|
||||
<sl-icon-button
|
||||
part="close-button"
|
||||
exportparts="base:close-button__base"
|
||||
class="alert__close-button"
|
||||
name="x-lg"
|
||||
library="system"
|
||||
label=${this.localize.term('close')}
|
||||
@click=${this.handleCloseClick}
|
||||
></sl-icon-button>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
setDefaultAnimation('alert.show', {
|
||||
keyframes: [
|
||||
{ opacity: 0, scale: 0.8 },
|
||||
{ opacity: 1, scale: 1 }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
|
||||
setDefaultAnimation('alert.hide', {
|
||||
keyframes: [
|
||||
{ opacity: 1, scale: 1 },
|
||||
{ opacity: 0, scale: 0.8 }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sl-alert': SlAlert;
|
||||
}
|
||||
}
|
||||
import SlAlert from './alert.component';
|
||||
export default SlAlert;
|
||||
window.customElements.define('sl-alert', class extends SlAlert {});
|
||||
|
||||
124
src/components/animated-image/animated-image.component.ts
Normal file
124
src/components/animated-image/animated-image.component.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { html } from 'lit';
|
||||
import { property, query, state } from 'lit/decorators.js';
|
||||
import { watch } from '../../internal/watch';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import SlIcon from '../icon/icon';
|
||||
import styles from './animated-image.styles';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
/**
|
||||
* @summary A component for displaying animated GIFs and WEBPs that play and pause on interaction.
|
||||
* @documentation https://shoelace.style/components/animated-image
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @dependency sl-icon
|
||||
*
|
||||
* @event sl-load - Emitted when the image loads successfully.
|
||||
* @event sl-error - Emitted when the image fails to load.
|
||||
*
|
||||
* @slot play-icon - Optional play icon to use instead of the default. Works best with `<sl-icon>`.
|
||||
* @slot pause-icon - Optional pause icon to use instead of the default. Works best with `<sl-icon>`.
|
||||
*
|
||||
* @part - control-box - The container that surrounds the pause/play icons and provides their background.
|
||||
*
|
||||
* @cssproperty --control-box-size - The size of the icon box.
|
||||
* @cssproperty --icon-size - The size of the play/pause icons.
|
||||
*/
|
||||
export default class SlAnimatedImage extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
static get scopedElements() {
|
||||
return { 'sl-icon': SlIcon };
|
||||
}
|
||||
|
||||
@query('.animated-image__animated') animatedImage: HTMLImageElement;
|
||||
|
||||
@state() frozenFrame: string;
|
||||
@state() isLoaded = false;
|
||||
|
||||
/** The path to the image to load. */
|
||||
@property() src: string;
|
||||
|
||||
/** A description of the image used by assistive devices. */
|
||||
@property() alt: string;
|
||||
|
||||
/** Plays the animation. When this attribute is remove, the animation will pause. */
|
||||
@property({ type: Boolean, reflect: true }) play: boolean;
|
||||
|
||||
private handleClick() {
|
||||
this.play = !this.play;
|
||||
}
|
||||
|
||||
private handleLoad() {
|
||||
const canvas = document.createElement('canvas');
|
||||
const { width, height } = this.animatedImage;
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
canvas.getContext('2d')!.drawImage(this.animatedImage, 0, 0, width, height);
|
||||
this.frozenFrame = canvas.toDataURL('image/gif');
|
||||
|
||||
if (!this.isLoaded) {
|
||||
this.emit('sl-load');
|
||||
this.isLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
private handleError() {
|
||||
this.emit('sl-error');
|
||||
}
|
||||
|
||||
@watch('play', { waitUntilFirstUpdate: true })
|
||||
handlePlayChange() {
|
||||
// When the animation starts playing, reset the src so it plays from the beginning. Since the src is cached, this
|
||||
// won't trigger another request.
|
||||
if (this.play) {
|
||||
this.animatedImage.src = '';
|
||||
this.animatedImage.src = this.src;
|
||||
}
|
||||
}
|
||||
|
||||
@watch('src')
|
||||
handleSrcChange() {
|
||||
this.isLoaded = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="animated-image">
|
||||
<img
|
||||
class="animated-image__animated"
|
||||
src=${this.src}
|
||||
alt=${this.alt}
|
||||
crossorigin="anonymous"
|
||||
aria-hidden=${this.play ? 'false' : 'true'}
|
||||
@click=${this.handleClick}
|
||||
@load=${this.handleLoad}
|
||||
@error=${this.handleError}
|
||||
/>
|
||||
|
||||
${this.isLoaded
|
||||
? html`
|
||||
<img
|
||||
class="animated-image__frozen"
|
||||
src=${this.frozenFrame}
|
||||
alt=${this.alt}
|
||||
aria-hidden=${this.play ? 'true' : 'false'}
|
||||
@click=${this.handleClick}
|
||||
/>
|
||||
|
||||
<div part="control-box" class="animated-image__control-box">
|
||||
<slot name="play-icon"><sl-icon name="play-fill" library="system"></sl-icon></slot>
|
||||
<slot name="pause-icon"><sl-icon name="pause-fill" library="system"></sl-icon></slot>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sl-animated-image': SlAnimatedImage;
|
||||
}
|
||||
}
|
||||
@@ -1,122 +1,3 @@
|
||||
import '../icon/icon';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { html } from 'lit';
|
||||
import { watch } from '../../internal/watch';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import styles from './animated-image.styles';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
/**
|
||||
* @summary A component for displaying animated GIFs and WEBPs that play and pause on interaction.
|
||||
* @documentation https://shoelace.style/components/animated-image
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @dependency sl-icon
|
||||
*
|
||||
* @event sl-load - Emitted when the image loads successfully.
|
||||
* @event sl-error - Emitted when the image fails to load.
|
||||
*
|
||||
* @slot play-icon - Optional play icon to use instead of the default. Works best with `<sl-icon>`.
|
||||
* @slot pause-icon - Optional pause icon to use instead of the default. Works best with `<sl-icon>`.
|
||||
*
|
||||
* @part - control-box - The container that surrounds the pause/play icons and provides their background.
|
||||
*
|
||||
* @cssproperty --control-box-size - The size of the icon box.
|
||||
* @cssproperty --icon-size - The size of the play/pause icons.
|
||||
*/
|
||||
@customElement('sl-animated-image')
|
||||
export default class SlAnimatedImage extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
@query('.animated-image__animated') animatedImage: HTMLImageElement;
|
||||
|
||||
@state() frozenFrame: string;
|
||||
@state() isLoaded = false;
|
||||
|
||||
/** The path to the image to load. */
|
||||
@property() src: string;
|
||||
|
||||
/** A description of the image used by assistive devices. */
|
||||
@property() alt: string;
|
||||
|
||||
/** Plays the animation. When this attribute is remove, the animation will pause. */
|
||||
@property({ type: Boolean, reflect: true }) play: boolean;
|
||||
|
||||
private handleClick() {
|
||||
this.play = !this.play;
|
||||
}
|
||||
|
||||
private handleLoad() {
|
||||
const canvas = document.createElement('canvas');
|
||||
const { width, height } = this.animatedImage;
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
canvas.getContext('2d')!.drawImage(this.animatedImage, 0, 0, width, height);
|
||||
this.frozenFrame = canvas.toDataURL('image/gif');
|
||||
|
||||
if (!this.isLoaded) {
|
||||
this.emit('sl-load');
|
||||
this.isLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
private handleError() {
|
||||
this.emit('sl-error');
|
||||
}
|
||||
|
||||
@watch('play', { waitUntilFirstUpdate: true })
|
||||
handlePlayChange() {
|
||||
// When the animation starts playing, reset the src so it plays from the beginning. Since the src is cached, this
|
||||
// won't trigger another request.
|
||||
if (this.play) {
|
||||
this.animatedImage.src = '';
|
||||
this.animatedImage.src = this.src;
|
||||
}
|
||||
}
|
||||
|
||||
@watch('src')
|
||||
handleSrcChange() {
|
||||
this.isLoaded = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="animated-image">
|
||||
<img
|
||||
class="animated-image__animated"
|
||||
src=${this.src}
|
||||
alt=${this.alt}
|
||||
crossorigin="anonymous"
|
||||
aria-hidden=${this.play ? 'false' : 'true'}
|
||||
@click=${this.handleClick}
|
||||
@load=${this.handleLoad}
|
||||
@error=${this.handleError}
|
||||
/>
|
||||
|
||||
${this.isLoaded
|
||||
? html`
|
||||
<img
|
||||
class="animated-image__frozen"
|
||||
src=${this.frozenFrame}
|
||||
alt=${this.alt}
|
||||
aria-hidden=${this.play ? 'true' : 'false'}
|
||||
@click=${this.handleClick}
|
||||
/>
|
||||
|
||||
<div part="control-box" class="animated-image__control-box">
|
||||
<slot name="play-icon"><sl-icon name="play-fill" library="system"></sl-icon></slot>
|
||||
<slot name="pause-icon"><sl-icon name="pause-fill" library="system"></sl-icon></slot>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sl-animated-image': SlAnimatedImage;
|
||||
}
|
||||
}
|
||||
import SlAnimatedImage from './animated-image.component';
|
||||
export default SlAnimatedImage;
|
||||
window.customElements.define('sl-animated-image', class extends SlAnimatedImage {});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { LitElement } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { ScopedElementsMixin } from '@open-wc/scoped-elements';
|
||||
|
||||
// Match event type name strings that are registered on GlobalEventHandlersEventMap...
|
||||
type EventTypeRequiresDetail<T> = T extends keyof GlobalEventHandlersEventMap
|
||||
@@ -62,11 +63,15 @@ type GetCustomEventType<T> = T extends keyof GlobalEventHandlersEventMap
|
||||
// `keyof ValidEventTypeMap` is equivalent to `keyof GlobalEventHandlersEventMap` but gives a nicer error message
|
||||
type ValidEventTypeMap = EventTypesWithRequiredDetail | EventTypesWithoutRequiredDetail;
|
||||
|
||||
export default class ShoelaceElement extends LitElement {
|
||||
export default class ShoelaceElement extends ScopedElementsMixin(LitElement) {
|
||||
// Make localization attributes reactive
|
||||
@property() dir: string;
|
||||
@property() lang: string;
|
||||
|
||||
static get scopedElements(): Record<string, typeof LitElement> {
|
||||
return {};
|
||||
}
|
||||
|
||||
/** Emits a custom event with more convenient defaults. */
|
||||
emit<T extends string & keyof EventTypesWithoutRequiredDetail>(
|
||||
name: EventTypeDoesNotRequireDetail<T>,
|
||||
|
||||
Reference in New Issue
Block a user