-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Expand file tree
/
Copy pathtooltip.dart
More file actions
150 lines (130 loc) · 4.21 KB
/
tooltip.dart
File metadata and controls
150 lines (130 loc) · 4.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
// Copyright 2025 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:jaspr/dom.dart';
import 'package:jaspr/jaspr.dart';
import 'package:universal_web/web.dart' as web;
import '../../util.dart';
import '../util/global_event_listener.dart';
class Tooltip extends StatefulComponent {
const Tooltip({
required this.target,
required this.content,
super.key,
});
final Component target;
final Component? content;
@override
State<Tooltip> createState() => _TooltipState();
}
class _TooltipState extends State<Tooltip> {
static final isTouchscreen =
kIsWeb && web.window.matchMedia('(pointer: coarse)').matches;
final wrapperKey = GlobalNodeKey<web.HTMLElement>();
final targetKey = GlobalNodeKey<web.HTMLElement>();
final tooltipKey = GlobalNodeKey<web.HTMLElement>();
bool isVisible = false;
double tooltipOffset = 0.0;
@override
void initState() {
super.initState();
if (kIsWeb) {
setupTooltip();
}
}
void setupTooltip() {
context.binding.addPostFrameCallback(ensureVisible);
// Reposition tooltips on window resize.
web.EventStreamProviders.resizeEvent.forTarget(web.window).listen((_) {
ensureVisible();
});
}
/// Adjust the tooltip position to ensure it is fully inside the
/// ancestor .content element.
void ensureVisible() {
final target = targetKey.currentNode;
final tooltip = tooltipKey.currentNode;
if (tooltip == null || target == null) return;
setState(() {
tooltipOffset = calculateTooltipOffset(target, tooltip);
});
}
@override
Component build(BuildContext context) {
return span(
key: wrapperKey,
classes: 'tooltip-wrapper',
[
span(
key: targetKey,
classes: 'tooltip-target',
events: {
if (isTouchscreen)
'click': (e) {
if (!isVisible) {
setState(() => isVisible = true);
e.preventDefault();
}
},
},
[component.target],
),
if (component.content case final content?)
GlobalEventListener(
// Close tooltip when clicking outside of this wrapper.
onClick: isTouchscreen
? (e) {
if (wrapperKey.currentNode?.contains(
e.target as web.Node?,
) ==
true) {
return;
}
setState(() => isVisible = false);
}
: null,
// On touchscreen devices, close tooltips when scrolling.
onScroll: isTouchscreen
? (_) {
setState(() => isVisible = false);
}
: null,
span(
key: tooltipKey,
classes: ['tooltip', if (isVisible) 'visible'].toClasses,
styles: Styles(
raw: {
'left': tooltipOffset == 0
? '50%'
: tooltipOffset > 0
? 'calc(50% + ${tooltipOffset}px)'
: 'calc(50% - ${tooltipOffset.abs()}px)',
},
),
[
content,
],
),
),
],
);
}
}
double calculateTooltipOffset(web.HTMLElement target, web.HTMLElement tooltip) {
final targetRect = target.getBoundingClientRect();
final tooltipRect = tooltip.getBoundingClientRect();
final containerRect = tooltip.closest('.content')?.getBoundingClientRect();
final targetCenter = targetRect.left + (targetRect.width / 2);
final tooltipWidth = tooltipRect.width;
final initialLeft = targetCenter - (tooltipWidth / 2);
final initialRight = targetCenter + (tooltipWidth / 2);
final containerLeft = containerRect?.left ?? 0.0;
final containerRight = containerRect?.right ?? web.window.innerWidth;
if (initialLeft < containerLeft) {
return containerLeft - initialLeft;
} else if (initialRight > containerRight) {
return containerRight - initialRight;
} else {
return 0;
}
}