@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,131 @@
1
+ import React, { MouseEvent, KeyboardEvent, ReactNode, Fragment } from "react";
2
+ import { isFunction } from "lodash-es";
3
+ import { TagInputChangeContext, TagInputValue, TdTagInputProps } from "../type";
4
+ import { InputValue } from "../../input";
5
+ import { Tag } from "../../tag";
6
+ import useConfig from "../../hooks/useConfig";
7
+ import useControlled from "../../hooks/useControlled";
8
+ import { DragSortInnerProps } from "../../hooks/useDragSorter";
9
+
10
+ export type ChangeParams = [TagInputChangeContext];
11
+
12
+ interface TagInputProps extends TdTagInputProps, DragSortInnerProps {
13
+ options?: unknown[]; // 参数穿透options, 给SelectInput/SelectInput 自定义选中项呈现的内容和多选状态下设置折叠项内容
14
+ }
15
+
16
+ export default function useTagList(props: TagInputProps) {
17
+ const { classPrefix: prefix } = useConfig();
18
+ const { onRemove, max, minCollapsedNum, size, disabled, readonly, tagProps, tag, collapsedItems, getDragProps } =
19
+ props;
20
+ const [tagValue, setTagValue] = useControlled(props, "value", props.onChange);
21
+
22
+ const onClose = (p: { e?: MouseEvent<SVGSVGElement>; index: number }) => {
23
+ const arr = [...tagValue];
24
+ const [item] = arr.splice(p.index, 1);
25
+ setTagValue(arr, { trigger: "tag-remove", ...p, item });
26
+ onRemove?.({ ...p, item, trigger: "tag-remove", value: arr });
27
+ };
28
+
29
+ const clearAll = (context: { e: MouseEvent<SVGSVGElement> }) => {
30
+ setTagValue([], { trigger: "clear", e: context.e });
31
+ };
32
+
33
+ // 按下 Enter 键,新增标签
34
+ const onInnerEnter = (value: InputValue, context: { e: KeyboardEvent<HTMLInputElement> }) => {
35
+ const valueStr = value ? String(value).trim() : "";
36
+ let newValue: TagInputValue = tagValue;
37
+ const isLimitExceeded = max && tagValue?.length >= max;
38
+ if (valueStr && !isLimitExceeded) {
39
+ newValue = tagValue instanceof Array ? tagValue.concat(String(valueStr)) : [valueStr];
40
+ setTagValue(newValue, {
41
+ trigger: "enter",
42
+ index: newValue.length - 1,
43
+ item: valueStr,
44
+ e: context.e
45
+ });
46
+ }
47
+ props?.onEnter?.(newValue, { ...context, inputValue: value });
48
+ };
49
+
50
+ const onInputBackspaceKeyUp = (_value: InputValue) => {
51
+ if (!tagValue || !tagValue.length) return;
52
+ };
53
+
54
+ // 按下回退键,删除标签
55
+ const onInputBackspaceKeyDown = (value: InputValue, context: { e: KeyboardEvent<HTMLInputElement> }) => {
56
+ const { e } = context;
57
+ if (!tagValue || !tagValue.length || readonly) return;
58
+ // 回车键删除,输入框值为空时,才允许 Backspace 删除标签
59
+ if (!value && ["Backspace", "NumpadDelete"].includes(e.key)) {
60
+ const index = tagValue.length - 1;
61
+ const item = tagValue[index];
62
+ const trigger = "backspace";
63
+ const newValue = tagValue.slice(0, -1);
64
+ setTagValue(newValue, { e, index, item, trigger });
65
+ onRemove?.({ e, index, item, trigger, value: newValue });
66
+ }
67
+ };
68
+
69
+ const renderLabel = ({ displayNode, label }: { displayNode: ReactNode; label: ReactNode }) => {
70
+ const newList = minCollapsedNum ? tagValue.slice(0, minCollapsedNum) : tagValue;
71
+
72
+ const list = displayNode
73
+ ? [<Fragment key="display-node">{displayNode}</Fragment>]
74
+ : newList?.map((item, index) => {
75
+ const tagContent = isFunction(tag) ? tag({ value: item }) : tag;
76
+ return (
77
+ <Tag
78
+ key={index}
79
+ size={size}
80
+ disabled={disabled}
81
+ onClose={(context) => onClose({ e: context.e, index })}
82
+ closable={!readonly && !disabled}
83
+ {...getDragProps?.(index, item)}
84
+ {...tagProps}
85
+ >
86
+ {tagContent ?? item}
87
+ </Tag>
88
+ );
89
+ });
90
+ if (label) {
91
+ list?.unshift(
92
+ <div className={`${prefix}-tag-input__prefix`} key="label">
93
+ {label}
94
+ </div>
95
+ );
96
+ }
97
+ // 超出省略
98
+ if (newList.length !== tagValue.length) {
99
+ const len = tagValue.length - newList.length;
100
+ // 这里会从selectInput/SelectInput中传递options参数,用于自定义选中项呈现的内容和多选状态下设置折叠项内容
101
+ const options = Array.isArray(props?.options) ? props.options : tagValue;
102
+ const params = {
103
+ value: tagValue,
104
+ count: tagValue.length - minCollapsedNum,
105
+ collapsedTags: tagValue.slice(minCollapsedNum, tagValue.length),
106
+ collapsedSelectedItems: options.slice(minCollapsedNum, tagValue.length),
107
+ onClose
108
+ };
109
+ const more = isFunction(collapsedItems) ? collapsedItems(params) : collapsedItems;
110
+ list.push(
111
+ <Fragment key="more">
112
+ {more ?? (
113
+ <Tag size={size} {...tagProps}>
114
+ +{len}
115
+ </Tag>
116
+ )}
117
+ </Fragment>
118
+ );
119
+ }
120
+ return list;
121
+ };
122
+ return {
123
+ tagValue,
124
+ clearAll,
125
+ onClose,
126
+ onInnerEnter,
127
+ onInputBackspaceKeyDown,
128
+ onInputBackspaceKeyUp,
129
+ renderLabel
130
+ };
131
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * 当标签数量过多时,输入框显示不下,则需要滚动查看,以下为滚动逻辑
3
+ * 如果标签过多时的处理方式,是标签省略,则不需要此功能
4
+ */
5
+
6
+ import { isFunction } from "lodash-es";
7
+ import { useRef, useEffect, useState, WheelEvent } from "react";
8
+ import { TdTagInputProps } from "../type";
9
+
10
+ let mouseEnterTimer: ReturnType<typeof setTimeout> | null = null;
11
+
12
+ export default function useTagScroll(props: TdTagInputProps) {
13
+ const tagInputRef = useRef<{ currentElement: HTMLDivElement }>(null);
14
+ const { excessTagsDisplayType = "scroll", readonly, disabled } = props;
15
+ // 允许向右滚动的最大距离
16
+ const [scrollDistance, setScrollDistance] = useState(0);
17
+ const [scrollElement, setScrollElement] = useState<HTMLDivElement>();
18
+
19
+ const updateScrollElement = (element: HTMLDivElement) => {
20
+ const scrollElement = element.children[0] as HTMLDivElement;
21
+ setScrollElement(scrollElement);
22
+ };
23
+
24
+ const updateScrollDistance = () => {
25
+ if (!scrollElement) return;
26
+ setScrollDistance(scrollElement.scrollWidth - scrollElement.clientWidth);
27
+ };
28
+
29
+ const scrollTo = (distance: number) => {
30
+ if (isFunction(scrollElement?.scroll)) {
31
+ scrollElement.scroll({ left: distance, behavior: "smooth" });
32
+ }
33
+ };
34
+
35
+ const scrollToRight = () => {
36
+ updateScrollDistance();
37
+ scrollTo(scrollDistance);
38
+ };
39
+
40
+ const scrollToLeft = () => {
41
+ scrollTo(0);
42
+ };
43
+
44
+ // TODO:MAC 电脑横向滚动,Windows 纵向滚动。当前只处理了横向滚动
45
+ const onWheel = ({ e }: { e: WheelEvent<HTMLDivElement> }) => {
46
+ if (readonly || disabled) return;
47
+ if (!scrollElement) return;
48
+ if (e.deltaX > 0) {
49
+ const distance = Math.min(scrollElement.scrollLeft + 120, scrollDistance);
50
+ scrollTo(distance);
51
+ } else {
52
+ const distance = Math.max(scrollElement.scrollLeft - 120, 0);
53
+ scrollTo(distance);
54
+ }
55
+ };
56
+
57
+ // 鼠标 hover,自动滑动到最右侧,以便输入新标签
58
+ const scrollToRightOnEnter = () => {
59
+ if (excessTagsDisplayType !== "scroll") return;
60
+ // 一闪而过的 mousenter 不需要执行
61
+ mouseEnterTimer = setTimeout(() => {
62
+ scrollToRight();
63
+ if (mouseEnterTimer) clearTimeout(mouseEnterTimer);
64
+ }, 100);
65
+ };
66
+
67
+ const scrollToLeftOnLeave = () => {
68
+ if (excessTagsDisplayType !== "scroll") return;
69
+ scrollTo(0);
70
+ if (mouseEnterTimer) {
71
+ clearTimeout(mouseEnterTimer);
72
+ }
73
+ };
74
+
75
+ const clearScroll = () => {
76
+ if (mouseEnterTimer) clearTimeout(mouseEnterTimer);
77
+ };
78
+
79
+ const initScroll = (element: HTMLDivElement) => {
80
+ if (!element) return;
81
+ updateScrollElement(element);
82
+ };
83
+
84
+ useEffect(() => {
85
+ if (tagInputRef.current) initScroll(tagInputRef?.current?.currentElement);
86
+ return clearScroll;
87
+ // eslint-disable-next-line react-hooks/exhaustive-deps
88
+ }, []);
89
+
90
+ return {
91
+ initScroll,
92
+ clearScroll,
93
+ tagInputRef,
94
+ scrollElement,
95
+ scrollDistance,
96
+ scrollTo,
97
+ scrollToRight,
98
+ scrollToLeft,
99
+ updateScrollElement,
100
+ updateScrollDistance,
101
+ onWheel,
102
+ scrollToRightOnEnter,
103
+ scrollToLeftOnLeave
104
+ };
105
+ }
@@ -0,0 +1,9 @@
1
+ import _TagInput from "./TagInput";
2
+ import "./style/index.js";
3
+
4
+ export * from "./type";
5
+
6
+ export type { TagInputProps } from "./TagInput";
7
+
8
+ export const TagInput = _TagInput;
9
+ export default TagInput;
@@ -0,0 +1 @@
1
+ import "./index.css";
@@ -0,0 +1 @@
1
+ import "../../styles/components/tag-input/_index.scss";
@@ -0,0 +1,224 @@
1
+ import { InputProps } from "../input";
2
+ import { TagProps } from "../tag";
3
+ import { TNode, TElement, SizeEnum } from "../common";
4
+ import { MouseEvent, KeyboardEvent, ClipboardEvent, FocusEvent, FormEvent, CompositionEvent } from "react";
5
+
6
+ export interface TdTagInputProps {
7
+ /**
8
+ * 宽度随内容自适应
9
+ * @default false
10
+ */
11
+ autoWidth?: boolean;
12
+ /**
13
+ * 无边框模式
14
+ * @default false
15
+ */
16
+ borderless?: boolean;
17
+ /**
18
+ * 是否可清空
19
+ * @default false
20
+ */
21
+ clearable?: boolean;
22
+ /**
23
+ * 标签过多的情况下,折叠项内容,默认为 `+N`。如果需要悬浮就显示其他内容,可以使用 collapsedItems 自定义。`value` 表示当前存在的所有标签,`collapsedSelectedItems` 表示折叠的标签,`count` 表示折叠的数量,`onClose` 表示移除标签的事件回调
24
+ */
25
+ collapsedItems?: TNode<{
26
+ value: TagInputValue;
27
+ collapsedSelectedItems: TagInputValue;
28
+ count: number;
29
+ onClose: (context: { index: number; e?: MouseEvent }) => void;
30
+ }>;
31
+ /**
32
+ * 是否禁用标签输入框
33
+ */
34
+ disabled?: boolean;
35
+ /**
36
+ * 拖拽调整标签顺序
37
+ * @default false
38
+ */
39
+ dragSort?: boolean;
40
+ /**
41
+ * 标签超出时的呈现方式,有两种:横向滚动显示 和 换行显示
42
+ * @default break-line
43
+ */
44
+ excessTagsDisplayType?: "scroll" | "break-line";
45
+ /**
46
+ * 标签最大换行数
47
+ * @default 1
48
+ */
49
+ maxRows?: number;
50
+ /**
51
+ * 透传 Input 输入框组件全部属性
52
+ */
53
+ inputProps?: InputProps;
54
+ /**
55
+ * 输入框的值
56
+ * @default ''
57
+ */
58
+ inputValue?: string;
59
+ /**
60
+ * 输入框的值,非受控属性
61
+ * @default ''
62
+ */
63
+ defaultInputValue?: string;
64
+ /**
65
+ * 左侧文本
66
+ */
67
+ label?: TNode;
68
+ /**
69
+ * 最大允许输入的标签数量
70
+ */
71
+ max?: number;
72
+ /**
73
+ * 最小折叠数量,用于标签数量过多的情况下折叠选中项,超出该数值的选中项折叠。值为 0 则表示不折叠
74
+ * @default 0
75
+ */
76
+ minCollapsedNum?: number;
77
+ /**
78
+ * 占位符
79
+ */
80
+ placeholder?: string;
81
+ /**
82
+ * 组件前置图标
83
+ */
84
+ prefixIcon?: TElement;
85
+ /**
86
+ * 只读状态,值为真会隐藏标签移除按钮和输入框
87
+ * @default false
88
+ */
89
+ readonly?: boolean;
90
+ /**
91
+ * 组件尺寸
92
+ * @default medium
93
+ */
94
+ size?: SizeEnum;
95
+ /**
96
+ * 输入框状态
97
+ */
98
+ status?: "default" | "success" | "warning" | "error";
99
+ /**
100
+ * 后置图标前的后置内容
101
+ */
102
+ suffix?: TNode;
103
+ /**
104
+ * 组件后置图标
105
+ */
106
+ suffixIcon?: TElement;
107
+ /**
108
+ * 自定义标签的内部内容,每一个标签的当前值。注意和 `valueDisplay` 区分,`valueDisplay` 是用来定义全部标签内容,而非某一个标签
109
+ */
110
+ tag?: string | TNode<{ value: string | number }>;
111
+ /**
112
+ * 透传 Tag 组件全部属性
113
+ */
114
+ tagProps?: TagProps;
115
+ /**
116
+ * 输入框下方提示文本,会根据不同的 `status` 呈现不同的样式
117
+ */
118
+ tips?: TNode;
119
+ /**
120
+ * 值
121
+ * @default []
122
+ */
123
+ value?: TagInputValue;
124
+ /**
125
+ * 值,非受控属性
126
+ * @default []
127
+ */
128
+ defaultValue?: TagInputValue;
129
+ /**
130
+ * 自定义值呈现的全部内容,参数为所有标签的值
131
+ */
132
+ valueDisplay?:
133
+ | string
134
+ | TNode<{
135
+ value: TagInputValue;
136
+ onClose: (index: number, item?: string | number) => void;
137
+ }>;
138
+ /**
139
+ * 失去焦点时触发
140
+ */
141
+ onBlur?: (value: TagInputValue, context: { inputValue: string; e: FocusEvent<HTMLInputElement> }) => void;
142
+ /**
143
+ * 值变化时触发,参数 `context.trigger` 表示数据变化的触发来源;`context.index` 指当前变化项的下标;`context.item` 指当前变化项;`context.e` 表示事件参数
144
+ */
145
+ onChange?: (value: TagInputValue, context: TagInputChangeContext) => void;
146
+ /**
147
+ * 清空按钮点击时触发
148
+ */
149
+ onClear?: (context: { e: MouseEvent<SVGSVGElement> }) => void;
150
+ /**
151
+ * 点击组件时触发
152
+ */
153
+ onClick?: (context: { e: MouseEvent<HTMLDivElement> }) => void;
154
+ /**
155
+ * 【开发中】拖拽排序时触发
156
+ */
157
+ onDragSort?: (context: TagInputDragSortContext) => void;
158
+ /**
159
+ * 按键按下 Enter 时触发
160
+ */
161
+ onEnter?: (value: TagInputValue, context: { e: KeyboardEvent<HTMLDivElement>; inputValue: string }) => void;
162
+ /**
163
+ * 聚焦时触发
164
+ */
165
+ onFocus?: (value: TagInputValue, context: { inputValue: string; e: FocusEvent<HTMLInputElement> }) => void;
166
+ /**
167
+ * 输入框值发生变化时触发,`context.trigger` 表示触发输入框值变化的来源:文本输入触发、清除按钮触发、回车键触发等
168
+ */
169
+ onInputChange?: (value: string, context?: InputValueChangeContext) => void;
170
+ /**
171
+ * 进入输入框时触发
172
+ */
173
+ onMouseenter?: (context: { e: MouseEvent<HTMLDivElement> }) => void;
174
+ /**
175
+ * 离开输入框时触发
176
+ */
177
+ onMouseleave?: (context: { e: MouseEvent<HTMLDivElement> }) => void;
178
+ /**
179
+ * 粘贴事件,`pasteValue` 表示粘贴板的内容
180
+ */
181
+ onPaste?: (context: { e: ClipboardEvent<HTMLDivElement>; pasteValue: string }) => void;
182
+ /**
183
+ * 移除单个标签时触发
184
+ */
185
+ onRemove?: (context: TagInputRemoveContext) => void;
186
+ }
187
+
188
+ export type TagInputValue = Array<string | number>;
189
+
190
+ export interface TagInputChangeContext {
191
+ trigger: TagInputTriggerSource;
192
+ index?: number;
193
+ item?: string | number;
194
+ e?: MouseEvent<SVGSVGElement> | KeyboardEvent<HTMLInputElement>;
195
+ }
196
+
197
+ export type TagInputTriggerSource = "enter" | "tag-remove" | "backspace" | "clear";
198
+
199
+ export interface TagInputDragSortContext {
200
+ newTags: TagInputValue;
201
+ currentIndex: number;
202
+ current: string | number;
203
+ targetIndex: number;
204
+ target: string | number;
205
+ }
206
+
207
+ export interface InputValueChangeContext {
208
+ e?:
209
+ | FormEvent<HTMLInputElement>
210
+ | MouseEvent<HTMLElement | SVGElement>
211
+ | CompositionEvent<HTMLDivElement>
212
+ | KeyboardEvent<HTMLInputElement>;
213
+ trigger: "input" | "clear" | "enter" | "blur";
214
+ }
215
+
216
+ export interface TagInputRemoveContext {
217
+ value: TagInputValue;
218
+ index: number;
219
+ item: string | number;
220
+ e?: MouseEvent<SVGSVGElement> | KeyboardEvent<HTMLInputElement>;
221
+ trigger: TagInputRemoveTrigger;
222
+ }
223
+
224
+ export type TagInputRemoveTrigger = "tag-remove" | "backspace";
@@ -0,0 +1,131 @@
1
+ import React, { MouseEvent, KeyboardEvent, ReactNode, Fragment } from "react";
2
+ import { isFunction } from "lodash-es";
3
+ import { TagInputChangeContext, TagInputValue, TdTagInputProps } from "./type";
4
+ import { InputValue } from "../input";
5
+ import { Tag } from "../tag";
6
+ import useConfig from "../hooks/useConfig";
7
+ import useControlled from "../hooks/useControlled";
8
+ import { DragSortInnerProps } from "../hooks/useDragSorter";
9
+
10
+ export type ChangeParams = [TagInputChangeContext];
11
+
12
+ interface TagInputProps extends TdTagInputProps, DragSortInnerProps {
13
+ options?: unknown[]; // 参数穿透options, 给SelectInput/SelectInput 自定义选中项呈现的内容和多选状态下设置折叠项内容
14
+ }
15
+
16
+ export default function useTagList(props: TagInputProps) {
17
+ const { classPrefix: prefix } = useConfig();
18
+ const { onRemove, max, minCollapsedNum, size, disabled, readonly, tagProps, tag, collapsedItems, getDragProps } =
19
+ props;
20
+ const [tagValue, setTagValue] = useControlled(props, "value", props.onChange);
21
+
22
+ const onClose = (p: { e?: MouseEvent<SVGSVGElement>; index: number }) => {
23
+ const arr = [...tagValue];
24
+ const [item] = arr.splice(p.index, 1);
25
+ setTagValue(arr, { trigger: "tag-remove", ...p, item });
26
+ onRemove?.({ ...p, item, trigger: "tag-remove", value: arr });
27
+ };
28
+
29
+ const clearAll = (context: { e: MouseEvent<SVGSVGElement> }) => {
30
+ setTagValue([], { trigger: "clear", e: context.e });
31
+ };
32
+
33
+ // 按下 Enter 键,新增标签
34
+ const onInnerEnter = (value: InputValue, context: { e: KeyboardEvent<HTMLInputElement> }) => {
35
+ const valueStr = value ? String(value).trim() : "";
36
+ let newValue: TagInputValue = tagValue;
37
+ const isLimitExceeded = max && tagValue?.length >= max;
38
+ if (valueStr && !isLimitExceeded) {
39
+ newValue = tagValue instanceof Array ? tagValue.concat(String(valueStr)) : [valueStr];
40
+ setTagValue(newValue, {
41
+ trigger: "enter",
42
+ index: newValue.length - 1,
43
+ item: valueStr,
44
+ e: context.e
45
+ });
46
+ }
47
+ props?.onEnter?.(newValue, { ...context, inputValue: value });
48
+ };
49
+
50
+ const onInputBackspaceKeyUp = () => {
51
+ if (!tagValue || !tagValue.length) return;
52
+ };
53
+
54
+ // 按下回退键,删除标签
55
+ const onInputBackspaceKeyDown = (value: InputValue, context: { e: KeyboardEvent<HTMLInputElement> }) => {
56
+ const { e } = context;
57
+ if (!tagValue || !tagValue.length || readonly) return;
58
+ // 回车键删除,输入框值为空时,才允许 Backspace 删除标签
59
+ if (!value && ["Backspace", "NumpadDelete"].includes(e.key)) {
60
+ const index = tagValue.length - 1;
61
+ const item = tagValue[index];
62
+ const trigger = "backspace";
63
+ const newValue = tagValue.slice(0, -1);
64
+ setTagValue(newValue, { e, index, item, trigger });
65
+ onRemove?.({ e, index, item, trigger, value: newValue });
66
+ }
67
+ };
68
+
69
+ const renderLabel = ({ displayNode, label }: { displayNode: ReactNode; label: ReactNode }) => {
70
+ const newList = minCollapsedNum ? tagValue.slice(0, minCollapsedNum) : tagValue;
71
+
72
+ const list = displayNode
73
+ ? [<Fragment key="display-node">{displayNode}</Fragment>]
74
+ : newList?.map((item, index) => {
75
+ const tagContent = isFunction(tag) ? tag({ value: item }) : tag;
76
+ return (
77
+ <Tag
78
+ key={index}
79
+ size={size}
80
+ disabled={disabled}
81
+ onClose={(context) => onClose({ e: context.e, index })}
82
+ closable={!readonly && !disabled}
83
+ {...getDragProps?.(index, item)}
84
+ {...tagProps}
85
+ >
86
+ {tagContent ?? item}
87
+ </Tag>
88
+ );
89
+ });
90
+ if (label) {
91
+ list?.unshift(
92
+ <div className={`${prefix}-tag-input__prefix`} key="label">
93
+ {label}
94
+ </div>
95
+ );
96
+ }
97
+ // 超出省略
98
+ if (newList.length !== tagValue.length) {
99
+ const len = tagValue.length - newList.length;
100
+ // 这里会从selectInput/SelectInput中传递options参数,用于自定义选中项呈现的内容和多选状态下设置折叠项内容
101
+ const options = Array.isArray(props?.options) ? props.options : tagValue;
102
+ const params = {
103
+ value: tagValue,
104
+ count: tagValue.length - minCollapsedNum,
105
+ collapsedTags: tagValue.slice(minCollapsedNum, tagValue.length),
106
+ collapsedSelectedItems: options.slice(minCollapsedNum, tagValue.length) as TagInputValue,
107
+ onClose
108
+ };
109
+ const more = isFunction(collapsedItems) ? collapsedItems(params) : collapsedItems;
110
+ list.push(
111
+ <Fragment key="more">
112
+ {more ?? (
113
+ <Tag size={size} {...tagProps}>
114
+ +{len}
115
+ </Tag>
116
+ )}
117
+ </Fragment>
118
+ );
119
+ }
120
+ return list;
121
+ };
122
+ return {
123
+ tagValue,
124
+ clearAll,
125
+ onClose,
126
+ onInnerEnter,
127
+ onInputBackspaceKeyDown,
128
+ onInputBackspaceKeyUp,
129
+ renderLabel
130
+ };
131
+ }
@@ -0,0 +1,14 @@
1
+ import { Ref } from "react";
2
+
3
+ // 同时处理多个 ref
4
+ export default function composeRefs<T>(...refs: Ref<T>[]) {
5
+ return (instance: T) => {
6
+ for (const ref of refs) {
7
+ if (typeof ref === "function") {
8
+ ref(instance);
9
+ } else if (ref) {
10
+ (ref as { current: T }).current = instance;
11
+ }
12
+ }
13
+ };
14
+ }
package/utils/dom.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { isString } from "lodash-es";
2
+
3
+ export const canUseDocument = !!(typeof window !== "undefined" && window.document && window.document.createElement);
4
+
5
+ export const getWindowSize = (): { width: number; height: number } => {
6
+ if (!canUseDocument) {
7
+ return { width: 0, height: 0 };
8
+ }
9
+ return {
10
+ width: window.innerWidth || document.documentElement.clientWidth,
11
+ height: window.innerHeight || document.documentElement.clientHeight
12
+ };
13
+ };
14
+
15
+ export const getAttach = (
16
+ node: string | HTMLElement | (() => string | HTMLElement) | null | undefined
17
+ ): HTMLElement | null => {
18
+ const attachNode = typeof node === "function" ? node() : node;
19
+ if (!attachNode) {
20
+ return document.body;
21
+ }
22
+ if (isString(attachNode)) {
23
+ return document.querySelector(attachNode);
24
+ }
25
+ if (attachNode instanceof HTMLElement) {
26
+ return attachNode;
27
+ }
28
+ return document.body;
29
+ };
@@ -0,0 +1,12 @@
1
+ import React, { RefAttributes, forwardRef } from "react";
2
+ import hoistNonReactStatics from "hoist-non-react-statics";
3
+
4
+ export default function forwardRefWithStatics<P, T = HTMLElement, S = Record<string, unknown>>(
5
+ component: React.ForwardRefRenderFunction<T, P>,
6
+ statics?: S
7
+ ): React.FunctionComponent<P & RefAttributes<T>> & S {
8
+ return hoistNonReactStatics(
9
+ forwardRef(component as React.ForwardRefRenderFunction<T, P>),
10
+ statics as S
11
+ ) as React.FunctionComponent<P & RefAttributes<T>> & S;
12
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @description: Calculate scroll bar width
3
+ * @param container Container used to calculate scrollbar width
4
+ * @default container: document.body
5
+ */
6
+ export function getScrollbarWidth(container: HTMLElement = document.body) {
7
+ if (container === document.body) {
8
+ return window.innerWidth - document.documentElement.clientWidth;
9
+ }
10
+ return container.offsetWidth - container.clientWidth;
11
+ }