UNPKG

@intility/bifrost-react

Version:

React library for Intility's design system, Bifrost.

134 lines (133 loc) 5.67 kB
"use client"; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { createRef, useCallback, useEffect, useRef, useState } from "react"; import { TransitionGroup, CSSTransition } from "react-transition-group"; import classNames from "classnames"; import Message from "../Message/Message.js"; import FloatingMessageContext from "./FloatingMessageContext.internal.js"; import useAnimationDuration from "../../hooks/useAnimationDuration.js"; import useFloatingMessage from "../../hooks/useFloatingMessage.js"; /** * Provider for stacking floating notifications (aka toast or snackbar) via `useFloatingMessage()` hook */ export default function FloatingMessage({ children, max = 3, timeout = 7000, top = false, left = false }) { const [messages, setMessages] = useState([]); const removeMessageByReference = (message)=>{ // this might get called for messages that are already removed, // but it shouldn't matter as long as we compare by reference setMessages((messages)=>messages.filter((m)=>m !== message)); }; const showFloatingMessage = useCallback((message, options)=>{ const floatingMessage = { id: performance.now(), message, options }; setMessages((messages)=>max === 1 ? [ floatingMessage ] : [ ...messages.slice((max - 1) * -1), floatingMessage ]); if (timeout) { setTimeout(()=>removeMessageByReference(floatingMessage), timeout); } }, [ timeout, setMessages ]); return /*#__PURE__*/ _jsxs(FloatingMessageContext.Provider, { value: { showFloatingMessage, _stack: { top, left, timeout, messages, setMessages, removeMessageByReference } }, children: [ children, /*#__PURE__*/ _jsx(FloatingMessageStack, {}) ] }); } export function FloatingMessageStack() { const stackRef = useRef(null); const animationDuration = useAnimationDuration(); const { showFloatingMessage, _stack } = useFloatingMessage(); if (!_stack) { throw new Error("Cannot instantiate <FloatingMessageStack> outside of <FloatingMessage> context"); } const { top, left, timeout, messages, setMessages, removeMessageByReference } = _stack; const isVisible = messages.length > 0; useEffect(()=>{ if (!stackRef.current) return; const stackElement = stackRef.current; if (!isVisible && stackElement.matches(":popover-open")) { // avoid hiding the popover until exit animation is completed const closeTimer = setTimeout(()=>stackElement.hidePopover?.(), animationDuration); return ()=>clearTimeout(closeTimer); } if (isVisible && !stackElement.matches(":popover-open")) { stackElement.showPopover?.(); } }, [ isVisible ]); const handleMouseEnter = (message)=>{ setMessages((messages)=>{ // message might be hovered as it is fading out and has already been removed. do nothing if (!messages.includes(message)) return messages; // replace existing message with a new message object // this will stop the removal since the object reference is no longer the same return messages.map((m)=>m === message ? { ...message } : m); }); }; const handleMouseLeave = (message)=>{ // re-schedule removal of the message if (timeout) setTimeout(()=>removeMessageByReference(message), timeout); }; return /*#__PURE__*/ _jsx("div", { className: classNames("bf-floatingmessage-stack", { "bf-floatingmessage-stack-top": top, "bf-floatingmessage-stack-left": left }), popover: "manual", ref: stackRef, children: /*#__PURE__*/ _jsx(TransitionGroup, { component: null, children: messages.map((message)=>{ const transitionNodeRef = /*#__PURE__*/ createRef(); return /*#__PURE__*/ _jsx(CSSTransition, { timeout: animationDuration, classNames: "bf-floatingmessage", nodeRef: transitionNodeRef, children: /*#__PURE__*/ _jsx("div", { ref: transitionNodeRef, className: "bf-stack-element", children: /*#__PURE__*/ _jsx("div", { children: /*#__PURE__*/ _jsx(FloatingMessageContext.Provider, { value: { showFloatingMessage, removeFloatingMessage: ()=>removeMessageByReference(message) }, children: /*#__PURE__*/ _jsx(Message, { "aria-live": "polite", onClose: ()=>removeMessageByReference(message), onMouseEnter: ()=>handleMouseEnter(message), onMouseLeave: ()=>handleMouseLeave(message), ...message.options, header: message.message }) }) }) }) }, message.id); }) }) }); }