@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
JavaScript
;
'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";