Skip to content

Commit

Permalink
feat: Implement optionRender API (#987)
Browse files Browse the repository at this point in the history
* feat: implement  optionRender api

* chore: clean code

* docs: update md

* refactor: modify api

* refactor: fix type definition
  • Loading branch information
RedJue committed Oct 25, 2023
1 parent eda3dca commit 44d9806
Show file tree
Hide file tree
Showing 7 changed files with 83 additions and 16 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

React Select

<!-- prettier-ignore -->
[![NPM version][npm-image]][npm-url]
[![npm download][download-image]][download-url]
[![build status][github-actions-image]][github-actions-url]
Expand Down Expand Up @@ -67,6 +68,7 @@ export default () => (

### Select props

<!-- prettier-ignore -->
| name | description | type | default |
| --- | --- | --- | --- |
| id | html id to set on the component wrapper | String | '' |
Expand Down Expand Up @@ -126,6 +128,7 @@ export default () => (
| loading | show loading icon in arrow | boolean | false |
| 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 | - |

### Methods

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

<code src="../examples/option-render.tsx"></code>
17 changes: 17 additions & 0 deletions docs/examples/option-render.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* eslint-disable no-console */
import Select from 'rc-select';
import '../../assets/index.less';

export default () => {
return (
<Select
options={[
{ label: 'test1', value: '1' },
{ label: 'test2', value: '2' },
]}
optionRender={(option, { index }) => {
return `${option.label} - ${index}`;
}}
/>
);
};
13 changes: 9 additions & 4 deletions src/OptionList.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import classNames from 'classnames';
import useMemo from 'rc-util/lib/hooks/useMemo';
import KeyCode from 'rc-util/lib/KeyCode';
import useMemo from 'rc-util/lib/hooks/useMemo';
import omit from 'rc-util/lib/omit';
import pickAttrs from 'rc-util/lib/pickAttrs';
import type { ListRef } from 'rc-virtual-list';
import List from 'rc-virtual-list';
import type { ScrollConfig } from 'rc-virtual-list/lib/List';
import * as React from 'react';
import { useEffect } from 'react';
import useBaseProps from './hooks/useBaseProps';
import type { FlattenOptionData } from './interface';
import type { BaseOptionType, RawValueType } from './Select';
import SelectContext from './SelectContext';
import TransBtn from './TransBtn';
import useBaseProps from './hooks/useBaseProps';
import type { FlattenOptionData } from './interface';
import { isPlatformMac } from './utils/platformUtil';

// export interface OptionListProps<OptionsType extends object[]> {
Expand Down Expand Up @@ -56,6 +56,7 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
direction,
listHeight,
listItemHeight,
optionRender,
} = React.useContext(SelectContext);

const itemPrefixCls = `${prefixCls}-item`;
Expand Down Expand Up @@ -366,7 +367,11 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
}}
style={style}
>
<div className={`${optionPrefixCls}-content`}>{content}</div>
<div className={`${optionPrefixCls}-content`}>
{typeof optionRender === 'function'
? optionRender(item, { index: itemIndex })
: content}
</div>
{React.isValidElement(menuItemSelectedIcon) || selected}
{iconVisible && (
<TransBtn
Expand Down
16 changes: 12 additions & 4 deletions src/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,16 @@ import type {
RenderNode,
} from './BaseSelect';
import BaseSelect, { isMultiple } from './BaseSelect';
import OptGroup from './OptGroup';
import Option from './Option';
import OptionList from './OptionList';
import SelectContext from './SelectContext';
import useCache from './hooks/useCache';
import useFilterOptions from './hooks/useFilterOptions';
import useId from './hooks/useId';
import useOptions from './hooks/useOptions';
import useRefFunc from './hooks/useRefFunc';
import OptGroup from './OptGroup';
import Option from './Option';
import OptionList from './OptionList';
import SelectContext from './SelectContext';
import type { FlattenOptionData } from './interface';
import { hasValue, isComboNoValue, toArray } from './utils/commonUtil';
import { fillFieldNames, flattenOptions, injectPropsWithOption } from './utils/valueUtil';
import warningProps, { warningNullOptions } from './utils/warningPropsUtil';
Expand Down Expand Up @@ -138,6 +139,10 @@ export interface SelectProps<ValueType = any, OptionType extends BaseOptionType
optionLabelProp?: string;
children?: React.ReactNode;
options?: OptionType[];
optionRender?: (
oriOption: FlattenOptionData<BaseOptionType>,
info: { index: number },
) => React.ReactNode;
defaultActiveFirstOption?: boolean;
virtual?: boolean;
direction?: 'ltr' | 'rtl';
Expand Down Expand Up @@ -184,6 +189,7 @@ const Select = React.forwardRef(
optionFilterProp,
optionLabelProp,
options,
optionRender,
children,
defaultActiveFirstOption,
menuItemSelectedIcon,
Expand Down Expand Up @@ -605,6 +611,7 @@ const Select = React.forwardRef(
listHeight,
listItemHeight,
childrenAsData,
optionRender,
};
}, [
parsedOptions,
Expand All @@ -620,6 +627,7 @@ const Select = React.forwardRef(
listHeight,
listItemHeight,
childrenAsData,
optionRender,
]);

// ========================== Warning ===========================
Expand Down
11 changes: 9 additions & 2 deletions src/SelectContext.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import * as React from 'react';
import type { RawValueType, RenderNode } from './BaseSelect';
import type {
BaseOptionType,
FieldNames,
OnActiveValue,
OnInternalSelect,
SelectProps,
} from './Select';
import type { FlattenOptionData } from './interface';
import type { BaseOptionType, FieldNames, OnActiveValue, OnInternalSelect } from './Select';

// Use any here since we do not get the type during compilation
export interface SelectContextProps {
options: BaseOptionType[];
optionRender?: SelectProps['optionRender'];
flattenOptions: FlattenOptionData<BaseOptionType>[];
onActiveValue: OnActiveValue;
defaultActiveFirstOption?: boolean;
Expand All @@ -14,7 +21,7 @@ export interface SelectContextProps {
rawValues: Set<RawValueType>;
fieldNames?: FieldNames;
virtual?: boolean;
direction?: "ltr" | "rtl";
direction?: 'ltr' | 'rtl';
listHeight?: number;
listItemHeight?: number;
childrenAsData?: boolean;
Expand Down
31 changes: 25 additions & 6 deletions tests/Select.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ describe('Select.Basic', () => {
expect(wrapper2.find('.rc-select-clear-icon').length).toBeFalsy();

const wrapper3 = mount(
<Select allowClear={{ clearIcon: <div className='custom-clear-icon'>x</div> }} value="1">
<Select allowClear={{ clearIcon: <div className="custom-clear-icon">x</div> }} value="1">
<Option value="1">1</Option>
<Option value="2">2</Option>
</Select>,
Expand All @@ -277,17 +277,16 @@ describe('Select.Basic', () => {
expect(wrapper3.find('.custom-clear-icon').text()).toBe('x');

const wrapper4 = mount(
<Select allowClear={{ clearIcon: <div className='custom-clear-icon'>x</div> }}>
<Select allowClear={{ clearIcon: <div className="custom-clear-icon">x</div> }}>
<Option value="1">1</Option>
<Option value="2">2</Option>
</Select>,
);
expect(wrapper4.find('.custom-clear-icon').length).toBeFalsy();


resetWarned();
const wrapper5 = mount(
<Select allowClear clearIcon={<div className='custom-clear-icon'>x</div>} value="1">
<Select allowClear clearIcon={<div className="custom-clear-icon">x</div>} value="1">
<Option value="1">1</Option>
<Option value="2">2</Option>
</Select>,
Expand All @@ -296,7 +295,7 @@ describe('Select.Basic', () => {
expect(wrapper5.find('.custom-clear-icon').text()).toBe('x');

const wrapper6 = mount(
<Select allowClear clearIcon={<div className='custom-clear-icon'>x</div>}>
<Select allowClear clearIcon={<div className="custom-clear-icon">x</div>}>
<Option value="1">1</Option>
<Option value="2">2</Option>
</Select>,
Expand Down Expand Up @@ -1361,7 +1360,7 @@ describe('Select.Basic', () => {
beforeAll(() => {
domHook = spyElementPrototypes(HTMLElement, {
getBoundingClientRect: () => ({
width: 1000
width: 1000,
}),
});
});
Expand Down Expand Up @@ -2095,4 +2094,24 @@ describe('Select.Basic', () => {
const { container } = testingRender(<Select open direction="rtl" options={options} />);
expect(container.querySelector('.rc-virtual-list-rtl')).toBeTruthy();
});

it('Should optionRender work', () => {
const options = [
{ label: 'test1', value: '1' },
{ label: 'test2', value: '2' },
];

const { container } = testingRender(
<Select
open
options={options}
optionRender={(option, {index}) => {
return `${option.label} - ${index}`;
}}
/>,
);
expect(container.querySelector('.rc-select-item-option-content').innerHTML).toEqual(
'test1 - 0',
);
});
});

1 comment on commit 44d9806

@vercel
Copy link

@vercel vercel bot commented on 44d9806 Oct 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

select – ./

select.vercel.app
select-react-component.vercel.app
select-git-master-react-component.vercel.app

Please sign in to comment.