Skip to content

Commit 95c643f

Browse files
committed
feat(ui): add toast component
1 parent 16b1fdb commit 95c643f

21 files changed

Lines changed: 744 additions & 29 deletions

File tree

packages/site/src/app/App.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
22
import { useTranslation } from 'react-i18next';
33
import { useLocation } from 'react-router-dom';
44

5-
import { DRoot, NotificationService } from '@react-devui/ui';
5+
import { DRoot, NotificationService, ToastService } from '@react-devui/ui';
66
import { useAsync } from '@react-devui/ui/hooks';
77

88
import { environment } from '../environments/environment';
@@ -59,6 +59,7 @@ export function App() {
5959
const location = useLocation();
6060
useEffect(() => {
6161
NotificationService.closeAll(false);
62+
ToastService.closeAll(false);
6263
}, [location]);
6364

6465
const contextValue = useMemo<AppContextData>(

packages/site/src/app/styles/_app.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ h3 {
191191
section[id^='Button'],
192192
section[id^='Dropdown'],
193193
section[id^='Tooltip'],
194+
section[id^='Toast'],
194195
section[id^='Notification'] {
195196
.d-button {
196197
margin-right: 8px;

packages/ui/src/components/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ export { DTag } from './tag';
6767
export type { DTextareaProps } from './textarea';
6868
export { DTextarea } from './textarea';
6969

70+
export type { DToastProps } from './toast';
71+
export { ToastService } from './toast';
72+
7073
export type { DTooltipProps } from './tooltip';
7174
export { DTooltip } from './tooltip';
7275

packages/ui/src/components/notification/Notification.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ export function DNotification(props: DNotificationProps & { dVisible: boolean })
128128
'leave-to': {
129129
height: '0',
130130
opacity: '0',
131-
margin: '0',
131+
marginBottom: '0',
132132
transition: 'height 166ms ease-in, opacity 166ms ease-in, margin 166ms ease-in',
133133
},
134134
};

packages/ui/src/components/pagination/Pagination.tsx

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ export function DPagination(props: DPaginationProps) {
119119
}
120120
)}
121121
role="button"
122-
tabIndex={dDisabled ? undefined : 0}
122+
tabIndex={0}
123123
title={t('Previous page')}
124124
aria-disabled={active === 1}
125125
onClick={() => {
@@ -152,7 +152,7 @@ export function DPagination(props: DPaginationProps) {
152152
[`${dPrefix}pagination__item--border`]: !(dCustomRender && dCustomRender.next),
153153
})}
154154
role="button"
155-
tabIndex={dDisabled ? undefined : 0}
155+
tabIndex={0}
156156
title={t('Next page')}
157157
aria-disabled={active === lastPage}
158158
onClick={() => {
@@ -180,7 +180,7 @@ export function DPagination(props: DPaginationProps) {
180180
},
181181
nextNode,
182182
];
183-
}, [active, changeActive, dCompose, dCustomRender, dDisabled, dPrefix, lastPage, t]);
183+
}, [active, changeActive, dCompose, dCustomRender, dPrefix, lastPage, t]);
184184

185185
const sizeNode = useMemo(() => {
186186
const options = dPageSizeOptions.map((size) => ({
@@ -251,6 +251,25 @@ export function DPagination(props: DPaginationProps) {
251251
return null;
252252
}, [changeActive, dCompose, dCustomRender, dDisabled, dMini, dPrefix, jumpValue, lastPage, t]);
253253

254+
const handleMouseDown = useCallback<React.MouseEventHandler<HTMLElement>>(
255+
(e) => {
256+
if (dDisabled) {
257+
e.preventDefault();
258+
e.stopPropagation();
259+
}
260+
},
261+
[dDisabled]
262+
);
263+
const handleClick = useCallback<React.MouseEventHandler<HTMLElement>>(
264+
(e) => {
265+
if (dDisabled) {
266+
e.preventDefault();
267+
e.stopPropagation();
268+
}
269+
},
270+
[dDisabled]
271+
);
272+
254273
return (
255274
<nav
256275
{...restProps}
@@ -259,7 +278,7 @@ export function DPagination(props: DPaginationProps) {
259278
'is-disabled': dDisabled,
260279
'is-change': isChange,
261280
})}
262-
tabIndex={dDisabled ? undefined : -1}
281+
tabIndex={-1}
263282
role="navigation"
264283
aria-label="Pagination Navigation"
265284
>
@@ -303,7 +322,7 @@ export function DPagination(props: DPaginationProps) {
303322
}
304323

305324
return (
306-
<ul key="pages" className={`${dPrefix}pagination__list`}>
325+
<ul key="pages" className={`${dPrefix}pagination__list`} onMouseDownCapture={handleMouseDown} onClickCapture={handleClick}>
307326
{prevNode}
308327
{pages.map((n) => {
309328
if (n === 'prev5') {
@@ -316,7 +335,7 @@ export function DPagination(props: DPaginationProps) {
316335
`${dPrefix}pagination__item--jump5`
317336
)}
318337
role="button"
319-
tabIndex={dDisabled ? undefined : 0}
338+
tabIndex={0}
320339
title={t('5 pages forward')}
321340
onClick={() => {
322341
changeActive(Math.max(active - 5, 1));
@@ -344,7 +363,7 @@ export function DPagination(props: DPaginationProps) {
344363
`${dPrefix}pagination__item--jump5`
345364
)}
346365
role="button"
347-
tabIndex={dDisabled ? undefined : 0}
366+
tabIndex={0}
348367
title={t('5 pages backward')}
349368
onClick={() => {
350369
changeActive(Math.min(active + 5, lastPage));
@@ -374,7 +393,7 @@ export function DPagination(props: DPaginationProps) {
374393
'is-active': active === n,
375394
}
376395
)}
377-
tabIndex={dDisabled ? undefined : 0}
396+
tabIndex={0}
378397
onClick={() => {
379398
changeActive(n);
380399
}}

packages/ui/src/components/root/Root.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useEffect } from 'react';
44

55
import { DConfigContext } from '../../hooks/d-config';
66
import { Notification } from './Notification';
7+
import { Toast } from './Toast';
78

89
export interface DRootProps extends DConfigContextData {
910
children: React.ReactNode;
@@ -26,6 +27,7 @@ export function DRoot(props: DRootProps) {
2627
<DConfigContext.Provider value={restProps}>
2728
{children}
2829
<Notification></Notification>
30+
<Toast></Toast>
2931
</DConfigContext.Provider>
3032
);
3133
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import type { Subscription } from 'rxjs';
3+
4+
import { useEffect, useMemo } from 'react';
5+
import ReactDOM from 'react-dom';
6+
7+
import { useImmer, usePrefixConfig } from '../../hooks';
8+
import { DToast, ToastService, toastSubject } from '../toast';
9+
10+
export function Toast() {
11+
//#region Context
12+
const dPrefix = usePrefixConfig();
13+
//#endregion
14+
15+
const [toasts, setToasts] = useImmer(new Map<number, any>());
16+
17+
useEffect(() => {
18+
const obs: Subscription[] = [];
19+
const mergeProps = (uniqueId: number, props: any) => {
20+
return {
21+
onClose: () => {
22+
props.onClose?.();
23+
24+
ToastService.close(uniqueId);
25+
},
26+
afterVisibleChange: (visible: boolean) => {
27+
props.afterVisibleChange?.(visible);
28+
29+
if (!visible) {
30+
setToasts((draft) => {
31+
draft.delete(uniqueId);
32+
});
33+
}
34+
},
35+
};
36+
};
37+
obs.push(
38+
toastSubject.open.subscribe({
39+
next: ({ uniqueId, props }) => {
40+
setToasts((draft) => {
41+
draft.set(uniqueId, { ...props, dVisible: true, ...mergeProps(uniqueId, props) });
42+
});
43+
},
44+
}),
45+
toastSubject.close.subscribe({
46+
next: (uniqueId) => {
47+
setToasts((draft) => {
48+
const props = draft.get(uniqueId);
49+
if (props) {
50+
draft.set(uniqueId, { ...props, dVisible: false });
51+
}
52+
});
53+
},
54+
}),
55+
toastSubject.rerender.subscribe({
56+
next: ({ uniqueId, props: newProps }) => {
57+
setToasts((draft) => {
58+
const props = draft.get(uniqueId);
59+
if (props) {
60+
draft.set(uniqueId, { ...newProps, dVisible: props.dVisible, ...mergeProps(uniqueId, newProps) });
61+
}
62+
});
63+
},
64+
}),
65+
toastSubject.closeAll.subscribe({
66+
next: (animation) => {
67+
setToasts((draft) => {
68+
if (animation) {
69+
for (const props of draft.values()) {
70+
props.dVisible = false;
71+
}
72+
} else {
73+
draft.clear();
74+
}
75+
});
76+
},
77+
})
78+
);
79+
}, [setToasts]);
80+
81+
const [toastTRoot, toastBRoot] = useMemo(() => {
82+
const getRoot = (id: string) => {
83+
let root = document.getElementById(`${dPrefix}toast-root`);
84+
if (!root) {
85+
root = document.createElement('div');
86+
root.id = `${dPrefix}toast-root`;
87+
document.body.appendChild(root);
88+
}
89+
90+
let el = document.getElementById(id);
91+
if (!el) {
92+
el = document.createElement('div');
93+
el.id = id;
94+
root.appendChild(el);
95+
}
96+
return el;
97+
};
98+
99+
return [getRoot(`${dPrefix}toast-t-root`), getRoot(`${dPrefix}toast-b-root`)];
100+
}, [dPrefix]);
101+
102+
return (
103+
<>
104+
{ReactDOM.createPortal(
105+
Array.from(toasts.entries())
106+
.filter(([uniqueId, toastProps]) => (toastProps.dPlacement ?? 'top') === 'top')
107+
.map(([uniqueId, toastProps]) => <DToast key={uniqueId} {...toastProps}></DToast>),
108+
toastTRoot
109+
)}
110+
{ReactDOM.createPortal(
111+
Array.from(toasts.entries())
112+
.filter(([uniqueId, toastProps]) => toastProps.dPlacement === 'bottom')
113+
.map(([uniqueId, toastProps]) => <DToast key={uniqueId} {...toastProps}></DToast>),
114+
toastBRoot
115+
)}
116+
</>
117+
);
118+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
group: Feedback
3+
title: Toast
4+
---
5+
6+
Toast.
7+
8+
## When To Use
9+
10+
Global display operation feedback information.
11+
12+
## API
13+
14+
### DToastProps
15+
16+
Extend `React.HTMLAttributes<HTMLDivElement>`.
17+
18+
<!-- prettier-ignore-start -->
19+
| Property | Description | Type | Default |
20+
| --- | --- | --- | --- |
21+
| dType | Toast type | 'success' \| 'warning' \| 'error' \| 'info' | - |
22+
| dIcon | Custom toast icon | React.ReactNode | - |
23+
| dContent | Content | React.ReactNode | - |
24+
| dDuration | Display duration, will not be closed automatically when it is 0 | number | 9.6 |
25+
| dPlacement | Toast pop-up direction | 'top' \| 'bottom' | 'top' |
26+
| onClose | Callback when the toast is closed | `() => void` | - |
27+
| afterVisibleChange | Callback to the end of the opening/closing animation | `(visible: boolean) => void` | - |
28+
<!-- prettier-ignore-end -->
29+
30+
### ToastService
31+
32+
```tsx
33+
class ToastService {
34+
// Toast list
35+
static readonly toasts: Toast[];
36+
37+
// Open toast
38+
static open(props: DToastProps): Toast;
39+
40+
// Close toast
41+
static close(uniqueId: number): void;
42+
43+
// Update toast
44+
static rerender(uniqueId: number, props: DToastProps): void;
45+
46+
// Close all toasts
47+
static closeAll(animation = true): void;
48+
}
49+
```
50+
51+
### Toast
52+
53+
```tsx
54+
class Toast {
55+
// Uniquely identifies
56+
readonly uniqueId: number;
57+
58+
// Close toast
59+
close(): void;
60+
61+
// Update toast
62+
rerender(props: DToastProps): void;
63+
}
64+
```

0 commit comments

Comments
 (0)