diff --git a/examples/uncontrolled.tsx b/examples/uncontrolled.tsx index 2be9dcef8..1debaa240 100644 --- a/examples/uncontrolled.tsx +++ b/examples/uncontrolled.tsx @@ -9,6 +9,15 @@ export default () => (

Uncontrolled

+ + generateConfig={momentGenerateConfig} + locale={zhCN} + picker="week" + allowClear + onOpenChange={open => { + console.log('1 =>', open); + }} + /> generateConfig={momentGenerateConfig} locale={zhCN} @@ -16,7 +25,7 @@ export default () => ( allowClear open onOpenChange={open => { - console.log('=>', open); + console.log('2 =>', open); }} /> diff --git a/src/Picker.tsx b/src/Picker.tsx index a66117f44..f08068645 100644 --- a/src/Picker.tsx +++ b/src/Picker.tsx @@ -28,7 +28,7 @@ import { PickerMode } from './interface'; import { getDefaultFormat, getInputSize, - addGlobalClickEvent, + addGlobalMouseDownEvent, } from './utils/uiUtil'; export interface PickerSharedProps { @@ -326,10 +326,25 @@ function InnerPicker(props: PickerProps) { } }; - const onInputBlur: React.FocusEventHandler = e => { + /** + * We will prevent blur to handle open event when user click outside, + * since this will repeat trigger `onOpenChange` event. + */ + const preventBlurRef = React.useRef(false); + + const triggerClose = () => { triggerOpen(false); setInnerValue(selectedValue); triggerChange(selectedValue); + }; + + const onInputBlur: React.FocusEventHandler = e => { + if (preventBlurRef.current) { + preventBlurRef.current = false; + return; + } + + triggerClose(); setFocused(false); if (onBlur) { @@ -361,7 +376,7 @@ function InnerPicker(props: PickerProps) { // Global click handler React.useEffect(() => - addGlobalClickEvent(({ target }: MouseEvent) => { + addGlobalMouseDownEvent(({ target }: MouseEvent) => { if ( mergedOpen && panelDivRef.current && @@ -370,7 +385,13 @@ function InnerPicker(props: PickerProps) { !inputDivRef.current.contains(target as Node) && onOpenChange ) { - onOpenChange(false); + preventBlurRef.current = true; + triggerClose(); + + // Always set back in case `onBlur` prevented by user + window.setTimeout(() => { + preventBlurRef.current = false; + }, 0); } }), ); diff --git a/src/utils/uiUtil.ts b/src/utils/uiUtil.ts index f5ee4545a..1256044dd 100644 --- a/src/utils/uiUtil.ts +++ b/src/utils/uiUtil.ts @@ -157,7 +157,7 @@ type ClickEventHandler = (event: MouseEvent) => void; let globalClickFunc: ClickEventHandler | null = null; const clickCallbacks = new Set(); -export function addGlobalClickEvent(callback: ClickEventHandler) { +export function addGlobalMouseDownEvent(callback: ClickEventHandler) { if ( !globalClickFunc && typeof window !== 'undefined' && @@ -168,7 +168,7 @@ export function addGlobalClickEvent(callback: ClickEventHandler) { queueFunc(e); }); }; - window.addEventListener('click', globalClickFunc); + window.addEventListener('mousedown', globalClickFunc); } clickCallbacks.add(callback); @@ -176,7 +176,7 @@ export function addGlobalClickEvent(callback: ClickEventHandler) { return () => { clickCallbacks.delete(callback); if (clickCallbacks.size === 0) { - window.removeEventListener('click', globalClickFunc!); + window.removeEventListener('mousedown', globalClickFunc!); globalClickFunc = null; } }; diff --git a/tests/picker.spec.tsx b/tests/picker.spec.tsx index 6f93b1313..fe9164888 100644 --- a/tests/picker.spec.tsx +++ b/tests/picker.spec.tsx @@ -120,17 +120,21 @@ describe('Basic', () => { }); it('fixed open need repeat trigger onOpenChange', () => { + jest.useFakeTimers(); const onOpenChange = jest.fn(); - mount(); + const wrapper = mount(); for (let i = 0; i < 10; i += 1) { - const clickEvent = new Event('click'); + const clickEvent = new Event('mousedown'); Object.defineProperty(clickEvent, 'target', { get: () => document.body, }); window.dispatchEvent(clickEvent); + wrapper.find('input').simulate('blur'); expect(onOpenChange).toHaveBeenCalledTimes(i + 1); } + jest.runAllTimers(); + jest.useRealTimers(); }); it('disabled should not open', () => {