Skip to content

Commit e3deedd

Browse files
authored
feat: support tagRender in multiple picker (#963)
* feat: support tagRender in multiple picker * fix: guard tagRender removal when disabled
1 parent d27127f commit e3deedd

File tree

6 files changed

+145
-17
lines changed

6 files changed

+145
-17
lines changed

docs/examples/multiple.tsx

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,50 @@ export default () => {
2929
return (
3030
<div>
3131
<SinglePicker {...sharedLocale} multiple ref={singleRef} onOpenChange={console.error} />
32-
<SinglePicker {...sharedLocale} multiple ref={singleRef} needConfirm />
33-
<SinglePicker {...sharedLocale} multiple picker="week" defaultValue={[
34-
dayjs('2021-01-01'),
35-
dayjs('2021-01-08'),
36-
]} />
32+
<SinglePicker {...sharedLocale} multiple needConfirm />
33+
<SinglePicker
34+
{...sharedLocale}
35+
multiple
36+
picker="week"
37+
defaultValue={[dayjs('2021-01-01'), dayjs('2021-01-08')]}
38+
/>
39+
<SinglePicker
40+
{...sharedLocale}
41+
multiple
42+
defaultValue={[dayjs('2021-01-01'), dayjs('2021-01-08')]}
43+
tagRender={({ label, value, closable, onClose }) => {
44+
const locked = value.isBefore(dayjs('2021-01-05'), 'day');
45+
46+
return (
47+
<span
48+
style={{
49+
display: 'inline-flex',
50+
alignItems: 'center',
51+
gap: 4,
52+
marginInlineEnd: 4,
53+
padding: '0 6px',
54+
border: '1px solid #1677ff',
55+
borderRadius: 12,
56+
}}
57+
>
58+
<span>{label}</span>
59+
{!closable || locked ? (
60+
<span>locked</span>
61+
) : (
62+
<button
63+
type="button"
64+
onMouseDown={(event) => {
65+
event.preventDefault();
66+
}}
67+
onClick={onClose}
68+
>
69+
remove
70+
</button>
71+
)}
72+
</span>
73+
);
74+
}}
75+
/>
3776
</div>
3877
);
3978
};

src/PickerInput/Selector/SingleSelector/MultipleDates.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import { clsx } from 'clsx';
22
import Overflow from '@rc-component/overflow';
33
import * as React from 'react';
44
import type { MouseEventHandler } from 'react';
5-
import type { PickerProps } from '../../SinglePicker';
5+
import type { CustomTagProps, PickerProps } from '../../SinglePicker';
66

77
export interface MultipleDatesProps<DateType extends object = any> extends Pick<
88
PickerProps,
9-
'maxTagCount'
9+
'maxTagCount' | 'tagRender'
1010
> {
1111
prefixCls: string;
1212
value: DateType[];
@@ -28,6 +28,7 @@ export default function MultipleDates<DateType extends object = any>(
2828
formatDate,
2929
disabled,
3030
maxTagCount,
31+
tagRender,
3132
placeholder,
3233
} = props;
3334

@@ -60,12 +61,25 @@ export default function MultipleDates<DateType extends object = any>(
6061

6162
function renderItem(date: DateType) {
6263
const displayLabel: React.ReactNode = formatDate(date);
64+
const closable = !disabled;
6365

64-
const onClose = (event?: React.MouseEvent) => {
66+
const onClose: CustomTagProps<DateType>['onClose'] = (event) => {
6567
if (event) event.stopPropagation();
66-
onRemove(date);
68+
if (!disabled) {
69+
onRemove(date);
70+
}
6771
};
6872

73+
if (tagRender) {
74+
return tagRender({
75+
label: displayLabel,
76+
value: date,
77+
disabled: !!disabled,
78+
closable,
79+
onClose,
80+
});
81+
}
82+
6983
return renderSelector(displayLabel, onClose);
7084
}
7185

src/PickerInput/Selector/SingleSelector/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ import useRootProps from '../hooks/useRootProps';
1111
import MultipleDates from './MultipleDates';
1212

1313
export interface SingleSelectorProps<DateType extends object = any>
14-
extends SelectorProps<DateType>,
15-
Pick<PickerProps, 'multiple' | 'maxTagCount'> {
14+
extends SelectorProps<DateType>, Pick<PickerProps, 'multiple' | 'maxTagCount' | 'tagRender'> {
1615
id?: string;
1716

1817
value?: DateType[];
@@ -75,6 +74,7 @@ function SingleSelector<DateType extends object = any>(
7574
onInputChange,
7675
multiple,
7776
maxTagCount,
77+
tagRender,
7878

7979
// Valid
8080
format,
@@ -170,6 +170,7 @@ function SingleSelector<DateType extends object = any>(
170170
onRemove={onMultipleRemove}
171171
formatDate={getText}
172172
maxTagCount={maxTagCount}
173+
tagRender={tagRender}
173174
disabled={disabled}
174175
removeIcon={removeIcon}
175176
placeholder={placeholder}

src/PickerInput/SinglePicker.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,27 @@ import useSemantic from '../hooks/useSemantic';
3636

3737
// TODO: isInvalidateDate with showTime.disabledTime should not provide `range` prop
3838

39-
export interface BasePickerProps<DateType extends object = any>
40-
extends SharedPickerProps<DateType> {
39+
export interface CustomTagProps<DateType extends object = any> {
40+
label: React.ReactNode;
41+
value: DateType;
42+
disabled: boolean;
43+
onClose: (event?: React.MouseEvent<HTMLElement>) => void;
44+
closable: boolean;
45+
}
46+
47+
export interface BasePickerProps<
48+
DateType extends object = any,
49+
> extends SharedPickerProps<DateType> {
4150
// Structure
4251
id?: string;
4352

4453
/** Not support `time` or `datetime` picker */
4554
multiple?: boolean;
4655
removeIcon?: React.ReactNode;
47-
/** Only work when `multiple` is in used */
56+
/** Only works when `multiple` is in use */
4857
maxTagCount?: number | 'responsive';
58+
/** Only works when `multiple` is in use */
59+
tagRender?: (props: CustomTagProps<DateType>) => React.ReactNode;
4960

5061
// Value
5162
value?: DateType | DateType[] | null;
@@ -100,8 +111,7 @@ export interface BasePickerProps<DateType extends object = any>
100111
}
101112

102113
export interface PickerProps<DateType extends object = any>
103-
extends BasePickerProps<DateType>,
104-
Omit<SharedTimeProps<DateType>, 'format' | 'defaultValue'> {}
114+
extends BasePickerProps<DateType>, Omit<SharedTimeProps<DateType>, 'format' | 'defaultValue'> {}
105115

106116
/** Internal usage. For cross function get same aligned props */
107117
export type ReplacedPickerProps<DateType extends object = any> = {
@@ -174,6 +184,7 @@ function Picker<DateType extends object = any>(
174184

175185
suffixIcon,
176186
removeIcon,
187+
tagRender,
177188

178189
// Focus
179190
onFocus,
@@ -657,6 +668,7 @@ function Picker<DateType extends object = any>(
657668
// Icon
658669
suffixIcon={suffixIcon}
659670
removeIcon={removeIcon}
671+
tagRender={tagRender}
660672
// Active
661673
activeHelp={!!internalHoverValue}
662674
allHelp={!!internalHoverValue && hoverSource === 'preset'}

src/index.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@
2929

3030
import type { PickerRef, SharedTimeProps } from './interface';
3131
import RangePicker, { type RangePickerProps } from './PickerInput/RangePicker';
32-
import Picker, { type BasePickerProps, type PickerProps } from './PickerInput/SinglePicker';
32+
import Picker, {
33+
type BasePickerProps,
34+
type CustomTagProps,
35+
type PickerProps,
36+
} from './PickerInput/SinglePicker';
3337
import PickerPanel, { type BasePickerPanelProps, type PickerPanelProps } from './PickerPanel';
3438

3539
export { Picker, RangePicker, PickerPanel };
@@ -40,6 +44,7 @@ export type {
4044
PickerRef,
4145
BasePickerProps,
4246
BasePickerPanelProps,
47+
CustomTagProps,
4348
SharedTimeProps,
4449
};
4550
export default Picker;

tests/multiple.spec.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,63 @@ describe('Picker.Multiple', () => {
140140
expect(container.querySelector('.custom-remove')).toBeTruthy();
141141
});
142142

143+
it('tagRender', () => {
144+
const onChange = jest.fn();
145+
const { container } = render(
146+
<DayPicker
147+
multiple
148+
onChange={onChange}
149+
defaultValue={[getDay('2000-01-01'), getDay('2000-01-02')]}
150+
tagRender={({ label, value, onClose }) => (
151+
<span className="custom-tag">
152+
<span className="custom-tag-label">{label}</span>
153+
{value.date() !== 1 && (
154+
<button type="button" className="custom-tag-close" onClick={onClose}>
155+
x
156+
</button>
157+
)}
158+
</span>
159+
)}
160+
/>,
161+
);
162+
163+
expect(container.querySelectorAll('.custom-tag')).toHaveLength(2);
164+
expect(container.querySelectorAll('.custom-tag-close')).toHaveLength(1);
165+
166+
fireEvent.click(container.querySelector('.custom-tag-close'));
167+
expect(onChange).toHaveBeenCalledWith(expect.anything(), ['2000-01-01']);
168+
});
169+
170+
it('tagRender should not remove when disabled', () => {
171+
const onChange = jest.fn();
172+
const tagRender = jest.fn(({ label, onClose }) => (
173+
<button type="button" className="custom-tag-close" onClick={onClose}>
174+
{label}
175+
</button>
176+
));
177+
178+
const { container } = render(
179+
<DayPicker
180+
multiple
181+
disabled
182+
onChange={onChange}
183+
defaultValue={[getDay('2000-01-01')]}
184+
tagRender={tagRender}
185+
/>,
186+
);
187+
188+
expect(tagRender).toHaveBeenCalledWith(
189+
expect.objectContaining({
190+
disabled: true,
191+
closable: false,
192+
}),
193+
);
194+
195+
fireEvent.click(container.querySelector('.custom-tag-close'));
196+
expect(onChange).not.toHaveBeenCalled();
197+
expect(container.querySelectorAll('.custom-tag-close')).toHaveLength(1);
198+
});
199+
143200
describe('placeholder', () => {
144201
it('show placeholder', () => {
145202
const { container } = render(<DayPicker multiple placeholder="bamboo" />);

0 commit comments

Comments
 (0)