Skip to content

feat: support classnames and styles #1128

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

Merged
merged 7 commits into from
Mar 13, 2025
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
15 changes: 13 additions & 2 deletions src/BaseSelect/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { getSeparatedContent, isValidCount } from '../utils/valueUtil';
import SelectContext from '../SelectContext';
import type { SelectContextProps } from '../SelectContext';
import Polite from './Polite';
export type BaseSelectSemanticName = 'prefix' | 'suffix' | 'input';

export type {
DisplayInfoType,
Expand Down Expand Up @@ -131,6 +132,8 @@ export type BaseSelectPropsWithoutPrivate = Omit<BaseSelectProps, keyof BaseSele
export interface BaseSelectProps extends BaseSelectPrivateProps, React.AriaAttributes {
className?: string;
style?: React.CSSProperties;
classNames?: Partial<Record<BaseSelectSemanticName, string>>;
styles?: Partial<Record<BaseSelectSemanticName, React.CSSProperties>>;
title?: string;
showSearch?: boolean;
tagRender?: (props: CustomTagProps) => React.ReactElement;
Expand Down Expand Up @@ -405,7 +408,12 @@ const BaseSelect = React.forwardRef<BaseSelectRef, BaseSelectProps>((props, ref)
[tokenSeparators],
);

const { maxCount, rawValues } = React.useContext<SelectContextProps>(SelectContext) || {};
const {
maxCount,
rawValues,
classNames: selectClassNames,
styles,
} = React.useContext<SelectContextProps>(SelectContext) || {};

const onInternalSearch = (searchText: string, fromTyping: boolean, isCompositing: boolean) => {
if (multiple && isValidCount(maxCount) && rawValues?.size >= maxCount) {
Expand Down Expand Up @@ -720,9 +728,10 @@ const BaseSelect = React.forwardRef<BaseSelectRef, BaseSelectProps>((props, ref)
if (showSuffixIcon) {
arrowNode = (
<TransBtn
className={classNames(`${prefixCls}-arrow`, {
className={classNames(`${prefixCls}-arrow`, selectClassNames?.suffix, {
[`${prefixCls}-arrow-loading`]: loading,
})}
style={styles?.suffix}
customizeIcon={suffixIcon}
customizeIconProps={{
loading,
Expand Down Expand Up @@ -812,6 +821,8 @@ const BaseSelect = React.forwardRef<BaseSelectRef, BaseSelectProps>((props, ref)
) : (
<Selector
{...props}
prefixClassName={selectClassNames?.prefix}
prefixStyle={styles?.prefix}
domRef={selectorDomRef}
prefixCls={prefixCls}
inputElement={customizeInputElement}
Expand Down
24 changes: 17 additions & 7 deletions src/OptionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
listHeight,
listItemHeight,
optionRender,
classNames: contextClassNames,
styles: contextStyles,
} = React.useContext(SelectContext);

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

const optionPrefixCls = `${itemPrefixCls}-option`;
const optionClassName = classNames(itemPrefixCls, optionPrefixCls, className, {
[`${optionPrefixCls}-grouped`]: groupOption,
[`${optionPrefixCls}-active`]: activeIndex === itemIndex && !mergedDisabled,
[`${optionPrefixCls}-disabled`]: mergedDisabled,
[`${optionPrefixCls}-selected`]: selected,
});
const optionClassName = classNames(
itemPrefixCls,
optionPrefixCls,
className,
contextClassNames?.listItem,
{
[`${optionPrefixCls}-grouped`]: groupOption,
[`${optionPrefixCls}-active`]: activeIndex === itemIndex && !mergedDisabled,
[`${optionPrefixCls}-disabled`]: mergedDisabled,
[`${optionPrefixCls}-selected`]: selected,
},
);

const mergedLabel = getLabel(item);

Expand Down Expand Up @@ -393,7 +403,7 @@ const OptionList: React.ForwardRefRenderFunction<RefOptionListProps, {}> = (_, r
onSelectValue(value);
}
}}
style={style}
style={{ ...contextStyles?.listItem, ...style }}
>
<div className={`${optionPrefixCls}-content`}>
{typeof optionRender === 'function'
Expand Down
11 changes: 10 additions & 1 deletion src/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import type {
BaseSelectProps,
BaseSelectPropsWithoutPrivate,
BaseSelectRef,
BaseSelectSemanticName,
DisplayInfoType,
DisplayValueType,
RenderNode,
Expand Down Expand Up @@ -107,6 +108,7 @@ export type SelectHandler<ValueType, OptionType extends BaseOptionType = Default

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

export type SemanticName = BaseSelectSemanticName | 'listItem' | 'list';
export interface SelectProps<ValueType = any, OptionType extends BaseOptionType = DefaultOptionType>
extends BaseSelectPropsWithoutPrivate {
prefixCls?: string;
Expand Down Expand Up @@ -157,6 +159,8 @@ export interface SelectProps<ValueType = any, OptionType extends BaseOptionType
defaultValue?: ValueType | null;
maxCount?: number;
onChange?: (value: ValueType, option?: OptionType | OptionType[]) => void;
classNames?: Partial<Record<SemanticName, string>>;
styles?: Partial<Record<SemanticName, React.CSSProperties>>;
}

function isRawValue(value: DraftValueType): value is RawValueType {
Expand Down Expand Up @@ -204,7 +208,8 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
labelInValue,
onChange,
maxCount,

classNames: selectClassNames,
styles,
...restProps
} = props;

Expand Down Expand Up @@ -626,8 +631,12 @@ const Select = React.forwardRef<BaseSelectRef, SelectProps<any, DefaultOptionTyp
childrenAsData,
maxCount,
optionRender,
classNames: selectClassNames,
styles,
};
}, [
selectClassNames,
styles,
maxCount,
parsedOptions,
displayOptions,
Expand Down
3 changes: 3 additions & 0 deletions src/SelectContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import type {
OnActiveValue,
OnInternalSelect,
SelectProps,
SemanticName,
} from './Select';
import type { FlattenOptionData } from './interface';

// Use any here since we do not get the type during compilation
export interface SelectContextProps {
classNames?: Partial<Record<SemanticName, string>>;
styles?: Partial<Record<SemanticName, React.CSSProperties>>;
options: BaseOptionType[];
optionRender?: SelectProps['optionRender'];
flattenOptions: FlattenOptionData<BaseOptionType>[];
Expand Down
13 changes: 9 additions & 4 deletions src/Selector/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react';
import classNames from 'classnames';
import { composeRef } from '@rc-component/util/lib/ref';
import { warning } from '@rc-component/util/lib/warning';

import SelectContext from '../SelectContext';
type InputRef = HTMLInputElement | HTMLTextAreaElement;

interface InputProps {
Expand Down Expand Up @@ -57,6 +57,8 @@ const Input: React.ForwardRefRenderFunction<InputRef, InputProps> = (props, ref)
open,
attrs,
} = props;
const { classNames: contextClassNames, styles: contextStyles } =
React.useContext(SelectContext) || {};

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

Expand All @@ -80,7 +82,6 @@ const Input: React.ForwardRefRenderFunction<InputRef, InputProps> = (props, ref)
inputNode = React.cloneElement(inputNode, {
type: 'search',
...originProps,

// Override over origin props
id,
ref: composeRef(ref, originRef as any),
Expand All @@ -89,7 +90,11 @@ const Input: React.ForwardRefRenderFunction<InputRef, InputProps> = (props, ref)
autoComplete: autoComplete || 'off',

autoFocus,
className: classNames(`${prefixCls}-selection-search-input`, inputNode?.props?.className),
className: classNames(
`${prefixCls}-selection-search-input`,
inputNode?.props?.className,
contextClassNames?.input,
),

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

style: { ...style, opacity: editable ? null : 0 },
style: { ...style, opacity: editable ? null : 0, ...contextStyles?.input },

onKeyDown: (event: React.KeyboardEvent<HTMLElement>) => {
onKeyDown(event);
Expand Down
11 changes: 10 additions & 1 deletion src/Selector/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import useLock from '../hooks/useLock';
import { isValidateOpenKey } from '../utils/keyUtil';
import MultipleSelector from './MultipleSelector';
import SingleSelector from './SingleSelector';
import classNames from 'classnames';

export interface InnerSelectorProps {
prefixCls: string;
Expand Down Expand Up @@ -54,6 +55,8 @@ export interface RefSelectorProps {
}

export interface SelectorProps {
prefixClassName: string;
prefixStyle: React.CSSProperties;
id: string;
prefixCls: string;
showSearch?: boolean;
Expand Down Expand Up @@ -107,6 +110,8 @@ const Selector: React.ForwardRefRenderFunction<RefSelectorProps, SelectorProps>
const compositionStatusRef = useRef<boolean>(false);

const {
prefixClassName,
prefixStyle,
prefixCls,
open,
mode,
Expand Down Expand Up @@ -290,7 +295,11 @@ const Selector: React.ForwardRefRenderFunction<RefSelectorProps, SelectorProps>
onClick={onClick}
onMouseDown={onMouseDown}
>
{prefix && <div className={`${prefixCls}-prefix`}>{prefix}</div>}
{prefix && (
<div className={classNames(`${prefixCls}-prefix`, prefixClassName)} style={prefixStyle}>
{prefix}
</div>
)}
{selectNode}
</div>
);
Expand Down
6 changes: 4 additions & 2 deletions src/TransBtn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { RenderNode } from './BaseSelect';

export interface TransBtnProps {
className: string;
style?: React.CSSProperties;
customizeIcon: RenderNode;
customizeIconProps?: any;
onMouseDown?: React.MouseEventHandler<HTMLSpanElement>;
Expand All @@ -12,7 +13,8 @@ export interface TransBtnProps {
}

const TransBtn: React.FC<TransBtnProps> = (props) => {
const { className, customizeIcon, customizeIconProps, children, onMouseDown, onClick } = props;
const { className, style, customizeIcon, customizeIconProps, children, onMouseDown, onClick } =
props;

const icon =
typeof customizeIcon === 'function' ? customizeIcon(customizeIconProps) : customizeIcon;
Expand All @@ -24,7 +26,7 @@ const TransBtn: React.FC<TransBtnProps> = (props) => {
event.preventDefault();
onMouseDown?.(event);
}}
style={{ userSelect: 'none', WebkitUserSelect: 'none' }}
style={{ userSelect: 'none', WebkitUserSelect: 'none', ...style }}
unselectable="on"
onClick={onClick}
aria-hidden
Expand Down
47 changes: 47 additions & 0 deletions tests/Select.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2417,4 +2417,51 @@ describe('Select.Basic', () => {
expect(onBlur).toHaveBeenCalledTimes(2);
expect(inputElem.value).toEqual('bb');
});
it('support classnames and styles', () => {
const customClassNames = {
prefix: 'cutsom-prefix',
suffix: 'custom-suffix',
list: 'custom-list',
listItem: 'custom-item',
input: 'custom-input',
};
const customStyle = {
prefix: { color: 'red' },
suffix: { color: 'green' },
list: { color: 'yellow' },
listItem: { color: 'blue' },
input: { color: 'black' },
};
const { container } = render(
<Select
open
classNames={customClassNames}
styles={customStyle}
suffixIcon={<div>arrow</div>}
prefix="Foobar"
value={['bamboo']}
mode="multiple"
options={[
{ value: 'jack', label: 'Jack' },
{ value: 'lucy', label: 'Lucy' },
]}
/>,
);

const prefix = container.querySelector('.rc-select-prefix');
const suffix = container.querySelector('.rc-select-arrow');
const item = container.querySelector('.rc-select-item-option');
const list = container.querySelector('.rc-virtual-list');
const input = container.querySelector('.rc-select-selection-search-input');
expect(prefix).toHaveClass(customClassNames.prefix);
expect(prefix).toHaveStyle(customStyle.prefix);
expect(suffix).toHaveClass(customClassNames.suffix);
expect(suffix).toHaveStyle(customStyle.suffix);
expect(item).toHaveClass(customClassNames.listItem);
expect(item).toHaveStyle(customStyle.listItem);
expect(list).toHaveClass(customClassNames.list);
expect(list).toHaveStyle(customStyle.list);
expect(input).toHaveClass(customClassNames.input);
expect(input).toHaveStyle(customStyle.input);
});
});
Loading