Skip to content

Commit

Permalink
[@mantine/notifications] Allow displaying notifications at any position
Browse files Browse the repository at this point in the history
  • Loading branch information
rtivital committed Jul 17, 2024
1 parent 3cd3dcc commit b2d1302
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 83 deletions.
1 change: 0 additions & 1 deletion .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ export const decorators = [
),
(renderStory: any) => (
<MantineProvider theme={theme}>
<Notifications zIndex={10000} />
<MantineEmotionProvider>{renderStory()}</MantineEmotionProvider>
</MantineProvider>
),
Expand Down
37 changes: 32 additions & 5 deletions packages/@mantine/notifications/src/Notifications.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,39 @@
width: calc(100% - var(--mantine-spacing-md) * 2);
position: fixed;
z-index: var(--notifications-z-index);
top: var(--notifications-top);
left: var(--notifications-left);
right: var(--notifications-right);
bottom: var(--notifications-bottom);
transform: var(--notifications-transform);
max-width: var(--notifications-container-width);

&:where([data-position='top-center']) {
top: var(--mantine-spacing-md);
left: 50%;
transform: translateX(-50%);
}

&:where([data-position='top-left']) {
top: var(--mantine-spacing-md);
left: var(--mantine-spacing-md);
}

&:where([data-position='top-right']) {
top: var(--mantine-spacing-md);
right: var(--mantine-spacing-md);
}

&:where([data-position='bottom-center']) {
bottom: var(--mantine-spacing-md);
left: 50%;
transform: translateX(-50%);
}

&:where([data-position='bottom-left']) {
bottom: var(--mantine-spacing-md);
left: var(--mantine-spacing-md);
}

&:where([data-position='bottom-right']) {
bottom: var(--mantine-spacing-md);
right: var(--mantine-spacing-md);
}
}

.notification {
Expand Down
25 changes: 22 additions & 3 deletions packages/@mantine/notifications/src/Notifications.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,34 @@ export default { title: 'Notifications' };
export function Usage() {
return (
<div style={{ padding: 40 }}>
<Notifications autoClose={false} position="top-center" limit={1} />
<Notifications autoClose={false} position="top-center" limit={5} />

<Group>
<Button
onClick={() =>
showNotification({ message: 'Test', title: 'Test', style: { background: 'red' } })
showNotification({ message: 'Test', title: 'Test', position: 'bottom-right' })
}
>
Show notification
bottom-right
</Button>
<Button
onClick={() =>
showNotification({ message: 'Test', title: 'Test', position: 'bottom-left' })
}
>
bottom-left
</Button>
<Button
onClick={() => showNotification({ message: 'Test', title: 'Test', position: 'top-left' })}
>
top-left
</Button>
<Button
onClick={() =>
showNotification({ message: 'Test', title: 'Test', position: 'top-right' })
}
>
top-right
</Button>
</Group>
</div>
Expand Down
152 changes: 80 additions & 72 deletions packages/@mantine/notifications/src/Notifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,15 @@ import {
useStyles,
} from '@mantine/core';
import { useDidUpdate, useForceUpdate, useReducedMotion } from '@mantine/hooks';
import {
getGroupedNotifications,
positions,
} from './get-grouped-notifications/get-grouped-notifications';
import { getNotificationStateStyles } from './get-notification-state-styles';
import { NotificationContainer } from './NotificationContainer';
import {
hideNotification,
NotificationPosition,
notifications,
NotificationsStore,
notificationsStore,
Expand All @@ -36,28 +41,15 @@ const Transition: any = _Transition;

export type NotificationsStylesNames = 'root' | 'notification';
export type NotificationsCssVariables = {
root:
| '--notifications-z-index'
| '--notifications-top'
| '--notifications-right'
| '--notifications-left'
| '--notifications-left'
| '--notifications-transform'
| '--notifications-container-width';
root: '--notifications-z-index' | '--notifications-container-width';
};

export interface NotificationsProps
extends BoxProps,
StylesApiProps<NotificationsFactory>,
ElementProps<'div'> {
/** Notifications position, `'bottom-right'` by default */
position?:
| 'top-left'
| 'top-right'
| 'top-center'
| 'bottom-left'
| 'bottom-right'
| 'bottom-center';
/** Notifications default position, `'bottom-right'` by default */
position?: NotificationPosition;

/** Auto close timeout for all notifications in ms, `false` to disable auto close, can be overwritten for individual notifications in `notifications.show` function, `4000` by default */
autoClose?: number | false;
Expand Down Expand Up @@ -114,28 +106,12 @@ const defaultProps: Partial<NotificationsProps> = {
withinPortal: true,
};

const varsResolver = createVarsResolver<NotificationsFactory>(
(_, { zIndex, position, containerWidth }) => {
const [vertical, horizontal] = position!.split('-');

return {
root: {
'--notifications-z-index': zIndex?.toString(),
'--notifications-top': vertical === 'top' ? 'var(--mantine-spacing-md)' : undefined,
'--notifications-bottom': vertical === 'bottom' ? 'var(--mantine-spacing-md)' : undefined,
'--notifications-left':
horizontal === 'left'
? 'var(--mantine-spacing-md)'
: horizontal === 'center'
? '50%'
: undefined,
'--notifications-right': horizontal === 'right' ? 'var(--mantine-spacing-md)' : undefined,
'--notifications-transform': horizontal === 'center' ? 'translateX(-50%)' : undefined,
'--notifications-container-width': rem(containerWidth),
},
};
}
);
const varsResolver = createVarsResolver<NotificationsFactory>((_, { zIndex, containerWidth }) => ({
root: {
'--notifications-z-index': zIndex?.toString(),
'--notifications-container-width': rem(containerWidth),
},
}));

export const Notifications = factory<NotificationsFactory>((_props, ref) => {
const props = useProps('Notifications', defaultProps, _props);
Expand Down Expand Up @@ -183,8 +159,12 @@ export const Notifications = factory<NotificationsFactory>((_props, ref) => {
});

useEffect(() => {
store?.updateState((current) => ({ ...current, limit: limit || 5 }));
}, [limit]);
store?.updateState((current) => ({
...current,
limit: limit || 5,
defaultPosition: position!,
}));
}, [limit, position]);

useDidUpdate(() => {
if (data.notifications.length > previousLength.current) {
Expand All @@ -193,41 +173,69 @@ export const Notifications = factory<NotificationsFactory>((_props, ref) => {
previousLength.current = data.notifications.length;
}, [data.notifications]);

const items = data.notifications.map(({ style: notificationStyle, ...notification }) => (
<Transition
key={notification.id}
timeout={duration}
onEnter={() => refs.current[notification.id!].offsetHeight}
nodeRef={{ current: refs.current[notification.id!] }}
>
{(state: TransitionStatus) => (
<NotificationContainer
ref={(node) => {
refs.current[notification.id!] = node!;
}}
data={notification}
onHide={(id) => hideNotification(id, store)}
autoClose={autoClose!}
{...getStyles('notification', {
style: {
...getNotificationStateStyles({
state,
position,
transitionDuration: duration!,
maxHeight: notificationMaxHeight!,
}),
...notificationStyle,
},
})}
/>
)}
</Transition>
));
const grouped = getGroupedNotifications(data.notifications, position!);
const groupedComponents = positions.reduce(
(acc, pos) => {
acc[pos] = grouped[pos].map(({ style: notificationStyle, ...notification }) => (
<Transition
key={notification.id}
timeout={duration}
onEnter={() => refs.current[notification.id!].offsetHeight}
nodeRef={{ current: refs.current[notification.id!] }}
>
{(state: TransitionStatus) => (
<NotificationContainer
ref={(node) => {
refs.current[notification.id!] = node!;
}}
data={notification}
onHide={(id) => hideNotification(id, store)}
autoClose={autoClose!}
{...getStyles('notification', {
style: {
...getNotificationStateStyles({
state,
position: pos,
transitionDuration: duration!,
maxHeight: notificationMaxHeight!,
}),
...notificationStyle,
},
})}
/>
)}
</Transition>
));

return acc;
},
{} as Record<NotificationPosition, React.ReactNode>
);

return (
<OptionalPortal withinPortal={withinPortal} {...portalProps}>
<Box {...getStyles('root')} ref={ref} {...others}>
<TransitionGroup>{items}</TransitionGroup>
<Box {...getStyles('root')} data-position="top-center" ref={ref} {...others}>
<TransitionGroup>{groupedComponents['top-center']}</TransitionGroup>
</Box>

<Box {...getStyles('root')} data-position="top-left" {...others}>
<TransitionGroup>{groupedComponents['top-left']}</TransitionGroup>
</Box>

<Box {...getStyles('root')} data-position="top-right" {...others}>
<TransitionGroup>{groupedComponents['top-right']}</TransitionGroup>
</Box>

<Box {...getStyles('root')} data-position="bottom-right" {...others}>
<TransitionGroup>{groupedComponents['bottom-right']}</TransitionGroup>
</Box>

<Box {...getStyles('root')} data-position="bottom-left" {...others}>
<TransitionGroup>{groupedComponents['bottom-left']}</TransitionGroup>
</Box>

<Box {...getStyles('root')} data-position="bottom-center" {...others}>
<TransitionGroup>{groupedComponents['bottom-center']}</TransitionGroup>
</Box>
</OptionalPortal>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { NotificationData, NotificationPosition } from '../notifications.store';

export type GroupedNotifications = Record<NotificationPosition, NotificationData[]>;

export const positions: NotificationPosition[] = [
'bottom-center',
'bottom-left',
'bottom-right',
'top-center',
'top-left',
'top-right',
];

export function getGroupedNotifications(
notifications: NotificationData[],
defaultPosition: NotificationPosition
) {
return notifications.reduce<GroupedNotifications>(
(acc, notification) => {
acc[notification.position || defaultPosition].push(notification);
return acc;
},
positions.reduce<GroupedNotifications>((acc, item) => {
acc[item] = [];
return acc;
}, {} as GroupedNotifications)
);
}
43 changes: 41 additions & 2 deletions packages/@mantine/notifications/src/notifications.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,21 @@ import { NotificationProps } from '@mantine/core';
import { randomId } from '@mantine/hooks';
import { createStore, MantineStore, useStore } from '@mantine/store';

export type NotificationPosition =
| 'top-left'
| 'top-right'
| 'top-center'
| 'bottom-left'
| 'bottom-right'
| 'bottom-center';

export interface NotificationData extends Omit<NotificationProps, 'onClose'>, Record<string, any> {
/** Notification id, can be used to close or update notification */
id?: string;

/** Position of the notification, if not set, the position is determined based on `position` prop on Notifications component */
position?: NotificationPosition;

/** Notification message, required for all notifications */
message: React.ReactNode;

Expand All @@ -24,15 +35,41 @@ export interface NotificationData extends Omit<NotificationProps, 'onClose'>, Re
export interface NotificationsState {
notifications: NotificationData[];
queue: NotificationData[];
defaultPosition: NotificationPosition;
limit: number;
}

export type NotificationsStore = MantineStore<NotificationsState>;

function getDistributedNotifications(
data: NotificationData[],
defaultPosition: NotificationPosition,
limit: number
) {
const queue: NotificationData[] = [];
const notifications: NotificationData[] = [];
const count: Record<string, number> = {};

for (const item of data) {
const position = item.position || defaultPosition;
count[position] = count[position] || 0;
count[position] += 1;

if (count[position] <= limit) {
notifications.push(item);
} else {
queue.push(item);
}
}

return { notifications, queue };
}

export const createNotificationsStore = () =>
createStore<NotificationsState>({
notifications: [],
queue: [],
defaultPosition: 'bottom-right',
limit: 5,
});

Expand All @@ -45,11 +82,13 @@ export function updateNotificationsState(
) {
const state = store.getState();
const notifications = update([...state.notifications, ...state.queue]);
const updated = getDistributedNotifications(notifications, state.defaultPosition, state.limit);

store.setState({
notifications: notifications.slice(0, state.limit),
queue: notifications.slice(state.limit),
notifications: updated.notifications,
queue: updated.queue,
limit: state.limit,
defaultPosition: state.defaultPosition,
});
}

Expand Down

0 comments on commit b2d1302

Please sign in to comment.