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
### 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
<sl-drawer label="Drawer" placement="left" class="drawer-placement-left">
This drawer slides in from the left.
<sl-drawer label="Drawer" placement="start" class="drawer-placement-start">
This drawer slides in from the start.
<sl-button slot="footer" type="primary">Close</sl-button>
</sl-drawer>
<sl-button>Open Drawer</sl-button>
<script>
const drawer = document.querySelector('.drawer-placement-left');
const drawer = document.querySelector('.drawer-placement-start');
const openButton = drawer.nextElementSibling;
const closeButton = drawer.querySelector('sl-button[type="primary"]');

Wyświetl plik

@ -17,10 +17,6 @@
height: 100%;
pointer-events: none;
overflow: hidden;
&:not(.drawer--visible) {
@include hide.hidden;
}
}
.drawer--contained {
@ -58,17 +54,15 @@
left: 0;
width: 100%;
height: var(--size);
transform: translate(0, -100%);
}
.drawer--right .drawer__panel {
.drawer--end .drawer__panel {
top: 0;
right: 0;
bottom: auto;
left: auto;
width: var(--size);
height: 100%;
transform: translate(100%, 0);
}
.drawer--bottom .drawer__panel {
@ -78,21 +72,15 @@
left: 0;
width: 100%;
height: var(--size);
transform: translate(0, 100%);
}
.drawer--left .drawer__panel {
.drawer--start .drawer__panel {
top: 0;
right: auto;
bottom: auto;
left: 0;
width: var(--size);
height: 100%;
transform: translate(-100%, 0);
}
.drawer--open .drawer__panel {
transform: translate(0, 0);
}
.drawer__header {
@ -142,15 +130,9 @@
bottom: 0;
left: 0;
background-color: var(--sl-overlay-background-color);
opacity: 0;
transition: var(--sl-transition-medium) opacity;
pointer-events: all;
}
.drawer--contained .drawer__overlay {
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 { classMap } from 'lit-html/directives/class-map';
import { ifDefined } from 'lit-html/directives/if-defined';
import { animateTo, stopAnimations } from '../../internal/animate';
import { event, EventEmitter, watch } from '../../internal/decorators';
import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll';
import { hasSlot } from '../../internal/slot';
import { uppercaseFirstLetter } from '../../internal/string';
import { isPreventScrollSupported } from '../../internal/support';
import Modal from '../../internal/modal';
import { setDefaultAnimation, getAnimation } from '../../utilities/animation-registry';
import styles from 'sass:./drawer.scss';
const hasPreventScroll = isPreventScrollSupported();
@ -32,11 +35,22 @@ let id = 0;
* @part body - The drawer body.
* @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
* `placement`. Note that the drawer will shrink to accommodate smaller screens.
* @customProperty --size - The preferred size of the drawer. This will be applied to the drawer's width or height
* 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 --body-spacing - The amount of padding to use for the body.
* @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')
export default class SlDrawer extends LitElement {
@ -44,15 +58,14 @@ export default class SlDrawer extends LitElement {
@query('.drawer') drawer: HTMLElement;
@query('.drawer__panel') panel: HTMLElement;
@query('.drawer__overlay') overlay: HTMLElement;
private componentId = `drawer-${++id}`;
private hasInitialized = false;
private modal: Modal;
private originalTrigger: HTMLElement | null;
private willShow = false;
private willHide = 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. */
@property({ type: Boolean, reflect: true }) open = false;
@ -64,7 +77,7 @@ export default class SlDrawer extends LitElement {
@property({ reflect: true }) label = '';
/** 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
@ -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() {
super.disconnectedCallback();
unlockBodyScrolling(this);
}
/** Shows the drawer */
show() {
if (this.willShow) {
async show() {
if (!this.hasInitialized) {
return;
}
@ -126,8 +148,6 @@ export default class SlDrawer extends LitElement {
}
this.originalTrigger = document.activeElement as HTMLElement;
this.willShow = true;
this.isVisible = true;
this.open = true;
// Lock body scrolling only if the drawer isn't contained
@ -136,40 +156,39 @@ export default class SlDrawer extends LitElement {
lockBodyScrolling(this);
}
if (this.open) {
if (hasPreventScroll) {
// Wait for the next frame before setting initial focus so the drawer is technically visible
requestAnimationFrame(() => {
const slInitialFocus = this.slInitialFocus.emit();
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 }
);
await Promise.all([stopAnimations(this.drawer), stopAnimations(this.overlay)]);
this.drawer.hidden = false;
// Browsers that support el.focus({ preventScroll }) can set initial focus immediately
if (hasPreventScroll) {
const slInitialFocus = this.slInitialFocus.emit();
if (!slInitialFocus.defaultPrevented) {
this.panel.focus({ preventScroll: 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 */
hide() {
if (this.willHide) {
async hide() {
if (!this.hasInitialized) {
return;
}
@ -179,15 +198,27 @@ export default class SlDrawer extends LitElement {
return;
}
this.willHide = true;
this.open = false;
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
const trigger = this.originalTrigger;
if (trigger && typeof trigger.focus === 'function') {
setTimeout(() => trigger.focus());
}
this.slAfterHide.emit();
}
handleCloseClick() {
@ -217,22 +248,6 @@ export default class SlDrawer extends LitElement {
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() {
return html`
<div
@ -240,17 +255,15 @@ export default class SlDrawer extends LitElement {
class=${classMap({
drawer: true,
'drawer--open': this.open,
'drawer--visible': this.isVisible,
'drawer--top': this.placement === 'top',
'drawer--right': this.placement === 'right',
'drawer--end': this.placement === 'end',
'drawer--bottom': this.placement === 'bottom',
'drawer--left': this.placement === 'left',
'drawer--start': this.placement === 'start',
'drawer--contained': this.contained,
'drawer--fixed': !this.contained,
'drawer--has-footer': this.hasFooter
})}
@keydown=${this.handleKeyDown}
@transitionend=${this.handleTransitionEnd}
>
<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 {
interface HTMLElementTagNameMap {
'sl-drawer': SlDrawer;

Wyświetl plik

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