Skip to content

Commit

Permalink
refactor: use notification element
Browse files Browse the repository at this point in the history
  • Loading branch information
mfal committed Jun 6, 2024
1 parent 777b2e7 commit b0ec72a
Show file tree
Hide file tree
Showing 18 changed files with 169 additions and 156 deletions.
2 changes: 1 addition & 1 deletion packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@
"@storybook/test": "^8.1.5",
"@tabler/icons-react": "^3.5.0",
"@tanstack/react-table": "^8.17.3",
"@types/luxon": "^3",
"clsx": "^2.1.1",
"copy-to-clipboard": "^3.3.3",
"dot-prop": "^9.0.0",
Expand Down Expand Up @@ -318,7 +319,6 @@
"@testing-library/react": "~15.0.7",
"@testing-library/user-event": "^14.5.2",
"@types/invariant": "^2.2.37",
"@types/luxon": "^3",
"@types/node": "20.14.1",
"@types/prettier": "^3.0.0",
"@types/prop-types": "^15.7.12",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable */
/* auto-generated file */
export { IconApp } from "./IconApp";
export { IconBackLink } from "./IconBackLink";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface NotificationProps
extends PropsWithChildren<ComponentProps<"div">>,
PropsWithStatus {
href?: string;
autoClose?: boolean;
onClick?: () => void;
onClose?: () => void;
}
Expand All @@ -27,6 +28,7 @@ export const Notification: FC<NotificationProps> = (props) => {
href,
onClick,
onClose,
autoClose: ignoredAutoClose,
...rest
} = props;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import type { FC } from "react";
import React from "react";
import { cloneElement } from "react";
import type {
NotificationController,
NotificationData,
} from "@/components/NotificationProvider/NotificationController";
import { Notification } from "@/components/Notification";
import { Heading } from "@/components/Heading";
import { Text } from "@/components/Text";

interface Props {
notification: NotificationData;
Expand All @@ -16,22 +13,17 @@ interface Props {
export const ControlledNotification: FC<Props> = (props) => {
const { notification, controller } = props;

return (
<Notification
onClose={() => controller.remove(notification.meta.id)}
onClick={notification.onClick}
onMouseEnter={() => {
notification.meta.autoCloseTimer.pause();
}}
onMouseLeave={() => {
notification.meta.autoCloseTimer.resume();
}}
status={notification.status}
>
<Heading>{notification.heading}</Heading>
<Text>{notification.text} </Text>
</Notification>
);
return cloneElement(notification.element, {
onMouseEnter: () => {
notification.meta.autoCloseTimer.pause();
},
onMouseLeave: () => {
notification.meta.autoCloseTimer.resume();
},
onClose: () => {
controller.remove(notification.meta.id);
},
});
};

export default ControlledNotification;
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { ReactNode } from "react";
import type { Status } from "@/lib/types/props";
import type { ReactElement } from "react";
import { useRef } from "react";
import useSelector from "@/lib/mobx/useSelector";
import { action, makeObservable, observable } from "mobx";
import Timer from "@/lib/timer/Timer";
import type { NotificationProps } from "@/components/Notification";

interface NotificationMetaData {
readonly id: number;
Expand All @@ -11,18 +12,10 @@ interface NotificationMetaData {
}

export interface NotificationData {
readonly heading: ReactNode;
readonly text: ReactNode;
readonly onClose?: () => void;
readonly onClick?: () => void;
readonly autoClose?: boolean;
readonly status?: Status;

readonly element: ReactElement<NotificationProps>;
readonly meta: NotificationMetaData;
}

type NotificationInputData = Omit<NotificationData, "meta">;

export class NotificationController {
public readonly notificationsData = new Map<number, NotificationData>();
private id = 0;
Expand All @@ -35,11 +28,15 @@ export class NotificationController {
});
}

public static useNew(): NotificationController {
return useRef(new NotificationController()).current;
}

public useNotifications(): NotificationData[] {
return useSelector(() => Array.from(this.notificationsData.values()));
}

public add(data: NotificationInputData): void {
public add(notification: ReactElement<NotificationProps>): void {
const id = this.id++;

const meta: NotificationMetaData = {
Expand All @@ -49,11 +46,11 @@ export class NotificationController {
};

this.notificationsData.set(id, {
...data,
element: notification,
meta,
});

if (data.autoClose) {
if (notification.props.autoClose) {
meta.autoCloseTimer.start({ seconds: 10 }, () => {
this.remove(id);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const useNotificationController = (): NotificationController =>

export const NotificationProvider: FC<Props> = (props) => {
const { children, ...containerProps } = props;
const controller = new NotificationController();
const controller = NotificationController.useNew();

return (
<context.Provider value={controller}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import styles from "./NotificationContainer.module.scss";
import type NotificationController from "@/components/NotificationProvider/NotificationController";
import ControlledNotification from "@/components/NotificationProvider/ControlledNotification";
import { AnimatePresence, domAnimation, LazyMotion, m } from "framer-motion";
import { useIsSSR } from "react-aria";

export interface NotificationsContainerProps extends ComponentProps<"div"> {
controller: NotificationController;
Expand All @@ -20,6 +21,8 @@ export const NotificationsContainer: FC<NotificationsContainerProps> = (

const notifications = controller.useNotifications();

const isSsr = useIsSSR();

const content = (
<LazyMotion features={domAnimation}>
<div className={rootClassName} {...rest}>
Expand All @@ -46,7 +49,7 @@ export const NotificationsContainer: FC<NotificationsContainerProps> = (
</LazyMotion>
);

return ReactDOM.createPortal(content, document.body);
return isSsr ? null : ReactDOM.createPortal(content, document.body);
};

export default NotificationsContainer;
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
useNotificationController,
} from "@/components/NotificationProvider";
import { Text } from "@/components/Text";
import { Notification } from "@/components/Notification";
import { Heading } from "@/components/Heading";

const meta: Meta<{ autoClose: boolean }> = {
title: "Status/Notifications",
Expand All @@ -20,22 +22,30 @@ const meta: Meta<{ autoClose: boolean }> = {

useEffect(() => {
setTimeout(() => {
notificationController.add({
autoClose: props.autoClose,
heading: "Email address archived",
text: "Your email address [email protected] has been archived.",
onClick: () => alert("Notification clicked"),
});
notificationController.add(
<Notification
autoClose={props.autoClose}
onClick={() => alert("Notification clicked")}
>
<Heading>Email address archived</Heading>
<Text>
Your email address [email protected] has been archived.
</Text>
</Notification>,
);
}, 500);

setTimeout(() => {
notificationController.add({
autoClose: props.autoClose,
heading: "No SSL certificate",
text: "No SSL certificate could be issued for examples.de.",
status: "warning",
onClick: () => alert("Notification clicked"),
});
notificationController.add(
<Notification
autoClose={props.autoClose}
onClick={() => alert("Notification clicked")}
status="warning"
>
<Heading>No SSL certificate</Heading>
<Text>No SSL certificate could be issued for examples.de.</Text>
</Notification>,
);
}, 2000);
}, []);

Expand Down
49 changes: 26 additions & 23 deletions packages/docs/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { LinkProvider } from "@mittwald/flow-react-components/nextjs";
import { IconMittwald } from "@mittwald/flow-react-components/Icons";
import MainNavigation from "@/app/_components/layout/MainNavigation";
import MobileNavigation from "@/app/_components/layout/MobileNavigation/MobileNavigation";
import { NotificationProvider } from "@mittwald/flow-react-components/NotificationProvider";

export const metadata: Metadata = {
title: "Flow – mittwald Design System",
Expand All @@ -28,29 +29,31 @@ const RootLayout: FC<PropsWithChildren> = async (props) => {
<html lang="en">
<body className={bodyClassName}>
<LinkProvider>
<header className={styles.header}>
<IconMittwald size="l" className={styles.logo} />
<Heading level={1} className={styles.heading}>
Flow
</Heading>
<StatusBadge className={styles.betaBadge} status="warning">
beta
</StatusBadge>
<HeaderNavigation
className={styles.headerNavigation}
docs={docs.map((mdx) => mdx.serialize())}
/>
<MobileNavigation
docs={docs.map((mdx) => mdx.serialize())}
className={styles.mobileNavigation}
/>
</header>
<div className={styles.center}>
<LayoutCard className={styles.mainNavigation}>
<MainNavigation docs={docs.map((mdx) => mdx.serialize())} />
</LayoutCard>
<main className={styles.main}>{props.children}</main>
</div>
<NotificationProvider>
<header className={styles.header}>
<IconMittwald size="l" className={styles.logo} />
<Heading level={1} className={styles.heading}>
Flow
</Heading>
<StatusBadge className={styles.betaBadge} status="warning">
beta
</StatusBadge>
<HeaderNavigation
className={styles.headerNavigation}
docs={docs.map((mdx) => mdx.serialize())}
/>
<MobileNavigation
docs={docs.map((mdx) => mdx.serialize())}
className={styles.mobileNavigation}
/>
</header>
<div className={styles.center}>
<LayoutCard className={styles.mainNavigation}>
<MainNavigation docs={docs.map((mdx) => mdx.serialize())} />
</LayoutCard>
<main className={styles.main}>{props.children}</main>
</div>
</NotificationProvider>
</LinkProvider>
</body>
</html>
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

Loading

0 comments on commit b0ec72a

Please sign in to comment.