Skip to content

Commit 23a6ea7

Browse files
EmilyyyLiu刘欢
andauthored
feat: Add attribute itemRender to 'segmented' (#301)
* feat: Add attributes to 'segmented' * feat: add props to itemRender * feat:add ts to ItemContent * feat: using...props * feat: change itemInfo value * feat: change ts * feat: delete SegementItem * feat: pass optionData to SegmentedOption * test: add itemRender test --------- Co-authored-by: 刘欢 <lh01217311@antgroup.com>
1 parent 17b4ba3 commit 23a6ea7

File tree

2 files changed

+105
-34
lines changed

2 files changed

+105
-34
lines changed

src/index.tsx

Lines changed: 62 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ export interface SegmentedLabeledOption<ValueType = SegmentedRawOption> {
2222
title?: string;
2323
}
2424

25+
type ItemRender = (
26+
node: React.ReactNode,
27+
info: { item: SegmentedLabeledOption },
28+
) => React.ReactNode;
29+
2530
type SegmentedOptions<T = SegmentedRawOption> = (
2631
| T
2732
| SegmentedLabeledOption<T>
@@ -44,6 +49,7 @@ export interface SegmentedProps<ValueType = SegmentedValue>
4449
name?: string;
4550
classNames?: Partial<Record<SemanticName, string>>;
4651
styles?: Partial<Record<SemanticName, React.CSSProperties>>;
52+
itemRender?: ItemRender;
4753
}
4854

4955
function getValidTitle(option: SegmentedLabeledOption) {
@@ -80,6 +86,7 @@ const InternalSegmentedOption: React.FC<{
8086
style?: React.CSSProperties;
8187
classNames?: Partial<Record<SemanticName, string>>;
8288
styles?: Partial<Record<SemanticName, React.CSSProperties>>;
89+
data: SegmentedLabeledOption;
8390
disabled?: boolean;
8491
checked: boolean;
8592
label: React.ReactNode;
@@ -95,12 +102,14 @@ const InternalSegmentedOption: React.FC<{
95102
onKeyDown: (e: React.KeyboardEvent) => void;
96103
onKeyUp: (e: React.KeyboardEvent) => void;
97104
onMouseDown: () => void;
105+
itemRender?: ItemRender;
98106
}> = ({
99107
prefixCls,
100108
className,
101109
style,
102110
styles,
103111
classNames: segmentedClassNames,
112+
data,
104113
disabled,
105114
checked,
106115
label,
@@ -113,15 +122,15 @@ const InternalSegmentedOption: React.FC<{
113122
onKeyDown,
114123
onKeyUp,
115124
onMouseDown,
125+
itemRender = (node: React.ReactNode) => node,
116126
}) => {
117127
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
118128
if (disabled) {
119129
return;
120130
}
121131
onChange(event, value);
122132
};
123-
124-
return (
133+
const itemContent: React.ReactNode = (
125134
<label
126135
className={classNames(className, {
127136
[`${prefixCls}-item-disabled`]: disabled,
@@ -155,6 +164,7 @@ const InternalSegmentedOption: React.FC<{
155164
</div>
156165
</label>
157166
);
167+
return itemRender(itemContent, { item: data });
158168
};
159169

160170
const Segmented = React.forwardRef<HTMLDivElement, SegmentedProps>(
@@ -174,6 +184,7 @@ const Segmented = React.forwardRef<HTMLDivElement, SegmentedProps>(
174184
styles,
175185
classNames: segmentedClassNames,
176186
motionName = 'thumb-motion',
187+
itemRender,
177188
...restProps
178189
} = props;
179190

@@ -258,6 +269,54 @@ const Segmented = React.forwardRef<HTMLDivElement, SegmentedProps>(
258269
break;
259270
}
260271
};
272+
273+
const renderOption = (segmentedOption: SegmentedLabeledOption) => {
274+
const {
275+
label,
276+
value: optionValue,
277+
disabled: optionDisabled,
278+
title,
279+
} = segmentedOption;
280+
const optionData: SegmentedLabeledOption = {
281+
label,
282+
value: optionValue,
283+
disabled: optionDisabled,
284+
title,
285+
};
286+
return (
287+
<InternalSegmentedOption
288+
{...segmentedOption}
289+
name={name}
290+
data={optionData}
291+
itemRender={itemRender}
292+
key={optionValue}
293+
prefixCls={prefixCls}
294+
className={classNames(
295+
segmentedOption.className,
296+
`${prefixCls}-item`,
297+
segmentedClassNames?.item,
298+
{
299+
[`${prefixCls}-item-selected`]:
300+
optionValue === rawValue && !thumbShow,
301+
[`${prefixCls}-item-focused`]:
302+
isFocused && isKeyboard && optionValue === rawValue,
303+
},
304+
)}
305+
style={styles?.item}
306+
classNames={segmentedClassNames}
307+
styles={styles}
308+
checked={optionValue === rawValue}
309+
onChange={handleChange}
310+
onFocus={handleFocus}
311+
onBlur={handleBlur}
312+
onKeyDown={handleKeyDown}
313+
onKeyUp={handleKeyUp}
314+
onMouseDown={handleMouseDown}
315+
disabled={!!disabled || !!optionDisabled}
316+
/>
317+
);
318+
};
319+
261320
return (
262321
<div
263322
role="radiogroup"
@@ -294,38 +353,7 @@ const Segmented = React.forwardRef<HTMLDivElement, SegmentedProps>(
294353
setThumbShow(false);
295354
}}
296355
/>
297-
{segmentedOptions.map((segmentedOption) => (
298-
<InternalSegmentedOption
299-
{...segmentedOption}
300-
name={name}
301-
key={segmentedOption.value}
302-
prefixCls={prefixCls}
303-
className={classNames(
304-
segmentedOption.className,
305-
`${prefixCls}-item`,
306-
segmentedClassNames?.item,
307-
{
308-
[`${prefixCls}-item-selected`]:
309-
segmentedOption.value === rawValue && !thumbShow,
310-
[`${prefixCls}-item-focused`]:
311-
isFocused &&
312-
isKeyboard &&
313-
segmentedOption.value === rawValue,
314-
},
315-
)}
316-
style={styles?.item}
317-
classNames={segmentedClassNames}
318-
styles={styles}
319-
checked={segmentedOption.value === rawValue}
320-
onChange={handleChange}
321-
onFocus={handleFocus}
322-
onBlur={handleBlur}
323-
onKeyDown={handleKeyDown}
324-
onKeyUp={handleKeyUp}
325-
onMouseDown={handleMouseDown}
326-
disabled={!!disabled || !!segmentedOption.disabled}
327-
/>
328-
))}
356+
{segmentedOptions.map(renderOption)}
329357
</div>
330358
</div>
331359
);

tests/index.test.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,4 +799,47 @@ describe('Segmented keyboard navigation', () => {
799799
expect(itemElement.style.color).toBe('yellow');
800800
expect(labelElement.style.backgroundColor).toBe('black');
801801
});
802+
describe('itemRender', () => {
803+
it('When "itemRender" is not configured, render the original "label"', () => {
804+
const { container } = render(
805+
<Segmented options={['iOS', 'Android', 'Web']} />,
806+
);
807+
const label = container.querySelector('.rc-segmented-item-label');
808+
expect(label).toHaveTextContent('iOS');
809+
});
810+
it('Configure "itemRender" to render the return value', () => {
811+
const { container } = render(
812+
<Segmented
813+
options={['iOS', 'Android', 'Web']}
814+
itemRender={(node) => <div className="test-title">{node}</div>}
815+
/>,
816+
);
817+
const labels = container.querySelectorAll('.test-title');
818+
expect(labels).toHaveLength(3);
819+
});
820+
it('should pass complete params to itemRender', () => {
821+
const mockItemRender = jest.fn((node, params) => node);
822+
const testData = {
823+
label: 'iOS',
824+
value: 'iOS',
825+
disabled: false,
826+
title: 'iOS',
827+
};
828+
render(
829+
<Segmented
830+
options={[{ ...testData, className: 'test-class' }, 'Android', 'Web']}
831+
itemRender={mockItemRender}
832+
/>,
833+
);
834+
expect(mockItemRender).toHaveBeenCalledTimes(3);
835+
const callArgs = mockItemRender.mock.calls[0];
836+
const receivedParams = callArgs[1];
837+
expect(receivedParams).toEqual({
838+
item: {
839+
...testData,
840+
},
841+
});
842+
expect(React.isValidElement(callArgs[0])).toBeTruthy();
843+
});
844+
});
802845
});

0 commit comments

Comments
 (0)