botframework-webchat-component
Version:
React component of botframework-webchat
160 lines (139 loc) • 5.11 kB
JavaScript
/* eslint no-magic-numbers: ["error", { "ignore": [0, 1, 2, 3, 4, 5] }] */
/* eslint react/forbid-dom-props: "off" */
import { hooks } from 'botframework-webchat-api';
import classNames from 'classnames';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import CollapseIcon from './Toast/CollapseIcon';
import ExpandIcon from './Toast/ExpandIcon';
import NotificationIcon from './Toast/NotificationIcon';
import randomId from './Utils/randomId';
import useStyleSet from './hooks/useStyleSet';
import useStyleToEmotionObject from './hooks/internal/useStyleToEmotionObject';
const { useDebouncedNotifications, useLocalizer, useRenderToast } = hooks;
const ROOT_STYLE = {
display: 'flex',
flexDirection: 'column',
'& .webchat__toaster__header': {
display: 'flex',
flexShrink: 0
},
'& .webchat__toaster__expandText': {
flex: 1
},
'& .webchat__toaster__list': {
display: 'block',
listStyleType: 'none'
}
};
const LEVEL_AS_NUMBER = {
error: 1,
warn: 2,
info: 3,
success: 4
};
function getLevelAsNumber(level) {
return LEVEL_AS_NUMBER[level] || 5;
}
function compareLevel(x, y) {
return getLevelAsNumber(x) - getLevelAsNumber(y);
}
function sortNotifications(map) {
return (
Object.values(map)
// We want the order reversed, most recent on top.
.sort(({ timestamp: x }, { timestamp: y }) => y - x)
);
}
const PASSTHRU_FN = value => value;
const TOAST_ACCORDION_IDS = {
two: 'TOAST_ACCORDION_TWO',
few: 'TOAST_ACCORDION_FEW',
many: 'TOAST_ACCORDION_MANY',
other: 'TOAST_ACCORDION_OTHER'
};
const BasicToaster = () => {
const instanceId = useMemo(randomId, []);
const [{ toaster: toasterStyleSet }] = useStyleSet();
const [debouncedNotifications] = useDebouncedNotifications();
const [expanded, setExpanded] = useState(false);
const localizeWithPlural = useLocalizer({ plural: true });
const renderToast = useRenderToast();
const rootClassName = useStyleToEmotionObject()(ROOT_STYLE) + '';
const handleToggleExpand = useCallback(() => setExpanded(!expanded), [expanded, setExpanded]);
const sortedNotifications = useMemo(() => sortNotifications(debouncedNotifications), [debouncedNotifications]);
const sortedNotificationsWithChildren = useMemo(
() =>
sortedNotifications
.map(notification => {
const children = renderToast({ notification });
return children && { children, notification };
})
.filter(PASSTHRU_FN),
[renderToast, sortedNotifications]
);
const expandable = sortedNotificationsWithChildren.length > 1;
const [highestLevel] = sortedNotificationsWithChildren.map(({ notification: { level } }) => level).sort(compareLevel);
const expandableElementId = useMemo(
() => (!expandable || expanded ? `webchat__toaster__list__${instanceId}` : undefined),
[expandable, expanded, instanceId]
);
const headerElementId = useMemo(() => (expandable ? `webchat__toaster__header__${instanceId}` : undefined), [
expandable,
instanceId
]);
useEffect(() => {
!expandable && setExpanded(false);
}, [expandable]);
return (
<div
aria-labelledby={headerElementId}
aria-live="polite"
aria-relevant="all"
className={classNames(
'webchat__toaster',
{
'webchat__toaster--expandable': expandable,
'webchat__toaster--expanded': expanded,
'webchat__toaster--error': highestLevel === 'error',
'webchat__toaster--info': highestLevel === 'info',
'webchat__toaster--success': highestLevel === 'success',
'webchat__toaster--warn': highestLevel === 'warn'
},
rootClassName,
toasterStyleSet + ''
)}
role="log"
>
{expandable && (
<button
aria-controls={expandableElementId}
aria-expanded={expanded}
className="webchat__toaster__header"
id={headerElementId}
onClick={handleToggleExpand}
type="button"
>
<div aria-hidden={true} className="webchat__toaster__expandLevelIconBox">
<NotificationIcon className="webchat__toaster__expandLevelIcon" level={highestLevel} />
</div>
<div className="webchat__toaster__expandText">
{localizeWithPlural(TOAST_ACCORDION_IDS, sortedNotificationsWithChildren.length)}
</div>
<div aria-hidden={true} className="webchat__toaster__expandIcon">
<div className="webchat__toaster__expandIconFocus">{expanded ? <CollapseIcon /> : <ExpandIcon />}</div>
</div>
</button>
)}
{(!expandable || expanded) && (
<ul aria-labelledby={headerElementId} className="webchat__toaster__list" id={expandableElementId} role="region">
{sortedNotificationsWithChildren.map(({ children, notification: { id } }) => (
<li aria-atomic={true} className="webchat__toaster__listItem" key={id} role="none">
{children}
</li>
))}
</ul>
)}
</div>
);
};
export default BasicToaster;