Skip to content

Commit f058629

Browse files
committed
fix: styles
1 parent 8f5e206 commit f058629

13 files changed

Lines changed: 763 additions & 136 deletions

File tree

packages/react/src/auto-complete/style/index.scss

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,51 +7,51 @@
77

88
&_disabled {
99
cursor: not-allowed;
10-
opacity: 0.6;
10+
opacity: var(--ty-auto-complete-opacity-disabled);
1111
}
1212

1313
// Dropdown
1414
&__dropdown {
1515
width: 100%;
1616
margin: 0;
17-
padding: 4px 0;
17+
padding: var(--ty-auto-complete-dropdown-padding);
1818
list-style-type: none;
19-
background-color: var(--ty-select-dropdown-bg);
19+
background-color: var(--ty-auto-complete-dropdown-bg);
2020
box-sizing: border-box;
2121
overflow: hidden auto;
2222
z-index: 10;
23-
box-shadow: var(--ty-shadow-popup);
24-
border-radius: var(--ty-border-radius);
25-
font-size: var(--ty-font-size-base);
26-
max-height: $select-dropdown-max-height;
23+
box-shadow: var(--ty-auto-complete-dropdown-shadow);
24+
border-radius: var(--ty-auto-complete-dropdown-radius);
25+
font-size: var(--ty-auto-complete-dropdown-font-size);
26+
max-height: var(--ty-auto-complete-dropdown-max-height);
2727
}
2828

2929
// Empty state
3030
&__empty {
31-
padding: 16px;
31+
padding: var(--ty-auto-complete-empty-padding);
3232
text-align: center;
33-
color: var(--ty-color-text-quaternary);
34-
font-size: var(--ty-font-size-sm);
33+
color: var(--ty-auto-complete-empty-color);
34+
font-size: var(--ty-auto-complete-empty-font-size);
3535
}
3636
}
3737

3838
// Option
3939
.#{$prefix}-auto-complete-option {
4040
display: flex;
4141
align-items: center;
42-
padding: 7px 12px;
43-
font-size: 14px;
44-
line-height: 22px;
42+
padding: var(--ty-auto-complete-option-padding);
43+
font-size: var(--ty-auto-complete-option-font-size);
44+
line-height: var(--ty-auto-complete-option-line-height);
4545
cursor: pointer;
46-
color: var(--ty-color-text);
46+
color: var(--ty-auto-complete-option-color);
4747
transition: background-color 0.2s;
4848

4949
&_active {
50-
background-color: var(--ty-select-option-active-bg);
50+
background-color: var(--ty-auto-complete-option-active-bg);
5151
}
5252

5353
&_disabled {
5454
cursor: not-allowed;
55-
opacity: 0.5;
55+
opacity: var(--ty-auto-complete-option-disabled-opacity);
5656
}
5757
}

packages/react/src/cascader/__tests__/cascader.test.tsx

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,28 @@ describe('<Cascader />', () => {
7676
expect(container.firstChild).toHaveClass('ty-cascader_disabled');
7777
});
7878

79+
it('should clear the selected value when allowClear is clicked', () => {
80+
const onChange = jest.fn();
81+
const { container, queryByText } = render(
82+
<Cascader
83+
options={options}
84+
defaultValue={['zhejiang', 'hangzhou', 'xihu']}
85+
allowClear
86+
onChange={onChange}
87+
/>
88+
);
89+
90+
const clearButton = container.querySelector('.ty-cascader__clear') as HTMLButtonElement;
91+
92+
expect(clearButton).toHaveAttribute('aria-label', 'Clear selection');
93+
expect(clearButton.querySelector('svg')).toBeTruthy();
94+
95+
fireEvent.click(clearButton);
96+
97+
expect(onChange).toHaveBeenCalledWith([], []);
98+
expect(queryByText('Zhejiang / Hangzhou / West Lake')).not.toBeInTheDocument();
99+
});
100+
79101
it('should close on outside click', async () => {
80102
const { container } = render(
81103
<div>
@@ -102,6 +124,85 @@ describe('<Cascader />', () => {
102124
expect(getByText('Zhejiang / Hangzhou / West Lake')).toBeInTheDocument();
103125
});
104126

127+
it('should update value on parent selection when changeOnSelect is enabled', () => {
128+
const onChange = jest.fn();
129+
const { container, getByText } = render(
130+
<Cascader options={options} changeOnSelect onChange={onChange} />
131+
);
132+
133+
const selector = container.querySelector('.ty-cascader__selector');
134+
fireEvent.click(selector!);
135+
fireEvent.click(getByText('Zhejiang'));
136+
137+
expect(onChange).toHaveBeenCalledWith(
138+
['zhejiang'],
139+
[expect.objectContaining({ value: 'zhejiang', label: 'Zhejiang' })]
140+
);
141+
expect(container.querySelector('.ty-cascader__display')).toHaveTextContent('Zhejiang');
142+
expect(document.body.querySelector('.ty-cascader__dropdown')).toBeTruthy();
143+
expect(getByText('Hangzhou')).toBeInTheDocument();
144+
});
145+
146+
it('should keep the full path after selecting a child in controlled changeOnSelect mode', () => {
147+
const ChangeOnSelectControlledDemo = () => {
148+
const [value, setValue] = React.useState<(string | number)[]>([]);
149+
150+
return (
151+
<Cascader
152+
options={options}
153+
value={value}
154+
onChange={(nextValue) => setValue(nextValue)}
155+
changeOnSelect
156+
placeholder="Select any level"
157+
/>
158+
);
159+
};
160+
161+
const { container, getByText } = render(<ChangeOnSelectControlledDemo />);
162+
163+
const selector = container.querySelector('.ty-cascader__selector');
164+
fireEvent.click(selector!);
165+
fireEvent.click(getByText('Zhejiang'));
166+
fireEvent.click(getByText('Hangzhou'));
167+
fireEvent.click(getByText('West Lake'));
168+
169+
expect(container.querySelector('.ty-cascader__display')).toHaveTextContent(
170+
'Zhejiang / Hangzhou / West Lake'
171+
);
172+
});
173+
174+
it('should preserve parent path when selecting a child after reopening in controlled changeOnSelect mode', () => {
175+
const ChangeOnSelectControlledDemo = () => {
176+
const [value, setValue] = React.useState<(string | number)[]>([]);
177+
178+
return (
179+
<div>
180+
<Cascader
181+
options={options}
182+
value={value}
183+
onChange={(nextValue) => setValue(nextValue)}
184+
changeOnSelect
185+
placeholder="Select any level"
186+
/>
187+
<button>Outside</button>
188+
</div>
189+
);
190+
};
191+
192+
const { container, getByText } = render(<ChangeOnSelectControlledDemo />);
193+
194+
const selector = container.querySelector('.ty-cascader__selector');
195+
fireEvent.click(selector!);
196+
fireEvent.click(getByText('Jiangsu'));
197+
fireEvent.click(getByText('Outside'));
198+
fireEvent.click(selector!);
199+
fireEvent.click(getByText('Nanjing'));
200+
201+
expect(container.querySelector('.ty-cascader__display')).toHaveTextContent(
202+
'Jiangsu / Nanjing'
203+
);
204+
});
205+
105206
it('should respect the configured popup container', () => {
106207
const popupContainer = document.createElement('div');
107208
document.body.appendChild(popupContainer);

packages/react/src/cascader/cascader.tsx

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef, useContext, useMemo, useCallback }
22
import classNames from 'classnames';
33
import { ConfigContext } from '../config-provider/config-context';
44
import { getPrefixCls } from '../_utils/general';
5-
import { ArrowDown } from '../_utils/components';
5+
import { ArrowDown, ClearIcon } from '../_utils/components';
66
import Popup from '../popup';
77
import { CascaderProps, CascaderOption, CascaderValue } from './types';
88

@@ -98,10 +98,21 @@ const Cascader = React.forwardRef<HTMLDivElement, CascaderProps>((props, ref) =>
9898
setDropdownOpen(next);
9999
};
100100

101+
const getPathAtLevel = useCallback((level: number): CascaderValue => {
102+
if (hoveredPath.length > level) {
103+
return hoveredPath;
104+
}
105+
if (selectedValue.length > level) {
106+
return selectedValue;
107+
}
108+
return hoveredPath.length > 0 ? hoveredPath : selectedValue;
109+
}, [hoveredPath, selectedValue]);
110+
101111
const handleOptionSelect = (option: CascaderOption, level: number) => {
102112
if (option.disabled) return;
103113

104-
const newPath = [...hoveredPath.slice(0, level), option.value];
114+
const basePath = getPathAtLevel(level);
115+
const newPath = [...basePath.slice(0, level), option.value];
105116
setHoveredPath(newPath);
106117

107118
// Update columns
@@ -127,7 +138,8 @@ const Cascader = React.forwardRef<HTMLDivElement, CascaderProps>((props, ref) =>
127138

128139
const handleOptionHover = (option: CascaderOption, level: number) => {
129140
if (expandTrigger !== 'hover' || option.disabled) return;
130-
const newPath = [...hoveredPath.slice(0, level), option.value];
141+
const basePath = getPathAtLevel(level);
142+
const newPath = [...basePath.slice(0, level), option.value];
131143
setHoveredPath(newPath);
132144

133145
const cols = activeColumns.slice(0, level + 1);
@@ -164,6 +176,7 @@ const Cascader = React.forwardRef<HTMLDivElement, CascaderProps>((props, ref) =>
164176
[`${prefixCls}_${cascaderSize}`]: cascaderSize,
165177
[`${prefixCls}_disabled`]: disabled,
166178
[`${prefixCls}_open`]: open,
179+
[`${prefixCls}_has-value`]: allowClear && selectedValue.length > 0 && !disabled,
167180
});
168181

169182
const dropdown = open
@@ -225,9 +238,14 @@ const Cascader = React.forwardRef<HTMLDivElement, CascaderProps>((props, ref) =>
225238
<span className={`${prefixCls}__placeholder`}>{placeholder}</span>
226239
)}
227240
{allowClear && selectedValue.length > 0 && (
228-
<span className={`${prefixCls}__clear`} onClick={handleClear}>
229-
230-
</span>
241+
<button
242+
type="button"
243+
className={`${prefixCls}__clear`}
244+
onClick={handleClear}
245+
aria-label="Clear selection"
246+
>
247+
<ClearIcon size="1em" />
248+
</button>
231249
)}
232250
<span className={`${prefixCls}__arrow`}><ArrowDown size={10} /></span>
233251
</div>

packages/react/src/cascader/demo/ChangeOnSelect.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export default function ChangeOnSelectDemo() {
3131
},
3232
];
3333

34-
const [value, setValue] = React.useState([]);
34+
const [value, setValue] = React.useState<(string | number)[]>([]);
3535

3636
return (
3737
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
@@ -42,13 +42,7 @@ export default function ChangeOnSelectDemo() {
4242
changeOnSelect
4343
placeholder="Select any level"
4444
/>
45-
<span>Selected: {value.length > 0 ? value.join(' / ') : 'none'}</span>
46-
<Cascader
47-
options={options}
48-
defaultValue={['zhejiang', 'hangzhou', 'xihu']}
49-
displayRender={(labels) => labels.join(' > ')}
50-
placeholder="Custom separator"
51-
/>
45+
<span>Selected value: {value.length > 0 ? value.join(' / ') : 'none'}</span>
5246
</div>
5347
);
54-
}
48+
}

packages/react/src/cascader/style/index.scss

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
.#{$prefix}-cascader {
44
position: relative;
55
display: inline-block;
6-
min-width: 200px;
6+
min-width: var(--ty-cascader-min-width);
77

88
&_disabled {
9-
opacity: 0.5;
9+
opacity: var(--ty-cascader-opacity-disabled);
1010
pointer-events: none;
1111
}
1212

@@ -64,32 +64,47 @@
6464

6565
&__clear {
6666
position: absolute;
67-
right: 24px;
68-
font-size: 12px;
67+
right: var(--ty-cascader-arrow-offset-inline-end);
68+
top: 50%;
69+
transform: translateY(-50%);
70+
width: var(--ty-cascader-clear-size, 14px);
71+
height: var(--ty-cascader-clear-size, 14px);
72+
font-size: var(--ty-cascader-clear-font-size);
6973
color: var(--ty-cascader-clear-color);
7074
cursor: pointer;
7175
display: none;
72-
73-
.#{$prefix}-cascader:hover & {
74-
display: inline;
75-
}
76+
border: 0;
77+
padding: 0;
78+
background: transparent;
79+
line-height: 1;
80+
align-items: center;
81+
justify-content: center;
82+
z-index: 1;
7683

7784
&:hover {
7885
color: var(--ty-cascader-clear-color-hover);
7986
}
8087
}
8188

89+
&_has-value:hover &__clear {
90+
display: inline-flex;
91+
}
92+
8293
&__arrow {
8394
position: absolute;
84-
right: 8px;
85-
font-size: 12px;
95+
right: var(--ty-cascader-arrow-offset-inline-end);
96+
font-size: var(--ty-cascader-arrow-font-size);
8697
color: var(--ty-cascader-arrow-color);
8798
transition: transform 0.2s;
8899
display: inline-flex;
89100
align-items: center;
90101
justify-content: center;
91102
}
92103

104+
&_has-value:hover &__arrow {
105+
opacity: 0;
106+
}
107+
93108
&_open &__arrow {
94109
transform: rotate(180deg);
95110
}
@@ -108,8 +123,8 @@
108123
&__menu {
109124
list-style: none;
110125
margin: 0;
111-
padding: 4px 0;
112-
min-width: 120px;
126+
padding: var(--ty-cascader-menu-padding);
127+
min-width: var(--ty-cascader-menu-min-width);
113128
max-height: var(--ty-cascader-dropdown-max-height);
114129
overflow-y: auto;
115130

@@ -119,7 +134,7 @@
119134
}
120135

121136
&__menu-empty {
122-
padding: 8px 12px;
137+
padding: var(--ty-cascader-menu-empty-padding);
123138
color: var(--ty-cascader-menu-empty-color);
124139
text-align: center;
125140
}
@@ -128,7 +143,7 @@
128143
display: flex;
129144
align-items: center;
130145
justify-content: space-between;
131-
padding: 5px 12px;
146+
padding: var(--ty-cascader-menu-item-padding);
132147
cursor: pointer;
133148
transition: background 0.15s;
134149

@@ -144,7 +159,7 @@
144159

145160
&_disabled {
146161
cursor: not-allowed;
147-
opacity: 0.5;
162+
opacity: var(--ty-cascader-menu-item-disabled-opacity);
148163
}
149164
}
150165

@@ -156,8 +171,8 @@
156171
}
157172

158173
&__menu-item-arrow {
159-
margin-left: 8px;
160-
font-size: 12px;
174+
margin-left: var(--ty-cascader-menu-item-arrow-gap);
175+
font-size: var(--ty-cascader-menu-item-arrow-font-size);
161176
color: var(--ty-cascader-menu-item-arrow-color);
162177
}
163178
}

0 commit comments

Comments
 (0)