UNPKG

@storybook/addon-ondevice-controls

Version:

Display storybook controls on your device.

223 lines (218 loc) 9.89 kB
"use strict"; // taken from https://github.com/xnimorz/use-debounce/blob/master/src/useDebouncedCallback.ts /* MIT License Copyright (c) 2018 Nikita Mostovoy Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.default = useDebouncedCallback; const react_1 = require("react"); /** * Creates a debounced function that delays invoking `func` until after `wait` * milliseconds have elapsed since the last time the debounced function was * invoked, or until the next browser frame is drawn. The debounced function * comes with a `cancel` method to cancel delayed `func` invocations and a * `flush` method to immediately invoke them. Provide `options` to indicate * whether `func` should be invoked on the leading and/or trailing edge of the * `wait` timeout. The `func` is invoked with the last arguments provided to the * debounced function. Subsequent calls to the debounced function return the * result of the last `func` invocation. * * **Note:** If `leading` and `trailing` options are `true`, `func` is * invoked on the trailing edge of the timeout only if the debounced function * is invoked more than once during the `wait` timeout. * * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred * until the next tick, similar to `setTimeout` with a timeout of `0`. * * If `wait` is omitted in an environment with `requestAnimationFrame`, `func` * invocation will be deferred until the next frame is drawn (typically about * 16ms). * * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) * for details over the differences between `debounce` and `throttle`. * * @category Function * @param {Function} func The function to debounce. * @param {number} [wait=0] * The number of milliseconds to delay; if omitted, `requestAnimationFrame` is * used (if available, otherwise it will be setTimeout(...,0)). * @param {Object} [options={}] The options object. * Specify invoking on the leading edge of the timeout. * @param {boolean} [options.leading=false] * The maximum time `func` is allowed to be delayed before it's invoked. * @param {number} [options.maxWait] * Specify invoking on the trailing edge of the timeout. * @param {boolean} [options.trailing=true] * @returns {Function} Returns the new debounced function. * @example * * // Avoid costly calculations while the window size is in flux. * const resizeHandler = useDebouncedCallback(calculateLayout, 150); * window.addEventListener('resize', resizeHandler) * * // Invoke `sendMail` when clicked, debouncing subsequent calls. * const clickHandler = useDebouncedCallback(sendMail, 300, { * leading: true, * trailing: false, * }) * <button onClick={clickHandler}>click me</button> * * // Ensure `batchLog` is invoked once after 1 second of debounced calls. * const debounced = useDebouncedCallback(batchLog, 250, { 'maxWait': 1000 }) * const source = new EventSource('/stream') * source.addEventListener('message', debounced) * * // Cancel the trailing debounced invocation. * window.addEventListener('popstate', debounced.cancel) * * // Check for pending invocations. * const status = debounced.pending() ? "Pending..." : "Ready" */ function useDebouncedCallback(callback, wait, options) { const lastCallTime = (0, react_1.useRef)(null); const lastInvokeTime = (0, react_1.useRef)(0); const timerId = (0, react_1.useRef)(null); const lastArgs = (0, react_1.useRef)([]); const lastThis = (0, react_1.useRef)(undefined); const result = (0, react_1.useRef)(undefined); const funcRef = (0, react_1.useRef)(callback); const mounted = (0, react_1.useRef)(true); funcRef.current = callback; if (typeof callback !== 'function') { throw new TypeError('Expected a function'); } wait = +wait || 0; options = options || {}; const leading = !!options.leading; const trailing = 'trailing' in options ? !!options.trailing : true; // `true` by default const maxing = 'maxWait' in options; const maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : null; (0, react_1.useEffect)(() => { mounted.current = true; return () => { mounted.current = false; }; }, []); // You may have a question, why we have so many code under the useMemo definition. // // This was made as we want to escape from useCallback hell and // not to initialize a number of functions each time useDebouncedCallback is called. // // It means that we have less garbage for our GC calls which improves performance. // Also, it makes this library smaller. // // And the last reason, that the code without lots of useCallback with deps is easier to read. // You have only one place for that. const debounced = (0, react_1.useMemo)(() => { const invokeFunc = (time) => { const args = lastArgs.current; const thisArg = lastThis.current; lastArgs.current = lastThis.current = null; lastInvokeTime.current = time; return (result.current = funcRef.current.apply(thisArg, args)); }; const startTimer = (pendingFunc, timerWait) => { timerId.current = setTimeout(pendingFunc, timerWait); }; const shouldInvoke = (time) => { if (!mounted.current) { return false; } const timeSinceLastCall = time - lastCallTime.current; const timeSinceLastInvoke = time - lastInvokeTime.current; // Either this is the first call, activity has stopped and we're at the // trailing edge, the system time has gone backwards and we're treating // it as the trailing edge, or we've hit the `maxWait` limit. return (!lastCallTime.current || timeSinceLastCall >= wait || timeSinceLastCall < 0 || (maxing && timeSinceLastInvoke >= maxWait)); }; const trailingEdge = (time) => { timerId.current = null; // Only invoke if we have `lastArgs` which means `func` has been // debounced at least once. if (trailing && lastArgs.current) { return invokeFunc(time); } lastArgs.current = lastThis.current = null; return result.current; }; const timerExpired = () => { const time = Date.now(); if (shouldInvoke(time)) { return trailingEdge(time); } // https://github.com/xnimorz/use-debounce/issues/97 if (!mounted.current) { return; } // Remaining wait calculation const timeSinceLastCall = time - lastCallTime.current; const timeSinceLastInvoke = time - lastInvokeTime.current; const timeWaiting = wait - timeSinceLastCall; const remainingWait = maxing ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting; // Restart the timer startTimer(timerExpired, remainingWait); }; const func = (...args) => { const time = Date.now(); const isInvoking = shouldInvoke(time); lastArgs.current = args; lastThis.current = this; lastCallTime.current = time; if (isInvoking) { if (!timerId.current && mounted.current) { // Reset any `maxWait` timer. lastInvokeTime.current = lastCallTime.current; // Start the timer for the trailing edge. startTimer(timerExpired, wait); // Invoke the leading edge. return leading ? invokeFunc(lastCallTime.current) : result.current; } if (maxing) { // Handle invocations in a tight loop. startTimer(timerExpired, wait); return invokeFunc(lastCallTime.current); } } if (!timerId.current) { startTimer(timerExpired, wait); } return result.current; }; func.cancel = () => { if (timerId.current) { clearTimeout(timerId.current); } lastInvokeTime.current = 0; lastArgs.current = lastCallTime.current = lastThis.current = timerId.current = null; }; func.isPending = () => { return !!timerId.current; }; func.flush = () => { return !timerId.current ? result.current : trailingEdge(Date.now()); }; return func; }, [leading, maxing, wait, maxWait, trailing]); return debounced; }