21
21
* THE SOFTWARE.
22
22
*/
23
23
24
+ import { AnimationFrame } from '@material/animation/animationframe' ;
24
25
import { MDCFoundation } from '@material/base/foundation' ;
26
+ import { SpecificEventListener } from '@material/base/types' ;
27
+
25
28
import { MDCDialogAdapter } from './adapter' ;
26
29
import { cssClasses , numbers , strings } from './constants' ;
27
30
31
+ enum AnimationKeys {
32
+ POLL_SCROLL_POS = 'poll_scroll_position'
33
+ }
34
+
28
35
export class MDCDialogFoundation extends MDCFoundation < MDCDialogAdapter > {
29
36
static get cssClasses ( ) {
30
37
return cssClasses ;
@@ -58,27 +65,41 @@ export class MDCDialogFoundation extends MDCFoundation<MDCDialogAdapter> {
58
65
removeClass : ( ) => undefined ,
59
66
reverseButtons : ( ) => undefined ,
60
67
trapFocus : ( ) => undefined ,
68
+ registerContentEventHandler : ( ) => undefined ,
69
+ deregisterContentEventHandler : ( ) => undefined ,
70
+ isScrollableContentAtTop : ( ) => false ,
71
+ isScrollableContentAtBottom : ( ) => false ,
61
72
} ;
62
73
}
63
74
64
75
private dialogOpen = false ;
76
+ private isFullscreen = false ;
65
77
private animationFrame = 0 ;
66
78
private animationTimer = 0 ;
67
79
private layoutFrame = 0 ;
68
80
private escapeKeyAction = strings . CLOSE_ACTION ;
69
81
private scrimClickAction = strings . CLOSE_ACTION ;
70
82
private autoStackButtons = true ;
71
83
private areButtonsStacked = false ;
72
- private suppressDefaultPressSelector = strings . SUPPRESS_DEFAULT_PRESS_SELECTOR ;
84
+ private suppressDefaultPressSelector =
85
+ strings . SUPPRESS_DEFAULT_PRESS_SELECTOR ;
86
+ private readonly contentScrollHandler : SpecificEventListener < 'scroll' > ;
87
+ private readonly animFrame : AnimationFrame ;
73
88
74
89
constructor ( adapter ?: Partial < MDCDialogAdapter > ) {
75
90
super ( { ...MDCDialogFoundation . defaultAdapter , ...adapter } ) ;
91
+
92
+ this . animFrame = new AnimationFrame ( ) ;
93
+ this . contentScrollHandler = ( ) => {
94
+ this . handleScrollEvent ( ) ;
95
+ } ;
76
96
}
77
97
78
98
init ( ) {
79
99
if ( this . adapter . hasClass ( cssClasses . STACKED ) ) {
80
100
this . setAutoStackButtons ( false ) ;
81
101
}
102
+ this . isFullscreen = this . adapter . hasClass ( cssClasses . FULLSCREEN ) ;
82
103
}
83
104
84
105
destroy ( ) {
@@ -95,14 +116,24 @@ export class MDCDialogFoundation extends MDCFoundation<MDCDialogAdapter> {
95
116
cancelAnimationFrame ( this . layoutFrame ) ;
96
117
this . layoutFrame = 0 ;
97
118
}
119
+
120
+ if ( this . isFullscreen && this . adapter . isContentScrollable ( ) ) {
121
+ this . adapter . deregisterContentEventHandler (
122
+ 'scroll' , this . contentScrollHandler ) ;
123
+ }
98
124
}
99
125
100
126
open ( ) {
101
127
this . dialogOpen = true ;
102
128
this . adapter . notifyOpening ( ) ;
103
129
this . adapter . addClass ( cssClasses . OPENING ) ;
130
+ if ( this . isFullscreen && this . adapter . isContentScrollable ( ) ) {
131
+ this . adapter . registerContentEventHandler (
132
+ 'scroll' , this . contentScrollHandler ) ;
133
+ }
104
134
105
- // Wait a frame once display is no longer "none", to establish basis for animation
135
+ // Wait a frame once display is no longer "none", to establish basis for
136
+ // animation
106
137
this . runNextAnimationFrame ( ( ) => {
107
138
this . adapter . addClass ( cssClasses . OPEN ) ;
108
139
this . adapter . addBodyClass ( cssClasses . SCROLL_LOCK ) ;
@@ -119,7 +150,8 @@ export class MDCDialogFoundation extends MDCFoundation<MDCDialogAdapter> {
119
150
120
151
close ( action = '' ) {
121
152
if ( ! this . dialogOpen ) {
122
- // Avoid redundant close calls (and events), e.g. from keydown on elements that inherently emit click
153
+ // Avoid redundant close calls (and events), e.g. from keydown on elements
154
+ // that inherently emit click
123
155
return ;
124
156
}
125
157
@@ -128,6 +160,10 @@ export class MDCDialogFoundation extends MDCFoundation<MDCDialogAdapter> {
128
160
this . adapter . addClass ( cssClasses . CLOSING ) ;
129
161
this . adapter . removeClass ( cssClasses . OPEN ) ;
130
162
this . adapter . removeBodyClass ( cssClasses . SCROLL_LOCK ) ;
163
+ if ( this . isFullscreen && this . adapter . isContentScrollable ( ) ) {
164
+ this . adapter . deregisterContentEventHandler (
165
+ 'scroll' , this . contentScrollHandler ) ;
166
+ }
131
167
132
168
cancelAnimationFrame ( this . animationFrame ) ;
133
169
this . animationFrame = 0 ;
@@ -245,11 +281,25 @@ export class MDCDialogFoundation extends MDCFoundation<MDCDialogAdapter> {
245
281
}
246
282
}
247
283
284
+ /**
285
+ * Handles scroll event on the dialog's content element -- showing a scroll
286
+ * divider on the header or footer based on the scroll position. This handler
287
+ * should only be registered on full-screen dialogs with scrollable content.
288
+ */
289
+ private handleScrollEvent ( ) {
290
+ // Since scroll events can fire at a high rate, we throttle these events by
291
+ // using requestAnimationFrame.
292
+ this . animFrame . request ( AnimationKeys . POLL_SCROLL_POS , ( ) => {
293
+ this . toggleScrollDividerHeader ( ) ;
294
+ this . toggleScrollDividerFooter ( ) ;
295
+ } ) ;
296
+ }
297
+
248
298
private layoutInternal ( ) {
249
299
if ( this . autoStackButtons ) {
250
300
this . detectStackedButtons ( ) ;
251
301
}
252
- this . detectScrollableContent ( ) ;
302
+ this . toggleScrollableClasses ( ) ;
253
303
}
254
304
255
305
private handleAnimationTimerEnd ( ) {
@@ -259,7 +309,8 @@ export class MDCDialogFoundation extends MDCFoundation<MDCDialogAdapter> {
259
309
}
260
310
261
311
/**
262
- * Runs the given logic on the next animation frame, using setTimeout to factor in Firefox reflow behavior.
312
+ * Runs the given logic on the next animation frame, using setTimeout to
313
+ * factor in Firefox reflow behavior.
263
314
*/
264
315
private runNextAnimationFrame ( callback : ( ) => void ) {
265
316
cancelAnimationFrame ( this . animationFrame ) ;
@@ -286,11 +337,35 @@ export class MDCDialogFoundation extends MDCFoundation<MDCDialogAdapter> {
286
337
}
287
338
}
288
339
289
- private detectScrollableContent ( ) {
290
- // Remove the class first to let us measure the natural height of the content.
340
+ private toggleScrollableClasses ( ) {
341
+ // Remove the class first to let us measure the natural height of the
342
+ // content.
291
343
this . adapter . removeClass ( cssClasses . SCROLLABLE ) ;
292
344
if ( this . adapter . isContentScrollable ( ) ) {
293
345
this . adapter . addClass ( cssClasses . SCROLLABLE ) ;
346
+
347
+ if ( this . isFullscreen ) {
348
+ // If dialog is full-screen and scrollable, check if a scroll divider
349
+ // should be shown.
350
+ this . toggleScrollDividerHeader ( ) ;
351
+ this . toggleScrollDividerFooter ( ) ;
352
+ }
353
+ }
354
+ }
355
+
356
+ private toggleScrollDividerHeader ( ) {
357
+ if ( ! this . adapter . isScrollableContentAtTop ( ) ) {
358
+ this . adapter . addClass ( cssClasses . SCROLL_DIVIDER_HEADER ) ;
359
+ } else if ( this . adapter . hasClass ( cssClasses . SCROLL_DIVIDER_HEADER ) ) {
360
+ this . adapter . removeClass ( cssClasses . SCROLL_DIVIDER_HEADER ) ;
361
+ }
362
+ }
363
+
364
+ private toggleScrollDividerFooter ( ) {
365
+ if ( ! this . adapter . isScrollableContentAtBottom ( ) ) {
366
+ this . adapter . addClass ( cssClasses . SCROLL_DIVIDER_FOOTER ) ;
367
+ } else if ( this . adapter . hasClass ( cssClasses . SCROLL_DIVIDER_FOOTER ) ) {
368
+ this . adapter . removeClass ( cssClasses . SCROLL_DIVIDER_FOOTER ) ;
294
369
}
295
370
}
296
371
}
0 commit comments