diff --git a/package.json b/package.json index 96dc68449e..c92d52c09d 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "@babel/preset-flow": "^7.0.0", "@babel/preset-react": "^7.0.0", "@reach/menu-button": "^0.1.12", - "@reach/tooltip": "^0.1.1", + "@reach/tooltip": "file:./vendor/@reach/tooltip", "adm-zip": "^0.4.7", "babel-core": "^7.0.0-bridge", "babel-eslint": "^9.0.0", diff --git a/src/devtools/views/Button.js b/src/devtools/views/Button.js index 79a857429a..78a9c8201f 100644 --- a/src/devtools/views/Button.js +++ b/src/devtools/views/Button.js @@ -1,10 +1,10 @@ // @flow import React from 'react'; -// TODO (tooltips) import Tooltip from '@reach/tooltip'; +import Tooltip from '@reach/tooltip'; import styles from './Button.css'; -// TODO (tooltips) import tooltipStyles from './Tooltip.css'; +import tooltipStyles from './Tooltip.css'; type Props = { children: React$Node, @@ -19,14 +19,13 @@ export default function Button({ ...rest }: Props) { let button = ( - ); - /* TODO (tooltips) if (title) { button = ( @@ -34,7 +33,6 @@ export default function Button({ ); } - */ return button; } diff --git a/src/devtools/views/Components/OwnersStack.js b/src/devtools/views/Components/OwnersStack.js index b2f0c3299d..3e98b2e594 100644 --- a/src/devtools/views/Components/OwnersStack.js +++ b/src/devtools/views/Components/OwnersStack.js @@ -6,7 +6,7 @@ import React, { useRef, useState, } from 'react'; -// TODO (tooltips) import Tooltip from '@reach/tooltip'; +import Tooltip from '@reach/tooltip'; import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; @@ -94,13 +94,13 @@ function ElementsDropdown({ return ( - {/* TODO (tooltips) */} - - - + + + + + - {/* TODO (tooltips) */} {ownerStack.map((id, index) => ( {children} @@ -50,7 +49,6 @@ export default function Toggle({ ); - /* TODO (tooltips) if (title) { toggle = ( @@ -58,7 +56,6 @@ export default function Toggle({ ); } - */ return toggle; } diff --git a/src/devtools/views/Tooltip.css b/src/devtools/views/Tooltip.css index 1703e2c76a..e8724b3855 100644 --- a/src/devtools/views/Tooltip.css +++ b/src/devtools/views/Tooltip.css @@ -8,5 +8,4 @@ /* Make sure this is above the DevTools, which are above the Overlay */ z-index: 10000002; - position: relative; } diff --git a/vendor/@reach/tooltip/es/index.js b/vendor/@reach/tooltip/es/index.js new file mode 100644 index 0000000000..7dd9a012bc --- /dev/null +++ b/vendor/@reach/tooltip/es/index.js @@ -0,0 +1,482 @@ +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; }; + +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; } + +//////////////////////////////////////////////////////////////////////////////// +// Welcome to @reach/tooltip! +// +// Quick definitions: +// +// - "on rest" or "rested on": describes when the element receives keyboard +// focus or mouse hover after a short delay (and hopefully soon, touch +// longpress). +// +// - "activation": describes a mouse click, keyboard enter, or keyboard space. +// +// Only one tooltip can be visible at a time, so we use a global state chart to +// describe the various states and transitions between states that are +// possible. With the all the timeouts involved with tooltips it's important to +// "make impossible states impossible" with a state machine. +// +// It's also okay to use these module globals because you don't server render +// tooltips. None of the state is changed outside of user events. +// +// There are a few features that are important to understand. +// +// 1. Tooltips don't show up until the user has rested on one, we don't +// want tooltips popupping up as you move your mouse around the page. +// +// 2. Once any tooltip becomes visible, other tooltips nearby should skip +// resting and display immediately. +// +// 3. Tooltips stick around for a little bit after blur/mouseleave. + +/* eslint-disable default-case */ + +import React, { Fragment, cloneElement, Children, useState, useRef, forwardRef, useEffect } from "react"; +import { useId } from "@reach/auto-id"; +import { wrapEvent } from "@reach/utils"; +import Portal from "@reach/portal"; +import VisuallyHidden from "@reach/visually-hidden"; +import { useRect } from "@reach/rect"; +import { node, string, func } from "prop-types"; + +//////////////////////////////////////////////////////////////////////////////// +var chart = { + initial: "idle", + states: { + idle: { + enter: clearContextId, + on: { + mouseenter: "focused", + focus: "focused" + } + }, + focused: { + enter: startRestTimer, + leave: clearRestTimer, + on: { + mousemove: "focused", + mouseleave: "idle", + mousedown: "dismissed", + blur: "idle", + rest: "visible" + } + }, + visible: { + on: { + mouseleave: "leavingVisible", + blur: "leavingVisible", + mousedown: "dismissed", + selectWithKeyboard: "dismissed" + } + }, + leavingVisible: { + enter: startLeavingVisibleTimer, + leave: function leave() { + clearLeavingVisibleTimer(); + clearContextId(); + }, + on: { + mouseenter: "visible", + focus: "visible", + timecomplete: "idle" + } + }, + dismissed: { + leave: function leave() { + // allows us to come on back later w/o entering something else first + context.id = null; + }, + on: { + mouseleave: "idle", + blur: "idle" + } + } + } +}; + +// chart context allows us to persist some data around, in Tooltip all we use +// is the id of the current tooltip being interacted with. +var context = { id: null }; +var state = chart.initial; + +//////////////////////////////////////////////////////////////////////////////// +// Finds the next state from the current state + action. If the chart doesn't +// describe that transition, it will throw. +// +// It also manages lifecycles of the machine, (enter/leave hooks on the state +// chart) +function transition(action, newContext) { + var stateDef = chart.states[state]; + var nextState = stateDef.on[action]; + + if (!nextState) { + throw new Error("Unknown state for action \"" + action + "\" from state \"" + state + "\""); + } + + if (stateDef.leave) { + stateDef.leave(); + } + + if (newContext) { + context = newContext; + } + + var nextDef = chart.states[nextState]; + if (nextDef.enter) { + nextDef.enter(); + } + + state = nextState; + notify(); +} + +//////////////////////////////////////////////////////////////////////////////// +// Subscriptions: +// +// We could require apps to render a around the app and use +// React context to notify Tooltips of changes to our state machine, instead +// we manage subscriptions ourselves and simplify the Tooltip API. +// +// Maybe if default context could take a hook (instead of just a static value) +// that was rendered at the root for us, that'd be cool! But it doesn't. +var subscriptions = []; + +function subscribe(fn) { + subscriptions.push(fn); + return function () { + subscriptions.splice(subscriptions.indexOf(fn), 1); + }; +} + +function notify() { + subscriptions.forEach(function (fn) { + return fn(state, context); + }); +} + +//////////////////////////////////////////////////////////////////////////////// +// Timeouts: + +// Manages when the user "rests" on an element. Keeps the interface from being +// flashing tooltips all the time as the user moves the mouse around the screen. +var restTimeout = void 0; +function startRestTimer() { + clearTimeout(restTimeout); + restTimeout = setTimeout(function () { + return transition("rest"); + }, 100); +} + +function clearRestTimer() { + clearTimeout(restTimeout); +} + +// Manages the delay to hide the tooltip after rest leaves. +var leavingVisibleTimer = void 0; +function startLeavingVisibleTimer() { + clearTimeout(leavingVisibleTimer); + leavingVisibleTimer = setTimeout(function () { + return transition("timecomplete"); + }, 500); +} + +function clearLeavingVisibleTimer() { + clearTimeout(leavingVisibleTimer); +} + +//////////////////////////////////////////////////////////////////////////////// +// Rando helpers + +// allows us to come on back later w/o entering something else first after the +// user leaves or dismisses +function clearContextId() { + context.id = null; +} + +// Avoids focus/mouseEnter from fighting with each other since you can have +// one element focused and another mouseentered at the same time. The first +// one sets the context.id and then the second one is ignored (for a single +// element, most recent focus/mouseenter wins between elements). +function isActive(id) { + return context.id === id; +} + +//////////////////////////////////////////////////////////////////////////////// +// THE HOOK! +export function useTooltip() { + var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, + onMouseEnter = _ref.onMouseEnter, + onMouseMove = _ref.onMouseMove, + onMouseLeave = _ref.onMouseLeave, + onFocus = _ref.onFocus, + onBlur = _ref.onBlur, + onKeyDown = _ref.onKeyDown, + onMouseDown = _ref.onMouseDown, + ref = _ref.ref; + + var _useState = useState(state === "visible"), + isVisible = _useState[0], + setIsVisible = _useState[1]; + + // hopefully they always pass a ref if they ever pass one + + + var triggerRef = ref || useRef(); + var triggerRect = useRect(triggerRef, isVisible); + var id = "tooltip:" + useId(); + + useEffect(function () { + return subscribe(function () { + if (context.id === id && (state === "visible" || state === "leavingVisible")) { + setIsVisible(true); + } else { + setIsVisible(false); + } + }); + }, [id]); + + var handleMouseEnter = function handleMouseEnter() { + if (isActive(id) && + // enables leaving then entering again before the times up + state !== "leavingVisible") { + return; + } + + switch (state) { + case "idle": + case "leavingVisible": + { + transition("mouseenter", { id: id }); + } + } + }; + + var handleMouseMove = function handleMouseMove() { + switch (state) { + case "focused": + { + transition("mousemove", { id: id }); + } + } + }; + + var handleFocus = function handleFocus() { + if (isActive(id)) return; + switch (state) { + case "idle": + case "leavingVisible": + { + transition("focus", { id: id }); + } + } + }; + + var handleMouseLeave = function handleMouseLeave() { + switch (state) { + case "focused": + case "visible": + case "dismissed": + { + transition("mouseleave"); + } + } + }; + + var handleBlur = function handleBlur() { + // Allow quick click from one tool to another + if (context.id !== id) return; + switch (state) { + case "focused": + case "visible": + case "dismissed": + { + transition("blur"); + } + } + }; + + var handleMouseDown = function handleMouseDown() { + // Allow quick click from one tool to another + if (context.id !== id) return; + switch (state) { + case "focused": + case "visible": + { + transition("mousedown"); + } + } + }; + + var handleKeyDown = function handleKeyDown(event) { + if (event.key === "Enter" || event.key === " ") { + switch (state) { + case "visible": + { + transition("selectWithKeyboard"); + } + } + } + }; + + var trigger = { + "aria-labelledby": id, + "data-reach-tooltip-trigger": "", + ref: triggerRef, + onMouseEnter: wrapEvent(onMouseEnter, handleMouseEnter), + onMouseMove: wrapEvent(onMouseMove, handleMouseMove), + onFocus: wrapEvent(onFocus, handleFocus), + onBlur: wrapEvent(onFocus, handleBlur), + onMouseLeave: wrapEvent(onMouseLeave, handleMouseLeave), + onKeyDown: wrapEvent(onKeyDown, handleKeyDown), + onMouseDown: wrapEvent(onMouseDown, handleMouseDown) + }; + + var tooltip = { + id: id, + triggerRect: triggerRect, + isVisible: isVisible + }; + + return [trigger, tooltip, isVisible]; +} + +//////////////////////////////////////////////////////////////////////////////// +export default function Tooltip(_ref2) { + var children = _ref2.children, + label = _ref2.label, + ariaLabel = _ref2.ariaLabel, + rest = _objectWithoutProperties(_ref2, ["children", "label", "ariaLabel"]); + + var _useTooltip = useTooltip(), + trigger = _useTooltip[0], + tooltip = _useTooltip[1]; + + return React.createElement( + Fragment, + null, + cloneElement(Children.only(children), trigger), + React.createElement(TooltipPopup, _extends({ + label: label, + ariaLabel: ariaLabel + }, tooltip, rest)) + ); +} + +process.env.NODE_ENV !== "production" ? Tooltip.propTypes = { + children: node.isRequired, + label: node.isRequired, + ariaLabel: string +} : void 0; + +//////////////////////////////////////////////////////////////////////////////// +var TooltipPopup = forwardRef(function TooltipPopup(_ref3, forwardRef) { + var label = _ref3.label, + ariaLabel = _ref3.ariaLabel, + position = _ref3.position, + isVisible = _ref3.isVisible, + id = _ref3.id, + triggerRect = _ref3.triggerRect, + rest = _objectWithoutProperties(_ref3, ["label", "ariaLabel", "position", "isVisible", "id", "triggerRect"]); + + return isVisible ? React.createElement( + Portal, + null, + React.createElement(TooltipContent, _extends({ + label: label, + ariaLabel: ariaLabel, + position: position, + isVisible: isVisible, + id: id, + triggerRect: triggerRect, + ref: forwardRef + }, rest)) + ) : null; +}); + +export { TooltipPopup }; +process.env.NODE_ENV !== "production" ? TooltipPopup.propTypes = { + label: node.isRequired, + ariaLabel: string, + position: func +} : void 0; + +// Need a separate component so that useRect works inside the portal +var TooltipContent = forwardRef(function TooltipContent(_ref4, forwardRef) { + var label = _ref4.label, + ariaLabel = _ref4.ariaLabel, + _ref4$position = _ref4.position, + position = _ref4$position === undefined ? positionDefault : _ref4$position, + isVisible = _ref4.isVisible, + id = _ref4.id, + triggerRect = _ref4.triggerRect, + style = _ref4.style, + rest = _objectWithoutProperties(_ref4, ["label", "ariaLabel", "position", "isVisible", "id", "triggerRect", "style"]); + + var useAriaLabel = ariaLabel != null; + var tooltipRef = useRef(); + var tooltipRect = useRect(tooltipRef, isVisible); + return React.createElement( + Fragment, + null, + React.createElement("div", _extends({ + "data-reach-tooltip": true, + role: useAriaLabel ? undefined : "tooltip", + id: useAriaLabel ? undefined : id, + children: label, + style: _extends({}, style, getStyles(position, triggerRect, tooltipRect)), + ref: function ref(node) { + tooltipRef.current = node; + if (forwardRef) forwardRef(node); + } + }, rest)), + useAriaLabel && React.createElement( + VisuallyHidden, + { role: "tooltip", id: id }, + ariaLabel + ) + ); +}); + +//////////////////////////////////////////////////////////////////////////////// +// TODO: research longpress tooltips on Android, iOS, implement here. +// - Probably want to position it by default above, since your thumb +// is below and would cover it +// - I'm thinking after longpress, display the tooltip and cancel any +// click events. Then on touchend, hide the tooltip after a timer +// in case their hand is obstructing it, then can remove and read +// the tooltip + +// feels awkward when it's perfectly aligned w/ the trigger +var OFFSET = 8; + +var getStyles = function getStyles(position, triggerRect, tooltipRect) { + var haventMeasuredTooltipYet = !tooltipRect; + if (haventMeasuredTooltipYet) { + return { visibility: "hidden" }; + } + return position(triggerRect, tooltipRect); +}; + +var positionDefault = function positionDefault(triggerRect, tooltipRect) { + var styles = { + left: triggerRect.left + window.scrollX + "px", + top: triggerRect.top + triggerRect.height + window.scrollY + "px" + }; + + var collisions = { + top: triggerRect.top - tooltipRect.height < 0, + right: window.innerWidth < triggerRect.left + tooltipRect.width, + bottom: window.innerHeight < triggerRect.bottom + tooltipRect.height + OFFSET, + left: triggerRect.left - tooltipRect.width < 0 + }; + + var directionRight = collisions.right && !collisions.left; + var directionUp = collisions.bottom && !collisions.top; + + return _extends({}, styles, { + left: directionRight ? triggerRect.right + OFFSET / 2 - tooltipRect.width + window.scrollX + "px" : triggerRect.left - OFFSET / 2 + window.scrollX + "px", + top: directionUp ? triggerRect.top - OFFSET - tooltipRect.height + window.scrollY + "px" : triggerRect.top + OFFSET + triggerRect.height + window.scrollY + "px" + }); +}; \ No newline at end of file diff --git a/vendor/@reach/tooltip/index.js b/vendor/@reach/tooltip/index.js new file mode 100644 index 0000000000..57668ecb39 --- /dev/null +++ b/vendor/@reach/tooltip/index.js @@ -0,0 +1,502 @@ +"use strict"; + +exports.__esModule = true; +exports.TooltipPopup = undefined; + +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; }; + +exports.useTooltip = useTooltip; +exports.default = Tooltip; + +var _react = require("react"); + +var _react2 = _interopRequireDefault(_react); + +var _autoId = require("@reach/auto-id"); + +var _utils = require("@reach/utils"); + +var _portal = require("@reach/portal"); + +var _portal2 = _interopRequireDefault(_portal); + +var _visuallyHidden = require("@reach/visually-hidden"); + +var _visuallyHidden2 = _interopRequireDefault(_visuallyHidden); + +var _rect = require("@reach/rect"); + +var _propTypes = require("prop-types"); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +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; } //////////////////////////////////////////////////////////////////////////////// +// Welcome to @reach/tooltip! +// +// Quick definitions: +// +// - "on rest" or "rested on": describes when the element receives keyboard +// focus or mouse hover after a short delay (and hopefully soon, touch +// longpress). +// +// - "activation": describes a mouse click, keyboard enter, or keyboard space. +// +// Only one tooltip can be visible at a time, so we use a global state chart to +// describe the various states and transitions between states that are +// possible. With the all the timeouts involved with tooltips it's important to +// "make impossible states impossible" with a state machine. +// +// It's also okay to use these module globals because you don't server render +// tooltips. None of the state is changed outside of user events. +// +// There are a few features that are important to understand. +// +// 1. Tooltips don't show up until the user has rested on one, we don't +// want tooltips popupping up as you move your mouse around the page. +// +// 2. Once any tooltip becomes visible, other tooltips nearby should skip +// resting and display immediately. +// +// 3. Tooltips stick around for a little bit after blur/mouseleave. + +/* eslint-disable default-case */ + +//////////////////////////////////////////////////////////////////////////////// +var chart = { + initial: "idle", + states: { + idle: { + enter: clearContextId, + on: { + mouseenter: "focused", + focus: "focused" + } + }, + focused: { + enter: startRestTimer, + leave: clearRestTimer, + on: { + mousemove: "focused", + mouseleave: "idle", + mousedown: "dismissed", + blur: "idle", + rest: "visible" + } + }, + visible: { + on: { + mouseleave: "leavingVisible", + blur: "leavingVisible", + mousedown: "dismissed", + selectWithKeyboard: "dismissed" + } + }, + leavingVisible: { + enter: startLeavingVisibleTimer, + leave: function leave() { + clearLeavingVisibleTimer(); + clearContextId(); + }, + on: { + mouseenter: "visible", + focus: "visible", + timecomplete: "idle" + } + }, + dismissed: { + leave: function leave() { + // allows us to come on back later w/o entering something else first + context.id = null; + }, + on: { + mouseleave: "idle", + blur: "idle" + } + } + } +}; + +// chart context allows us to persist some data around, in Tooltip all we use +// is the id of the current tooltip being interacted with. +var context = { id: null }; +var state = chart.initial; + +//////////////////////////////////////////////////////////////////////////////// +// Finds the next state from the current state + action. If the chart doesn't +// describe that transition, it will throw. +// +// It also manages lifecycles of the machine, (enter/leave hooks on the state +// chart) +function transition(action, newContext) { + var stateDef = chart.states[state]; + var nextState = stateDef.on[action]; + + if (!nextState) { + throw new Error("Unknown state for action \"" + action + "\" from state \"" + state + "\""); + } + + if (stateDef.leave) { + stateDef.leave(); + } + + if (newContext) { + context = newContext; + } + + var nextDef = chart.states[nextState]; + if (nextDef.enter) { + nextDef.enter(); + } + + state = nextState; + notify(); +} + +//////////////////////////////////////////////////////////////////////////////// +// Subscriptions: +// +// We could require apps to render a around the app and use +// React context to notify Tooltips of changes to our state machine, instead +// we manage subscriptions ourselves and simplify the Tooltip API. +// +// Maybe if default context could take a hook (instead of just a static value) +// that was rendered at the root for us, that'd be cool! But it doesn't. +var subscriptions = []; + +function subscribe(fn) { + subscriptions.push(fn); + return function () { + subscriptions.splice(subscriptions.indexOf(fn), 1); + }; +} + +function notify() { + subscriptions.forEach(function (fn) { + return fn(state, context); + }); +} + +//////////////////////////////////////////////////////////////////////////////// +// Timeouts: + +// Manages when the user "rests" on an element. Keeps the interface from being +// flashing tooltips all the time as the user moves the mouse around the screen. +var restTimeout = void 0; +function startRestTimer() { + clearTimeout(restTimeout); + restTimeout = setTimeout(function () { + return transition("rest"); + }, 100); +} + +function clearRestTimer() { + clearTimeout(restTimeout); +} + +// Manages the delay to hide the tooltip after rest leaves. +var leavingVisibleTimer = void 0; +function startLeavingVisibleTimer() { + clearTimeout(leavingVisibleTimer); + leavingVisibleTimer = setTimeout(function () { + return transition("timecomplete"); + }, 500); +} + +function clearLeavingVisibleTimer() { + clearTimeout(leavingVisibleTimer); +} + +//////////////////////////////////////////////////////////////////////////////// +// Rando helpers + +// allows us to come on back later w/o entering something else first after the +// user leaves or dismisses +function clearContextId() { + context.id = null; +} + +// Avoids focus/mouseEnter from fighting with each other since you can have +// one element focused and another mouseentered at the same time. The first +// one sets the context.id and then the second one is ignored (for a single +// element, most recent focus/mouseenter wins between elements). +function isActive(id) { + return context.id === id; +} + +//////////////////////////////////////////////////////////////////////////////// +// THE HOOK! +function useTooltip() { + var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, + onMouseEnter = _ref.onMouseEnter, + onMouseMove = _ref.onMouseMove, + onMouseLeave = _ref.onMouseLeave, + onFocus = _ref.onFocus, + onBlur = _ref.onBlur, + onKeyDown = _ref.onKeyDown, + onMouseDown = _ref.onMouseDown, + ref = _ref.ref; + + var _useState = (0, _react.useState)(state === "visible"), + isVisible = _useState[0], + setIsVisible = _useState[1]; + + // hopefully they always pass a ref if they ever pass one + + + var triggerRef = ref || (0, _react.useRef)(); + var triggerRect = (0, _rect.useRect)(triggerRef, isVisible); + var id = "tooltip:" + (0, _autoId.useId)(); + + (0, _react.useEffect)(function () { + return subscribe(function () { + if (context.id === id && (state === "visible" || state === "leavingVisible")) { + setIsVisible(true); + } else { + setIsVisible(false); + } + }); + }, [id]); + + var handleMouseEnter = function handleMouseEnter() { + if (isActive(id) && + // enables leaving then entering again before the times up + state !== "leavingVisible") { + return; + } + + switch (state) { + case "idle": + case "leavingVisible": + { + transition("mouseenter", { id: id }); + } + } + }; + + var handleMouseMove = function handleMouseMove() { + switch (state) { + case "focused": + { + transition("mousemove", { id: id }); + } + } + }; + + var handleFocus = function handleFocus() { + if (isActive(id)) return; + switch (state) { + case "idle": + case "leavingVisible": + { + transition("focus", { id: id }); + } + } + }; + + var handleMouseLeave = function handleMouseLeave() { + switch (state) { + case "focused": + case "visible": + case "dismissed": + { + transition("mouseleave"); + } + } + }; + + var handleBlur = function handleBlur() { + // Allow quick click from one tool to another + if (context.id !== id) return; + switch (state) { + case "focused": + case "visible": + case "dismissed": + { + transition("blur"); + } + } + }; + + var handleMouseDown = function handleMouseDown() { + // Allow quick click from one tool to another + if (context.id !== id) return; + switch (state) { + case "focused": + case "visible": + { + transition("mousedown"); + } + } + }; + + var handleKeyDown = function handleKeyDown(event) { + if (event.key === "Enter" || event.key === " ") { + switch (state) { + case "visible": + { + transition("selectWithKeyboard"); + } + } + } + }; + + var trigger = { + "aria-labelledby": id, + "data-reach-tooltip-trigger": "", + ref: triggerRef, + onMouseEnter: (0, _utils.wrapEvent)(onMouseEnter, handleMouseEnter), + onMouseMove: (0, _utils.wrapEvent)(onMouseMove, handleMouseMove), + onFocus: (0, _utils.wrapEvent)(onFocus, handleFocus), + onBlur: (0, _utils.wrapEvent)(onFocus, handleBlur), + onMouseLeave: (0, _utils.wrapEvent)(onMouseLeave, handleMouseLeave), + onKeyDown: (0, _utils.wrapEvent)(onKeyDown, handleKeyDown), + onMouseDown: (0, _utils.wrapEvent)(onMouseDown, handleMouseDown) + }; + + var tooltip = { + id: id, + triggerRect: triggerRect, + isVisible: isVisible + }; + + return [trigger, tooltip, isVisible]; +} + +//////////////////////////////////////////////////////////////////////////////// +function Tooltip(_ref2) { + var children = _ref2.children, + label = _ref2.label, + ariaLabel = _ref2.ariaLabel, + rest = _objectWithoutProperties(_ref2, ["children", "label", "ariaLabel"]); + + var _useTooltip = useTooltip(), + trigger = _useTooltip[0], + tooltip = _useTooltip[1]; + + return _react2.default.createElement( + _react.Fragment, + null, + (0, _react.cloneElement)(_react.Children.only(children), trigger), + _react2.default.createElement(TooltipPopup, _extends({ + label: label, + ariaLabel: ariaLabel + }, tooltip, rest)) + ); +} + +process.env.NODE_ENV !== "production" ? Tooltip.propTypes = { + children: _propTypes.node.isRequired, + label: _propTypes.node.isRequired, + ariaLabel: _propTypes.string +} : void 0; + +//////////////////////////////////////////////////////////////////////////////// +var TooltipPopup = (0, _react.forwardRef)(function TooltipPopup(_ref3, forwardRef) { + var label = _ref3.label, + ariaLabel = _ref3.ariaLabel, + position = _ref3.position, + isVisible = _ref3.isVisible, + id = _ref3.id, + triggerRect = _ref3.triggerRect, + rest = _objectWithoutProperties(_ref3, ["label", "ariaLabel", "position", "isVisible", "id", "triggerRect"]); + + return isVisible ? _react2.default.createElement( + _portal2.default, + null, + _react2.default.createElement(TooltipContent, _extends({ + label: label, + ariaLabel: ariaLabel, + position: position, + isVisible: isVisible, + id: id, + triggerRect: triggerRect, + ref: forwardRef + }, rest)) + ) : null; +}); + +exports.TooltipPopup = TooltipPopup; +process.env.NODE_ENV !== "production" ? TooltipPopup.propTypes = { + label: _propTypes.node.isRequired, + ariaLabel: _propTypes.string, + position: _propTypes.func +} : void 0; + +// Need a separate component so that useRect works inside the portal +var TooltipContent = (0, _react.forwardRef)(function TooltipContent(_ref4, forwardRef) { + var label = _ref4.label, + ariaLabel = _ref4.ariaLabel, + _ref4$position = _ref4.position, + position = _ref4$position === undefined ? positionDefault : _ref4$position, + isVisible = _ref4.isVisible, + id = _ref4.id, + triggerRect = _ref4.triggerRect, + style = _ref4.style, + rest = _objectWithoutProperties(_ref4, ["label", "ariaLabel", "position", "isVisible", "id", "triggerRect", "style"]); + + var useAriaLabel = ariaLabel != null; + var tooltipRef = (0, _react.useRef)(); + var tooltipRect = (0, _rect.useRect)(tooltipRef, isVisible); + return _react2.default.createElement( + _react.Fragment, + null, + _react2.default.createElement("div", _extends({ + "data-reach-tooltip": true, + role: useAriaLabel ? undefined : "tooltip", + id: useAriaLabel ? undefined : id, + children: label, + style: _extends({}, style, getStyles(position, triggerRect, tooltipRect)), + ref: function ref(node) { + tooltipRef.current = node; + if (forwardRef) forwardRef(node); + } + }, rest)), + useAriaLabel && _react2.default.createElement( + _visuallyHidden2.default, + { role: "tooltip", id: id }, + ariaLabel + ) + ); +}); + +//////////////////////////////////////////////////////////////////////////////// +// TODO: research longpress tooltips on Android, iOS, implement here. +// - Probably want to position it by default above, since your thumb +// is below and would cover it +// - I'm thinking after longpress, display the tooltip and cancel any +// click events. Then on touchend, hide the tooltip after a timer +// in case their hand is obstructing it, then can remove and read +// the tooltip + +// feels awkward when it's perfectly aligned w/ the trigger +var OFFSET = 8; + +var getStyles = function getStyles(position, triggerRect, tooltipRect) { + var haventMeasuredTooltipYet = !tooltipRect; + if (haventMeasuredTooltipYet) { + return { visibility: "hidden" }; + } + return position(triggerRect, tooltipRect); +}; + +var positionDefault = function positionDefault(triggerRect, tooltipRect) { + var styles = { + left: triggerRect.left + window.scrollX + "px", + top: triggerRect.top + triggerRect.height + window.scrollY + "px" + }; + + var collisions = { + top: triggerRect.top - tooltipRect.height < 0, + right: window.innerWidth < triggerRect.left + tooltipRect.width, + bottom: window.innerHeight < triggerRect.bottom + tooltipRect.height + OFFSET, + left: triggerRect.left - tooltipRect.width < 0 + }; + + var directionRight = collisions.right && !collisions.left; + var directionUp = collisions.bottom && !collisions.top; + + return _extends({}, styles, { + left: directionRight ? triggerRect.right + OFFSET / 2 - tooltipRect.width + window.scrollX + "px" : triggerRect.left - OFFSET / 2 + window.scrollX + "px", + top: directionUp ? triggerRect.top - OFFSET - tooltipRect.height + window.scrollY + "px" : triggerRect.top + OFFSET + triggerRect.height + window.scrollY + "px" + }); +}; \ No newline at end of file diff --git a/vendor/@reach/tooltip/package.json b/vendor/@reach/tooltip/package.json new file mode 100644 index 0000000000..36f1bb2f12 --- /dev/null +++ b/vendor/@reach/tooltip/package.json @@ -0,0 +1,36 @@ +{ + "name": "@reach/tooltip", + "version": "0.1.2", + "description": "Accessible tooltips", + "main": "index.js", + "module": "es/index.js", + "scripts": { + "test": "echo \"Write some tests you bum!\"", + "build": "node ../../shared/build-package", + "lint": "eslint . --max-warnings=0" + }, + "dependencies": { + "@reach/auto-id": "0.2.0", + "@reach/portal": "^0.2.0", + "@reach/rect": "^0.2.1", + "@reach/utils": "0.2.0", + "@reach/visually-hidden": "^0.1.3", + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": "^16.8.0", + "react-dom": "^16.8.0" + }, + "devDependencies": { + "react-spring": "^8.0.19" + }, + "author": "Ryan Florence", + "license": "MIT", + "files": [ + "es", + "src", + "lib", + "index.js", + "styles.css" + ] +} diff --git a/vendor/@reach/tooltip/src/.index.js.swp b/vendor/@reach/tooltip/src/.index.js.swp new file mode 100644 index 0000000000..0172def704 Binary files /dev/null and b/vendor/@reach/tooltip/src/.index.js.swp differ diff --git a/vendor/@reach/tooltip/src/index.js b/vendor/@reach/tooltip/src/index.js new file mode 100644 index 0000000000..d13b83542f --- /dev/null +++ b/vendor/@reach/tooltip/src/index.js @@ -0,0 +1,490 @@ +//////////////////////////////////////////////////////////////////////////////// +// Welcome to @reach/tooltip! +// +// Quick definitions: +// +// - "on rest" or "rested on": describes when the element receives keyboard +// focus or mouse hover after a short delay (and hopefully soon, touch +// longpress). +// +// - "activation": describes a mouse click, keyboard enter, or keyboard space. +// +// Only one tooltip can be visible at a time, so we use a global state chart to +// describe the various states and transitions between states that are +// possible. With the all the timeouts involved with tooltips it's important to +// "make impossible states impossible" with a state machine. +// +// It's also okay to use these module globals because you don't server render +// tooltips. None of the state is changed outside of user events. +// +// There are a few features that are important to understand. +// +// 1. Tooltips don't show up until the user has rested on one, we don't +// want tooltips popupping up as you move your mouse around the page. +// +// 2. Once any tooltip becomes visible, other tooltips nearby should skip +// resting and display immediately. +// +// 3. Tooltips stick around for a little bit after blur/mouseleave. + +/* eslint-disable default-case */ + +import React, { + Fragment, + cloneElement, + Children, + useState, + useRef, + forwardRef, + useEffect +} from "react"; +import { useId } from "@reach/auto-id"; +import { wrapEvent } from "@reach/utils"; +import Portal from "@reach/portal"; +import VisuallyHidden from "@reach/visually-hidden"; +import { useRect } from "@reach/rect"; +import { node, string, func } from "prop-types"; + +//////////////////////////////////////////////////////////////////////////////// +const chart = { + initial: "idle", + states: { + idle: { + enter: clearContextId, + on: { + mouseenter: "focused", + focus: "focused" + } + }, + focused: { + enter: startRestTimer, + leave: clearRestTimer, + on: { + mousemove: "focused", + mouseleave: "idle", + mousedown: "dismissed", + blur: "idle", + rest: "visible" + } + }, + visible: { + on: { + mouseleave: "leavingVisible", + blur: "leavingVisible", + mousedown: "dismissed", + selectWithKeyboard: "dismissed" + } + }, + leavingVisible: { + enter: startLeavingVisibleTimer, + leave: () => { + clearLeavingVisibleTimer(); + clearContextId(); + }, + on: { + mouseenter: "visible", + focus: "visible", + timecomplete: "idle" + } + }, + dismissed: { + leave: () => { + // allows us to come on back later w/o entering something else first + context.id = null; + }, + on: { + mouseleave: "idle", + blur: "idle" + } + } + } +}; + +// chart context allows us to persist some data around, in Tooltip all we use +// is the id of the current tooltip being interacted with. +let context = { id: null }; +let state = chart.initial; + +//////////////////////////////////////////////////////////////////////////////// +// Finds the next state from the current state + action. If the chart doesn't +// describe that transition, it will throw. +// +// It also manages lifecycles of the machine, (enter/leave hooks on the state +// chart) +function transition(action, newContext) { + const stateDef = chart.states[state]; + const nextState = stateDef.on[action]; + + if (!nextState) { + throw new Error( + `Unknown state for action "${action}" from state "${state}"` + ); + } + + if (stateDef.leave) { + stateDef.leave(); + } + + if (newContext) { + context = newContext; + } + + const nextDef = chart.states[nextState]; + if (nextDef.enter) { + nextDef.enter(); + } + + state = nextState; + notify(); +} + +//////////////////////////////////////////////////////////////////////////////// +// Subscriptions: +// +// We could require apps to render a around the app and use +// React context to notify Tooltips of changes to our state machine, instead +// we manage subscriptions ourselves and simplify the Tooltip API. +// +// Maybe if default context could take a hook (instead of just a static value) +// that was rendered at the root for us, that'd be cool! But it doesn't. +const subscriptions = []; + +function subscribe(fn) { + subscriptions.push(fn); + return () => { + subscriptions.splice(subscriptions.indexOf(fn), 1); + }; +} + +function notify() { + subscriptions.forEach(fn => fn(state, context)); +} + +//////////////////////////////////////////////////////////////////////////////// +// Timeouts: + +// Manages when the user "rests" on an element. Keeps the interface from being +// flashing tooltips all the time as the user moves the mouse around the screen. +let restTimeout; +function startRestTimer() { + clearTimeout(restTimeout); + restTimeout = setTimeout(() => transition("rest"), 100); +} + +function clearRestTimer() { + clearTimeout(restTimeout); +} + +// Manages the delay to hide the tooltip after rest leaves. +let leavingVisibleTimer; +function startLeavingVisibleTimer() { + clearTimeout(leavingVisibleTimer); + leavingVisibleTimer = setTimeout(() => transition("timecomplete"), 500); +} + +function clearLeavingVisibleTimer() { + clearTimeout(leavingVisibleTimer); +} + +//////////////////////////////////////////////////////////////////////////////// +// Rando helpers + +// allows us to come on back later w/o entering something else first after the +// user leaves or dismisses +function clearContextId() { + context.id = null; +} + +// Avoids focus/mouseEnter from fighting with each other since you can have +// one element focused and another mouseentered at the same time. The first +// one sets the context.id and then the second one is ignored (for a single +// element, most recent focus/mouseenter wins between elements). +function isActive(id) { + return context.id === id; +} + +//////////////////////////////////////////////////////////////////////////////// +// THE HOOK! +export function useTooltip({ + onMouseEnter, + onMouseMove, + onMouseLeave, + onFocus, + onBlur, + onKeyDown, + onMouseDown, + ref +} = {}) { + const [isVisible, setIsVisible] = useState(state === "visible"); + + // hopefully they always pass a ref if they ever pass one + const triggerRef = ref || useRef(); + const triggerRect = useRect(triggerRef, isVisible); + const id = `tooltip:${useId()}`; + + useEffect(() => { + return subscribe(() => { + if ( + context.id === id && + (state === "visible" || state === "leavingVisible") + ) { + setIsVisible(true); + } else { + setIsVisible(false); + } + }); + }, [id]); + + const handleMouseEnter = () => { + if ( + isActive(id) && + // enables leaving then entering again before the times up + state !== "leavingVisible" + ) { + return; + } + + switch (state) { + case "idle": + case "leavingVisible": { + transition("mouseenter", { id }); + } + } + }; + + const handleMouseMove = () => { + switch (state) { + case "focused": { + transition("mousemove", { id }); + } + } + }; + + const handleFocus = () => { + if (isActive(id)) return; + switch (state) { + case "idle": + case "leavingVisible": { + transition("focus", { id }); + } + } + }; + + const handleMouseLeave = () => { + switch (state) { + case "focused": + case "visible": + case "dismissed": { + transition("mouseleave"); + } + } + }; + + const handleBlur = () => { + // Allow quick click from one tool to another + if (context.id !== id) return; + switch (state) { + case "focused": + case "visible": + case "dismissed": { + transition("blur"); + } + } + }; + + const handleMouseDown = () => { + // Allow quick click from one tool to another + if (context.id !== id) return; + switch (state) { + case "focused": + case "visible": { + transition("mousedown"); + } + } + }; + + const handleKeyDown = event => { + if (event.key === "Enter" || event.key === " ") { + switch (state) { + case "visible": { + transition("selectWithKeyboard"); + } + } + } + }; + + const trigger = { + "aria-labelledby": id, + "data-reach-tooltip-trigger": "", + ref: triggerRef, + onMouseEnter: wrapEvent(onMouseEnter, handleMouseEnter), + onMouseMove: wrapEvent(onMouseMove, handleMouseMove), + onFocus: wrapEvent(onFocus, handleFocus), + onBlur: wrapEvent(onFocus, handleBlur), + onMouseLeave: wrapEvent(onMouseLeave, handleMouseLeave), + onKeyDown: wrapEvent(onKeyDown, handleKeyDown), + onMouseDown: wrapEvent(onMouseDown, handleMouseDown) + }; + + const tooltip = { + id, + triggerRect, + isVisible + }; + + return [trigger, tooltip, isVisible]; +} + +//////////////////////////////////////////////////////////////////////////////// +export default function Tooltip({ children, label, ariaLabel, ...rest }) { + const [trigger, tooltip] = useTooltip(); + return ( + + {cloneElement(Children.only(children), trigger)} + + + ); +} + +Tooltip.propTypes = { + children: node.isRequired, + label: node.isRequired, + ariaLabel: string +}; + +//////////////////////////////////////////////////////////////////////////////// +export const TooltipPopup = forwardRef(function TooltipPopup( + { + // own props + label, // could use children but want to encourage simple strings + ariaLabel, + position, + + // hook spread props + isVisible, + id, + triggerRect, + ...rest + }, + forwardRef +) { + return isVisible ? ( + + + + ) : null; +}); + +TooltipPopup.propTypes = { + label: node.isRequired, + ariaLabel: string, + position: func +}; + +// Need a separate component so that useRect works inside the portal +const TooltipContent = forwardRef(function TooltipContent( + { + label, + ariaLabel, + position = positionDefault, + isVisible, + id, + triggerRect, + style, + ...rest + }, + forwardRef +) { + const useAriaLabel = ariaLabel != null; + const tooltipRef = useRef(); + const tooltipRect = useRect(tooltipRef, isVisible); + return ( + +
{ + tooltipRef.current = node; + if (forwardRef) forwardRef(node); + }} + {...rest} + /> + {useAriaLabel && ( + + {ariaLabel} + + )} + + ); +}); + +//////////////////////////////////////////////////////////////////////////////// +// TODO: research longpress tooltips on Android, iOS, implement here. +// - Probably want to position it by default above, since your thumb +// is below and would cover it +// - I'm thinking after longpress, display the tooltip and cancel any +// click events. Then on touchend, hide the tooltip after a timer +// in case their hand is obstructing it, then can remove and read +// the tooltip + +// feels awkward when it's perfectly aligned w/ the trigger +const OFFSET = 8; + +const getStyles = (position, triggerRect, tooltipRect) => { + const haventMeasuredTooltipYet = !tooltipRect; + if (haventMeasuredTooltipYet) { + return { visibility: "hidden" }; + } + return position(triggerRect, tooltipRect); +}; + +const positionDefault = (triggerRect, tooltipRect) => { + const styles = { + left: `${triggerRect.left + window.scrollX}px`, + top: `${triggerRect.top + triggerRect.height + window.scrollY}px` + }; + + const collisions = { + top: triggerRect.top - tooltipRect.height < 0, + right: window.innerWidth < triggerRect.left + tooltipRect.width, + bottom: + window.innerHeight < triggerRect.bottom + tooltipRect.height + OFFSET, + left: triggerRect.left - tooltipRect.width < 0 + }; + + const directionRight = collisions.right && !collisions.left; + const directionUp = collisions.bottom && !collisions.top; + + return { + ...styles, + left: directionRight + ? `${triggerRect.right + + OFFSET / 2 - + tooltipRect.width + + window.scrollX}px` + : `${triggerRect.left - OFFSET / 2 + window.scrollX}px`, + top: directionUp + ? `${triggerRect.top - OFFSET - tooltipRect.height + window.scrollY}px` + : `${triggerRect.top + OFFSET + triggerRect.height + window.scrollY}px` + }; +}; diff --git a/vendor/@reach/tooltip/styles.css b/vendor/@reach/tooltip/styles.css new file mode 100644 index 0000000000..9c5e395afa --- /dev/null +++ b/vendor/@reach/tooltip/styles.css @@ -0,0 +1,11 @@ +[data-reach-tooltip] { + pointer-events: none; + position: absolute; + padding: 0.25em; + box-shadow: 2px 2px 10px hsla(0, 0%, 0%, 0.1); + white-space: nowrap; + font-size: 85%; + background: #f0f0f0; + color: #444; + border: solid 1px #ccc; +} diff --git a/yarn.lock b/yarn.lock index 5b81a1d831..8fad6396cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1052,10 +1052,8 @@ react-lifecycles-compat "^3.0.4" warning "^3.0.0" -"@reach/tooltip@^0.1.1": - version "0.1.1" - resolved "https://registry.yarnpkg.com/@reach/tooltip/-/tooltip-0.1.1.tgz#7f0385624eaffabcd4f075a098ef0fe4b472ce5d" - integrity sha512-EawPeRdeajl5HwtM8Q1rz0aNzOgRKnbs4GNW4tUa/6+F8yN93nZGxfYyefYGQgjzVOd0HdgeatUVqDQMgcldYA== +"@reach/tooltip@file:./vendor/@reach/tooltip": + version "0.1.2" dependencies: "@reach/auto-id" "0.2.0" "@reach/portal" "^0.2.0"