'use client'; import * as React from 'react'; import { getIntrinsicElementProps, useMergedRefs, useEventCallback, useId, slot } from '@fluentui/react-utilities'; import { useFluent_unstable } from '@fluentui/react-shared-contexts'; import { Delete, Tab } from '@fluentui/keyboard-keys'; import { useFocusableGroup, useFocusFinders } from '@fluentui/react-tabster'; import { Timer } from '../Timer/Timer'; const intentPolitenessMap = { success: 'assertive', warning: 'assertive', error: 'assertive', info: 'polite' }; /** * Create the state required to render ToastContainer. * * The returned state can be modified with hooks such as useToastContainerStyles_unstable, * before being passed to renderToastContainer_unstable. * * @param props - props from this instance of ToastContainer * @param ref - reference to root HTMLElement of ToastContainer */ export const useToastContainer_unstable = (props, ref)=>{ const { visible, children, close: closeProp, remove, updateId, announce, data, timeout: timerTimeout, politeness: desiredPoliteness, intent = 'info', pauseOnHover, pauseOnWindowBlur, imperativeRef, tryRestoreFocus, content: _content, ...rest } = props; const titleId = useId('toast-title'); const bodyId = useId('toast-body'); const toastRef = React.useRef(null); const { targetDocument } = useFluent_unstable(); const [running, setRunning] = React.useState(false); const imperativePauseRef = React.useRef(false); const focusedToastBeforeClose = React.useRef(false); const focusableGroupAttribute = useFocusableGroup({ tabBehavior: 'limited-trap-focus', // Users should only use Tab to focus into the toast // Escape is already reserved to dismiss all toasts ignoreDefaultKeydown: { Tab: true, Escape: true, Enter: true } }); const close = useEventCallback(()=>{ var _toastRef_current; const activeElement = targetDocument === null || targetDocument === void 0 ? void 0 : targetDocument.activeElement; if (activeElement && ((_toastRef_current = toastRef.current) === null || _toastRef_current === void 0 ? void 0 : _toastRef_current.contains(activeElement))) { focusedToastBeforeClose.current = true; } closeProp(); }); const onStatusChange = useEventCallback((status)=>{ var _props_onStatusChange; return (_props_onStatusChange = props.onStatusChange) === null || _props_onStatusChange === void 0 ? void 0 : _props_onStatusChange.call(props, null, { status, ...props }); }); const pause = useEventCallback(()=>setRunning(false)); const play = useEventCallback(()=>{ var _toastRef_current; if (imperativePauseRef.current) { return; } var _targetDocument_activeElement; const containsActive = !!((_toastRef_current = toastRef.current) === null || _toastRef_current === void 0 ? void 0 : _toastRef_current.contains((_targetDocument_activeElement = targetDocument === null || targetDocument === void 0 ? void 0 : targetDocument.activeElement) !== null && _targetDocument_activeElement !== void 0 ? _targetDocument_activeElement : null)); if (timerTimeout < 0) { setRunning(true); return; } if (!containsActive) { setRunning(true); } }); React.useImperativeHandle(imperativeRef, ()=>({ focus: ()=>{ if (!toastRef.current) { return; } toastRef.current.focus(); }, play: ()=>{ imperativePauseRef.current = false; play(); }, pause: ()=>{ imperativePauseRef.current = true; pause(); } })); React.useEffect(()=>{ return ()=>onStatusChange('unmounted'); }, [ onStatusChange ]); React.useEffect(()=>{ if (!targetDocument) { return; } if (pauseOnWindowBlur) { var _targetDocument_defaultView, _targetDocument_defaultView1; (_targetDocument_defaultView = targetDocument.defaultView) === null || _targetDocument_defaultView === void 0 ? void 0 : _targetDocument_defaultView.addEventListener('focus', play); (_targetDocument_defaultView1 = targetDocument.defaultView) === null || _targetDocument_defaultView1 === void 0 ? void 0 : _targetDocument_defaultView1.addEventListener('blur', pause); return ()=>{ var _targetDocument_defaultView, _targetDocument_defaultView1; (_targetDocument_defaultView = targetDocument.defaultView) === null || _targetDocument_defaultView === void 0 ? void 0 : _targetDocument_defaultView.removeEventListener('focus', play); (_targetDocument_defaultView1 = targetDocument.defaultView) === null || _targetDocument_defaultView1 === void 0 ? void 0 : _targetDocument_defaultView1.removeEventListener('blur', pause); }; } }, [ targetDocument, pause, play, pauseOnWindowBlur ]); // Users never actually use ToastContainer as a JSX but imperatively through useToastContainerController const userRootSlot = data.root; const onMotionFinish = React.useCallback((_, { direction })=>{ if (direction === 'exit') { remove(); } if (direction === 'enter') { // start toast once it's fully animated in play(); onStatusChange('visible'); } }, [ onStatusChange, play, remove ]); const onMouseEnter = useEventCallback((e)=>{ var _userRootSlot_onMouseEnter; pause(); userRootSlot === null || userRootSlot === void 0 ? void 0 : (_userRootSlot_onMouseEnter = userRootSlot.onMouseEnter) === null || _userRootSlot_onMouseEnter === void 0 ? void 0 : _userRootSlot_onMouseEnter.call(userRootSlot, e); }); const onMouseLeave = useEventCallback((e)=>{ var _userRootSlot_onMouseEnter; play(); userRootSlot === null || userRootSlot === void 0 ? void 0 : (_userRootSlot_onMouseEnter = userRootSlot.onMouseEnter) === null || _userRootSlot_onMouseEnter === void 0 ? void 0 : _userRootSlot_onMouseEnter.call(userRootSlot, e); }); const { findFirstFocusable, findLastFocusable } = useFocusFinders(); const onKeyDown = useEventCallback((e)=>{ var _userRootSlot_onKeyDown; if (e.key === Delete) { e.preventDefault(); close(); } if (e.key === Tab && e.currentTarget === e.target) { e.preventDefault(); if (e.shiftKey) { var _findLastFocusable; (_findLastFocusable = findLastFocusable(e.currentTarget)) === null || _findLastFocusable === void 0 ? void 0 : _findLastFocusable.focus(); } else { var _findFirstFocusable; (_findFirstFocusable = findFirstFocusable(e.currentTarget)) === null || _findFirstFocusable === void 0 ? void 0 : _findFirstFocusable.focus(); } } userRootSlot === null || userRootSlot === void 0 ? void 0 : (_userRootSlot_onKeyDown = userRootSlot.onKeyDown) === null || _userRootSlot_onKeyDown === void 0 ? void 0 : _userRootSlot_onKeyDown.call(userRootSlot, e); }); React.useEffect(()=>{ var _toastRef_current; if (!visible) { return; } const politeness = desiredPoliteness !== null && desiredPoliteness !== void 0 ? desiredPoliteness : intentPolitenessMap[intent]; var _toastRef_current_textContent; announce((_toastRef_current_textContent = (_toastRef_current = toastRef.current) === null || _toastRef_current === void 0 ? void 0 : _toastRef_current.textContent) !== null && _toastRef_current_textContent !== void 0 ? _toastRef_current_textContent : '', { politeness }); }, [ announce, desiredPoliteness, toastRef, visible, updateId, intent ]); React.useEffect(()=>{ return ()=>{ if (focusedToastBeforeClose.current) { focusedToastBeforeClose.current = false; tryRestoreFocus(); } }; }, [ tryRestoreFocus ]); return { components: { timer: Timer, root: 'div' }, timer: slot.always({ onTimeout: close, running, timeout: timerTimeout !== null && timerTimeout !== void 0 ? timerTimeout : -1 }, { elementType: Timer }), root: slot.always(getIntrinsicElementProps('div', { // FIXME: // `ref` is wrongly assigned to be `HTMLElement` instead of `HTMLDivElement` // but since it would be a breaking change to fix it, we are casting ref to it's proper type ref: useMergedRefs(ref, toastRef), children, tabIndex: 0, role: 'listitem', 'aria-labelledby': titleId, 'aria-describedby': bodyId, ...rest, ...userRootSlot, ...focusableGroupAttribute, onMouseEnter, onMouseLeave, onKeyDown }), { elementType: 'div' }), timerTimeout, transitionTimeout: 0, running, visible, remove, close, onTransitionEntering: ()=>{ /* no-op */ }, updateId, nodeRef: toastRef, intent, titleId, bodyId, onMotionFinish }; };