Added @reach/tooltip
Brian Vaughn committed
Apr 23, 2019 at 08:41 UTC
8b33dd673d965d23d2516bd30c9c3d05386ce881
13 files changed
+1536
-23
package.json
+1
-1
@@ -74,7 +74,7 @@
74
"@babel/preset-flow": "^7.0.0",
75
"@babel/preset-react": "^7.0.0",
76
"@reach/menu-button": "^0.1.12",
77
- "@reach/tooltip": "^0.1.1",
77
+ "@reach/tooltip": "file:./vendor/@reach/tooltip",
78
"adm-zip": "^0.4.7",
79
"babel-core": "^7.0.0-bridge",
80
"babel-eslint": "^9.0.0",
src/devtools/views/Button.js
+3
-5
@@ -1,10 +1,10 @@
1
// @flow
2
3
import React from 'react';
4
-// TODO (tooltips) import Tooltip from '@reach/tooltip';
4
+import Tooltip from '@reach/tooltip';
5
6
import styles from './Button.css';
7
-// TODO (tooltips) import tooltipStyles from './Tooltip.css';
7
+import tooltipStyles from './Tooltip.css';
8
9
type Props = {
10
children: React$Node,
@@ -19,14 +19,13 @@ export default function Button({
19
...rest
20
}: Props) {
21
let button = (
22
- <button className={`${styles.Button} ${className}`} title={title} {...rest}>
22
+ <button className={`${styles.Button} ${className}`} {...rest}>
23
<span className={`${styles.ButtonContent} ${className}`} tabIndex={-1}>
24
{children}
25
</span>
26
</button>
27
);
28
29
- /* TODO (tooltips)
29
if (title) {
30
button = (
31
<Tooltip className={tooltipStyles.Tooltip} label={title}>
@@ -34,7 +33,6 @@ export default function Button({
33
</Tooltip>
34
);
35
}
37
- */
36
37
return button;
38
}
src/devtools/views/Components/OwnersStack.js
+6
-6
@@ -6,7 +6,7 @@ import React, {
6
useRef,
7
useState,
8
} from 'react';
9
-// TODO (tooltips) import Tooltip from '@reach/tooltip';
9
+import Tooltip from '@reach/tooltip';
10
import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
11
import Button from '../Button';
12
import ButtonIcon from '../ButtonIcon';
@@ -94,13 +94,13 @@ function ElementsDropdown({
94
95
return (
96
<Menu>
97
- {/* TODO (tooltips) <Tooltip label="Open elements dropdown"> */}
97
<MenuButton className={styles.MenuButton}>
99
- <span className={styles.MenuButtonContent} tabIndex={-1}>
100
- <ButtonIcon type="more" />
101
- </span>
98
+ <Tooltip label="Open elements dropdown">
99
+ <span className={styles.MenuButtonContent} tabIndex={-1}>
100
+ <ButtonIcon type="more" />
101
+ </span>
102
+ </Tooltip>
103
</MenuButton>
103
- {/* TODO (tooltips) </Tooltip> */}
104
<MenuList className={styles.Modal}>
105
{ownerStack.map((id, index) => (
106
<MenuItem
src/devtools/views/DevTools.js
+1
-1
@@ -3,7 +3,7 @@
3
// Reach styles need to come before any component styles.
4
// This makes overridding the styles simpler.
5
import '@reach/menu-button/styles.css';
6
-// TODO (tooltips) import '@reach/tooltip/styles.css';
6
+import '@reach/tooltip/styles.css';
7
8
import React, { useEffect, useState } from 'react';
9
import Store from '../store';
src/devtools/views/Toggle.js
+2
-5
@@ -1,10 +1,10 @@
1
// @flow
2
3
import React, { useCallback } from 'react';
4
-// TODO (tooltips) import Tooltip from '@reach/tooltip';
4
+import Tooltip from '@reach/tooltip';
5
6
import styles from './Toggle.css';
7
-// TODO (tooltips) import tooltipStyles from './Tooltip.css';
7
+import tooltipStyles from './Tooltip.css';
8
9
type Props = {
10
children: React$Node,
@@ -42,7 +42,6 @@ export default function Toggle({
42
className={`${defaultClassName} ${className}`}
43
disabled={isDisabled}
44
onClick={handleClick}
45
- title={title}
45
>
46
<span className={styles.ToggleContent} tabIndex={-1}>
47
{children}
@@ -50,7 +49,6 @@ export default function Toggle({
49
</button>
50
);
51
53
- /* TODO (tooltips)
52
if (title) {
53
toggle = (
54
<Tooltip className={tooltipStyles.Tooltip} label={title}>
@@ -58,7 +56,6 @@ export default function Toggle({
56
</Tooltip>
57
);
58
}
61
- */
59
60
return toggle;
61
}
src/devtools/views/Tooltip.css
-1
@@ -8,5 +8,4 @@
8
9
/* Make sure this is above the DevTools, which are above the Overlay */
10
z-index: 10000002;
11
- position: relative;
11
}
vendor/@reach/tooltip/es/index.js
new
+482
@@ -0,0 +1,482 @@
1
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
2
+
3
+function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }
4
+
5
+////////////////////////////////////////////////////////////////////////////////
6
+// Welcome to @reach/tooltip!
7
+//
8
+// Quick definitions:
9
+//
10
+// - "on rest" or "rested on": describes when the element receives keyboard
11
+// focus or mouse hover after a short delay (and hopefully soon, touch
12
+// longpress).
13
+//
14
+// - "activation": describes a mouse click, keyboard enter, or keyboard space.
15
+//
16
+// Only one tooltip can be visible at a time, so we use a global state chart to
17
+// describe the various states and transitions between states that are
18
+// possible. With the all the timeouts involved with tooltips it's important to
19
+// "make impossible states impossible" with a state machine.
20
+//
21
+// It's also okay to use these module globals because you don't server render
22
+// tooltips. None of the state is changed outside of user events.
23
+//
24
+// There are a few features that are important to understand.
25
+//
26
+// 1. Tooltips don't show up until the user has rested on one, we don't
27
+// want tooltips popupping up as you move your mouse around the page.
28
+//
29
+// 2. Once any tooltip becomes visible, other tooltips nearby should skip
30
+// resting and display immediately.
31
+//
32
+// 3. Tooltips stick around for a little bit after blur/mouseleave.
33
+
34
+/* eslint-disable default-case */
35
+
36
+import React, { Fragment, cloneElement, Children, useState, useRef, forwardRef, useEffect } from "react";
37
+import { useId } from "@reach/auto-id";
38
+import { wrapEvent } from "@reach/utils";
39
+import Portal from "@reach/portal";
40
+import VisuallyHidden from "@reach/visually-hidden";
41
+import { useRect } from "@reach/rect";
42
+import { node, string, func } from "prop-types";
43
+
44
+////////////////////////////////////////////////////////////////////////////////
45
+var chart = {
46
+ initial: "idle",
47
+ states: {
48
+ idle: {
49
+ enter: clearContextId,
50
+ on: {
51
+ mouseenter: "focused",
52
+ focus: "focused"
53
+ }
54
+ },
55
+ focused: {
56
+ enter: startRestTimer,
57
+ leave: clearRestTimer,
58
+ on: {
59
+ mousemove: "focused",
60
+ mouseleave: "idle",
61
+ mousedown: "dismissed",
62
+ blur: "idle",
63
+ rest: "visible"
64
+ }
65
+ },
66
+ visible: {
67
+ on: {
68
+ mouseleave: "leavingVisible",
69
+ blur: "leavingVisible",
70
+ mousedown: "dismissed",
71
+ selectWithKeyboard: "dismissed"
72
+ }
73
+ },
74
+ leavingVisible: {
75
+ enter: startLeavingVisibleTimer,
76
+ leave: function leave() {
77
+ clearLeavingVisibleTimer();
78
+ clearContextId();
79
+ },
80
+ on: {
81
+ mouseenter: "visible",
82
+ focus: "visible",
83
+ timecomplete: "idle"
84
+ }
85
+ },
86
+ dismissed: {
87
+ leave: function leave() {
88
+ // allows us to come on back later w/o entering something else first
89
+ context.id = null;
90
+ },
91
+ on: {
92
+ mouseleave: "idle",
93
+ blur: "idle"
94
+ }
95
+ }
96
+ }
97
+};
98
+
99
+// chart context allows us to persist some data around, in Tooltip all we use
100
+// is the id of the current tooltip being interacted with.
101
+var context = { id: null };
102
+var state = chart.initial;
103
+
104
+////////////////////////////////////////////////////////////////////////////////
105
+// Finds the next state from the current state + action. If the chart doesn't
106
+// describe that transition, it will throw.
107
+//
108
+// It also manages lifecycles of the machine, (enter/leave hooks on the state
109
+// chart)
110
+function transition(action, newContext) {
111
+ var stateDef = chart.states[state];
112
+ var nextState = stateDef.on[action];
113
+
114
+ if (!nextState) {
115
+ throw new Error("Unknown state for action \"" + action + "\" from state \"" + state + "\"");
116
+ }
117
+
118
+ if (stateDef.leave) {
119
+ stateDef.leave();
120
+ }
121
+
122
+ if (newContext) {
123
+ context = newContext;
124
+ }
125
+
126
+ var nextDef = chart.states[nextState];
127
+ if (nextDef.enter) {
128
+ nextDef.enter();
129
+ }
130
+
131
+ state = nextState;
132
+ notify();
133
+}
134
+
135
+////////////////////////////////////////////////////////////////////////////////
136
+// Subscriptions:
137
+//
138
+// We could require apps to render a <TooltipProvider> around the app and use
139
+// React context to notify Tooltips of changes to our state machine, instead
140
+// we manage subscriptions ourselves and simplify the Tooltip API.
141
+//
142
+// Maybe if default context could take a hook (instead of just a static value)
143
+// that was rendered at the root for us, that'd be cool! But it doesn't.
144
+var subscriptions = [];
145
+
146
+function subscribe(fn) {
147
+ subscriptions.push(fn);
148
+ return function () {
149
+ subscriptions.splice(subscriptions.indexOf(fn), 1);
150
+ };
151
+}
152
+
153
+function notify() {
154
+ subscriptions.forEach(function (fn) {
155
+ return fn(state, context);
156
+ });
157
+}
158
+
159
+////////////////////////////////////////////////////////////////////////////////
160
+// Timeouts:
161
+
162
+// Manages when the user "rests" on an element. Keeps the interface from being
163
+// flashing tooltips all the time as the user moves the mouse around the screen.
164
+var restTimeout = void 0;
165
+function startRestTimer() {
166
+ clearTimeout(restTimeout);
167
+ restTimeout = setTimeout(function () {
168
+ return transition("rest");
169
+ }, 100);
170
+}
171
+
172
+function clearRestTimer() {
173
+ clearTimeout(restTimeout);
174
+}
175
+
176
+// Manages the delay to hide the tooltip after rest leaves.
177
+var leavingVisibleTimer = void 0;
178
+function startLeavingVisibleTimer() {
179
+ clearTimeout(leavingVisibleTimer);
180
+ leavingVisibleTimer = setTimeout(function () {
181
+ return transition("timecomplete");
182
+ }, 500);
183
+}
184
+
185
+function clearLeavingVisibleTimer() {
186
+ clearTimeout(leavingVisibleTimer);
187
+}
188
+
189
+////////////////////////////////////////////////////////////////////////////////
190
+// Rando helpers
191
+
192
+// allows us to come on back later w/o entering something else first after the
193
+// user leaves or dismisses
194
+function clearContextId() {
195
+ context.id = null;
196
+}
197
+
198
+// Avoids focus/mouseEnter from fighting with each other since you can have
199
+// one element focused and another mouseentered at the same time. The first
200
+// one sets the context.id and then the second one is ignored (for a single
201
+// element, most recent focus/mouseenter wins between elements).
202
+function isActive(id) {
203
+ return context.id === id;
204
+}
205
+
206
+////////////////////////////////////////////////////////////////////////////////
207
+// THE HOOK!
208
+export function useTooltip() {
209
+ var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
210
+ onMouseEnter = _ref.onMouseEnter,
211
+ onMouseMove = _ref.onMouseMove,
212
+ onMouseLeave = _ref.onMouseLeave,
213
+ onFocus = _ref.onFocus,
214
+ onBlur = _ref.onBlur,
215
+ onKeyDown = _ref.onKeyDown,
216
+ onMouseDown = _ref.onMouseDown,
217
+ ref = _ref.ref;
218
+
219
+ var _useState = useState(state === "visible"),
220
+ isVisible = _useState[0],
221
+ setIsVisible = _useState[1];
222
+
223
+ // hopefully they always pass a ref if they ever pass one
224
+
225
+
226
+ var triggerRef = ref || useRef();
227
+ var triggerRect = useRect(triggerRef, isVisible);
228
+ var id = "tooltip:" + useId();
229
+
230
+ useEffect(function () {
231
+ return subscribe(function () {
232
+ if (context.id === id && (state === "visible" || state === "leavingVisible")) {
233
+ setIsVisible(true);
234
+ } else {
235
+ setIsVisible(false);
236
+ }
237
+ });
238
+ }, [id]);
239
+
240
+ var handleMouseEnter = function handleMouseEnter() {
241
+ if (isActive(id) &&
242
+ // enables leaving then entering again before the times up
243
+ state !== "leavingVisible") {
244
+ return;
245
+ }
246
+
247
+ switch (state) {
248
+ case "idle":
249
+ case "leavingVisible":
250
+ {
251
+ transition("mouseenter", { id: id });
252
+ }
253
+ }
254
+ };
255
+
256
+ var handleMouseMove = function handleMouseMove() {
257
+ switch (state) {
258
+ case "focused":
259
+ {
260
+ transition("mousemove", { id: id });
261
+ }
262
+ }
263
+ };
264
+
265
+ var handleFocus = function handleFocus() {
266
+ if (isActive(id)) return;
267
+ switch (state) {
268
+ case "idle":
269
+ case "leavingVisible":
270
+ {
271
+ transition("focus", { id: id });
272
+ }
273
+ }
274
+ };
275
+
276
+ var handleMouseLeave = function handleMouseLeave() {
277
+ switch (state) {
278
+ case "focused":
279
+ case "visible":
280
+ case "dismissed":
281
+ {
282
+ transition("mouseleave");
283
+ }
284
+ }
285
+ };
286
+
287
+ var handleBlur = function handleBlur() {
288
+ // Allow quick click from one tool to another
289
+ if (context.id !== id) return;
290
+ switch (state) {
291
+ case "focused":
292
+ case "visible":
293
+ case "dismissed":
294
+ {
295
+ transition("blur");
296
+ }
297
+ }
298
+ };
299
+
300
+ var handleMouseDown = function handleMouseDown() {
301
+ // Allow quick click from one tool to another
302
+ if (context.id !== id) return;
303
+ switch (state) {
304
+ case "focused":
305
+ case "visible":
306
+ {
307
+ transition("mousedown");
308
+ }
309
+ }
310
+ };
311
+
312
+ var handleKeyDown = function handleKeyDown(event) {
313
+ if (event.key === "Enter" || event.key === " ") {
314
+ switch (state) {
315
+ case "visible":
316
+ {
317
+ transition("selectWithKeyboard");
318
+ }
319
+ }
320
+ }
321
+ };
322
+
323
+ var trigger = {
324
+ "aria-labelledby": id,
325
+ "data-reach-tooltip-trigger": "",
326
+ ref: triggerRef,
327
+ onMouseEnter: wrapEvent(onMouseEnter, handleMouseEnter),
328
+ onMouseMove: wrapEvent(onMouseMove, handleMouseMove),
329
+ onFocus: wrapEvent(onFocus, handleFocus),
330
+ onBlur: wrapEvent(onFocus, handleBlur),
331
+ onMouseLeave: wrapEvent(onMouseLeave, handleMouseLeave),
332
+ onKeyDown: wrapEvent(onKeyDown, handleKeyDown),
333
+ onMouseDown: wrapEvent(onMouseDown, handleMouseDown)
334
+ };
335
+
336
+ var tooltip = {
337
+ id: id,
338
+ triggerRect: triggerRect,
339
+ isVisible: isVisible
340
+ };
341
+
342
+ return [trigger, tooltip, isVisible];
343
+}
344
+
345
+////////////////////////////////////////////////////////////////////////////////
346
+export default function Tooltip(_ref2) {
347
+ var children = _ref2.children,
348
+ label = _ref2.label,
349
+ ariaLabel = _ref2.ariaLabel,
350
+ rest = _objectWithoutProperties(_ref2, ["children", "label", "ariaLabel"]);
351
+
352
+ var _useTooltip = useTooltip(),
353
+ trigger = _useTooltip[0],
354
+ tooltip = _useTooltip[1];
355
+
356
+ return React.createElement(
357
+ Fragment,
358
+ null,
359
+ cloneElement(Children.only(children), trigger),
360
+ React.createElement(TooltipPopup, _extends({
361
+ label: label,
362
+ ariaLabel: ariaLabel
363
+ }, tooltip, rest))
364
+ );
365
+}
366
+
367
+process.env.NODE_ENV !== "production" ? Tooltip.propTypes = {
368
+ children: node.isRequired,
369
+ label: node.isRequired,
370
+ ariaLabel: string
371
+} : void 0;
372
+
373
+////////////////////////////////////////////////////////////////////////////////
374
+var TooltipPopup = forwardRef(function TooltipPopup(_ref3, forwardRef) {
375
+ var label = _ref3.label,
376
+ ariaLabel = _ref3.ariaLabel,
377
+ position = _ref3.position,
378
+ isVisible = _ref3.isVisible,
379
+ id = _ref3.id,
380
+ triggerRect = _ref3.triggerRect,
381
+ rest = _objectWithoutProperties(_ref3, ["label", "ariaLabel", "position", "isVisible", "id", "triggerRect"]);
382
+
383
+ return isVisible ? React.createElement(
384
+ Portal,
385
+ null,
386
+ React.createElement(TooltipContent, _extends({
387
+ label: label,
388
+ ariaLabel: ariaLabel,
389
+ position: position,
390
+ isVisible: isVisible,
391
+ id: id,
392
+ triggerRect: triggerRect,
393
+ ref: forwardRef
394
+ }, rest))
395
+ ) : null;
396
+});
397
+
398
+export { TooltipPopup };
399
+process.env.NODE_ENV !== "production" ? TooltipPopup.propTypes = {
400
+ label: node.isRequired,
401
+ ariaLabel: string,
402
+ position: func
403
+} : void 0;
404
+
405
+// Need a separate component so that useRect works inside the portal
406
+var TooltipContent = forwardRef(function TooltipContent(_ref4, forwardRef) {
407
+ var label = _ref4.label,
408
+ ariaLabel = _ref4.ariaLabel,
409
+ _ref4$position = _ref4.position,
410
+ position = _ref4$position === undefined ? positionDefault : _ref4$position,
411
+ isVisible = _ref4.isVisible,
412
+ id = _ref4.id,
413
+ triggerRect = _ref4.triggerRect,
414
+ style = _ref4.style,
415
+ rest = _objectWithoutProperties(_ref4, ["label", "ariaLabel", "position", "isVisible", "id", "triggerRect", "style"]);
416
+
417
+ var useAriaLabel = ariaLabel != null;
418
+ var tooltipRef = useRef();
419
+ var tooltipRect = useRect(tooltipRef, isVisible);
420
+ return React.createElement(
421
+ Fragment,
422
+ null,
423
+ React.createElement("div", _extends({
424
+ "data-reach-tooltip": true,
425
+ role: useAriaLabel ? undefined : "tooltip",
426
+ id: useAriaLabel ? undefined : id,
427
+ children: label,
428
+ style: _extends({}, style, getStyles(position, triggerRect, tooltipRect)),
429
+ ref: function ref(node) {
430
+ tooltipRef.current = node;
431
+ if (forwardRef) forwardRef(node);
432
+ }
433
+ }, rest)),
434
+ useAriaLabel && React.createElement(
435
+ VisuallyHidden,
436
+ { role: "tooltip", id: id },
437
+ ariaLabel
438
+ )
439
+ );
440
+});
441
+
442
+////////////////////////////////////////////////////////////////////////////////
443
+// TODO: research longpress tooltips on Android, iOS, implement here.
444
+// - Probably want to position it by default above, since your thumb
445
+// is below and would cover it
446
+// - I'm thinking after longpress, display the tooltip and cancel any
447
+// click events. Then on touchend, hide the tooltip after a timer
448
+// in case their hand is obstructing it, then can remove and read
449
+// the tooltip
450
+
451
+// feels awkward when it's perfectly aligned w/ the trigger
452
+var OFFSET = 8;
453
+
454
+var getStyles = function getStyles(position, triggerRect, tooltipRect) {
455
+ var haventMeasuredTooltipYet = !tooltipRect;
456
+ if (haventMeasuredTooltipYet) {
457
+ return { visibility: "hidden" };
458
+ }
459
+ return position(triggerRect, tooltipRect);
460
+};
461
+
462
+var positionDefault = function positionDefault(triggerRect, tooltipRect) {
463
+ var styles = {
464
+ left: triggerRect.left + window.scrollX + "px",
465
+ top: triggerRect.top + triggerRect.height + window.scrollY + "px"
466
+ };
467
+
468
+ var collisions = {
469
+ top: triggerRect.top - tooltipRect.height < 0,
470
+ right: window.innerWidth < triggerRect.left + tooltipRect.width,
471
+ bottom: window.innerHeight < triggerRect.bottom + tooltipRect.height + OFFSET,
472
+ left: triggerRect.left - tooltipRect.width < 0
473
+ };
474
+
475
+ var directionRight = collisions.right && !collisions.left;
476
+ var directionUp = collisions.bottom && !collisions.top;
477
+
478
+ return _extends({}, styles, {
479
+ left: directionRight ? triggerRect.right + OFFSET / 2 - tooltipRect.width + window.scrollX + "px" : triggerRect.left - OFFSET / 2 + window.scrollX + "px",
480
+ top: directionUp ? triggerRect.top - OFFSET - tooltipRect.height + window.scrollY + "px" : triggerRect.top + OFFSET + triggerRect.height + window.scrollY + "px"
481
+ });
482
+};
\ No newline at end of file
vendor/@reach/tooltip/index.js
new
+502
@@ -0,0 +1,502 @@
1
+"use strict";
2
+
3
+exports.__esModule = true;
4
+exports.TooltipPopup = undefined;
5
+
6
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
7
+
8
+exports.useTooltip = useTooltip;
9
+exports.default = Tooltip;
10
+
11
+var _react = require("react");
12
+
13
+var _react2 = _interopRequireDefault(_react);
14
+
15
+var _autoId = require("@reach/auto-id");
16
+
17
+var _utils = require("@reach/utils");
18
+
19
+var _portal = require("@reach/portal");
20
+
21
+var _portal2 = _interopRequireDefault(_portal);
22
+
23
+var _visuallyHidden = require("@reach/visually-hidden");
24
+
25
+var _visuallyHidden2 = _interopRequireDefault(_visuallyHidden);
26
+
27
+var _rect = require("@reach/rect");
28
+
29
+var _propTypes = require("prop-types");
30
+
31
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
32
+
33
+function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } ////////////////////////////////////////////////////////////////////////////////
34
+// Welcome to @reach/tooltip!
35
+//
36
+// Quick definitions:
37
+//
38
+// - "on rest" or "rested on": describes when the element receives keyboard
39
+// focus or mouse hover after a short delay (and hopefully soon, touch
40
+// longpress).
41
+//
42
+// - "activation": describes a mouse click, keyboard enter, or keyboard space.
43
+//
44
+// Only one tooltip can be visible at a time, so we use a global state chart to
45
+// describe the various states and transitions between states that are
46
+// possible. With the all the timeouts involved with tooltips it's important to
47
+// "make impossible states impossible" with a state machine.
48
+//
49
+// It's also okay to use these module globals because you don't server render
50
+// tooltips. None of the state is changed outside of user events.
51
+//
52
+// There are a few features that are important to understand.
53
+//
54
+// 1. Tooltips don't show up until the user has rested on one, we don't
55
+// want tooltips popupping up as you move your mouse around the page.
56
+//
57
+// 2. Once any tooltip becomes visible, other tooltips nearby should skip
58
+// resting and display immediately.
59
+//
60
+// 3. Tooltips stick around for a little bit after blur/mouseleave.
61
+
62
+/* eslint-disable default-case */
63
+
64
+////////////////////////////////////////////////////////////////////////////////
65
+var chart = {
66
+ initial: "idle",
67
+ states: {
68
+ idle: {
69
+ enter: clearContextId,
70
+ on: {
71
+ mouseenter: "focused",
72
+ focus: "focused"
73
+ }
74
+ },
75
+ focused: {
76
+ enter: startRestTimer,
77
+ leave: clearRestTimer,
78
+ on: {
79
+ mousemove: "focused",
80
+ mouseleave: "idle",
81
+ mousedown: "dismissed",
82
+ blur: "idle",
83
+ rest: "visible"
84
+ }
85
+ },
86
+ visible: {
87
+ on: {
88
+ mouseleave: "leavingVisible",
89
+ blur: "leavingVisible",
90
+ mousedown: "dismissed",
91
+ selectWithKeyboard: "dismissed"
92
+ }
93
+ },
94
+ leavingVisible: {
95
+ enter: startLeavingVisibleTimer,
96
+ leave: function leave() {
97
+ clearLeavingVisibleTimer();
98
+ clearContextId();
99
+ },
100
+ on: {
101
+ mouseenter: "visible",
102
+ focus: "visible",
103
+ timecomplete: "idle"
104
+ }
105
+ },
106
+ dismissed: {
107
+ leave: function leave() {
108
+ // allows us to come on back later w/o entering something else first
109
+ context.id = null;
110
+ },
111
+ on: {
112
+ mouseleave: "idle",
113
+ blur: "idle"
114
+ }
115
+ }
116
+ }
117
+};
118
+
119
+// chart context allows us to persist some data around, in Tooltip all we use
120
+// is the id of the current tooltip being interacted with.
121
+var context = { id: null };
122
+var state = chart.initial;
123
+
124
+////////////////////////////////////////////////////////////////////////////////
125
+// Finds the next state from the current state + action. If the chart doesn't
126
+// describe that transition, it will throw.
127
+//
128
+// It also manages lifecycles of the machine, (enter/leave hooks on the state
129
+// chart)
130
+function transition(action, newContext) {
131
+ var stateDef = chart.states[state];
132
+ var nextState = stateDef.on[action];
133
+
134
+ if (!nextState) {
135
+ throw new Error("Unknown state for action \"" + action + "\" from state \"" + state + "\"");
136
+ }
137
+
138
+ if (stateDef.leave) {
139
+ stateDef.leave();
140
+ }
141
+
142
+ if (newContext) {
143
+ context = newContext;
144
+ }
145
+
146
+ var nextDef = chart.states[nextState];
147
+ if (nextDef.enter) {
148
+ nextDef.enter();
149
+ }
150
+
151
+ state = nextState;
152
+ notify();
153
+}
154
+
155
+////////////////////////////////////////////////////////////////////////////////
156
+// Subscriptions:
157
+//
158
+// We could require apps to render a <TooltipProvider> around the app and use
159
+// React context to notify Tooltips of changes to our state machine, instead
160
+// we manage subscriptions ourselves and simplify the Tooltip API.
161
+//
162
+// Maybe if default context could take a hook (instead of just a static value)
163
+// that was rendered at the root for us, that'd be cool! But it doesn't.
164
+var subscriptions = [];
165
+
166
+function subscribe(fn) {
167
+ subscriptions.push(fn);
168
+ return function () {
169
+ subscriptions.splice(subscriptions.indexOf(fn), 1);
170
+ };
171
+}
172
+
173
+function notify() {
174
+ subscriptions.forEach(function (fn) {
175
+ return fn(state, context);
176
+ });
177
+}
178
+
179
+////////////////////////////////////////////////////////////////////////////////
180
+// Timeouts:
181
+
182
+// Manages when the user "rests" on an element. Keeps the interface from being
183
+// flashing tooltips all the time as the user moves the mouse around the screen.
184
+var restTimeout = void 0;
185
+function startRestTimer() {
186
+ clearTimeout(restTimeout);
187
+ restTimeout = setTimeout(function () {
188
+ return transition("rest");
189
+ }, 100);
190
+}
191
+
192
+function clearRestTimer() {
193
+ clearTimeout(restTimeout);
194
+}
195
+
196
+// Manages the delay to hide the tooltip after rest leaves.
197
+var leavingVisibleTimer = void 0;
198
+function startLeavingVisibleTimer() {
199
+ clearTimeout(leavingVisibleTimer);
200
+ leavingVisibleTimer = setTimeout(function () {
201
+ return transition("timecomplete");
202
+ }, 500);
203
+}
204
+
205
+function clearLeavingVisibleTimer() {
206
+ clearTimeout(leavingVisibleTimer);
207
+}
208
+
209
+////////////////////////////////////////////////////////////////////////////////
210
+// Rando helpers
211
+
212
+// allows us to come on back later w/o entering something else first after the
213
+// user leaves or dismisses
214
+function clearContextId() {
215
+ context.id = null;
216
+}
217
+
218
+// Avoids focus/mouseEnter from fighting with each other since you can have
219
+// one element focused and another mouseentered at the same time. The first
220
+// one sets the context.id and then the second one is ignored (for a single
221
+// element, most recent focus/mouseenter wins between elements).
222
+function isActive(id) {
223
+ return context.id === id;
224
+}
225
+
226
+////////////////////////////////////////////////////////////////////////////////
227
+// THE HOOK!
228
+function useTooltip() {
229
+ var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
230
+ onMouseEnter = _ref.onMouseEnter,
231
+ onMouseMove = _ref.onMouseMove,
232
+ onMouseLeave = _ref.onMouseLeave,
233
+ onFocus = _ref.onFocus,
234
+ onBlur = _ref.onBlur,
235
+ onKeyDown = _ref.onKeyDown,
236
+ onMouseDown = _ref.onMouseDown,
237
+ ref = _ref.ref;
238
+
239
+ var _useState = (0, _react.useState)(state === "visible"),
240
+ isVisible = _useState[0],
241
+ setIsVisible = _useState[1];
242
+
243
+ // hopefully they always pass a ref if they ever pass one
244
+
245
+
246
+ var triggerRef = ref || (0, _react.useRef)();
247
+ var triggerRect = (0, _rect.useRect)(triggerRef, isVisible);
248
+ var id = "tooltip:" + (0, _autoId.useId)();
249
+
250
+ (0, _react.useEffect)(function () {
251
+ return subscribe(function () {
252
+ if (context.id === id && (state === "visible" || state === "leavingVisible")) {
253
+ setIsVisible(true);
254
+ } else {
255
+ setIsVisible(false);
256
+ }
257
+ });
258
+ }, [id]);
259
+
260
+ var handleMouseEnter = function handleMouseEnter() {
261
+ if (isActive(id) &&
262
+ // enables leaving then entering again before the times up
263
+ state !== "leavingVisible") {
264
+ return;
265
+ }
266
+
267
+ switch (state) {
268
+ case "idle":
269
+ case "leavingVisible":
270
+ {
271
+ transition("mouseenter", { id: id });
272
+ }
273
+ }
274
+ };
275
+
276
+ var handleMouseMove = function handleMouseMove() {
277
+ switch (state) {
278
+ case "focused":
279
+ {
280
+ transition("mousemove", { id: id });
281
+ }
282
+ }
283
+ };
284
+
285
+ var handleFocus = function handleFocus() {
286
+ if (isActive(id)) return;
287
+ switch (state) {
288
+ case "idle":
289
+ case "leavingVisible":
290
+ {
291
+ transition("focus", { id: id });
292
+ }
293
+ }
294
+ };
295
+
296
+ var handleMouseLeave = function handleMouseLeave() {
297
+ switch (state) {
298
+ case "focused":
299
+ case "visible":
300
+ case "dismissed":
301
+ {
302
+ transition("mouseleave");
303
+ }
304
+ }
305
+ };
306
+
307
+ var handleBlur = function handleBlur() {
308
+ // Allow quick click from one tool to another
309
+ if (context.id !== id) return;
310
+ switch (state) {
311
+ case "focused":
312
+ case "visible":
313
+ case "dismissed":
314
+ {
315
+ transition("blur");
316
+ }
317
+ }
318
+ };
319
+
320
+ var handleMouseDown = function handleMouseDown() {
321
+ // Allow quick click from one tool to another
322
+ if (context.id !== id) return;
323
+ switch (state) {
324
+ case "focused":
325
+ case "visible":
326
+ {
327
+ transition("mousedown");
328
+ }
329
+ }
330
+ };
331
+
332
+ var handleKeyDown = function handleKeyDown(event) {
333
+ if (event.key === "Enter" || event.key === " ") {
334
+ switch (state) {
335
+ case "visible":
336
+ {
337
+ transition("selectWithKeyboard");
338
+ }
339
+ }
340
+ }
341
+ };
342
+
343
+ var trigger = {
344
+ "aria-labelledby": id,
345
+ "data-reach-tooltip-trigger": "",
346
+ ref: triggerRef,
347
+ onMouseEnter: (0, _utils.wrapEvent)(onMouseEnter, handleMouseEnter),
348
+ onMouseMove: (0, _utils.wrapEvent)(onMouseMove, handleMouseMove),
349
+ onFocus: (0, _utils.wrapEvent)(onFocus, handleFocus),
350
+ onBlur: (0, _utils.wrapEvent)(onFocus, handleBlur),
351
+ onMouseLeave: (0, _utils.wrapEvent)(onMouseLeave, handleMouseLeave),
352
+ onKeyDown: (0, _utils.wrapEvent)(onKeyDown, handleKeyDown),
353
+ onMouseDown: (0, _utils.wrapEvent)(onMouseDown, handleMouseDown)
354
+ };
355
+
356
+ var tooltip = {
357
+ id: id,
358
+ triggerRect: triggerRect,
359
+ isVisible: isVisible
360
+ };
361
+
362
+ return [trigger, tooltip, isVisible];
363
+}
364
+
365
+////////////////////////////////////////////////////////////////////////////////
366
+function Tooltip(_ref2) {
367
+ var children = _ref2.children,
368
+ label = _ref2.label,
369
+ ariaLabel = _ref2.ariaLabel,
370
+ rest = _objectWithoutProperties(_ref2, ["children", "label", "ariaLabel"]);
371
+
372
+ var _useTooltip = useTooltip(),
373
+ trigger = _useTooltip[0],
374
+ tooltip = _useTooltip[1];
375
+
376
+ return _react2.default.createElement(
377
+ _react.Fragment,
378
+ null,
379
+ (0, _react.cloneElement)(_react.Children.only(children), trigger),
380
+ _react2.default.createElement(TooltipPopup, _extends({
381
+ label: label,
382
+ ariaLabel: ariaLabel
383
+ }, tooltip, rest))
384
+ );
385
+}
386
+
387
+process.env.NODE_ENV !== "production" ? Tooltip.propTypes = {
388
+ children: _propTypes.node.isRequired,
389
+ label: _propTypes.node.isRequired,
390
+ ariaLabel: _propTypes.string
391
+} : void 0;
392
+
393
+////////////////////////////////////////////////////////////////////////////////
394
+var TooltipPopup = (0, _react.forwardRef)(function TooltipPopup(_ref3, forwardRef) {
395
+ var label = _ref3.label,
396
+ ariaLabel = _ref3.ariaLabel,
397
+ position = _ref3.position,
398
+ isVisible = _ref3.isVisible,
399
+ id = _ref3.id,
400
+ triggerRect = _ref3.triggerRect,
401
+ rest = _objectWithoutProperties(_ref3, ["label", "ariaLabel", "position", "isVisible", "id", "triggerRect"]);
402
+
403
+ return isVisible ? _react2.default.createElement(
404
+ _portal2.default,
405
+ null,
406
+ _react2.default.createElement(TooltipContent, _extends({
407
+ label: label,
408
+ ariaLabel: ariaLabel,
409
+ position: position,
410
+ isVisible: isVisible,
411
+ id: id,
412
+ triggerRect: triggerRect,
413
+ ref: forwardRef
414
+ }, rest))
415
+ ) : null;
416
+});
417
+
418
+exports.TooltipPopup = TooltipPopup;
419
+process.env.NODE_ENV !== "production" ? TooltipPopup.propTypes = {
420
+ label: _propTypes.node.isRequired,
421
+ ariaLabel: _propTypes.string,
422
+ position: _propTypes.func
423
+} : void 0;
424
+
425
+// Need a separate component so that useRect works inside the portal
426
+var TooltipContent = (0, _react.forwardRef)(function TooltipContent(_ref4, forwardRef) {
427
+ var label = _ref4.label,
428
+ ariaLabel = _ref4.ariaLabel,
429
+ _ref4$position = _ref4.position,
430
+ position = _ref4$position === undefined ? positionDefault : _ref4$position,
431
+ isVisible = _ref4.isVisible,
432
+ id = _ref4.id,
433
+ triggerRect = _ref4.triggerRect,
434
+ style = _ref4.style,
435
+ rest = _objectWithoutProperties(_ref4, ["label", "ariaLabel", "position", "isVisible", "id", "triggerRect", "style"]);
436
+
437
+ var useAriaLabel = ariaLabel != null;
438
+ var tooltipRef = (0, _react.useRef)();
439
+ var tooltipRect = (0, _rect.useRect)(tooltipRef, isVisible);
440
+ return _react2.default.createElement(
441
+ _react.Fragment,
442
+ null,
443
+ _react2.default.createElement("div", _extends({
444
+ "data-reach-tooltip": true,
445
+ role: useAriaLabel ? undefined : "tooltip",
446
+ id: useAriaLabel ? undefined : id,
447
+ children: label,
448
+ style: _extends({}, style, getStyles(position, triggerRect, tooltipRect)),
449
+ ref: function ref(node) {
450
+ tooltipRef.current = node;
451
+ if (forwardRef) forwardRef(node);
452
+ }
453
+ }, rest)),
454
+ useAriaLabel && _react2.default.createElement(
455
+ _visuallyHidden2.default,
456
+ { role: "tooltip", id: id },
457
+ ariaLabel
458
+ )
459
+ );
460
+});
461
+
462
+////////////////////////////////////////////////////////////////////////////////
463
+// TODO: research longpress tooltips on Android, iOS, implement here.
464
+// - Probably want to position it by default above, since your thumb
465
+// is below and would cover it
466
+// - I'm thinking after longpress, display the tooltip and cancel any
467
+// click events. Then on touchend, hide the tooltip after a timer
468
+// in case their hand is obstructing it, then can remove and read
469
+// the tooltip
470
+
471
+// feels awkward when it's perfectly aligned w/ the trigger
472
+var OFFSET = 8;
473
+
474
+var getStyles = function getStyles(position, triggerRect, tooltipRect) {
475
+ var haventMeasuredTooltipYet = !tooltipRect;
476
+ if (haventMeasuredTooltipYet) {
477
+ return { visibility: "hidden" };
478
+ }
479
+ return position(triggerRect, tooltipRect);
480
+};
481
+
482
+var positionDefault = function positionDefault(triggerRect, tooltipRect) {
483
+ var styles = {
484
+ left: triggerRect.left + window.scrollX + "px",
485
+ top: triggerRect.top + triggerRect.height + window.scrollY + "px"
486
+ };
487
+
488
+ var collisions = {
489
+ top: triggerRect.top - tooltipRect.height < 0,
490
+ right: window.innerWidth < triggerRect.left + tooltipRect.width,
491
+ bottom: window.innerHeight < triggerRect.bottom + tooltipRect.height + OFFSET,
492
+ left: triggerRect.left - tooltipRect.width < 0
493
+ };
494
+
495
+ var directionRight = collisions.right && !collisions.left;
496
+ var directionUp = collisions.bottom && !collisions.top;
497
+
498
+ return _extends({}, styles, {
499
+ left: directionRight ? triggerRect.right + OFFSET / 2 - tooltipRect.width + window.scrollX + "px" : triggerRect.left - OFFSET / 2 + window.scrollX + "px",
500
+ top: directionUp ? triggerRect.top - OFFSET - tooltipRect.height + window.scrollY + "px" : triggerRect.top + OFFSET + triggerRect.height + window.scrollY + "px"
501
+ });
502
+};
\ No newline at end of file
vendor/@reach/tooltip/package.json
new
+36
@@ -0,0 +1,36 @@
1
+{
2
+ "name": "@reach/tooltip",
3
+ "version": "0.1.2",
4
+ "description": "Accessible tooltips",
5
+ "main": "index.js",
6
+ "module": "es/index.js",
7
+ "scripts": {
8
+ "test": "echo \"Write some tests you bum!\"",
9
+ "build": "node ../../shared/build-package",
10
+ "lint": "eslint . --max-warnings=0"
11
+ },
12
+ "dependencies": {
13
+ "@reach/auto-id": "0.2.0",
14
+ "@reach/portal": "^0.2.0",
15
+ "@reach/rect": "^0.2.1",
16
+ "@reach/utils": "0.2.0",
17
+ "@reach/visually-hidden": "^0.1.3",
18
+ "prop-types": "^15.7.2"
19
+ },
20
+ "peerDependencies": {
21
+ "react": "^16.8.0",
22
+ "react-dom": "^16.8.0"
23
+ },
24
+ "devDependencies": {
25
+ "react-spring": "^8.0.19"
26
+ },
27
+ "author": "Ryan Florence",
28
+ "license": "MIT",
29
+ "files": [
30
+ "es",
31
+ "src",
32
+ "lib",
33
+ "index.js",
34
+ "styles.css"
35
+ ]
36
+}
vendor/@reach/tooltip/src/.index.js.swp
Binary files /dev/null and b/vendor/@reach/tooltip/src/.index.js.swp differ
vendor/@reach/tooltip/src/index.js
new
+490
@@ -0,0 +1,490 @@
1
+////////////////////////////////////////////////////////////////////////////////
2
+// Welcome to @reach/tooltip!
3
+//
4
+// Quick definitions:
5
+//
6
+// - "on rest" or "rested on": describes when the element receives keyboard
7
+// focus or mouse hover after a short delay (and hopefully soon, touch
8
+// longpress).
9
+//
10
+// - "activation": describes a mouse click, keyboard enter, or keyboard space.
11
+//
12
+// Only one tooltip can be visible at a time, so we use a global state chart to
13
+// describe the various states and transitions between states that are
14
+// possible. With the all the timeouts involved with tooltips it's important to
15
+// "make impossible states impossible" with a state machine.
16
+//
17
+// It's also okay to use these module globals because you don't server render
18
+// tooltips. None of the state is changed outside of user events.
19
+//
20
+// There are a few features that are important to understand.
21
+//
22
+// 1. Tooltips don't show up until the user has rested on one, we don't
23
+// want tooltips popupping up as you move your mouse around the page.
24
+//
25
+// 2. Once any tooltip becomes visible, other tooltips nearby should skip
26
+// resting and display immediately.
27
+//
28
+// 3. Tooltips stick around for a little bit after blur/mouseleave.
29
+
30
+/* eslint-disable default-case */
31
+
32
+import React, {
33
+ Fragment,
34
+ cloneElement,
35
+ Children,
36
+ useState,
37
+ useRef,
38
+ forwardRef,
39
+ useEffect
40
+} from "react";
41
+import { useId } from "@reach/auto-id";
42
+import { wrapEvent } from "@reach/utils";
43
+import Portal from "@reach/portal";
44
+import VisuallyHidden from "@reach/visually-hidden";
45
+import { useRect } from "@reach/rect";
46
+import { node, string, func } from "prop-types";
47
+
48
+////////////////////////////////////////////////////////////////////////////////
49
+const chart = {
50
+ initial: "idle",
51
+ states: {
52
+ idle: {
53
+ enter: clearContextId,
54
+ on: {
55
+ mouseenter: "focused",
56
+ focus: "focused"
57
+ }
58
+ },
59
+ focused: {
60
+ enter: startRestTimer,
61
+ leave: clearRestTimer,
62
+ on: {
63
+ mousemove: "focused",
64
+ mouseleave: "idle",
65
+ mousedown: "dismissed",
66
+ blur: "idle",
67
+ rest: "visible"
68
+ }
69
+ },
70
+ visible: {
71
+ on: {
72
+ mouseleave: "leavingVisible",
73
+ blur: "leavingVisible",
74
+ mousedown: "dismissed",
75
+ selectWithKeyboard: "dismissed"
76
+ }
77
+ },
78
+ leavingVisible: {
79
+ enter: startLeavingVisibleTimer,
80
+ leave: () => {
81
+ clearLeavingVisibleTimer();
82
+ clearContextId();
83
+ },
84
+ on: {
85
+ mouseenter: "visible",
86
+ focus: "visible",
87
+ timecomplete: "idle"
88
+ }
89
+ },
90
+ dismissed: {
91
+ leave: () => {
92
+ // allows us to come on back later w/o entering something else first
93
+ context.id = null;
94
+ },
95
+ on: {
96
+ mouseleave: "idle",
97
+ blur: "idle"
98
+ }
99
+ }
100
+ }
101
+};
102
+
103
+// chart context allows us to persist some data around, in Tooltip all we use
104
+// is the id of the current tooltip being interacted with.
105
+let context = { id: null };
106
+let state = chart.initial;
107
+
108
+////////////////////////////////////////////////////////////////////////////////
109
+// Finds the next state from the current state + action. If the chart doesn't
110
+// describe that transition, it will throw.
111
+//
112
+// It also manages lifecycles of the machine, (enter/leave hooks on the state
113
+// chart)
114
+function transition(action, newContext) {
115
+ const stateDef = chart.states[state];
116
+ const nextState = stateDef.on[action];
117
+
118
+ if (!nextState) {
119
+ throw new Error(
120
+ `Unknown state for action "${action}" from state "${state}"`
121
+ );
122
+ }
123
+
124
+ if (stateDef.leave) {
125
+ stateDef.leave();
126
+ }
127
+
128
+ if (newContext) {
129
+ context = newContext;
130
+ }
131
+
132
+ const nextDef = chart.states[nextState];
133
+ if (nextDef.enter) {
134
+ nextDef.enter();
135
+ }
136
+
137
+ state = nextState;
138
+ notify();
139
+}
140
+
141
+////////////////////////////////////////////////////////////////////////////////
142
+// Subscriptions:
143
+//
144
+// We could require apps to render a <TooltipProvider> around the app and use
145
+// React context to notify Tooltips of changes to our state machine, instead
146
+// we manage subscriptions ourselves and simplify the Tooltip API.
147
+//
148
+// Maybe if default context could take a hook (instead of just a static value)
149
+// that was rendered at the root for us, that'd be cool! But it doesn't.
150
+const subscriptions = [];
151
+
152
+function subscribe(fn) {
153
+ subscriptions.push(fn);
154
+ return () => {
155
+ subscriptions.splice(subscriptions.indexOf(fn), 1);
156
+ };
157
+}
158
+
159
+function notify() {
160
+ subscriptions.forEach(fn => fn(state, context));
161
+}
162
+
163
+////////////////////////////////////////////////////////////////////////////////
164
+// Timeouts:
165
+
166
+// Manages when the user "rests" on an element. Keeps the interface from being
167
+// flashing tooltips all the time as the user moves the mouse around the screen.
168
+let restTimeout;
169
+function startRestTimer() {
170
+ clearTimeout(restTimeout);
171
+ restTimeout = setTimeout(() => transition("rest"), 100);
172
+}
173
+
174
+function clearRestTimer() {
175
+ clearTimeout(restTimeout);
176
+}
177
+
178
+// Manages the delay to hide the tooltip after rest leaves.
179
+let leavingVisibleTimer;
180
+function startLeavingVisibleTimer() {
181
+ clearTimeout(leavingVisibleTimer);
182
+ leavingVisibleTimer = setTimeout(() => transition("timecomplete"), 500);
183
+}
184
+
185
+function clearLeavingVisibleTimer() {
186
+ clearTimeout(leavingVisibleTimer);
187
+}
188
+
189
+////////////////////////////////////////////////////////////////////////////////
190
+// Rando helpers
191
+
192
+// allows us to come on back later w/o entering something else first after the
193
+// user leaves or dismisses
194
+function clearContextId() {
195
+ context.id = null;
196
+}
197
+
198
+// Avoids focus/mouseEnter from fighting with each other since you can have
199
+// one element focused and another mouseentered at the same time. The first
200
+// one sets the context.id and then the second one is ignored (for a single
201
+// element, most recent focus/mouseenter wins between elements).
202
+function isActive(id) {
203
+ return context.id === id;
204
+}
205
+
206
+////////////////////////////////////////////////////////////////////////////////
207
+// THE HOOK!
208
+export function useTooltip({
209
+ onMouseEnter,
210
+ onMouseMove,
211
+ onMouseLeave,
212
+ onFocus,
213
+ onBlur,
214
+ onKeyDown,
215
+ onMouseDown,
216
+ ref
217
+} = {}) {
218
+ const [isVisible, setIsVisible] = useState(state === "visible");
219
+
220
+ // hopefully they always pass a ref if they ever pass one
221
+ const triggerRef = ref || useRef();
222
+ const triggerRect = useRect(triggerRef, isVisible);
223
+ const id = `tooltip:${useId()}`;
224
+
225
+ useEffect(() => {
226
+ return subscribe(() => {
227
+ if (
228
+ context.id === id &&
229
+ (state === "visible" || state === "leavingVisible")
230
+ ) {
231
+ setIsVisible(true);
232
+ } else {
233
+ setIsVisible(false);
234
+ }
235
+ });
236
+ }, [id]);
237
+
238
+ const handleMouseEnter = () => {
239
+ if (
240
+ isActive(id) &&
241
+ // enables leaving then entering again before the times up
242
+ state !== "leavingVisible"
243
+ ) {
244
+ return;
245
+ }
246
+
247
+ switch (state) {
248
+ case "idle":
249
+ case "leavingVisible": {
250
+ transition("mouseenter", { id });
251
+ }
252
+ }
253
+ };
254
+
255
+ const handleMouseMove = () => {
256
+ switch (state) {
257
+ case "focused": {
258
+ transition("mousemove", { id });
259
+ }
260
+ }
261
+ };
262
+
263
+ const handleFocus = () => {
264
+ if (isActive(id)) return;
265
+ switch (state) {
266
+ case "idle":
267
+ case "leavingVisible": {
268
+ transition("focus", { id });
269
+ }
270
+ }
271
+ };
272
+
273
+ const handleMouseLeave = () => {
274
+ switch (state) {
275
+ case "focused":
276
+ case "visible":
277
+ case "dismissed": {
278
+ transition("mouseleave");
279
+ }
280
+ }
281
+ };
282
+
283
+ const handleBlur = () => {
284
+ // Allow quick click from one tool to another
285
+ if (context.id !== id) return;
286
+ switch (state) {
287
+ case "focused":
288
+ case "visible":
289
+ case "dismissed": {
290
+ transition("blur");
291
+ }
292
+ }
293
+ };
294
+
295
+ const handleMouseDown = () => {
296
+ // Allow quick click from one tool to another
297
+ if (context.id !== id) return;
298
+ switch (state) {
299
+ case "focused":
300
+ case "visible": {
301
+ transition("mousedown");
302
+ }
303
+ }
304
+ };
305
+
306
+ const handleKeyDown = event => {
307
+ if (event.key === "Enter" || event.key === " ") {
308
+ switch (state) {
309
+ case "visible": {
310
+ transition("selectWithKeyboard");
311
+ }
312
+ }
313
+ }
314
+ };
315
+
316
+ const trigger = {
317
+ "aria-labelledby": id,
318
+ "data-reach-tooltip-trigger": "",
319
+ ref: triggerRef,
320
+ onMouseEnter: wrapEvent(onMouseEnter, handleMouseEnter),
321
+ onMouseMove: wrapEvent(onMouseMove, handleMouseMove),
322
+ onFocus: wrapEvent(onFocus, handleFocus),
323
+ onBlur: wrapEvent(onFocus, handleBlur),
324
+ onMouseLeave: wrapEvent(onMouseLeave, handleMouseLeave),
325
+ onKeyDown: wrapEvent(onKeyDown, handleKeyDown),
326
+ onMouseDown: wrapEvent(onMouseDown, handleMouseDown)
327
+ };
328
+
329
+ const tooltip = {
330
+ id,
331
+ triggerRect,
332
+ isVisible
333
+ };
334
+
335
+ return [trigger, tooltip, isVisible];
336
+}
337
+
338
+////////////////////////////////////////////////////////////////////////////////
339
+export default function Tooltip({ children, label, ariaLabel, ...rest }) {
340
+ const [trigger, tooltip] = useTooltip();
341
+ return (
342
+ <Fragment>
343
+ {cloneElement(Children.only(children), trigger)}
344
+ <TooltipPopup
345
+ label={label}
346
+ ariaLabel={ariaLabel}
347
+ {...tooltip}
348
+ {...rest}
349
+ />
350
+ </Fragment>
351
+ );
352
+}
353
+
354
+Tooltip.propTypes = {
355
+ children: node.isRequired,
356
+ label: node.isRequired,
357
+ ariaLabel: string
358
+};
359
+
360
+////////////////////////////////////////////////////////////////////////////////
361
+export const TooltipPopup = forwardRef(function TooltipPopup(
362
+ {
363
+ // own props
364
+ label, // could use children but want to encourage simple strings
365
+ ariaLabel,
366
+ position,
367
+
368
+ // hook spread props
369
+ isVisible,
370
+ id,
371
+ triggerRect,
372
+ ...rest
373
+ },
374
+ forwardRef
375
+) {
376
+ return isVisible ? (
377
+ <Portal>
378
+ <TooltipContent
379
+ label={label}
380
+ ariaLabel={ariaLabel}
381
+ position={position}
382
+ isVisible={isVisible}
383
+ id={id}
384
+ triggerRect={triggerRect}
385
+ ref={forwardRef}
386
+ {...rest}
387
+ />
388
+ </Portal>
389
+ ) : null;
390
+});
391
+
392
+TooltipPopup.propTypes = {
393
+ label: node.isRequired,
394
+ ariaLabel: string,
395
+ position: func
396
+};
397
+
398
+// Need a separate component so that useRect works inside the portal
399
+const TooltipContent = forwardRef(function TooltipContent(
400
+ {
401
+ label,
402
+ ariaLabel,
403
+ position = positionDefault,
404
+ isVisible,
405
+ id,
406
+ triggerRect,
407
+ style,
408
+ ...rest
409
+ },
410
+ forwardRef
411
+) {
412
+ const useAriaLabel = ariaLabel != null;
413
+ const tooltipRef = useRef();
414
+ const tooltipRect = useRect(tooltipRef, isVisible);
415
+ return (
416
+ <Fragment>
417
+ <div
418
+ data-reach-tooltip
419
+ role={useAriaLabel ? undefined : "tooltip"}
420
+ id={useAriaLabel ? undefined : id}
421
+ children={label}
422
+ style={{
423
+ ...style,
424
+ ...getStyles(position, triggerRect, tooltipRect)
425
+ }}
426
+ ref={node => {
427
+ tooltipRef.current = node;
428
+ if (forwardRef) forwardRef(node);
429
+ }}
430
+ {...rest}
431
+ />
432
+ {useAriaLabel && (
433
+ <VisuallyHidden role="tooltip" id={id}>
434
+ {ariaLabel}
435
+ </VisuallyHidden>
436
+ )}
437
+ </Fragment>
438
+ );
439
+});
440
+
441
+////////////////////////////////////////////////////////////////////////////////
442
+// TODO: research longpress tooltips on Android, iOS, implement here.
443
+// - Probably want to position it by default above, since your thumb
444
+// is below and would cover it
445
+// - I'm thinking after longpress, display the tooltip and cancel any
446
+// click events. Then on touchend, hide the tooltip after a timer
447
+// in case their hand is obstructing it, then can remove and read
448
+// the tooltip
449
+
450
+// feels awkward when it's perfectly aligned w/ the trigger
451
+const OFFSET = 8;
452
+
453
+const getStyles = (position, triggerRect, tooltipRect) => {
454
+ const haventMeasuredTooltipYet = !tooltipRect;
455
+ if (haventMeasuredTooltipYet) {
456
+ return { visibility: "hidden" };
457
+ }
458
+ return position(triggerRect, tooltipRect);
459
+};
460
+
461
+const positionDefault = (triggerRect, tooltipRect) => {
462
+ const styles = {
463
+ left: `${triggerRect.left + window.scrollX}px`,
464
+ top: `${triggerRect.top + triggerRect.height + window.scrollY}px`
465
+ };
466
+
467
+ const collisions = {
468
+ top: triggerRect.top - tooltipRect.height < 0,
469
+ right: window.innerWidth < triggerRect.left + tooltipRect.width,
470
+ bottom:
471
+ window.innerHeight < triggerRect.bottom + tooltipRect.height + OFFSET,
472
+ left: triggerRect.left - tooltipRect.width < 0
473
+ };
474
+
475
+ const directionRight = collisions.right && !collisions.left;
476
+ const directionUp = collisions.bottom && !collisions.top;
477
+
478
+ return {
479
+ ...styles,
480
+ left: directionRight
481
+ ? `${triggerRect.right +
482
+ OFFSET / 2 -
483
+ tooltipRect.width +
484
+ window.scrollX}px`
485
+ : `${triggerRect.left - OFFSET / 2 + window.scrollX}px`,
486
+ top: directionUp
487
+ ? `${triggerRect.top - OFFSET - tooltipRect.height + window.scrollY}px`
488
+ : `${triggerRect.top + OFFSET + triggerRect.height + window.scrollY}px`
489
+ };
490
+};
vendor/@reach/tooltip/styles.css
new
+11
@@ -0,0 +1,11 @@
1
+[data-reach-tooltip] {
2
+ pointer-events: none;
3
+ position: absolute;
4
+ padding: 0.25em;
5
+ box-shadow: 2px 2px 10px hsla(0, 0%, 0%, 0.1);
6
+ white-space: nowrap;
7
+ font-size: 85%;
8
+ background: #f0f0f0;
9
+ color: #444;
10
+ border: solid 1px #ccc;
11
+}
yarn.lock
+2
-4
@@ -1052,10 +1052,8 @@
1052
react-lifecycles-compat "^3.0.4"
1053
warning "^3.0.0"
1054
1055
-"@reach/tooltip@^0.1.1":
1056
- version "0.1.1"
1057
- resolved "https://registry.yarnpkg.com/@reach/tooltip/-/tooltip-0.1.1.tgz#7f0385624eaffabcd4f075a098ef0fe4b472ce5d"
1058
- integrity sha512-EawPeRdeajl5HwtM8Q1rz0aNzOgRKnbs4GNW4tUa/6+F8yN93nZGxfYyefYGQgjzVOd0HdgeatUVqDQMgcldYA==
1055
+"@reach/tooltip@file:./vendor/@reach/tooltip":
1056
+ version "0.1.2"
1057
dependencies:
1058
"@reach/auto-id" "0.2.0"
1059
"@reach/portal" "^0.2.0"