Skip to content

Commit 45c7fe4

Browse files
authored
Merge branch 'master' into fix/prevent-extra-props-in-selected-item
2 parents 7476c9b + aab0e1a commit 45c7fe4

File tree

13 files changed

+152
-68
lines changed

13 files changed

+152
-68
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@rc-component/select",
3-
"version": "1.3.2",
3+
"version": "1.3.6",
44
"description": "React Select",
55
"engines": {
66
"node": ">=8.x"

src/BaseSelect/index.tsx

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -358,8 +358,11 @@ const BaseSelect = React.forwardRef<BaseSelectRef, BaseSelectProps>((props, ref)
358358
// Not trigger `open` when `notFoundContent` is empty
359359
const emptyListContent = !notFoundContent && emptyOptions;
360360

361-
const [mergedOpen, triggerOpen] = useOpen(open, onPopupVisibleChange, (nextOpen) =>
362-
disabled || emptyListContent ? false : nextOpen,
361+
const [mergedOpen, triggerOpen] = useOpen(
362+
defaultOpen || false,
363+
open,
364+
onPopupVisibleChange,
365+
(nextOpen) => (disabled || emptyListContent ? false : nextOpen),
363366
);
364367

365368
// ============================= Search =============================
@@ -547,8 +550,7 @@ const BaseSelect = React.forwardRef<BaseSelectRef, BaseSelectProps>((props, ref)
547550
useSelectTriggerControl(getSelectElements, mergedOpen, triggerOpen, !!mergedComponents.root);
548551

549552
// ========================== Focus / Blur ==========================
550-
/** Record real focus status */
551-
// const focusRef = React.useRef<boolean>(false);
553+
const internalMouseDownRef = React.useRef(false);
552554

553555
const onInternalFocus: React.FocusEventHandler<HTMLElement> = (event) => {
554556
setFocused(true);
@@ -564,11 +566,12 @@ const BaseSelect = React.forwardRef<BaseSelectRef, BaseSelectProps>((props, ref)
564566
};
565567

566568
const onRootBlur = () => {
567-
macroTask(() => {
568-
if (!isInside(getSelectElements(), document.activeElement as HTMLElement)) {
569-
triggerOpen(false);
570-
}
571-
});
569+
// Delay close should check the activeElement
570+
if (mergedOpen && !internalMouseDownRef.current) {
571+
triggerOpen(false, {
572+
cancelFun: () => isInside(getSelectElements(), document.activeElement as HTMLElement),
573+
});
574+
}
572575
};
573576

574577
const onInternalBlur: React.FocusEventHandler<HTMLElement> = (event) => {
@@ -593,19 +596,22 @@ const BaseSelect = React.forwardRef<BaseSelectRef, BaseSelectProps>((props, ref)
593596
}
594597
};
595598

596-
const onInternalMouseDown: React.MouseEventHandler<HTMLDivElement> = (event, ...restArgs) => {
599+
const onRootMouseDown: React.MouseEventHandler<HTMLDivElement> = (event, ...restArgs) => {
597600
const { target } = event;
598601
const popupElement: HTMLDivElement = triggerRef.current?.getPopupElement();
599602

600603
// We should give focus back to selector if clicked item is not focusable
601604
if (popupElement?.contains(target as HTMLElement) && triggerOpen) {
602605
// Tell `open` not to close since it's safe in the popup
603-
triggerOpen(true, {
604-
ignoreNext: true,
605-
});
606+
triggerOpen(true);
606607
}
607608

608609
onMouseDown?.(event, ...restArgs);
610+
611+
internalMouseDownRef.current = true;
612+
macroTask(() => {
613+
internalMouseDownRef.current = false;
614+
});
609615
};
610616

611617
// ============================ Dropdown ============================
@@ -747,7 +753,7 @@ const BaseSelect = React.forwardRef<BaseSelectRef, BaseSelectProps>((props, ref)
747753
// Token handling
748754
tokenWithEnter={tokenWithEnter}
749755
// Open
750-
onMouseDown={onInternalMouseDown}
756+
onMouseDown={onRootMouseDown}
751757
// Components
752758
components={mergedComponents}
753759
/>
@@ -774,7 +780,7 @@ const BaseSelect = React.forwardRef<BaseSelectRef, BaseSelectProps>((props, ref)
774780
empty={emptyOptions}
775781
onPopupVisibleChange={onTriggerVisibleChange}
776782
onPopupMouseEnter={onPopupMouseEnter}
777-
onPopupMouseDown={onInternalMouseDown}
783+
onPopupMouseDown={onRootMouseDown}
778784
onPopupBlur={onRootBlur}
779785
>
780786
{renderNode}

src/SelectInput/Input.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,9 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
179179
role: role || 'combobox',
180180
'aria-expanded': open || false,
181181
'aria-haspopup': 'listbox' as const,
182-
'aria-owns': `${id}_list`,
182+
'aria-owns': open ? `${id}_list` : undefined,
183183
'aria-autocomplete': 'list' as const,
184-
'aria-controls': `${id}_list`,
184+
'aria-controls': open ? `${id}_list` : undefined,
185185
'aria-activedescendant': open ? activeDescendantId : undefined,
186186
};
187187

src/SelectInput/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,13 +170,14 @@ export default React.forwardRef<SelectInputRef, SelectInputProps>(function Selec
170170
// ====================== Open ======================
171171
const onInternalMouseDown: SelectInputProps['onMouseDown'] = useEvent((event) => {
172172
if (!disabled) {
173+
const inputDOM = getDOM(inputRef.current);
174+
173175
// https://github.com/ant-design/ant-design/issues/56002
174176
// Tell `useSelectTriggerControl` to ignore this event
175177
// When icon is dynamic render, the parentNode will miss
176178
// so we need to mark the event directly
177-
(event.nativeEvent as any)._ignore_global_close = true;
179+
(event.nativeEvent as any)._ori_target = inputDOM;
178180

179-
const inputDOM = getDOM(inputRef.current);
180181
if (inputDOM && event.target !== inputDOM && !inputDOM.contains(event.target as Node)) {
181182
event.preventDefault();
182183
}

src/hooks/useOpen.ts

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,12 @@ export const macroTask = (fn: VoidFunction, times = 1) => {
2020

2121
/**
2222
* Trigger by latest open call, if nextOpen is undefined, means toggle.
23-
* ignoreNext will skip next call in the macro task queue.
23+
* `weak` means this call can be ignored if previous call exists.
2424
*/
2525
export type TriggerOpenType = (
2626
nextOpen?: boolean,
2727
config?: {
28-
ignoreNext?: boolean;
29-
lazy?: boolean;
28+
cancelFun?: () => boolean;
3029
},
3130
) => void;
3231

@@ -40,6 +39,7 @@ export type TriggerOpenType = (
4039
* On client-side hydration, it syncs with the actual open state.
4140
*/
4241
export default function useOpen(
42+
defaultOpen: boolean,
4343
propOpen: boolean,
4444
onOpen: (nextOpen: boolean) => void,
4545
postOpen: (nextOpen: boolean) => boolean,
@@ -51,14 +51,13 @@ export default function useOpen(
5151
setRendered(true);
5252
}, []);
5353

54-
const [stateOpen, internalSetOpen] = useControlledState(false, propOpen);
54+
const [stateOpen, internalSetOpen] = useControlledState(defaultOpen, propOpen);
5555

5656
// During SSR, always return false for open state
5757
const ssrSafeOpen = rendered ? stateOpen : false;
5858
const mergedOpen = postOpen(ssrSafeOpen);
5959

6060
const taskIdRef = useRef(0);
61-
const taskLockRef = useRef(false);
6261

6362
const triggerEvent = useEvent((nextOpen: boolean) => {
6463
if (onOpen && mergedOpen !== nextOpen) {
@@ -68,35 +67,32 @@ export default function useOpen(
6867
});
6968

7069
const toggleOpen = useEvent<TriggerOpenType>((nextOpen, config = {}) => {
71-
const { ignoreNext = false } = config;
70+
const { cancelFun } = config;
7271

7372
taskIdRef.current += 1;
7473
const id = taskIdRef.current;
7574

7675
const nextOpenVal = typeof nextOpen === 'boolean' ? nextOpen : !mergedOpen;
7776

78-
// Since `mergedOpen` is post-processed, we need to check if the value really changed
79-
if (nextOpenVal) {
80-
if (!taskLockRef.current) {
77+
function triggerUpdate() {
78+
if (
79+
// Always check if id is match
80+
id === taskIdRef.current &&
81+
// Check if need to cancel
82+
!cancelFun?.()
83+
) {
8184
triggerEvent(nextOpenVal);
82-
83-
// Lock if needed
84-
if (ignoreNext) {
85-
taskLockRef.current = ignoreNext;
86-
87-
macroTask(() => {
88-
taskLockRef.current = false;
89-
}, 3);
90-
}
9185
}
92-
return;
9386
}
9487

95-
macroTask(() => {
96-
if (id === taskIdRef.current && !taskLockRef.current) {
97-
triggerEvent(nextOpenVal);
98-
}
99-
});
88+
// Weak update can be ignored
89+
if (nextOpenVal) {
90+
triggerUpdate();
91+
} else {
92+
macroTask(() => {
93+
triggerUpdate();
94+
});
95+
}
10096
});
10197

10298
return [mergedOpen, toggleOpen] as const;

src/hooks/useSelectTriggerControl.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as React from 'react';
22
import { useEvent } from '@rc-component/util';
3+
import type { TriggerOpenType } from './useOpen';
34

45
export function isInside(elements: (HTMLElement | SVGElement | undefined)[], target: HTMLElement) {
56
return elements
@@ -10,7 +11,7 @@ export function isInside(elements: (HTMLElement | SVGElement | undefined)[], tar
1011
export default function useSelectTriggerControl(
1112
elements: () => (HTMLElement | SVGElement | undefined)[],
1213
open: boolean,
13-
triggerOpen: (open: boolean) => void,
14+
triggerOpen: TriggerOpenType,
1415
customizedTrigger: boolean,
1516
) {
1617
const onGlobalMouseDown = useEvent((event: MouseEvent) => {
@@ -25,10 +26,13 @@ export default function useSelectTriggerControl(
2526
target = (event.composedPath()[0] || target) as HTMLElement;
2627
}
2728

29+
if ((event as any)._ori_target) {
30+
target = (event as any)._ori_target;
31+
}
32+
2833
if (
2934
open &&
3035
// Marked by SelectInput mouseDown event
31-
!(event as any)._ignore_global_close &&
3236
!isInside(elements(), target)
3337
) {
3438
// Should trigger close

tests/Accessibility.test.tsx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,4 +214,62 @@ describe('Select.Accessibility', () => {
214214
});
215215
});
216216
});
217+
218+
describe('Select Input attributes', () => {
219+
it('should have correct aria and role attributes by default', () => {
220+
const { container } = render(
221+
<Select
222+
options={[
223+
{
224+
value: '123',
225+
},
226+
{
227+
value: '1234',
228+
},
229+
{
230+
value: '12345',
231+
},
232+
]}
233+
/>,
234+
);
235+
236+
const input = container.querySelector('input');
237+
expect(input).toHaveAttribute('role', 'combobox');
238+
expect(input).toHaveAttribute('aria-expanded', 'false');
239+
expect(input).toHaveAttribute('aria-haspopup', 'listbox');
240+
expect(input).not.toHaveAttribute('aria-owns');
241+
expect(input).toHaveAttribute('aria-autocomplete', 'list');
242+
expect(input).not.toHaveAttribute('aria-controls');
243+
expect(input).not.toHaveAttribute('aria-activedescendant');
244+
});
245+
246+
it('should have correct aria and role attributes when open', () => {
247+
const { container } = render(
248+
<Select
249+
id="select"
250+
open
251+
options={[
252+
{
253+
value: '123',
254+
},
255+
{
256+
value: '1234',
257+
},
258+
{
259+
value: '12345',
260+
},
261+
]}
262+
/>,
263+
);
264+
265+
const input = container.querySelector('input');
266+
expect(input).toHaveAttribute('role', 'combobox');
267+
expect(input).toHaveAttribute('aria-expanded', 'true');
268+
expect(input).toHaveAttribute('aria-haspopup', 'listbox');
269+
expect(input).toHaveAttribute('aria-owns', 'select_list');
270+
expect(input).toHaveAttribute('aria-autocomplete', 'list');
271+
expect(input).toHaveAttribute('aria-controls', 'select_list');
272+
expect(input).toHaveAttribute('aria-activedescendant', 'select_list_0');
273+
});
274+
});
217275
});

tests/Combobox.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,8 @@ describe('Select.Combobox', () => {
500500
for (let i = 0; i < 10; i += 1) {
501501
fireEvent.mouseDown(container.querySelector('input')!);
502502
expectOpen(container);
503+
504+
await delay(100);
503505
}
504506

505507
fireEvent.blur(container.querySelector('input')!);

tests/Select.test.tsx

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,16 @@ describe('Select.Basic', () => {
6666
expect(onPopupVisibleChange).toHaveBeenCalledWith(false);
6767
});
6868

69+
it('should defaultOpen work', () => {
70+
const { container } = render(
71+
<Select defaultOpen>
72+
<Option value="1">One</Option>
73+
<Option value="2">Two</Option>
74+
</Select>,
75+
);
76+
expectOpen(container);
77+
});
78+
6979
describe('render', () => {
7080
function genSelect(props?: Partial<SelectProps>) {
7181
return (
@@ -603,7 +613,7 @@ describe('Select.Basic', () => {
603613
</Select>,
604614
);
605615

606-
keyDown(container.querySelector('input'), 40);
616+
keyDown(container.querySelector('input'), KeyCode.DOWN);
607617
expectOpen(container);
608618
});
609619

@@ -2566,9 +2576,9 @@ describe('Select.Basic', () => {
25662576
await waitFakeTimer();
25672577
expectOpen(container, true);
25682578

2569-
keyDown(inputElem!, 40);
2570-
keyUp(inputElem!, 40);
2571-
keyDown(inputElem!, 13);
2579+
keyDown(inputElem!, KeyCode.DOWN);
2580+
keyUp(inputElem!, KeyCode.DOWN);
2581+
keyDown(inputElem!, KeyCode.ENTER);
25722582

25732583
await waitFakeTimer();
25742584
expect(onBlur).toHaveBeenCalledTimes(1);
@@ -2579,9 +2589,9 @@ describe('Select.Basic', () => {
25792589
await waitFakeTimer();
25802590
expectOpen(container, true);
25812591

2582-
keyDown(inputElem!, 40);
2583-
keyUp(inputElem!, 40);
2584-
keyDown(inputElem!, 13);
2592+
keyDown(inputElem!, KeyCode.DOWN);
2593+
keyUp(inputElem!, KeyCode.DOWN);
2594+
keyDown(inputElem!, KeyCode.ENTER);
25852595

25862596
await waitFakeTimer();
25872597
expect(onBlur).toHaveBeenCalledTimes(2);

tests/__snapshots__/Combobox.test.tsx.snap

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,8 @@ exports[`Select.Combobox renders controlled correctly 1`] = `
1515
</div>
1616
<input
1717
aria-autocomplete="list"
18-
aria-controls="test-id_list"
1918
aria-expanded="false"
2019
aria-haspopup="listbox"
21-
aria-owns="test-id_list"
2220
autocomplete="off"
2321
class="rc-select-input"
2422
id="test-id"
@@ -45,10 +43,8 @@ exports[`Select.Combobox renders correctly 1`] = `
4543
</div>
4644
<input
4745
aria-autocomplete="list"
48-
aria-controls="test-id_list"
4946
aria-expanded="false"
5047
aria-haspopup="listbox"
51-
aria-owns="test-id_list"
5248
autocomplete="off"
5349
class="rc-select-input"
5450
id="test-id"

0 commit comments

Comments
 (0)