@tendaui/components 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +176 -0
  3. package/alert/Alert.tsx +147 -0
  4. package/alert/defaultProps.ts +3 -0
  5. package/alert/index.ts +9 -0
  6. package/alert/style/css.js +1 -0
  7. package/alert/style/index.js +1 -0
  8. package/alert/type.ts +44 -0
  9. package/badge/Badge.tsx +85 -0
  10. package/badge/defaultProps.ts +10 -0
  11. package/badge/index.ts +9 -0
  12. package/badge/style/css.js +1 -0
  13. package/badge/style/index.js +1 -0
  14. package/badge/type.ts +51 -0
  15. package/button/Button.tsx +95 -0
  16. package/button/defaultProps.ts +13 -0
  17. package/button/index.ts +7 -0
  18. package/button/style/css.js +1 -0
  19. package/button/style/index.js +1 -0
  20. package/button/type.ts +82 -0
  21. package/checkbox/Checkbox.tsx +19 -0
  22. package/checkbox/CheckboxGroup.tsx +207 -0
  23. package/checkbox/defaultProps.ts +14 -0
  24. package/checkbox/index.ts +10 -0
  25. package/checkbox/style/css.js +1 -0
  26. package/checkbox/style/index.js +1 -0
  27. package/checkbox/type.ts +117 -0
  28. package/common/Check.tsx +131 -0
  29. package/common/FakeArrow.tsx +36 -0
  30. package/common/PluginContainer.tsx +21 -0
  31. package/common/Portal.tsx +67 -0
  32. package/common.ts +76 -0
  33. package/config-provider/ConfigContext.tsx +21 -0
  34. package/config-provider/ConfigProvider.tsx +53 -0
  35. package/config-provider/index.ts +9 -0
  36. package/config-provider/type.ts +1062 -0
  37. package/dialog/Dialog.tsx +254 -0
  38. package/dialog/DialogCard.tsx +152 -0
  39. package/dialog/defaultProps.ts +25 -0
  40. package/dialog/hooks/useDialogDrag.ts +50 -0
  41. package/dialog/hooks/useDialogEsc.ts +31 -0
  42. package/dialog/hooks/useDialogPosition.ts +36 -0
  43. package/dialog/hooks/useLockStyle.ts +54 -0
  44. package/dialog/index.ts +13 -0
  45. package/dialog/plugin.tsx +78 -0
  46. package/dialog/style/css.js +1 -0
  47. package/dialog/style/index.js +1 -0
  48. package/dialog/type.ts +241 -0
  49. package/dialog/utils.ts +4 -0
  50. package/form/Form.tsx +136 -0
  51. package/form/FormContext.tsx +64 -0
  52. package/form/FormItem.tsx +554 -0
  53. package/form/FormList.tsx +303 -0
  54. package/form/const.ts +6 -0
  55. package/form/defaultProps.ts +26 -0
  56. package/form/formModel.ts +117 -0
  57. package/form/hooks/interface.ts +20 -0
  58. package/form/hooks/useForm.ts +122 -0
  59. package/form/hooks/useFormItemInitialData.ts +95 -0
  60. package/form/hooks/useFormItemStyle.tsx +122 -0
  61. package/form/hooks/useInstance.tsx +275 -0
  62. package/form/hooks/useWatch.ts +42 -0
  63. package/form/index.ts +11 -0
  64. package/form/style/css.js +1 -0
  65. package/form/style/index.js +1 -0
  66. package/form/type.ts +519 -0
  67. package/form/utils/index.ts +69 -0
  68. package/hooks/useAttach.ts +24 -0
  69. package/hooks/useCommonClassName.ts +45 -0
  70. package/hooks/useConfig.ts +3 -0
  71. package/hooks/useControlled.ts +39 -0
  72. package/hooks/useDefaultProps.ts +16 -0
  73. package/hooks/useDomCallback.ts +13 -0
  74. package/hooks/useDomRefCallback.ts +12 -0
  75. package/hooks/useDragSorter.tsx +151 -0
  76. package/hooks/useEventCallback.ts +47 -0
  77. package/hooks/useGlobalConfig.ts +14 -0
  78. package/hooks/useGlobalIcon.ts +14 -0
  79. package/hooks/useLastest.ts +13 -0
  80. package/hooks/useLayoutEffect.ts +7 -0
  81. package/hooks/useMouseEvent.ts +142 -0
  82. package/hooks/useMutationObserver.ts +56 -0
  83. package/hooks/usePopper.ts +189 -0
  84. package/hooks/useRipple.ts +0 -0
  85. package/hooks/useSetState.ts +25 -0
  86. package/hooks/useVirtualScroll.ts +246 -0
  87. package/hooks/useWindowSize.ts +31 -0
  88. package/index.ts +70 -0
  89. package/input/Input.tsx +383 -0
  90. package/input/InputGroup.tsx +29 -0
  91. package/input/defaultProps.ts +22 -0
  92. package/input/index.ts +11 -0
  93. package/input/style/css.js +1 -0
  94. package/input/style/index.js +1 -0
  95. package/input/type.ts +219 -0
  96. package/loading/Gradient.tsx +36 -0
  97. package/loading/Loading.tsx +169 -0
  98. package/loading/circleAdapter.ts +44 -0
  99. package/loading/defaultProps.ts +12 -0
  100. package/loading/index.ts +13 -0
  101. package/loading/style/css.js +1 -0
  102. package/loading/style/index.js +1 -0
  103. package/loading/type.ts +71 -0
  104. package/loading/utils/setStyle.ts +13 -0
  105. package/myform/index.ts +0 -0
  106. package/notification/Notify.ts +24 -0
  107. package/notification/NotifyContainer.tsx +90 -0
  108. package/notification/NotifyContext.tsx +173 -0
  109. package/notification/NotifyItem.tsx +121 -0
  110. package/notification/index.ts +3 -0
  111. package/notification/style/css.js +1 -0
  112. package/notification/style/index.js +1 -0
  113. package/notification/type.ts +23 -0
  114. package/package.json +52 -0
  115. package/popup/Popup.tsx +264 -0
  116. package/popup/defaultProps.ts +13 -0
  117. package/popup/hooks/useTrigger.ts +276 -0
  118. package/popup/index.ts +6 -0
  119. package/popup/style/css.js +1 -0
  120. package/popup/style/index.js +1 -0
  121. package/popup/type.ts +130 -0
  122. package/portal/Portal.tsx +63 -0
  123. package/portal/index.ts +1 -0
  124. package/select/Option.tsx +162 -0
  125. package/select/OptionGroup.tsx +30 -0
  126. package/select/PopupContent.tsx +271 -0
  127. package/select/Select.tsx +586 -0
  128. package/select/defaultProps.ts +27 -0
  129. package/select/hooks/useOptions.ts +120 -0
  130. package/select/hooks/usePanelVirtualScroll.ts +111 -0
  131. package/select/index.ts +9 -0
  132. package/select/style/css.js +1 -0
  133. package/select/style/index.js +2 -0
  134. package/select/type.ts +382 -0
  135. package/select/utils/helper.ts +256 -0
  136. package/select-input/SelectInput.tsx +98 -0
  137. package/select-input/defaultProps.ts +15 -0
  138. package/select-input/hook/useMultiple.tsx +100 -0
  139. package/select-input/hook/useOverlayInnerStyle.ts +84 -0
  140. package/select-input/hook/useSingle.tsx +112 -0
  141. package/select-input/index.ts +6 -0
  142. package/select-input/interface.ts +18 -0
  143. package/select-input/style/css.js +1 -0
  144. package/select-input/style/index.js +1 -0
  145. package/select-input/type.ts +280 -0
  146. package/space/defaultProps.ts +0 -0
  147. package/space/index.ts +0 -0
  148. package/space/type.ts +0 -0
  149. package/style/index.js +2 -0
  150. package/styles/_global.scss +39 -0
  151. package/styles/_vars.scss +386 -0
  152. package/styles/components/alert/_index.scss +175 -0
  153. package/styles/components/alert/_vars.scss +39 -0
  154. package/styles/components/badge/_index.scss +70 -0
  155. package/styles/components/badge/_vars.scss +25 -0
  156. package/styles/components/button/_index.scss +511 -0
  157. package/styles/components/button/_mixins.scss +39 -0
  158. package/styles/components/button/_vars.scss +122 -0
  159. package/styles/components/checkbox/_index.scss +158 -0
  160. package/styles/components/checkbox/_mixin.scss +0 -0
  161. package/styles/components/checkbox/_var.scss +60 -0
  162. package/styles/components/dialog/_animate.scss +135 -0
  163. package/styles/components/dialog/_index.scss +311 -0
  164. package/styles/components/dialog/_mixins.scss +0 -0
  165. package/styles/components/dialog/_vars.scss +59 -0
  166. package/styles/components/form/_index.scss +174 -0
  167. package/styles/components/form/_mixins.scss +76 -0
  168. package/styles/components/form/_vars.scss +100 -0
  169. package/styles/components/input/_index.scss +349 -0
  170. package/styles/components/input/_map.scss +0 -0
  171. package/styles/components/input/_mixins.scss +116 -0
  172. package/styles/components/input/_vars.scss +134 -0
  173. package/styles/components/loading/_index.scss +112 -0
  174. package/styles/components/loading/_vars.scss +39 -0
  175. package/styles/components/notification/_index.scss +160 -0
  176. package/styles/components/notification/_mixins.scss +12 -0
  177. package/styles/components/notification/_vars.scss +59 -0
  178. package/styles/components/popup/_index.scss +82 -0
  179. package/styles/components/popup/_mixin.scss +149 -0
  180. package/styles/components/popup/_var.scss +31 -0
  181. package/styles/components/select/_index.scss +290 -0
  182. package/styles/components/select/_var.scss +65 -0
  183. package/styles/components/select-input/_index.scss +5 -0
  184. package/styles/components/select-input/_var.scss +3 -0
  185. package/styles/components/switch/_index.scss +279 -0
  186. package/styles/components/switch/_mixins.scss +0 -0
  187. package/styles/components/switch/_vars.scss +61 -0
  188. package/styles/components/tag/_index.scss +316 -0
  189. package/styles/components/tag/_var.scss +85 -0
  190. package/styles/components/tag-input/_index.scss +163 -0
  191. package/styles/components/tag-input/_vars.scss +16 -0
  192. package/styles/globals.css +250 -0
  193. package/styles/mixins/_focus.scss +7 -0
  194. package/styles/mixins/_layout.scss +32 -0
  195. package/styles/mixins/_reset.scss +10 -0
  196. package/styles/mixins/_scrollbar.scss +31 -0
  197. package/styles/mixins/_text.scss +48 -0
  198. package/styles/rillple.css +16 -0
  199. package/styles/scrollbar.css +42 -0
  200. package/styles/themes/_dark.scss +191 -0
  201. package/styles/themes/_font.scss +79 -0
  202. package/styles/themes/_index.scss +5 -0
  203. package/styles/themes/_light.scss +190 -0
  204. package/styles/themes/_radius.scss +9 -0
  205. package/styles/themes/_size.scss +68 -0
  206. package/styles/themes.css +66 -0
  207. package/styles/utilities/_animation.scss +57 -0
  208. package/styles/utilities/_tips.scss +9 -0
  209. package/switch/Switch.tsx +120 -0
  210. package/switch/defaultProps.ts +3 -0
  211. package/switch/index.ts +7 -0
  212. package/switch/style/css.js +1 -0
  213. package/switch/style/index.js +1 -0
  214. package/switch/type.ts +46 -0
  215. package/tag/Tag.tsx +149 -0
  216. package/tag/defaultProps.ts +19 -0
  217. package/tag/index.ts +8 -0
  218. package/tag/style/css.js +1 -0
  219. package/tag/style/index.js +1 -0
  220. package/tag/type.ts +170 -0
  221. package/tag-input/TagInput.tsx +215 -0
  222. package/tag-input/defaultProps.ts +15 -0
  223. package/tag-input/hooks/useHover.ts +28 -0
  224. package/tag-input/hooks/useTagList.tsx +131 -0
  225. package/tag-input/hooks/useTagScroll.ts +105 -0
  226. package/tag-input/index.ts +9 -0
  227. package/tag-input/style/css.js +1 -0
  228. package/tag-input/style/index.js +1 -0
  229. package/tag-input/type.ts +224 -0
  230. package/tag-input/useTagList.tsx +131 -0
  231. package/utils/composeRefs.ts +14 -0
  232. package/utils/dom.ts +29 -0
  233. package/utils/forwardRefWithStatics.ts +12 -0
  234. package/utils/getScrollbarWidth.ts +11 -0
  235. package/utils/helper.ts +161 -0
  236. package/utils/isFragment.ts +22 -0
  237. package/utils/listener.ts +37 -0
  238. package/utils/noop.ts +3 -0
  239. package/utils/parentTNode.ts +38 -0
  240. package/utils/parseTNode.ts +38 -0
  241. package/utils/react-render.ts +108 -0
  242. package/utils/ref.ts +6 -0
  243. package/utils/refs.ts +81 -0
  244. package/utils/style.ts +60 -0
  245. package/utils/transition.ts +28 -0
@@ -0,0 +1,276 @@
1
+ import React, { useRef, useEffect, isValidElement, useCallback, useMemo } from "react";
2
+ import { isFragment } from "react-is";
3
+ import { supportRef, getRefDom, getNodeRef } from "../../utils/refs";
4
+ import composeRefs from "../../utils/composeRefs";
5
+ import { off, on } from "../../utils/listener";
6
+ import classNames from "classnames";
7
+ import { TNode } from "../../common";
8
+ const ESC_KEY = "Escape";
9
+
10
+ interface UseTriggerProps {
11
+ content: TNode;
12
+ disabled?: boolean;
13
+ trigger: "hover" | "click" | "mousedown" | "focus" | "context-menu" | undefined;
14
+ visible: boolean | undefined;
15
+ onVisibleChange: (
16
+ visible: boolean,
17
+ context: {
18
+ e?:
19
+ | React.MouseEvent<HTMLElement>
20
+ | React.TouchEvent<HTMLElement>
21
+ | React.FocusEvent<HTMLElement>
22
+ | React.KeyboardEvent<HTMLElement>;
23
+ trigger: string;
24
+ }
25
+ ) => void;
26
+ triggerRef: React.RefObject<HTMLElement>;
27
+ delay?: number | [number, number] | number[];
28
+ }
29
+
30
+ export default function useTrigger({
31
+ content,
32
+ disabled,
33
+ trigger,
34
+ visible,
35
+ onVisibleChange,
36
+ triggerRef,
37
+ delay
38
+ }: UseTriggerProps) {
39
+ const hasPopupMouseDown = useRef(false);
40
+ const mouseDownTimer = useRef(0);
41
+ const visibleTimer = useRef(null);
42
+ const triggerDataKey = useRef<string>("");
43
+ const leaveFlag = useRef(false); // 防止多次触发显隐
44
+
45
+ // 在 effect 中初始化 triggerDataKey,避免在渲染期间调用不纯函数
46
+ useEffect(() => {
47
+ if (!triggerDataKey.current) {
48
+ triggerDataKey.current = `t-popup--${Math.random().toFixed(10)}`;
49
+ }
50
+ }, []);
51
+
52
+ // 禁用和无内容时不展示
53
+ const shouldToggle = useMemo(() => {
54
+ if (disabled) return false; // 禁用
55
+ return !disabled && content === 0 ? true : content; // 无内容时
56
+ }, [disabled, content]);
57
+
58
+ // 解析 delay 数据类型
59
+ const [appearDelay = 0, exitDelay = 0] = useMemo(() => {
60
+ if (Array.isArray(delay)) return delay;
61
+ return [delay, delay];
62
+ }, [delay]);
63
+
64
+ function callFuncWithDelay({ delay, callback }: { delay?: number; callback: () => void }) {
65
+ if (delay) {
66
+ clearTimeout(visibleTimer.current);
67
+ visibleTimer.current = setTimeout(callback, delay);
68
+ } else {
69
+ callback();
70
+ }
71
+ }
72
+
73
+ // 点击 trigger overlay 以外的元素关闭
74
+ useEffect(() => {
75
+ if (!shouldToggle) return;
76
+
77
+ const handleDocumentClick = (e: MouseEvent | TouchEvent) => {
78
+ if (getRefDom(triggerRef)?.contains?.(e.target as Node) || hasPopupMouseDown.current) {
79
+ return;
80
+ }
81
+ if (visible) {
82
+ onVisibleChange(false, { trigger: "document" });
83
+ }
84
+ };
85
+ on(document, "mousedown", handleDocumentClick);
86
+ on(document, "touchend", handleDocumentClick);
87
+
88
+ // 清理事件监听
89
+ return () => {
90
+ off(document, "mousedown", handleDocumentClick);
91
+ off(document, "touchend", handleDocumentClick);
92
+ };
93
+ }, [shouldToggle, visible, onVisibleChange, triggerRef]);
94
+
95
+ // 弹出内容交互处理
96
+ function getPopupProps(): React.HTMLAttributes<HTMLElement> {
97
+ if (!shouldToggle) return {};
98
+
99
+ return {
100
+ onMouseEnter: (e: React.MouseEvent<HTMLElement>) => {
101
+ console.log("popup mouse enter");
102
+ // leaveFlag表示是从 trigger 元素 hover 过来的,避免频繁显示隐藏
103
+ if (trigger === "hover" && !leaveFlag.current) {
104
+ // 清除延迟显示定时器
105
+ clearTimeout(visibleTimer.current);
106
+ // 立即显示
107
+ onVisibleChange(true, { e, trigger: "trigger-element-hover" });
108
+ }
109
+ },
110
+ onMouseLeave: (e: React.MouseEvent<HTMLElement>) => {
111
+ console.log("popup mouse leave");
112
+ if (trigger === "hover") {
113
+ // 防止 鼠标移出后,马上移入 popup 元素,导致 popup 重新显示 特别是有延迟显示时 是一个防抖标志
114
+ leaveFlag.current = true;
115
+
116
+ // 清除延迟显示定时器
117
+ clearTimeout(visibleTimer.current);
118
+ // 立即隐藏
119
+ onVisibleChange(false, { e, trigger: "trigger-element-hover" });
120
+ }
121
+ },
122
+ onMouseDown: () => {
123
+ // 清除鼠标按下定时器
124
+ clearTimeout(mouseDownTimer.current);
125
+ // 鼠标按下时,触发 useEffect 中的 document click 事件,点击trigger和popup元素 不需要隐藏
126
+ hasPopupMouseDown.current = true;
127
+ mouseDownTimer.current = window.setTimeout(() => {
128
+ // 事件处理完毕后,重置标志
129
+ hasPopupMouseDown.current = false;
130
+ });
131
+ },
132
+ // 触摸结束时,和mouseDown 类似
133
+
134
+ onTouchEnd: () => {
135
+ clearTimeout(mouseDownTimer.current);
136
+ hasPopupMouseDown.current = true;
137
+ mouseDownTimer.current = window.setTimeout(() => {
138
+ hasPopupMouseDown.current = false;
139
+ });
140
+ }
141
+ };
142
+ }
143
+
144
+ // 整理 trigger props
145
+ function getTriggerProps(triggerNode: React.ReactElement<React.HTMLAttributes<HTMLElement>>) {
146
+ if (!shouldToggle) return {};
147
+
148
+ const triggerProps: React.HTMLAttributes<HTMLElement> & { ref?: React.Ref<HTMLElement> } = {
149
+ className: visible ? classNames(triggerNode.props.className, `t-popup-open`) : triggerNode.props.className,
150
+ onMouseDown: (e: React.MouseEvent<HTMLElement>) => {
151
+ if (trigger === "mousedown") {
152
+ callFuncWithDelay({
153
+ delay: visible ? appearDelay : exitDelay,
154
+ callback: () =>
155
+ onVisibleChange(!visible, {
156
+ e,
157
+ trigger: "trigger-element-mousedown"
158
+ })
159
+ });
160
+ }
161
+ triggerNode.props.onMouseDown?.(e);
162
+ },
163
+ onClick: (e: React.MouseEvent<HTMLElement>) => {
164
+ if (trigger === "click") {
165
+ callFuncWithDelay({
166
+ // appearDelay 和 exitDelay 分别表示点击时的延迟显示和隐藏
167
+ delay: visible ? appearDelay : exitDelay,
168
+ callback: () =>
169
+ onVisibleChange(!visible, {
170
+ e,
171
+ trigger: "trigger-element-click"
172
+ })
173
+ });
174
+ }
175
+ triggerNode.props.onClick?.(e);
176
+ },
177
+ onTouchStart: (e: React.TouchEvent<HTMLElement>) => {
178
+ if (trigger === "hover" || trigger === "mousedown") {
179
+ // leaveFlag 表示是从 trigger 元素 hover 过来的,避免频繁显示隐藏
180
+ leaveFlag.current = false;
181
+ callFuncWithDelay({
182
+ delay: appearDelay,
183
+ callback: () => onVisibleChange(true, { e, trigger: "trigger-element-hover" })
184
+ });
185
+ }
186
+ triggerNode.props.onTouchStart?.(e);
187
+ },
188
+ onMouseEnter: (e: React.MouseEvent<HTMLElement>) => {
189
+ if (trigger === "hover") {
190
+ leaveFlag.current = false;
191
+ callFuncWithDelay({
192
+ delay: appearDelay,
193
+ callback: () => onVisibleChange(true, { e, trigger: "trigger-element-hover" })
194
+ });
195
+ }
196
+ triggerNode.props.onMouseEnter?.(e);
197
+ },
198
+ onMouseLeave: (e: React.MouseEvent<HTMLElement>) => {
199
+ if (trigger === "hover") {
200
+ leaveFlag.current = false;
201
+ callFuncWithDelay({
202
+ delay: exitDelay,
203
+ callback: () => onVisibleChange(false, { e, trigger: "trigger-element-hover" })
204
+ });
205
+ }
206
+ triggerNode.props.onMouseLeave?.(e);
207
+ },
208
+ onFocus: (e: React.FocusEvent<HTMLElement>) => {
209
+ if (trigger === "focus") {
210
+ callFuncWithDelay({
211
+ delay: appearDelay,
212
+ callback: () => onVisibleChange(true, { e, trigger: "trigger-element-focus" })
213
+ });
214
+ }
215
+ triggerNode.props.onFocus?.(e);
216
+ },
217
+ onBlur: (e: React.FocusEvent<HTMLElement>) => {
218
+ if (trigger === "focus") {
219
+ callFuncWithDelay({
220
+ delay: appearDelay,
221
+ callback: () => onVisibleChange(false, { e, trigger: "trigger-element-blur" })
222
+ });
223
+ }
224
+ triggerNode.props.onBlur?.(e);
225
+ },
226
+ onContextMenu: (e: React.MouseEvent<HTMLElement>) => {
227
+ if (trigger === "context-menu") {
228
+ e.preventDefault();
229
+ callFuncWithDelay({
230
+ delay: appearDelay,
231
+ callback: () => onVisibleChange(true, { e, trigger: "context-menu" })
232
+ });
233
+ }
234
+ triggerNode.props.onContextMenu?.(e);
235
+ },
236
+ onKeyDown: (e: React.KeyboardEvent<HTMLElement>) => {
237
+ if (e?.key === ESC_KEY) {
238
+ callFuncWithDelay({
239
+ delay: exitDelay,
240
+ callback: () => onVisibleChange(false, { e, trigger: "keydown-esc" })
241
+ });
242
+ }
243
+ triggerNode.props.onKeyDown?.(e);
244
+ }
245
+ };
246
+ // 如果支持 ref 透传,composeRefs 返回一个函数,当组件挂载时,执行函数,triggerRef 和 tiggerNode 指向触发元素的dom
247
+ if (supportRef(triggerNode)) {
248
+ triggerProps.ref = composeRefs(triggerRef, getNodeRef(triggerNode));
249
+ } else {
250
+ // 标记 trigger 元素
251
+ triggerProps["data-popup"] = triggerDataKey.current;
252
+ }
253
+
254
+ return triggerProps;
255
+ }
256
+
257
+ // 整理 trigger 元素
258
+ function getTriggerNode(children: React.ReactNode) {
259
+ const triggerNode =
260
+ isValidElement(children) && !isFragment(children) ? children : React.createElement("span", {}, children);
261
+
262
+ return React.cloneElement(triggerNode, getTriggerProps(triggerNode));
263
+ }
264
+
265
+ // ref 透传失败时使用 dom 查找
266
+ const getTriggerDom = useCallback(() => {
267
+ if (typeof document === "undefined") return {};
268
+ return document.querySelector(`[data-popup="${triggerDataKey.current}"]`);
269
+ }, []);
270
+
271
+ return {
272
+ getTriggerNode,
273
+ getPopupProps,
274
+ getTriggerDom
275
+ };
276
+ }
package/popup/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ import _Popup from "./Popup";
2
+ import "./style/index";
3
+ export type { PopupProps, PopupRef } from "./Popup";
4
+ export * from "./type";
5
+ export const Popup = _Popup;
6
+ export default Popup;
@@ -0,0 +1 @@
1
+ import "./index.css";
@@ -0,0 +1 @@
1
+ import "../../styles/components/popup/_index.scss";
package/popup/type.ts ADDED
@@ -0,0 +1,130 @@
1
+ import { TNode, ClassName, Styles, AttachNode } from "../common";
2
+ import { MouseEvent, KeyboardEvent, FocusEvent, WheelEvent } from "react";
3
+
4
+ export interface TdPopupProps {
5
+ /**
6
+ * 制定挂载节点。数据类型为 String 时,会被当作选择器处理,进行节点查询。示例:'body' 或 () => document.body
7
+ * @default 'body'
8
+ */
9
+ attach?: AttachNode;
10
+ /**
11
+ * 触发元素,同 triggerElement
12
+ */
13
+ children?: TNode;
14
+ /**
15
+ * 浮层里面的内容
16
+ */
17
+ content?: TNode;
18
+ /**
19
+ * 延时显示或隐藏浮层,[延迟显示的时间,延迟隐藏的时间],单位:毫秒。如果只有一个时间,则表示显示和隐藏的延迟时间相同。示例 `'300'` 或者 `[200, 200]`。默认为:[250, 150]
20
+ */
21
+ delay?: number | Array<number>;
22
+ /**
23
+ * 是否在关闭浮层时销毁浮层
24
+ * @default false
25
+ */
26
+ destroyOnClose?: boolean;
27
+ /**
28
+ * 是否禁用组件
29
+ */
30
+ disabled?: boolean;
31
+ /**
32
+ * 浮层是否隐藏空内容,默认不隐藏
33
+ * @default false
34
+ */
35
+ hideEmptyPopup?: boolean;
36
+ /**
37
+ * 浮层类名,示例:'name1 name2 name3' 或 `['name1', 'name2']` 或 `[{ 'name1': true }]`
38
+ */
39
+ overlayClassName?: ClassName;
40
+ /**
41
+ * 浮层内容部分类名,示例:'name1 name2 name3' 或 `['name1', 'name2']` 或 `[{ 'name1': true }]`
42
+ */
43
+ overlayInnerClassName?: ClassName;
44
+ /**
45
+ * 浮层内容部分样式,第一个参数 `triggerElement` 表示触发元素 DOM 节点,第二个参数 `popupElement` 表示浮层元素 DOM 节点
46
+ */
47
+ overlayInnerStyle?: Styles | ((triggerElement: HTMLElement, popupElement: HTMLElement) => Styles);
48
+ /**
49
+ * 浮层样式,第一个参数 `triggerElement` 表示触发元素 DOM 节点,第二个参数 `popupElement` 表示浮层元素 DOM 节点
50
+ */
51
+ overlayStyle?: Styles | ((triggerElement: HTMLElement, popupElement: HTMLElement) => Styles);
52
+ /**
53
+ * 浮层出现位置
54
+ * @default top
55
+ */
56
+ placement?: PopupPlacement;
57
+ /**
58
+ * popper 初始化配置,详情参考 https://popper.js.org/docs/
59
+ */
60
+ popperOptions?: object;
61
+ /**
62
+ * 是否显示浮层箭头
63
+ * @default false
64
+ */
65
+ showArrow?: boolean;
66
+ /**
67
+ * 触发浮层出现的方式
68
+ * @default hover
69
+ */
70
+ trigger?: "hover" | "click" | "focus" | "mousedown" | "context-menu";
71
+ /**
72
+ * 触发元素。值类型为字符串表示元素选择器
73
+ */
74
+ triggerElement?: TNode;
75
+ /**
76
+ * 是否显示浮层
77
+ */
78
+ visible?: boolean;
79
+ /**
80
+ * 是否显示浮层,非受控属性
81
+ */
82
+ defaultVisible?: boolean;
83
+ /**
84
+ * 组件层级,Web 侧样式默认为 5500,移动端和小程序样式默认为 1500
85
+ */
86
+ zIndex?: number;
87
+ /**
88
+ * 下拉选项滚动事件
89
+ */
90
+ onScroll?: (context: { e: WheelEvent<HTMLDivElement> }) => void;
91
+ /**
92
+ * 下拉滚动触底事件,常用于滚动到底执行具体业务逻辑
93
+ */
94
+ onScrollToBottom?: (context: { e: WheelEvent<HTMLDivElement> }) => void;
95
+ /**
96
+ * 当浮层隐藏或显示时触发,`trigger=document` 表示点击非浮层元素触发;`trigger=context-menu` 表示右击触发
97
+ */
98
+ onVisibleChange?: (visible: boolean, context: PopupVisibleChangeContext) => void;
99
+ }
100
+
101
+ export type PopupPlacement =
102
+ | "top"
103
+ | "left"
104
+ | "right"
105
+ | "bottom"
106
+ | "top-left"
107
+ | "top-right"
108
+ | "bottom-left"
109
+ | "bottom-right"
110
+ | "left-top"
111
+ | "left-bottom"
112
+ | "right-top"
113
+ | "right-bottom";
114
+
115
+ export interface PopupVisibleChangeContext {
116
+ e?: PopupTriggerEvent;
117
+ trigger?: PopupTriggerSource;
118
+ }
119
+
120
+ export type PopupTriggerEvent = MouseEvent<HTMLDivElement> | FocusEvent<HTMLDivElement> | KeyboardEvent<HTMLDivElement>;
121
+
122
+ export type PopupTriggerSource =
123
+ | "document"
124
+ | "trigger-element-click"
125
+ | "trigger-element-hover"
126
+ | "trigger-element-blur"
127
+ | "trigger-element-focus"
128
+ | "trigger-element-mousedown"
129
+ | "context-menu"
130
+ | "keydown-esc";
@@ -0,0 +1,63 @@
1
+ import React, { forwardRef, useMemo, useImperativeHandle } from "react";
2
+ import { createPortal } from "react-dom";
3
+ import { canUseDocument } from "../utils/dom";
4
+ import { AttachNode, AttachNodeReturnValue } from "../common";
5
+ import useIsomorphicLayoutEffect from "../hooks/useLayoutEffect";
6
+
7
+ export interface PortalProps {
8
+ /**
9
+ * 指定挂载的 HTML 节点, false 为挂载在 body
10
+ */
11
+ attach?: React.ReactElement | AttachNode | boolean;
12
+ /**
13
+ * 触发元素
14
+ */
15
+ triggerNode?: HTMLElement;
16
+ children: React.ReactNode;
17
+ }
18
+ // 获取挂载的 HTML 节点
19
+
20
+ export function getAttach(attach: PortalProps["attach"], triggerNode?: HTMLElement): AttachNodeReturnValue {
21
+ if (!canUseDocument) return null;
22
+
23
+ let el: AttachNodeReturnValue = null;
24
+ if (typeof attach === "string") {
25
+ el = document.querySelector(attach);
26
+ }
27
+ if (typeof attach === "function") {
28
+ el = attach(triggerNode);
29
+ }
30
+ if (typeof attach === "object" && attach instanceof window.HTMLElement) {
31
+ el = attach;
32
+ }
33
+
34
+ // fix el in iframe
35
+ if (el && el.nodeType === 1) return el;
36
+
37
+ return document.body;
38
+ }
39
+
40
+ const Portal = forwardRef((props: PortalProps, ref) => {
41
+ const { attach, children, triggerNode } = props;
42
+ const container = useMemo(() => {
43
+ const div = document.createElement("div");
44
+ div.className = "portal-container bg-primary";
45
+ return div;
46
+ }, []);
47
+ // 兼容 SSR 环境
48
+ useIsomorphicLayoutEffect(() => {
49
+ const parentElement = getAttach(attach, triggerNode);
50
+ parentElement?.appendChild?.(container);
51
+ // Clean up the container when the component unmounts
52
+ return () => {
53
+ parentElement?.removeChild?.(container);
54
+ };
55
+ }, [container, attach, triggerNode]);
56
+ // 自定义暴露给父组件的ref是container
57
+ useImperativeHandle(ref, () => container);
58
+
59
+ return canUseDocument ? createPortal(children, container) : null;
60
+ });
61
+
62
+ Portal.displayName = "Portal";
63
+ export default Portal;
@@ -0,0 +1 @@
1
+ export * from "./Portal";
@@ -0,0 +1,162 @@
1
+ import React, { useEffect, useMemo } from "react";
2
+ import classNames from "classnames";
3
+ import { isNumber, isString, get } from "lodash-es";
4
+
5
+ import useConfig from "../hooks/useConfig";
6
+ import useDomRefCallback from "../hooks/useDomRefCallback";
7
+
8
+ import { StyledProps } from "../common";
9
+ import { SelectValue, TdOptionProps, TdSelectProps, SelectKeysType, SelectOption } from "./type";
10
+
11
+ export interface SelectOptionProps
12
+ extends StyledProps,
13
+ TdOptionProps,
14
+ Pick<TdSelectProps, "size" | "multiple" | "max"> {
15
+ selectedValue?: SelectValue;
16
+ children?: React.ReactNode;
17
+ onSelect?: (
18
+ value: string | number,
19
+ context: {
20
+ label?: string;
21
+ selected?: boolean;
22
+ event: React.MouseEvent<HTMLLIElement>;
23
+ restData?: Record<string, unknown>;
24
+ }
25
+ ) => void;
26
+ onCheckAllChange?: (checkAll: boolean, e: React.MouseEvent<HTMLLIElement>) => void;
27
+ restData?: Record<string, unknown>;
28
+ keys?: SelectKeysType;
29
+ optionLength?: number;
30
+ isVirtual?: boolean;
31
+ onRowMounted?: (rowData: { ref: HTMLElement; data: SelectOption }) => void;
32
+ }
33
+
34
+ const componentType = "select";
35
+
36
+ const Option: React.FC<SelectOptionProps> = (props) => {
37
+ const {
38
+ disabled: propDisabled,
39
+ label: propLabel,
40
+ title: propTitle,
41
+ selectedValue,
42
+ checkAll,
43
+ multiple,
44
+ size,
45
+ max,
46
+ keys,
47
+ value,
48
+ onSelect,
49
+ children,
50
+ content,
51
+ restData,
52
+ style,
53
+ className,
54
+ isVirtual
55
+ } = props;
56
+ let selected: boolean;
57
+ let indeterminate: boolean;
58
+ const label = propLabel || value;
59
+ const disabled = propDisabled || (multiple && Array.isArray(selectedValue) && max && selectedValue.length >= max);
60
+
61
+ const titleContent = useMemo(() => {
62
+ // 外部设置 props,说明希望受控
63
+ const controlledTitle = Reflect.has(props, "title");
64
+ if (controlledTitle) return propTitle;
65
+ if (typeof label === "string") return label;
66
+ return null;
67
+ // eslint-disable-next-line react-hooks/exhaustive-deps
68
+ }, [propTitle, label]);
69
+ const { classPrefix } = useConfig();
70
+ // 使用斜八角动画
71
+ const [optionRef, setRefCurrent] = useDomRefCallback();
72
+
73
+ useEffect(() => {
74
+ if (isVirtual && optionRef) {
75
+ props.onRowMounted?.({
76
+ ref: optionRef,
77
+ data: props
78
+ });
79
+ }
80
+ // eslint-disable-next-line
81
+ }, [isVirtual, optionRef]);
82
+
83
+ // 处理单选场景
84
+ if (!multiple) {
85
+ selected =
86
+ isNumber(selectedValue) || isString(selectedValue)
87
+ ? value === selectedValue
88
+ : value === get(selectedValue, keys?.value || "value");
89
+ }
90
+
91
+ // 处理多选场景
92
+ if (multiple && Array.isArray(selectedValue)) {
93
+ selected = selectedValue.some((item) => {
94
+ if (isNumber(item) || isString(item)) {
95
+ // 如果非 object 类型
96
+ return item === value;
97
+ }
98
+ return get(item, keys?.value || "value") === value;
99
+ });
100
+ if (props.checkAll) {
101
+ selected = selectedValue.length === props.optionLength;
102
+ indeterminate = selectedValue.length > 0 && !selected;
103
+ }
104
+ }
105
+
106
+ const handleSelect = (event: React.MouseEvent<HTMLLIElement>) => {
107
+ if (!disabled && !checkAll) {
108
+ if (value) onSelect?.(value, { label: String(label), selected, event, restData });
109
+ }
110
+ if (checkAll) {
111
+ props.onCheckAllChange?.(selected, event);
112
+ }
113
+ };
114
+
115
+ const renderItem = (children: React.ReactNode) => {
116
+ if (multiple) {
117
+ return (
118
+ <label
119
+ className={classNames(`${classPrefix}-checkbox`, {
120
+ [`${classPrefix}-is-indeterminate`]: indeterminate,
121
+ [`${classPrefix}-is-disabled`]: disabled,
122
+ [`${classPrefix}-is-checked`]: selected
123
+ })}
124
+ title={titleContent}
125
+ >
126
+ <input
127
+ type="checkbox"
128
+ className={classNames(`${classPrefix}-checkbox__former`)}
129
+ value=""
130
+ disabled={disabled && !selected}
131
+ onClick={(e) => {
132
+ e.stopPropagation();
133
+ e.nativeEvent.stopImmediatePropagation();
134
+ }}
135
+ />
136
+ <span className={classNames(`${classPrefix}-checkbox__input`)}></span>
137
+ <span className={classNames(`${classPrefix}-checkbox__label`)}>{children || content || label}</span>
138
+ </label>
139
+ );
140
+ }
141
+ return <span title={titleContent}>{children || content || label}</span>;
142
+ };
143
+
144
+ return (
145
+ <li
146
+ className={classNames(className, `${classPrefix}-${componentType}-option`, {
147
+ [`${classPrefix}-is-disabled`]: disabled,
148
+ [`${classPrefix}-is-selected`]: selected,
149
+ [`${classPrefix}-size-s`]: size === "small",
150
+ [`${classPrefix}-size-l`]: size === "large"
151
+ })}
152
+ key={value}
153
+ onClick={handleSelect}
154
+ ref={setRefCurrent}
155
+ style={style}
156
+ >
157
+ {renderItem(children)}
158
+ </li>
159
+ );
160
+ };
161
+
162
+ export default Option;
@@ -0,0 +1,30 @@
1
+ import React from "react";
2
+ import classNames from "classnames";
3
+ import useConfig from "../hooks/useConfig";
4
+
5
+ import { TdOptionGroupProps } from "./type";
6
+ import { optionGroupDefaultProps } from "./defaultProps";
7
+ import useDefaultProps from "../hooks/useDefaultProps";
8
+
9
+ export interface SelectGOptionGroupProps extends TdOptionGroupProps {
10
+ children?: React.ReactNode;
11
+ }
12
+
13
+ const OptionGroup: React.FC<SelectGOptionGroupProps> = (props) => {
14
+ const { children, label, divider } = useDefaultProps<SelectGOptionGroupProps>(props, optionGroupDefaultProps);
15
+
16
+ const { classPrefix } = useConfig();
17
+
18
+ return (
19
+ <li
20
+ className={classNames(`${classPrefix}-select-option-group`, {
21
+ [`${classPrefix}-select-option-group__divider`]: divider
22
+ })}
23
+ >
24
+ {(label ?? false) && <div className={`${classPrefix}-select-option-group__header`}>{label}</div>}
25
+ {children}
26
+ </li>
27
+ );
28
+ };
29
+
30
+ export default OptionGroup;