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: Implement optionRender API #987

Merged
merged 5 commits into from
Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 -->
Copy link
Contributor

Choose a reason for hiding this comment

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

可以直接在 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 -->
Copy link
Contributor

@crazyair crazyair Oct 24, 2023

Choose a reason for hiding this comment

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

这个格式化,可以在 prettierrc 里加

 overrides: [
        {
            files: '*.md',
            options: {
                proseWrap: 'never'
            }
        }
    ]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

改是直接加方便点,这样就是全局配了,应该也可以

| 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 | (params: OptionRenderParams) => 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 <div>{`${option.label} - ${index}`}</div>;
}}
/>
);
};
14 changes: 13 additions & 1 deletion src/OptionList.tsx
Original file line number Diff line number Diff line change
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,18 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
}}
style={style}
>
<div className={`${optionPrefixCls}-content`}>{content}</div>
<div className={`${optionPrefixCls}-content`}>
{typeof optionRender === 'function'
? optionRender({
Copy link
Member

Choose a reason for hiding this comment

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

和 SubItem 的 API 对齐吧:

type OptionRender = (oriOption, info: { index }) => ReactNode;

早期,给的东西不用太多。index 就够了~

ref https://github.com/ant-design/ant-design/wiki/API-Naming-rules#props

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok 我改下

option: { ...item.data, label: item.label },
index: itemIndex,
options: memoFlattenOptions.map((flattenItem) => ({
...flattenItem.data,
label: flattenItem.label,
})),
})
: content}
</div>
{React.isValidElement(menuItemSelectedIcon) || selected}
{iconVisible && (
<TransBtn
Expand Down
10 changes: 10 additions & 0 deletions src/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ export type SelectHandler<ValueType, OptionType extends BaseOptionType = Default
option: OptionType,
) => void;

export type OptionRenderParams = {
option: DefaultOptionType;
index: number;
options: DefaultOptionType[];
};

type ArrayElementType<T> = T extends (infer E)[] ? E : T;

export interface SelectProps<ValueType = any, OptionType extends BaseOptionType = DefaultOptionType>
Expand Down Expand Up @@ -138,6 +144,7 @@ export interface SelectProps<ValueType = any, OptionType extends BaseOptionType
optionLabelProp?: string;
children?: React.ReactNode;
options?: OptionType[];
optionRender?: (params: OptionRenderParams) => React.ReactNode;
defaultActiveFirstOption?: boolean;
virtual?: boolean;
direction?: 'ltr' | 'rtl';
Expand Down Expand Up @@ -184,6 +191,7 @@ const Select = React.forwardRef(
optionFilterProp,
optionLabelProp,
options,
optionRender,
children,
defaultActiveFirstOption,
menuItemSelectedIcon,
Expand Down Expand Up @@ -605,6 +613,7 @@ const Select = React.forwardRef(
listHeight,
listItemHeight,
childrenAsData,
optionRender,
};
}, [
parsedOptions,
Expand All @@ -620,6 +629,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 { FlattenOptionData } from './interface';
import type { BaseOptionType, FieldNames, OnActiveValue, OnInternalSelect } from './Select';
import type {
BaseOptionType,
FieldNames,
OnActiveValue,
OnInternalSelect,
SelectProps,
} 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',
);
});
});
Loading