2021-03-08 12:51:31 +00:00
import { LitElement , html , internalProperty , property , query , unsafeCSS } from 'lit-element' ;
2021-03-06 17:01:39 +00:00
import { classMap } from 'lit-html/directives/class-map' ;
2021-03-15 11:56:15 +00:00
import { ifDefined } from 'lit-html/directives/if-defined' ;
2021-03-08 12:51:31 +00:00
import { event , EventEmitter , tag } from '../../internal/decorators' ;
2021-02-26 14:09:13 +00:00
import styles from 'sass:./drawer.scss' ;
import { lockBodyScrolling , unlockBodyScrolling } from '../../internal/scroll' ;
import { hasSlot } from '../../internal/slot' ;
import { isPreventScrollSupported } from '../../internal/support' ;
import Modal from '../../internal/modal' ;
2020-07-15 21:30:37 +00:00
2021-01-08 15:25:29 +00:00
const hasPreventScroll = isPreventScrollSupported ( ) ;
2020-07-15 21:30:37 +00:00
let id = 0 ;
/ * *
2020-07-17 10:09:10 +00:00
* @since 2.0
2020-07-15 21:30:37 +00:00
* @status stable
*
2021-02-26 14:09:13 +00:00
* @dependency sl - icon - button
*
2020-07-15 21:30:37 +00:00
* @slot - The drawer ' s content .
2021-01-07 15:17:08 +00:00
* @slot label - The drawer ' s label . Alternatively , you can use the label prop .
2020-07-15 21:30:37 +00:00
* @slot footer - The drawer ' s footer , usually one or more buttons representing various options .
*
* @part base - The component ' s base wrapper .
* @part overlay - The overlay .
2020-11-25 21:16:03 +00:00
* @part panel - The drawer panel ( where the drawer and its content is rendered ) .
2020-07-15 21:30:37 +00:00
* @part header - The drawer header .
* @part title - The drawer title .
* @part close - button - The close button .
* @part body - The drawer body .
* @part footer - The drawer footer .
* /
2021-03-08 12:51:31 +00:00
@tag ( 'sl-drawer' )
2021-03-09 00:14:32 +00:00
export default class SlDrawer extends LitElement {
2021-03-06 17:01:39 +00:00
static styles = unsafeCSS ( styles ) ;
@query ( '.drawer' ) drawer : HTMLElement ;
@query ( '.drawer__panel' ) panel : HTMLElement ;
2021-02-26 14:09:13 +00:00
private componentId = ` drawer- ${ ++ id } ` ;
private modal : Modal ;
private willShow = false ;
private willHide = false ;
2020-07-24 12:38:00 +00:00
2021-03-06 17:01:39 +00:00
@internalProperty ( ) private hasFooter = false ;
@internalProperty ( ) private isVisible = false ;
2020-07-15 21:30:37 +00:00
/** Indicates whether or not the drawer is open. You can use this in lieu of the show/hide methods. */
2021-03-06 17:01:39 +00:00
@property ( { type : Boolean , reflect : true } ) open = false ;
2020-07-15 21:30:37 +00:00
/ * *
* The drawer ' s label as displayed in the header . You should always include a relevant label even when using
* ` no-header ` , as it is required for proper accessibility .
* /
2021-03-06 17:01:39 +00:00
@property ( { reflect : true } ) label = '' ;
2020-07-15 21:30:37 +00:00
/** The direction from which the drawer will open. */
2021-03-06 17:01:39 +00:00
@property ( { reflect : true } ) placement : 'top' | 'right' | 'bottom' | 'left' = 'right' ;
2020-07-15 21:30:37 +00:00
/ * *
* By default , the drawer slides out of its containing block ( usually the viewport ) . To make the drawer slide out of
* its parent element , set this prop and add ` position: relative ` to the parent .
* /
2021-03-06 17:01:39 +00:00
@property ( { type : Boolean , reflect : true } ) contained = false ;
2020-07-15 21:30:37 +00:00
/ * *
* Removes the header . This will also remove the default close button , so please ensure you provide an easy ,
* accessible way for users to dismiss the drawer .
* /
2021-03-06 17:01:39 +00:00
@property ( { attribute : 'no-header' , type : Boolean , reflect : true } ) noHeader = false ;
/** Emitted when the drawer opens. Calling `event.preventDefault()` will prevent it from being opened. */
@event ( 'sl-show' ) slShow : EventEmitter < void > ;
/** Emitted after the drawer opens and all transitions are complete. */
@event ( 'sl-after-show' ) slAfterShow : EventEmitter < void > ;
/** Emitted when the drawer closes. Calling `event.preventDefault()` will prevent it from being closed. */
@event ( 'sl-hide' ) slHide : EventEmitter < void > ;
/** Emitted after the drawer closes and all transitions are complete. */
@event ( 'sl-after-hide' ) slAfterHide : EventEmitter < void > ;
/** Emitted when the drawer opens and the panel gains focus. Calling `event.preventDefault()` will prevent focus and allow you to set it on a different element in the drawer, such as an input or button. */
@event ( 'sl-initial-focus' ) slInitialFocus : EventEmitter < void > ;
/** Emitted when the overlay is clicked. Calling `event.preventDefault()` will prevent the drawer from closing. */
@event ( 'sl-overlay-dismiss' ) slOverlayDismiss : EventEmitter < void > ;
connectedCallback() {
super . connectedCallback ( ) ;
2021-01-07 15:17:08 +00:00
2021-02-26 14:09:13 +00:00
this . modal = new Modal ( this , {
onfocusOut : ( ) = > ( this . contained ? null : this . panel . focus ( ) )
2020-10-15 17:55:42 +00:00
} ) ;
2020-07-21 19:18:58 +00:00
2020-12-22 22:08:19 +00:00
this . handleSlotChange ( ) ;
2020-07-15 21:30:37 +00:00
// Show on init if open
if ( this . open ) {
this . show ( ) ;
}
}
2021-03-06 17:01:39 +00:00
disconnectedCallback() {
super . disconnectedCallback ( ) ;
2021-02-26 14:09:13 +00:00
unlockBodyScrolling ( this ) ;
2020-07-15 21:30:37 +00:00
}
/** Shows the drawer */
2021-02-26 14:09:13 +00:00
show() {
2020-11-25 14:26:01 +00:00
if ( this . willShow ) {
2020-08-13 14:29:31 +00:00
return ;
}
2020-07-15 21:30:37 +00:00
2021-03-06 17:01:39 +00:00
const slShow = this . slShow . emit ( ) ;
2020-07-15 21:30:37 +00:00
if ( slShow . defaultPrevented ) {
2020-08-13 14:29:31 +00:00
this . open = false ;
return ;
2020-07-15 21:30:37 +00:00
}
2020-11-25 14:26:01 +00:00
this . willShow = true ;
2020-10-13 12:26:03 +00:00
this . isVisible = true ;
2020-08-13 14:29:31 +00:00
this . open = true ;
2020-07-15 21:30:37 +00:00
// Lock body scrolling only if the drawer isn't contained
if ( ! this . contained ) {
2020-10-15 17:55:42 +00:00
this . modal . activate ( ) ;
2021-02-26 14:09:13 +00:00
lockBodyScrolling ( this ) ;
2020-07-15 21:30:37 +00:00
}
2021-01-07 15:17:08 +00:00
if ( this . open ) {
2021-01-08 15:25:29 +00:00
if ( hasPreventScroll ) {
2021-02-26 14:09:13 +00:00
// Wait for the next frame before setting initial focus so the drawer is technically visible
2021-01-08 15:25:29 +00:00
requestAnimationFrame ( ( ) = > {
2021-03-06 17:01:39 +00:00
const slInitialFocus = this . slInitialFocus . emit ( ) ;
2021-01-08 15:25:29 +00:00
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
//
2021-01-11 13:09:05 +00:00
this . drawer . addEventListener (
'transitionend' ,
( ) = > {
2021-03-06 17:01:39 +00:00
const slInitialFocus = this . slInitialFocus . emit ( ) ;
2021-01-11 13:09:05 +00:00
if ( ! slInitialFocus . defaultPrevented ) {
this . panel . focus ( ) ;
}
} ,
{ once : true }
) ;
2021-01-08 15:25:29 +00:00
}
2021-01-07 15:17:08 +00:00
}
2020-07-15 21:30:37 +00:00
}
/** Hides the drawer */
2021-02-26 14:09:13 +00:00
hide() {
2020-11-25 14:26:01 +00:00
if ( this . willHide ) {
2020-08-13 14:29:31 +00:00
return ;
}
2020-07-15 21:30:37 +00:00
2021-03-06 17:01:39 +00:00
const slHide = this . slHide . emit ( ) ;
2020-07-15 21:30:37 +00:00
if ( slHide . defaultPrevented ) {
2020-08-13 14:29:31 +00:00
this . open = true ;
return ;
2020-07-15 21:30:37 +00:00
}
2020-11-25 14:26:01 +00:00
this . willHide = true ;
2020-07-15 21:30:37 +00:00
this . open = false ;
2020-10-15 17:55:42 +00:00
this . modal . deactivate ( ) ;
2020-07-15 21:30:37 +00:00
2021-02-26 14:09:13 +00:00
unlockBodyScrolling ( this ) ;
2020-07-15 21:30:37 +00:00
}
handleCloseClick() {
this . hide ( ) ;
}
handleKeyDown ( event : KeyboardEvent ) {
if ( event . key === 'Escape' ) {
this . hide ( ) ;
}
}
handleOverlayClick() {
2021-03-06 17:01:39 +00:00
const slOverlayDismiss = this . slOverlayDismiss . emit ( ) ;
2020-07-15 21:30:37 +00:00
if ( ! slOverlayDismiss . defaultPrevented ) {
this . hide ( ) ;
}
}
2020-10-15 20:35:11 +00:00
handleSlotChange() {
2021-02-26 14:09:13 +00:00
this . hasFooter = hasSlot ( this , 'footer' ) ;
2020-10-15 20:35:11 +00:00
}
2020-07-15 21:30:37 +00:00
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' ) ) {
2020-10-13 12:26:03 +00:00
this . isVisible = this . open ;
2020-11-25 14:26:01 +00:00
this . willShow = false ;
this . willHide = false ;
2021-03-06 17:01:39 +00:00
this . open ? this . slAfterShow . emit ( ) : this . slAfterHide . emit ( ) ;
2020-07-15 21:30:37 +00:00
}
}
render() {
2021-02-26 14:09:13 +00:00
return html `
2020-07-15 21:30:37 +00:00
< div
part = "base"
2021-02-26 14:09:13 +00:00
class = $ { classMap ( {
2020-07-15 21:30:37 +00:00
drawer : true ,
'drawer--open' : this . open ,
2020-10-13 12:26:03 +00:00
'drawer--visible' : this . isVisible ,
2020-07-15 21:30:37 +00:00
'drawer--top' : this . placement === 'top' ,
'drawer--right' : this . placement === 'right' ,
'drawer--bottom' : this . placement === 'bottom' ,
'drawer--left' : this . placement === 'left' ,
'drawer--contained' : this . contained ,
2020-07-24 12:38:00 +00:00
'drawer--fixed' : ! this . contained ,
'drawer--has-footer' : this . hasFooter
2021-02-26 14:09:13 +00:00
} ) }
2021-03-06 17:01:39 +00:00
@keydown = $ { this . handleKeyDown }
@transitionend = $ { this . handleTransitionEnd }
2020-07-15 21:30:37 +00:00
>
2021-03-06 17:01:39 +00:00
< div part = "overlay" class = "drawer__overlay" @ click = $ { this.handleOverlayClick } tabindex = "-1" > < / div >
2020-07-15 21:30:37 +00:00
< div
part = "panel"
class = "drawer__panel"
role = "dialog"
aria - modal = "true"
2021-02-26 14:09:13 +00:00
aria - hidden = $ { this . open ? 'false' : 'true' }
2021-03-15 11:56:15 +00:00
aria - label = $ { ifDefined ( this . noHeader ? this . label : undefined ) }
aria - labelledby = $ { ifDefined ( ! this . noHeader ? ` ${ this . componentId } -title ` : undefined ) }
2021-02-26 14:09:13 +00:00
tabindex = "0"
2020-07-15 21:30:37 +00:00
>
2021-02-26 14:09:13 +00:00
$ { ! this . noHeader
? html `
< header part = "header" class = "drawer__header" >
< span part = "title" class = "drawer__title" id = $ { ` $ { this.componentId } - title ` } >
<!-- If there ' s no label , use an invisible character to prevent the heading from collapsing -- >
< slot name = "label" > $ { this . label || String . fromCharCode ( 65279 ) } < / slot >
< / span >
< sl - icon - button
exportparts = "base:close-button"
class = "drawer__close"
name = "x"
2021-03-06 17:01:39 +00:00
@click = $ { this . handleCloseClick }
> < / s l - i c o n - b u t t o n >
2021-02-26 14:09:13 +00:00
< / header >
`
: '' }
2020-07-15 21:30:37 +00:00
< div part = "body" class = "drawer__body" >
2021-03-06 17:01:39 +00:00
< slot > < / slot >
2020-07-15 21:30:37 +00:00
< / div >
2020-07-24 12:38:00 +00:00
< footer part = "footer" class = "drawer__footer" >
2021-03-06 17:01:39 +00:00
< slot name = "footer" @ slotchange = $ { this.handleSlotChange } > < / slot >
2020-07-24 12:38:00 +00:00
< / footer >
2020-07-15 21:30:37 +00:00
< / div >
< / div >
2021-02-26 14:09:13 +00:00
` ;
2020-07-15 21:30:37 +00:00
}
}
2021-03-12 14:07:38 +00:00
2021-03-12 14:09:08 +00:00
declare global {
interface HTMLElementTagNameMap {
'sl-drawer' : SlDrawer ;
}
}