@hypothesis/frontend-shared
Version:
Shared components, styles and utilities for Hypothesis projects
155 lines • 5.47 kB
JavaScript
var _jsxFileName = "/home/runner/work/frontend-shared/frontend-shared/src/components/feedback/ToastMessages.tsx";
import classnames from 'classnames';
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'preact/hooks';
import Callout from './Callout';
import { jsxDEV as _jsxDEV } from "preact/jsx-dev-runtime";
/**
* An individual toast message: a brief and transient success or error message.
* The message may be dismissed by clicking on it. `visuallyHidden` toast
* messages will not be visible but are still available to screen readers.
*/
function ToastMessageItem({
message,
onDismiss
}) {
return _jsxDEV(Callout, {
classes: classnames({
'sr-only': message.visuallyHidden
}),
status: message.type,
onClick: () => onDismiss(message.id),
variant: "raised",
children: message.message
}, void 0, false, {
fileName: _jsxFileName,
lineNumber: 55,
columnNumber: 5
}, this);
}
const ToastMessageTransition = ({
direction,
onTransitionEnd,
children,
transitionClasses = {}
}) => {
const isDismissed = direction === 'out';
const containerRef = useRef(null);
const handleAnimation = e => {
// Ignore animations happening on child elements
if (e.target !== containerRef.current) {
return;
}
onTransitionEnd === null || onTransitionEnd === void 0 || onTransitionEnd(direction !== null && direction !== void 0 ? direction : 'in');
};
const classes = useMemo(() => {
const {
transitionIn = 'animate-fade-in',
transitionOut = 'animate-fade-out'
} = transitionClasses;
return {
[transitionIn]: !isDismissed,
[transitionOut]: isDismissed
};
}, [isDismissed, transitionClasses]);
return _jsxDEV("div", {
"data-testid": "animation-container",
onAnimationEnd: handleAnimation,
ref: containerRef,
className: classnames('relative w-full container', classes),
children: children
}, void 0, false, {
fileName: _jsxFileName,
lineNumber: 103,
columnNumber: 5
}, this);
};
/**
* A collection of toast messages. These are rendered within an `aria-live`
* region for accessibility with screen readers.
*/
export default function ToastMessages({
messages,
onMessageDismiss,
transitionClasses,
/* istanbul ignore next - test seam */
setTimeout_ = setTimeout
}) {
// List of IDs of toast messages that have been dismissed and have an
// in-progress 'out' transition
const [dismissedMessages, setDismissedMessages] = useState([]);
// Tracks not finished timeouts for auto-dismiss toast messages
const messageSchedules = useRef(new Map());
const dismissMessage = useCallback(id => setDismissedMessages(ids => [...ids, id]), []);
const scheduleMessageDismiss = useCallback(id => {
const timeout = setTimeout_(() => {
dismissMessage(id);
messageSchedules.current.delete(id);
}, 5000);
messageSchedules.current.set(id, timeout);
}, [dismissMessage, setTimeout_]);
const onTransitionEnd = useCallback((direction, message) => {
var _message$autoDismiss;
const autoDismiss = (_message$autoDismiss = message.autoDismiss) !== null && _message$autoDismiss !== void 0 ? _message$autoDismiss : true;
if (direction === 'in' && autoDismiss) {
scheduleMessageDismiss(message.id);
}
if (direction === 'out') {
onMessageDismiss(message.id);
setDismissedMessages(ids => ids.filter(id => id !== message.id));
}
}, [scheduleMessageDismiss, onMessageDismiss]);
useLayoutEffect(() => {
// Clear all pending timeouts for not yet dismissed toast messages when the
// component is unmounted
const pendingTimeouts = messageSchedules.current;
return () => {
pendingTimeouts.forEach(timeout => clearTimeout(timeout));
};
}, []);
return _jsxDEV("ul", {
"aria-live": "polite",
"aria-relevant": "additions",
className: "w-full space-y-2",
"data-component": "ToastMessages",
children: messages.map(message => {
const isDismissed = dismissedMessages.includes(message.id);
return _jsxDEV("li", {
className: classnames({
// Add a bottom margin to visible messages only. Typically, we'd
// use a `space-y-2` class on the parent to space children.
// Doing that here could cause an undesired top margin on
// the first visible message in a list that contains (only)
// visually-hidden messages before it.
// See https://tailwindcss.com/docs/space#limitations
'mb-2': !message.visuallyHidden
}),
children: _jsxDEV(ToastMessageTransition, {
direction: isDismissed ? 'out' : 'in',
onTransitionEnd: direction => onTransitionEnd(direction, message),
transitionClasses: transitionClasses,
children: _jsxDEV(ToastMessageItem, {
message: message,
onDismiss: dismissMessage
}, void 0, false, {
fileName: _jsxFileName,
lineNumber: 206,
columnNumber: 15
}, this)
}, void 0, false, {
fileName: _jsxFileName,
lineNumber: 201,
columnNumber: 13
}, this)
}, message.id, false, {
fileName: _jsxFileName,
lineNumber: 189,
columnNumber: 11
}, this);
})
}, void 0, false, {
fileName: _jsxFileName,
lineNumber: 180,
columnNumber: 5
}, this);
}
//# sourceMappingURL=ToastMessages.js.map