react/vendor/@reach/tooltip/index.js

502 lines
15 KiB
JavaScript

"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 <TooltipProvider> 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"
});
};