From c159d0db908bed014071fcd46ac09d79bee328ce Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 14 Dec 2020 11:12:16 +0800 Subject: [PATCH 1/4] feat: Support responsive --- assets/index.less | 12 +++ examples/tags.tsx | 21 +++- package.json | 5 +- src/Selector/MultipleSelector.tsx | 174 ++++++++++++++++++++++-------- src/Selector/index.tsx | 2 +- src/generate.tsx | 8 +- 6 files changed, 165 insertions(+), 57 deletions(-) diff --git a/assets/index.less b/assets/index.less index cbd4a1309..e9c6e11e5 100644 --- a/assets/index.less +++ b/assets/index.less @@ -117,8 +117,20 @@ } } + .@{select-prefix}-selection-overflow { + display: flex; + flex-wrap: wrap; + width: 100%; + + &-item { + flex: none; + max-width: 100%; + } + } + .@{select-prefix}-selection-search { position: relative; + max-width: 100%; &-input, &-mirror { diff --git a/examples/tags.tsx b/examples/tags.tsx index 20368ce26..c76dab474 100644 --- a/examples/tags.tsx +++ b/examples/tags.tsx @@ -14,10 +14,20 @@ for (let i = 10; i < 36; i += 1) { const Test: React.FC = () => { const [disabled, setDisabled] = React.useState(false); - const [value, setValue] = React.useState(['name2', 'name3']); - const [maxTagCount, setMaxTagCount] = React.useState(null); + const [value, setValue] = React.useState([ + 'name1', + 'name2', + 'name3', + 'name4', + 'name5', + 'a10', + 'b11', + 'c12', + 'd13', + ]); + const [maxTagCount, setMaxTagCount] = React.useState('responsive'); - const toggleMaxTagCount = (count: number) => { + const toggleMaxTagCount = (count: number | 'responsive') => { setMaxTagCount(count); }; @@ -29,7 +39,7 @@ const Test: React.FC = () => { - - {/* Measure Node */} - - {inputValue}  - + {childNode} + + ); + } + + function renderItem(item: DisplayLabelValueType) { + const { label, value, disabled: itemDisabled } = item; + const closable = !disabled && !itemDisabled; + + const onMouseDown = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + }; + const onClose = (event?: React.MouseEvent) => { + if (event) event.stopPropagation(); + onSelect(value, { selected: false }); + }; + + return renderSelectorNode( + <> + {label} + {closable && ( + + × + + )} + , + itemDisabled, + ); + } + + function renderRest(omittedValues: DisplayLabelValueType[]) { + return renderSelectorNode( + typeof maxTagPlaceholder === 'function' + ? maxTagPlaceholder(omittedValues) + : maxTagPlaceholder, + ); + } + + // ===================== Render ====================== + console.log('>>>', inputWidth); + + // >>> Input Node + const inputNode = ( +
{ + setFocused(true); + }} + onBlur={() => { + setFocused(false); + }} + > + + + {/* Measure Node */} + + {inputValue}  +
+ ); + + // >>> Selections + const selectionNode = ( + + ); + + return ( + <> + {selectionNode} {!values.length && !inputValue && ( - {placeholder} + {placeholder} )} ); diff --git a/src/Selector/index.tsx b/src/Selector/index.tsx index ddc276d42..7c3350df5 100644 --- a/src/Selector/index.tsx +++ b/src/Selector/index.tsx @@ -69,7 +69,7 @@ export interface SelectorProps { removeIcon?: RenderNode; // Tags - maxTagCount?: number; + maxTagCount?: number | 'responsive'; maxTagTextLength?: number; maxTagPlaceholder?: React.ReactNode | ((omittedValues: LabelValueType[]) => React.ReactNode); tagRender?: (props: CustomTagProps) => React.ReactElement; diff --git a/src/generate.tsx b/src/generate.tsx index 5a6d1cac1..2ea931f2f 100644 --- a/src/generate.tsx +++ b/src/generate.tsx @@ -129,7 +129,7 @@ export interface SelectProps extends Re getInputElement?: () => JSX.Element; optionLabelProp?: string; maxTagTextLength?: number; - maxTagCount?: number; + maxTagCount?: number | 'responsive'; maxTagPlaceholder?: React.ReactNode | ((omittedValues: LabelValueType[]) => React.ReactNode); tokenSeparators?: string[]; tagRender?: (props: CustomTagProps) => React.ReactElement; @@ -192,7 +192,7 @@ export interface GenerateConfig { getLabeledValue: GetLabeledValue>; filterOptions: FilterOptions; findValueOption: // Need still support legacy ts api - | ((values: RawValueType[], options: FlattenOptionsType) => OptionsType) + | ((values: RawValueType[], options: FlattenOptionsType) => OptionsType) // New API add prevValueOptions support | (( values: RawValueType[], @@ -714,7 +714,9 @@ export default function generateSelector< // If menu is open, OptionList will take charge // If mode isn't tags, press enter is not meaningful when you can't see any option const onSearchSubmit = (searchText: string) => { - const newRawValues = Array.from(new Set([...mergedRawValue, searchText])); + const newRawValues = Array.from( + new Set([...mergedRawValue, searchText]), + ); triggerChange(newRawValues); newRawValues.forEach(newRawValue => { triggerSelect(newRawValue, true, 'input'); From e0935aafd0700b788809b894989bb82af9b586d5 Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 14 Dec 2020 11:34:36 +0800 Subject: [PATCH 2/4] feat: Support responsive --- src/Selector/MultipleSelector.tsx | 201 ++++++++++-------------------- src/generate.tsx | 6 +- src/interface/generator.ts | 25 +--- 3 files changed, 71 insertions(+), 161 deletions(-) diff --git a/src/Selector/MultipleSelector.tsx b/src/Selector/MultipleSelector.tsx index c30c1b304..c8c3ff8cd 100644 --- a/src/Selector/MultipleSelector.tsx +++ b/src/Selector/MultipleSelector.tsx @@ -3,21 +3,19 @@ import { useState } from 'react'; import classNames from 'classnames'; import pickAttrs from 'rc-util/lib/pickAttrs'; import Overflow from 'rc-overflow'; -import { CSSMotionList } from 'rc-motion'; import TransBtn from '../TransBtn'; import { LabelValueType, DisplayLabelValueType, RawValueType, CustomTagProps, + DefaultValueType, } from '../interface/generator'; import { RenderNode } from '../interface'; import { InnerSelectorProps } from '.'; import Input from './Input'; import useLayoutEffect from '../hooks/useLayoutEffect'; -const REST_TAG_KEY = '__RC_SELECT_MAX_REST_COUNT__'; - interface SelectorProps extends InnerSelectorProps { // Icon removeIcon?: RenderNode; @@ -36,6 +34,10 @@ interface SelectorProps extends InnerSelectorProps { onSelect: (value: RawValueType, option: { selected: boolean }) => void; } +const onPreventMouseDown = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); +}; const SelectSelector: React.FC = props => { const { id, @@ -55,7 +57,6 @@ const SelectSelector: React.FC = props => { tabIndex, removeIcon, - choiceTransitionName, maxTagCount, maxTagTextLength, @@ -71,18 +72,12 @@ const SelectSelector: React.FC = props => { onInputCompositionEnd, } = props; - const [motionAppear, setMotionAppear] = useState(false); const measureRef = React.useRef(null); const [inputWidth, setInputWidth] = useState(0); const [focused, setFocused] = useState(false); const selectionPrefixCls = `${prefixCls}-selection`; - // ===================== Motion ====================== - React.useEffect(() => { - setMotionAppear(true); - }, []); - // ===================== Search ====================== const inputValue = open || mode === 'tags' ? searchValue : ''; const inputEditable: boolean = mode === 'tags' || (showSearch && (open || focused)); @@ -92,21 +87,61 @@ const SelectSelector: React.FC = props => { setInputWidth(measureRef.current.scrollWidth); }, [inputValue]); - // ==================== Selection ==================== - let displayValues: LabelValueType[] = values; + // ===================== Render ====================== + // >>> Render Selector Node. Includes Item & Rest + function defaultRenderSelector( + content: React.ReactNode, + itemDisabled: boolean, + closable?: boolean, + onClose?: React.MouseEventHandler, + ) { + return ( + + {content} + {closable && ( + + × + + )} + + ); + } - // Cut by `maxTagCount` - let restCount: number; - if (typeof maxTagCount === 'number') { - restCount = values.length - maxTagCount; - displayValues = values.slice(0, maxTagCount); + function customizeRenderSelector( + value: DefaultValueType, + content: React.ReactNode, + itemDisabled: boolean, + closable: boolean, + onClose: React.MouseEventHandler, + ) { + return ( + + {tagRender({ + label: content, + value, + disabled: itemDisabled, + closable, + onClose, + })} + + ); } - // Update by `maxTagTextLength` - if (typeof maxTagTextLength === 'number') { - displayValues = displayValues.map(({ label, ...rest }) => { - let displayLabel: React.ReactNode = label; + function renderItem({ disabled: itemDisabled, label, value }: DisplayLabelValueType) { + const closable = !disabled && !itemDisabled; + + let displayLabel: React.ReactNode = label; + if (typeof maxTagTextLength === 'number') { if (typeof label === 'string' || typeof label === 'number') { const strLabel = String(displayLabel); @@ -114,136 +149,26 @@ const SelectSelector: React.FC = props => { displayLabel = `${strLabel.slice(0, maxTagTextLength)}...`; } } - - return { - ...rest, - label: displayLabel, - }; - }); - } - - // Fill rest - if (restCount > 0) { - displayValues.push({ - key: REST_TAG_KEY, - label: - typeof maxTagPlaceholder === 'function' - ? maxTagPlaceholder(values.slice(maxTagCount as any)) - : maxTagPlaceholder, - }); - } - - const selectionNode1 = ( - []} - motionName={choiceTransitionName} - motionAppear={motionAppear} - > - {({ key, label, value, disabled: itemDisabled, className, style }) => { - const mergedKey = key || value; - const closable = !disabled && key !== REST_TAG_KEY && !itemDisabled; - const onMouseDown = (event: React.MouseEvent) => { - event.preventDefault(); - event.stopPropagation(); - }; - const onClose = (event?: React.MouseEvent) => { - if (event) event.stopPropagation(); - onSelect(value, { selected: false }); - }; - - return typeof tagRender === 'function' ? ( - - {tagRender({ - label, - value, - disabled: itemDisabled, - closable, - onClose, - })} - - ) : ( - - {label} - {closable && ( - - × - - )} - - ); - }} - - ); - - function renderSelectorNode(childNode: React.ReactNode, itemDisabled?: boolean) { - if (typeof tagRender === 'function') { - // TODO: handle this } - return ( - - {childNode} - - ); - } - - function renderItem(item: DisplayLabelValueType) { - const { label, value, disabled: itemDisabled } = item; - const closable = !disabled && !itemDisabled; - - const onMouseDown = (event: React.MouseEvent) => { - event.preventDefault(); - event.stopPropagation(); - }; const onClose = (event?: React.MouseEvent) => { if (event) event.stopPropagation(); onSelect(value, { selected: false }); }; - return renderSelectorNode( - <> - {label} - {closable && ( - - × - - )} - , - itemDisabled, - ); + return typeof tagRender === 'function' + ? customizeRenderSelector(value, displayLabel, itemDisabled, closable, onClose) + : defaultRenderSelector(displayLabel, itemDisabled, closable, onClose); } function renderRest(omittedValues: DisplayLabelValueType[]) { - return renderSelectorNode( + const content = typeof maxTagPlaceholder === 'function' ? maxTagPlaceholder(omittedValues) - : maxTagPlaceholder, - ); - } + : maxTagPlaceholder; - // ===================== Render ====================== - console.log('>>>', inputWidth); + return defaultRenderSelector(content, false); + } // >>> Input Node const inputNode = ( diff --git a/src/generate.tsx b/src/generate.tsx index 2ea931f2f..a3b38ad71 100644 --- a/src/generate.tsx +++ b/src/generate.tsx @@ -192,7 +192,7 @@ export interface GenerateConfig { getLabeledValue: GetLabeledValue>; filterOptions: FilterOptions; findValueOption: // Need still support legacy ts api - | ((values: RawValueType[], options: FlattenOptionsType) => OptionsType) + | ((values: RawValueType[], options: FlattenOptionsType) => OptionsType) // New API add prevValueOptions support | (( values: RawValueType[], @@ -714,9 +714,7 @@ export default function generateSelector< // If menu is open, OptionList will take charge // If mode isn't tags, press enter is not meaningful when you can't see any option const onSearchSubmit = (searchText: string) => { - const newRawValues = Array.from( - new Set([...mergedRawValue, searchText]), - ); + const newRawValues = Array.from(new Set([...mergedRawValue, searchText])); triggerChange(newRawValues); newRawValues.forEach(newRawValue => { triggerSelect(newRawValue, true, 'input'); diff --git a/src/interface/generator.ts b/src/interface/generator.ts index f56dad888..ccccea005 100644 --- a/src/interface/generator.ts +++ b/src/interface/generator.ts @@ -14,24 +14,18 @@ export interface LabelValueType { value?: RawValueType; label?: React.ReactNode; } -export type DefaultValueType = - | RawValueType - | RawValueType[] - | LabelValueType - | LabelValueType[]; +export type DefaultValueType = RawValueType | RawValueType[] | LabelValueType | LabelValueType[]; export interface DisplayLabelValueType extends LabelValueType { disabled?: boolean; } -export type SingleType = MixType extends (infer Single)[] - ? Single - : MixType; +export type SingleType = MixType extends (infer Single)[] ? Single : MixType; export type OnClear = () => void; export type CustomTagProps = { - label: DefaultValueType; + label: React.ReactNode; value: DefaultValueType; disabled: boolean; onClose: (event?: React.MouseEvent) => void; @@ -59,19 +53,12 @@ export type FilterOptions = ( }, ) => OptionsType; -export type FilterFunc = ( - inputValue: string, - option?: OptionType, -) => boolean; +export type FilterFunc = (inputValue: string, option?: OptionType) => boolean; export declare function RefSelectFunc( - Component: React.RefForwardingComponent< - RefSelectProps, - SelectProps - >, + Component: React.RefForwardingComponent>, ): React.ForwardRefExoticComponent< - React.PropsWithoutRef> & - React.RefAttributes + React.PropsWithoutRef> & React.RefAttributes >; export type FlattenOptionsType = { From 0c1b6981b50f46ffa39f7b1a71a5d18f99fec192 Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 15 Dec 2020 11:07:22 +0800 Subject: [PATCH 3/4] bump rc-overflow --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6716d6028..fb9835ad4 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.0.1", - "rc-overflow": "^0.0.0-alpha.4", + "rc-overflow": "^0.0.0-alpha.5", "rc-trigger": "^5.0.4", "rc-util": "^5.0.1", "rc-virtual-list": "^3.2.0" From 98828b41a9c9aeb20fcf4a8ac3daf136a64273e9 Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 22 Dec 2020 23:12:52 +0800 Subject: [PATCH 4/4] update snapshot --- tests/__snapshots__/Multiple.test.tsx.snap | 516 +++++++++-------- tests/__snapshots__/Tags.test.tsx.snap | 625 ++++++++++++--------- 2 files changed, 656 insertions(+), 485 deletions(-) diff --git a/tests/__snapshots__/Multiple.test.tsx.snap b/tests/__snapshots__/Multiple.test.tsx.snap index 03be8fbcd..d3f03dbbc 100644 --- a/tests/__snapshots__/Multiple.test.tsx.snap +++ b/tests/__snapshots__/Multiple.test.tsx.snap @@ -7,33 +7,42 @@ exports[`Select.Multiple render not display maxTagPlaceholder if maxTagCount not
- - - - + +
+ @@ -48,86 +57,110 @@ exports[`Select.Multiple render truncates tags by maxTagCount and show maxTagPla
- - - One - - - - - - Two - -
`; @@ -139,86 +172,110 @@ exports[`Select.Multiple render truncates tags by maxTagCount and show maxTagPla
- - - One - - - - - - Two - -
`; @@ -230,75 +287,94 @@ exports[`Select.Multiple render truncates values by maxTagTextLength 1`] = `
- - - On... - - - - - - Tw... - -
`; diff --git a/tests/__snapshots__/Tags.test.tsx.snap b/tests/__snapshots__/Tags.test.tsx.snap index 592568685..1be6a1c65 100644 --- a/tests/__snapshots__/Tags.test.tsx.snap +++ b/tests/__snapshots__/Tags.test.tsx.snap @@ -8,73 +8,92 @@ exports[`Select.Tags OptGroup renders correctly 1`] = `
- - - Jack - - - - - - foo - -
- - - - + +
+
@@ -252,83 +280,107 @@ exports[`Select.Tags render truncates tags by maxTagCount and show maxTagPlaceho
- - - One - - - - - - Two - -
`; @@ -340,83 +392,107 @@ exports[`Select.Tags render truncates tags by maxTagCount and show maxTagPlaceho
- - - One - - - - - - Two - -
`; @@ -428,72 +504,91 @@ exports[`Select.Tags render truncates values by maxTagTextLength 1`] = `
- - - On... - - - - - - Tw... - -
`;