1- import { css , html , LitElement } from "lit" ;
2- import { customElement , query , state } from "lit/decorators" ;
3- import { styleMap } from "lit/directives/style-map" ;
4- import { fireEvent } from "../common/dom/fire_event" ;
1+ import { css , html , LitElement , type PropertyValues } from "lit" ;
2+ import "@home-assistant/webawesome/dist/components/drawer/drawer" ;
3+ import { customElement , property , state } from "lit/decorators" ;
54
6- const ANIMATION_DURATION_MS = 300 ;
5+ export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300 ;
76
8- /**
9- * A bottom sheet component that slides up from the bottom of the screen.
10- *
11- * The bottom sheet provides a draggable interface that allows users to resize
12- * the sheet by dragging the handle at the top. It supports both mouse and touch
13- * interactions and automatically closes when dragged below a 20% of screen height.
14- *
15- * @fires bottom-sheet-closed - Fired when the bottom sheet is closed
16- *
17- * @cssprop --ha-bottom-sheet-border-width - Border width for the sheet
18- * @cssprop --ha-bottom-sheet-border-style - Border style for the sheet
19- * @cssprop --ha-bottom-sheet-border-color - Border color for the sheet
20- */
217@customElement ( "ha-bottom-sheet" )
228export class HaBottomSheet extends LitElement {
23- @query ( "dialog" ) private _dialog ! : HTMLDialogElement ;
9+ @property ( { type : Boolean } ) public open = false ;
2410
25- private _dragging = false ;
11+ @ state ( ) private _drawerOpen = false ;
2612
27- private _dragStartY = 0 ;
28-
29- private _initialSize = 0 ;
30-
31- @state ( ) private _dialogMaxViewpointHeight = 70 ;
32-
33- @state ( ) private _dialogMinViewpointHeight = 55 ;
34-
35- @state ( ) private _dialogViewportHeight ?: number ;
36-
37- render ( ) {
38- return html `<dialog
39- open
40- @transitionend = ${ this . _handleTransitionEnd }
41- style= ${ styleMap ( {
42- height : this . _dialogViewportHeight
43- ? `${ this . _dialogViewportHeight } vh`
44- : "auto" ,
45- maxHeight : `${ this . _dialogMaxViewpointHeight } vh` ,
46- minHeight : `${ this . _dialogMinViewpointHeight } vh` ,
47- } ) }
48- >
49- <div class= "handle-wrapper" >
50- <div
51- @mousedown = ${ this . _handleMouseDown }
52- @touchstart = ${ this . _handleTouchStart }
53- class= "handle"
54- > </ div>
55- </ div>
56- <slot> </ slot>
57- </ dialog> ` ;
58- }
59-
60- protected firstUpdated ( changedProperties ) {
61- super . firstUpdated ( changedProperties ) ;
62- this . _openSheet ( ) ;
63- }
64-
65- private _openSheet ( ) {
66- requestAnimationFrame ( ( ) => {
67- // trigger opening animation
68- this . _dialog . classList . add ( "show" ) ;
69- } ) ;
70- }
71-
72- public closeSheet ( ) {
73- requestAnimationFrame ( ( ) => {
74- this . _dialog . classList . remove ( "show" ) ;
75- } ) ;
76- }
77-
78- private _handleTransitionEnd ( ) {
79- if ( this . _dialog . classList . contains ( "show" ) ) {
80- // after show animation is done
81- // - set the height to the natural height, to prevent content shift when switch content
82- // - set max height to 90vh, so it opens at max 70vh but can be resized to 90vh
83- this . _dialogViewportHeight =
84- ( this . _dialog . offsetHeight / window . innerHeight ) * 100 ;
85- this . _dialogMaxViewpointHeight = 90 ;
86- this . _dialogMinViewpointHeight = 20 ;
87- } else {
88- // after close animation is done close dialog element and fire closed event
89- this . _dialog . close ( ) ;
90- fireEvent ( this , "bottom-sheet-closed" ) ;
91- }
92- }
93-
94- connectedCallback ( ) {
95- super . connectedCallback ( ) ;
96-
97- // register event listeners for drag handling
98- document . addEventListener ( "mousemove" , this . _handleMouseMove ) ;
99- document . addEventListener ( "mouseup" , this . _handleMouseUp ) ;
100- document . addEventListener ( "touchmove" , this . _handleTouchMove , {
101- passive : false ,
13+ private _handleAfterHide ( ) {
14+ this . open = false ;
15+ const ev = new Event ( "closed" , {
16+ bubbles : true ,
17+ composed : true ,
10218 } ) ;
103- document . addEventListener ( "touchend" , this . _handleTouchEnd ) ;
104- document . addEventListener ( "touchcancel" , this . _handleTouchEnd ) ;
105- }
106-
107- disconnectedCallback ( ) {
108- super . disconnectedCallback ( ) ;
109-
110- // unregister event listeners for drag handling
111- document . removeEventListener ( "mousemove" , this . _handleMouseMove ) ;
112- document . removeEventListener ( "mouseup" , this . _handleMouseUp ) ;
113- document . removeEventListener ( "touchmove" , this . _handleTouchMove ) ;
114- document . removeEventListener ( "touchend" , this . _handleTouchEnd ) ;
115- document . removeEventListener ( "touchcancel" , this . _handleTouchEnd ) ;
116- }
117-
118- private _handleMouseDown = ( ev : MouseEvent ) => {
119- this . _startDrag ( ev . clientY ) ;
120- } ;
121-
122- private _handleTouchStart = ( ev : TouchEvent ) => {
123- // Prevent the browser from interpreting this as a scroll/PTR gesture.
124- ev . preventDefault ( ) ;
125- this . _startDrag ( ev . touches [ 0 ] . clientY ) ;
126- } ;
127-
128- private _startDrag ( clientY : number ) {
129- this . _dragging = true ;
130- this . _dragStartY = clientY ;
131- this . _initialSize = ( this . _dialog . offsetHeight / window . innerHeight ) * 100 ;
132- document . body . style . setProperty ( "cursor" , "grabbing" ) ;
19+ this . dispatchEvent ( ev ) ;
13320 }
13421
135- private _handleMouseMove = ( ev : MouseEvent ) => {
136- if ( ! this . _dragging ) {
137- return ;
138- }
139- this . _updateSize ( ev . clientY ) ;
140- } ;
141-
142- private _handleTouchMove = ( ev : TouchEvent ) => {
143- if ( ! this . _dragging ) {
144- return ;
145- }
146- ev . preventDefault ( ) ; // Prevent scrolling
147- this . _updateSize ( ev . touches [ 0 ] . clientY ) ;
148- } ;
149-
150- private _updateSize ( clientY : number ) {
151- const deltaY = this . _dragStartY - clientY ;
152- const viewportHeight = window . innerHeight ;
153- const deltaVh = ( deltaY / viewportHeight ) * 100 ;
154-
155- // Calculate new size and clamp between 10vh and 90vh
156- let newSize = this . _initialSize + deltaVh ;
157- newSize = Math . max ( 10 , Math . min ( 90 , newSize ) ) ;
158-
159- // on drag down and below 20vh
160- if ( newSize < 20 && deltaY < 0 ) {
161- this . _endDrag ( ) ;
162- this . closeSheet ( ) ;
163- return ;
22+ protected updated ( changedProperties : PropertyValues ) : void {
23+ super . updated ( changedProperties ) ;
24+ if ( changedProperties . has ( "open" ) ) {
25+ this . _drawerOpen = this . open ;
16426 }
165-
166- this . _dialogViewportHeight = newSize ;
16727 }
16828
169- private _handleMouseUp = ( ) => {
170- this . _endDrag ( ) ;
171- } ;
172-
173- private _handleTouchEnd = ( ) => {
174- this . _endDrag ( ) ;
175- } ;
176-
177- private _endDrag ( ) {
178- if ( ! this . _dragging ) {
179- return ;
180- }
181- this . _dragging = false ;
182- document . body . style . removeProperty ( "cursor" ) ;
29+ render ( ) {
30+ return html `
31+ <wa- drawer
32+ placement= "bottom"
33+ .open = ${ this . _drawerOpen }
34+ @wa-after-hide = ${ this . _handleAfterHide }
35+ without- header
36+ >
37+ <slot> </ slot>
38+ </ wa- drawer>
39+ ` ;
18340 }
18441
18542 static styles = css `
186- .handle-wrapper {
187- position : absolute;
188- top : 0 ;
189- width : 100% ;
190- padding-bottom : 2px ;
191- display : flex;
192- justify-content : center;
193- align-items : center;
194- cursor : grab;
195- touch-action : none;
196- }
197- .handle-wrapper .handle {
198- height : 20px ;
199- width : 200px ;
200- display : flex;
201- justify-content : center;
202- align-items : center;
203- z-index : 7 ;
204- padding-bottom : 76px ;
205- }
206- .handle-wrapper .handle ::after {
207- content : "" ;
208- border-radius : 8px ;
209- height : 4px ;
210- background : var (--divider-color , # e0e0e0 );
211- width : 80px ;
212- }
213- .handle-wrapper .handle : active ::after {
214- cursor : grabbing;
215- }
216- dialog {
217- height : auto;
218- max-height : 70vh ;
219- min-height : 30vh ;
220- background-color : var (
43+ wa- drawer {
44+ --wa-color-surface-raised : var (
22145 --ha-dialog-surface-background ,
22246 var (--mdc-theme-surface , # fff )
22347 );
224- display : flex;
225- flex-direction : column;
226- top : 0 ;
227- inset-inline-start : 0 ;
228- position : fixed;
229- width : calc (100% - 4px );
230- max-width : 100% ;
231- border : none;
232- box-shadow : var (--wa-shadow-l );
233- padding : 0 ;
234- margin : 0 ;
235- top : auto;
236- inset-inline-end : auto;
237- bottom : 0 ;
238- inset-inline-start : 0 ;
239- box-shadow : 0px -8px 16px rgba (0 , 0 , 0 , 0.2 );
240- border-top-left-radius : var (
241- --ha-dialog-border-radius ,
242- var (--ha-border-radius-2xl )
243- );
244- border-top-right-radius : var (
245- --ha-dialog-border-radius ,
246- var (--ha-border-radius-2xl )
247- );
248- transform : translateY (100% );
249- transition : transform ${ ANIMATION_DURATION_MS } ms ease;
250- border-top-width : var (--ha-bottom-sheet-border-width );
251- border-right-width : var (--ha-bottom-sheet-border-width );
252- border-left-width : var (--ha-bottom-sheet-border-width );
253- border-bottom-width : 0 ;
254- border-style : var (--ha-bottom-sheet-border-style );
255- border-color : var (--ha-bottom-sheet-border-color );
48+ --spacing : 0 ;
49+ --size : auto;
50+ --show-duration : ${ BOTTOM_SHEET_ANIMATION_DURATION_MS } ms;
51+ --hide-duration : ${ BOTTOM_SHEET_ANIMATION_DURATION_MS } ms;
25652 }
257-
258- dialog .show {
259- transform : translateY (0 );
53+ wa-drawer ::part (dialog ) {
54+ border-top-left-radius : var (--ha-border-radius-lg );
55+ border-top-right-radius : var (--ha-border-radius-lg );
56+ max-height : 90vh ;
57+ }
58+ wa-drawer ::part (body ) {
59+ padding-bottom : var (--safe-area-inset-bottom );
26060 }
26161 ` ;
26262}
@@ -265,8 +65,4 @@ declare global {
26565 interface HTMLElementTagNameMap {
26666 "ha-bottom-sheet" : HaBottomSheet ;
26767 }
268-
269- interface HASSDomEvents {
270- "bottom-sheet-closed" : undefined ;
271- }
27268}
0 commit comments