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 all 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 | (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',
);
});
});
Loading