Commit c32da40b authored by Mickaël Bourgier's avatar Mickaël Bourgier
Browse files

🚧 Add createNotificationCenter and AlertNotificationCenter

parent ed5eba10
Pipeline #1888 passed with stage
in 1 minute and 39 seconds
import React from 'react';
import Alert, {
AlertActions,
AlertContent,
AlertProps,
AlertTitle,
} from '../Alert';
import Button, { ButtonProps } from '../Button';
import createNotificationCenter, {
RenderCallbacks,
} from '../createNotificationCenter';
type TitleOrDescription = { title: string } | { description: string };
type Notification = TitleOrDescription & {
actionButtons?: Array<
React.ReactElement<
Omit<ButtonProps, 'onClick'> & {
onClick: (e: Event, callbacks: RenderCallbacks<Notification>) => void;
},
typeof Button
>
>;
alertProps?: Partial<AlertProps>;
closable?: boolean;
description?: string;
title?: string;
};
function renderNotification(
notification: Notification,
{ removeNotification, replaceNotification }: RenderCallbacks<Notification>
): React.ReactNode {
const {
actionButtons,
alertProps = {},
closable,
description,
title,
} = notification;
const onClose = alertProps.onClose;
if (closable || (closable !== false && typeof onClose !== 'undefined')) {
alertProps.onClose = (e): void => {
removeNotification();
if (typeof onClose !== 'undefined') {
onClose(e);
}
};
}
return (
<Alert {...alertProps} className='my-3'>
{title && <AlertTitle>{title}</AlertTitle>}
{description && <AlertContent>{description}</AlertContent>}
{actionButtons && (
<AlertActions>
{actionButtons.map((action) => {
function handleClick(e): void {
action.props.onClick(e, {
notification,
removeNotification,
replaceNotification,
});
}
return React.cloneElement(action, { onClick: handleClick });
})}
</AlertActions>
)}
</Alert>
);
}
const {
NotificationCenterProvider,
NotificationCenter,
useNotifier,
} = createNotificationCenter(renderNotification);
export { NotificationCenterProvider, useNotifier };
export default NotificationCenter;
export {
default,
NotificationCenterProvider,
useNotifier,
} from './AlertNotificationCenter';
import React, { useContext, useRef, useState } from 'react';
const DEFAULT_NOTIFICATION_DELAY = 0;
const DEFAULT_NOTIFICATION_DURATION = 5000;
interface NotificationOptions {
delay?: number;
duration?: number;
}
interface UseNotifierReturn<N> {
addNotification: (notification: N, options?: NotificationOptions) => void;
removeNotification: (id: number) => void;
replaceNotification: (
id: number,
notification: N,
options?: NotificationOptions
) => void;
}
export interface RenderCallbacks<N> {
notification: N;
removeNotification: () => void;
replaceNotification: (notification: N, options?: NotificationOptions) => void;
}
interface NotificationWithMetadata<N> {
id: number;
notification: N;
createdAt: Date;
updatedAt: Date;
delay: number;
duration: number;
}
interface ContextValue<N> {
notifications: Array<NotificationWithMetadata<N>>;
setNotifications: React.Dispatch<
React.SetStateAction<Array<NotificationWithMetadata<N>>>
>;
}
interface NotificationCenterProps<N> {
transitionComponent?: undefined;
// enteringPosition?: 'top' | 'left' | 'bottom' | 'right'; // Position d'apparition des nouvelles notificaions
sort?: (
a: NotificationWithMetadata<N>,
b: NotificationWithMetadata<N>
) => number;
}
interface CreateNotificationCenterReturn<N> {
NotificationCenterProvider: React.FC;
NotificationCenter: React.FC<NotificationCenterProps<N>>;
useNotifier: () => UseNotifierReturn<N>;
}
export function createNotificationSort<N>(options: {
sortBy: 'createdAt' | 'updatedAt';
keepFixedToStart: boolean;
reverse: boolean;
}) {
return (
a: NotificationWithMetadata<N>,
b: NotificationWithMetadata<N>
): number => {
let result = 0;
if (options.keepFixedToStart) {
if (a.duration < 0 && b.duration < 0) {
result = 0;
} else if (a.duration < 0) {
result = -1;
} else if (b.duration < 0) {
result = 1;
}
}
switch (options.sortBy) {
case 'createdAt':
result = a.createdAt.getTime() - b.createdAt.getTime();
break;
case 'updatedAt':
result = a.updatedAt.getTime() - b.updatedAt.getTime();
break;
}
return options.reverse ? -result : result;
};
}
const defaultNotificationSort = createNotificationSort({
sortBy: 'createdAt',
keepFixedToStart: true,
reverse: false,
});
export default function createNotificationCenter<N>(
render: (notification: N, callbacks: RenderCallbacks<N>) => React.ReactNode,
defaultNotificationOptions: NotificationOptions = {}
): CreateNotificationCenterReturn<N> {
const Context = React.createContext<ContextValue<N> | undefined>(undefined);
const NotificationCenterProvider: React.FC = ({ children }) => {
const [notifications, setNotifications] = useState<
Array<NotificationWithMetadata<N>>
>([]);
return (
<Context.Provider value={{ notifications, setNotifications }}>
{children}
</Context.Provider>
);
};
function useNotifier(): UseNotifierReturn<N> {
const context = useContext(Context);
const nextId = useRef(0);
const removeTimeoutIds = useRef({});
if (typeof context === 'undefined') {
// eslint-disable-next-line no-console
console.error(
'`useNotifier` cannot be used without a `NotificationCenterProvider` in ancestors.'
);
/* eslint-disable @typescript-eslint/no-empty-function */
return {
addNotification: (): void => {},
removeNotification: (): void => {},
replaceNotification: (): void => {},
};
/* eslint-enable @typescript-eslint/no-empty-function */
}
const { setNotifications } = context;
function removeNotification(id: number): void {
setNotifications((notifications) =>
notifications.filter((n) => n.id !== id)
);
}
function addNotification(
notification: N,
options: NotificationOptions = {}
): void {
const {
delay = DEFAULT_NOTIFICATION_DELAY,
duration = DEFAULT_NOTIFICATION_DURATION,
} = { ...defaultNotificationOptions, ...options };
function doAdd(): void {
const id = nextId.current++;
const createdAt = new Date();
setNotifications((notifications) => [
...notifications,
{
id,
notification,
createdAt,
updatedAt: createdAt,
delay,
duration,
},
]);
if (duration >= 0) {
removeTimeoutIds.current[id] = setTimeout(
() => removeNotification(id),
duration
);
}
}
setTimeout(doAdd, delay);
}
function replaceNotification(
id: number,
notification: N,
options: Omit<NotificationOptions, 'delay'> = {}
): void {
setNotifications((notifications) =>
notifications.map((notificationWithMetadata) => {
if (notificationWithMetadata.id === id) {
return {
...notificationWithMetadata,
notification,
updatedAt: new Date(),
...(typeof options.duration !== 'undefined'
? { duration: options.duration }
: {}),
};
}
return notificationWithMetadata;
})
);
if (typeof options.duration !== 'undefined') {
if (removeTimeoutIds.current[id]) {
clearTimeout(removeTimeoutIds.current[id]);
}
if (options.duration >= 0) {
removeTimeoutIds.current[id] = setTimeout(
() => removeNotification(id),
options.duration
);
}
}
}
return {
addNotification,
removeNotification,
replaceNotification,
};
}
const NotificationCenter: React.FC<NotificationCenterProps<N>> = (props) => {
const { sort = defaultNotificationSort } = props;
const context = useContext(Context);
const { removeNotification, replaceNotification } = useNotifier();
if (typeof context === 'undefined') {
// eslint-disable-next-line no-console
console.error(
'`NotificationCenter` cannot be used without a `NotificationCenterProvider` in ancestors.'
);
return null;
}
const { notifications } = context;
return (
<div>
{notifications.sort(sort).map(({ id, notification }) => {
const renderCallbacks = {
notification,
removeNotification: (): void => removeNotification(id),
replaceNotification: (
notification: N,
options?: NotificationOptions
): void => replaceNotification(id, notification, options),
};
return (
<React.Fragment key={id}>
{render(notification, renderCallbacks)}
</React.Fragment>
);
})}
</div>
);
};
return {
NotificationCenterProvider,
NotificationCenter,
useNotifier,
};
}
export {
default,
createNotificationSort,
RenderCallbacks,
} from './createNotificationCenter';
......@@ -6,6 +6,11 @@ export {
AlertTitle,
AlertProps,
} from './Alert';
export {
default as AlertNotificationCenter,
NotificationCenterProvider as AlertNotificationCenterProvider,
useNotifier as useAlertNotifier,
} from './AlertNotificationCenter';
export { default as AppBar } from './AppBar';
export { default as Box } from './Box';
export { default as Button } from './Button';
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment