@@ -14,8 +14,10 @@ export default class TooltipItemView extends Backbone.View {
14
14
return [
15
15
'tooltip' ,
16
16
this . model . get ( 'isTargetFixedPosition' ) && 'is-fixed' ,
17
+ this . isStatic && 'is-static' ,
17
18
this . model . get ( 'tooltipClasses' ) || 'is-vertical-axis is-arrow-middle is-bottom is-middle' ,
18
19
this . model . get ( 'isShown' ) && 'is-shown' ,
20
+ this . model . get ( 'wasShown' ) && 'was-shown' ,
19
21
this . model . get ( '_classes' )
20
22
] . filter ( Boolean ) . join ( ' ' ) ;
21
23
}
@@ -39,61 +41,119 @@ export default class TooltipItemView extends Backbone.View {
39
41
40
42
initialize ( { $target, parent } ) {
41
43
_ . bindAll ( this , 'onDeviceResize' , 'onMouseOut' , 'doSubsequentPasses' ) ;
44
+ // Slow down change rendering to ~30fps as it's expensive
45
+ this . changed = _ . throttle ( this . changed . bind ( this ) , 34 ) ;
42
46
this . _classSet = new Set ( _ . result ( this , 'className' ) . trim ( ) . split ( / \s + / ) ) ;
43
47
this . $target = $target ;
44
48
this . parent = parent ;
45
49
this . $target . attr ( 'aria-describedby' , `tooltip-${ this . model . get ( '_id' ) } ` ) ;
46
- this . model . set ( 'ariaLabel' , this . $target . attr ( 'aria-label' ) || this . $target . find ( '.aria-label' ) . text ( ) ) ;
50
+ this . listenTo ( this . model , 'change' , this . changed ) ;
47
51
this . listenTo ( Adapt , 'device:resize' , this . onDeviceResize ) ;
48
- $ ( document ) . on ( 'mouseleave blur' , '[data-tooltip-id]' , this . onMouseOut ) ;
49
- this . doFirstPass ( ) ;
50
- setTimeout ( this . doSubsequentPasses , 17 ) ;
52
+ this . listenTo ( Adapt . parentView , 'preRemove' , this . remove ) ;
53
+ if ( ! this . isStatic ) {
54
+ // Should not hide static tooltips on blur
55
+ $ ( document ) . on ( 'mouseleave blur' , '[data-tooltip-id]' , this . onMouseOut ) ;
56
+ }
57
+ this . changed ( ) ;
58
+ }
59
+
60
+ applyStaticScrollOffset ( rect ) {
61
+ if ( ! rect ) return rect ;
62
+ const scrollTop = $ ( window ) . scrollTop ( ) ;
63
+ return {
64
+ left : rect . left ,
65
+ right : rect . right ,
66
+ width : rect . width ,
67
+ height : rect . height ,
68
+ x : rect . x ,
69
+ top : rect . top + scrollTop ,
70
+ bottom : rect . bottom + scrollTop ,
71
+ y : rect . y + scrollTop
72
+ } ;
51
73
}
52
74
53
75
get environment ( ) {
54
76
// Determine if the navigation is bottom so tooltip doesn't overlap nav bar
55
77
const navigationAlignment = Adapt . course . get ( '_navigation' ) ?. _navigationAlignment ?? 'top' ;
56
78
const navHeight = $ ( '.nav' ) . outerHeight ( true ) ;
57
79
const $target = this . $target ;
80
+ // Fetch the CSS variable values for distance and viewport padding
81
+ const {
82
+ offset,
83
+ distance,
84
+ viewPortPadding,
85
+ hasArrow,
86
+ shouldOverrideOffset,
87
+ shouldOverrideDistance,
88
+ shouldOverrideArrow
89
+ } = fetchCSSVariables ( this . el , {
90
+ offset : this . model . get ( '_offset' ) ,
91
+ distance : this . model . get ( '_distance' ) ,
92
+ hasArrow : this . model . get ( '_hasArrow' )
93
+ } ) ;
94
+
95
+ const arrowDOMReact = this . $ ( '.tooltip__arrow' ) [ 0 ] ?. getBoundingClientRect ( ) ;
96
+ const tooltipDOMRect = this . $ ( '.tooltip__body' ) [ 0 ] ?. getBoundingClientRect ( ) ;
97
+ const targetDOMRect = $target [ 0 ] ?. getBoundingClientRect ( ) ;
98
+
58
99
return {
59
100
position : this . model . get ( '_position' ) || 'outside bottom middle right' ,
101
+ offset,
102
+ distance,
103
+ viewPortPadding,
104
+ hasArrow,
105
+ shouldOverrideOffset,
106
+ shouldOverrideDistance,
107
+ shouldOverrideArrow,
60
108
isDisabled : $target . attr ( 'aria-disabled' ) !== undefined || $target . hasClass ( 'is-disabled' ) || $target . is ( ':disabled' ) ,
109
+ isStatic : this . isStatic ,
61
110
isTargetFixedPosition : Boolean ( this . $target . add ( this . $target . parents ( ) ) . filter ( ( index , el ) => $ ( el ) . css ( 'position' ) === 'fixed' ) . length ) ,
62
111
isRTL : Adapt . config . get ( '_defaultDirection' ) === 'rtl' ,
63
112
topNavOffset : navigationAlignment === 'top' ? navHeight : 0 ,
64
113
bottomNavOffset : navigationAlignment === 'bottom' ? navHeight : 0 ,
65
- targetDOMRect : $target [ 0 ] ?. getBoundingClientRect ( ) ,
114
+ targetDOMRect : this . isStatic
115
+ ? this . applyStaticScrollOffset ( targetDOMRect )
116
+ : targetDOMRect ,
66
117
clientDOMRect : {
67
118
width : parseInt ( getComputedStyle ( document . body ) . width ) ,
68
- height : $ ( 'html' ) [ 0 ] . clientHeight
119
+ height : this . isStatic
120
+ ? parseInt ( getComputedStyle ( document . getElementById ( 'app' ) ) . height )
121
+ : $ ( 'html' ) [ 0 ] . clientHeight
69
122
} ,
70
- tooltipDOMRect : this . $ ( '.tooltip__body' ) [ 0 ] ?. getBoundingClientRect ( ) ,
71
- arrowDOMRect : this . $ ( '.tooltip__arrow' ) [ 0 ] ?. getBoundingClientRect ( ) ,
72
- ariaHidden : ( document . activeElement === this . $target [ 0 ] )
123
+ tooltipDOMRect : this . isStatic
124
+ ? this . applyStaticScrollOffset ( tooltipDOMRect )
125
+ : tooltipDOMRect ,
126
+ arrowDOMRect : this . isStatic
127
+ ? this . applyStaticScrollOffset ( arrowDOMReact )
128
+ : arrowDOMReact ,
129
+ ariaHidden : ( document . activeElement === this . $target [ 0 ] || this . isStatic )
73
130
} ;
74
131
}
75
132
76
133
doFirstPass ( ) {
77
- this . model . set ( 'isShown' , false ) ;
134
+ if ( ! this . model ) return ;
135
+ this . model . set ( 'isShown' , false , { silent : true } ) ;
78
136
const environment = this . environment ;
79
137
const positions = position ( environment , { } , FIRST_PASS ) ;
80
138
const {
81
139
isDisabled,
140
+ isStatic,
82
141
isTargetFixedPosition,
83
142
ariaHidden
84
143
} = environment ;
85
144
this . model . set ( {
86
145
isDisabled,
146
+ isStatic,
87
147
isTargetFixedPosition,
88
148
ariaHidden,
89
149
...positions
90
- } ) ;
150
+ } , { silent : true } ) ;
91
151
this . render ( ) ;
92
152
}
93
153
94
154
doSubsequentPasses ( ) {
95
155
if ( ! this . model ) return ;
96
- this . model . set ( 'hasLoaded' , true ) ;
156
+ this . model . set ( 'hasLoaded' , true , { silent : true } ) ;
97
157
const multipassCache = { } ;
98
158
// First pass - render at the requested position
99
159
// Second pass - if needed, swap sides, switch axis and/or fill area
@@ -103,41 +163,66 @@ export default class TooltipItemView extends Backbone.View {
103
163
const positions = position ( this . environment , multipassCache , pass ) ;
104
164
const {
105
165
isDisabled,
166
+ isStatic,
106
167
isTargetFixedPosition,
107
168
ariaHidden
108
169
} = environment ;
109
170
this . model . set ( {
110
171
isDisabled,
172
+ isStatic,
111
173
isTargetFixedPosition,
112
174
ariaHidden,
113
175
...positions
114
- } ) ;
176
+ } , { silent : true } ) ;
115
177
this . render ( ) ;
116
178
}
117
- this . model . set ( 'isShown' , true ) ;
179
+ this . model . set ( 'isShown' , true , { silent : true } ) ;
118
180
this . render ( ) ;
181
+ this . model . set ( 'wasShown' , true , { silent : true } ) ;
119
182
}
120
183
121
184
render ( ) {
122
185
if ( ! this . model ) return ;
123
186
const Template = templates . tooltip ;
187
+ this . model . set ( 'ariaLabel' , this . $target . attr ( 'aria-label' ) || this . $target . find ( '.aria-label' ) . text ( ) , { silent : true } ) ;
124
188
this . updateViewProperties ( ) ;
125
189
ReactDOM . render ( < Template { ...this . model . toJSON ( ) } /> , this . el ) ;
126
190
}
127
191
192
+ changed ( ) {
193
+ if ( ! this . model ) return ;
194
+ requestAnimationFrame ( ( ) => {
195
+ this . doFirstPass ( ) ;
196
+ this . doSubsequentPasses ( ) ;
197
+ } ) ;
198
+ }
199
+
200
+ get isStatic ( ) {
201
+ return Boolean ( this . model . get ( '_isStatic' ) ) ;
202
+ }
203
+
204
+ get isTargetPresent ( ) {
205
+ return Boolean ( this . $target . parents ( 'body' ) . length ) ;
206
+ }
207
+
128
208
onDeviceResize ( ) {
209
+ if ( this . isStatic && this . isTargetPresent ) return this . changed ( ) ;
129
210
this . remove ( ) ;
130
211
}
131
212
132
213
onMouseOut ( ) {
214
+ if ( this . isStatic ) return ;
133
215
this . remove ( ) ;
134
216
}
135
217
136
218
remove ( ) {
137
219
if ( this . $el . hasClass ( 'test' ) ) return ;
138
220
this . stopListening ( Adapt ) ;
139
221
$ ( document ) . off ( 'mouseleave blur' , '[data-tooltip-id]' , this . onMouseOut ) ;
140
- this . model ?. set ( 'isShown' , false ) ;
222
+ this . model ?. set ( {
223
+ isShown : false ,
224
+ wasShown : false
225
+ } ) ;
141
226
this . render ( ) ;
142
227
this . model = null ;
143
228
this . $target = null ;
@@ -156,12 +241,18 @@ export default class TooltipItemView extends Backbone.View {
156
241
* Extract the offset, distance and padding properties from the css
157
242
* @returns {Object }
158
243
*/
159
- function fetchCSSVariables ( ) {
160
- const computed = getComputedStyle ( document . documentElement ) ;
244
+ function fetchCSSVariables ( target , { offset, distance, hasArrow } ) {
245
+ const computed = getComputedStyle ( target ) ;
246
+ offset = offset < 0 ? null : offset ?? null ;
247
+ distance = distance < 0 ? null : distance ?? null ;
161
248
return {
162
- offset : lengthToPx ( '@tooltip-offset' , computed . getPropertyValue ( '--adapt-tooltip-offset' ) ) ,
163
- distance : lengthToPx ( '@tooltip-distance' , computed . getPropertyValue ( '--adapt-tooltip-distance' ) ) ,
164
- viewPortPadding : lengthToPx ( '@tooltip-viewport-padding' , computed . getPropertyValue ( '--adapt-tooltip-viewport-padding' ) )
249
+ shouldOverrideOffset : offset !== null && offset !== undefined && offset !== 'default' ,
250
+ shouldOverrideDistance : distance !== null && distance !== undefined && distance !== 'default' ,
251
+ shouldOverrideArrow : hasArrow !== null && hasArrow !== undefined ,
252
+ offset : lengthToPx ( '@tooltip-offset' , offset ?? computed . getPropertyValue ( '--adapt-tooltip-offset' ) ) ,
253
+ distance : lengthToPx ( '@tooltip-distance' , distance ?? computed . getPropertyValue ( '--adapt-tooltip-distance' ) ) ,
254
+ viewPortPadding : lengthToPx ( '@tooltip-viewport-padding' , computed . getPropertyValue ( '--adapt-tooltip-viewport-padding' ) ) ,
255
+ hasArrow : hasArrow ?? ( computed . getPropertyValue ( '--adapt-tooltip-arrow' ) === 'true' )
165
256
} ;
166
257
} ;
167
258
@@ -175,7 +266,7 @@ function lengthToPx (name, length) {
175
266
const unit = String ( length ) . replaceAll ( / [ \d . ] + / g, '' ) . trim ( ) ;
176
267
const value = parseFloat ( length ) ;
177
268
if ( unit === 'rem' ) return value * parseInt ( getComputedStyle ( document . body ) . fontSize ) ;
178
- if ( unit === 'px' ) return value ;
269
+ if ( unit === 'px' || unit === '' ) return value ;
179
270
throw new Error ( `Cannot convert ${ name } ${ length } to pixels` ) ;
180
271
} ;
181
272
@@ -490,10 +581,11 @@ function swapValues (a, b) {
490
581
* @returns {Object }
491
582
*/
492
583
function calculateScrollOffset ( {
584
+ isStatic,
493
585
isTargetFixedPosition
494
586
} ) {
495
- const scrollOffsetTop = isTargetFixedPosition ? 0 : $ ( window ) . scrollTop ( ) ;
496
- const scrollOffsetLeft = isTargetFixedPosition ? 0 : $ ( window ) . scrollLeft ( ) ;
587
+ const scrollOffsetTop = isTargetFixedPosition || isStatic ? 0 : $ ( window ) . scrollTop ( ) ;
588
+ const scrollOffsetLeft = isTargetFixedPosition || isStatic ? 0 : $ ( window ) . scrollLeft ( ) ;
497
589
return {
498
590
scrollOffsetLeft,
499
591
scrollOffsetTop
@@ -525,8 +617,16 @@ function calculateScrollOffset ({
525
617
*/
526
618
function position (
527
619
{
620
+ isStatic,
528
621
isTargetFixedPosition,
529
622
position,
623
+ offset,
624
+ distance,
625
+ viewPortPadding,
626
+ hasArrow,
627
+ shouldOverrideOffset,
628
+ shouldOverrideDistance,
629
+ shouldOverrideArrow,
530
630
isRTL,
531
631
topNavOffset,
532
632
bottomNavOffset,
@@ -539,13 +639,6 @@ function position (
539
639
pass
540
640
) {
541
641
542
- // Fetch the CSS variable values for distance and viewport padding
543
- const {
544
- offset,
545
- distance,
546
- viewPortPadding
547
- } = fetchCSSVariables ( ) ;
548
-
549
642
// Convert target DOMRect to DistanceRect
550
643
const targetDistRect = convertToDistanceRect ( targetDOMRect , clientDOMRect ) ;
551
644
// Constrain shapes to padding and, when target is not fixed position, the navigation bar
@@ -780,6 +873,7 @@ function position (
780
873
isFillHeight && 'is-fill-height' ,
781
874
isSnapTop && 'is-snap-top' ,
782
875
isSnapBottom && 'is-snap-bottom' ,
876
+ hasArrow && 'has-arrow' ,
783
877
isArrowStart && 'is-arrow-start' ,
784
878
isArrowMiddle && 'is-arrow-middle' ,
785
879
isArrowEnd && 'is-arrow-end'
@@ -789,6 +883,7 @@ function position (
789
883
scrollOffsetLeft,
790
884
scrollOffsetTop
791
885
} = calculateScrollOffset ( {
886
+ isStatic,
792
887
isTargetFixedPosition
793
888
} ) ;
794
889
@@ -804,6 +899,10 @@ function position (
804
899
'--adapt-tooltip-target-position-height' : `${ targetDistRect . height } px`
805
900
} ) ;
806
901
902
+ if ( shouldOverrideOffset ) tooltipStyles [ '--adapt-tooltip-offset' ] = `${ offset } px` ;
903
+ if ( shouldOverrideDistance ) tooltipStyles [ '--adapt-tooltip-distance' ] = `${ distance } px` ;
904
+ if ( shouldOverrideArrow ) tooltipStyles [ '--adapt-tooltip-arrow' ] = String ( hasArrow ) . toLowerCase ( ) ;
905
+
807
906
return {
808
907
tooltipClasses,
809
908
tooltipStyles
0 commit comments