Skip to content
This repository was archived by the owner on Jan 13, 2025. It is now read-only.

Commit e383944

Browse files
sayriscopybara-github
authored andcommitted
feat(dialog): Adding styling for scroll bar dividers, and adding logic to show said dividers only when content is scrolled "behind" the header or footer of the dialog.
BREAKING_CHANGE: This change adds four additional methods into the MDCDialogAdapter: * registerContentEventHandler<K extends EventType>( evtType: K, handler: SpecificEventListener<K>): void; * deregisterContentEventHandler<K extends EventType>( evtType: K, handler: SpecificEventListener<K>): void; * isScrollableContentAtTop(): boolean; * isScrollableContentAtBottom(): boolean; Note that full-screen dialogs are currently under development and not yet ready for use. PiperOrigin-RevId: 353767497
1 parent 96be07c commit e383944

File tree

8 files changed

+403
-11
lines changed

8 files changed

+403
-11
lines changed

packages/mdc-dialog/_mixins.scss

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,18 @@
197197
.mdc-dialog__close {
198198
@include iconbutton-mixins.size($size: 24px, $query: $query);
199199
}
200+
201+
&.mdc-dialog--scrollable .mdc-dialog__actions {
202+
// If full-screen dialog is scrollable, the scroll divider over the action
203+
// buttons (i.e. the "footer") should only be visible when the content is
204+
// "cut off" by the footer. To toggle this divider, we override the
205+
// styling set by the mdc-dialog--scrollable class, and instead rely on
206+
// the mdc-dialog-scroll-divider-footer class to determine when the
207+
// border-top should be visible.
208+
@include feature-targeting.targets($feat-structure) {
209+
border-top: 1px solid transparent;
210+
}
211+
}
200212
}
201213

202214
.mdc-dialog__content {
@@ -454,11 +466,18 @@
454466
$feat-color: feature-targeting.create-target($query, color);
455467

456468
&.mdc-dialog--scrollable .mdc-dialog__title,
457-
&.mdc-dialog--scrollable .mdc-dialog__actions {
469+
&.mdc-dialog--scrollable .mdc-dialog__actions,
470+
&.mdc-dialog--scrollable.mdc-dialog-scroll-divider-footer
471+
.mdc-dialog__actions {
458472
@include feature-targeting.targets($feat-color) {
459473
border-color: rgba(theme-color.prop-value($color), $opacity);
460474
}
461475
}
476+
477+
&.mdc-dialog-scroll-divider-header.mdc-dialog--fullscreen
478+
.mdc-dialog__header {
479+
@include elevation-mixins.elevation(2, $query: $query);
480+
}
462481
}
463482

464483
@mixin shape-radius(

packages/mdc-dialog/adapter.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
* THE SOFTWARE.
2222
*/
2323

24+
import {EventType, SpecificEventListener} from '@material/base/types';
25+
2426
/**
2527
* Defines the shape of the adapter expected by the foundation.
2628
* Implement this adapter for your framework of choice to delegate updates to
@@ -34,11 +36,11 @@ export interface MDCDialogAdapter {
3436
hasClass(className: string): boolean;
3537
addBodyClass(className: string): void;
3638
removeBodyClass(className: string): void;
37-
eventTargetMatches(target: EventTarget | null, selector: string): boolean;
39+
eventTargetMatches(target: EventTarget|null, selector: string): boolean;
3840

3941
isContentScrollable(): boolean;
4042
areButtonsStacked(): boolean;
41-
getActionFromEvent(evt: Event): string | null;
43+
getActionFromEvent(evt: Event): string|null;
4244

4345
trapFocus(focusElement: HTMLElement|null): void;
4446
releaseFocus(): void;
@@ -51,4 +53,33 @@ export interface MDCDialogAdapter {
5153
notifyOpened(): void;
5254
notifyClosing(action: string): void;
5355
notifyClosed(action: string): void;
56+
57+
/**
58+
* Registers an event listener on the dialog's content element (indicated
59+
* with the 'mdc-dialog__content' class).
60+
*/
61+
registerContentEventHandler<K extends EventType>(
62+
evtType: K, handler: SpecificEventListener<K>): void;
63+
64+
/**
65+
* Deregisters an event listener on the dialog's content element.
66+
*/
67+
deregisterContentEventHandler<K extends EventType>(
68+
evtType: K, handler: SpecificEventListener<K>): void;
69+
70+
/**
71+
* @return true if the content has been scrolled (that is, for
72+
* scrollable content, if it is at the "top"). This is used in full-screen
73+
* dialogs, where the scroll divider is expected only to appear once the
74+
* content has been scrolled "underneath" the header bar.
75+
*/
76+
isScrollableContentAtTop(): boolean;
77+
78+
/**
79+
* @return true if the content has been scrolled all
80+
* the way to the bottom. This is used in full-screen dialogs, where the
81+
* footer scroll divider is expected only to appear when the content is
82+
* "cut-off" by the footer bar.
83+
*/
84+
isScrollableContentAtBottom(): boolean;
5485
}

packages/mdc-dialog/component.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,22 @@ export class MDCDialog extends MDCComponent<MDCDialogFoundation> {
210210
trapFocus: () => {
211211
this.focusTrap.trapFocus();
212212
},
213+
registerContentEventHandler: (evt, handler) => {
214+
if (this.content instanceof HTMLElement) {
215+
this.content.addEventListener(evt, handler);
216+
}
217+
},
218+
deregisterContentEventHandler: (evt, handler) => {
219+
if (this.content instanceof HTMLElement) {
220+
this.content.removeEventListener(evt, handler);
221+
}
222+
},
223+
isScrollableContentAtTop: () => {
224+
return util.isScrollAtTop(this.content);
225+
},
226+
isScrollableContentAtBottom: () => {
227+
return util.isScrollAtBottom(this.content);
228+
},
213229
};
214230
return new MDCDialogFoundation(adapter);
215231
}

packages/mdc-dialog/constants.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@ export const cssClasses = {
2828
SCROLLABLE: 'mdc-dialog--scrollable',
2929
SCROLL_LOCK: 'mdc-dialog-scroll-lock',
3030
STACKED: 'mdc-dialog--stacked',
31+
FULLSCREEN: 'mdc-dialog--fullscreen',
32+
// Class for showing a scroll divider on full-screen dialog header element.
33+
// Should only be displayed on scrollable content, when the dialog content is
34+
// scrolled "underneath" the header.
35+
SCROLL_DIVIDER_HEADER: 'mdc-dialog-scroll-divider-header',
36+
// Class for showing a scroll divider on a full-screen dialog footer element.
37+
// Should only be displayed on scrolalble content, when the dialog content is
38+
// obscured "underneath" the footer.
39+
SCROLL_DIVIDER_FOOTER: 'mdc-dialog-scroll-divider-footer',
3140
};
3241

3342
export const strings = {

packages/mdc-dialog/foundation.ts

Lines changed: 82 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,17 @@
2121
* THE SOFTWARE.
2222
*/
2323

24+
import {AnimationFrame} from '@material/animation/animationframe';
2425
import {MDCFoundation} from '@material/base/foundation';
26+
import {SpecificEventListener} from '@material/base/types';
27+
2528
import {MDCDialogAdapter} from './adapter';
2629
import {cssClasses, numbers, strings} from './constants';
2730

31+
enum AnimationKeys {
32+
POLL_SCROLL_POS = 'poll_scroll_position'
33+
}
34+
2835
export class MDCDialogFoundation extends MDCFoundation<MDCDialogAdapter> {
2936
static get cssClasses() {
3037
return cssClasses;
@@ -58,27 +65,41 @@ export class MDCDialogFoundation extends MDCFoundation<MDCDialogAdapter> {
5865
removeClass: () => undefined,
5966
reverseButtons: () => undefined,
6067
trapFocus: () => undefined,
68+
registerContentEventHandler: () => undefined,
69+
deregisterContentEventHandler: () => undefined,
70+
isScrollableContentAtTop: () => false,
71+
isScrollableContentAtBottom: () => false,
6172
};
6273
}
6374

6475
private dialogOpen = false;
76+
private isFullscreen = false;
6577
private animationFrame = 0;
6678
private animationTimer = 0;
6779
private layoutFrame = 0;
6880
private escapeKeyAction = strings.CLOSE_ACTION;
6981
private scrimClickAction = strings.CLOSE_ACTION;
7082
private autoStackButtons = true;
7183
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;
7388

7489
constructor(adapter?: Partial<MDCDialogAdapter>) {
7590
super({...MDCDialogFoundation.defaultAdapter, ...adapter});
91+
92+
this.animFrame = new AnimationFrame();
93+
this.contentScrollHandler = () => {
94+
this.handleScrollEvent();
95+
};
7696
}
7797

7898
init() {
7999
if (this.adapter.hasClass(cssClasses.STACKED)) {
80100
this.setAutoStackButtons(false);
81101
}
102+
this.isFullscreen = this.adapter.hasClass(cssClasses.FULLSCREEN);
82103
}
83104

84105
destroy() {
@@ -95,14 +116,24 @@ export class MDCDialogFoundation extends MDCFoundation<MDCDialogAdapter> {
95116
cancelAnimationFrame(this.layoutFrame);
96117
this.layoutFrame = 0;
97118
}
119+
120+
if (this.isFullscreen && this.adapter.isContentScrollable()) {
121+
this.adapter.deregisterContentEventHandler(
122+
'scroll', this.contentScrollHandler);
123+
}
98124
}
99125

100126
open() {
101127
this.dialogOpen = true;
102128
this.adapter.notifyOpening();
103129
this.adapter.addClass(cssClasses.OPENING);
130+
if (this.isFullscreen && this.adapter.isContentScrollable()) {
131+
this.adapter.registerContentEventHandler(
132+
'scroll', this.contentScrollHandler);
133+
}
104134

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
106137
this.runNextAnimationFrame(() => {
107138
this.adapter.addClass(cssClasses.OPEN);
108139
this.adapter.addBodyClass(cssClasses.SCROLL_LOCK);
@@ -119,7 +150,8 @@ export class MDCDialogFoundation extends MDCFoundation<MDCDialogAdapter> {
119150

120151
close(action = '') {
121152
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
123155
return;
124156
}
125157

@@ -128,6 +160,10 @@ export class MDCDialogFoundation extends MDCFoundation<MDCDialogAdapter> {
128160
this.adapter.addClass(cssClasses.CLOSING);
129161
this.adapter.removeClass(cssClasses.OPEN);
130162
this.adapter.removeBodyClass(cssClasses.SCROLL_LOCK);
163+
if (this.isFullscreen && this.adapter.isContentScrollable()) {
164+
this.adapter.deregisterContentEventHandler(
165+
'scroll', this.contentScrollHandler);
166+
}
131167

132168
cancelAnimationFrame(this.animationFrame);
133169
this.animationFrame = 0;
@@ -245,11 +281,25 @@ export class MDCDialogFoundation extends MDCFoundation<MDCDialogAdapter> {
245281
}
246282
}
247283

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+
248298
private layoutInternal() {
249299
if (this.autoStackButtons) {
250300
this.detectStackedButtons();
251301
}
252-
this.detectScrollableContent();
302+
this.toggleScrollableClasses();
253303
}
254304

255305
private handleAnimationTimerEnd() {
@@ -259,7 +309,8 @@ export class MDCDialogFoundation extends MDCFoundation<MDCDialogAdapter> {
259309
}
260310

261311
/**
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.
263314
*/
264315
private runNextAnimationFrame(callback: () => void) {
265316
cancelAnimationFrame(this.animationFrame);
@@ -286,11 +337,35 @@ export class MDCDialogFoundation extends MDCFoundation<MDCDialogAdapter> {
286337
}
287338
}
288339

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.
291343
this.adapter.removeClass(cssClasses.SCROLLABLE);
292344
if (this.adapter.isContentScrollable()) {
293345
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);
294369
}
295370
}
296371
}

0 commit comments

Comments
 (0)