Skip to content

Commit

Permalink
feat: added NotificationsPopupWrapper component + cleaned up the stor…
Browse files Browse the repository at this point in the history
…ies and README.md
  • Loading branch information
Ruminat committed Jul 26, 2023
1 parent 01240f7 commit a39a2c9
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 139 deletions.
2 changes: 1 addition & 1 deletion src/components/Notifications/Notifications.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ $block: '.#{variables.$ns}notifications';
display: flex;
flex-direction: column;
color: var(--g-color-text-primary);
background: var(--g-color-base-background);
height: 100%;
width: 100%;

&__head {
display: flex;
Expand Down
25 changes: 25 additions & 0 deletions src/components/Notifications/NotificationsPopupWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';

const WIDTH = '320px';
const HEIGHT = '470px';

type Props = React.PropsWithChildren<{
className?: string;
style?: React.CSSProperties;
fullHeight?: boolean;
}>;

export const NotificationsPopupWrapper = (props: Props) => {
const {className, style, fullHeight = true, children} = props;

const finalStyles = React.useMemo((): React.CSSProperties => {
const heightStyles = fullHeight ? {height: HEIGHT} : {maxHeight: HEIGHT};
return {...heightStyles, width: WIDTH, overflowY: 'auto', ...style};
}, [fullHeight, style]);

return (
<div className={className} style={finalStyles}>
{children}
</div>
);
};
60 changes: 34 additions & 26 deletions src/components/Notifications/README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
## Notifications

Components for displaying notifications.
Components for displaying notifications ([storybook](https://preview.gravity-ui.com/components/?path=/story/components-notifications--default)).
Can be used on desktop and touch devices.

### Simple usage example

```typescript
const YourComponent: React.FC = () => {
const notifications = useMemo(
const notifications: NotificationProps = React.useMemo(
() => [
{
id: 'minimum',
Expand All @@ -18,19 +18,27 @@ const YourComponent: React.FC = () => {
[],
);

const action = useMemo(() => ({icon: Plus, text: 'Add', onClick: () => console.log('ADD')}), []);
const action = React.useMemo(
() => ({icon: Plus, text: 'Add', onClick: () => console.log('ADD')}),
[],
);

return (
<Notifications
title="Notifications"
notifications={notifications}
actions={<NotificationAction action={action} />}
emptyMessage={'Unfortunately, there are no notifications for you, pal'}
/>
// If you use Notifications inside a popup, use NotificationsPopupWrapper
<NotificationsPopupWrapper>
<Notifications
title="Notifications"
notifications={notifications}
actions={<NotificationAction action={action} />}
emptyMessage={'Unfortunately, there are no notifications for you, pal'}
/>
</NotificationsPopupWrapper>
);
};
```

For more code examples go to [Notifications.stories.tsx](https://github.com/gravity-ui/components/blob/main/src/components/Notifications/__stories__/Notifications.stories.tsx).

### Components

**Notifications** — renders notifications and actions on these notifications.
Expand All @@ -55,22 +63,22 @@ const YourComponent: React.FC = () => {

**NotificationProps** — notification's type:

| Property | Type | Required | Default | Description |
| :------------ | :---------------------------------- | :------: | :------ | :---------------------------------------------------- |
| id | `string` | `true` | | Unique identifier (used in `key` for example) |
| content | `ReactNode` | `true` | | Notification's content (what it's about) |
| title | `ReactNode` | | | Notification's title (bold) |
| formattedDate | `ReactNode` | | | Notification's creation date (already formatted) |
| unread | `boolean` | | `false` | Is notification unread |
| archived | `boolean` | | `false` | Is notification archived (invisible to the user) |
| source | `NotificationSourceProps` | | | Notification's source (e.g. Cloud/Tracker/Console) |
| theme | `NotificationTheme` | | | Notification's theme (e.g. warning/danger) |
| className | `string` | | | Notification's `className` |
| sideActions | `ReactNode` | | | Notification's actions on the right side |
| bottomActions | `ReactNode` | | | Notification's bottom actions (as buttons by default) |
| swipeActions | `NotificationSwipeActionsProps` | | | Notification's action on left/right swipe |
| onMouseEnter | `MouseEventHandler<HTMLDivElement>` | | | Callback for `onMouseEnter` |
| onMouseLeave | `MouseEventHandler<HTMLDivElement>` | | | Callback for `onMouseLeave` |
| onClick | `MouseEventHandler<HTMLDivElement>` | | | Callback for `onClick` |
| Property | Type | Required | Default | Description |
| :------------ | :------------------------------ | :------: | :------ | :--------------------------------------------------------------- |
| id | `string` | `true` | | Unique identifier (used in `key` for example) |
| content | `ReactNode` | `true` | | Notification's content (what it's about) |
| title | `ReactNode` | | | Notification's title (bold) |
| formattedDate | `ReactNode` | | | Notification's creation date (already formatted) |
| unread | `boolean` | | `false` | Is notification unread |
| archived | `boolean` | | `false` | Is notification archived (invisible to the user) |
| source | `NotificationSourceProps` | | | Notification's source (e.g. Cloud/Tracker/Console) |
| theme | `NotificationTheme` | | | Notification's theme (e.g. warning/danger) |
| className | `string` | | | Notification's `className` |
| sideActions | `ReactNode` | | | Notification's actions on the right side |
| bottomActions | `ReactNode` | | | Notification's bottom actions (as buttons by default) |
| swipeActions | `NotificationSwipeActionsProps` | | | Notification's action on left/right swipe (mobile mode required) |
| onMouseEnter | `MouseEventHandler` | | | Callback for `onMouseEnter` |
| onMouseLeave | `MouseEventHandler` | | | Callback for `onMouseLeave` |
| onClick | `MouseEventHandler` | | | Callback for `onClick` |

For a more detailed info on types go to [Notifications' types](https://github.com/gravity-ui/components/blob/main/src/components/Notifications/definitions.ts) and [Notification' types](https://github.com/gravity-ui/components/blob/main/src/components/Notification/definitions.ts).
190 changes: 93 additions & 97 deletions src/components/Notifications/__stories__/Notifications.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,119 +1,43 @@
/* eslint-disable no-console */
import {Archive, ArrowRotateLeft, CircleCheck, Funnel} from '@gravity-ui/icons';
import {Button, DropdownMenu} from '@gravity-ui/uikit';
import {Bell} from '@gravity-ui/icons';
import {Button, Icon, Popup} from '@gravity-ui/uikit';
import {Meta, StoryFn} from '@storybook/react';
import React, {useState} from 'react';
import React from 'react';
import {delay} from '../../InfiniteScroll/__stories__/utils';
import {NotificationAction} from '../../Notification/NotificationAction';
import {NotificationProps} from '../../Notification/definitions';
import {Notifications} from '../Notifications';
import {generateNotification, mockNotifications} from './mockData';
import {NotificationsPopupWrapper} from '../NotificationsPopupWrapper';
import {
generateNotification,
mockNotifications,
notificationSideActions,
notificationsMockActions,
} from './mockData';

export default {
title: 'Components/Notifications',
component: Notifications,
} as Meta<typeof Notifications>;

const wrapperStyles = {
borderRadius: '8px',
border: '1px solid var(--g-color-line-generic)',
background: 'var(--g-color-base-background)',
margin: '4px',
};

const Wrapper = (props: React.PropsWithChildren) => {
return (
<div
style={{
borderRadius: '8px',
border: '1px solid var(--g-color-line-generic)',
margin: '4px',
width: '350px',
height: '470px',
overflowY: 'auto',
}}
>
<NotificationsPopupWrapper style={wrapperStyles}>
{props.children}
</div>
</NotificationsPopupWrapper>
);
};

type BooleanMap = Record<string, boolean | undefined>;

export const Default: StoryFn = () => {
const [unreadNotifications, setUnreadNotifications] = React.useState<BooleanMap>({
tracker: true,
samurai: true,
});

const [archivedNotifications, setArchivedNotifications] = React.useState<BooleanMap>({});

const getSideActions = React.useCallback(
(
id: NotificationProps['id'],
unread: boolean | undefined,
archived: boolean | undefined,
) => (
<>
<NotificationAction
action={{
icon: unread ? CircleCheck : ArrowRotateLeft,
text: `Mark as ${unread ? 'read' : 'unread'}`,
onClick: () =>
setUnreadNotifications((current) => ({...current, [id]: !unread})),
}}
/>
<NotificationAction
action={{
icon: Archive,
text: 'Archive',
onClick: () =>
setArchivedNotifications((current) => ({...current, [id]: !archived})),
}}
/>
</>
),
[],
);

const notifications = React.useMemo<NotificationProps[]>(
() =>
mockNotifications.map((notification: NotificationProps) => {
const id = notification.id;
const unread = unreadNotifications[id];
const archived = archivedNotifications[id];

return {
...notification,
unread,
archived,
sideActions: getSideActions(id, unread, archived),
};
}),
[unreadNotifications, archivedNotifications],
);

const actions = (
<>
<NotificationAction
action={{
icon: Archive,
text: 'Remove all from archive',
onClick: () => setArchivedNotifications({}),
}}
/>
<DropdownMenu
switcher={
<NotificationAction
action={{
icon: Funnel,
text: 'Filter',
onClick: () => console.log('FILTER'),
}}
/>
}
items={[
{text: 'Any', action: () => console.log('any')},
{text: 'Tracker', action: () => console.log('tracker')},
{text: 'Cloud', action: () => console.log('cloud')},
{text: 'You can put any popup here', action: () => console.log('cloud')},
]}
/>
</>
);
const {notifications, actions} = getNotificationsWithActions();

return (
<Wrapper>
Expand All @@ -123,7 +47,7 @@ export const Default: StoryFn = () => {
};

export const LoadByScrolling: StoryFn = () => {
const [notifications, setNotifications] = useState<NotificationProps[]>([]);
const [notifications, setNotifications] = React.useState<NotificationProps[]>([]);
const areAllNotificationsLoaded = notifications.length >= 40;

const onLoadMoreNotifications = async () => {
Expand All @@ -147,6 +71,25 @@ export const LoadByScrolling: StoryFn = () => {
);
};

export const InsideAPopup: StoryFn = () => {
const {notifications, actions} = getNotificationsWithActions();
const [isOpen, setIsOpen] = React.useState(false);
const ref = React.useRef(null);

return (
<>
<Button onClick={() => setIsOpen(!isOpen)} ref={ref}>
<Icon data={Bell} />
</Button>
<Popup open={isOpen} anchorRef={ref}>
<NotificationsPopupWrapper>
<Notifications notifications={notifications} actions={actions} />
</NotificationsPopupWrapper>
</Popup>
</>
);
};

export const Loading: StoryFn = () => {
return (
<Wrapper>
Expand Down Expand Up @@ -183,3 +126,56 @@ export const Empty: StoryFn = () => {
</Wrapper>
);
};

function getNotificationsWithActions() {
const [unreadNotifications, setUnreadNotifications] = React.useState<BooleanMap>({
tracker: true,
samurai: true,
});

const [archivedNotifications, setArchivedNotifications] = React.useState<BooleanMap>({});

const getSideActions = React.useCallback(
(
id: NotificationProps['id'],
unread: boolean | undefined,
archived: boolean | undefined,
) => (
<>
{notificationSideActions.read(Boolean(unread), () =>
setUnreadNotifications((current) => ({...current, [id]: !unread})),
)}
{notificationSideActions.archive(() =>
setArchivedNotifications((current) => ({...current, [id]: !archived})),
)}
</>
),
[],
);

const notifications = React.useMemo<NotificationProps[]>(
() =>
mockNotifications.map((notification: NotificationProps) => {
const id = notification.id;
const unread = unreadNotifications[id];
const archived = archivedNotifications[id];

return {
...notification,
unread,
archived,
sideActions: getSideActions(id, unread, archived),
};
}),
[unreadNotifications, archivedNotifications],
);

const actions = (
<>
{notificationsMockActions.unarchive(() => setArchivedNotifications({}))}
{notificationsMockActions.filter()}
</>
);

return {notifications, actions};
}
Loading

0 comments on commit a39a2c9

Please sign in to comment.