Skip to content

Commit b490e63

Browse files
authored
feat: support classNames and styles (#587)
1 parent beeeb55 commit b490e63

File tree

5 files changed

+117
-14
lines changed

5 files changed

+117
-14
lines changed

examples/search.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@ import React from 'react';
22
import '../assets/index.less';
33
import Cascader from '../src';
44

5+
const testClassNames = {
6+
prefix: 'test-prefix',
7+
suffix: 'test-suffix',
8+
input: 'test-input',
9+
popup: {
10+
list: 'test-popup-list',
11+
listItem: 'test-popup-list-item',
12+
},
13+
};
14+
const testStyles = {
15+
popup: {
16+
list: { background: 'red' },
17+
listItem: { color: 'yellow' },
18+
},
19+
};
520
const addressOptions = [
621
{
722
label: '福建',
@@ -68,6 +83,10 @@ const addressOptions = [
6883
const Demo = () => {
6984
return (
7085
<Cascader
86+
prefix="prefix"
87+
suffixIcon={() => 'icon'}
88+
classNames={testClassNames}
89+
styles={testStyles}
7190
options={addressOptions}
7291
showSearch
7392
style={{ width: 300 }}

src/Cascader.tsx

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@ interface BaseCascaderProps<
7171
OptionType extends DefaultOptionType = DefaultOptionType,
7272
ValueField extends keyof OptionType = keyof OptionType,
7373
> extends Omit<
74-
BaseSelectPropsWithoutPrivate,
75-
'tokenSeparators' | 'labelInValue' | 'mode' | 'showSearch'
76-
> {
74+
BaseSelectPropsWithoutPrivate,
75+
'tokenSeparators' | 'labelInValue' | 'mode' | 'showSearch'
76+
> {
7777
// MISC
7878
id?: string;
7979
prefixCls?: string;
@@ -129,8 +129,8 @@ export type ValueType<
129129
ValueField extends keyof OptionType = keyof OptionType,
130130
> = keyof OptionType extends ValueField
131131
? unknown extends OptionType['value']
132-
? OptionType[ValueField]
133-
: OptionType['value']
132+
? OptionType[ValueField]
133+
: OptionType['value']
134134
: OptionType[ValueField];
135135

136136
export type GetValueType<
@@ -146,11 +146,19 @@ export type GetOptionType<
146146
Multiple extends boolean | React.ReactNode = false,
147147
> = false extends Multiple ? OptionType[] : OptionType[][];
148148

149+
type SemanticName = 'input' | 'prefix' | 'suffix';
150+
type PopupSemantic = 'list' | 'listItem';
149151
export interface CascaderProps<
150152
OptionType extends DefaultOptionType = DefaultOptionType,
151153
ValueField extends keyof OptionType = keyof OptionType,
152154
Multiple extends boolean | React.ReactNode = false,
153155
> extends BaseCascaderProps<OptionType, ValueField> {
156+
styles?: Partial<Record<SemanticName, React.CSSProperties>> & {
157+
popup?: Partial<Record<PopupSemantic, React.CSSProperties>>;
158+
};
159+
classNames?: Partial<Record<SemanticName, string>> & {
160+
popup?: Partial<Record<PopupSemantic, string>>;
161+
};
154162
checkable?: Multiple;
155163
value?: GetValueType<OptionType, ValueField, Multiple>;
156164
defaultValue?: GetValueType<OptionType, ValueField, Multiple>;
@@ -216,6 +224,9 @@ const Cascader = React.forwardRef<CascaderRef, InternalCascaderProps>((props, re
216224
popupMenuColumnStyle,
217225
popupStyle: customPopupStyle,
218226

227+
classNames,
228+
styles,
229+
219230
placement,
220231

221232
onPopupVisibleChange,
@@ -372,7 +383,6 @@ const Cascader = React.forwardRef<CascaderRef, InternalCascaderProps>((props, re
372383
onPopupVisibleChange?.(nextVisible);
373384
};
374385

375-
376386
// ========================== Warning ===========================
377387
if (process.env.NODE_ENV !== 'production') {
378388
warningNullOptions(mergedOptions, mergedFieldNames);
@@ -381,6 +391,8 @@ const Cascader = React.forwardRef<CascaderRef, InternalCascaderProps>((props, re
381391
// ========================== Context ===========================
382392
const cascaderContext = React.useMemo(
383393
() => ({
394+
classNames,
395+
styles,
384396
options: mergedOptions,
385397
fieldNames: mergedFieldNames,
386398
values: checkedValues,
@@ -424,12 +436,12 @@ const Cascader = React.forwardRef<CascaderRef, InternalCascaderProps>((props, re
424436
const popupStyle: React.CSSProperties =
425437
// Search to match width
426438
(mergedSearchValue && searchConfig.matchInputWidth) ||
427-
// Empty keep the width
428-
emptyOptions
439+
// Empty keep the width
440+
emptyOptions
429441
? {}
430442
: {
431-
minWidth: 'auto',
432-
};
443+
minWidth: 'auto',
444+
};
433445

434446
return (
435447
<CascaderContext.Provider value={cascaderContext}>
@@ -441,6 +453,16 @@ const Cascader = React.forwardRef<CascaderRef, InternalCascaderProps>((props, re
441453
prefixCls={prefixCls}
442454
autoClearSearchValue={autoClearSearchValue}
443455
popupMatchSelectWidth={popupMatchSelectWidth}
456+
classNames={{
457+
prefix: classNames?.prefix,
458+
suffix: classNames?.suffix,
459+
input: classNames?.input,
460+
}}
461+
styles={{
462+
prefix: styles?.prefix,
463+
suffix: styles?.suffix,
464+
input: styles?.input,
465+
}}
444466
popupStyle={{
445467
...popupStyle,
446468
...customPopupStyle,

src/OptionList/Column.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import classNames from 'classnames';
1+
import cls from 'classnames';
22
import * as React from 'react';
33
import type { DefaultOptionType, SingleValueType } from '../Cascader';
44
import CascaderContext from '../context';
@@ -53,6 +53,8 @@ export default function Column<OptionType extends DefaultOptionType = DefaultOpt
5353
loadingIcon,
5454
popupMenuColumnStyle,
5555
optionRender,
56+
classNames,
57+
styles,
5658
} = React.useContext(CascaderContext);
5759

5860
const hoverOpen = expandTrigger === 'hover';
@@ -117,7 +119,12 @@ export default function Column<OptionType extends DefaultOptionType = DefaultOpt
117119

118120
// ============================ Render ============================
119121
return (
120-
<ul className={menuPrefixCls} ref={menuRef} role="menu">
122+
<ul
123+
className={cls(menuPrefixCls, classNames?.popup?.list)}
124+
style={styles?.popup?.list}
125+
ref={menuRef}
126+
role="menu"
127+
>
121128
{optionInfoList.map(
122129
({
123130
disabled,
@@ -163,14 +170,14 @@ export default function Column<OptionType extends DefaultOptionType = DefaultOpt
163170
return (
164171
<li
165172
key={fullPathKey}
166-
className={classNames(menuItemPrefixCls, {
173+
className={cls(menuItemPrefixCls, classNames?.popup?.listItem, {
167174
[`${menuItemPrefixCls}-expand`]: !isMergedLeaf,
168175
[`${menuItemPrefixCls}-active`]:
169176
activeValue === value || activeValue === fullPathKey,
170177
[`${menuItemPrefixCls}-disabled`]: isOptionDisabled(disabled),
171178
[`${menuItemPrefixCls}-loading`]: isLoading,
172179
})}
173-
style={popupMenuColumnStyle}
180+
style={{ ...popupMenuColumnStyle, ...styles?.popup?.listItem }}
174181
role="menuitemcheckbox"
175182
title={title}
176183
aria-checked={checked}

src/context.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export interface CascaderContextProps {
2222
loadingIcon?: React.ReactNode;
2323
popupMenuColumnStyle?: React.CSSProperties;
2424
optionRender?: CascaderProps['optionRender'];
25+
classNames?: CascaderProps['classNames'];
26+
styles?: CascaderProps['styles'];
2527
}
2628

2729
const CascaderContext = React.createContext<CascaderContextProps>({} as CascaderContextProps);

tests/semantic.spec.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { render } from '@testing-library/react';
2+
3+
import React from 'react';
4+
import Cascader from '../src';
5+
6+
describe('Cascader.Search', () => {
7+
it('Should support semantic', () => {
8+
const testClassNames = {
9+
prefix: 'test-prefix',
10+
suffix: 'test-suffix',
11+
input: 'test-input',
12+
popup: {
13+
list: 'test-popup-list',
14+
listItem: 'test-popup-list-item',
15+
},
16+
};
17+
const testStyles = {
18+
prefix: { color: 'green' },
19+
suffix: { color: 'blue' },
20+
input: { color: 'purple' },
21+
popup: {
22+
list: { background: 'red' },
23+
listItem: { color: 'yellow' },
24+
},
25+
};
26+
const { container } = render(
27+
<Cascader
28+
classNames={testClassNames}
29+
styles={testStyles}
30+
prefix="prefix"
31+
suffixIcon={() => 'icon'}
32+
open
33+
options={[{ label: 'bamboo', value: 'bamboo' }]}
34+
optionRender={option => `${option.label} - test`}
35+
/>,
36+
);
37+
const input = container.querySelector('.rc-cascader-selection-search-input');
38+
const prefix = container.querySelector('.rc-cascader-prefix');
39+
const suffix = container.querySelector('.rc-cascader-arrow');
40+
const list = container.querySelector('.rc-cascader-menu');
41+
const listItem = container.querySelector('.rc-cascader-menu-item');
42+
expect(input).toHaveStyle(testStyles.input);
43+
expect(prefix).toHaveStyle(testStyles.prefix);
44+
expect(suffix).toHaveStyle(testStyles.suffix);
45+
expect(list).toHaveStyle(testStyles.popup.list);
46+
expect(listItem).toHaveStyle(testStyles.popup.listItem);
47+
expect(input?.className).toContain(testClassNames.input);
48+
expect(prefix?.className).toContain(testClassNames.prefix);
49+
expect(suffix?.className).toContain(testClassNames.suffix);
50+
expect(list?.className).toContain(testClassNames.popup.list);
51+
expect(listItem?.className).toContain(testClassNames.popup.listItem);
52+
});
53+
});

0 commit comments

Comments
 (0)