Skip to content

Commit ec849bd

Browse files
fguittonzombieJ
andauthored
Adding a custom renderer for tags (#445)
* Adding a custom Renderer for tags * Adding Readme description * Passing over CSSMotionList properties to enable proper refresh * Flattening getTagCloseProps into separate closable and closeTag * Addressing variable rename from #445 (comment) * Update Tags.test.tsx Co-authored-by: 二货机器人 <[email protected]>
1 parent 3ce2892 commit ec849bd

File tree

7 files changed

+248
-30
lines changed

7 files changed

+248
-30
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ React.render(c, container);
8484
|showArrow | whether show arrow | bool | true (single mode), false (multiple mode) |
8585
|allowClear | whether allowClear | bool | false |
8686
|tags | when tagging is enabled the user can select from pre-existing options or create a new tag by picking the first choice, which is what the user has typed into the search box so far. | bool | false |
87+
|tagRender | render custom tags. | (props: CustomTagProps) => ReactNode | - |
8788
|maxTagTextLength | max tag text length to show | number | - |
8889
|maxTagCount | max tag count to show | number | - |
8990
|maxTagPlaceholder | placeholder for omitted values | ReactNode/function(omittedValues) | - |

examples/custom-tags.tsx

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/* eslint-disable no-console */
2+
import React from 'react';
3+
import Select, { Option } from '../src';
4+
import '../assets/index.less';
5+
import { CustomTagProps } from '../src/interface/generator';
6+
7+
const children = [];
8+
for (let i = 10; i < 36; i += 1) {
9+
children.push(
10+
<Option key={i.toString(36) + i} test={i}>
11+
{i.toString(36) + i}
12+
</Option>,
13+
);
14+
}
15+
16+
const Test: React.FC = () => {
17+
const [disabled, setDisabled] = React.useState(false);
18+
const [value, setValue] = React.useState<string[]>(['name2', '42', 'name3']);
19+
const [maxTagCount, setMaxTagCount] = React.useState<number>(null);
20+
21+
const toggleMaxTagCount = (count: number) => {
22+
setMaxTagCount(count);
23+
};
24+
25+
const tagRender = (props: CustomTagProps) => {
26+
const { label, closable, onClose } = props;
27+
let style: React.CSSProperties;
28+
if (parseInt(label as string, 10)) {
29+
style = { backgroundColor: 'blue' };
30+
} else if (!closable) {
31+
style = { backgroundColor: 'white' };
32+
} else {
33+
style = { backgroundColor: 'red' };
34+
}
35+
return (
36+
<span style={style}>
37+
{label}
38+
{closable ? (
39+
<button type="button" onClick={onClose}>
40+
x
41+
</button>
42+
) : null}
43+
</span>
44+
);
45+
};
46+
47+
return (
48+
<div>
49+
<h2>tags select with custom renderer(scroll the menu)</h2>
50+
51+
<div>
52+
<Select
53+
placeholder="placeholder"
54+
mode="tags"
55+
style={{ width: 500 }}
56+
disabled={disabled}
57+
maxTagCount={maxTagCount}
58+
maxTagTextLength={10}
59+
value={value}
60+
onChange={(val: string[], option) => {
61+
console.log('change:', val, option);
62+
setValue(val);
63+
}}
64+
onSelect={(val, option) => {
65+
console.log('selected', val, option);
66+
}}
67+
onDeselect={(val, option) => {
68+
console.log('deselected', val, option);
69+
}}
70+
tokenSeparators={[',']}
71+
tagRender={tagRender}
72+
onFocus={() => console.log('focus')}
73+
onBlur={() => console.log('blur')}
74+
>
75+
{children}
76+
</Select>
77+
</div>
78+
<p>
79+
<button
80+
type="button"
81+
onClick={() => {
82+
setDisabled(!disabled);
83+
}}
84+
>
85+
toggle disabled
86+
</button>
87+
<button type="button" onClick={() => toggleMaxTagCount(0)}>
88+
toggle maxTagCount (0)
89+
</button>
90+
<button type="button" onClick={() => toggleMaxTagCount(1)}>
91+
toggle maxTagCount (1)
92+
</button>
93+
<button type="button" onClick={() => toggleMaxTagCount(null)}>
94+
toggle maxTagCount (null)
95+
</button>
96+
</p>
97+
</div>
98+
);
99+
};
100+
101+
export default Test;
102+
/* eslint-enable */

src/Selector/MultipleSelector.tsx

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import React from 'react';
22
import classNames from 'classnames';
33
import CSSMotionList from 'rc-animate/lib/CSSMotionList';
44
import TransBtn from '../TransBtn';
5-
import { LabelValueType, RawValueType } from '../interface/generator';
5+
import {
6+
LabelValueType,
7+
RawValueType,
8+
CustomTagProps,
9+
} from '../interface/generator';
610
import { RenderNode } from '../interface';
711
import { InnerSelectorProps } from '.';
812
import Input from './Input';
@@ -17,8 +21,11 @@ interface SelectorProps extends InnerSelectorProps {
1721
// Tags
1822
maxTagCount?: number;
1923
maxTagTextLength?: number;
20-
maxTagPlaceholder?: React.ReactNode | ((omittedValues: LabelValueType[]) => React.ReactNode);
24+
maxTagPlaceholder?:
25+
| React.ReactNode
26+
| ((omittedValues: LabelValueType[]) => React.ReactNode);
2127
tokenSeparators?: string[];
28+
tagRender?: (props: CustomTagProps) => React.ReactElement;
2229

2330
// Motion
2431
choiceTransitionName?: string;
@@ -48,7 +55,9 @@ const SelectSelector: React.FC<SelectorProps> = ({
4855

4956
maxTagCount,
5057
maxTagTextLength,
51-
maxTagPlaceholder = (omittedValues: LabelValueType[]) => `+ ${omittedValues.length} ...`,
58+
maxTagPlaceholder = (omittedValues: LabelValueType[]) =>
59+
`+ ${omittedValues.length} ...`,
60+
tagRender,
5261

5362
onSelect,
5463
onInputChange,
@@ -122,27 +131,47 @@ const SelectSelector: React.FC<SelectorProps> = ({
122131
>
123132
{({ key, label, value, disabled: itemDisabled, className, style }) => {
124133
const mergedKey = key || value;
125-
126-
return (
134+
const closable = key !== REST_TAG_KEY && !itemDisabled;
135+
const onMouseDown = (event: React.MouseEvent) => {
136+
event.preventDefault();
137+
event.stopPropagation();
138+
};
139+
const onClose = (event?: React.MouseEvent) => {
140+
if (event) event.stopPropagation();
141+
onSelect(value, { selected: false });
142+
};
143+
144+
return typeof tagRender === 'function' ? (
145+
<span
146+
key={mergedKey}
147+
onMouseDown={onMouseDown}
148+
className={className}
149+
style={style}
150+
>
151+
{tagRender({
152+
label,
153+
value,
154+
disabled: itemDisabled,
155+
closable,
156+
onClose,
157+
})}
158+
</span>
159+
) : (
127160
<span
128161
key={mergedKey}
129162
className={classNames(className, `${prefixCls}-selection-item`, {
130163
[`${prefixCls}-selection-item-disabled`]: itemDisabled,
131164
})}
132165
style={style}
133166
>
134-
<span className={`${prefixCls}-selection-item-content`}>{label}</span>
135-
{key !== REST_TAG_KEY && !itemDisabled && (
167+
<span className={`${prefixCls}-selection-item-content`}>
168+
{label}
169+
</span>
170+
{closable && (
136171
<TransBtn
137172
className={`${prefixCls}-selection-item-remove`}
138-
onMouseDown={event => {
139-
event.preventDefault();
140-
event.stopPropagation();
141-
}}
142-
onClick={event => {
143-
event.stopPropagation();
144-
onSelect(value, { selected: false });
145-
}}
173+
onMouseDown={onMouseDown}
174+
onClick={onClose}
146175
customizeIcon={removeIcon}
147176
>
148177
×
@@ -158,7 +187,10 @@ const SelectSelector: React.FC<SelectorProps> = ({
158187
<>
159188
{selectionNode}
160189

161-
<span className={`${prefixCls}-selection-search`} style={{ width: inputWidth }}>
190+
<span
191+
className={`${prefixCls}-selection-search`}
192+
style={{ width: inputWidth }}
193+
>
162194
<Input
163195
ref={inputRef}
164196
open={open}
@@ -177,13 +209,19 @@ const SelectSelector: React.FC<SelectorProps> = ({
177209
/>
178210

179211
{/* Measure Node */}
180-
<span ref={measureRef} className={`${prefixCls}-selection-search-mirror`} aria-hidden>
212+
<span
213+
ref={measureRef}
214+
className={`${prefixCls}-selection-search-mirror`}
215+
aria-hidden
216+
>
181217
{searchValue}&nbsp;
182218
</span>
183219
</span>
184220

185221
{!values.length && !searchValue && (
186-
<span className={`${prefixCls}-selection-placeholder`}>{placeholder}</span>
222+
<span className={`${prefixCls}-selection-placeholder`}>
223+
{placeholder}
224+
</span>
187225
)}
188226
</>
189227
);

src/Selector/index.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ import * as React from 'react';
1212
import KeyCode from 'rc-util/lib/KeyCode';
1313
import MultipleSelector from './MultipleSelector';
1414
import SingleSelector from './SingleSelector';
15-
import { LabelValueType, RawValueType } from '../interface/generator';
15+
import {
16+
LabelValueType,
17+
RawValueType,
18+
CustomTagProps,
19+
} from '../interface/generator';
1620
import { RenderNode, Mode } from '../interface';
1721
import useLock from '../hooks/useLock';
1822

@@ -74,6 +78,7 @@ export interface SelectorProps {
7478
maxTagPlaceholder?:
7579
| React.ReactNode
7680
| ((omittedValues: LabelValueType[]) => React.ReactNode);
81+
tagRender?: (props: CustomTagProps) => React.ReactElement;
7782

7883
// Motion
7984
choiceTransitionName?: string;

src/generate.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
OnClear,
2929
INTERNAL_PROPS_MARK,
3030
SelectSource,
31+
CustomTagProps,
3132
} from './interface/generator';
3233
import { OptionListProps, RefOptionListProps } from './OptionList';
3334
import {
@@ -128,6 +129,7 @@ export interface SelectProps<OptionsType extends object[], ValueType>
128129
| React.ReactNode
129130
| ((omittedValues: LabelValueType[]) => React.ReactNode);
130131
tokenSeparators?: string[];
132+
tagRender?: (props: CustomTagProps) => React.ReactElement;
131133
showAction?: ('focus' | 'click')[];
132134
tabIndex?: number;
133135

@@ -307,6 +309,7 @@ export default function generateSelector<
307309

308310
// Tags
309311
tokenSeparators,
312+
tagRender,
310313

311314
// Events
312315
onPopupScroll,
@@ -1049,6 +1052,7 @@ export default function generateSelector<
10491052
mode={mode}
10501053
accessibilityIndex={accessibilityIndex}
10511054
multiple={isMultiple}
1055+
tagRender={tagRender}
10521056
values={displayValues}
10531057
open={mergedOpen}
10541058
onToggleOpen={onToggleOpen}

src/interface/generator.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,30 @@ export interface LabelValueType {
1414
value?: RawValueType;
1515
label?: React.ReactNode;
1616
}
17-
export type DefaultValueType = RawValueType | RawValueType[] | LabelValueType | LabelValueType[];
17+
export type DefaultValueType =
18+
| RawValueType
19+
| RawValueType[]
20+
| LabelValueType
21+
| LabelValueType[];
1822

1923
export interface DisplayLabelValueType extends LabelValueType {
2024
disabled?: boolean;
2125
}
2226

23-
export type SingleType<MixType> = MixType extends (infer Single)[] ? Single : MixType;
27+
export type SingleType<MixType> = MixType extends (infer Single)[]
28+
? Single
29+
: MixType;
2430

2531
export type OnClear = () => void;
2632

33+
export type CustomTagProps = {
34+
label: DefaultValueType;
35+
value: DefaultValueType;
36+
disabled: boolean;
37+
onClose: (event?: React.MouseEvent<HTMLElement, MouseEvent>) => void;
38+
closable: boolean;
39+
};
40+
2741
// ==================================== Generator ====================================
2842
export type GetLabeledValue<FOT extends FlattenOptionsType> = (
2943
value: RawValueType,
@@ -39,15 +53,25 @@ export type FilterOptions<OptionsType extends object[]> = (
3953
searchValue: string,
4054
options: OptionsType,
4155
/** Component props, since Select & TreeSelect use different prop name, use any here */
42-
config: { optionFilterProp: string; filterOption: boolean | FilterFunc<OptionsType[number]> },
56+
config: {
57+
optionFilterProp: string;
58+
filterOption: boolean | FilterFunc<OptionsType[number]>;
59+
},
4360
) => OptionsType;
4461

45-
export type FilterFunc<OptionType> = (inputValue: string, option?: OptionType) => boolean;
62+
export type FilterFunc<OptionType> = (
63+
inputValue: string,
64+
option?: OptionType,
65+
) => boolean;
4666

4767
export declare function RefSelectFunc<OptionsType extends object[], ValueType>(
48-
Component: React.RefForwardingComponent<RefSelectProps, SelectProps<OptionsType, ValueType>>,
68+
Component: React.RefForwardingComponent<
69+
RefSelectProps,
70+
SelectProps<OptionsType, ValueType>
71+
>,
4972
): React.ForwardRefExoticComponent<
50-
React.PropsWithoutRef<SelectProps<OptionsType, ValueType>> & React.RefAttributes<RefSelectProps>
73+
React.PropsWithoutRef<SelectProps<OptionsType, ValueType>> &
74+
React.RefAttributes<RefSelectProps>
5175
>;
5276

5377
export type FlattenOptionsType<OptionsType extends object[] = object[]> = {

0 commit comments

Comments
 (0)