UNPKG

@base-ui-components/react

Version:

Base UI is a library of headless ('unstyled') React components and low-level hooks. You gain complete control over your app's CSS and accessibility features.

301 lines (296 loc) 9.93 kB
"use strict"; 'use client'; var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default; Object.defineProperty(exports, "__esModule", { value: true }); exports.ToastProvider = void 0; var React = _interopRequireWildcard(require("react")); var _owner = require("@base-ui-components/utils/owner"); var _useStableCallback = require("@base-ui-components/utils/useStableCallback"); var _generateId = require("@base-ui-components/utils/generateId"); var _useTimeout = require("@base-ui-components/utils/useTimeout"); var _utils = require("../../floating-ui-react/utils"); var _ToastProviderContext = require("./ToastProviderContext"); var _focusVisible = require("../utils/focusVisible"); var _resolvePromiseOptions = require("../utils/resolvePromiseOptions"); var _jsxRuntime = require("react/jsx-runtime"); /** * Provides a context for creating and managing toasts. * * Documentation: [Base UI Toast](https://base-ui.com/react/components/toast) */ const ToastProvider = exports.ToastProvider = function ToastProvider(props) { const { children, timeout = 5000, limit = 3, toastManager } = props; const [toasts, setToasts] = React.useState([]); const [hovering, setHovering] = React.useState(false); const [focused, setFocused] = React.useState(false); const [prevFocusElement, setPrevFocusElement] = React.useState(null); if (toasts.length === 0) { if (hovering) { setHovering(false); } if (focused) { setFocused(false); } } const expanded = hovering || focused; const timersRef = React.useRef(new Map()); const viewportRef = React.useRef(null); const windowFocusedRef = React.useRef(true); const isPausedRef = React.useRef(false); function handleFocusManagement(toastId) { const activeEl = (0, _utils.activeElement)((0, _owner.ownerDocument)(viewportRef.current)); if (!viewportRef.current || !(0, _utils.contains)(viewportRef.current, activeEl) || !(0, _focusVisible.isFocusVisible)(activeEl)) { return; } const currentIndex = toasts.findIndex(toast => toast.id === toastId); let nextToast = null; // Try to find the next toast that isn't animating out let index = currentIndex + 1; while (index < toasts.length) { if (toasts[index].transitionStatus !== 'ending') { nextToast = toasts[index]; break; } index += 1; } // Go backwards if no next toast is found if (!nextToast) { index = currentIndex - 1; while (index >= 0) { if (toasts[index].transitionStatus !== 'ending') { nextToast = toasts[index]; break; } index -= 1; } } if (nextToast) { nextToast.ref?.current?.focus(); } else { prevFocusElement?.focus({ preventScroll: true }); } } const pauseTimers = (0, _useStableCallback.useStableCallback)(() => { if (isPausedRef.current) { return; } isPausedRef.current = true; timersRef.current.forEach(timer => { if (timer.timeout) { timer.timeout.clear(); const elapsed = Date.now() - timer.start; const remaining = timer.delay - elapsed; timer.remaining = remaining > 0 ? remaining : 0; } }); }); const resumeTimers = (0, _useStableCallback.useStableCallback)(() => { if (!isPausedRef.current) { return; } isPausedRef.current = false; timersRef.current.forEach((timer, id) => { timer.remaining = timer.remaining > 0 ? timer.remaining : timer.delay; timer.timeout ??= _useTimeout.Timeout.create(); timer.timeout.start(timer.remaining, () => { timersRef.current.delete(id); timer.callback(); }); timer.start = Date.now(); }); }); const close = (0, _useStableCallback.useStableCallback)(toastId => { setToasts(prevToasts => { const toastsWithEnding = prevToasts.map(toast => toast.id === toastId ? { ...toast, transitionStatus: 'ending', height: 0 } : toast); const activeToasts = toastsWithEnding.filter(t => t.transitionStatus !== 'ending'); return toastsWithEnding.map(toast => { if (toast.transitionStatus === 'ending') { return toast; } const isActiveToastLimited = activeToasts.indexOf(toast) >= limit; return { ...toast, limited: isActiveToastLimited }; }); }); const timer = timersRef.current.get(toastId); if (timer && timer.timeout) { timer.timeout.clear(); timersRef.current.delete(toastId); } const toast = toasts.find(t => t.id === toastId); toast?.onClose?.(); handleFocusManagement(toastId); if (toasts.length === 1) { setHovering(false); setFocused(false); } }); const remove = (0, _useStableCallback.useStableCallback)(toastId => { setToasts(prev => prev.filter(toast => toast.id !== toastId)); const toast = toasts.find(t => t.id === toastId); toast?.onRemove?.(); }); const scheduleTimer = (0, _useStableCallback.useStableCallback)((id, delay, callback) => { const start = Date.now(); const shouldStartActive = windowFocusedRef.current && !hovering && !focused; const currentTimeout = shouldStartActive ? _useTimeout.Timeout.create() : undefined; currentTimeout?.start(delay, () => { timersRef.current.delete(id); callback(); }); timersRef.current.set(id, { timeout: currentTimeout, start: shouldStartActive ? start : 0, delay, remaining: delay, callback }); }); const add = (0, _useStableCallback.useStableCallback)(toast => { const id = toast.id || (0, _generateId.generateId)('toast'); const toastToAdd = { ...toast, id, transitionStatus: 'starting' }; setToasts(prev => { const updatedToasts = [toastToAdd, ...prev]; const activeToasts = updatedToasts.filter(t => t.transitionStatus !== 'ending'); // Mark oldest toasts for removal when over limit if (activeToasts.length > limit) { const excessCount = activeToasts.length - limit; const oldestActiveToasts = activeToasts.slice(-excessCount); return updatedToasts.map(t => oldestActiveToasts.some(old => old.id === t.id) ? { ...t, limited: true } : { ...t, limited: false }); } return updatedToasts.map(t => ({ ...t, limited: false })); }); const duration = toastToAdd.timeout ?? timeout; if (toastToAdd.type !== 'loading' && duration > 0) { scheduleTimer(id, duration, () => close(id)); } if (hovering || focused || !windowFocusedRef.current) { pauseTimers(); } return id; }); const update = (0, _useStableCallback.useStableCallback)((id, updates) => { setToasts(prev => prev.map(toast => toast.id === id ? { ...toast, ...updates } : toast)); }); const promise = (0, _useStableCallback.useStableCallback)((promiseValue, options) => { // Create a loading toast (which does not auto-dismiss). const loadingOptions = (0, _resolvePromiseOptions.resolvePromiseOptions)(options.loading); const id = add({ ...loadingOptions, type: 'loading' }); const handledPromise = promiseValue.then(result => { const successOptions = (0, _resolvePromiseOptions.resolvePromiseOptions)(options.success, result); update(id, { ...successOptions, type: 'success' }); const successTimeout = successOptions.timeout ?? timeout; if (successTimeout > 0) { scheduleTimer(id, successTimeout, () => close(id)); } if (hovering || focused || !windowFocusedRef.current) { pauseTimers(); } return result; }).catch(error => { const errorOptions = (0, _resolvePromiseOptions.resolvePromiseOptions)(options.error, error); update(id, { ...errorOptions, type: 'error' }); const errorTimeout = errorOptions.timeout ?? timeout; if (errorTimeout > 0) { scheduleTimer(id, errorTimeout, () => close(id)); } if (hovering || focused || !windowFocusedRef.current) { pauseTimers(); } return Promise.reject(error); }); // Private API used exclusively by `Manager` to handoff the promise // back to the manager after it's handled here. if ({}.hasOwnProperty.call(options, 'setPromise')) { options.setPromise(handledPromise); } return handledPromise; }); React.useEffect(function subscribeToToastManager() { if (!toastManager) { return undefined; } const unsubscribe = toastManager[' subscribe'](({ action, options }) => { const id = options.id; if (action === 'promise' && options.promise) { promise(options.promise, options); } else if (action === 'update' && id) { update(id, options); } else if (action === 'close' && id) { close(id); } else { add(options); } }); return unsubscribe; }, [add, update, scheduleTimer, timeout, toastManager, promise, close]); const contextValue = React.useMemo(() => ({ toasts, setToasts, hovering, setHovering, focused, setFocused, expanded, add, close, remove, update, promise, pauseTimers, resumeTimers, prevFocusElement, setPrevFocusElement, viewportRef, scheduleTimer, windowFocusedRef }), [add, close, focused, hovering, expanded, pauseTimers, prevFocusElement, promise, remove, resumeTimers, scheduleTimer, toasts, update]); return /*#__PURE__*/(0, _jsxRuntime.jsx)(_ToastProviderContext.ToastContext.Provider, { value: contextValue, children: children }); }; if (process.env.NODE_ENV !== "production") ToastProvider.displayName = "ToastProvider";