pull/463/head
Cory LaViska 2021-05-26 07:32:16 -04:00
rodzic 01bb476023
commit cadbae85a5
4 zmienionych plików z 162 dodań i 85 usunięć

Wyświetl plik

@ -24,20 +24,20 @@ Drawers slide in from a container to expose additional options and information.
## Examples ## Examples
### Slide in From Left ### Slide in From Start
To make the drawer slide in from the left, set the `placement` attribute to `left`. By default, drawers slide in from the end. To make the drawer slide in from the start, set the `placement` attribute to `start`.
```html preview ```html preview
<sl-drawer label="Drawer" placement="left" class="drawer-placement-left"> <sl-drawer label="Drawer" placement="start" class="drawer-placement-start">
This drawer slides in from the left. This drawer slides in from the start.
<sl-button slot="footer" type="primary">Close</sl-button> <sl-button slot="footer" type="primary">Close</sl-button>
</sl-drawer> </sl-drawer>
<sl-button>Open Drawer</sl-button> <sl-button>Open Drawer</sl-button>
<script> <script>
const drawer = document.querySelector('.drawer-placement-left'); const drawer = document.querySelector('.drawer-placement-start');
const openButton = drawer.nextElementSibling; const openButton = drawer.nextElementSibling;
const closeButton = drawer.querySelector('sl-button[type="primary"]'); const closeButton = drawer.querySelector('sl-button[type="primary"]');

Wyświetl plik

@ -17,10 +17,6 @@
height: 100%; height: 100%;
pointer-events: none; pointer-events: none;
overflow: hidden; overflow: hidden;
&:not(.drawer--visible) {
@include hide.hidden;
}
} }
.drawer--contained { .drawer--contained {
@ -58,17 +54,15 @@
left: 0; left: 0;
width: 100%; width: 100%;
height: var(--size); height: var(--size);
transform: translate(0, -100%);
} }
.drawer--right .drawer__panel { .drawer--end .drawer__panel {
top: 0; top: 0;
right: 0; right: 0;
bottom: auto; bottom: auto;
left: auto; left: auto;
width: var(--size); width: var(--size);
height: 100%; height: 100%;
transform: translate(100%, 0);
} }
.drawer--bottom .drawer__panel { .drawer--bottom .drawer__panel {
@ -78,21 +72,15 @@
left: 0; left: 0;
width: 100%; width: 100%;
height: var(--size); height: var(--size);
transform: translate(0, 100%);
} }
.drawer--left .drawer__panel { .drawer--start .drawer__panel {
top: 0; top: 0;
right: auto; right: auto;
bottom: auto; bottom: auto;
left: 0; left: 0;
width: var(--size); width: var(--size);
height: 100%; height: 100%;
transform: translate(-100%, 0);
}
.drawer--open .drawer__panel {
transform: translate(0, 0);
} }
.drawer__header { .drawer__header {
@ -142,15 +130,9 @@
bottom: 0; bottom: 0;
left: 0; left: 0;
background-color: var(--sl-overlay-background-color); background-color: var(--sl-overlay-background-color);
opacity: 0;
transition: var(--sl-transition-medium) opacity;
pointer-events: all; pointer-events: all;
} }
.drawer--contained .drawer__overlay { .drawer--contained .drawer__overlay {
position: absolute; position: absolute;
} }
.drawer--open .drawer__overlay {
opacity: 1;
}

Wyświetl plik

@ -2,11 +2,14 @@ import { LitElement, html, unsafeCSS } from 'lit';
import { customElement, property, query, state } from 'lit/decorators'; import { customElement, property, query, state } from 'lit/decorators';
import { classMap } from 'lit-html/directives/class-map'; import { classMap } from 'lit-html/directives/class-map';
import { ifDefined } from 'lit-html/directives/if-defined'; import { ifDefined } from 'lit-html/directives/if-defined';
import { animateTo, stopAnimations } from '../../internal/animate';
import { event, EventEmitter, watch } from '../../internal/decorators'; import { event, EventEmitter, watch } from '../../internal/decorators';
import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll'; import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll';
import { hasSlot } from '../../internal/slot'; import { hasSlot } from '../../internal/slot';
import { uppercaseFirstLetter } from '../../internal/string';
import { isPreventScrollSupported } from '../../internal/support'; import { isPreventScrollSupported } from '../../internal/support';
import Modal from '../../internal/modal'; import Modal from '../../internal/modal';
import { setDefaultAnimation, getAnimation } from '../../utilities/animation-registry';
import styles from 'sass:./drawer.scss'; import styles from 'sass:./drawer.scss';
const hasPreventScroll = isPreventScrollSupported(); const hasPreventScroll = isPreventScrollSupported();
@ -32,11 +35,22 @@ let id = 0;
* @part body - The drawer body. * @part body - The drawer body.
* @part footer - The drawer footer. * @part footer - The drawer footer.
* *
* @customProperty --size - The preferred size of the drawer. This will be applied to the drawer's width or height depending on its * @customProperty --size - The preferred size of the drawer. This will be applied to the drawer's width or height
* `placement`. Note that the drawer will shrink to accommodate smaller screens. * depending on its `placement`. Note that the drawer will shrink to accommodate smaller screens.
* @customProperty --header-spacing - The amount of padding to use for the header. * @customProperty --header-spacing - The amount of padding to use for the header.
* @customProperty --body-spacing - The amount of padding to use for the body. * @customProperty --body-spacing - The amount of padding to use for the body.
* @customProperty --footer-spacing - The amount of padding to use for the footer. * @customProperty --footer-spacing - The amount of padding to use for the footer.
*
* @animation drawer.showTop - The animation to use when showing a drawer with `top` placement.
* @animation drawer.showEnd - The animation to use when showing a drawer with `end` placement.
* @animation drawer.showBottom - The animation to use when showing a drawer with `bottom` placement.
* @animation drawer.showStart - The animation to use when showing a drawer with `start` placement.
* @animation drawer.hideTop - The animation to use when hiding a drawer with `top` placement.
* @animation drawer.hideEnd - The animation to use when hiding a drawer with `end` placement.
* @animation drawer.hideBottom - The animation to use when hiding a drawer with `bottom` placement.
* @animation drawer.hideStart - The animation to use when hiding a drawer with `start` placement.
* @animation drawer.overlay.show - The animation to use when showing the drawer's overlay.
* @animation drawer.overlay.hide - The animation to use when hiding the drawer's overlay.
*/ */
@customElement('sl-drawer') @customElement('sl-drawer')
export default class SlDrawer extends LitElement { export default class SlDrawer extends LitElement {
@ -44,15 +58,14 @@ export default class SlDrawer extends LitElement {
@query('.drawer') drawer: HTMLElement; @query('.drawer') drawer: HTMLElement;
@query('.drawer__panel') panel: HTMLElement; @query('.drawer__panel') panel: HTMLElement;
@query('.drawer__overlay') overlay: HTMLElement;
private componentId = `drawer-${++id}`; private componentId = `drawer-${++id}`;
private hasInitialized = false;
private modal: Modal; private modal: Modal;
private originalTrigger: HTMLElement | null; private originalTrigger: HTMLElement | null;
private willShow = false;
private willHide = false;
@state() private hasFooter = false; @state() private hasFooter = false;
@state() private isVisible = false;
/** Indicates whether or not the drawer is open. You can use this in lieu of the show/hide methods. */ /** Indicates whether or not the drawer is open. You can use this in lieu of the show/hide methods. */
@property({ type: Boolean, reflect: true }) open = false; @property({ type: Boolean, reflect: true }) open = false;
@ -64,7 +77,7 @@ export default class SlDrawer extends LitElement {
@property({ reflect: true }) label = ''; @property({ reflect: true }) label = '';
/** The direction from which the drawer will open. */ /** The direction from which the drawer will open. */
@property({ reflect: true }) placement: 'top' | 'right' | 'bottom' | 'left' = 'right'; @property({ reflect: true }) placement: 'top' | 'end' | 'bottom' | 'start' = 'end';
/** /**
* By default, the drawer slides out of its containing block (usually the viewport). To make the drawer slide out of * By default, the drawer slides out of its containing block (usually the viewport). To make the drawer slide out of
@ -108,14 +121,23 @@ export default class SlDrawer extends LitElement {
} }
} }
async firstUpdated() {
// Set initial visibility
this.drawer.hidden = !this.open;
// Set the initialized flag after the first update is complete
await this.updateComplete;
this.hasInitialized = true;
}
disconnectedCallback() { disconnectedCallback() {
super.disconnectedCallback(); super.disconnectedCallback();
unlockBodyScrolling(this); unlockBodyScrolling(this);
} }
/** Shows the drawer */ /** Shows the drawer */
show() { async show() {
if (this.willShow) { if (!this.hasInitialized) {
return; return;
} }
@ -126,8 +148,6 @@ export default class SlDrawer extends LitElement {
} }
this.originalTrigger = document.activeElement as HTMLElement; this.originalTrigger = document.activeElement as HTMLElement;
this.willShow = true;
this.isVisible = true;
this.open = true; this.open = true;
// Lock body scrolling only if the drawer isn't contained // Lock body scrolling only if the drawer isn't contained
@ -136,40 +156,39 @@ export default class SlDrawer extends LitElement {
lockBodyScrolling(this); lockBodyScrolling(this);
} }
if (this.open) { await Promise.all([stopAnimations(this.drawer), stopAnimations(this.overlay)]);
if (hasPreventScroll) { this.drawer.hidden = false;
// Wait for the next frame before setting initial focus so the drawer is technically visible
requestAnimationFrame(() => { // Browsers that support el.focus({ preventScroll }) can set initial focus immediately
const slInitialFocus = this.slInitialFocus.emit(); if (hasPreventScroll) {
if (!slInitialFocus.defaultPrevented) { const slInitialFocus = this.slInitialFocus.emit();
this.panel.focus({ preventScroll: true }); if (!slInitialFocus.defaultPrevented) {
} this.panel.focus({ preventScroll: true });
});
} else {
// Once Safari supports { preventScroll: true } we can remove this nasty little hack, but until then we need to
// wait for the transition to complete before setting focus, otherwise the panel may render in a buggy way its
// out of view initially.
//
// Fiddle: https://jsfiddle.net/g6buoafq/1/
// Safari: https://bugs.webkit.org/show_bug.cgi?id=178583
//
this.drawer.addEventListener(
'transitionend',
() => {
const slInitialFocus = this.slInitialFocus.emit();
if (!slInitialFocus.defaultPrevented) {
this.panel.focus();
}
},
{ once: true }
);
} }
} }
const panelAnimation = getAnimation(this, `drawer.show${uppercaseFirstLetter(this.placement)}`);
const overlayAnimation = getAnimation(this, 'drawer.overlay.show');
await Promise.all([
animateTo(this.panel, panelAnimation.keyframes, panelAnimation.options),
animateTo(this.overlay, overlayAnimation.keyframes, overlayAnimation.options)
]);
// Browsers that don't support el.focus({ preventScroll }) have to wait for the animation to finish before initial
// focus to prevent scrolling issues. See: https://caniuse.com/mdn-api_htmlelement_focus_preventscroll_option
if (!hasPreventScroll) {
const slInitialFocus = this.slInitialFocus.emit();
if (!slInitialFocus.defaultPrevented) {
this.panel.focus({ preventScroll: true });
}
}
this.slAfterShow.emit();
} }
/** Hides the drawer */ /** Hides the drawer */
hide() { async hide() {
if (this.willHide) { if (!this.hasInitialized) {
return; return;
} }
@ -179,15 +198,27 @@ export default class SlDrawer extends LitElement {
return; return;
} }
this.willHide = true;
this.open = false; this.open = false;
this.modal.deactivate(); this.modal.deactivate();
unlockBodyScrolling(this);
await Promise.all([stopAnimations(this.drawer), stopAnimations(this.overlay)]);
const panelAnimation = getAnimation(this, `drawer.hide${uppercaseFirstLetter(this.placement)}`);
const overlayAnimation = getAnimation(this, 'drawer.overlay.hide');
await Promise.all([
animateTo(this.panel, panelAnimation.keyframes, panelAnimation.options),
animateTo(this.overlay, overlayAnimation.keyframes, overlayAnimation.options)
]);
this.drawer.hidden = true;
// Restore focus to the original trigger // Restore focus to the original trigger
const trigger = this.originalTrigger; const trigger = this.originalTrigger;
if (trigger && typeof trigger.focus === 'function') { if (trigger && typeof trigger.focus === 'function') {
setTimeout(() => trigger.focus()); setTimeout(() => trigger.focus());
} }
this.slAfterHide.emit();
} }
handleCloseClick() { handleCloseClick() {
@ -217,22 +248,6 @@ export default class SlDrawer extends LitElement {
this.hasFooter = hasSlot(this, 'footer'); this.hasFooter = hasSlot(this, 'footer');
} }
handleTransitionEnd(event: TransitionEvent) {
const target = event.target as HTMLElement;
// Ensure we only emit one event when the target element is no longer visible
if (event.propertyName === 'transform' && target.classList.contains('drawer__panel')) {
this.isVisible = this.open;
this.willShow = false;
this.willHide = false;
this.open ? this.slAfterShow.emit() : this.slAfterHide.emit();
if (!this.open) {
unlockBodyScrolling(this);
}
}
}
render() { render() {
return html` return html`
<div <div
@ -240,17 +255,15 @@ export default class SlDrawer extends LitElement {
class=${classMap({ class=${classMap({
drawer: true, drawer: true,
'drawer--open': this.open, 'drawer--open': this.open,
'drawer--visible': this.isVisible,
'drawer--top': this.placement === 'top', 'drawer--top': this.placement === 'top',
'drawer--right': this.placement === 'right', 'drawer--end': this.placement === 'end',
'drawer--bottom': this.placement === 'bottom', 'drawer--bottom': this.placement === 'bottom',
'drawer--left': this.placement === 'left', 'drawer--start': this.placement === 'start',
'drawer--contained': this.contained, 'drawer--contained': this.contained,
'drawer--fixed': !this.contained, 'drawer--fixed': !this.contained,
'drawer--has-footer': this.hasFooter 'drawer--has-footer': this.hasFooter
})} })}
@keydown=${this.handleKeyDown} @keydown=${this.handleKeyDown}
@transitionend=${this.handleTransitionEnd}
> >
<div part="overlay" class="drawer__overlay" @click=${this.handleOverlayClick} tabindex="-1"></div> <div part="overlay" class="drawer__overlay" @click=${this.handleOverlayClick} tabindex="-1"></div>
@ -295,6 +308,85 @@ export default class SlDrawer extends LitElement {
} }
} }
// Top
setDefaultAnimation('drawer.showTop', {
keyframes: [
{ opacity: 0, transform: 'translateY(-100%)' },
{ opacity: 1, transform: 'translateY(0)' }
],
options: { duration: 250, easing: 'ease' }
});
setDefaultAnimation('drawer.hideTop', {
keyframes: [
{ opacity: 1, transform: 'translateY(0)' },
{ opacity: 0, transform: 'translateY(-100%)' }
],
options: { duration: 250, easing: 'ease' }
});
// End
setDefaultAnimation('drawer.showEnd', {
keyframes: [
{ opacity: 0, transform: 'translateX(100%)' },
{ opacity: 1, transform: 'translateX(0)' }
],
options: { duration: 250, easing: 'ease' }
});
setDefaultAnimation('drawer.hideEnd', {
keyframes: [
{ opacity: 1, transform: 'translateX(0)' },
{ opacity: 0, transform: 'translateX(100%)' }
],
options: { duration: 250, easing: 'ease' }
});
// Bottom
setDefaultAnimation('drawer.showBottom', {
keyframes: [
{ opacity: 0, transform: 'translateY(100%)' },
{ opacity: 1, transform: 'translateY(0)' }
],
options: { duration: 250, easing: 'ease' }
});
setDefaultAnimation('drawer.hideBottom', {
keyframes: [
{ opacity: 1, transform: 'translateY(0)' },
{ opacity: 0, transform: 'translateY(100%)' }
],
options: { duration: 250, easing: 'ease' }
});
// Start
setDefaultAnimation('drawer.showStart', {
keyframes: [
{ opacity: 0, transform: 'translateX(-100%)' },
{ opacity: 1, transform: 'translateX(0)' }
],
options: { duration: 250, easing: 'ease' }
});
setDefaultAnimation('drawer.hideStart', {
keyframes: [
{ opacity: 1, transform: 'translateX(0)' },
{ opacity: 0, transform: 'translateX(-100%)' }
],
options: { duration: 250, easing: 'ease' }
});
// Overlay
setDefaultAnimation('drawer.overlay.show', {
keyframes: [{ opacity: 0 }, { opacity: 1 }],
options: { duration: 250 }
});
setDefaultAnimation('drawer.overlay.hide', {
keyframes: [{ opacity: 1 }, { opacity: 0 }],
options: { duration: 250 }
});
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
'sl-drawer': SlDrawer; 'sl-drawer': SlDrawer;

Wyświetl plik

@ -0,0 +1,3 @@
export function uppercaseFirstLetter(string: string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}