@@ -24,10 +24,25 @@ import {
2424} from '../core' ;
2525import { Platform } from '@angular/cdk/platform' ;
2626
27+ /** The options for the MatButtonRippleLoader's event listeners. */
28+ const OPTIONS = { passive : true , capture : true } ;
29+
30+ /** The attribute attached to a mat-button whose ripple has not yet been initialized. */
31+ const MAT_BUTTON_RIPPLE_UNINITIALIZED = 'mat-button-ripple-uninitialized' ;
32+
33+ /** The attribute attached to a mat-button whose internals (excluding the ripple) have not yet been initialized. */
34+ const MAT_BUTTON_INTERNALS_UNINITIALIZED = 'mat-button-internals-uninitialized' ;
35+
2736@Injectable ( { providedIn : 'root' } )
28- export class MatButtonRippleLoader implements OnDestroy {
37+ export class MatButtonLazyLoader implements OnDestroy {
2938 private _document : Document ;
3039
40+ /** A batch of actions to run. */
41+ private _actionQueue : Function [ ] = [ ] ;
42+
43+ /** A timeout for when the action queue will be emptied / ran. */
44+ private _runActionsTimeout : any | null = null ;
45+
3146 constructor (
3247 private _platform : Platform ,
3348 private _ngZone : NgZone ,
@@ -40,46 +55,84 @@ export class MatButtonRippleLoader implements OnDestroy {
4055 this . _document = document ;
4156
4257 this . _ngZone . runOutsideAngular ( ( ) => {
43- const options = { passive : true , capture : true } ;
44- this . _document . addEventListener ( 'focus' , this . onInteraction , options ) ;
45- this . _document . addEventListener ( 'mouseenter' , this . onInteraction , options ) ;
58+ this . _document . addEventListener ( 'focus' , this . onInteraction , OPTIONS ) ;
59+ this . _document . addEventListener ( 'mouseenter' , this . onInteraction , OPTIONS ) ;
4660 } ) ;
4761 }
4862
4963 ngOnDestroy ( ) {
5064 this . _ngZone . runOutsideAngular ( ( ) => {
51- document . removeEventListener ( 'focus' , this . onInteraction ) ;
52- document . removeEventListener ( 'mouseenter' , this . onInteraction ) ;
65+ this . _document . removeEventListener ( 'focus' , this . onInteraction , OPTIONS ) ;
66+ this . _document . removeEventListener ( 'mouseenter' , this . onInteraction , OPTIONS ) ;
5367 } ) ;
5468 }
5569
70+ /** Handles creating and attaching button internals when a button is initially interacted with. */
5671 private onInteraction = ( event : Event ) => {
5772 if ( ! ( event . target instanceof Element ) ) {
5873 return ;
5974 }
6075
61- const button = event . target . closest ( '.mat-mdc-button-base:not([data-mat-button-interacted])' ) ;
76+ const button = this . _closest ( event . target ) ;
6277 if ( ! button ) {
6378 return ;
6479 }
6580
81+ button . removeAttribute ( MAT_BUTTON_INTERNALS_UNINITIALIZED ) ;
82+ this . _actionQueue . push ( ( ) => this . _attachButtonInternals ( button as HTMLButtonElement ) ) ;
83+
84+ // Immediately run all of the queued actions if a focus event occurs.
85+
86+ if ( event . type === 'focus' ) {
87+ this . _runActions ( ) ;
88+ } else if ( event . type === 'mouseenter' ) {
89+ this . _runActionsTimeout = setTimeout ( ( ) => this . _runActions ( ) , 50 ) ;
90+ }
91+ } ;
92+
93+ /** Runs all of the actions that have been queued up. */
94+ private _runActions ( ) : void {
95+ if ( this . _runActionsTimeout !== null ) {
96+ clearTimeout ( this . _runActionsTimeout ) ;
97+ this . _runActionsTimeout = null ;
98+ }
99+ for ( const callback of this . _actionQueue ) {
100+ callback ( ) ;
101+ }
102+ this . _actionQueue = [ ] ;
103+ }
104+
105+ /**
106+ * Traverses the element and its parents (heading toward the document root)
107+ * until it finds a mat-button that has not been initialized.
108+ */
109+ private _closest ( element : Element ) : Element | null {
110+ let el : Element | null = element ;
111+ while ( el ) {
112+ if ( el . hasAttribute ( MAT_BUTTON_INTERNALS_UNINITIALIZED ) ) {
113+ return el ;
114+ }
115+ el = el . parentElement ;
116+ }
117+ return null ;
118+ }
119+
120+ private _attachButtonInternals ( button : HTMLButtonElement ) : void {
66121 button . prepend ( this . _createSpan ( this . _getPersistentRippleClassName ( button ) ) ) ;
67122
68- // A separate flag is used for the ripple because the ripple can
69- // be rendered separately from the rest of the button DOM internals
70- // if it is interacted with via the MatButton's ripple API.
71- if ( ! button . hasAttribute ( 'data-mat-button-ripple-rendered' ) ) {
123+ if ( button . hasAttribute ( MAT_BUTTON_RIPPLE_UNINITIALIZED ) ) {
124+ button . removeAttribute ( MAT_BUTTON_RIPPLE_UNINITIALIZED ) ;
72125 button . append ( this . _createSpan ( 'mat-mdc-focus-indicator' ) ) ;
73- this . _appendRipple ( button as HTMLButtonElement ) ;
74- button . setAttribute ( 'data-mat-button-ripple-rendered' , '' ) ;
126+ this . _appendRipple ( button ) ;
75127 } else {
76128 const rippleEl = button . querySelector ( '.mat-mdc-button-ripple' ) ;
77129 rippleEl ! . before ( this . _createSpan ( 'mat-mdc-focus-indicator' ) ) ;
78130 }
79131
80- button . append ( this . _createSpan ( 'mat-mdc-button-touch-target' ) ) ;
81- button . setAttribute ( 'data-mat-button-interacted' , '' ) ;
82- } ;
132+ // Move the touch target to the correct location in the button.
133+ const touchTarget = button . querySelector ( '.mat-mdc-button-touch-target' ) ! ;
134+ button . appendChild ( touchTarget ) ;
135+ }
83136
84137 private _appendRipple ( button : HTMLButtonElement ) : void {
85138 const ripple = this . _document . createElement ( 'span' ) ;
0 commit comments