Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support custom label render #995

Merged
merged 11 commits into from
Jan 2, 2024
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export default () => (
| virtual | Disable virtual scroll | boolean | true |
| direction | direction of dropdown | 'ltr' \| 'rtl' | 'ltr' |
| optionRender | Custom rendering options | (oriOption: FlattenOptionData\<BaseOptionType\> , info: { index: number }) => React.ReactNode | - |
| labelRender | Custom rendering label | (props: LabelInValueType) => React.ReactNode | - |
| maxCount | The max number of items can be selected | number | - |

### Methods
Expand Down
8 changes: 8 additions & 0 deletions docs/demo/custom-label.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: custom-label
nav:
title: Demo
path: /demo
---

<code src="../examples/custom-label.tsx"></code>
66 changes: 66 additions & 0 deletions docs/examples/custom-label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/* eslint-disable no-console */
import Select, { Option } from 'rc-select';
import React from 'react';
import '../../assets/index.less';

const children = [];
for (let i = 10; i < 36; i += 1) {
children.push(
<Option key={i.toString(36) + i} test={i}>
{i.toString(36) + i}
</Option>,
);
}

const Test: React.FC = () => {
const [value, setValue] = React.useState<string>('test');

return (
<div>
<h2>custom label render</h2>

<div>
<Select
placeholder="placeholder"
style={{ width: 500 }}
value={value}
onChange={(val: string, option) => {
console.log('change', val, option);
setValue(val);
}}
onSelect={(val, option) => {
console.log('selected', val, option);
}}
onDeselect={(val, option) => {
console.log('deselected', val, option);
}}
tokenSeparators={[',']}
labelRender={(props) => {
const { label, value: _value } = props;
const style: React.CSSProperties = { backgroundColor: 'red' };
if (label) {
return _value;
} else return <span style={style}>no this value in options</span>;
}}
onFocus={() => console.log('focus')}
onBlur={() => console.log('blur')}
>
{children}
</Select>
</div>
<p>
<button
type="button"
onClick={() => {
setValue('test');
}}
>
set value as test
</button>
</p>
</div>
);
};

export default Test;
/* eslint-enable */
6 changes: 4 additions & 2 deletions src/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export interface SelectProps<ValueType = any, OptionType extends BaseOptionType
direction?: 'ltr' | 'rtl';
listHeight?: number;
listItemHeight?: number;
labelRender?: (props: LabelInValueType) => React.ReactNode;

// >>> Icon
menuItemSelectedIcon?: RenderNode;
Expand Down Expand Up @@ -199,6 +200,7 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
direction,
listHeight = 200,
listItemHeight = 20,
labelRender,

// Value
value,
Expand Down Expand Up @@ -343,9 +345,9 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp

return mergedValues.map((item) => ({
...item,
label: item.label ?? item.value,
label: (typeof labelRender === 'function' ? labelRender(item) : item.label) ?? item.value,
yoyo837 marked this conversation as resolved.
Show resolved Hide resolved
}));
}, [mode, mergedValues]);
}, [mode, mergedValues, labelRender]);

/** Convert `displayValues` to raw value type set */
const rawValues = React.useMemo(
Expand Down
65 changes: 65 additions & 0 deletions tests/Select.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { LabelInValueType } from '@/Select';
import { fireEvent, render as testingRender } from '@testing-library/react';
import { mount, render } from 'enzyme';
import KeyCode from 'rc-util/lib/KeyCode';
Expand Down Expand Up @@ -2115,6 +2116,70 @@ describe('Select.Basic', () => {
);
});

it('labelRender', () => {
const onLabelRender = jest.fn();
const labelRender = (props: LabelInValueType) => {
const { label, value } = props;
onLabelRender();
return `${label}-${value}`;
};
const wrapper = mount(
<Select options={[{ label: 'realLabel', value: 'a' }]} value="a" labelRender={labelRender} />,
);

expect(onLabelRender).toHaveBeenCalled();
expect(findSelection(wrapper).text()).toEqual('realLabel-a');
});

it('labelRender when value is not in options', () => {
const onLabelRender = jest.fn();
const options = [{ label: 'realLabel', value: 'b' }];
const labelRender = (props: LabelInValueType) => {
const { label, value } = props;
// current value is in options
if (options.find((item) => item.value === value)) {
return label;
} else {
// current value is not in options
onLabelRender();
return `${label || 'fakeLabel'}-${value}`;
}
};
const wrapper = mount(<Select value="a" labelRender={labelRender} options={options} />);

expect(onLabelRender).toHaveBeenCalled();
expect(findSelection(wrapper).text()).toEqual('fakeLabel-a');
});
xliez marked this conversation as resolved.
Show resolved Hide resolved

it('labelRender when labelInValue and useCache', () => {
const onLabelRender = jest.fn();
const labelRender = (props: LabelInValueType) => {
const { label, value } = props;
onLabelRender({ label, value });
return `custom label`;
};

const wrapper = mount(
<Select
labelInValue
value={{ key: 1, label: 'One' }}
labelRender={labelRender}
options={[
{
value: 2,
label: 'Two',
},
]}
/>,
);

expect(onLabelRender).toHaveBeenCalledWith({ label: 'One', value: 1 });
expect(findSelection(wrapper).text()).toEqual('custom label');

wrapper.setProps({ options: [] });
expect(findSelection(wrapper).text()).toEqual('custom label');
});

it('multiple items should not disabled', () => {
const { container } = testingRender(
<Select
Expand Down
Loading