index.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. import React, { CSSProperties } from 'react';
  2. /* REACT_18_START */
  3. import ReactDOM from 'react-dom';
  4. /* REACT_18_END */
  5. /* REACT_19_START */
  6. // import { createRoot } from 'react-dom/client';
  7. /* REACT_19_END */
  8. import PropTypes from 'prop-types';
  9. import ToastListFoundation, {
  10. ToastListAdapter,
  11. ToastListProps,
  12. ToastListState
  13. } from '@douyinfe/semi-foundation/toast/toastListFoundation';
  14. import { cssClasses, strings } from '@douyinfe/semi-foundation/toast/constants';
  15. import BaseComponent from '../_base/baseComponent';
  16. import Toast from './toast';
  17. import '@douyinfe/semi-foundation/toast/toast.scss';
  18. import getUuid from '@douyinfe/semi-foundation/utils/uuid';
  19. import useToast from './useToast';
  20. import { ConfigProps, ToastInstance, ToastProps, ToastState } from '@douyinfe/semi-foundation/toast/toastFoundation';
  21. import CSSAnimation from '../_cssAnimation';
  22. import cls from 'classnames';
  23. export interface ToastReactProps extends ToastProps{
  24. id?: string;
  25. style?: CSSProperties;
  26. icon?: React.ReactNode;
  27. content: React.ReactNode
  28. }
  29. export type {
  30. ConfigProps,
  31. ToastListProps,
  32. ToastListState,
  33. ToastState
  34. };
  35. const createBaseToast = () => class ToastList extends BaseComponent<ToastListProps, ToastListState> {
  36. static ref: ToastList;
  37. static useToast: typeof useToast;
  38. static defaultOpts: ToastReactProps & { motion: boolean } = {
  39. motion: true,
  40. zIndex: 1010,
  41. content: '',
  42. };
  43. static propTypes = {
  44. content: PropTypes.node,
  45. duration: PropTypes.number,
  46. onClose: PropTypes.func,
  47. icon: PropTypes.node,
  48. direction: PropTypes.oneOf(strings.directions),
  49. stack: PropTypes.bool,
  50. };
  51. static defaultProps = {};
  52. static wrapperId: null | string;
  53. /* REACT_19_START */
  54. // static root: any = null;
  55. /* REACT_19_END */
  56. stack: boolean = false;
  57. innerWrapperRef: React.RefObject<HTMLDivElement> = React.createRef();
  58. constructor(props: ToastListProps) {
  59. super(props);
  60. this.state = {
  61. list: [],
  62. removedItems: [],
  63. updatedItems: [],
  64. mouseInSide: false
  65. };
  66. this.foundation = new ToastListFoundation(this.adapter);
  67. }
  68. get adapter(): ToastListAdapter {
  69. return {
  70. ...super.adapter,
  71. updateToast: (list: ToastInstance[], removedItems: ToastInstance[], updatedItems: ToastInstance[]) => {
  72. this.setState({ list, removedItems, updatedItems });
  73. },
  74. handleMouseInSideChange: (mouseInSide: boolean) => {
  75. this.setState({ mouseInSide });
  76. },
  77. getInputWrapperRect: () => {
  78. return this.innerWrapperRef.current?.getBoundingClientRect();
  79. }
  80. };
  81. }
  82. handleMouseEnter = (e: React.MouseEvent) => {
  83. if (this.stack) {
  84. this.foundation.handleMouseInSideChange(true);
  85. }
  86. }
  87. handleMouseLeave = (e: React.MouseEvent) => {
  88. if (this.stack) {
  89. const height = this.foundation.getInputWrapperRect()?.height;
  90. if (height) {
  91. this.foundation.handleMouseInSideChange(false);
  92. }
  93. }
  94. }
  95. static create(opts: ToastReactProps) {
  96. const id = opts.id ?? getUuid('toast');
  97. // this.id = id;
  98. if (!ToastList.ref) {
  99. const div = document.createElement('div');
  100. if (!this.wrapperId) {
  101. this.wrapperId = getUuid('toast-wrapper').slice(0, 26);
  102. }
  103. div.className = cssClasses.WRAPPER;
  104. div.id = this.wrapperId;
  105. div.style.zIndex = String(typeof opts.zIndex === 'number' ?
  106. opts.zIndex : ToastList.defaultOpts.zIndex);
  107. ['top', 'left', 'bottom', 'right'].map(pos => {
  108. if (pos in ToastList.defaultOpts || pos in opts) {
  109. const val = opts[pos] ? opts[pos] : ToastList.defaultOpts[pos];
  110. div.style[pos] = typeof val === 'number' ? `${val}px` : val;
  111. }
  112. });
  113. // document.body.appendChild(div);
  114. if (ToastList.defaultOpts.getPopupContainer) {
  115. const container = ToastList.defaultOpts.getPopupContainer();
  116. container.appendChild(div);
  117. } else {
  118. document.body.appendChild(div);
  119. }
  120. /* REACT_18_START */
  121. ReactDOM.render(React.createElement(
  122. ToastList,
  123. { ref: instance => (ToastList.ref = instance) }
  124. ),
  125. div,
  126. () => {
  127. ToastList.ref.add({ ...opts, id });
  128. ToastList.ref.stack = Boolean(opts.stack);
  129. });
  130. /* REACT_18_END */
  131. /* REACT_19_START */
  132. // if (!this.root) {
  133. // this.root = createRoot(div);
  134. // }
  135. // this.root.render(React.createElement(
  136. // ToastList,
  137. // { ref: instance => (ToastList.ref = instance) }
  138. // ));
  139. // // 在 React 19 中,render 是同步的,确保 ref 已赋值后再执行add方法
  140. // if (typeof queueMicrotask === 'function') {
  141. // queueMicrotask(() => {
  142. // ToastList.ref.add({ ...opts, id });
  143. // ToastList.ref.stack = Boolean(opts.stack);
  144. // });
  145. // } else {
  146. // Promise.resolve().then(() => {
  147. // ToastList.ref.add({ ...opts, id });
  148. // ToastList.ref.stack = Boolean(opts.stack);
  149. // });
  150. // }
  151. /* REACT_19_END */
  152. } else {
  153. const node = document.querySelector(`#${this.wrapperId}`) as HTMLElement;
  154. ['top', 'left', 'bottom', 'right'].map(pos => {
  155. if (pos in opts) {
  156. node.style[pos] = typeof opts[pos] === 'number' ? `${opts[pos]}px` : opts[pos];
  157. }
  158. });
  159. if (Boolean(opts.stack) !== ToastList.ref.stack) {
  160. ToastList.ref.stack = Boolean(opts.stack);
  161. }
  162. if (ToastList.ref.has(id)) {
  163. ToastList.ref.update(id, { ...opts, id });
  164. } else {
  165. ToastList.ref.add({ ...opts, id });
  166. }
  167. }
  168. return id;
  169. }
  170. static close(id: string) {
  171. if (ToastList.ref) {
  172. ToastList.ref.remove(id);
  173. }
  174. }
  175. static destroyAll() {
  176. if (ToastList.ref) {
  177. ToastList.ref.destroyAll();
  178. const wrapper = document.querySelector(`#${this.wrapperId}`);
  179. /* REACT_18_START */
  180. ReactDOM.unmountComponentAtNode(wrapper);
  181. wrapper && wrapper.parentNode.removeChild(wrapper);
  182. /* REACT_18_END */
  183. /* REACT_19_START */
  184. // if (this.root) {
  185. // this.root.unmount();
  186. // this.root = null;
  187. // }
  188. // wrapper && wrapper.parentNode.removeChild(wrapper);
  189. /* REACT_19_END */
  190. ToastList.ref = null;
  191. this.wrapperId = null;
  192. }
  193. }
  194. static getWrapperId() {
  195. return this.wrapperId;
  196. }
  197. static info(opts: Omit<ToastReactProps, 'type'> | string) {
  198. if (typeof opts === 'string') {
  199. opts = { content: opts };
  200. }
  201. return this.create({ ...ToastList.defaultOpts, ...opts, type: 'info' });
  202. }
  203. static warning(opts: Omit<ToastReactProps, 'type'> | string) {
  204. if (typeof opts === 'string') {
  205. opts = { content: opts };
  206. }
  207. return this.create({ ...ToastList.defaultOpts, ...opts, type: 'warning' });
  208. }
  209. static error(opts: Omit<ToastReactProps, 'type'> | string) {
  210. if (typeof opts === 'string') {
  211. opts = { content: opts };
  212. }
  213. return this.create({ ...ToastList.defaultOpts, ...opts, type: 'error' });
  214. }
  215. static success(opts: Omit<ToastReactProps, 'type'> | string) {
  216. if (typeof opts === 'string') {
  217. opts = { content: opts };
  218. }
  219. return this.create({ ...ToastList.defaultOpts, ...opts, type: 'success' });
  220. }
  221. static config(opts: ConfigProps) {
  222. ['top', 'left', 'bottom', 'right'].forEach(pos => {
  223. if (pos in opts) {
  224. ToastList.defaultOpts[pos] = opts[pos];
  225. }
  226. });
  227. if (typeof opts.theme === 'string' && strings.themes.includes(opts.theme)) {
  228. ToastList.defaultOpts.theme = opts.theme;
  229. }
  230. if (typeof opts.zIndex === 'number') {
  231. ToastList.defaultOpts.zIndex = opts.zIndex;
  232. }
  233. if (typeof opts.duration === 'number') {
  234. ToastList.defaultOpts.duration = opts.duration;
  235. }
  236. if (typeof opts.getPopupContainer === 'function') {
  237. ToastList.defaultOpts.getPopupContainer = opts.getPopupContainer;
  238. }
  239. }
  240. has(id: string) {
  241. return this.foundation.hasToast(id);
  242. }
  243. add(opts: ToastInstance) {
  244. return this.foundation.addToast(opts);
  245. }
  246. update(id: string, opts: ToastInstance) {
  247. return this.foundation.updateToast(id, opts);
  248. }
  249. remove(id: string) {
  250. return this.foundation.removeToast(id);
  251. }
  252. destroyAll() {
  253. return this.foundation.destroyAll();
  254. }
  255. render() {
  256. let { list } = this.state;
  257. const { removedItems, updatedItems } = this.state;
  258. list = Array.from(new Set([...list, ...removedItems]));
  259. const updatedIds = updatedItems.map(({ id }) => id);
  260. const refFn: React.LegacyRef<Toast> = (toast) => {
  261. if (toast?.foundation?._id && updatedIds.includes(toast.foundation._id)) {
  262. toast.foundation.restartCloseTimer();
  263. }
  264. };
  265. return (
  266. <React.Fragment>
  267. <div className={cls({
  268. [`${cssClasses.PREFIX}-innerWrapper`]: true,
  269. [`${cssClasses.PREFIX}-innerWrapper-hover`]: this.state.mouseInSide
  270. })} ref={this.innerWrapperRef} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
  271. {list.map((item, index) =>{
  272. const isRemoved = removedItems.find(removedItem=>removedItem.id===item.id) !== undefined;
  273. return <CSSAnimation key={item.id} motion={item.motion} animationState={isRemoved?"leave":"enter"} startClassName={isRemoved?`${cssClasses.PREFIX}-animation-hide`:`${cssClasses.PREFIX}-animation-show`}>
  274. {
  275. ({ animationClassName, animationEventsNeedBind, isAnimating })=>{
  276. return (isRemoved && !isAnimating) ? null : <Toast {...item} stack={this.stack} stackExpanded={this.state.mouseInSide} positionInList={{ length: list.length, index }} className={cls({
  277. [item.className]: Boolean(item.className),
  278. [animationClassName]: true
  279. })} {...animationEventsNeedBind} style={{ ...item.style }} close={id => this.remove(id)} ref={refFn} />;
  280. }
  281. }
  282. </CSSAnimation>;
  283. }
  284. )}
  285. </div>
  286. </React.Fragment>
  287. );
  288. }
  289. };
  290. export class ToastFactory {
  291. static create(config?: ConfigProps): ReturnType<typeof createBaseToast> {
  292. const newToast = createBaseToast();
  293. newToast.useToast = useToast;
  294. config && newToast.config(config);
  295. return newToast;
  296. }
  297. }
  298. export default ToastFactory.create();