@samitouri / QOS-React / commits / 8b33dd673d

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"