Skip to content

Commit

Permalink
Merge branch 'master' into feat/notification-stack
Browse files Browse the repository at this point in the history
  • Loading branch information
MadCcc committed Aug 29, 2023
2 parents b0dff7e + a74f68d commit b1d00ef
Show file tree
Hide file tree
Showing 9 changed files with 245 additions and 117 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "rc-notification",
"version": "5.0.5",
"version": "5.1.1",
"description": "notification ui component for react",
"engines": {
"node": ">=8.x"
Expand Down
15 changes: 1 addition & 14 deletions src/Notice.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,7 @@
import classNames from 'classnames';
import KeyCode from 'rc-util/lib/KeyCode';
import * as React from 'react';

export interface NoticeConfig {
content?: React.ReactNode;
duration?: number | null;
closeIcon?: React.ReactNode;
closable?: boolean;
className?: string;
style?: React.CSSProperties;
/** @private Internal usage. Do not override in your code */
props?: React.HTMLAttributes<HTMLDivElement> & Record<string, any>;

onClose?: VoidFunction;
onClick?: React.MouseEventHandler<HTMLDivElement>;
}
import type { NoticeConfig } from './interface';

export interface NoticeProps extends Omit<NoticeConfig, 'onClose'> {
prefixCls: string;
Expand Down
132 changes: 132 additions & 0 deletions src/NoticeList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import type { CSSProperties, FC } from 'react';
import React, { useContext, useRef, useState } from 'react';
import clsx from 'classnames';
import type { CSSMotionProps } from 'rc-motion';
import { CSSMotionList } from 'rc-motion';
import type { InnerOpenConfig, NoticeConfig, OpenConfig, Placement } from './interface';
import Notice from './Notice';
import { NotificationContext } from './NotificationProvider';

export interface NoticeListProps {
configList?: OpenConfig[];
placement?: Placement;
prefixCls?: string;
motion?: CSSMotionProps | ((placement: Placement) => CSSMotionProps);
stack?:
| boolean
| {
threshold?: number;
};

// Events
onAllNoticeRemoved?: (placement: Placement) => void;
onNoticeClose?: (key: React.Key) => void;

// Common
className?: string;
style?: CSSProperties;
}

const NoticeList: FC<NoticeListProps> = (props) => {
const {
configList,
placement,
prefixCls,
className,
style,
motion,
onAllNoticeRemoved,
onNoticeClose,
stack,
} = props;

const { classNames: ctxCls } = useContext(NotificationContext);

const listRef = useRef<HTMLDivElement[]>([]);
const [latestNotice, setLatestNotice] = useState<HTMLDivElement>(null);
const [hoverCount, setHoverCount] = useState(0);

const keys = configList.map((config) => ({
config,
key: config.key,
}));

const expanded =
!!stack &&
(hoverCount > 0 ||
keys.length <= (typeof stack === 'object' && 'threshold' in stack ? stack.threshold : 3));

const placementMotion = typeof motion === 'function' ? motion(placement) : motion;

return (
<CSSMotionList
key={placement}
className={clsx(prefixCls, `${prefixCls}-${placement}`, ctxCls?.list, className)}
style={style}
keys={keys}
motionAppear
{...placementMotion}
onAllRemoved={() => {
onAllNoticeRemoved(placement);
}}
onAppearPrepare={async (element) => {
if (element.parentNode.lastElementChild === element) {
setLatestNotice(element as HTMLDivElement);
}
}}
>
{({ config, className: motionClassName, style: motionStyle }, nodeRef) => {
const { key, times } = config as InnerOpenConfig;
const { className: configClassName, style: configStyle } = config as NoticeConfig;

const index = keys.length - 1 - keys.findIndex((item) => item.key === key);
const stackStyle: CSSProperties = {};
if (stack) {
if (index > 0) {
stackStyle.height = expanded ? '' : latestNotice.offsetHeight;
stackStyle.transform = `translateY(${
index * 8 +
(expanded
? listRef.current.reduce(
(acc, item, refIndex) => acc + (refIndex < index ? item.offsetHeight : 0),
0,
)
: 0)
}px)`;
}
}

return (
<Notice
{...config}
ref={(node) => {
nodeRef(node);
listRef.current[index] = node;
}}
prefixCls={prefixCls}
className={clsx(motionClassName, configClassName, ctxCls?.notice)}
style={{
...motionStyle,
...configStyle,
...stackStyle,
}}
times={times}
key={key}
eventKey={key}
onNoticeClose={onNoticeClose}
props={{
onMouseEnter: () => setHoverCount((c) => c + 1),
onMouseLeave: () => setHoverCount((c) => c - 1),
}}
/>
);
}}
</CSSMotionList>
);
};

if (process.env.NODE_ENV !== 'production') {
NoticeList.displayName = 'NoticeList';
}

export default NoticeList;
23 changes: 23 additions & 0 deletions src/NotificationProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { FC } from 'react';
import React from 'react';

export interface NotificationContextProps {
classNames?: {
notice?: string;
list?: string;
};
}

export const NotificationContext = React.createContext<NotificationContextProps>({});

export interface NotificationProviderProps extends NotificationContextProps {
children: React.ReactNode;
}

const NotificationProvider: FC<NotificationProviderProps> = ({ children, classNames }) => {
return (
<NotificationContext.Provider value={{ classNames }}>{children}</NotificationContext.Provider>
);
};

export default NotificationProvider;
121 changes: 22 additions & 99 deletions src/Notifications.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
import * as React from 'react';
import { ReactElement } from 'react';
import { createPortal } from 'react-dom';
import { CSSMotionList } from 'rc-motion';
import type { CSSMotionProps } from 'rc-motion';
import classNames from 'classnames';
import Notice from './Notice';
import type { NoticeConfig } from './Notice';
import { CSSProperties, useEffect, useRef, useState } from 'react';

export interface OpenConfig extends NoticeConfig {
key: React.Key;
placement?: Placement;
content?: React.ReactNode;
duration?: number | null;
}
import type { InnerOpenConfig, OpenConfig, Placement, Placements } from './interface';
import NoticeList from './NoticeList';

export interface NotificationsProps {
prefixCls?: string;
Expand All @@ -27,14 +18,12 @@ export interface NotificationsProps {
| {
threshold?: number;
};
renderNotifications?: (
node: ReactElement,
info: { prefixCls: string; key: React.Key },
) => ReactElement;
}

export type Placement = 'top' | 'topLeft' | 'topRight' | 'bottom' | 'bottomLeft' | 'bottomRight';

type Placements = Partial<Record<Placement, OpenConfig[]>>;

type InnerOpenConfig = OpenConfig & { times?: number };

export interface NotificationsRef {
open: (config: OpenConfig) => void;
close: (key: React.Key) => void;
Expand All @@ -52,6 +41,7 @@ const Notifications = React.forwardRef<NotificationsRef, NotificationsProps>((pr
style,
onAllRemoved,
stack,
renderNotifications,
} = props;
const [configList, setConfigList] = React.useState<OpenConfig[]>([]);

Expand Down Expand Up @@ -146,10 +136,6 @@ const Notifications = React.forwardRef<NotificationsRef, NotificationsProps>((pr
emptyRef.current = false;
}
}, [placements]);

const listRef = useRef<HTMLDivElement[]>([]);
const [latestNotice, setLatestNotice] = useState<HTMLDivElement>(null);
const [hoverCount, setHoverCount] = useState(0);
// ======================== Render ========================
if (!container) {
return null;
Expand All @@ -161,87 +147,24 @@ const Notifications = React.forwardRef<NotificationsRef, NotificationsProps>((pr
<>
{placementList.map((placement) => {
const placementConfigList = placements[placement];
const keys = placementConfigList.map((config) => ({
config,
key: config.key,
}));

const expanded =
!!stack &&
(hoverCount > 0 ||
keys.length <=
(typeof stack === 'object' && 'threshold' in stack ? stack.threshold : 3));

const placementMotion = typeof motion === 'function' ? motion(placement) : motion;

return (
<CSSMotionList
const list = (
<NoticeList
key={placement}
className={classNames(prefixCls, `${prefixCls}-${placement}`, className?.(placement), {
[`${prefixCls}-stack`]: !!stack,
[`${prefixCls}-stack-expanded`]: expanded,
})}
configList={placementConfigList}
placement={placement}
prefixCls={prefixCls}
className={className?.(placement)}
style={style?.(placement)}
keys={keys}
motionAppear
{...placementMotion}
onAllRemoved={() => {
onAllNoticeRemoved(placement);
}}
onAppearPrepare={async (element) => {
if (element.parentNode.lastElementChild === element) {
setLatestNotice(element as HTMLDivElement);
}
}}
>
{({ config, className: motionClassName, style: motionStyle }, nodeRef) => {
const { key, times } = config as InnerOpenConfig;
const { className: configClassName, style: configStyle } = config as NoticeConfig;

const index = keys.length - 1 - keys.findIndex((item) => item.key === key);
const stackStyle: CSSProperties = {};
if (stack) {
if (index > 0) {
stackStyle.height = expanded ? '' : latestNotice.offsetHeight;
stackStyle.transform = `translateY(${
index * 8 +
(expanded
? listRef.current.reduce(
(acc, item, refIndex) => acc + (refIndex < index ? item.offsetHeight : 0),
0,
)
: 0)
}px)`;
}
}

return (
<Notice
{...config}
ref={(node) => {
nodeRef(node);
listRef.current[index] = node;
}}
prefixCls={prefixCls}
className={classNames(motionClassName, configClassName)}
style={{
...motionStyle,
...configStyle,
...stackStyle,
}}
times={times}
key={key}
eventKey={key}
onNoticeClose={onNoticeClose}
props={{
onMouseEnter: () => setHoverCount((c) => c + 1),
onMouseLeave: () => setHoverCount((c) => c - 1),
}}
/>
);
}}
</CSSMotionList>
motion={motion}
onNoticeClose={onNoticeClose}
onAllNoticeRemoved={onAllNoticeRemoved}
/>
);

return renderNotifications
? renderNotifications(list, { prefixCls, key: placement })
: list;
})}
</>,
container,
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import useNotification from './useNotification';
import Notice from './Notice';
import type { NotificationAPI, NotificationConfig } from './useNotification';
import NotificationProvider from './NotificationProvider';

export { useNotification, Notice };
export { useNotification, Notice, NotificationProvider };
export type { NotificationAPI, NotificationConfig };
28 changes: 28 additions & 0 deletions src/interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type React from 'react';

export type Placement = 'top' | 'topLeft' | 'topRight' | 'bottom' | 'bottomLeft' | 'bottomRight';

export interface NoticeConfig {
content?: React.ReactNode;
duration?: number | null;
closeIcon?: React.ReactNode;
closable?: boolean;
className?: string;
style?: React.CSSProperties;
/** @private Internal usage. Do not override in your code */
props?: React.HTMLAttributes<HTMLDivElement> & Record<string, any>;

onClose?: VoidFunction;
onClick?: React.MouseEventHandler<HTMLDivElement>;
}

export interface OpenConfig extends NoticeConfig {
key: React.Key;
placement?: Placement;
content?: React.ReactNode;
duration?: number | null;
}

export type InnerOpenConfig = OpenConfig & { times?: number };

export type Placements = Partial<Record<Placement, OpenConfig[]>>;
Loading

0 comments on commit b1d00ef

Please sign in to comment.