index.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  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 cls from 'classnames';
  9. import PropTypes from 'prop-types';
  10. import ConfigContext, { ContextValue } from '../configProvider/context';
  11. import NotificationListFoundation, {
  12. ConfigProps, NotificationListAdapter,
  13. NotificationListProps,
  14. NotificationListState
  15. } from '@douyinfe/semi-foundation/notification/notificationListFoundation';
  16. import { cssClasses, strings } from '@douyinfe/semi-foundation/notification/constants';
  17. import Notice from './notice';
  18. import BaseComponent from '../_base/baseComponent';
  19. import '@douyinfe/semi-foundation/notification/notification.scss';
  20. import getUuid from '@douyinfe/semi-foundation/utils/uuid';
  21. import useNotification from './useNotification';
  22. import {
  23. NoticeInstance,
  24. NoticePosition,
  25. NoticeProps,
  26. NoticeState
  27. } from '@douyinfe/semi-foundation/notification/notificationFoundation';
  28. import CSSAnimation from "../_cssAnimation";
  29. // TODO: Automatic folding + unfolding function when there are more than N
  30. export interface NoticeReactProps extends NoticeProps {
  31. style?: CSSProperties
  32. }
  33. export type {
  34. NoticeState,
  35. NotificationListProps,
  36. NotificationListState,
  37. ConfigProps
  38. };
  39. export type NoticesInPosition = {
  40. top: NoticeInstance[];
  41. topLeft: NoticeInstance[];
  42. topRight: NoticeInstance[];
  43. bottom: NoticeInstance[];
  44. bottomLeft: NoticeInstance[];
  45. bottomRight: NoticeInstance[]
  46. };
  47. let ref: NotificationList = null;
  48. const defaultConfig = {
  49. duration: 3,
  50. position: 'topRight' as NoticePosition,
  51. motion: true,
  52. content: '',
  53. title: '',
  54. zIndex: 1010,
  55. };
  56. class NotificationList extends BaseComponent<NotificationListProps, NotificationListState> {
  57. static contextType = ConfigContext;
  58. static propTypes = {
  59. style: PropTypes.object,
  60. className: PropTypes.string,
  61. direction: PropTypes.oneOf(strings.directions),
  62. };
  63. static defaultProps = {};
  64. static useNotification: typeof useNotification;
  65. private static wrapperId: string;
  66. /* REACT_19_START */
  67. // private static root: any = null;
  68. /* REACT_19_END */
  69. private noticeStorage: NoticeInstance[];
  70. private removeItemStorage: NoticeInstance[];
  71. constructor(props: NotificationListProps) {
  72. super(props);
  73. this.state = {
  74. notices: [],
  75. removedItems: [],
  76. updatedItems: []
  77. };
  78. this.noticeStorage = [];
  79. this.removeItemStorage = [];
  80. this.foundation = new NotificationListFoundation(this.adapter);
  81. }
  82. context: ContextValue;
  83. get adapter(): NotificationListAdapter {
  84. return {
  85. ...super.adapter,
  86. updateNotices: (notices: NoticeInstance[], removedItems: NoticeInstance[] = [], updatedItems: NoticeInstance[] = []) => {
  87. this.noticeStorage = [...notices];
  88. this.removeItemStorage = [...removedItems];
  89. // setState is async sometimes and react often merges state, so use "this" , make sure other code always get right data.
  90. this.setState({ notices, removedItems, updatedItems });
  91. },
  92. getNotices: () => this.noticeStorage,
  93. };
  94. }
  95. static addNotice(notice: NoticeProps) {
  96. notice = { ...defaultConfig, ...notice };
  97. const id = notice.id ?? getUuid('notification');
  98. if (!ref) {
  99. const { getPopupContainer } = notice;
  100. const div = document.createElement('div');
  101. if (!this.wrapperId) {
  102. this.wrapperId = getUuid('notification-wrapper').slice(0, 32);
  103. }
  104. div.className = cssClasses.WRAPPER;
  105. div.id = this.wrapperId;
  106. div.style.zIndex = String(typeof notice.zIndex === 'number' ? notice.zIndex : defaultConfig.zIndex);
  107. if (getPopupContainer) {
  108. const container = getPopupContainer();
  109. container.appendChild(div);
  110. } else {
  111. document.body.appendChild(div);
  112. }
  113. /* REACT_18_START */
  114. ReactDOM.render(React.createElement(NotificationList, { ref: instance => (ref = instance) }), div, () => {
  115. ref.add({ ...notice, id });
  116. });
  117. /* REACT_18_END */
  118. /* REACT_19_START */
  119. // if (!this.root) {
  120. // this.root = createRoot(div);
  121. // }
  122. // this.root.render(React.createElement(NotificationList, { ref: instance => (ref = instance) }));
  123. // // 在 React 19 中,render 是同步的,所以可以立即执行 callback
  124. // ref.add({ ...notice, id });
  125. /* REACT_19_END */
  126. } else {
  127. if (ref.has(`${id}`)) {
  128. ref.update(id, notice);
  129. } else {
  130. ref.add({ ...notice, id });
  131. }
  132. }
  133. return id;
  134. }
  135. static removeNotice(id: string) {
  136. if (ref) {
  137. ref.remove(id);
  138. }
  139. return id;
  140. }
  141. static info(opts: NoticeProps) {
  142. return this.addNotice({ ...opts, type: 'info' });
  143. }
  144. static success(opts: NoticeProps) {
  145. return this.addNotice({ ...opts, type: 'success' });
  146. }
  147. static error(opts: NoticeProps) {
  148. return this.addNotice({ ...opts, type: 'error' });
  149. }
  150. static warning(opts: NoticeProps) {
  151. return this.addNotice({ ...opts, type: 'warning' });
  152. }
  153. static open(opts: NoticeProps) {
  154. return this.addNotice({ ...opts, type: 'default' });
  155. }
  156. static close(id: string) {
  157. return this.removeNotice(id);
  158. }
  159. static destroyAll() {
  160. if (ref) {
  161. ref.destroyAll();
  162. const wrapper = document.querySelector(`#${this.wrapperId}`);
  163. /* REACT_18_START */
  164. ReactDOM.unmountComponentAtNode(wrapper);
  165. wrapper && wrapper.parentNode.removeChild(wrapper);
  166. /* REACT_18_END */
  167. /* REACT_19_START */
  168. // if (this.root) {
  169. // this.root.unmount();
  170. // this.root = null;
  171. // }
  172. // wrapper && wrapper.parentNode.removeChild(wrapper);
  173. /* REACT_19_END */
  174. ref = null;
  175. this.wrapperId = null;
  176. }
  177. }
  178. static config(opts: ConfigProps) {
  179. ['top', 'left', 'bottom', 'right'].map(pos => {
  180. if (pos in opts) {
  181. defaultConfig[pos] = opts[pos];
  182. }
  183. });
  184. if (typeof opts.zIndex === 'number') {
  185. defaultConfig.zIndex = opts.zIndex;
  186. }
  187. if (typeof opts.duration === 'number') {
  188. defaultConfig.duration = opts.duration;
  189. }
  190. if (typeof opts.position === 'string') {
  191. defaultConfig.position = opts.position as NoticePosition;
  192. }
  193. }
  194. add = (noticeOpts: NoticeProps) => this.foundation.addNotice(noticeOpts);
  195. has = (id: string) => this.foundation.has(id);
  196. remove = (id: string) => {
  197. this.foundation.removeNotice(String(id));
  198. };
  199. update = (id: string, opts: NoticeProps)=>{
  200. return this.foundation.update(id, opts);
  201. }
  202. destroyAll = () => this.foundation.destroyAll();
  203. renderNoticeInPosition = (
  204. notices: NoticeInstance[],
  205. position: NoticePosition,
  206. removedItems: NoticeInstance[] = [],
  207. updatedItems: NoticeInstance[] = []
  208. ) => {
  209. const className = cls(cssClasses.LIST);
  210. // TODO notifyOnClose
  211. if (notices.length) {
  212. const style = this.setPosInStyle(notices[0]);
  213. return (
  214. // @ts-ignore
  215. <div placement={position} key={position} className={className} style={style}>
  216. {notices.map((notice, index) => {
  217. const isRemoved = removedItems.find(removedItem => removedItem.id === notice.id) !== undefined;
  218. return <CSSAnimation key={notice.id}
  219. animationState={isRemoved ? "leave" : "enter"}
  220. startClassName={`${cssClasses.NOTICE}-animation-${isRemoved ? "hide" : "show"}_${position}`}>
  221. {({ animationClassName, animationEventsNeedBind, isAnimating }) => {
  222. return isRemoved && !isAnimating ? null : <Notice
  223. {...notice}
  224. ref={(notice)=>{
  225. if (notice && updatedItems.some(item=>item.id===notice.props.id)) {
  226. notice.foundation.restartCloseTimer();
  227. }
  228. }}
  229. className={cls({
  230. [notice.className]: Boolean(notice.className),
  231. [animationClassName]: true,
  232. })}
  233. {...animationEventsNeedBind}
  234. style={{ ...notice.style }}
  235. close={this.remove}
  236. />;
  237. }}
  238. </CSSAnimation>;
  239. }
  240. )}
  241. </div>
  242. );
  243. }
  244. return null;
  245. };
  246. setPosInStyle(noticeInstance: NoticeInstance) {
  247. const style = {};
  248. ['top', 'left', 'bottom', 'right'].forEach(pos => {
  249. if (pos in noticeInstance) {
  250. const val = noticeInstance[pos];
  251. style[pos] = typeof val === 'number' ? `${val}px` : val;
  252. }
  253. });
  254. return style;
  255. }
  256. render() {
  257. let { notices } = this.state;
  258. const { removedItems, updatedItems } = this.state;
  259. notices = Array.from(new Set([...notices, ...removedItems]));
  260. const noticesInPosition: NoticesInPosition = {
  261. top: [],
  262. topLeft: [],
  263. topRight: [],
  264. bottom: [],
  265. bottomLeft: [],
  266. bottomRight: [],
  267. };
  268. notices.forEach(notice => {
  269. const direction = notice.direction || this.context.direction;
  270. const defaultPosition = direction === 'rtl' ? 'topLeft' : 'topRight';
  271. const position = notice.position || defaultPosition;
  272. noticesInPosition[position].push(notice);
  273. });
  274. const noticesList = Object.entries(noticesInPosition).map(obj => {
  275. const pos = obj[0];
  276. const noticesInPos = obj[1];
  277. return this.renderNoticeInPosition(noticesInPos, pos as NoticePosition, removedItems, updatedItems);
  278. });
  279. return <React.Fragment>{noticesList}</React.Fragment>;
  280. }
  281. }
  282. NotificationList.useNotification = useNotification;
  283. export default NotificationList;