This commit is contained in:
2026-05-25 17:08:18 +08:00
parent df501f6151
commit f1259b71b5
9178 changed files with 1626125 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
The MIT License (MIT)
Copyright (c) 2014-present yiminghe
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+194
View File
@@ -0,0 +1,194 @@
# @rc-component/notification
React Notification UI Component
[![NPM version][npm-image]][npm-url] [![dumi](https://img.shields.io/badge/docs%20by-dumi-blue?style=flat-square)](https://github.com/umijs/dumi) [![build status][github-actions-image]][github-actions-url] [![Test coverage][coveralls-image]][coveralls-url] [![npm download][download-image]][download-url] [![bundle size][bundlephobia-image]][bundlephobia-url]
[npm-image]: http://img.shields.io/npm/v/@rc-component/notification.svg?style=flat-square
[npm-url]: http://npmjs.org/package/@rc-component/notification
[github-actions-image]: https://github.com/react-component/notification/workflows/CI/badge.svg
[github-actions-url]: https://github.com/react-component/notification/actions
[coveralls-image]: https://img.shields.io/coveralls/react-component/notification.svg?style=flat-square
[coveralls-url]: https://coveralls.io/r/react-component/notification?branch=master
[download-image]: https://img.shields.io/npm/dm/@rc-component/notification.svg?style=flat-square
[download-url]: https://npmjs.org/package/@rc-component/notification
[bundlephobia-url]: https://bundlephobia.com/result?p=@rc-component/notification
[bundlephobia-image]: https://badgen.net/bundlephobia/minzip/@rc-component/notification
## Install
[![@rc-component/notification](https://nodei.co/npm/@rc-component/notification.png)](https://npmjs.org/package/@rc-component/notification)
## Usage
```js
import Notification from '@rc-component/notification';
Notification.newInstance({}, (notification) => {
notification.notice({
content: 'content',
});
});
```
## Compatibility
| Browser | Supported Version |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
| [![Firefox](https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png)<br>Firefox](http://godban.github.io/browsers-support-badges/) | last 2 versions |
| [![Chrome](https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png)<br>Chrome](http://godban.github.io/browsers-support-badges/) | last 2 versions |
| [![Safari](https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png)<br>Safari](http://godban.github.io/browsers-support-badges/) | last 2 versions |
| [![Electron](https://raw.githubusercontent.com/alrra/browser-logos/master/src/electron/electron_48x48.png)<br>Electron](http://godban.github.io/browsers-support-badges/) | last 2 versions |
## Example
http://localhost:8001
online example: https://notification-react-component.vercel.app
## API
### Notification.newInstance(props, (notification) => void) => void
props details:
<table class="table table-bordered table-striped">
<thead>
<tr>
<th style="width: 100px;">name</th>
<th style="width: 50px;">type</th>
<th style="width: 50px;">default</th>
<th>description</th>
</tr>
</thead>
<tbody>
<tr>
<td>prefixCls</td>
<td>String</td>
<td></td>
<td>prefix class name for notification container</td>
</tr>
<tr>
<td>style</td>
<td>Object</td>
<td>{'top': 65, left: '50%'}</td>
<td>additional style for notification container.</td>
</tr>
<tr>
<td>getContainer</td>
<td>getContainer(): HTMLElement</td>
<td></td>
<td>function returning html node which will act as notification container</td>
</tr>
<tr>
<td>maxCount</td>
<td>number</td>
<td></td>
<td>max notices show, drop first notice if exceed limit</td>
</tr>
</tbody>
</table>
### notification.notice(props)
props details:
<table class="table table-bordered table-striped">
<thead>
<tr>
<th style="width: 100px;">name</th>
<th style="width: 200px;">type</th>
<th style="width: 50px;">default</th>
<th>description</th>
</tr>
</thead>
<tbody>
<tr>
<td>content</td>
<td>React.Element</td>
<td></td>
<td>content of notice</td>
</tr>
<tr>
<td>key</td>
<td>String</td>
<td></td>
<td>id of this notice</td>
</tr>
<tr>
<td>closable</td>
<td>Boolean | { closeIcon: ReactNode, onClose: VoidFunction }</td>
<td></td>
<td>whether show close button</td>
</tr>
<tr>
<td>onClose</td>
<td>Function</td>
<td></td>
<td>called when notice close</td>
</tr>
<tr>
<td>duration</td>
<td>number | false</td>
<td>4.5</td>
<td>The delay for automatic closing in seconds. Set to 0 or false to not close automatically.</td>
</tr>
<tr>
<td>showProgress</td>
<td>boolean</td>
<td>false</td>
<td>show with progress bar for auto-closing notification</td>
</tr>
<tr>
<td>pauseOnHover</td>
<td>boolean</td>
<td>true</td>
<td>keep the timer running or not on hover</td>
</tr>
<tr>
<td>style</td>
<td>Object</td>
<td> { right: '50%' } </td>
<td>additional style for single notice node.</td>
</tr>
<tr>
<td>closeIcon</td>
<td>ReactNode</td>
<td></td>
<td>specific the close icon.</td>
</tr>
<tr>
<td>props</td>
<td>Object</td>
<td></td>
<td>An object that can contain <code>data-*</code>, <code>aria-*</code>, or <code>role</code> props, to be put on the notification <code>div</code>. This currently only allows <code>data-testid</code> instead of <code>data-*</code> in TypeScript. See https://github.com/microsoft/TypeScript/issues/28960.</td>
</tr>
</tbody>
</table>
### notification.removeNotice(key:string)
remove single notice with specified key
### notification.destroy()
destroy current notification
## Test Case
```
npm test
npm run chrome-test
```
## Coverage
```
npm run coverage
```
open coverage/ dir
## License
@rc-component/notification is released under the MIT license.
@@ -0,0 +1,172 @@
.rc-notification {
position: fixed;
z-index: 1000;
display: flex;
max-height: 100vh;
padding: 10px;
align-items: flex-end;
width: 340px;
overflow-x: hidden;
overflow-y: auto;
height: 100vh;
box-sizing: border-box;
pointer-events: none;
flex-direction: column;
}
.rc-notification-top,
.rc-notification-topLeft,
.rc-notification-topRight {
top: 0;
}
.rc-notification-bottom,
.rc-notification-bottomRight,
.rc-notification-bottomLeft {
bottom: 0;
}
.rc-notification-bottomRight,
.rc-notification-topRight {
right: 0;
}
.rc-notification-notice {
position: relative;
display: block;
box-sizing: border-box;
line-height: 1.5;
width: 100%;
}
.rc-notification-notice-wrapper {
pointer-events: auto;
position: relative;
display: block;
box-sizing: border-box;
border-radius: 3px 3px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
margin: 0 0 16px;
border: 1px solid #999;
border: 0px solid rgba(0, 0, 0, 0);
background: #fff;
width: 300px;
}
.rc-notification-notice-content {
padding: 7px 20px 7px 10px;
}
.rc-notification-notice-closable .rc-notification-notice-content {
padding-right: 20px;
}
.rc-notification-notice-close {
position: absolute;
top: 3px;
right: 5px;
color: #000;
font-weight: 700;
font-size: 16px;
line-height: 1;
text-decoration: none;
text-shadow: 0 1px 0 #fff;
outline: none;
cursor: pointer;
opacity: 0.2;
filter: alpha(opacity=20);
border: 0;
background-color: #fff;
}
.rc-notification-notice-close-x:after {
content: '×';
}
.rc-notification-notice-close:hover {
text-decoration: none;
opacity: 1;
filter: alpha(opacity=100);
}
.rc-notification-notice-progress {
position: absolute;
left: 3px;
right: 3px;
border-radius: 1px;
overflow: hidden;
appearance: none;
-webkit-appearance: none;
display: block;
inline-size: 100%;
block-size: 2px;
border: 0;
}
.rc-notification-notice-progress,
.rc-notification-notice-progress::-webkit-progress-bar {
background-color: rgba(0, 0, 0, 0.04);
}
.rc-notification-notice-progress::-moz-progress-bar {
background-color: #31afff;
}
.rc-notification-notice-progress::-webkit-progress-value {
background-color: #31afff;
}
.rc-notification-fade {
overflow: hidden;
transition: all 0.3s;
}
.rc-notification-fade-appear-prepare {
pointer-events: none;
opacity: 0 !important;
}
.rc-notification-fade-appear-start {
transform: translateX(100%);
opacity: 0;
}
.rc-notification-fade-appear-active {
transform: translateX(0);
opacity: 1;
}
.rc-notification-stack > .rc-notification-notice-wrapper {
transition: all 0.3s;
position: absolute;
top: 12px;
opacity: 1;
}
.rc-notification-stack > .rc-notification-notice-wrapper:not(:nth-last-child(-n + 3)) {
opacity: 0;
right: 34px;
width: 252px;
overflow: hidden;
color: transparent;
pointer-events: none;
}
.rc-notification-stack > .rc-notification-notice-wrapper:nth-last-child(1) {
right: 10px;
}
.rc-notification-stack > .rc-notification-notice-wrapper:nth-last-child(2) {
right: 18px;
width: 284px;
color: transparent;
overflow: hidden;
}
.rc-notification-stack > .rc-notification-notice-wrapper:nth-last-child(3) {
right: 26px;
width: 268px;
color: transparent;
overflow: hidden;
}
.rc-notification-stack.rc-notification-stack-expanded > .rc-notification-notice-wrapper:not(:nth-last-child(-n + 1)) {
opacity: 1;
width: 300px;
right: 10px;
overflow: unset;
color: inherit;
pointer-events: auto;
}
.rc-notification-stack.rc-notification-stack-expanded > .rc-notification-notice-wrapper::before {
content: "";
position: absolute;
left: 0;
right: 0;
top: -16px;
width: 100%;
height: calc(100% + 32px);
background: transparent;
pointer-events: auto;
color: #000000;
}
.rc-notification-stack.rc-notification-bottomRight > .rc-notification-notice-wrapper {
top: unset;
bottom: 12px;
}
@@ -0,0 +1,265 @@
@notificationPrefixCls: rc-notification;
.@{notificationPrefixCls} {
// ====================== Notification ======================
position: fixed;
z-index: 1000;
display: flex;
max-height: 100vh;
padding: 10px;
align-items: flex-end;
width: 340px;
overflow-x: hidden;
overflow-y: auto;
height: 100vh;
box-sizing: border-box;
pointer-events: none;
flex-direction: column;
// Position
&-top,
&-topLeft,
&-topRight {
top: 0;
}
&-bottom,
&-bottomRight,
&-bottomLeft {
bottom: 0;
}
&-bottomRight,
&-topRight {
right: 0;
}
// ========================= Notice =========================
&-notice {
position: relative;
display: block;
box-sizing: border-box;
line-height: 1.5;
width: 100%;
&-wrapper {
pointer-events: auto;
position: relative;
display: block;
box-sizing: border-box;
border-radius: 3px 3px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
margin: 0 0 16px;
border: 1px solid #999;
border: 0px solid rgba(0, 0, 0, 0);
background: #fff;
width: 300px;
}
// Content
&-content {
padding: 7px 20px 7px 10px;
}
&-closable &-content {
padding-right: 20px;
}
&-close {
position: absolute;
top: 3px;
right: 5px;
color: #000;
font-weight: 700;
font-size: 16px;
line-height: 1;
text-decoration: none;
text-shadow: 0 1px 0 #fff;
outline: none;
cursor: pointer;
opacity: 0.2;
filter: alpha(opacity=20);
border: 0;
background-color: #fff;
&-x:after {
content: '×';
}
&:hover {
text-decoration: none;
opacity: 1;
filter: alpha(opacity=100);
}
}
// Progress
&-progress {
position: absolute;
left: 3px;
right: 3px;
border-radius: 1px;
overflow: hidden;
appearance: none;
-webkit-appearance: none;
display: block;
inline-size: 100%;
block-size: 2px;
border: 0;
&,
&::-webkit-progress-bar {
background-color: rgba(0, 0, 0, 0.04);
}
&::-moz-progress-bar {
background-color: #31afff;
}
&::-webkit-progress-value {
background-color: #31afff;
}
}
}
&-fade {
overflow: hidden;
transition: all 0.3s;
}
&-fade-appear-prepare {
pointer-events: none;
opacity: 0 !important;
}
&-fade-appear-start {
transform: translateX(100%);
opacity: 0;
}
&-fade-appear-active {
transform: translateX(0);
opacity: 1;
}
// .fade-effect() {
// animation-duration: 0.3s;
// animation-timing-function: cubic-bezier(0.55, 0, 0.55, 0.2);
// animation-fill-mode: both;
// }
// &-fade-appear,
// &-fade-enter {
// opacity: 0;
// animation-play-state: paused;
// .fade-effect();
// }
// &-fade-leave {
// .fade-effect();
// animation-play-state: paused;
// }
// &-fade-appear&-fade-appear-active,
// &-fade-enter&-fade-enter-active {
// animation-name: rcNotificationFadeIn;
// animation-play-state: running;
// }
// &-fade-leave&-fade-leave-active {
// animation-name: rcDialogFadeOut;
// animation-play-state: running;
// }
// @keyframes rcNotificationFadeIn {
// 0% {
// opacity: 0;
// }
// 100% {
// opacity: 1;
// }
// }
// @keyframes rcDialogFadeOut {
// 0% {
// opacity: 1;
// }
// 100% {
// opacity: 0;
// }
// }
// ========================= Stack =========================
&-stack {
& > .@{notificationPrefixCls}-notice {
&-wrapper {
transition: all 0.3s;
position: absolute;
top: 12px;
opacity: 1;
&:not(:nth-last-child(-n + 3)) {
opacity: 0;
right: 34px;
width: 252px;
overflow: hidden;
color: transparent;
pointer-events: none;
}
&:nth-last-child(1) {
right: 10px;
}
&:nth-last-child(2) {
right: 18px;
width: 284px;
color: transparent;
overflow: hidden;
}
&:nth-last-child(3) {
right: 26px;
width: 268px;
color: transparent;
overflow: hidden;
}
}
}
&&-expanded {
& > .@{notificationPrefixCls}-notice {
&-wrapper {
&:not(:nth-last-child(-n + 1)) {
opacity: 1;
width: 300px;
right: 10px;
overflow: unset;
color: inherit;
pointer-events: auto;
}
&::before {
content: "";
position: absolute;
left: 0;
right: 0;
top: -16px;
width: 100%;
height: calc(100% + 32px);
background: transparent;
pointer-events: auto;
color: rgb(0,0,0);
}
}
}
}
&.@{notificationPrefixCls}-bottomRight {
& > .@{notificationPrefixCls}-notice-wrapper {
top: unset;
bottom: 12px;
}
}
}
}
+15
View File
@@ -0,0 +1,15 @@
import * as React from 'react';
import type { NoticeConfig } from './interface';
export interface NoticeProps extends Omit<NoticeConfig, 'onClose'> {
prefixCls: string;
className?: string;
style?: React.CSSProperties;
eventKey: React.Key;
onClick?: React.MouseEventHandler<HTMLDivElement>;
onNoticeClose?: (key: React.Key) => void;
hovering?: boolean;
}
declare const Notify: React.ForwardRefExoticComponent<NoticeProps & {
times?: number;
} & React.RefAttributes<HTMLDivElement>>;
export default Notify;
+128
View File
@@ -0,0 +1,128 @@
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
import { clsx } from 'clsx';
import KeyCode from "@rc-component/util/es/KeyCode";
import * as React from 'react';
import pickAttrs from "@rc-component/util/es/pickAttrs";
const Notify = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
prefixCls,
style,
className,
duration = 4.5,
showProgress,
pauseOnHover = true,
eventKey,
content,
closable,
props: divProps,
onClick,
onNoticeClose,
times,
hovering: forcedHovering
} = props;
const [hovering, setHovering] = React.useState(false);
const [percent, setPercent] = React.useState(0);
const [spentTime, setSpentTime] = React.useState(0);
const mergedHovering = forcedHovering || hovering;
const mergedDuration = typeof duration === 'number' ? duration : 0;
const mergedShowProgress = mergedDuration > 0 && showProgress;
// ======================== Close =========================
const onInternalClose = () => {
onNoticeClose(eventKey);
};
const onCloseKeyDown = e => {
if (e.key === 'Enter' || e.code === 'Enter' || e.keyCode === KeyCode.ENTER) {
onInternalClose();
}
};
// ======================== Effect ========================
React.useEffect(() => {
if (!mergedHovering && mergedDuration > 0) {
const start = Date.now() - spentTime;
const timeout = setTimeout(() => {
onInternalClose();
}, mergedDuration * 1000 - spentTime);
return () => {
if (pauseOnHover) {
clearTimeout(timeout);
}
setSpentTime(Date.now() - start);
};
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mergedDuration, mergedHovering, times]);
React.useEffect(() => {
if (!mergedHovering && mergedShowProgress && (pauseOnHover || spentTime === 0)) {
const start = performance.now();
let animationFrame;
const calculate = () => {
cancelAnimationFrame(animationFrame);
animationFrame = requestAnimationFrame(timestamp => {
const runtime = timestamp + spentTime - start;
const progress = Math.min(runtime / (mergedDuration * 1000), 1);
setPercent(progress * 100);
if (progress < 1) {
calculate();
}
});
};
calculate();
return () => {
if (pauseOnHover) {
cancelAnimationFrame(animationFrame);
}
};
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mergedDuration, spentTime, mergedHovering, mergedShowProgress, times]);
// ======================== Closable ========================
const closableObj = React.useMemo(() => {
if (typeof closable === 'object' && closable !== null) {
return closable;
}
return {};
}, [closable]);
const ariaProps = pickAttrs(closableObj, true);
// ======================== Progress ========================
const validPercent = 100 - (!percent || percent < 0 ? 0 : percent > 100 ? 100 : percent);
// ======================== Render ========================
const noticePrefixCls = `${prefixCls}-notice`;
return /*#__PURE__*/React.createElement("div", _extends({}, divProps, {
ref: ref,
className: clsx(noticePrefixCls, className, {
[`${noticePrefixCls}-closable`]: closable
}),
style: style,
onMouseEnter: e => {
setHovering(true);
divProps?.onMouseEnter?.(e);
},
onMouseLeave: e => {
setHovering(false);
divProps?.onMouseLeave?.(e);
},
onClick: onClick
}), /*#__PURE__*/React.createElement("div", {
className: `${noticePrefixCls}-content`
}, content), closable && /*#__PURE__*/React.createElement("button", _extends({
className: `${noticePrefixCls}-close`,
onKeyDown: onCloseKeyDown,
"aria-label": "Close"
}, ariaProps, {
onClick: e => {
e.preventDefault();
e.stopPropagation();
onInternalClose();
}
}), closableObj.closeIcon ?? 'x'), mergedShowProgress && /*#__PURE__*/React.createElement("progress", {
className: `${noticePrefixCls}-progress`,
max: "100",
value: validPercent
}, validPercent + '%'));
});
export default Notify;
@@ -0,0 +1,17 @@
import type { CSSProperties, FC } from 'react';
import React from 'react';
import type { CSSMotionProps } from '@rc-component/motion';
import type { OpenConfig, Placement, StackConfig } from './interface';
export interface NoticeListProps {
configList?: OpenConfig[];
placement?: Placement;
prefixCls?: string;
motion?: CSSMotionProps | ((placement: Placement) => CSSMotionProps);
stack?: StackConfig;
onAllNoticeRemoved?: (placement: Placement) => void;
onNoticeClose?: (key: React.Key) => void;
className?: string;
style?: CSSProperties;
}
declare const NoticeList: FC<NoticeListProps>;
export default NoticeList;
@@ -0,0 +1,141 @@
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
import React, { useContext, useEffect, useRef, useState } from 'react';
import { clsx } from 'clsx';
import { CSSMotionList } from '@rc-component/motion';
import Notice from "./Notice";
import { NotificationContext } from "./NotificationProvider";
import useStack from "./hooks/useStack";
const NoticeList = props => {
const {
configList,
placement,
prefixCls,
className,
style,
motion,
onAllNoticeRemoved,
onNoticeClose,
stack: stackConfig
} = props;
const {
classNames: ctxCls
} = useContext(NotificationContext);
const dictRef = useRef({});
const [latestNotice, setLatestNotice] = useState(null);
const [hoverKeys, setHoverKeys] = useState([]);
const keys = configList.map(config => ({
config,
key: String(config.key)
}));
const [stack, {
offset,
threshold,
gap
}] = useStack(stackConfig);
const expanded = stack && (hoverKeys.length > 0 || keys.length <= threshold);
const placementMotion = typeof motion === 'function' ? motion(placement) : motion;
// Clean hover key
useEffect(() => {
if (stack && hoverKeys.length > 1) {
setHoverKeys(prev => prev.filter(key => keys.some(({
key: dataKey
}) => key === dataKey)));
}
}, [hoverKeys, keys, stack]);
// Force update latest notice
useEffect(() => {
if (stack && dictRef.current[keys[keys.length - 1]?.key]) {
setLatestNotice(dictRef.current[keys[keys.length - 1]?.key]);
}
}, [keys, stack]);
return /*#__PURE__*/React.createElement(CSSMotionList, _extends({
key: placement,
className: clsx(prefixCls, `${prefixCls}-${placement}`, ctxCls?.list, className, {
[`${prefixCls}-stack`]: !!stack,
[`${prefixCls}-stack-expanded`]: expanded
}),
style: style,
keys: keys,
motionAppear: true
}, placementMotion, {
onAllRemoved: () => {
onAllNoticeRemoved(placement);
}
}), ({
config,
className: motionClassName,
style: motionStyle,
index: motionIndex
}, nodeRef) => {
const {
key,
times
} = config;
const strKey = String(key);
const {
className: configClassName,
style: configStyle,
classNames: configClassNames,
styles: configStyles,
...restConfig
} = config;
const dataIndex = keys.findIndex(item => item.key === strKey);
// If dataIndex is -1, that means this notice has been removed in data, but still in dom
// Should minus (motionIndex - 1) to get the correct index because keys.length is not the same as dom length
const stackStyle = {};
if (stack) {
const index = keys.length - 1 - (dataIndex > -1 ? dataIndex : motionIndex - 1);
const transformX = placement === 'top' || placement === 'bottom' ? '-50%' : '0';
if (index > 0) {
stackStyle.height = expanded ? dictRef.current[strKey]?.offsetHeight : latestNotice?.offsetHeight;
// Transform
let verticalOffset = 0;
for (let i = 0; i < index; i++) {
verticalOffset += dictRef.current[keys[keys.length - 1 - i].key]?.offsetHeight + gap;
}
const transformY = (expanded ? verticalOffset : index * offset) * (placement.startsWith('top') ? 1 : -1);
const scaleX = !expanded && latestNotice?.offsetWidth && dictRef.current[strKey]?.offsetWidth ? (latestNotice?.offsetWidth - offset * 2 * (index < 3 ? index : 3)) / dictRef.current[strKey]?.offsetWidth : 1;
stackStyle.transform = `translate3d(${transformX}, ${transformY}px, 0) scaleX(${scaleX})`;
} else {
stackStyle.transform = `translate3d(${transformX}, 0, 0)`;
}
}
return /*#__PURE__*/React.createElement("div", {
ref: nodeRef,
className: clsx(`${prefixCls}-notice-wrapper`, motionClassName, configClassNames?.wrapper),
style: {
...motionStyle,
...stackStyle,
...configStyles?.wrapper
},
onMouseEnter: () => setHoverKeys(prev => prev.includes(strKey) ? prev : [...prev, strKey]),
onMouseLeave: () => setHoverKeys(prev => prev.filter(k => k !== strKey))
}, /*#__PURE__*/React.createElement(Notice, _extends({}, restConfig, {
ref: node => {
if (dataIndex > -1) {
dictRef.current[strKey] = node;
} else {
delete dictRef.current[strKey];
}
},
prefixCls: prefixCls,
classNames: configClassNames,
styles: configStyles,
className: clsx(configClassName, ctxCls?.notice),
style: configStyle,
times: times,
key: key,
eventKey: key,
onNoticeClose: onNoticeClose,
hovering: stack && hoverKeys.length > 0
})));
});
};
if (process.env.NODE_ENV !== 'production') {
NoticeList.displayName = 'NoticeList';
}
export default NoticeList;
@@ -0,0 +1,14 @@
import type { FC } from 'react';
import React from 'react';
export interface NotificationContextProps {
classNames?: {
notice?: string;
list?: string;
};
}
export declare const NotificationContext: React.Context<NotificationContextProps>;
export interface NotificationProviderProps extends NotificationContextProps {
children: React.ReactNode;
}
declare const NotificationProvider: FC<NotificationProviderProps>;
export default NotificationProvider;
@@ -0,0 +1,13 @@
import React from 'react';
export const NotificationContext = /*#__PURE__*/React.createContext({});
const NotificationProvider = ({
children,
classNames
}) => {
return /*#__PURE__*/React.createElement(NotificationContext.Provider, {
value: {
classNames
}
}, children);
};
export default NotificationProvider;
@@ -0,0 +1,25 @@
import * as React from 'react';
import type { ReactElement } from 'react';
import type { CSSMotionProps } from '@rc-component/motion';
import type { OpenConfig, Placement, StackConfig } from './interface';
export interface NotificationsProps {
prefixCls?: string;
motion?: CSSMotionProps | ((placement: Placement) => CSSMotionProps);
container?: HTMLElement | ShadowRoot;
maxCount?: number;
className?: (placement: Placement) => string;
style?: (placement: Placement) => React.CSSProperties;
onAllRemoved?: VoidFunction;
stack?: StackConfig;
renderNotifications?: (node: ReactElement, info: {
prefixCls: string;
key: React.Key;
}) => ReactElement;
}
export interface NotificationsRef {
open: (config: OpenConfig) => void;
close: (key: React.Key) => void;
destroy: () => void;
}
declare const Notifications: React.ForwardRefExoticComponent<NotificationsProps & React.RefAttributes<NotificationsRef>>;
export default Notifications;
@@ -0,0 +1,139 @@
import * as React from 'react';
import { createPortal } from 'react-dom';
import NoticeList from "./NoticeList";
// ant-notification ant-notification-topRight
const Notifications = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
prefixCls = 'rc-notification',
container,
motion,
maxCount,
className,
style,
onAllRemoved,
stack,
renderNotifications
} = props;
const [configList, setConfigList] = React.useState([]);
// ======================== Close =========================
const onNoticeClose = key => {
// Trigger close event
const config = configList.find(item => item.key === key);
const closable = config?.closable;
const closableObj = closable && typeof closable === 'object' ? closable : {};
const {
onClose: closableOnClose
} = closableObj;
closableOnClose?.();
config?.onClose?.();
setConfigList(list => list.filter(item => item.key !== key));
};
// ========================= Refs =========================
React.useImperativeHandle(ref, () => ({
open: config => {
setConfigList(list => {
let clone = [...list];
// Replace if exist
const index = clone.findIndex(item => item.key === config.key);
const innerConfig = {
...config
};
if (index >= 0) {
innerConfig.times = (list[index]?.times || 0) + 1;
clone[index] = innerConfig;
} else {
innerConfig.times = 0;
clone.push(innerConfig);
}
if (maxCount > 0 && clone.length > maxCount) {
clone = clone.slice(-maxCount);
}
return clone;
});
},
close: key => {
onNoticeClose(key);
},
destroy: () => {
setConfigList([]);
}
}));
// ====================== Placements ======================
const [placements, setPlacements] = React.useState({});
React.useEffect(() => {
const nextPlacements = {};
configList.forEach(config => {
const {
placement = 'topRight'
} = config;
if (placement) {
nextPlacements[placement] = nextPlacements[placement] || [];
nextPlacements[placement].push(config);
}
});
// Fill exist placements to avoid empty list causing remove without motion
Object.keys(placements).forEach(placement => {
nextPlacements[placement] = nextPlacements[placement] || [];
});
setPlacements(nextPlacements);
}, [configList]);
// Clean up container if all notices fade out
const onAllNoticeRemoved = placement => {
setPlacements(originPlacements => {
const clone = {
...originPlacements
};
const list = clone[placement] || [];
if (!list.length) {
delete clone[placement];
}
return clone;
});
};
// Effect tell that placements is empty now
const emptyRef = React.useRef(false);
React.useEffect(() => {
if (Object.keys(placements).length > 0) {
emptyRef.current = true;
} else if (emptyRef.current) {
// Trigger only when from exist to empty
onAllRemoved?.();
emptyRef.current = false;
}
}, [placements]);
// ======================== Render ========================
if (!container) {
return null;
}
const placementList = Object.keys(placements);
return /*#__PURE__*/createPortal( /*#__PURE__*/React.createElement(React.Fragment, null, placementList.map(placement => {
const placementConfigList = placements[placement];
const list = /*#__PURE__*/React.createElement(NoticeList, {
key: placement,
configList: placementConfigList,
placement: placement,
prefixCls: prefixCls,
className: className?.(placement),
style: style?.(placement),
motion: motion,
onNoticeClose: onNoticeClose,
onAllNoticeRemoved: onAllNoticeRemoved,
stack: stack
});
return renderNotifications ? renderNotifications(list, {
prefixCls,
key: placement
}) : list;
})), container);
});
if (process.env.NODE_ENV !== 'production') {
Notifications.displayName = 'Notifications';
}
export default Notifications;
@@ -0,0 +1,35 @@
import type { CSSMotionProps } from '@rc-component/motion';
import * as React from 'react';
import type { NotificationsProps } from '../Notifications';
import type { OpenConfig, Placement, StackConfig } from '../interface';
type OptionalConfig = Partial<OpenConfig>;
export interface NotificationConfig {
prefixCls?: string;
/** Customize container. It will repeat call which means you should return same container element. */
getContainer?: () => HTMLElement | ShadowRoot;
motion?: CSSMotionProps | ((placement: Placement) => CSSMotionProps);
closable?: boolean | ({
closeIcon?: React.ReactNode;
onClose?: VoidFunction;
} & React.AriaAttributes);
maxCount?: number;
duration?: number | false | null;
showProgress?: boolean;
pauseOnHover?: boolean;
/** @private. Config for notification holder style. Safe to remove if refactor */
className?: (placement: Placement) => string;
/** @private. Config for notification holder style. Safe to remove if refactor */
style?: (placement: Placement) => React.CSSProperties;
/** @private Trigger when all the notification closed. */
onAllRemoved?: VoidFunction;
stack?: StackConfig;
/** @private Slot for style in Notifications */
renderNotifications?: NotificationsProps['renderNotifications'];
}
export interface NotificationAPI {
open: (config: OptionalConfig) => void;
close: (key: React.Key) => void;
destroy: () => void;
}
export default function useNotification(rootConfig?: NotificationConfig): [NotificationAPI, React.ReactElement];
export {};
@@ -0,0 +1,125 @@
import * as React from 'react';
import Notifications from "../Notifications";
import { useEvent } from '@rc-component/util';
const defaultGetContainer = () => document.body;
let uniqueKey = 0;
function mergeConfig(...objList) {
const clone = {};
objList.forEach(obj => {
if (obj) {
Object.keys(obj).forEach(key => {
const val = obj[key];
if (val !== undefined) {
clone[key] = val;
}
});
}
});
return clone;
}
export default function useNotification(rootConfig = {}) {
const {
getContainer = defaultGetContainer,
motion,
prefixCls,
maxCount,
className,
style,
onAllRemoved,
stack,
renderNotifications,
...shareConfig
} = rootConfig;
const [container, setContainer] = React.useState();
const notificationsRef = React.useRef();
const contextHolder = /*#__PURE__*/React.createElement(Notifications, {
container: container,
ref: notificationsRef,
prefixCls: prefixCls,
motion: motion,
maxCount: maxCount,
className: className,
style: style,
onAllRemoved: onAllRemoved,
stack: stack,
renderNotifications: renderNotifications
});
const [taskQueue, setTaskQueue] = React.useState([]);
const open = useEvent(config => {
const mergedConfig = mergeConfig(shareConfig, config);
if (mergedConfig.key === null || mergedConfig.key === undefined) {
mergedConfig.key = `rc-notification-${uniqueKey}`;
uniqueKey += 1;
}
setTaskQueue(queue => [...queue, {
type: 'open',
config: mergedConfig
}]);
});
// ========================= Refs =========================
const api = React.useMemo(() => ({
open: open,
close: key => {
setTaskQueue(queue => [...queue, {
type: 'close',
key
}]);
},
destroy: () => {
setTaskQueue(queue => [...queue, {
type: 'destroy'
}]);
}
}), []);
// ======================= Container ======================
// React 18 should all in effect that we will check container in each render
// Which means getContainer should be stable.
React.useEffect(() => {
setContainer(getContainer());
});
// ======================== Effect ========================
React.useEffect(() => {
// Flush task when node ready
if (notificationsRef.current && taskQueue.length) {
taskQueue.forEach(task => {
switch (task.type) {
case 'open':
notificationsRef.current.open(task.config);
break;
case 'close':
notificationsRef.current.close(task.key);
break;
case 'destroy':
notificationsRef.current.destroy();
break;
}
});
// https://github.com/ant-design/ant-design/issues/52590
// React `startTransition` will run once `useEffect` but many times `setState`,
// So `setTaskQueue` with filtered array will cause infinite loop.
// We cache the first match queue instead.
let oriTaskQueue;
let tgtTaskQueue;
// React 17 will mix order of effect & setState in async
// - open: setState[0]
// - effect[0]
// - open: setState[1]
// - effect setState([]) * here will clean up [0, 1] in React 17
setTaskQueue(oriQueue => {
if (oriTaskQueue !== oriQueue || !tgtTaskQueue) {
oriTaskQueue = oriQueue;
tgtTaskQueue = oriQueue.filter(task => !taskQueue.includes(task));
}
return tgtTaskQueue;
});
}
}, [taskQueue]);
// ======================== Return ========================
return [api, contextHolder];
}
@@ -0,0 +1,5 @@
import type { StackConfig } from '../interface';
type StackParams = Exclude<StackConfig, boolean>;
type UseStack = (config?: StackConfig) => [boolean, StackParams];
declare const useStack: UseStack;
export default useStack;
@@ -0,0 +1,17 @@
const DEFAULT_OFFSET = 8;
const DEFAULT_THRESHOLD = 3;
const DEFAULT_GAP = 16;
const useStack = config => {
const result = {
offset: DEFAULT_OFFSET,
threshold: DEFAULT_THRESHOLD,
gap: DEFAULT_GAP
};
if (config && typeof config === 'object') {
result.offset = config.offset ?? DEFAULT_OFFSET;
result.threshold = config.threshold ?? DEFAULT_THRESHOLD;
result.gap = config.gap ?? DEFAULT_GAP;
}
return [!!config, result];
};
export default useStack;
@@ -0,0 +1,6 @@
import useNotification from './hooks/useNotification';
import Notice from './Notice';
import type { NotificationAPI, NotificationConfig } from './hooks/useNotification';
import NotificationProvider from './NotificationProvider';
export { useNotification, Notice, NotificationProvider };
export type { NotificationAPI, NotificationConfig };
+4
View File
@@ -0,0 +1,4 @@
import useNotification from "./hooks/useNotification";
import Notice from "./Notice";
import NotificationProvider from "./NotificationProvider";
export { useNotification, Notice, NotificationProvider };
@@ -0,0 +1,52 @@
import type React from 'react';
export type Placement = 'top' | 'topLeft' | 'topRight' | 'bottom' | 'bottomLeft' | 'bottomRight';
type NoticeSemanticProps = 'wrapper';
export interface NoticeConfig {
content?: React.ReactNode;
duration?: number | false | null;
showProgress?: boolean;
pauseOnHover?: boolean;
closable?: boolean | ({
closeIcon?: React.ReactNode;
onClose?: VoidFunction;
} & React.AriaAttributes);
className?: string;
style?: React.CSSProperties;
classNames?: {
[key in NoticeSemanticProps]?: string;
};
styles?: {
[key in NoticeSemanticProps]?: 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 | false | null;
}
export type InnerOpenConfig = OpenConfig & {
times?: number;
};
export type Placements = Partial<Record<Placement, OpenConfig[]>>;
export type StackConfig = boolean | {
/**
* When number is greater than threshold, notifications will be stacked together.
* @default 3
*/
threshold?: number;
/**
* Offset when notifications are stacked together.
* @default 8
*/
offset?: number;
/**
* Spacing between each notification when expanded.
*/
gap?: number;
};
export {};
@@ -0,0 +1 @@
export {};
@@ -0,0 +1,15 @@
import * as React from 'react';
import type { NoticeConfig } from './interface';
export interface NoticeProps extends Omit<NoticeConfig, 'onClose'> {
prefixCls: string;
className?: string;
style?: React.CSSProperties;
eventKey: React.Key;
onClick?: React.MouseEventHandler<HTMLDivElement>;
onNoticeClose?: (key: React.Key) => void;
hovering?: boolean;
}
declare const Notify: React.ForwardRefExoticComponent<NoticeProps & {
times?: number;
} & React.RefAttributes<HTMLDivElement>>;
export default Notify;
+137
View File
@@ -0,0 +1,137 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _clsx = require("clsx");
var _KeyCode = _interopRequireDefault(require("@rc-component/util/lib/KeyCode"));
var React = _interopRequireWildcard(require("react"));
var _pickAttrs = _interopRequireDefault(require("@rc-component/util/lib/pickAttrs"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
const Notify = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
prefixCls,
style,
className,
duration = 4.5,
showProgress,
pauseOnHover = true,
eventKey,
content,
closable,
props: divProps,
onClick,
onNoticeClose,
times,
hovering: forcedHovering
} = props;
const [hovering, setHovering] = React.useState(false);
const [percent, setPercent] = React.useState(0);
const [spentTime, setSpentTime] = React.useState(0);
const mergedHovering = forcedHovering || hovering;
const mergedDuration = typeof duration === 'number' ? duration : 0;
const mergedShowProgress = mergedDuration > 0 && showProgress;
// ======================== Close =========================
const onInternalClose = () => {
onNoticeClose(eventKey);
};
const onCloseKeyDown = e => {
if (e.key === 'Enter' || e.code === 'Enter' || e.keyCode === _KeyCode.default.ENTER) {
onInternalClose();
}
};
// ======================== Effect ========================
React.useEffect(() => {
if (!mergedHovering && mergedDuration > 0) {
const start = Date.now() - spentTime;
const timeout = setTimeout(() => {
onInternalClose();
}, mergedDuration * 1000 - spentTime);
return () => {
if (pauseOnHover) {
clearTimeout(timeout);
}
setSpentTime(Date.now() - start);
};
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mergedDuration, mergedHovering, times]);
React.useEffect(() => {
if (!mergedHovering && mergedShowProgress && (pauseOnHover || spentTime === 0)) {
const start = performance.now();
let animationFrame;
const calculate = () => {
cancelAnimationFrame(animationFrame);
animationFrame = requestAnimationFrame(timestamp => {
const runtime = timestamp + spentTime - start;
const progress = Math.min(runtime / (mergedDuration * 1000), 1);
setPercent(progress * 100);
if (progress < 1) {
calculate();
}
});
};
calculate();
return () => {
if (pauseOnHover) {
cancelAnimationFrame(animationFrame);
}
};
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mergedDuration, spentTime, mergedHovering, mergedShowProgress, times]);
// ======================== Closable ========================
const closableObj = React.useMemo(() => {
if (typeof closable === 'object' && closable !== null) {
return closable;
}
return {};
}, [closable]);
const ariaProps = (0, _pickAttrs.default)(closableObj, true);
// ======================== Progress ========================
const validPercent = 100 - (!percent || percent < 0 ? 0 : percent > 100 ? 100 : percent);
// ======================== Render ========================
const noticePrefixCls = `${prefixCls}-notice`;
return /*#__PURE__*/React.createElement("div", _extends({}, divProps, {
ref: ref,
className: (0, _clsx.clsx)(noticePrefixCls, className, {
[`${noticePrefixCls}-closable`]: closable
}),
style: style,
onMouseEnter: e => {
setHovering(true);
divProps?.onMouseEnter?.(e);
},
onMouseLeave: e => {
setHovering(false);
divProps?.onMouseLeave?.(e);
},
onClick: onClick
}), /*#__PURE__*/React.createElement("div", {
className: `${noticePrefixCls}-content`
}, content), closable && /*#__PURE__*/React.createElement("button", _extends({
className: `${noticePrefixCls}-close`,
onKeyDown: onCloseKeyDown,
"aria-label": "Close"
}, ariaProps, {
onClick: e => {
e.preventDefault();
e.stopPropagation();
onInternalClose();
}
}), closableObj.closeIcon ?? 'x'), mergedShowProgress && /*#__PURE__*/React.createElement("progress", {
className: `${noticePrefixCls}-progress`,
max: "100",
value: validPercent
}, validPercent + '%'));
});
var _default = exports.default = Notify;
@@ -0,0 +1,17 @@
import type { CSSProperties, FC } from 'react';
import React from 'react';
import type { CSSMotionProps } from '@rc-component/motion';
import type { OpenConfig, Placement, StackConfig } from './interface';
export interface NoticeListProps {
configList?: OpenConfig[];
placement?: Placement;
prefixCls?: string;
motion?: CSSMotionProps | ((placement: Placement) => CSSMotionProps);
stack?: StackConfig;
onAllNoticeRemoved?: (placement: Placement) => void;
onNoticeClose?: (key: React.Key) => void;
className?: string;
style?: CSSProperties;
}
declare const NoticeList: FC<NoticeListProps>;
export default NoticeList;
@@ -0,0 +1,150 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _react = _interopRequireWildcard(require("react"));
var _clsx = require("clsx");
var _motion = require("@rc-component/motion");
var _Notice = _interopRequireDefault(require("./Notice"));
var _NotificationProvider = require("./NotificationProvider");
var _useStack = _interopRequireDefault(require("./hooks/useStack"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
const NoticeList = props => {
const {
configList,
placement,
prefixCls,
className,
style,
motion,
onAllNoticeRemoved,
onNoticeClose,
stack: stackConfig
} = props;
const {
classNames: ctxCls
} = (0, _react.useContext)(_NotificationProvider.NotificationContext);
const dictRef = (0, _react.useRef)({});
const [latestNotice, setLatestNotice] = (0, _react.useState)(null);
const [hoverKeys, setHoverKeys] = (0, _react.useState)([]);
const keys = configList.map(config => ({
config,
key: String(config.key)
}));
const [stack, {
offset,
threshold,
gap
}] = (0, _useStack.default)(stackConfig);
const expanded = stack && (hoverKeys.length > 0 || keys.length <= threshold);
const placementMotion = typeof motion === 'function' ? motion(placement) : motion;
// Clean hover key
(0, _react.useEffect)(() => {
if (stack && hoverKeys.length > 1) {
setHoverKeys(prev => prev.filter(key => keys.some(({
key: dataKey
}) => key === dataKey)));
}
}, [hoverKeys, keys, stack]);
// Force update latest notice
(0, _react.useEffect)(() => {
if (stack && dictRef.current[keys[keys.length - 1]?.key]) {
setLatestNotice(dictRef.current[keys[keys.length - 1]?.key]);
}
}, [keys, stack]);
return /*#__PURE__*/_react.default.createElement(_motion.CSSMotionList, _extends({
key: placement,
className: (0, _clsx.clsx)(prefixCls, `${prefixCls}-${placement}`, ctxCls?.list, className, {
[`${prefixCls}-stack`]: !!stack,
[`${prefixCls}-stack-expanded`]: expanded
}),
style: style,
keys: keys,
motionAppear: true
}, placementMotion, {
onAllRemoved: () => {
onAllNoticeRemoved(placement);
}
}), ({
config,
className: motionClassName,
style: motionStyle,
index: motionIndex
}, nodeRef) => {
const {
key,
times
} = config;
const strKey = String(key);
const {
className: configClassName,
style: configStyle,
classNames: configClassNames,
styles: configStyles,
...restConfig
} = config;
const dataIndex = keys.findIndex(item => item.key === strKey);
// If dataIndex is -1, that means this notice has been removed in data, but still in dom
// Should minus (motionIndex - 1) to get the correct index because keys.length is not the same as dom length
const stackStyle = {};
if (stack) {
const index = keys.length - 1 - (dataIndex > -1 ? dataIndex : motionIndex - 1);
const transformX = placement === 'top' || placement === 'bottom' ? '-50%' : '0';
if (index > 0) {
stackStyle.height = expanded ? dictRef.current[strKey]?.offsetHeight : latestNotice?.offsetHeight;
// Transform
let verticalOffset = 0;
for (let i = 0; i < index; i++) {
verticalOffset += dictRef.current[keys[keys.length - 1 - i].key]?.offsetHeight + gap;
}
const transformY = (expanded ? verticalOffset : index * offset) * (placement.startsWith('top') ? 1 : -1);
const scaleX = !expanded && latestNotice?.offsetWidth && dictRef.current[strKey]?.offsetWidth ? (latestNotice?.offsetWidth - offset * 2 * (index < 3 ? index : 3)) / dictRef.current[strKey]?.offsetWidth : 1;
stackStyle.transform = `translate3d(${transformX}, ${transformY}px, 0) scaleX(${scaleX})`;
} else {
stackStyle.transform = `translate3d(${transformX}, 0, 0)`;
}
}
return /*#__PURE__*/_react.default.createElement("div", {
ref: nodeRef,
className: (0, _clsx.clsx)(`${prefixCls}-notice-wrapper`, motionClassName, configClassNames?.wrapper),
style: {
...motionStyle,
...stackStyle,
...configStyles?.wrapper
},
onMouseEnter: () => setHoverKeys(prev => prev.includes(strKey) ? prev : [...prev, strKey]),
onMouseLeave: () => setHoverKeys(prev => prev.filter(k => k !== strKey))
}, /*#__PURE__*/_react.default.createElement(_Notice.default, _extends({}, restConfig, {
ref: node => {
if (dataIndex > -1) {
dictRef.current[strKey] = node;
} else {
delete dictRef.current[strKey];
}
},
prefixCls: prefixCls,
classNames: configClassNames,
styles: configStyles,
className: (0, _clsx.clsx)(configClassName, ctxCls?.notice),
style: configStyle,
times: times,
key: key,
eventKey: key,
onNoticeClose: onNoticeClose,
hovering: stack && hoverKeys.length > 0
})));
});
};
if (process.env.NODE_ENV !== 'production') {
NoticeList.displayName = 'NoticeList';
}
var _default = exports.default = NoticeList;
@@ -0,0 +1,14 @@
import type { FC } from 'react';
import React from 'react';
export interface NotificationContextProps {
classNames?: {
notice?: string;
list?: string;
};
}
export declare const NotificationContext: React.Context<NotificationContextProps>;
export interface NotificationProviderProps extends NotificationContextProps {
children: React.ReactNode;
}
declare const NotificationProvider: FC<NotificationProviderProps>;
export default NotificationProvider;
@@ -0,0 +1,20 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = exports.NotificationContext = void 0;
var _react = _interopRequireDefault(require("react"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
const NotificationContext = exports.NotificationContext = /*#__PURE__*/_react.default.createContext({});
const NotificationProvider = ({
children,
classNames
}) => {
return /*#__PURE__*/_react.default.createElement(NotificationContext.Provider, {
value: {
classNames
}
}, children);
};
var _default = exports.default = NotificationProvider;
@@ -0,0 +1,25 @@
import * as React from 'react';
import type { ReactElement } from 'react';
import type { CSSMotionProps } from '@rc-component/motion';
import type { OpenConfig, Placement, StackConfig } from './interface';
export interface NotificationsProps {
prefixCls?: string;
motion?: CSSMotionProps | ((placement: Placement) => CSSMotionProps);
container?: HTMLElement | ShadowRoot;
maxCount?: number;
className?: (placement: Placement) => string;
style?: (placement: Placement) => React.CSSProperties;
onAllRemoved?: VoidFunction;
stack?: StackConfig;
renderNotifications?: (node: ReactElement, info: {
prefixCls: string;
key: React.Key;
}) => ReactElement;
}
export interface NotificationsRef {
open: (config: OpenConfig) => void;
close: (key: React.Key) => void;
destroy: () => void;
}
declare const Notifications: React.ForwardRefExoticComponent<NotificationsProps & React.RefAttributes<NotificationsRef>>;
export default Notifications;
@@ -0,0 +1,148 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var React = _interopRequireWildcard(require("react"));
var _reactDom = require("react-dom");
var _NoticeList = _interopRequireDefault(require("./NoticeList"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
// ant-notification ant-notification-topRight
const Notifications = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
prefixCls = 'rc-notification',
container,
motion,
maxCount,
className,
style,
onAllRemoved,
stack,
renderNotifications
} = props;
const [configList, setConfigList] = React.useState([]);
// ======================== Close =========================
const onNoticeClose = key => {
// Trigger close event
const config = configList.find(item => item.key === key);
const closable = config?.closable;
const closableObj = closable && typeof closable === 'object' ? closable : {};
const {
onClose: closableOnClose
} = closableObj;
closableOnClose?.();
config?.onClose?.();
setConfigList(list => list.filter(item => item.key !== key));
};
// ========================= Refs =========================
React.useImperativeHandle(ref, () => ({
open: config => {
setConfigList(list => {
let clone = [...list];
// Replace if exist
const index = clone.findIndex(item => item.key === config.key);
const innerConfig = {
...config
};
if (index >= 0) {
innerConfig.times = (list[index]?.times || 0) + 1;
clone[index] = innerConfig;
} else {
innerConfig.times = 0;
clone.push(innerConfig);
}
if (maxCount > 0 && clone.length > maxCount) {
clone = clone.slice(-maxCount);
}
return clone;
});
},
close: key => {
onNoticeClose(key);
},
destroy: () => {
setConfigList([]);
}
}));
// ====================== Placements ======================
const [placements, setPlacements] = React.useState({});
React.useEffect(() => {
const nextPlacements = {};
configList.forEach(config => {
const {
placement = 'topRight'
} = config;
if (placement) {
nextPlacements[placement] = nextPlacements[placement] || [];
nextPlacements[placement].push(config);
}
});
// Fill exist placements to avoid empty list causing remove without motion
Object.keys(placements).forEach(placement => {
nextPlacements[placement] = nextPlacements[placement] || [];
});
setPlacements(nextPlacements);
}, [configList]);
// Clean up container if all notices fade out
const onAllNoticeRemoved = placement => {
setPlacements(originPlacements => {
const clone = {
...originPlacements
};
const list = clone[placement] || [];
if (!list.length) {
delete clone[placement];
}
return clone;
});
};
// Effect tell that placements is empty now
const emptyRef = React.useRef(false);
React.useEffect(() => {
if (Object.keys(placements).length > 0) {
emptyRef.current = true;
} else if (emptyRef.current) {
// Trigger only when from exist to empty
onAllRemoved?.();
emptyRef.current = false;
}
}, [placements]);
// ======================== Render ========================
if (!container) {
return null;
}
const placementList = Object.keys(placements);
return /*#__PURE__*/(0, _reactDom.createPortal)( /*#__PURE__*/React.createElement(React.Fragment, null, placementList.map(placement => {
const placementConfigList = placements[placement];
const list = /*#__PURE__*/React.createElement(_NoticeList.default, {
key: placement,
configList: placementConfigList,
placement: placement,
prefixCls: prefixCls,
className: className?.(placement),
style: style?.(placement),
motion: motion,
onNoticeClose: onNoticeClose,
onAllNoticeRemoved: onAllNoticeRemoved,
stack: stack
});
return renderNotifications ? renderNotifications(list, {
prefixCls,
key: placement
}) : list;
})), container);
});
if (process.env.NODE_ENV !== 'production') {
Notifications.displayName = 'Notifications';
}
var _default = exports.default = Notifications;
@@ -0,0 +1,35 @@
import type { CSSMotionProps } from '@rc-component/motion';
import * as React from 'react';
import type { NotificationsProps } from '../Notifications';
import type { OpenConfig, Placement, StackConfig } from '../interface';
type OptionalConfig = Partial<OpenConfig>;
export interface NotificationConfig {
prefixCls?: string;
/** Customize container. It will repeat call which means you should return same container element. */
getContainer?: () => HTMLElement | ShadowRoot;
motion?: CSSMotionProps | ((placement: Placement) => CSSMotionProps);
closable?: boolean | ({
closeIcon?: React.ReactNode;
onClose?: VoidFunction;
} & React.AriaAttributes);
maxCount?: number;
duration?: number | false | null;
showProgress?: boolean;
pauseOnHover?: boolean;
/** @private. Config for notification holder style. Safe to remove if refactor */
className?: (placement: Placement) => string;
/** @private. Config for notification holder style. Safe to remove if refactor */
style?: (placement: Placement) => React.CSSProperties;
/** @private Trigger when all the notification closed. */
onAllRemoved?: VoidFunction;
stack?: StackConfig;
/** @private Slot for style in Notifications */
renderNotifications?: NotificationsProps['renderNotifications'];
}
export interface NotificationAPI {
open: (config: OptionalConfig) => void;
close: (key: React.Key) => void;
destroy: () => void;
}
export default function useNotification(rootConfig?: NotificationConfig): [NotificationAPI, React.ReactElement];
export {};
@@ -0,0 +1,134 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = useNotification;
var React = _interopRequireWildcard(require("react"));
var _Notifications = _interopRequireDefault(require("../Notifications"));
var _util = require("@rc-component/util");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
const defaultGetContainer = () => document.body;
let uniqueKey = 0;
function mergeConfig(...objList) {
const clone = {};
objList.forEach(obj => {
if (obj) {
Object.keys(obj).forEach(key => {
const val = obj[key];
if (val !== undefined) {
clone[key] = val;
}
});
}
});
return clone;
}
function useNotification(rootConfig = {}) {
const {
getContainer = defaultGetContainer,
motion,
prefixCls,
maxCount,
className,
style,
onAllRemoved,
stack,
renderNotifications,
...shareConfig
} = rootConfig;
const [container, setContainer] = React.useState();
const notificationsRef = React.useRef();
const contextHolder = /*#__PURE__*/React.createElement(_Notifications.default, {
container: container,
ref: notificationsRef,
prefixCls: prefixCls,
motion: motion,
maxCount: maxCount,
className: className,
style: style,
onAllRemoved: onAllRemoved,
stack: stack,
renderNotifications: renderNotifications
});
const [taskQueue, setTaskQueue] = React.useState([]);
const open = (0, _util.useEvent)(config => {
const mergedConfig = mergeConfig(shareConfig, config);
if (mergedConfig.key === null || mergedConfig.key === undefined) {
mergedConfig.key = `rc-notification-${uniqueKey}`;
uniqueKey += 1;
}
setTaskQueue(queue => [...queue, {
type: 'open',
config: mergedConfig
}]);
});
// ========================= Refs =========================
const api = React.useMemo(() => ({
open: open,
close: key => {
setTaskQueue(queue => [...queue, {
type: 'close',
key
}]);
},
destroy: () => {
setTaskQueue(queue => [...queue, {
type: 'destroy'
}]);
}
}), []);
// ======================= Container ======================
// React 18 should all in effect that we will check container in each render
// Which means getContainer should be stable.
React.useEffect(() => {
setContainer(getContainer());
});
// ======================== Effect ========================
React.useEffect(() => {
// Flush task when node ready
if (notificationsRef.current && taskQueue.length) {
taskQueue.forEach(task => {
switch (task.type) {
case 'open':
notificationsRef.current.open(task.config);
break;
case 'close':
notificationsRef.current.close(task.key);
break;
case 'destroy':
notificationsRef.current.destroy();
break;
}
});
// https://github.com/ant-design/ant-design/issues/52590
// React `startTransition` will run once `useEffect` but many times `setState`,
// So `setTaskQueue` with filtered array will cause infinite loop.
// We cache the first match queue instead.
let oriTaskQueue;
let tgtTaskQueue;
// React 17 will mix order of effect & setState in async
// - open: setState[0]
// - effect[0]
// - open: setState[1]
// - effect setState([]) * here will clean up [0, 1] in React 17
setTaskQueue(oriQueue => {
if (oriTaskQueue !== oriQueue || !tgtTaskQueue) {
oriTaskQueue = oriQueue;
tgtTaskQueue = oriQueue.filter(task => !taskQueue.includes(task));
}
return tgtTaskQueue;
});
}
}, [taskQueue]);
// ======================== Return ========================
return [api, contextHolder];
}
@@ -0,0 +1,5 @@
import type { StackConfig } from '../interface';
type StackParams = Exclude<StackConfig, boolean>;
type UseStack = (config?: StackConfig) => [boolean, StackParams];
declare const useStack: UseStack;
export default useStack;
@@ -0,0 +1,23 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
const DEFAULT_OFFSET = 8;
const DEFAULT_THRESHOLD = 3;
const DEFAULT_GAP = 16;
const useStack = config => {
const result = {
offset: DEFAULT_OFFSET,
threshold: DEFAULT_THRESHOLD,
gap: DEFAULT_GAP
};
if (config && typeof config === 'object') {
result.offset = config.offset ?? DEFAULT_OFFSET;
result.threshold = config.threshold ?? DEFAULT_THRESHOLD;
result.gap = config.gap ?? DEFAULT_GAP;
}
return [!!config, result];
};
var _default = exports.default = useStack;
@@ -0,0 +1,6 @@
import useNotification from './hooks/useNotification';
import Notice from './Notice';
import type { NotificationAPI, NotificationConfig } from './hooks/useNotification';
import NotificationProvider from './NotificationProvider';
export { useNotification, Notice, NotificationProvider };
export type { NotificationAPI, NotificationConfig };
+27
View File
@@ -0,0 +1,27 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "Notice", {
enumerable: true,
get: function () {
return _Notice.default;
}
});
Object.defineProperty(exports, "NotificationProvider", {
enumerable: true,
get: function () {
return _NotificationProvider.default;
}
});
Object.defineProperty(exports, "useNotification", {
enumerable: true,
get: function () {
return _useNotification.default;
}
});
var _useNotification = _interopRequireDefault(require("./hooks/useNotification"));
var _Notice = _interopRequireDefault(require("./Notice"));
var _NotificationProvider = _interopRequireDefault(require("./NotificationProvider"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
@@ -0,0 +1,52 @@
import type React from 'react';
export type Placement = 'top' | 'topLeft' | 'topRight' | 'bottom' | 'bottomLeft' | 'bottomRight';
type NoticeSemanticProps = 'wrapper';
export interface NoticeConfig {
content?: React.ReactNode;
duration?: number | false | null;
showProgress?: boolean;
pauseOnHover?: boolean;
closable?: boolean | ({
closeIcon?: React.ReactNode;
onClose?: VoidFunction;
} & React.AriaAttributes);
className?: string;
style?: React.CSSProperties;
classNames?: {
[key in NoticeSemanticProps]?: string;
};
styles?: {
[key in NoticeSemanticProps]?: 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 | false | null;
}
export type InnerOpenConfig = OpenConfig & {
times?: number;
};
export type Placements = Partial<Record<Placement, OpenConfig[]>>;
export type StackConfig = boolean | {
/**
* When number is greater than threshold, notifications will be stacked together.
* @default 3
*/
threshold?: number;
/**
* Offset when notifications are stacked together.
* @default 8
*/
offset?: number;
/**
* Spacing between each notification when expanded.
*/
gap?: number;
};
export {};
@@ -0,0 +1,5 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
+92
View File
@@ -0,0 +1,92 @@
{
"name": "@rc-component/notification",
"version": "1.2.0",
"description": "notification ui component for react",
"engines": {
"node": ">=8.x"
},
"keywords": [
"react",
"react-component",
"react-notification",
"notification"
],
"homepage": "http://github.com/react-component/notification",
"maintainers": [
"yiminghe@gmail.com",
"skyking_H@hotmail.com",
"hust2012jiangkai@gmail.com"
],
"files": [
"assets/*.css",
"assets/*.less",
"es",
"lib"
],
"repository": {
"type": "git",
"url": "git@github.com:react-component/notification.git"
},
"bugs": {
"url": "http://github.com/react-component/notification/issues"
},
"license": "MIT",
"main": "lib/index",
"module": "es/index",
"typings": "es/index.d.ts",
"scripts": {
"start": "dumi dev",
"build": "dumi build",
"docs:deploy": "gh-pages -d .doc",
"compile": "father build && lessc assets/index.less assets/index.css",
"prepublishOnly": "npm run compile && rc-np",
"lint": "eslint src/ docs/examples/ --ext .tsx,.ts,.jsx,.js",
"test": "vitest --watch=false",
"test:watch": "vitest",
"coverage": "vitest run --coverage",
"now-build": "npm run build",
"prepare": "husky install"
},
"dependencies": {
"@rc-component/motion": "^1.1.4",
"@rc-component/util": "^1.2.1",
"clsx": "^2.1.1"
},
"devDependencies": {
"@rc-component/father-plugin": "^2.0.4",
"@rc-component/np": "^1.0.3",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^15.0.7",
"@types/node": "^24.5.2",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@types/testing-library__jest-dom": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^5.59.7",
"@typescript-eslint/parser": "^5.59.7",
"@umijs/fabric": "^2.0.0",
"@vitest/coverage-v8": "^0.34.2",
"dumi": "^2.1.0",
"eslint": "^7.8.1",
"father": "^4.0.0",
"gh-pages": "^3.1.0",
"husky": "^8.0.3",
"jsdom": "^24.0.0",
"less": "^4.2.0",
"lint-staged": "^14.0.1",
"prettier": "^3.0.2",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"typescript": "^5.4.5",
"vitest": "^0.34.2"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
},
"lint-staged": {
"**/*.{js,jsx,tsx,ts,md,json}": [
"prettier --write",
"git add"
]
}
}