2121 * THE SOFTWARE.
2222 */
2323
24+ import { AnimationFrame } from '@material/animation/animationframe' ;
2425import { MDCFoundation } from '@material/base/foundation' ;
2526import { SpecificEventListener } from '@material/base/types' ;
2627import { KEY , normalizeKey } from '@material/dom/keyboard' ;
@@ -37,6 +38,10 @@ const {
3738 MULTILINE_TOOLTIP
3839} = CssClasses ;
3940
41+ enum AnimationKeys {
42+ POLL_ANCHOR = 'poll_anchor'
43+ }
44+
4045export class MDCTooltipFoundation extends MDCFoundation < MDCTooltipAdapter > {
4146 static get defaultAdapter ( ) : MDCTooltipAdapter {
4247 return {
@@ -55,6 +60,8 @@ export class MDCTooltipFoundation extends MDCFoundation<MDCTooltipAdapter> {
5560 isRTL : ( ) => false ,
5661 registerDocumentEventHandler : ( ) => undefined ,
5762 deregisterDocumentEventHandler : ( ) => undefined ,
63+ registerWindowEventHandler : ( ) => undefined ,
64+ deregisterWindowEventHandler : ( ) => undefined ,
5865 notifyHidden : ( ) => undefined ,
5966 } ;
6067 }
@@ -69,14 +76,19 @@ export class MDCTooltipFoundation extends MDCFoundation<MDCTooltipAdapter> {
6976 private readonly hideDelayMs = numbers . HIDE_DELAY_MS ;
7077 private readonly showDelayMs = numbers . SHOW_DELAY_MS ;
7178
79+ private anchorRect : ClientRect | null = null ;
7280 private frameId : number | null = null ;
7381 private hideTimeout : number | null = null ;
7482 private showTimeout : number | null = null ;
83+ private readonly animFrame : AnimationFrame ;
7584 private readonly documentClickHandler : SpecificEventListener < 'click' > ;
7685 private readonly documentKeydownHandler : SpecificEventListener < 'keydown' > ;
86+ private readonly windowScrollHandler : SpecificEventListener < 'scroll' > ;
87+ private readonly windowResizeHandler : SpecificEventListener < 'resize' > ;
7788
7889 constructor ( adapter ?: Partial < MDCTooltipAdapter > ) {
7990 super ( { ...MDCTooltipFoundation . defaultAdapter , ...adapter } ) ;
91+ this . animFrame = new AnimationFrame ( ) ;
8092
8193 this . documentClickHandler = ( ) => {
8294 this . handleClick ( ) ;
@@ -85,6 +97,14 @@ export class MDCTooltipFoundation extends MDCFoundation<MDCTooltipAdapter> {
8597 this . documentKeydownHandler = ( evt ) => {
8698 this . handleKeydown ( evt ) ;
8799 } ;
100+
101+ this . windowScrollHandler = ( ) => {
102+ this . handleWindowChangeEvent ( ) ;
103+ } ;
104+
105+ this . windowResizeHandler = ( ) => {
106+ this . handleWindowChangeEvent ( ) ;
107+ } ;
88108 }
89109
90110 handleAnchorMouseEnter ( ) {
@@ -135,6 +155,19 @@ export class MDCTooltipFoundation extends MDCFoundation<MDCTooltipAdapter> {
135155 }
136156 }
137157
158+ /**
159+ * On window resize or scroll, check the anchor position and size and
160+ * repostion tooltip if necessary.
161+ */
162+ private handleWindowChangeEvent ( ) {
163+ // Since scroll and resize events can fire at a high rate, we throttle
164+ // the potential re-positioning of tooltip component using
165+ // requestAnimationFrame.
166+ this . animFrame . request ( AnimationKeys . POLL_ANCHOR , ( ) => {
167+ this . repositionTooltipOnAnchorMove ( ) ;
168+ } ) ;
169+ }
170+
138171 show ( ) {
139172 this . clearHideTimeout ( ) ;
140173 this . clearShowTimeout ( ) ;
@@ -153,15 +186,17 @@ export class MDCTooltipFoundation extends MDCFoundation<MDCTooltipAdapter> {
153186 if ( this . isTooltipMultiline ( ) ) {
154187 this . adapter . addClass ( MULTILINE_TOOLTIP ) ;
155188 }
156- const { top, left} = this . calculateTooltipDistance ( ) ;
157- this . adapter . setStyleProperty ( 'top' , `${ top } px` ) ;
158- this . adapter . setStyleProperty ( 'left' , `${ left } px` ) ;
189+ this . anchorRect = this . adapter . getAnchorBoundingRect ( ) ;
190+ this . positionTooltip ( ) ;
159191
160192 this . adapter . registerDocumentEventHandler (
161193 'click' , this . documentClickHandler ) ;
162194 this . adapter . registerDocumentEventHandler (
163195 'keydown' , this . documentKeydownHandler ) ;
164196
197+ this . adapter . registerWindowEventHandler ( 'scroll' , this . windowScrollHandler ) ;
198+ this . adapter . registerWindowEventHandler ( 'resize' , this . windowResizeHandler ) ;
199+
165200 this . frameId = requestAnimationFrame ( ( ) => {
166201 this . clearAllAnimationClasses ( ) ;
167202 this . adapter . addClass ( SHOWN ) ;
@@ -192,6 +227,10 @@ export class MDCTooltipFoundation extends MDCFoundation<MDCTooltipAdapter> {
192227 'click' , this . documentClickHandler ) ;
193228 this . adapter . deregisterDocumentEventHandler (
194229 'keydown' , this . documentKeydownHandler ) ;
230+ this . adapter . deregisterWindowEventHandler (
231+ 'scroll' , this . windowScrollHandler ) ;
232+ this . adapter . deregisterWindowEventHandler (
233+ 'resize' , this . windowResizeHandler ) ;
195234 }
196235
197236 handleTransitionEnd ( ) {
@@ -247,6 +286,12 @@ export class MDCTooltipFoundation extends MDCFoundation<MDCTooltipAdapter> {
247286 tooltipSize . width >= numbers . MAX_WIDTH ;
248287 }
249288
289+ private positionTooltip ( ) {
290+ const { top, left} = this . calculateTooltipDistance ( this . anchorRect ) ;
291+ this . adapter . setStyleProperty ( 'top' , `${ top } px` ) ;
292+ this . adapter . setStyleProperty ( 'left' , `${ left } px` ) ;
293+ }
294+
250295 /**
251296 * Calculates the position of the tooltip. A tooltip will be placed beneath
252297 * the anchor element and aligned either with the 'start'/'end' edge of the
@@ -261,8 +306,7 @@ export class MDCTooltipFoundation extends MDCFoundation<MDCTooltipAdapter> {
261306 * Users can specify an alignment, however, if this alignment results in the
262307 * tooltip colliding with the viewport, this specification is overwritten.
263308 */
264- private calculateTooltipDistance ( ) {
265- const anchorRect = this . adapter . getAnchorBoundingRect ( ) ;
309+ private calculateTooltipDistance ( anchorRect : ClientRect | null ) {
266310 if ( ! anchorRect ) {
267311 return { top : 0 , left : 0 } ;
268312 }
@@ -457,6 +501,19 @@ export class MDCTooltipFoundation extends MDCFoundation<MDCTooltipAdapter> {
457501 return yPos + tooltipHeight <= viewportHeight && yPos >= 0 ;
458502 }
459503
504+ private repositionTooltipOnAnchorMove ( ) {
505+ const newAnchorRect = this . adapter . getAnchorBoundingRect ( ) ;
506+ if ( ! newAnchorRect || ! this . anchorRect ) return ;
507+
508+ if ( newAnchorRect . top !== this . anchorRect . top ||
509+ newAnchorRect . left !== this . anchorRect . left ||
510+ newAnchorRect . height !== this . anchorRect . height ||
511+ newAnchorRect . width !== this . anchorRect . width ) {
512+ this . anchorRect = newAnchorRect ;
513+ this . positionTooltip ( ) ;
514+ }
515+ }
516+
460517 private clearShowTimeout ( ) {
461518 if ( this . showTimeout ) {
462519 clearTimeout ( this . showTimeout ) ;
@@ -490,6 +547,13 @@ export class MDCTooltipFoundation extends MDCFoundation<MDCTooltipAdapter> {
490547 'click' , this . documentClickHandler ) ;
491548 this . adapter . deregisterDocumentEventHandler (
492549 'keydown' , this . documentKeydownHandler ) ;
550+
551+ this . adapter . deregisterWindowEventHandler (
552+ 'scroll' , this . windowScrollHandler ) ;
553+ this . adapter . deregisterWindowEventHandler (
554+ 'resize' , this . windowResizeHandler ) ;
555+
556+ this . animFrame . cancelAll ( ) ;
493557 }
494558}
495559
0 commit comments