Skip to content

Commit 0b3b098

Browse files
authored
Update: Allow static tooltips, improved render and styling (#529)
1 parent 4b8a99a commit 0b3b098

File tree

3 files changed

+208
-91
lines changed

3 files changed

+208
-91
lines changed

js/views/TooltipItemView.js

+129-30
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ export default class TooltipItemView extends Backbone.View {
1414
return [
1515
'tooltip',
1616
this.model.get('isTargetFixedPosition') && 'is-fixed',
17+
this.isStatic && 'is-static',
1718
this.model.get('tooltipClasses') || 'is-vertical-axis is-arrow-middle is-bottom is-middle',
1819
this.model.get('isShown') && 'is-shown',
20+
this.model.get('wasShown') && 'was-shown',
1921
this.model.get('_classes')
2022
].filter(Boolean).join(' ');
2123
}
@@ -39,61 +41,119 @@ export default class TooltipItemView extends Backbone.View {
3941

4042
initialize({ $target, parent }) {
4143
_.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);
4246
this._classSet = new Set(_.result(this, 'className').trim().split(/\s+/));
4347
this.$target = $target;
4448
this.parent = parent;
4549
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);
4751
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+
};
5173
}
5274

5375
get environment() {
5476
// Determine if the navigation is bottom so tooltip doesn't overlap nav bar
5577
const navigationAlignment = Adapt.course.get('_navigation')?._navigationAlignment ?? 'top';
5678
const navHeight = $('.nav').outerHeight(true);
5779
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+
5899
return {
59100
position: this.model.get('_position') || 'outside bottom middle right',
101+
offset,
102+
distance,
103+
viewPortPadding,
104+
hasArrow,
105+
shouldOverrideOffset,
106+
shouldOverrideDistance,
107+
shouldOverrideArrow,
60108
isDisabled: $target.attr('aria-disabled') !== undefined || $target.hasClass('is-disabled') || $target.is(':disabled'),
109+
isStatic: this.isStatic,
61110
isTargetFixedPosition: Boolean(this.$target.add(this.$target.parents()).filter((index, el) => $(el).css('position') === 'fixed').length),
62111
isRTL: Adapt.config.get('_defaultDirection') === 'rtl',
63112
topNavOffset: navigationAlignment === 'top' ? navHeight : 0,
64113
bottomNavOffset: navigationAlignment === 'bottom' ? navHeight : 0,
65-
targetDOMRect: $target[0]?.getBoundingClientRect(),
114+
targetDOMRect: this.isStatic
115+
? this.applyStaticScrollOffset(targetDOMRect)
116+
: targetDOMRect,
66117
clientDOMRect: {
67118
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
69122
},
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)
73130
};
74131
}
75132

76133
doFirstPass() {
77-
this.model.set('isShown', false);
134+
if (!this.model) return;
135+
this.model.set('isShown', false, { silent: true });
78136
const environment = this.environment;
79137
const positions = position(environment, {}, FIRST_PASS);
80138
const {
81139
isDisabled,
140+
isStatic,
82141
isTargetFixedPosition,
83142
ariaHidden
84143
} = environment;
85144
this.model.set({
86145
isDisabled,
146+
isStatic,
87147
isTargetFixedPosition,
88148
ariaHidden,
89149
...positions
90-
});
150+
}, { silent: true });
91151
this.render();
92152
}
93153

94154
doSubsequentPasses() {
95155
if (!this.model) return;
96-
this.model.set('hasLoaded', true);
156+
this.model.set('hasLoaded', true, { silent: true });
97157
const multipassCache = {};
98158
// First pass - render at the requested position
99159
// Second pass - if needed, swap sides, switch axis and/or fill area
@@ -103,41 +163,66 @@ export default class TooltipItemView extends Backbone.View {
103163
const positions = position(this.environment, multipassCache, pass);
104164
const {
105165
isDisabled,
166+
isStatic,
106167
isTargetFixedPosition,
107168
ariaHidden
108169
} = environment;
109170
this.model.set({
110171
isDisabled,
172+
isStatic,
111173
isTargetFixedPosition,
112174
ariaHidden,
113175
...positions
114-
});
176+
}, { silent: true });
115177
this.render();
116178
}
117-
this.model.set('isShown', true);
179+
this.model.set('isShown', true, { silent: true });
118180
this.render();
181+
this.model.set('wasShown', true, { silent: true });
119182
}
120183

121184
render() {
122185
if (!this.model) return;
123186
const Template = templates.tooltip;
187+
this.model.set('ariaLabel', this.$target.attr('aria-label') || this.$target.find('.aria-label').text(), { silent: true });
124188
this.updateViewProperties();
125189
ReactDOM.render(<Template {...this.model.toJSON()} />, this.el);
126190
}
127191

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+
128208
onDeviceResize() {
209+
if (this.isStatic && this.isTargetPresent) return this.changed();
129210
this.remove();
130211
}
131212

132213
onMouseOut() {
214+
if (this.isStatic) return;
133215
this.remove();
134216
}
135217

136218
remove() {
137219
if (this.$el.hasClass('test')) return;
138220
this.stopListening(Adapt);
139221
$(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+
});
141226
this.render();
142227
this.model = null;
143228
this.$target = null;
@@ -156,12 +241,18 @@ export default class TooltipItemView extends Backbone.View {
156241
* Extract the offset, distance and padding properties from the css
157242
* @returns {Object}
158243
*/
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;
161248
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')
165256
};
166257
};
167258

@@ -175,7 +266,7 @@ function lengthToPx (name, length) {
175266
const unit = String(length).replaceAll(/[\d.]+/g, '').trim();
176267
const value = parseFloat(length);
177268
if (unit === 'rem') return value * parseInt(getComputedStyle(document.body).fontSize);
178-
if (unit === 'px') return value;
269+
if (unit === 'px' || unit === '') return value;
179270
throw new Error(`Cannot convert ${name} ${length} to pixels`);
180271
};
181272

@@ -490,10 +581,11 @@ function swapValues (a, b) {
490581
* @returns {Object}
491582
*/
492583
function calculateScrollOffset ({
584+
isStatic,
493585
isTargetFixedPosition
494586
}) {
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();
497589
return {
498590
scrollOffsetLeft,
499591
scrollOffsetTop
@@ -525,8 +617,16 @@ function calculateScrollOffset ({
525617
*/
526618
function position (
527619
{
620+
isStatic,
528621
isTargetFixedPosition,
529622
position,
623+
offset,
624+
distance,
625+
viewPortPadding,
626+
hasArrow,
627+
shouldOverrideOffset,
628+
shouldOverrideDistance,
629+
shouldOverrideArrow,
530630
isRTL,
531631
topNavOffset,
532632
bottomNavOffset,
@@ -539,13 +639,6 @@ function position (
539639
pass
540640
) {
541641

542-
// Fetch the CSS variable values for distance and viewport padding
543-
const {
544-
offset,
545-
distance,
546-
viewPortPadding
547-
} = fetchCSSVariables();
548-
549642
// Convert target DOMRect to DistanceRect
550643
const targetDistRect = convertToDistanceRect(targetDOMRect, clientDOMRect);
551644
// Constrain shapes to padding and, when target is not fixed position, the navigation bar
@@ -780,6 +873,7 @@ function position (
780873
isFillHeight && 'is-fill-height',
781874
isSnapTop && 'is-snap-top',
782875
isSnapBottom && 'is-snap-bottom',
876+
hasArrow && 'has-arrow',
783877
isArrowStart && 'is-arrow-start',
784878
isArrowMiddle && 'is-arrow-middle',
785879
isArrowEnd && 'is-arrow-end'
@@ -789,6 +883,7 @@ function position (
789883
scrollOffsetLeft,
790884
scrollOffsetTop
791885
} = calculateScrollOffset({
886+
isStatic,
792887
isTargetFixedPosition
793888
});
794889

@@ -804,6 +899,10 @@ function position (
804899
'--adapt-tooltip-target-position-height': `${targetDistRect.height}px`
805900
});
806901

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+
807906
return {
808907
tooltipClasses,
809908
tooltipStyles

js/views/TooltipView.js

+15-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import logging from '../logging';
33
import TooltipItemView from './TooltipItemView';
44
import TooltipItemModel from '../models/TooltipItemModel';
55
import a11y from '../a11y';
6+
import documentModifications from '../DOMElementModifications';
67

78
export default class TooltipView extends Backbone.View {
89

@@ -22,6 +23,7 @@ export default class TooltipView extends Backbone.View {
2223
this._tooltipData = {};
2324
this._tooltips = [];
2425
this.listenToOnce(Adapt, 'adapt:preInitialize', this.onAdaptPreInitialize);
26+
this.listenTo(documentModifications, 'added:[data-tooltip-id]', this.onAdded);
2527
this.render();
2628
}
2729

@@ -65,13 +67,21 @@ export default class TooltipView extends Backbone.View {
6567
if (this._currentId === id && event.name === 'focusin') return;
6668
this._currentId = id;
6769
const tooltip = this.getTooltip(id);
70+
if (tooltip?.get('_isStatic')) return;
6871
if (!tooltip?.get('_isEnabled')) return this.hide();
6972
if (event.ctrlKey && this.config._allowTest) {
7073
this.showTest(tooltip, $mouseoverEl);
7174
} else {
7275
this.show(tooltip, $mouseoverEl);
7376
}
74-
$(document).on('scroll', this.onScroll);
77+
}
78+
79+
onAdded(event) {
80+
const $addedEl = $(event.target);
81+
const id = $addedEl.data('tooltip-id');
82+
const tooltip = this.getTooltip(id);
83+
if (!tooltip?.get('_isEnabled') || !tooltip?.get('_isStatic')) return;
84+
this.show(tooltip, $addedEl);
7585
}
7686

7787
/**
@@ -103,7 +113,9 @@ export default class TooltipView extends Backbone.View {
103113
$target: $mouseoverEl,
104114
parent: this
105115
});
106-
this._tooltips.push(tooltipItem);
116+
if (!tooltip?.get('_isStatic')) {
117+
this._tooltips.push(tooltipItem);
118+
}
107119
this.$el.append(tooltipItem.$el);
108120
}
109121

@@ -147,6 +159,7 @@ export default class TooltipView extends Backbone.View {
147159
register(tooltipData) {
148160
if (!tooltipData._id) return logging.warn('Tooltip cannot be registered with no id');
149161
this._tooltipData[tooltipData._id] = new TooltipItemModel(tooltipData);
162+
return this._tooltipData[tooltipData._id];
150163
}
151164

152165
/**

0 commit comments

Comments
 (0)