Skip to content

Commit a3b46b9

Browse files
authored
feat: support classnames and styles (#1128)
* feat: support classnames and styles * save * rm * rm * item to listItem * fix * fix type
1 parent e8096c0 commit a3b46b9

File tree

8 files changed

+113
-17
lines changed

8 files changed

+113
-17
lines changed

src/BaseSelect/index.tsx

+13-2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { getSeparatedContent, isValidCount } from '../utils/valueUtil';
3030
import SelectContext from '../SelectContext';
3131
import type { SelectContextProps } from '../SelectContext';
3232
import Polite from './Polite';
33+
export type BaseSelectSemanticName = 'prefix' | 'suffix' | 'input';
3334

3435
export type {
3536
DisplayInfoType,
@@ -131,6 +132,8 @@ export type BaseSelectPropsWithoutPrivate = Omit<BaseSelectProps, keyof BaseSele
131132
export interface BaseSelectProps extends BaseSelectPrivateProps, React.AriaAttributes {
132133
className?: string;
133134
style?: React.CSSProperties;
135+
classNames?: Partial<Record<BaseSelectSemanticName, string>>;
136+
styles?: Partial<Record<BaseSelectSemanticName, React.CSSProperties>>;
134137
title?: string;
135138
showSearch?: boolean;
136139
tagRender?: (props: CustomTagProps) => React.ReactElement;
@@ -405,7 +408,12 @@ const BaseSelect = React.forwardRef<BaseSelectRef, BaseSelectProps>((props, ref)
405408
[tokenSeparators],
406409
);
407410

408-
const { maxCount, rawValues } = React.useContext<SelectContextProps>(SelectContext) || {};
411+
const {
412+
maxCount,
413+
rawValues,
414+
classNames: selectClassNames,
415+
styles,
416+
} = React.useContext<SelectContextProps>(SelectContext) || {};
409417

410418
const onInternalSearch = (searchText: string, fromTyping: boolean, isCompositing: boolean) => {
411419
if (multiple && isValidCount(maxCount) && rawValues?.size >= maxCount) {
@@ -720,9 +728,10 @@ const BaseSelect = React.forwardRef<BaseSelectRef, BaseSelectProps>((props, ref)
720728
if (showSuffixIcon) {
721729
arrowNode = (
722730
<TransBtn
723-
className={classNames(`${prefixCls}-arrow`, {
731+
className={classNames(`${prefixCls}-arrow`, selectClassNames?.suffix, {
724732
[`${prefixCls}-arrow-loading`]: loading,
725733
})}
734+
style={styles?.suffix}
726735
customizeIcon={suffixIcon}
727736
customizeIconProps={{
728737
loading,
@@ -812,6 +821,8 @@ const BaseSelect = React.forwardRef<BaseSelectRef, BaseSelectProps>((props, ref)
812821
) : (
813822
<Selector
814823
{...props}
824+
prefixClassName={selectClassNames?.prefix}
825+
prefixStyle={styles?.prefix}
815826
domRef={selectorDomRef}
816827
prefixCls={prefixCls}
817828
inputElement={customizeInputElement}

src/OptionList.tsx

+17-7
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
6060
listHeight,
6161
listItemHeight,
6262
optionRender,
63+
classNames: contextClassNames,
64+
styles: contextStyles,
6365
} = React.useContext(SelectContext);
6466

6567
const itemPrefixCls = `${prefixCls}-item`;
@@ -327,6 +329,8 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
327329
direction={direction}
328330
innerProps={virtual ? null : a11yProps}
329331
showScrollBar={showScrollBar}
332+
className={contextClassNames?.list}
333+
style={contextStyles?.list}
330334
>
331335
{(item, itemIndex) => {
332336
const { group, groupOption, data, label, value } = item;
@@ -355,12 +359,18 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
355359
const mergedDisabled = disabled || (!selected && overMaxCount);
356360

357361
const optionPrefixCls = `${itemPrefixCls}-option`;
358-
const optionClassName = classNames(itemPrefixCls, optionPrefixCls, className, {
359-
[`${optionPrefixCls}-grouped`]: groupOption,
360-
[`${optionPrefixCls}-active`]: activeIndex === itemIndex && !mergedDisabled,
361-
[`${optionPrefixCls}-disabled`]: mergedDisabled,
362-
[`${optionPrefixCls}-selected`]: selected,
363-
});
362+
const optionClassName = classNames(
363+
itemPrefixCls,
364+
optionPrefixCls,
365+
className,
366+
contextClassNames?.listItem,
367+
{
368+
[`${optionPrefixCls}-grouped`]: groupOption,
369+
[`${optionPrefixCls}-active`]: activeIndex === itemIndex && !mergedDisabled,
370+
[`${optionPrefixCls}-disabled`]: mergedDisabled,
371+
[`${optionPrefixCls}-selected`]: selected,
372+
},
373+
);
364374

365375
const mergedLabel = getLabel(item);
366376

@@ -393,7 +403,7 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
393403
onSelectValue(value);
394404
}
395405
}}
396-
style={style}
406+
style={{ ...contextStyles?.listItem, ...style }}
397407
>
398408
<div className={`${optionPrefixCls}-content`}>
399409
{typeof optionRender === 'function'

src/Select.tsx

+10-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import type {
3636
BaseSelectProps,
3737
BaseSelectPropsWithoutPrivate,
3838
BaseSelectRef,
39+
BaseSelectSemanticName,
3940
DisplayInfoType,
4041
DisplayValueType,
4142
RenderNode,
@@ -107,6 +108,7 @@ export type SelectHandler<ValueType, OptionType extends BaseOptionType = Default
107108

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

111+
export type SemanticName = BaseSelectSemanticName | 'listItem' | 'list';
110112
export interface SelectProps<ValueType = any, OptionType extends BaseOptionType = DefaultOptionType>
111113
extends BaseSelectPropsWithoutPrivate {
112114
prefixCls?: string;
@@ -157,6 +159,8 @@ export interface SelectProps<ValueType = any, OptionType extends BaseOptionType
157159
defaultValue?: ValueType | null;
158160
maxCount?: number;
159161
onChange?: (value: ValueType, option?: OptionType | OptionType[]) => void;
162+
classNames?: Partial<Record<SemanticName, string>>;
163+
styles?: Partial<Record<SemanticName, React.CSSProperties>>;
160164
}
161165

162166
function isRawValue(value: DraftValueType): value is RawValueType {
@@ -204,7 +208,8 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
204208
labelInValue,
205209
onChange,
206210
maxCount,
207-
211+
classNames: selectClassNames,
212+
styles,
208213
...restProps
209214
} = props;
210215

@@ -626,8 +631,12 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
626631
childrenAsData,
627632
maxCount,
628633
optionRender,
634+
classNames: selectClassNames,
635+
styles,
629636
};
630637
}, [
638+
selectClassNames,
639+
styles,
631640
maxCount,
632641
parsedOptions,
633642
displayOptions,

src/SelectContext.ts

+3
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ import type {
66
OnActiveValue,
77
OnInternalSelect,
88
SelectProps,
9+
SemanticName,
910
} from './Select';
1011
import type { FlattenOptionData } from './interface';
1112

1213
// Use any here since we do not get the type during compilation
1314
export interface SelectContextProps {
15+
classNames?: Partial<Record<SemanticName, string>>;
16+
styles?: Partial<Record<SemanticName, React.CSSProperties>>;
1417
options: BaseOptionType[];
1518
optionRender?: SelectProps['optionRender'];
1619
flattenOptions: FlattenOptionData<BaseOptionType>[];

src/Selector/Input.tsx

+9-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as React from 'react';
22
import classNames from 'classnames';
33
import { composeRef } from '@rc-component/util/lib/ref';
44
import { warning } from '@rc-component/util/lib/warning';
5-
5+
import SelectContext from '../SelectContext';
66
type InputRef = HTMLInputElement | HTMLTextAreaElement;
77

88
interface InputProps {
@@ -57,6 +57,8 @@ const Input: React.ForwardRefRenderFunction<InputRef, InputProps> = (props, ref)
5757
open,
5858
attrs,
5959
} = props;
60+
const { classNames: contextClassNames, styles: contextStyles } =
61+
React.useContext(SelectContext) || {};
6062

6163
let inputNode: React.ComponentElement<any, any> = inputElement || <input />;
6264

@@ -80,7 +82,6 @@ const Input: React.ForwardRefRenderFunction<InputRef, InputProps> = (props, ref)
8082
inputNode = React.cloneElement(inputNode, {
8183
type: 'search',
8284
...originProps,
83-
8485
// Override over origin props
8586
id,
8687
ref: composeRef(ref, originRef as any),
@@ -89,7 +90,11 @@ const Input: React.ForwardRefRenderFunction<InputRef, InputProps> = (props, ref)
8990
autoComplete: autoComplete || 'off',
9091

9192
autoFocus,
92-
className: classNames(`${prefixCls}-selection-search-input`, inputNode?.props?.className),
93+
className: classNames(
94+
`${prefixCls}-selection-search-input`,
95+
inputNode?.props?.className,
96+
contextClassNames?.input,
97+
),
9398

9499
role: 'combobox',
95100
'aria-expanded': open || false,
@@ -104,7 +109,7 @@ const Input: React.ForwardRefRenderFunction<InputRef, InputProps> = (props, ref)
104109
readOnly: !editable,
105110
unselectable: !editable ? 'on' : null,
106111

107-
style: { ...style, opacity: editable ? null : 0 },
112+
style: { ...style, opacity: editable ? null : 0, ...contextStyles?.input },
108113

109114
onKeyDown: (event: React.KeyboardEvent<HTMLElement>) => {
110115
onKeyDown(event);

src/Selector/index.tsx

+10-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import useLock from '../hooks/useLock';
1717
import { isValidateOpenKey } from '../utils/keyUtil';
1818
import MultipleSelector from './MultipleSelector';
1919
import SingleSelector from './SingleSelector';
20+
import classNames from 'classnames';
2021

2122
export interface InnerSelectorProps {
2223
prefixCls: string;
@@ -54,6 +55,8 @@ export interface RefSelectorProps {
5455
}
5556

5657
export interface SelectorProps {
58+
prefixClassName: string;
59+
prefixStyle: React.CSSProperties;
5760
id: string;
5861
prefixCls: string;
5962
showSearch?: boolean;
@@ -107,6 +110,8 @@ const Selector: React.ForwardRefRenderFunction<RefSelectorProps, SelectorProps>
107110
const compositionStatusRef = useRef<boolean>(false);
108111

109112
const {
113+
prefixClassName,
114+
prefixStyle,
110115
prefixCls,
111116
open,
112117
mode,
@@ -290,7 +295,11 @@ const Selector: React.ForwardRefRenderFunction<RefSelectorProps, SelectorProps>
290295
onClick={onClick}
291296
onMouseDown={onMouseDown}
292297
>
293-
{prefix && <div className={`${prefixCls}-prefix`}>{prefix}</div>}
298+
{prefix && (
299+
<div className={classNames(`${prefixCls}-prefix`, prefixClassName)} style={prefixStyle}>
300+
{prefix}
301+
</div>
302+
)}
294303
{selectNode}
295304
</div>
296305
);

src/TransBtn.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { RenderNode } from './BaseSelect';
44

55
export interface TransBtnProps {
66
className: string;
7+
style?: React.CSSProperties;
78
customizeIcon: RenderNode;
89
customizeIconProps?: any;
910
onMouseDown?: React.MouseEventHandler<HTMLSpanElement>;
@@ -12,7 +13,8 @@ export interface TransBtnProps {
1213
}
1314

1415
const TransBtn: React.FC<TransBtnProps> = (props) => {
15-
const { className, customizeIcon, customizeIconProps, children, onMouseDown, onClick } = props;
16+
const { className, style, customizeIcon, customizeIconProps, children, onMouseDown, onClick } =
17+
props;
1618

1719
const icon =
1820
typeof customizeIcon === 'function' ? customizeIcon(customizeIconProps) : customizeIcon;
@@ -24,7 +26,7 @@ const TransBtn: React.FC<TransBtnProps> = (props) => {
2426
event.preventDefault();
2527
onMouseDown?.(event);
2628
}}
27-
style={{ userSelect: 'none', WebkitUserSelect: 'none' }}
29+
style={{ userSelect: 'none', WebkitUserSelect: 'none', ...style }}
2830
unselectable="on"
2931
onClick={onClick}
3032
aria-hidden

tests/Select.test.tsx

+47
Original file line numberDiff line numberDiff line change
@@ -2417,4 +2417,51 @@ describe('Select.Basic', () => {
24172417
expect(onBlur).toHaveBeenCalledTimes(2);
24182418
expect(inputElem.value).toEqual('bb');
24192419
});
2420+
it('support classnames and styles', () => {
2421+
const customClassNames = {
2422+
prefix: 'cutsom-prefix',
2423+
suffix: 'custom-suffix',
2424+
list: 'custom-list',
2425+
listItem: 'custom-item',
2426+
input: 'custom-input',
2427+
};
2428+
const customStyle = {
2429+
prefix: { color: 'red' },
2430+
suffix: { color: 'green' },
2431+
list: { color: 'yellow' },
2432+
listItem: { color: 'blue' },
2433+
input: { color: 'black' },
2434+
};
2435+
const { container } = render(
2436+
<Select
2437+
open
2438+
classNames={customClassNames}
2439+
styles={customStyle}
2440+
suffixIcon={<div>arrow</div>}
2441+
prefix="Foobar"
2442+
value={['bamboo']}
2443+
mode="multiple"
2444+
options={[
2445+
{ value: 'jack', label: 'Jack' },
2446+
{ value: 'lucy', label: 'Lucy' },
2447+
]}
2448+
/>,
2449+
);
2450+
2451+
const prefix = container.querySelector('.rc-select-prefix');
2452+
const suffix = container.querySelector('.rc-select-arrow');
2453+
const item = container.querySelector('.rc-select-item-option');
2454+
const list = container.querySelector('.rc-virtual-list');
2455+
const input = container.querySelector('.rc-select-selection-search-input');
2456+
expect(prefix).toHaveClass(customClassNames.prefix);
2457+
expect(prefix).toHaveStyle(customStyle.prefix);
2458+
expect(suffix).toHaveClass(customClassNames.suffix);
2459+
expect(suffix).toHaveStyle(customStyle.suffix);
2460+
expect(item).toHaveClass(customClassNames.listItem);
2461+
expect(item).toHaveStyle(customStyle.listItem);
2462+
expect(list).toHaveClass(customClassNames.list);
2463+
expect(list).toHaveStyle(customStyle.list);
2464+
expect(input).toHaveClass(customClassNames.input);
2465+
expect(input).toHaveStyle(customStyle.input);
2466+
});
24202467
});

0 commit comments

Comments
 (0)