kopia lustrzana https://github.com/shoelace-style/shoelace
Porównaj commity
7 Commity
ff4da89ed7
...
17b5386ddc
Autor | SHA1 | Data |
---|---|---|
RoCat | 17b5386ddc | |
Konnor Rogers | 64996b2d35 | |
Susanne Kirchner | 0daa5d8dee | |
Konnor Rogers | 16d5575307 | |
Konnor Rogers | a427433701 | |
Danny Andrews | c6da4f5b14 | |
rcatoio | c4856756b9 |
|
@ -247,6 +247,69 @@ const App = () => {
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Countdown
|
||||||
|
|
||||||
|
Set the `countdown` attribute to display a loading bar that indicates the alert remaining time. This is useful for alerts with relatively long duration.
|
||||||
|
|
||||||
|
```html:preview
|
||||||
|
<div class="alert-countdown">
|
||||||
|
<sl-button variant="primary">Show Alert</sl-button>
|
||||||
|
|
||||||
|
<sl-alert variant="primary" duration="10000" countdown="RtL" closable>
|
||||||
|
<sl-icon slot="icon" name="info-circle"></sl-icon>
|
||||||
|
You're not stuck, the alert will close after a pretty long duration.
|
||||||
|
</sl-alert>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const container = document.querySelector('.alert-countdown');
|
||||||
|
const button = container.querySelector('sl-button');
|
||||||
|
const alert = container.querySelector('sl-alert');
|
||||||
|
|
||||||
|
button.addEventListener('click', () => alert.show());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.alert-countdown sl-alert {
|
||||||
|
margin-top: var(--sl-spacing-medium);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
```jsx:react
|
||||||
|
import { useState } from 'react';
|
||||||
|
import SlAlert from '@shoelace-style/shoelace/dist/react/alert';
|
||||||
|
import SlButton from '@shoelace-style/shoelace/dist/react/button';
|
||||||
|
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
|
||||||
|
|
||||||
|
const css = `
|
||||||
|
.alert-countdown sl-alert {
|
||||||
|
margin-top: var(--sl-spacing-medium);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="alert-countdown">
|
||||||
|
<SlButton variant="primary" onClick={() => setOpen(true)}>
|
||||||
|
Show Alert
|
||||||
|
</SlButton>
|
||||||
|
|
||||||
|
<SlAlert variant="primary" duration="3000" countdown="RtL" open={open} closable onSlAfterHide={() => setOpen(false)}>
|
||||||
|
<SlIcon slot="icon" name="info-circle" />
|
||||||
|
You're not stuck, the alert will close after a pretty long duration.
|
||||||
|
</SlAlert>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>{css}</style>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
### Toast Notifications
|
### Toast Notifications
|
||||||
|
|
||||||
To display an alert as a toast notification, or "toast", create the alert and call its `toast()` method. This will move the alert out of its position in the DOM and into [the toast stack](#the-toast-stack) where it will be shown. Once dismissed, it will be removed from the DOM completely. To reuse a toast, store a reference to it and call `toast()` again later on.
|
To display an alert as a toast notification, or "toast", create the alert and call its `toast()` method. This will move the alert out of its position in the DOM and into [the toast stack](#the-toast-stack) where it will be shown. Once dismissed, it will be removed from the DOM completely. To reuse a toast, store a reference to it and call `toast()` again later on.
|
||||||
|
|
|
@ -236,7 +236,7 @@ When a `target` is set, the link will receive `rel="noreferrer noopener"` for [s
|
||||||
|
|
||||||
### Setting a Custom Width
|
### Setting a Custom Width
|
||||||
|
|
||||||
As expected, buttons can be given a custom width by setting the `width` attribute. This is useful for making buttons span the full width of their container on smaller screens.
|
As expected, buttons can be given a custom width by passing inline styles to the component (or using a class). This is useful for making buttons span the full width of their container on smaller screens.
|
||||||
|
|
||||||
```html:preview
|
```html:preview
|
||||||
<sl-button variant="default" size="small" style="width: 100%; margin-bottom: 1rem;">Small</sl-button>
|
<sl-button variant="default" size="small" style="width: 100%; margin-bottom: 1rem;">Small</sl-button>
|
||||||
|
|
|
@ -14,7 +14,10 @@ New versions of Shoelace are released as-needed and generally occur when a criti
|
||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|
||||||
|
- Fixed a bug in `<sl-split-panel>` that caused it not to recalculate it's position when going from being `display: none;` to its original display value. [#1942]
|
||||||
|
- Fixed a bug in `<dialog>` where when it showed it would cause a layout shift. [#1967]
|
||||||
- Fixed a bug in `<sl-tooltip>` that allowed unwanted text properties to leak in [#1947]
|
- Fixed a bug in `<sl-tooltip>` that allowed unwanted text properties to leak in [#1947]
|
||||||
|
- Fixed a bug in `<sl-button-group>` classes [#1974]
|
||||||
|
|
||||||
## 2.15.0
|
## 2.15.0
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { getAnimation, setDefaultAnimation } from '../../utilities/animation-reg
|
||||||
import { HasSlotController } from '../../internal/slot.js';
|
import { HasSlotController } from '../../internal/slot.js';
|
||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
import { LocalizeController } from '../../utilities/localize.js';
|
import { LocalizeController } from '../../utilities/localize.js';
|
||||||
import { property, query } from 'lit/decorators.js';
|
import { property, query, state } from 'lit/decorators.js';
|
||||||
import { waitForEvent } from '../../internal/event.js';
|
import { waitForEvent } from '../../internal/event.js';
|
||||||
import { watch } from '../../internal/watch.js';
|
import { watch } from '../../internal/watch.js';
|
||||||
import componentStyles from '../../styles/component.styles.js';
|
import componentStyles from '../../styles/component.styles.js';
|
||||||
|
@ -45,11 +45,15 @@ export default class SlAlert extends ShoelaceElement {
|
||||||
static dependencies = { 'sl-icon-button': SlIconButton };
|
static dependencies = { 'sl-icon-button': SlIconButton };
|
||||||
|
|
||||||
private autoHideTimeout: number;
|
private autoHideTimeout: number;
|
||||||
|
private remainingTimeInterval: number;
|
||||||
|
private countdownAnimation?: Animation;
|
||||||
private readonly hasSlotController = new HasSlotController(this, 'icon', 'suffix');
|
private readonly hasSlotController = new HasSlotController(this, 'icon', 'suffix');
|
||||||
private readonly localize = new LocalizeController(this);
|
private readonly localize = new LocalizeController(this);
|
||||||
|
|
||||||
@query('[part~="base"]') base: HTMLElement;
|
@query('[part~="base"]') base: HTMLElement;
|
||||||
|
|
||||||
|
@query('.alert__countdown-elapsed') countdownElement: HTMLElement;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates whether or not the alert is open. You can toggle this attribute to show and hide the alert, or you can
|
* 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.
|
* use the `show()` and `hide()` methods and this attribute will reflect the alert's open state.
|
||||||
|
@ -69,14 +73,57 @@ export default class SlAlert extends ShoelaceElement {
|
||||||
*/
|
*/
|
||||||
@property({ type: Number }) duration = Infinity;
|
@property({ type: Number }) duration = Infinity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables a countdown that indicates the remaining time the alert will be displayed.
|
||||||
|
* Typically used to indicate the remaining time before a whole app refresh.
|
||||||
|
*/
|
||||||
|
@property({ type: String, reflect: true }) countdown?: 'RtL' | 'LtR';
|
||||||
|
|
||||||
|
@state() private remainingTime = this.duration;
|
||||||
|
|
||||||
firstUpdated() {
|
firstUpdated() {
|
||||||
this.base.hidden = !this.open;
|
this.base.hidden = !this.open;
|
||||||
}
|
}
|
||||||
|
|
||||||
private restartAutoHide() {
|
private restartAutoHide() {
|
||||||
|
this.handleCountdownChange();
|
||||||
clearTimeout(this.autoHideTimeout);
|
clearTimeout(this.autoHideTimeout);
|
||||||
|
clearInterval(this.remainingTimeInterval);
|
||||||
if (this.open && this.duration < Infinity) {
|
if (this.open && this.duration < Infinity) {
|
||||||
this.autoHideTimeout = window.setTimeout(() => this.hide(), this.duration);
|
this.autoHideTimeout = window.setTimeout(() => this.hide(), this.duration);
|
||||||
|
this.remainingTime = this.duration;
|
||||||
|
this.remainingTimeInterval = window.setInterval(() => {
|
||||||
|
this.remainingTime -= 100;
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private pauseAutoHide() {
|
||||||
|
this.countdownAnimation?.pause();
|
||||||
|
clearTimeout(this.autoHideTimeout);
|
||||||
|
clearInterval(this.remainingTimeInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
private resumeAutoHide() {
|
||||||
|
this.autoHideTimeout = window.setTimeout(() => this.hide(), this.remainingTime);
|
||||||
|
this.remainingTimeInterval = window.setInterval(() => {
|
||||||
|
this.remainingTime -= 100;
|
||||||
|
}, 100);
|
||||||
|
this.countdownAnimation?.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleCountdownChange() {
|
||||||
|
if(this.open && this.duration < Infinity && this.countdown) {
|
||||||
|
const { countdownElement } = this;
|
||||||
|
const start = this.countdown === 'LtR' ? '0' : '100%';
|
||||||
|
const end = this.countdown === 'LtR' ? '100%' : '0';
|
||||||
|
this.countdownAnimation = countdownElement.animate([
|
||||||
|
{ width: start },
|
||||||
|
{ width: end }
|
||||||
|
], {
|
||||||
|
duration: this.duration,
|
||||||
|
easing: 'linear'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,10 +131,6 @@ export default class SlAlert extends ShoelaceElement {
|
||||||
this.hide();
|
this.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleMouseMove() {
|
|
||||||
this.restartAutoHide();
|
|
||||||
}
|
|
||||||
|
|
||||||
@watch('open', { waitUntilFirstUpdate: true })
|
@watch('open', { waitUntilFirstUpdate: true })
|
||||||
async handleOpenChange() {
|
async handleOpenChange() {
|
||||||
if (this.open) {
|
if (this.open) {
|
||||||
|
@ -109,6 +152,7 @@ export default class SlAlert extends ShoelaceElement {
|
||||||
this.emit('sl-hide');
|
this.emit('sl-hide');
|
||||||
|
|
||||||
clearTimeout(this.autoHideTimeout);
|
clearTimeout(this.autoHideTimeout);
|
||||||
|
clearInterval(this.remainingTimeInterval);
|
||||||
|
|
||||||
await stopAnimations(this.base);
|
await stopAnimations(this.base);
|
||||||
const { keyframes, options } = getAnimation(this, 'alert.hide', { dir: this.localize.dir() });
|
const { keyframes, options } = getAnimation(this, 'alert.hide', { dir: this.localize.dir() });
|
||||||
|
@ -151,6 +195,7 @@ export default class SlAlert extends ShoelaceElement {
|
||||||
*/
|
*/
|
||||||
async toast() {
|
async toast() {
|
||||||
return new Promise<void>(resolve => {
|
return new Promise<void>(resolve => {
|
||||||
|
this.handleCountdownChange();
|
||||||
if (toastStack.parentElement === null) {
|
if (toastStack.parentElement === null) {
|
||||||
document.body.append(toastStack);
|
document.body.append(toastStack);
|
||||||
}
|
}
|
||||||
|
@ -188,6 +233,7 @@ export default class SlAlert extends ShoelaceElement {
|
||||||
alert: true,
|
alert: true,
|
||||||
'alert--open': this.open,
|
'alert--open': this.open,
|
||||||
'alert--closable': this.closable,
|
'alert--closable': this.closable,
|
||||||
|
'alert--has-countdown': !!this.countdown,
|
||||||
'alert--has-icon': this.hasSlotController.test('icon'),
|
'alert--has-icon': this.hasSlotController.test('icon'),
|
||||||
'alert--primary': this.variant === 'primary',
|
'alert--primary': this.variant === 'primary',
|
||||||
'alert--success': this.variant === 'success',
|
'alert--success': this.variant === 'success',
|
||||||
|
@ -197,7 +243,8 @@ export default class SlAlert extends ShoelaceElement {
|
||||||
})}
|
})}
|
||||||
role="alert"
|
role="alert"
|
||||||
aria-hidden=${this.open ? 'false' : 'true'}
|
aria-hidden=${this.open ? 'false' : 'true'}
|
||||||
@mousemove=${this.handleMouseMove}
|
@mouseenter=${this.pauseAutoHide}
|
||||||
|
@mouseleave=${this.resumeAutoHide}
|
||||||
>
|
>
|
||||||
<div part="icon" class="alert__icon">
|
<div part="icon" class="alert__icon">
|
||||||
<slot name="icon"></slot>
|
<slot name="icon"></slot>
|
||||||
|
@ -220,6 +267,18 @@ export default class SlAlert extends ShoelaceElement {
|
||||||
></sl-icon-button>
|
></sl-icon-button>
|
||||||
`
|
`
|
||||||
: ''}
|
: ''}
|
||||||
|
|
||||||
|
<div role="timer" class="alert__timer">${this.remainingTime}</div>
|
||||||
|
|
||||||
|
${this.countdown ? html`
|
||||||
|
<div class="alert__countdown">
|
||||||
|
<div class=${classMap({
|
||||||
|
'alert__countdown-elapsed': true,
|
||||||
|
'alert__countdown-elapsed--rtl': this.countdown !== 'LtR'
|
||||||
|
})}>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`: ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ export default css`
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
color: var(--sl-color-neutral-700);
|
color: var(--sl-color-neutral-700);
|
||||||
margin: inherit;
|
margin: inherit;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert:not(.alert--has-icon) .alert__icon,
|
.alert:not(.alert--has-icon) .alert__icon,
|
||||||
|
@ -37,6 +38,10 @@ export default css`
|
||||||
padding-inline-start: var(--sl-spacing-large);
|
padding-inline-start: var(--sl-spacing-large);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alert--has-countdown {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
.alert--primary {
|
.alert--primary {
|
||||||
border-top-color: var(--sl-color-primary-600);
|
border-top-color: var(--sl-color-primary-600);
|
||||||
}
|
}
|
||||||
|
@ -91,4 +96,45 @@ export default css`
|
||||||
font-size: var(--sl-font-size-medium);
|
font-size: var(--sl-font-size-medium);
|
||||||
padding-inline-end: var(--sl-spacing-medium);
|
padding-inline-end: var(--sl-spacing-medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alert__countdown {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: calc(var(--sl-panel-border-width) * 3);
|
||||||
|
background-color: var(--sl-panel-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert__countdown .alert__countdown-elapsed {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert__countdown .alert__countdown-elapsed--rtl {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert--primary .alert__countdown-elapsed {
|
||||||
|
background-color: var(--sl-color-primary-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert--success .alert__countdown-elapsed {
|
||||||
|
background-color: var(--sl-color-success-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert--neutral .alert__countdown-elapsed {
|
||||||
|
background-color: var(--sl-color-neutral-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert--warning .alert__countdown-elapsed {
|
||||||
|
background-color: var(--sl-color-warning-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert--danger .alert__countdown-elapsed {
|
||||||
|
background-color: var(--sl-color-danger-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert__timer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -590,7 +590,7 @@ export default css`
|
||||||
|
|
||||||
/* Focus and checked are always on top */
|
/* Focus and checked are always on top */
|
||||||
:host([data-sl-button-group__button--focus]),
|
:host([data-sl-button-group__button--focus]),
|
||||||
:host([data-sl-button-group__button[checked]]) {
|
:host([data-sl-button-group__button][checked]) {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -189,6 +189,14 @@ export default class SlSplitPanel extends ShoelaceElement {
|
||||||
const { width, height } = entries[0].contentRect;
|
const { width, height } = entries[0].contentRect;
|
||||||
this.size = this.vertical ? height : width;
|
this.size = this.vertical ? height : width;
|
||||||
|
|
||||||
|
// There's some weird logic that gets `this.cachedPositionInPixels = NaN` or `this.position === Infinity` when
|
||||||
|
// a split-panel goes from `display: none;` to showing.
|
||||||
|
if (isNaN(this.cachedPositionInPixels) || this.position === Infinity) {
|
||||||
|
this.cachedPositionInPixels = Number(this.getAttribute('position-in-pixels'));
|
||||||
|
this.positionInPixels = Number(this.getAttribute('position-in-pixels'));
|
||||||
|
this.position = this.pixelsToPercentage(this.positionInPixels);
|
||||||
|
}
|
||||||
|
|
||||||
// Resize when a primary panel is set
|
// Resize when a primary panel is set
|
||||||
if (this.primary) {
|
if (this.primary) {
|
||||||
this.position = this.pixelsToPercentage(this.cachedPositionInPixels);
|
this.position = this.pixelsToPercentage(this.cachedPositionInPixels);
|
||||||
|
|
|
@ -33,6 +33,19 @@ export function lockBodyScrolling(lockingEl: HTMLElement) {
|
||||||
if (!document.documentElement.classList.contains('sl-scroll-lock')) {
|
if (!document.documentElement.classList.contains('sl-scroll-lock')) {
|
||||||
/** Scrollbar width + body padding calculation can go away once Safari has scrollbar-gutter support. */
|
/** Scrollbar width + body padding calculation can go away once Safari has scrollbar-gutter support. */
|
||||||
const scrollbarWidth = getScrollbarWidth() + getExistingBodyPadding(); // must be measured before the `sl-scroll-lock` class is applied
|
const scrollbarWidth = getScrollbarWidth() + getExistingBodyPadding(); // must be measured before the `sl-scroll-lock` class is applied
|
||||||
|
|
||||||
|
let scrollbarGutterProperty = getComputedStyle(document.documentElement).scrollbarGutter;
|
||||||
|
|
||||||
|
// default is auto, unsupported browsers is "undefined"
|
||||||
|
if (!scrollbarGutterProperty || scrollbarGutterProperty === 'auto') {
|
||||||
|
scrollbarGutterProperty = 'stable';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scrollbarWidth <= 0) {
|
||||||
|
// if there's no scrollbar, just set it to "revert" so whatever the user has set gets used. This is useful is the page is not overflowing and showing a scrollbar, or if the user has overflow: hidden, or any other reason a scrollbar may not be showing.
|
||||||
|
scrollbarGutterProperty = 'revert';
|
||||||
|
}
|
||||||
|
document.documentElement.style.setProperty('--sl-scroll-lock-gutter', scrollbarGutterProperty);
|
||||||
document.documentElement.classList.add('sl-scroll-lock');
|
document.documentElement.classList.add('sl-scroll-lock');
|
||||||
document.documentElement.style.setProperty('--sl-scroll-lock-size', `${scrollbarWidth}px`);
|
document.documentElement.style.setProperty('--sl-scroll-lock-size', `${scrollbarWidth}px`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,10 @@
|
||||||
|
|
||||||
@supports (scrollbar-gutter: stable) {
|
@supports (scrollbar-gutter: stable) {
|
||||||
.sl-scroll-lock {
|
.sl-scroll-lock {
|
||||||
scrollbar-gutter: stable !important;
|
scrollbar-gutter: var(--sl-scroll-lock-gutter) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sl-scroll-lock body {
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Ładowanie…
Reference in New Issue