From d4196da3878f24e7946f40adeb17711ce49bc34f Mon Sep 17 00:00:00 2001 From: Daniil Gaponov Date: Wed, 15 Jul 2026 18:04:30 +0300 Subject: [PATCH] fix(RelativeDateField): read latest state on deferred blur MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useFocusWithin now dispatches onBlurWithin asynchronously (via setTimeout), so confirmValue could run after an external value update with a stale render closure and re-commit the old value — clobbering a reset to null. Read the current text/value/parsedDate from a ref so the deferred blur always sees fresh state. Also await the async blur in the "restores last correct value" test, which asserted synchronously before the deferred blur fired. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../__tests__/RelativeDateField.test.tsx | 4 +++- .../hooks/useRelativeDateFieldState.ts | 18 +++++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/components/RelativeDateField/__tests__/RelativeDateField.test.tsx b/src/components/RelativeDateField/__tests__/RelativeDateField.test.tsx index 7adabd5..a172e64 100644 --- a/src/components/RelativeDateField/__tests__/RelativeDateField.test.tsx +++ b/src/components/RelativeDateField/__tests__/RelativeDateField.test.tsx @@ -127,7 +127,9 @@ describe('RelativeDateField', () => { await userEvent.click(screen.container); - expect(input).toHaveValue('now - 1d'); + // Blur is dispatched asynchronously (useFocusWithin defers it), so wait + // for the committed value to be restored before asserting. + await expect.poll(() => input.value).toBe('now - 1d'); expect(hiddenInput).toHaveValue('now - 1d'); expect(onUpdate).not.toHaveBeenCalled(); }); diff --git a/src/components/RelativeDateField/hooks/useRelativeDateFieldState.ts b/src/components/RelativeDateField/hooks/useRelativeDateFieldState.ts index f215639..72e3d01 100644 --- a/src/components/RelativeDateField/hooks/useRelativeDateFieldState.ts +++ b/src/components/RelativeDateField/hooks/useRelativeDateFieldState.ts @@ -86,20 +86,28 @@ export function useRelativeDateFieldState(props: RelativeDateFieldOptions): Rela [props.disabled, props.readOnly, setValue], ); + // Keep the latest state in a ref so `confirmValue` (invoked from a blur that + // may fire after an external value update) always reads current values + // instead of a stale render closure. + const latest = React.useRef({text, value, parsedDate}); + latest.current = {text, value, parsedDate}; + const confirmValue = React.useCallback(() => { - if (!text) { + const {text: currentText, value: currentValue, parsedDate: currentParsed} = latest.current; + + if (!currentText) { return; } - if (parsedDate) { - commitValue(text); + if (currentParsed) { + commitValue(currentText); return; } - const newValue = parseRelativeDate(value) ? value : null; + const newValue = parseRelativeDate(currentValue) ? currentValue : null; setText(newValue ?? ''); commitValue(newValue); - }, [commitValue, parseRelativeDate, parsedDate, text, value]); + }, [commitValue, parseRelativeDate]); const handleTextChange = React.useCallback( (t: string) => {