1
This commit is contained in:
+9
@@ -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
@@ -0,0 +1,194 @@
|
||||
# @rc-component/notification
|
||||
|
||||
React Notification UI Component
|
||||
|
||||
[![NPM version][npm-image]][npm-url] [](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
|
||||
|
||||
[](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 |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- |
|
||||
| [<br>Firefox](http://godban.github.io/browsers-support-badges/) | last 2 versions |
|
||||
| [<br>Chrome](http://godban.github.io/browsers-support-badges/) | last 2 versions |
|
||||
| [<br>Safari](http://godban.github.io/browsers-support-badges/) | last 2 versions |
|
||||
| [<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.
|
||||
+172
@@ -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;
|
||||
}
|
||||
+265
@@ -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
@@ -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
@@ -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;
|
||||
+17
@@ -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;
|
||||
+141
@@ -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;
|
||||
Generated
Vendored
+14
@@ -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;
|
||||
Generated
Vendored
+13
@@ -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;
|
||||
+25
@@ -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;
|
||||
+139
@@ -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;
|
||||
Generated
Vendored
+35
@@ -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 {};
|
||||
Generated
Vendored
+125
@@ -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];
|
||||
}
|
||||
+5
@@ -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;
|
||||
+17
@@ -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;
|
||||
+6
@@ -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
@@ -0,0 +1,4 @@
|
||||
import useNotification from "./hooks/useNotification";
|
||||
import Notice from "./Notice";
|
||||
import NotificationProvider from "./NotificationProvider";
|
||||
export { useNotification, Notice, NotificationProvider };
|
||||
+52
@@ -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 {};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
+15
@@ -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
@@ -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;
|
||||
+17
@@ -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;
|
||||
+150
@@ -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;
|
||||
Generated
Vendored
+14
@@ -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;
|
||||
Generated
Vendored
+20
@@ -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;
|
||||
+25
@@ -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;
|
||||
+148
@@ -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;
|
||||
Generated
Vendored
+35
@@ -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 {};
|
||||
Generated
Vendored
+134
@@ -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];
|
||||
}
|
||||
+5
@@ -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;
|
||||
+23
@@ -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;
|
||||
+6
@@ -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
@@ -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 }; }
|
||||
+52
@@ -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 {};
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
+92
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user