@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
JavaScript
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);
};