diff --git a/.github/workflows/deploy-site-preview.yml b/.github/workflows/deploy-site-preview.yml index 5e7a668451..850a1fd3ed 100644 --- a/.github/workflows/deploy-site-preview.yml +++ b/.github/workflows/deploy-site-preview.yml @@ -48,7 +48,7 @@ jobs: - name: setup node uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18 cache: 'yarn' - name: Download site-dist Artifact diff --git a/components/Cascader/README.en-US.md b/components/Cascader/README.en-US.md index 1c22015268..62a6c10b8e 100644 --- a/components/Cascader/README.en-US.md +++ b/components/Cascader/README.en-US.md @@ -32,6 +32,7 @@ Display options in a multi-level cascading dropdown component. |autoWidth|auto width. minWidth defaults to 0, maxWidth defaults to 100%|\| boolean\| { minWidth?: CSSProperties['minWidth']; maxWidth?: CSSProperties['maxWidth'] } |`-`|2.54.0| |checkedStrategy|Customize the return value
parent:Only return the parent node when all child nodes are selected
child: Return child nodes|'parent' \| 'child' |`child`|2.31.0| |expandTrigger|Set the way to display the next level menu. One of hover and click|'click' \| 'hover' |`click`|-| +|maxTagCount|The maximum number of `tags` is displayed, only valid in `multiple` and `label` mode. Setting the number of `responsive` responsive display tags is not recommended when there are many options, as there may be performance issues.|\| number\| 'responsive'\| {count: number \| 'responsive';render?: (invisibleTagCount: number) => ReactNode;} |`-`|Object type in 2.37.0. `responsive ` in `2.62.0`| |mode|Set mode|'multiple' |`-`|-| |showSearch|Whether single mode Select is searchable. `{ retainInputValue: true }` to retain the existing content when the search box is focused,`{ retainInputValueWhileSelect: true }` to retain the existing content when multiple selection is selected.`{ panelMode: 'select' }` Display options as a search panel (`2.39.0`)`renderOption` Custom rendering search option (`2.39.0`)|\| boolean\| {panelMode?: 'cascader' \| 'select';renderOption?: (inputValue: string,option: NodeProps<T>,options: [extraOptions](#extraoptions)) => ReactNode;retainInputValue?: boolean;retainInputValueWhileSelect?: boolean;} |`-`|-| |size|Height of element, `24px` `28px` `32px` `36px`|'mini' \| 'small' \| 'default' \| 'large' |`-`|-| @@ -61,7 +62,6 @@ Display options in a multi-level cascading dropdown component. |dropdownColumnRender|Customize columns of the menu.|(menu: ReactNode, level: number) => ReactNode |`-`|2.15.0, `level` in 2.17.0| |dropdownRender|Customize the popup menu.|(menu: ReactNode) => ReactNode |`-`|2.15.0| |getPopupContainer|ParentNode which the selector should be rendered to.|(node: HTMLElement) => Element |`-`|-| -|maxTagCount|The maximum number of `tags` is displayed, only valid in `multiple` and `label` mode.|\| number\| {count: number;render?: (invisibleTagCount: number) => ReactNode;} |`-`|Object type in 2.37.0| |onChange|Callback when finishing select.|(value: (string \| string[])[],selectedOptions,extra: { dropdownVisible?: boolean }) => void |`-`|-| |onClear|Callback when click clear icon.|(visible: boolean) => void |`-`|-| |onClick|Callback when the mouse clicks on the drop-down box|(e) => void |`-`|-| diff --git a/components/Cascader/README.zh-CN.md b/components/Cascader/README.zh-CN.md index 94d0ceac39..d9f589d447 100644 --- a/components/Cascader/README.zh-CN.md +++ b/components/Cascader/README.zh-CN.md @@ -32,6 +32,7 @@ |autoWidth|设置宽度自适应。minWidth 默认为 0,maxWidth 默认为 100%|\| boolean\| { minWidth?: CSSProperties['minWidth']; maxWidth?: CSSProperties['maxWidth'] } |`-`|2.54.0| |checkedStrategy|定制回填方式
parent: 子节点都被选中时候返回父节点
child: 返回子节点|'parent' \| 'child' |`child`|2.31.0| |expandTrigger|展开下一级方式|'click' \| 'hover' |`click`|-| +|maxTagCount|最多显示多少个 `tag`,仅在多选或标签模式有效。设置 `responsive` 响应式显示标签数不建议在选项较多时使用,可能存在性能问题,|\| number\| 'responsive'\| {count: number \| 'responsive';render?: (invisibleTagCount: number) => ReactNode;} |`-`|Object type in 2.37.0. `responsive ` in `2.62.0`| |mode|是否开启多选|'multiple' |`-`|-| |showSearch|使单选模式可搜索,传入 `{ retainInputValue: true }` 在搜索框聚焦时保留现有内容传入 `{ retainInputValueWhileSelect: true }` 在多选选择时保留输入框内容。传入 `{ panelMode: 'select' }` 以搜索面板形式展示可选项 (`2.39.0`)`renderOption` 自定义渲染搜索项 (`2.39.0`)|\| boolean\| {panelMode?: 'cascader' \| 'select';renderOption?: (inputValue: string,option: NodeProps<T>,options: [extraOptions](#extraoptions)) => ReactNode;retainInputValue?: boolean;retainInputValueWhileSelect?: boolean;} |`-`|-| |size|分别不同尺寸的选择器。对应 `24px`, `28px`, `32px`, `36px`|'mini' \| 'small' \| 'default' \| 'large' |`-`|-| @@ -61,7 +62,6 @@ |dropdownColumnRender|自定义下拉菜单每一列的展示。|(menu: ReactNode, level: number) => ReactNode |`-`|2.15.0, `level` in 2.17.0| |dropdownRender|自定义下拉菜单的展示。|(menu: ReactNode) => ReactNode |`-`|2.15.0| |getPopupContainer|弹出框挂在的父节点|(node: HTMLElement) => Element |`-`|-| -|maxTagCount|最多显示多少个 `tag`,仅在多选或标签模式有效。|\| number\| {count: number;render?: (invisibleTagCount: number) => ReactNode;} |`-`|Object type in 2.37.0| |onChange|点击选择框的回调。|(value: (string \| string[])[],selectedOptions,extra: { dropdownVisible?: boolean }) => void |`-`|-| |onClear|点击清除时触发,参数是当前下拉框的展开状态。|(visible: boolean) => void |`-`|-| |onClick|鼠标点击下拉框时的回调|(e) => void |`-`|-| diff --git a/components/InputNumber/__demo__/format.md b/components/InputNumber/__demo__/format.md index 096a07a2d1..f457ca3586 100644 --- a/components/InputNumber/__demo__/format.md +++ b/components/InputNumber/__demo__/format.md @@ -33,6 +33,7 @@ function App() { value={value} onChange={setValue} prefix="¥" + // 正则表达式:(/\B(?=(\d{3})+(?!\d))/g, ',')} formatter={(value) => `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')} parser={(value) => value.replace(/,/g, '')} /> diff --git a/components/InputTag/README.en-US.md b/components/InputTag/README.en-US.md index d604c0edeb..432faf6909 100644 --- a/components/InputTag/README.en-US.md +++ b/components/InputTag/README.en-US.md @@ -25,6 +25,7 @@ An input box which will display your input as tags. |saveOnBlur|Whether to automatically store the text entering when blur InputTag|boolean |`-`|2.25.0| |inputValue|To set input value|string |`-`|-| |placeholder|Placeholder of input element|string |`-`|-| +|maxTagCount|The maximum number of `tags` is displayed|\| number\| 'responsive'\| {count: number \| 'responsive';render?: (invisibleTagCount: number, value: T[]) => ReactNode;popoverProps?: Partial<[PopoverProps](popover#popover)>;} |`-`|2.59.0. `responsive ` in `2.62.0`| |size|Different sizes|'mini' \| 'small' \| 'default' \| 'large' |`-`|-| |status|Status|'error' \| 'warning' |`-`|2.45.0| |addAfter|The label text displayed after (on the right side of) the input-tag field|ReactNode |`-`|2.47.0| @@ -38,7 +39,6 @@ An input box which will display your input as tags. |tokenSeparators|Separator used to tokenize|string[] |`-`|2.44.0| |validate|Function to check user's input, which is triggered when `Enter` is pressed|(inputValue: string, values: T[]) => boolean \| Promise<boolean> \| T \| Promise<T> |`(inputValue, values) => inputValue && values.every((item) => item !== inputValue)`|return type T and `Promise` in 2.37.0| |value|To set value|T[] |`-`|-| -|maxTagCount|The maximum number of `tags` is displayed|\| number\| {count: number;render?: (invisibleTagCount: number, value: T[]) => ReactNode;} |`-`|2.59.0| |onBlur|Callback when input is blurred|(e) => void |`-`|-| |onChange|Callback when value changes|(value: T[], reason: [ValueChangeReason](#valuechangereason)) => void |`-`|`reason` in 2.27.0| |onClear|Callback when clear button is clicked|() => void |`-`|2.20.0| diff --git a/components/InputTag/README.zh-CN.md b/components/InputTag/README.zh-CN.md index b6616acd1e..9fd9e60247 100644 --- a/components/InputTag/README.zh-CN.md +++ b/components/InputTag/README.zh-CN.md @@ -25,6 +25,7 @@ |saveOnBlur|是否在失焦时自动存储正在输入的文本|boolean |`-`|2.25.0| |inputValue|控件的输入框内的值|string |`-`|-| |placeholder|预设文案|string |`-`|-| +|maxTagCount|最多显示多少个 `tag`|\| number\| 'responsive'\| {count: number \| 'responsive';render?: (invisibleTagCount: number, value: T[]) => ReactNode;popoverProps?: Partial<[PopoverProps](popover#popover)>;} |`-`|2.59.0. `responsive ` in `2.62.0`| |size|不同尺寸|'mini' \| 'small' \| 'default' \| 'large' |`-`|-| |status|状态|'error' \| 'warning' |`-`|2.45.0| |addAfter|输入框后添加元素|ReactNode |`-`|2.47.0| @@ -38,7 +39,6 @@ |tokenSeparators|触发自动分词的分隔符|string[] |`-`|2.44.0| |validate|校验函数,默认在 按下enter时候触发。|(inputValue: string, values: T[]) => boolean \| Promise<boolean> \| T \| Promise<T> |`(inputValue, values) => inputValue && values.every((item) => item !== inputValue)`|return type T and `Promise` in 2.37.0| |value|控件值|T[] |`-`|-| -|maxTagCount|最多显示多少个 `tag`|\| number\| {count: number;render?: (invisibleTagCount: number, value: T[]) => ReactNode;} |`-`|2.59.0| |onBlur|失去焦点时候触发|(e) => void |`-`|-| |onChange|控件值改变时触发|(value: T[], reason: [ValueChangeReason](#valuechangereason)) => void |`-`|`reason` in 2.27.0| |onClear|点击清除按钮的回调|() => void |`-`|2.20.0| diff --git a/components/InputTag/__demo__/responsive-tag.md b/components/InputTag/__demo__/responsive-tag.md new file mode 100644 index 0000000000..3e25a61457 --- /dev/null +++ b/components/InputTag/__demo__/responsive-tag.md @@ -0,0 +1,48 @@ +--- +order: 9 +title: + zh-CN: 响应式显示Tag + en-US: Responsive Tags +--- + +## zh-CN + +通过 `maxTagCount=responsive` 设置根据容器尺寸动态显示 Tag 数。会监听所有 Tag 及容器的尺寸变化,所以在标签数较多时不建议使用,可能存在性能问题。 +此时拖拽和动画效果不可用。 + + +## en-US + +Use `maxTagCount=responsive` to dynamically display the number of Tags based on the container size. It will monitor the size changes of all Tags and containers, so it is not recommended to use it when there are a large number of tags, as there may be performance issues. + +Drag and animation effects are not available at this time. + +```js +import { InputTag, Space } from '@arco-design/web-react'; + +const App = () => { + return ( +
+ + + +{invisibleTagCount} More, + }} + /> + +
+ ); +}; + +export default App; +``` diff --git a/components/InputTag/__test__/__snapshots__/demo.test.ts.snap b/components/InputTag/__test__/__snapshots__/demo.test.ts.snap index 01cdbc5ed2..08f5f4a22d 100644 --- a/components/InputTag/__test__/__snapshots__/demo.test.ts.snap +++ b/components/InputTag/__test__/__snapshots__/demo.test.ts.snap @@ -1006,6 +1006,425 @@ exports[`renders InputTag/demo/render-tag.md correctly 1`] = ` `; +exports[`renders InputTag/demo/responsive-tag.md correctly 1`] = ` +
+
+
+
+
+
+
+
+
+ + label 1 + + + + +
+
+
+
+ + label 2 + + + + +
+
+
+
+ + label 3 + + + + +
+
+
+
+ + label 4 + + + + +
+
+
+
+ + label 5 + + + + +
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+ + label 1 + + + + +
+
+
+
+ + label 2 + + + + +
+
+
+
+ + label 3 + + + + +
+
+
+
+ + label 4 + + + + +
+
+
+
+ + label 5 + + + + +
+
+
+ + +
+
+
+
+
+
+
+
+`; + exports[`renders InputTag/demo/save-on-blur.md correctly 1`] = `
{ @@ -80,13 +85,14 @@ const UsedTransitionGroup = ({ prefixCls, children, animation, -}: PropsWithChildren<{ prefixCls: string; animation: boolean }>) => { - return animation ? ( - - {children} - - ) : ( -
{children}
+}: PropsWithChildren<{ + prefixCls: string; + animation: boolean; +}>) => { + return ( +
+ {animation ? {children} : children} +
); }; @@ -136,6 +142,7 @@ function InputTag(baseProps: InputTagProps, ref) { } = props; const prefixCls = getPrefixCls('input-tag'); const size = 'size' in props ? props.size : ctxSize; + const maxTagCountValue = typeof maxTagCount === 'object' ? maxTagCount.count : maxTagCount; const refInput = useRef>(); const refTSLastSeparateTriggered = useRef(null); @@ -216,24 +223,51 @@ function InputTag(baseProps: InputTagProps, ref) { } }; - const mergedRenderTag = (item: ObjectValueType, index: number, inTooltip = false) => { - const { value: itemValue, label } = item; - const closable = !readOnly && !disabled && item.closable !== false; + const mergedRenderTag = ( + item: ObjectValueType, + index: number, + inTooltip = false + ): { valueKey: string | number; dom: ReactNode } => { + let { value: itemValue, label } = item; + let closable = !readOnly && !disabled && item.closable !== false; + + // 当前 Tag 的key + let valueKey = typeof itemValue === 'object' ? index : itemValue; + const onClose = (event) => { tagCloseHandler(item, index, event); }; + if (!inTooltip && typeof maxTagCountValue === 'number' && index >= maxTagCountValue) { + if (index === value.length - 1) { + // 为什么这里要重新赋值呢? 因为select里之前 maxTagCount 会执行 renderTag 逻辑 + // https://github.com/arco-design/arco-design/blob/main/components/_class/select-view.tsx#L462 + label = renderEllipsisNode(value.length - Number(maxTagCountValue)); + itemValue = MAX_TAG_COUNT_VALUE_PLACEHOLDER; + closable = false; + valueKey = MAX_TAG_COUNT_VALUE_PLACEHOLDER; + if (!renderTag) { + return { valueKey, dom: label }; + } + } else { + return { valueKey, dom: null }; + } + } + if (renderTag) { - return renderTag( - { - value: itemValue, - label, - closable, - onClose, - }, - index, - value - ); + return { + valueKey, + dom: renderTag( + { + value: itemValue, + label, + closable, + onClose, + }, + index, + value + ), + }; } const tagProps: Partial = { @@ -251,34 +285,31 @@ function InputTag(baseProps: InputTagProps, ref) { title: typeof label === 'string' ? label : undefined, }; - const maxTagCountInNumber = typeof maxTagCount === 'object' ? maxTagCount.count : maxTagCount; - if (!inTooltip && typeof maxTagCountInNumber === 'number' && index >= maxTagCountInNumber) { - if (index === value.length - 1) { - const invisibleTagCount = value.length - maxTagCountInNumber; - const renderEllipsisLabel = - typeof maxTagCount === 'object' - ? maxTagCount.render - : () => +{invisibleTagCount}; - return ( - - {value - .map((v, index) => ({ tagValue: v, tagIndex: index })) - .slice(-invisibleTagCount) - .map(({ tagValue, tagIndex }) => mergedRenderTag(tagValue, tagIndex, true))} - - } - /> - ); - } - return null; - } - - return ; + return { valueKey, dom: }; }; + function renderEllipsisNode(invisibleTagCount: number) { + const renderEllipsisLabel = + typeof maxTagCount === 'object' + ? maxTagCount.render + : () => +{invisibleTagCount}; + + return ( + + {value + .map((v, index) => ({ tagValue: v, tagIndex: index })) + .slice(-invisibleTagCount) + .map(({ tagValue, tagIndex }) => mergedRenderTag(tagValue, tagIndex, true)?.dom)} + + } + /> + ); + } + const handleTokenSeparators = async (str: string) => { // clear the timestamp, and then we can judge whether tokenSeparators has been triggered // according to timestamp value @@ -340,14 +371,14 @@ function InputTag(baseProps: InputTagProps, ref) { // CSSTransition needs to be a direct child of TransitionGroup, otherwise the animation will NOT work // https://github.com/arco-design/arco-design/issues/622 - const childrenWithAnimation = value - .map((x, i) => { + const childrenTagWithAnimation = useMemo(() => { + const items = value.map((x, i) => { // Check whether two tags have same value. If so, set different key for them to avoid only rendering one tag. const isRepeat = value.findIndex((item) => item.value === x.value) !== i; - const eleTag = mergedRenderTag(x, i); + const { dom: eleTag, valueKey } = mergedRenderTag(x, i); return React.isValidElement(eleTag) ? ( @@ -356,72 +387,71 @@ function InputTag(baseProps: InputTagProps, ref) { ) : ( eleTag ); - }) - .concat( - - refDelay.current, - pure: true, - }} - onPressEnter={async (e) => { - inputValue && e.preventDefault(); - onPressEnter?.(e); + }); + return items; + }, [value]); + + const suffixInput = [ + + refDelay.current, + pure: true, + }} + onPressEnter={async (e) => { + inputValue && e.preventDefault(); + onPressEnter?.(e); + await tryAddInputValueToTag(); + }} + onFocus={(e) => { + if (!disableInputComponent && !readOnly) { + setFocused(true); + onFocus?.(e); + } + }} + onBlur={async (e) => { + setFocused(false); + onBlur?.(e); + if (saveOnBlur) { await tryAddInputValueToTag(); - }} - onFocus={(e) => { - if (!disableInputComponent && !readOnly) { - setFocused(true); - onFocus?.(e); - } - }} - onBlur={async (e) => { - setFocused(false); - onBlur?.(e); - if (saveOnBlur) { - await tryAddInputValueToTag(); - } + } + setInputValue(''); + }} + value={inputValue} + onChange={(value, event) => { + // Only fire callback on user input to ensure parent component can get real input value on controlled mode. + onInputChange?.(value, event); + + // Pasting in the input box will trigger onPaste first and then onChange, but the value of onChange does not contain a newline character. + // If word segmentation has just been triggered due to pasting, onChange will no longer attempt word segmentation. + // Do NOT use await, need to update input value right away. + event.nativeEvent.inputType !== 'insertFromPaste' && handleTokenSeparators(value); + + if (refTSLastSeparateTriggered.current) { setInputValue(''); - }} - value={inputValue} - onChange={(value, event) => { - // Only fire callback on user input to ensure parent component can get real input value on controlled mode. - onInputChange?.(value, event); - - // Pasting in the input box will trigger onPaste first and then onChange, but the value of onChange does not contain a newline character. - // If word segmentation has just been triggered due to pasting, onChange will no longer attempt word segmentation. - // Do NOT use await, need to update input value right away. - event.nativeEvent.inputType !== 'insertFromPaste' && handleTokenSeparators(value); - - if (refTSLastSeparateTriggered.current) { - setInputValue(''); - } else { - setInputValue(value); - } - }} - onKeyDown={(event) => { - hotkeyHandler(event as any); - onKeyDown?.(event); - }} - onPaste={(event) => { - onPaste?.(event); - handleTokenSeparators(event.clipboardData.getData('text')); - }} - /> - - ); + } else { + setInputValue(value); + } + }} + onKeyDown={(event) => { + hotkeyHandler(event as any); + onKeyDown?.(event); + }} + onPaste={(event) => { + onPaste?.(event); + handleTokenSeparators(event.clipboardData.getData('text')); + }} + /> + , + ]; const hasPrefix = !isEmptyNode(prefix); const hasSuffix = !isEmptyNode(suffix) || !isEmptyNode(clearIcon); @@ -484,12 +514,20 @@ function InputTag(baseProps: InputTagProps, ref) { valueChangeHandler(moveItem(value, prevIndex, index), 'sort'); }} > - {childrenWithAnimation} + {childrenTagWithAnimation.concat(suffixInput)} ) : ( - {childrenWithAnimation} + {maxTagCountValue === MAX_TAG_RESPONSIVE ? ( + renderEllipsisNode(ellipsisCount)} + /> + ) : ( + childrenTagWithAnimation.concat(suffixInput) + )} )} diff --git a/components/InputTag/interface.ts b/components/InputTag/interface.ts index 443105b337..d2fa00ab8b 100644 --- a/components/InputTag/interface.ts +++ b/components/InputTag/interface.ts @@ -1,4 +1,5 @@ import { CSSProperties, ReactNode } from 'react'; +import { PopoverProps } from '../Popover'; export type ObjectValueType = { value?: any; @@ -99,13 +100,15 @@ export interface InputTagProps { /** * @zh 最多显示多少个 `tag` * @en The maximum number of `tags` is displayed - * @version 2.59.0 + * @version 2.59.0. `responsive ` in `2.62.0` */ maxTagCount?: | number + | 'responsive' | { - count: number; + count: number | 'responsive'; render?: (invisibleTagCount: number, value: T[]) => ReactNode; + popoverProps?: Partial; }; /** * @zh 添加前缀文字或者图标 diff --git a/components/Radio/README.en-US.md b/components/Radio/README.en-US.md index e024f2c26b..22e63e88eb 100644 --- a/components/Radio/README.en-US.md +++ b/components/Radio/README.en-US.md @@ -26,6 +26,7 @@ In a set of related and mutually exclusive data, the user can only select one op |Property|Description|Type|DefaultValue| |---|---|---|---| +|disabled|disabled|boolean |`-`| |name|`Radio`'s name attr|string |`-`| |disabled|Whether to disable all radios in the group|boolean |`-`| |direction|Arrangement direction|'vertical' \| 'horizontal' |`horizontal`| diff --git a/components/Radio/README.zh-CN.md b/components/Radio/README.zh-CN.md index bf74de1c14..f419a11022 100644 --- a/components/Radio/README.zh-CN.md +++ b/components/Radio/README.zh-CN.md @@ -26,6 +26,7 @@ |参数名|描述|类型|默认值| |---|---|---|---| +|disabled|禁用|boolean |`-`| |name|`Radio` 的 name|string |`-`| |disabled|禁用所有子单选器|boolean |`-`| |direction|方向|'vertical' \| 'horizontal' |`horizontal`| diff --git a/components/Radio/interface.ts b/components/Radio/interface.ts index a8406f2e1f..3c15dd41b7 100644 --- a/components/Radio/interface.ts +++ b/components/Radio/interface.ts @@ -41,6 +41,10 @@ export interface RadioProps export interface RadioGroupProps { style?: CSSProperties; className?: string | string[]; + /** + * @zh 禁用 + * @en disabled + */ disabled?: boolean; /** * @zh `Radio` 的 name diff --git a/components/Select/README.en-US.md b/components/Select/README.en-US.md index dd21b70605..5f9f06aad7 100644 --- a/components/Select/README.en-US.md +++ b/components/Select/README.en-US.md @@ -30,6 +30,7 @@ When users need to select one or more from a group of similar data, they can use |placeholder|Placeholder of element|string |`-`|-| |allowCreate|Whether to allow new options to be created by input.|\| boolean\| {formatter: (inputValue: string, creating: boolean) => [SelectProps](select#select)['options'][number];} |`-`|2.13.0, `{ formatter }` in 2.54.0| |autoWidth|auto width. minWidth defaults to 0, maxWidth defaults to 100%|\| boolean\| { minWidth?: CSSProperties['minWidth']; maxWidth?: CSSProperties['maxWidth'] } |`-`|2.54.0| +|maxTagCount|The maximum number of `tags` is displayed, only valid in `multiple` and `label` mode. Setting the number of `responsive` responsive display tags is not recommended when there are many options, as there may be performance issues.|\| number\| 'responsive'\| {count: number \| 'responsive';render?: (invisibleTagCount: number) => ReactNode;} |`-`|Object type in 2.37.0. `responsive ` in `2.62.0`| |mode|Set mode of Select(**`tags` recommends using `mode: multiple; allowCreate: true` instead, this mode will be removed in the next major version**)|'multiple' \| 'tags' |`-`|-| |size|Height of element, `24px` `28px` `32px` `36px`|'mini' \| 'small' \| 'default' \| 'large' |`-`|-| |status|Status|'error' \| 'warning' |`-`|2.45.0| @@ -55,7 +56,6 @@ When users need to select one or more from a group of similar data, they can use |dropdownRender|Customize dropdown content|(menu: ReactNode) => ReactNode |`-`|-| |filterOption|If it's true, filter options by input value. If it's a function, filter options base on the function.|boolean \| ((inputValue: string, option: ReactElement) => boolean) |`true`|-| |getPopupContainer|To set the container of the dropdown.|(node: HTMLElement) => Element |`-`|-| -|maxTagCount|The maximum number of `tags` is displayed, only valid in `multiple` and `label` mode.|\| number\| {count: number;render?: (invisibleTagCount: number) => ReactNode;} |`-`|Object type in 2.37.0| |onBlur|Callback when lose focus|(e) => void |`-`|-| |onChange|Callback when select an option or input value change.|(value, option: [OptionInfo](#optioninfo) \| [OptionInfo](#optioninfo)[]) => void |`-`|-| |onClear|Called when clear|(visible: boolean) => void |`-`|-| diff --git a/components/Select/README.zh-CN.md b/components/Select/README.zh-CN.md index 1341ecd838..18019135e8 100644 --- a/components/Select/README.zh-CN.md +++ b/components/Select/README.zh-CN.md @@ -30,6 +30,7 @@ |placeholder|选择框默认文字。|string |`-`|-| |allowCreate|是否允许通过输入创建新的选项。|\| boolean\| {formatter: (inputValue: string, creating: boolean) => [SelectProps](select#select)['options'][number];} |`-`|2.13.0, `{ formatter }` in 2.54.0| |autoWidth|设置宽度自适应。minWidth 默认为 0,maxWidth 默认为 100%|\| boolean\| { minWidth?: CSSProperties['minWidth']; maxWidth?: CSSProperties['maxWidth'] } |`-`|2.54.0| +|maxTagCount|最多显示多少个 `tag`,仅在多选或标签模式有效。设置 `responsive` 响应式显示标签数不建议在选项较多时使用,可能存在性能问题,|\| number\| 'responsive'\| {count: number \| 'responsive';render?: (invisibleTagCount: number) => ReactNode;} |`-`|Object type in 2.37.0. `responsive ` in `2.62.0`| |mode|是否开启多选模式或标签模式 (**`tags` 推荐使用 `mode: multiple; allowCreate: true` 替代,下一大版本将移除此模式**)|'multiple' \| 'tags' |`-`|-| |size|分别不同尺寸的选择器。对应 `24px`, `28px`, `32px`, `36px`|'mini' \| 'small' \| 'default' \| 'large' |`-`|-| |status|状态|'error' \| 'warning' |`-`|2.45.0| @@ -55,7 +56,6 @@ |dropdownRender|自定义弹出内容。|(menu: ReactNode) => ReactNode |`-`|-| |filterOption|是否根据输入的值筛选数据。如果传入函数的话,接收 `inputValue` 和 `option` 两个参数,当option符合筛选条件时,返回 `true`,反之返回 `false`。|boolean \| ((inputValue: string, option: ReactElement) => boolean) |`true`|-| |getPopupContainer|弹出框挂载的父节点。|(node: HTMLElement) => Element |`-`|-| -|maxTagCount|最多显示多少个 `tag`,仅在多选或标签模式有效。|\| number\| {count: number;render?: (invisibleTagCount: number) => ReactNode;} |`-`|Object type in 2.37.0| |onBlur|失去焦点时的回调|(e) => void |`-`|-| |onChange|点击选择框的回调|(value, option: [OptionInfo](#optioninfo) \| [OptionInfo](#optioninfo)[]) => void |`-`|-| |onClear|点击清除时触发,参数是当前下拉框的展开状态。|(visible: boolean) => void |`-`|-| diff --git a/components/Select/__demo__/darggable.md b/components/Select/__demo__/darggable.md index d89a896281..640fce9d24 100644 --- a/components/Select/__demo__/darggable.md +++ b/components/Select/__demo__/darggable.md @@ -16,7 +16,7 @@ In multiple mode, specify the `dragToSort` property to allow sort the entered va ```js import { Select, Message, Space } from '@arco-design/web-react'; const Option = Select.Option; -const options = ['Beijing', 'Shanghai', 'Guangzhou', 'Shenzhen']; +const options = ['Beijing', 'Shanghai', 'Guangzhou', 'Shenzhen','a', 'b']; const App = () => { return ( @@ -26,6 +26,7 @@ const App = () => { mode="multiple" dragToSort defaultValue={options.slice(0, 3)} + maxTagCount={3} > {options.map((option, index) => (
`; +exports[`renders Select/demo/maxTag.md correctly 1`] = ` +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+`; + exports[`renders Select/demo/multiple.md correctly 1`] = `
ReactNode;} |`-`|Object type in 2.37.0. `responsive ` in `2.62.0`| |size|Height of element, `24px` `28px` `32px` `36px`|'mini' \| 'small' \| 'default' \| 'large' |`-`|-| |status|Status|'error' \| 'warning' |`-`|2.45.0| |treeCheckedStrategy|Customize the return value|[TreeProps](tree#tree)['checkedStrategy'] |`all`|-| @@ -54,7 +55,6 @@ Can choose tree structure data.Only Single choice is supports. |filterTreeNode|Filter data based on entered value. Accepted two parameters, inputText and treeNode.When the option matches the filter conditions, it returns true, otherwise it returns false. treeNode is the tree node.|(inputText, treeNode: any) => boolean \| void |`-`|-| |getPopupContainer|The parent node of the popup|(node: HTMLElement) => Element |`-`|-| |loadMore|Callback when loaded data asynchronously|(treeNode: [NodeProps](tree#treenode), dataRef) => void |`-`|-| -|maxTagCount|The maximum number of `tags` is displayed, only valid in `multiple` and `label` mode.|\| number\| {count: number;render?: (invisibleTagCount: number) => ReactNode;} |`-`|Object type in 2.37.0| |onChange|Callback when the selection changed|(value: any,extra: {trigger?: [NodeProps](tree#treenode);checked?: boolean;selected?: boolean;}) => void |`-`|`extra` in `2.29.0`| |onClear|Callback when clicked clear, the parameter is the visible state of current dropdown|(visible: boolean) => void |`-`|-| |onClick|Callback when the mouse clicks on the drop-down box|(e) => void |`-`|-| diff --git a/components/TreeSelect/README.zh-CN.md b/components/TreeSelect/README.zh-CN.md index 0556268d14..ae2e280d8e 100644 --- a/components/TreeSelect/README.zh-CN.md +++ b/components/TreeSelect/README.zh-CN.md @@ -31,6 +31,7 @@ |placeholder|选择框默认文字。|string |`-`|-| |autoWidth|设置宽度自适应。minWidth 默认为 0,maxWidth 默认为 100%|\| boolean\| { minWidth?: CSSProperties['minWidth']; maxWidth?: CSSProperties['maxWidth'] } |`-`|2.54.0| |fieldNames|指定 key,title,isLeaf,disabled,children 对应的字段|[TreeProps](tree#tree)['fieldNames'] |`DefaultFieldNames`|2.11.0| +|maxTagCount|最多显示多少个 `tag`,仅在多选或标签模式有效。设置 `responsive` 响应式显示标签数不建议在选项较多时使用,可能存在性能问题,|\| number\| 'responsive'\| {count: number \| 'responsive';render?: (invisibleTagCount: number) => ReactNode;} |`-`|Object type in 2.37.0. `responsive ` in `2.62.0`| |size|分别不同尺寸的选择器。对应 `24px`, `28px`, `32px`, `36px`|'mini' \| 'small' \| 'default' \| 'large' |`-`|-| |status|状态|'error' \| 'warning' |`-`|2.45.0| |treeCheckedStrategy|定制回显方式|[TreeProps](tree#tree)['checkedStrategy'] |`all`|-| @@ -54,7 +55,6 @@ |filterTreeNode|根据输入的值筛选数据。接收 `inputText` 和 `treeNode` 两个参数,当 `option` 符合筛选条件时,返回 `true`,反之返回 `false`。treeNode 是树节点。|(inputText, treeNode: any) => boolean \| void |`-`|-| |getPopupContainer|弹出框挂载的父节点|(node: HTMLElement) => Element |`-`|-| |loadMore|动态加载数据|(treeNode: [NodeProps](tree#treenode), dataRef) => void |`-`|-| -|maxTagCount|最多显示多少个 `tag`,仅在多选或标签模式有效。|\| number\| {count: number;render?: (invisibleTagCount: number) => ReactNode;} |`-`|Object type in 2.37.0| |onChange|选中值改变的回调|(value: any,extra: {trigger?: [NodeProps](tree#treenode);checked?: boolean;selected?: boolean;}) => void |`-`|`extra` in `2.29.0`| |onClear|点击清除时触发,参数是当前下拉框的展开状态。|(visible: boolean) => void |`-`|-| |onClick|鼠标点击下拉框时的回调|(e) => void |`-`|-| diff --git a/components/_class/OverflowEllipsis/OverflowItem.tsx b/components/_class/OverflowEllipsis/OverflowItem.tsx new file mode 100644 index 0000000000..fc57b1f52d --- /dev/null +++ b/components/_class/OverflowEllipsis/OverflowItem.tsx @@ -0,0 +1,45 @@ +import React, { ReactNode, useContext, useEffect, useRef } from 'react'; +import ResizeObserver from '../../_util/resizeObserver'; +import { ConfigContext } from '../../ConfigProvider'; +import classNames from '../../_util/classNames'; + +interface OverflowItemProps { + className?: string | string[]; + onResize: (node: HTMLDivElement) => void; + unregister: (node: HTMLDivElement) => void; + hidden?: boolean; + children: ReactNode; +} + +export default function OverflowItem(props: OverflowItemProps) { + const { getPrefixCls } = useContext(ConfigContext); + const prefixCls = getPrefixCls('overflow-item'); + const itemRef = useRef(); + + useEffect(() => { + props.onResize(itemRef.current); + + return () => { + props.unregister(itemRef.current); + }; + }, []); + + const hidden = props.hidden; + + return ( + { + props.onResize(entry?.[0].target as HTMLDivElement); + }} + > +
+ {props.children} +
+
+ ); +} diff --git a/components/_class/OverflowEllipsis/index.tsx b/components/_class/OverflowEllipsis/index.tsx new file mode 100644 index 0000000000..d878de6150 --- /dev/null +++ b/components/_class/OverflowEllipsis/index.tsx @@ -0,0 +1,127 @@ +import React, { ReactNode, useLayoutEffect, useState, useContext, ReactElement } from 'react'; +import ResizeObserver from '../../_util/resizeObserver'; +import OverflowItem from './OverflowItem'; +import cs from '../../_util/classNames'; +import { ConfigContext } from '../../ConfigProvider'; + +interface OverflowEllipsisProps { + className?: string | string[]; + items: (ReactElement | ReactNode)[]; + suffixItems: (ReactElement | ReactNode)[]; + ellipsisNode?: (info: { ellipsisCount: number }) => ReactNode; +} + +export default function OverflowEllipsis(props: OverflowEllipsisProps) { + const { getPrefixCls } = useContext(ConfigContext); + const prefixCls = getPrefixCls('overflow'); + + const { items, suffixItems } = props; + const ellipsisNode = props.ellipsisNode || (({ ellipsisCount }) => `+${ellipsisCount}`); + + const [containerWidth, setContainerWidth] = useState(); + const [maxCount, setMaxCount] = useState(); + + const [overflowItems, setOverflowItems] = useState<{ + [key: string]: { + node: HTMLDivElement; + width: number; + }; + }>({}); + + const [suffixOverflowItems, setSuffixOverflowItems] = useState<{ + [key: string]: { + node: HTMLDivElement; + width: number; + }; + }>({}); + + const ellipsisCount = items.length - maxCount; + + const maxTag = ellipsisCount > 0 ? ellipsisNode({ ellipsisCount }) : null; + + useLayoutEffect(() => { + const total = Object.values(overflowItems).length; + + let totalWidth = Object.values(suffixOverflowItems).reduce((t, n) => { + return t + (n?.width || 0); + }, 0); + + let newMaxCount = total; + + Object.keys(overflowItems).some((key, index) => { + const target = overflowItems[key]; + + if (target && totalWidth + target.width > containerWidth) { + newMaxCount = index; + return true; + } + totalWidth += target?.width || 0; + }); + + setMaxCount(Math.max(newMaxCount, 0)); + }, [overflowItems, containerWidth, suffixOverflowItems]); + + return ( + { + setContainerWidth(entry?.[0]?.target.clientWidth || 0); + }} + > +
+ {items.map((item, index) => { + const key = `${(item as ReactElement)?.key || index}_overflow_item`; + return ( + { + setOverflowItems((overflowItems) => { + overflowItems[key] = { node, width: node.clientWidth }; + return overflowItems; + }); + }} + unregister={() => { + setOverflowItems((overflowItems) => { + delete overflowItems[key]; + return overflowItems; + }); + }} + hidden={maxCount < index + 1} + > + {item} + + ); + })} + {[maxTag, ...suffixItems].map((item: JSX.Element, index) => { + if (!item) { + return null; + } + const key = `${item?.key || index}_overflow_suffix_item`; + return ( + { + setSuffixOverflowItems((suffixOverflowItems) => { + return { + ...suffixOverflowItems, + [`${key}`]: { node, width: node.clientWidth }, + }; + }); + }} + unregister={() => { + setSuffixOverflowItems((suffixOverflowItems) => { + delete suffixOverflowItems[key]; + return { + ...suffixOverflowItems, + }; + }); + }} + > + {item} + + ); + })} +
+
+ ); +} diff --git a/components/_class/OverflowEllipsis/style/index.less b/components/_class/OverflowEllipsis/style/index.less new file mode 100644 index 0000000000..36691fee4d --- /dev/null +++ b/components/_class/OverflowEllipsis/style/index.less @@ -0,0 +1,29 @@ +@import '../../../style/theme/default.less'; + +@arco-overflow-prefix-cls: ~'@{prefix}-overflow'; + +.@{arco-overflow-prefix-cls} { + display: flex; + flex-wrap: nowrap; + width: 100%; + overflow: hidden; + max-width: 100%; + justify-content: flex-start; + align-items: center; + + &-item { + display: inline-flex; + max-width: 100%; + + &-hidden { + position: absolute; + z-index: -1; + opacity: 0; + } + } + + // &-suffix-item { + // display: inline-flex; + // max-width: 100%; + // } +} diff --git a/components/_class/select-view.tsx b/components/_class/select-view.tsx index 41438cc42e..e307e34cd4 100644 --- a/components/_class/select-view.tsx +++ b/components/_class/select-view.tsx @@ -25,6 +25,7 @@ import useForceUpdate from '../_util/hooks/useForceUpdate'; import IconHover from './icon-hover'; import { Backspace, Enter } from '../_util/keycode'; import fillNBSP from '../_util/fillNBSP'; +import Tag from '../Tag'; export interface SelectViewCommonProps extends Pick, 'animation' | 'renderTag' | 'dragToSort'> { @@ -86,14 +87,15 @@ export interface SelectViewCommonProps */ allowClear?: boolean; /** - * @zh 最多显示多少个 `tag`,仅在多选或标签模式有效。 - * @en The maximum number of `tags` is displayed, only valid in `multiple` and `label` mode. - * @version Object type in 2.37.0 + * @zh 最多显示多少个 `tag`,仅在多选或标签模式有效。设置 `responsive` 响应式显示标签数不建议在选项较多时使用,可能存在性能问题, + * @en The maximum number of `tags` is displayed, only valid in `multiple` and `label` mode. Setting the number of `responsive` responsive display tags is not recommended when there are many options, as there may be performance issues. + * @version Object type in 2.37.0. `responsive ` in `2.62.0` */ maxTagCount?: | number + | 'responsive' | { - count: number; + count: number | 'responsive'; render?: (invisibleTagCount: number) => ReactNode; }; /** @@ -184,8 +186,6 @@ const SearchStatus = { NONE: 2, }; -const MAX_TAG_COUNT_VALUE_PLACEHOLDER = '__arco_value_tag_placeholder'; - export type SelectViewHandle = { dom: HTMLDivElement; focus: () => void; @@ -430,42 +430,43 @@ const CoreSelectView = React.forwardRef( const renderMultiple = () => { const usedValue = isUndefined(value) ? [] : [].concat(value as []); - const maxTagCountNumber = isObject(maxTagCount) ? maxTagCount.count : maxTagCount; - - const maxTagCountRender = - isObject(maxTagCount) && isFunction(maxTagCount.render) - ? maxTagCount.render - : (invisibleCount) => `+${invisibleCount}...`; + // const maxTagCountValue = isObject(maxTagCount) ? maxTagCount.count : maxTagCount; + + const maxTagCountRender = (invisibleCount) => { + return ( + + {isObject(maxTagCount) && isFunction(maxTagCount.render) + ? maxTagCount.render(invisibleCount) + : `+${invisibleCount}...`} + + ); + }; - const usedMaxTagCount = - typeof maxTagCountNumber === 'number' ? Math.max(maxTagCountNumber, 0) : usedValue.length; const tagsToShow: ObjectValueType[] = []; let lastClosableTagIndex = -1; for (let i = usedValue.length - 1; i >= 0; i--) { const v = usedValue[i]; const result = renderText(v); - if (i < usedMaxTagCount) { - tagsToShow.unshift({ - value: v, - label: result.text, - closable: !result.disabled, - }); - } + tagsToShow.unshift({ + value: v, + label: result.text, + closable: !result.disabled, + }); if (!result.disabled && lastClosableTagIndex === -1) { lastClosableTagIndex = i; } } - const invisibleTagCount = usedValue.length - usedMaxTagCount; - if (invisibleTagCount > 0) { - tagsToShow.push({ - label: maxTagCountRender(invisibleTagCount), - closable: false, - // InputTag needs to extract value as key - value: MAX_TAG_COUNT_VALUE_PLACEHOLDER, - }); - } + // const invisibleTagCount = usedValue.length - usedMaxTagCount; + // if (invisibleTagCount > 0) { + // tagsToShow.push({ + // label: maxTagCountRender(invisibleTagCount), + // closable: false, + // // InputTag needs to extract value as key + // value: MAX_TAG_COUNT_VALUE_PLACEHOLDER, + // }); + // } const eventHandlers = { onPaste: inputEventHandlers.paste, @@ -514,18 +515,27 @@ const CoreSelectView = React.forwardRef( tagClassName={`${prefixCls}-tag`} renderTag={renderTag} icon={{ removeIcon }} + maxTagCount={ + maxTagCount + ? { + count: isObject(maxTagCount) ? maxTagCount.count : maxTagCount, + render: maxTagCountRender, + popoverProps: { disabled: true }, + } + : undefined + } onChange={(newValue, reason) => { if (onSort && reason === 'sort') { - const indexOfMaxTagCount = newValue.indexOf(MAX_TAG_COUNT_VALUE_PLACEHOLDER); - // inject the invisible values tags to middle after dragging the "+x" tag - if (indexOfMaxTagCount > -1) { - const headArr = newValue.slice(0, indexOfMaxTagCount); - const tailArr = newValue.slice(indexOfMaxTagCount + 1); - const midArr = usedValue.slice(-invisibleTagCount); - onSort(headArr.concat(midArr, tailArr)); - } else { - onSort(newValue); - } + // const indexOfMaxTagCount = newValue.indexOf(MAX_TAG_COUNT_VALUE_PLACEHOLDER); + // // inject the invisible values tags to middle after dragging the "+x" tag + // if (indexOfMaxTagCount > -1) { + // const headArr = newValue.slice(0, indexOfMaxTagCount); + // const tailArr = newValue.slice(indexOfMaxTagCount + 1); + // const midArr = usedValue.slice(-invisibleTagCount); + // onSort(headArr.concat(midArr, tailArr)); + // } else { + // } + onSort(newValue); } }} {...eventHandlers}