UNPKG

@atlaskit/onboarding

Version:

An onboarding spotlight introduces new features to users through focused messages or multi-step tours.

169 lines (166 loc) 6.15 kB
import _slicedToArray from "@babel/runtime/helpers/slicedToArray"; import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { bind } from 'bind-event-listener'; import __noop from '@atlaskit/ds-lib/noop'; import { fg } from '@atlaskit/platform-feature-flags/fg'; // The minimum interval between position updates in milliseconds var POSITION_UPDATE_INTERVAL = 200; var getElementRect = function getElementRect(element) { var _element$getBoundingC = element.getBoundingClientRect(), height = _element$getBoundingC.height, left = _element$getBoundingC.left, top = _element$getBoundingC.top, width = _element$getBoundingC.width; return { height: height, left: left, top: top, width: width }; }; var useResizeAwareElementBox = function useResizeAwareElementBox(element, updateMethod) { var _useState = useState({ width: 0, height: 0, left: 0, top: 0 }), _useState2 = _slicedToArray(_useState, 2), box = _useState2[0], setBox = _useState2[1]; useLayoutEffect(function () { if (updateMethod === 'resizeListener') { if (fg('scroll-lock-replacement')) { // use setTimeout 0 to defer the state update to avoid content shifting when pages have scrollbars // more details are https://www.loom.com/share/96a5d7c2afd74146a3c005bf20a8c69e?sid=968b00c1-e5ab-4ea0-9fe4-e534fe7088e4 setTimeout(function () { setBox(getElementRect(element)); }, 0); } else { setBox(getElementRect(element)); } } }, [element, updateMethod]); useEffect(function () { var onResize = function onResize() { requestAnimationFrame(function () { setBox(getElementRect(element)); }); }; if (updateMethod === 'resizeListener') { return bind(window, { type: 'resize', listener: onResize }); } return __noop; }, [element, updateMethod]); return box; }; var usePollingElementBox = function usePollingElementBox(element, updateMethod) { // These are intentionally tracked as number primitives rather than as a shared `box` object. // Since the requestAnimationFrame code below updates this often, we want to avoid re-renders // when the values are the same. React uses `Object.is` to figure out if the state changed after a setState. // If we represent this as a shared `box` object, this will re-render even if the two objects have identical contents. var _useState3 = useState(0), _useState4 = _slicedToArray(_useState3, 2), width = _useState4[0], setWidth = _useState4[1]; var _useState5 = useState(0), _useState6 = _slicedToArray(_useState5, 2), height = _useState6[0], setHeight = _useState6[1]; var _useState7 = useState(0), _useState8 = _slicedToArray(_useState7, 2), left = _useState8[0], setLeft = _useState8[1]; var _useState9 = useState(0), _useState0 = _slicedToArray(_useState9, 2), top = _useState0[0], setTop = _useState0[1]; useLayoutEffect(function () { if (updateMethod === 'polling') { if (fg('scroll-lock-replacement')) { // use setTimeout 0 to defer the state update to avoid content shifting when pages have scrollbars // more details are https://www.loom.com/share/96a5d7c2afd74146a3c005bf20a8c69e?sid=968b00c1-e5ab-4ea0-9fe4-e534fe7088e4 setTimeout(function () { var newBox = getElementRect(element); setWidth(newBox.width); setHeight(newBox.height); setLeft(newBox.left); setTop(newBox.top); }, 0); } else { var newBox = getElementRect(element); setWidth(newBox.width); setHeight(newBox.height); setLeft(newBox.left); setTop(newBox.top); } } }, [element, updateMethod]); // Souce: https://css-tricks.com/using-requestanimationframe-with-react-hooks/ // Use useRef for mutable variables that we want to persist // without triggering a re-render on their change var requestRef = useRef(); var previousUpdateTimeRef = useRef(); var animate = useCallback(function (time) { if (previousUpdateTimeRef.current !== undefined) { var timeSinceLastUpdate = time - previousUpdateTimeRef.current; if (timeSinceLastUpdate > POSITION_UPDATE_INTERVAL) { var newBox = getElementRect(element); setWidth(newBox.width); setHeight(newBox.height); setLeft(newBox.left); setTop(newBox.top); previousUpdateTimeRef.current = time; } } else { // Initialize previousUpdateTimeRef previousUpdateTimeRef.current = time; } requestRef.current = requestAnimationFrame(animate); }, [element]); useEffect(function () { if (updateMethod === 'polling') { requestRef.current = requestAnimationFrame(animate); } return function () { if (requestRef.current !== undefined) { cancelAnimationFrame(requestRef.current); } }; // This useEffect should only run on mount and when `element` or `updateMethod` changes. }, [animate, element, updateMethod]); var box = useMemo(function () { return { width: width, height: height, left: left, top: top }; }, [width, height, left, top]); return box; }; /** * Will listen to the document resizing to see if an element has moved positions. * Not using ResizeObserver because of IE11 support. * @param element HTMLElement to watch when resizing. */ var useElementBox = function useElementBox(element, resizeUpdateMethod) { var updateMethod = resizeUpdateMethod || 'resizeListener'; var boxViaResizeListener = useResizeAwareElementBox(element, updateMethod); var boxViaPolling = usePollingElementBox(element, updateMethod); return updateMethod === 'resizeListener' ? boxViaResizeListener : boxViaPolling; }; /** * __Element box__ * * Allows consumption of `userElementBox` hook through render props. * * @internal */ export var ElementBox = function ElementBox(props) { var box = useElementBox(props.element, props.resizeUpdateMethod); return props.children(box); };