@intility/bifrost-react
Version:
React library for Intility's design system, Bifrost.
134 lines (133 loc) • 5.67 kB
JavaScript
"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);
})
})
});
}