Skip to content

Commit 65895e2

Browse files
committed
fix: popup issues
1 parent f058629 commit 65895e2

77 files changed

Lines changed: 1380 additions & 243 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

REFACTOR_CHECKLIST.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,88 @@
11
# Refactor Checklist
22

3+
## Progress Tracker
4+
5+
Overall status:
6+
7+
- estimated progress: `88%`
8+
- estimated progress: `93%`
9+
- current focus: a11y and interaction sweep for complex popup, picker, and dialog components
10+
- current mode: breaking-major refactor, no legacy compatibility work
11+
12+
### Phases
13+
14+
| Phase | Scope | Status | Notes |
15+
| --- | --- | --- | --- |
16+
| 1 | tokens core | done | shared resolver, strict new contract, no alias/migration runtime |
17+
| 2 | react theme integration | done | `ConfigProvider` uses shared token resolver |
18+
| 3 | bottom-layer popup infrastructure | done | `Popup`, `Overlay`, `Portal` stabilized and test-covered |
19+
| 4 | first-layer popup consumers | done | `Dropdown`, `Popover`, `Tooltip`, `Modal static`, `Menu.SubMenu`, `PopConfirm`, `SpeedDial` |
20+
| 5 | selection and picker core | done at current checkpoint | `Select`, `AutoComplete`, `Cascader`, `DatePicker`, `TimePicker`, `ColorPicker` stabilized with focused keyboard / state fixes |
21+
| 6 | token coverage expansion | mostly done at current checkpoint | first high-value namespaces landed and are wired into component styles |
22+
| 7 | quality platform | in progress | focused regression tests and targeted a11y semantics improved, no full visual regression sweep yet |
23+
24+
### Component Groups
25+
26+
Completed or stable checkpoint:
27+
28+
- `Popup`
29+
- `Overlay`
30+
- `Portal`
31+
- `Dropdown`
32+
- `Popover`
33+
- `Tooltip`
34+
- `ConfigProvider`
35+
- `DatePicker`
36+
- `TimePicker`
37+
- `Select`
38+
- `AutoComplete`
39+
- `Cascader`
40+
- `ColorPicker`
41+
- `Menu.SubMenu`
42+
- `PopConfirm`
43+
- `SpeedDial`
44+
- `Calendar`
45+
- `Upload`
46+
- `Modal`
47+
- `Drawer`
48+
- `Table`
49+
- `Tree`
50+
- `Transfer`
51+
- `Tour`
52+
- `Form`
53+
- `NativeSelect`
54+
55+
Token ownership completed or materially improved:
56+
57+
- `picker`
58+
- `date-picker`
59+
- `time-picker`
60+
- `calendar`
61+
- `dropdown`
62+
- `overlay`
63+
- `pop-confirm`
64+
- `tour`
65+
- `select`
66+
- `auto-complete`
67+
- `cascader`
68+
- `color-picker`
69+
- `speed-dial`
70+
- `upload`
71+
72+
Queued next:
73+
74+
- full-library visual regression sweep
75+
- broader docs/demo smoke check
76+
- remaining component token gaps with lower business value
77+
78+
Known unfinished areas:
79+
80+
- `Modal / Drawer / Tour` focus, keyboard, and close semantics are substantially improved, but not fully systematized into shared hooks
81+
- `Form` is stabilized at a synchronous-validation checkpoint, but not redesigned
82+
- `Select / Cascader / AutoComplete / DatePicker / TimePicker / ColorPicker` have focused a11y improvements, but not a full WAI-ARIA parity pass
83+
- full-library visual regression has not started
84+
- docs/demo smoke coverage is still partial
85+
386
## Phase Checklist
487

588
Use this before starting any new phase.

THEME_TOKEN_REFACTOR_ROADMAP.md

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,100 @@ Completed:
2727
- browser-safe ESM runtime for docs and Theme Studio
2828
- `ConfigProvider` integrated with token resolution
2929
- `Popup / Overlay / Portal` bottom-layer cleanup and tests
30+
- temporary alias and migration logic removed again for the breaking-major path
31+
- first-layer popup consumers stabilized:
32+
- `Dropdown`
33+
- `Popover`
34+
- `Tooltip`
35+
- `Modal static`
36+
- `Menu.SubMenu`
37+
- `PopConfirm`
38+
- `SpeedDial`
39+
- picker and selection core partially stabilized:
40+
- `DatePicker`
41+
- `TimePicker`
42+
- `Select`
43+
- `AutoComplete`
44+
- `Cascader`
45+
- `ColorPicker`
46+
- `Upload`
47+
- `Modal`
48+
- `Drawer`
49+
- `Table`
50+
- `Tree`
51+
- `Transfer`
52+
- `Tour`
53+
- `Form`
54+
- token ownership expanded for:
55+
- `picker`
56+
- `date-picker`
57+
- `time-picker`
58+
- `calendar`
59+
- `dropdown`
60+
- `overlay`
61+
- `pop-confirm`
62+
- `tour`
63+
- `select`
64+
- `auto-complete`
65+
- `cascader`
66+
- `color-picker`
67+
- `speed-dial`
3068

3169
In progress:
3270

33-
- removing alias and migration logic that was temporarily added during the transition
34-
- tightening execution checklists for the next component phases
71+
- broader a11y and docs/demo sweep
72+
- checklist-driven progress tracking
73+
- token coverage expansion for the remaining component gaps
74+
75+
Recently stabilized:
76+
77+
- `Upload`
78+
- controlled `fileList` behavior corrected
79+
- callbacks now receive the latest file list snapshots
80+
- shared remove icon usage aligned
81+
- `Modal`
82+
- `onClose` and `onCancel` semantics separated
83+
- `keyboard` support added and tested
84+
- shared close icon usage aligned
85+
- `Drawer`
86+
- `keyboard` prop now works
87+
- shared close icon usage aligned
88+
- `Table`
89+
- controlled `selectedRowKeys` clearing fixed
90+
- controlled `pagination.current` sync fixed
91+
- `Tree`
92+
- node tree now refreshes when `data` changes
93+
- `Transfer`
94+
- controlled mode no longer clears panel selection on unrelated rerenders
95+
- `Tour`
96+
- keyboard-disabled regression covered
97+
- shared close icon usage aligned
98+
- focus is now captured on open and restored on close
99+
- title and description now participate in dialog semantics
100+
- `Form`
101+
- contract tightened to synchronous validation only
102+
- submit-level behavior now has direct regression coverage
103+
- `Select`
104+
- combobox/listbox aria relationships improved
105+
- `Cascader`
106+
- selector keyboard open/close behavior added
107+
- combobox semantics improved
108+
- `AutoComplete`
109+
- `onOpenChange` support aligned with controlled `open`
110+
- combobox/listbox aria relationships improved
111+
- `DatePicker`
112+
- combobox/dialog semantics added
113+
- keyboard open behavior added
114+
- `TimePicker`
115+
- combobox/dialog semantics added
116+
- keyboard open behavior added
117+
- `ColorPicker`
118+
- trigger now exposes button/dialog semantics
119+
- keyboard open behavior added
120+
- `Modal / Drawer`
121+
- body/header aria relationships added
122+
- `PopConfirm`
123+
- alertdialog title relationship added
35124

36125
## Target Architecture
37126

packages/react/jest.config.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ module.exports = {
1515
transform: {
1616
'^.+\\.tsx?$': ['ts-jest', {
1717
tsconfig: 'tsconfig.test.json',
18-
isolatedModules: true,
1918
}],
2019
},
2120
};

packages/react/src/auto-complete/__tests__/__snapshots__/autocomplete.test.tsx.snap

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ exports[`<AutoComplete /> should match the snapshot 1`] = `
99
class="ty-input ty-input_md"
1010
>
1111
<input
12+
aria-expanded="false"
13+
aria-haspopup="listbox"
1214
class="ty-input__input"
15+
role="combobox"
1316
style="--ty-input-padding-inline-start-extra: 0px; --ty-input-padding-inline-end-extra: 0px;"
1417
value=""
1518
/>

packages/react/src/auto-complete/__tests__/autocomplete.test.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,21 @@ describe('<AutoComplete />', () => {
6969
expect(onChange).toHaveBeenCalledWith('Banana');
7070
});
7171

72+
it('should expose combobox aria relationships when navigating options', () => {
73+
const { container } = render(<AutoComplete options={options} />);
74+
const input = container.querySelector('input') as HTMLInputElement;
75+
const wrapper = container.firstChild as HTMLElement;
76+
77+
fireEvent.keyDown(wrapper, { key: 'ArrowDown' });
78+
79+
const listboxId = input.getAttribute('aria-controls');
80+
expect(input).toHaveAttribute('role', 'combobox');
81+
expect(input).toHaveAttribute('aria-expanded', 'true');
82+
expect(listboxId).toBeTruthy();
83+
expect(input).toHaveAttribute('aria-activedescendant', `${listboxId}-option-0`);
84+
expect(document.getElementById(`${listboxId}-option-0`)).toHaveTextContent('Apple');
85+
});
86+
7287
it('should close on Escape', () => {
7388
const { container } = render(<AutoComplete options={options} defaultOpen />);
7489
const wrapper = container.firstChild as HTMLElement;
@@ -126,6 +141,39 @@ describe('<AutoComplete />', () => {
126141
expect(container.firstChild).toHaveClass('ty-auto-complete_open');
127142
});
128143

144+
it('should call onOpenChange when focus opens and outside click closes the popup', async () => {
145+
const onOpenChange = jest.fn();
146+
const { container } = render(
147+
<div>
148+
<AutoComplete options={options} onOpenChange={onOpenChange} />
149+
<button>Outside</button>
150+
</div>
151+
);
152+
153+
const input = container.querySelector('input') as HTMLInputElement;
154+
fireEvent.focus(input);
155+
expect(onOpenChange).toHaveBeenCalledWith(true);
156+
157+
fireEvent.click(screen.getByText('Outside'));
158+
159+
await waitFor(() => {
160+
expect(onOpenChange).toHaveBeenCalledWith(false);
161+
});
162+
});
163+
164+
it('should respect controlled open when Escape is pressed', () => {
165+
const onOpenChange = jest.fn();
166+
const { container } = render(
167+
<AutoComplete options={options} open={true} onOpenChange={onOpenChange} />
168+
);
169+
170+
const wrapper = container.firstChild as HTMLElement;
171+
fireEvent.keyDown(wrapper, { key: 'Escape' });
172+
173+
expect(container.firstChild).toHaveClass('ty-auto-complete_open');
174+
expect(onOpenChange).toHaveBeenCalledWith(false);
175+
});
176+
129177
it('should call onSearch callback', () => {
130178
const onSearch = jest.fn();
131179
const { container } = render(<AutoComplete options={options} onSearch={onSearch} />);

packages/react/src/auto-complete/auto-complete.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
1+
import React, { useCallback, useContext, useEffect, useId, useRef, useState } from 'react';
22
import classNames from 'classnames';
33
import { useCombobox } from '../_utils/useCombobox';
44
import { ConfigContext } from '../config-provider/config-context';
@@ -22,6 +22,7 @@ const AutoComplete = React.forwardRef<HTMLDivElement, AutoCompleteProps>(
2222
filterOption = true,
2323
onChange,
2424
onSelect,
25+
onOpenChange,
2526
onSearch,
2627
onFocus,
2728
onBlur,
@@ -40,6 +41,7 @@ const AutoComplete = React.forwardRef<HTMLDivElement, AutoCompleteProps>(
4041

4142
const wrapperRef = useRef<HTMLDivElement | null>(null);
4243
const inputRef = useRef<HTMLInputElement | null>(null);
44+
const listboxId = useId();
4345

4446
const handleOptionSelect = useCallback(
4547
(opt: AutoCompleteOption) => {
@@ -61,6 +63,7 @@ const AutoComplete = React.forwardRef<HTMLDivElement, AutoCompleteProps>(
6163
defaultOpen,
6264
disabled,
6365
defaultActiveFirstOption,
66+
onOpenChange,
6467
onSelect: handleOptionSelect,
6568
});
6669

@@ -123,6 +126,7 @@ const AutoComplete = React.forwardRef<HTMLDivElement, AutoCompleteProps>(
123126
return (
124127
<li
125128
key={opt.value}
129+
id={`${listboxId}-option-${index}`}
126130
role="option"
127131
className={optCls}
128132
aria-selected={index === combo.focusedIndex}
@@ -144,6 +148,7 @@ const AutoComplete = React.forwardRef<HTMLDivElement, AutoCompleteProps>(
144148
<ul
145149
className={`${prefixCls}__dropdown`}
146150
style={{ minWidth: selectorWidth || undefined }}
151+
id={listboxId}
147152
role="listbox">
148153
{dropdownContent}
149154
</ul>
@@ -176,6 +181,15 @@ const AutoComplete = React.forwardRef<HTMLDivElement, AutoCompleteProps>(
176181
placeholder={placeholder}
177182
disabled={disabled}
178183
value={inputValue}
184+
role="combobox"
185+
aria-expanded={showDropdown}
186+
aria-haspopup="listbox"
187+
aria-controls={showDropdown ? listboxId : undefined}
188+
aria-activedescendant={
189+
showDropdown && combo.focusedIndex >= 0
190+
? `${listboxId}-option-${combo.focusedIndex}`
191+
: undefined
192+
}
179193
clearable={allowClear}
180194
onChange={handleInputChange}
181195
onFocus={handleFocus}

packages/react/src/auto-complete/demo/Basic.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import React from 'react';
22
import { AutoComplete } from '@tiny-design/react';
3+
import type { AutoCompleteOption } from '@tiny-design/react';
34

45
export default function BasicDemo() {
5-
const [options, setOptions] = React.useState([]);
6+
const [options, setOptions] = React.useState<AutoCompleteOption[]>([]);
67

7-
const handleSearch = (value) => {
8+
const handleSearch = (value: string) => {
89
if (!value) {
910
setOptions([]);
1011
} else {
@@ -25,4 +26,4 @@ export default function BasicDemo() {
2526
/>
2627
</div>
2728
);
28-
}
29+
}

packages/react/src/auto-complete/demo/CustomFilter.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import React from 'react';
21
import { AutoComplete } from '@tiny-design/react';
2+
import type { AutoCompleteOption } from '@tiny-design/react';
33

44
export default function CustomFilterDemo() {
55
const options = [
@@ -9,7 +9,7 @@ export default function CustomFilterDemo() {
99
{ value: 'Svelte' },
1010
];
1111

12-
const filterOption = (inputValue, option) => {
12+
const filterOption = (inputValue: string, option: AutoCompleteOption) => {
1313
return option.value.toLowerCase().startsWith(inputValue.toLowerCase());
1414
};
1515

@@ -22,4 +22,4 @@ export default function CustomFilterDemo() {
2222
/>
2323
</div>
2424
);
25-
}
25+
}

packages/react/src/auto-complete/index.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ Three sizes: `sm`, `md` (default) and `lg`.
107107
| filterOption | Filter function or false to disable | boolean &#124; (inputValue, option) => boolean | true |
108108
| onChange | Callback when input value changes | (value: string) => void | - |
109109
| onSelect | Callback when an option is selected | (value: string, option) => void | - |
110+
| onOpenChange | Callback when dropdown open state changes | (open: boolean) => void | - |
110111
| onSearch | Callback when searching | (value: string) => void | - |
111112
| onFocus | Callback when input is focused | (e: FocusEvent) => void | - |
112-
| onBlur | Callback when input loses focus | (e: FocusEvent) => void | - |
113+
| onBlur | Callback when input loses focus | (e: FocusEvent) => void | - |

packages/react/src/auto-complete/index.zh_CN.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ import { AutoComplete } from 'tiny-design';
107107
| filterOption | 自定义过滤函数或 false 禁用过滤 | boolean &#124; (inputValue, option) => boolean | true |
108108
| onChange | 输入值变化时的回调 | (value: string) => void | - |
109109
| onSelect | 选中选项时的回调 | (value: string, option) => void | - |
110+
| onOpenChange | 下拉展开状态变化时的回调 | (open: boolean) => void | - |
110111
| onSearch | 搜索时的回调 | (value: string) => void | - |
111112
| onFocus | 获得焦点时的回调 | (e: FocusEvent) => void | - |
112-
| onBlur | 失去焦点时的回调 | (e: FocusEvent) => void | - |
113+
| onBlur | 失去焦点时的回调 | (e: FocusEvent) => void | - |

0 commit comments

Comments
 (0)