This is an automated email from the ASF dual-hosted git repository.

bbovenzi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new d38e9ceb749 Fix date range filter inputs committing on every keystroke 
(#74011)
d38e9ceb749 is described below

commit d38e9ceb749bea391d68f9d86d63b831e36eebd3
Author: kuse <[email protected]>
AuthorDate: Thu Oct 8 04:39:10 2026 +0800

    Fix date range filter inputs committing on every keystroke (#74011)
    
    * Fix date range filter inputs committing on every keystroke (#69891)
    
    Typing in the date range picker committed the partial value on every
    keystroke, which updated the URL search params and let the sync effect
    rewrite the inputs from the committed value mid-typing, so the typed
    text flashed or got replaced. Keep edits local and commit explicitly:
    pressing Enter, closing the popover, clicking the calendar, or clearing
    a field. Invalid ranges are never committed.
    
    * Fix date range picker timezone shift on open/close
    
    The inputs were filled in the browser timezone while every commit path
    (typed edits, calendar picks, popover dismissal) interprets them in the
    selected timezone, so opening and closing the picker re-committed the
    displayed wall time in the selected timezone and shifted the value
    (e.g. 01:00 UTC displayed as 10:00 with a Seoul browser came back as
    10:00 UTC). Fill the inputs in the selected timezone (initial state,
    value sync, and the invalid-input blur reset), and commit calendar
    picks as day boundaries in the selected timezone so a click followed
    by dismissal re-derives the same value.
    
    The dismissal commit now also compares at minute granularity (the
    inputs are HH:mm), so re-deriving an unchanged range on close —
    including a calendar-picked 23:59:59.999 end-of-day from a displayed
    23:59, or a non-canonical value round-tripped from the URL — no longer
    fires onChange, and the sync effect re-fills the inputs when the
    selected timezone changes.
    
    Generated-by: ZCode (GLM)
    
    * Retrigger CI: static checks failed after 24h queue on unchanged code 
(eslint/tsc/prettier verified clean locally)
    
    ---------
    
    Co-authored-by: liwenjie <[email protected]>
---
 .../src/components/FilterBar/filters/DateInput.tsx |  10 +-
 .../FilterBar/filters/DateRangeFilter.test.tsx     | 162 ++++++++++++++++++++-
 .../FilterBar/filters/DateRangeFilter.tsx          |   4 +
 .../FilterBar/filters/DateRangeInputs.tsx          |   8 +-
 .../src/airflow/ui/src/hooks/useDateRangeFilter.ts | 117 ++++++++++-----
 5 files changed, 261 insertions(+), 40 deletions(-)

diff --git 
a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateInput.tsx 
b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateInput.tsx
index 0394569b817..06ae1f723c2 100644
--- a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateInput.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateInput.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import type { ChangeEvent } from "react";
+import type { ChangeEvent, KeyboardEvent } from "react";
 
 import { Box, Input, Text } from "@chakra-ui/react";
 import { MdClose } from "react-icons/md";
@@ -38,6 +38,7 @@ type DateInputProps = {
   readonly inputValue: string;
   readonly label: string;
   readonly onClear: () => void;
+  readonly onCommit?: () => void;
   readonly onDateBlur?: () => void;
   readonly onFocus?: () => void;
   readonly placeholder: string;
@@ -53,11 +54,17 @@ export const DateInput = ({
   inputValue,
   label,
   onClear,
+  onCommit,
   onDateBlur,
   onFocus,
   placeholder,
 }: DateInputProps) => {
   const fieldName = inputType === "date" ? field : (`${field}Time` as const);
+  const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
+    if (event.key === "Enter") {
+      onCommit?.();
+    }
+  };
 
   return (
     <Box flex="1">
@@ -74,6 +81,7 @@ export const DateInput = ({
           onBlur={onDateBlur}
           onChange={handleInputChange(field, inputType)}
           onFocus={onFocus}
+          onKeyDown={handleKeyDown}
           placeholder={placeholder}
           value={inputValue}
           w="full"
diff --git 
a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeFilter.test.tsx
 
b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeFilter.test.tsx
index 38044062e2d..9ecb2bf325a 100644
--- 
a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeFilter.test.tsx
+++ 
b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeFilter.test.tsx
@@ -17,6 +17,7 @@
  * under the License.
  */
 import type { ReactNode } from "react";
+import { useState } from "react";
 
 import "@testing-library/jest-dom/vitest";
 import { render, screen, fireEvent, waitFor, cleanup } from 
"@testing-library/react";
@@ -28,7 +29,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } 
from "vitest";
 import { TimezoneContext } from "src/context/timezone";
 import { ChakraWrapper } from "src/utils/ChakraWrapper";
 
-import type { FilterPluginProps } from "../types";
+import type { DateRangeValue, FilterPluginProps } from "../types";
 import { DateRangeFilter } from "./DateRangeFilter";
 
 dayjs.extend(timezone);
@@ -57,10 +58,29 @@ vi.mock("react-i18next", () => ({
   }),
 }));
 
-const TestWrapper = ({ children }: { readonly children: ReactNode }) => {
+// A UI timezone whose offset differs from the runner's local offset at the
+// instant used below, so tests exercise a real browser/UI timezone mismatch no
+// matter which timezone CI runs in (CI runners are usually UTC).
+const mismatchedTimezone = (instant: string): string => {
+  const runnerOffset = dayjs(instant).utcOffset();
+  const candidates = ["UTC", "Asia/Kolkata", "America/New_York", "Asia/Seoul"];
+
+  return candidates.find((tz) => dayjs.tz(instant, tz).utcOffset() !== 
runnerOffset) ?? "UTC";
+};
+
+const FILTERED_INSTANT = "2024-01-15T01:00:00.000Z";
+const uiTimezone = mismatchedTimezone(FILTERED_INSTANT);
+
+const TestWrapper = ({
+  children,
+  selectedTimezone = "UTC",
+}: {
+  readonly children: ReactNode;
+  readonly selectedTimezone?: string;
+}) => {
   const timezoneContextValue = {
     availableTimezones: ["UTC", "America/New_York"],
-    selectedTimezone: "UTC",
+    selectedTimezone,
     setSelectedTimezone: vi.fn(),
   };
 
@@ -112,6 +132,18 @@ const changeTimeInput = (input: HTMLElement | undefined, 
value: string) => {
   }
 };
 
+const pressEnter = (input: HTMLElement | undefined) => {
+  if (input) {
+    fireEvent.keyDown(input, { key: "Enter" });
+  }
+};
+
+const focusInput = (input: HTMLElement | undefined) => {
+  if (input) {
+    fireEvent.focus(input);
+  }
+};
+
 const waitForError = async (errorText: string) => {
   await waitFor(() => {
     expect(screen.getByText(errorText)).toBeInTheDocument();
@@ -132,13 +164,31 @@ const waitForNoErrors = async (errorTexts: Array<string>) 
=> {
   });
 };
 
-const renderFilter = (props: FilterPluginProps = defaultProps) =>
+const renderFilter = (props: FilterPluginProps = defaultProps, 
selectedTimezone = "UTC") =>
   render(
-    <TestWrapper>
+    <TestWrapper selectedTimezone={selectedTimezone}>
       <DateRangeFilter {...props} />
     </TestWrapper>,
   );
 
+// The popover content mounts asynchronously after the trigger is clicked.
+const openPicker = async () => {
+  fireEvent.click(screen.getByTestId("dateRange-pill"));
+  fireEvent.click(screen.getByText("Date Range:"));
+  await waitFor(() => {
+    expect(screen.getAllByPlaceholderText("YYYY/MM/DD").length).toBe(2);
+  });
+};
+
+const closePicker = async () => {
+  fireEvent.click(screen.getByText("Date Range:"));
+  // Wait for the dismissal to fully settle (commit + collapse back to the 
pill)
+  // so any commit triggered by closing has landed before asserting on it.
+  await waitFor(() => {
+    expect(screen.getByTestId("dateRange-pill")).toBeInTheDocument();
+  });
+};
+
 describe("DateRangeFilter", () => {
   beforeEach(() => {
     vi.clearAllMocks();
@@ -155,6 +205,9 @@ describe("DateRangeFilter", () => {
     const { endDateInput } = getInputs();
 
     changeDateInput(endDateInput, "2024/01/15");
+    expect(onChange).not.toHaveBeenCalled();
+
+    pressEnter(endDateInput);
 
     await waitFor(() => {
       expect(onChange).toHaveBeenLastCalledWith({
@@ -164,6 +217,105 @@ describe("DateRangeFilter", () => {
     });
   });
 
+  it("keeps typed input local and does not commit on every keystroke", () => {
+    const onChange = vi.fn();
+
+    renderFilter({ ...defaultProps, onChange });
+    const { startDateInput } = getInputs();
+
+    // Committing per keystroke used to sync the URL search params, whose
+    // value was then written back into the inputs mid-typing and made the
+    // picker unusable.
+    changeDateInput(startDateInput, "2024");
+    changeDateInput(startDateInput, "2024/01");
+    changeDateInput(startDateInput, "2024/01/1");
+    changeDateInput(startDateInput, "2024/01/15");
+
+    expect(onChange).not.toHaveBeenCalled();
+    expect(startDateInput).toHaveValue("2024/01/15");
+  });
+
+  it("does not shift the value when opening and closing the picker without 
edits", async () => {
+    const onChange = vi.fn();
+    const props = {
+      ...defaultProps,
+      filter: { ...mockFilter, value: { endDate: undefined, startDate: 
FILTERED_INSTANT } },
+      onChange,
+    };
+
+    renderFilter(props, uiTimezone);
+    await openPicker();
+
+    const { startDateInput, startTimeInput } = getInputs();
+    const expected = dayjs(FILTERED_INSTANT).tz(uiTimezone);
+
+    // The inputs are labeled with and committed in the selected timezone, so
+    // they must be filled in that timezone too. Filling them in the browser
+    // timezone made closing the picker re-commit the displayed wall time as if
+    // it were in the selected timezone, shifting the value.
+    expect(startDateInput).toHaveValue(expected.format("YYYY/MM/DD"));
+    expect(startTimeInput).toHaveValue(expected.format("HH:mm"));
+
+    await closePicker();
+
+    expect(onChange).not.toHaveBeenCalled();
+  });
+
+  it("commits a clicked calendar date in the selected timezone", async () => {
+    const onChange = vi.fn();
+    const props = {
+      ...defaultProps,
+      filter: { ...mockFilter, value: { endDate: undefined, startDate: 
FILTERED_INSTANT } },
+      onChange,
+    };
+
+    renderFilter(props, uiTimezone);
+    await openPicker();
+
+    // Opening the picker focuses the start input, so aim the next pick at the
+    // range's end explicitly.
+    focusInput(getInputs().endDateInput);
+    fireEvent.click(screen.getByText("16"));
+
+    expect(onChange).toHaveBeenCalledTimes(1);
+    expect(onChange).toHaveBeenLastCalledWith({
+      endDate: dayjs.tz("2024-01-16", uiTimezone).endOf("day").toISOString(),
+      startDate: FILTERED_INSTANT,
+    });
+  });
+
+  it("does not re-commit a parent-synced range when the picker closes", async 
() => {
+    const onChange = vi.fn();
+    const StatefulFilter = () => {
+      const [value, setValue] = useState<DateRangeValue>({ endDate: undefined, 
startDate: FILTERED_INSTANT });
+
+      return (
+        <DateRangeFilter
+          {...defaultProps}
+          filter={{ ...mockFilter, value }}
+          onChange={(next) => {
+            onChange(next);
+            setValue(next as DateRangeValue);
+          }}
+        />
+      );
+    };
+
+    render(
+      <TestWrapper selectedTimezone={uiTimezone}>
+        <StatefulFilter />
+      </TestWrapper>,
+    );
+
+    await openPicker();
+    fireEvent.click(screen.getByText("16"));
+    await closePicker();
+
+    // The parent-synced end-of-day 23:59:59.999 marker is displayed as 23:59 
in
+    // the minute-granular inputs; re-deriving it on close must be a no-op.
+    expect(onChange).toHaveBeenCalledTimes(1);
+  });
+
   it("accepts a start time on the end date when the end time is empty", async 
() => {
     renderFilter();
     const { endDateInput, startDateInput, startTimeInput } = getInputs();
diff --git 
a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeFilter.tsx
 
b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeFilter.tsx
index 24defae811f..7db1db57760 100644
--- 
a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeFilter.tsx
+++ 
b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeFilter.tsx
@@ -40,6 +40,7 @@ export const DateRangeFilter = ({ filter, onChange, onRemove 
}: FilterPluginProp
   const hasValue = isValidFilterValue(filter.config.type, filter.value);
 
   const {
+    commitEditingState,
     editingState,
     endDateValue,
     formatDisplayValue,
@@ -67,8 +68,10 @@ export const DateRangeFilter = ({ filter, onChange, onRemove 
}: FilterPluginProp
           lazyMount
           // The popover owns dismissal, so the pill never sees a blur. 
Deferring lets a range
           // picked in the same tick commit first, so the pill collapses 
instead of being dropped.
+          // Typed input is only kept local while editing, so commit it on 
dismissal too.
           onOpenChange={({ open }) => {
             if (!open) {
+              commitEditingState();
               setTimeout(onRequestClose, 0);
             }
           }}
@@ -151,6 +154,7 @@ export const DateRangeFilter = ({ filter, onChange, 
onRemove }: FilterPluginProp
                 getFieldError={getFieldError}
                 handleInputChange={handleInputChange}
                 onChange={onChange}
+                onCommit={commitEditingState}
                 setEditingState={setEditingState}
                 startDateValue={startDateValue}
                 translate={translate}
diff --git 
a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeInputs.tsx
 
b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeInputs.tsx
index e9593ad69b1..b32babf34d1 100644
--- 
a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeInputs.tsx
+++ 
b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeInputs.tsx
@@ -39,6 +39,7 @@ type DateRangeInputsProps = {
     inputType: "date" | "time",
   ) => (event: ChangeEvent<HTMLInputElement>) => void;
   readonly onChange: (value: DateRangeValue) => void;
+  readonly onCommit?: () => void;
   readonly setEditingState: Dispatch<SetStateAction<DateRangeEditingState>>;
   readonly startDateValue: dayjs.Dayjs | undefined;
   readonly translate: TFunction;
@@ -51,6 +52,7 @@ export const DateRangeInputs = ({
   getFieldError,
   handleInputChange,
   onChange,
+  onCommit,
   setEditingState,
   startDateValue,
   translate,
@@ -94,7 +96,7 @@ export const DateRangeInputs = ({
         ...prev,
         inputs: {
           ...prev.inputs,
-          [field]: dateValue.format(DATE_INPUT_FORMAT),
+          [field]: dateValue.tz(selectedTimezone).format(DATE_INPUT_FORMAT),
         },
       }));
     }
@@ -134,6 +136,7 @@ export const DateRangeInputs = ({
           inputValue={editingState.inputs.start}
           label={translate("common:table.from")}
           onClear={() => clearField("start")}
+          onCommit={onCommit}
           onDateBlur={handleDateBlur("start")}
           onFocus={handleFocus("start")}
           placeholder={DATE_INPUT_FORMAT}
@@ -148,6 +151,7 @@ export const DateRangeInputs = ({
           inputValue={editingState.inputs.end}
           label={translate("common:table.to")}
           onClear={() => clearField("end")}
+          onCommit={onCommit}
           onDateBlur={handleDateBlur("end")}
           onFocus={handleFocus("end")}
           placeholder={DATE_INPUT_FORMAT}
@@ -164,6 +168,7 @@ export const DateRangeInputs = ({
           inputValue={editingState.inputs.startTime}
           label={translate("common:filters.startTime")}
           onClear={clearTime("start")}
+          onCommit={onCommit}
           placeholder={TIME_INPUT_FORMAT}
         />
 
@@ -176,6 +181,7 @@ export const DateRangeInputs = ({
           inputValue={editingState.inputs.endTime}
           label={translate("common:filters.endTime")}
           onClear={clearTime("end")}
+          onCommit={onCommit}
           placeholder={TIME_INPUT_FORMAT}
         />
       </HStack>
diff --git a/airflow-core/src/airflow/ui/src/hooks/useDateRangeFilter.ts 
b/airflow-core/src/airflow/ui/src/hooks/useDateRangeFilter.ts
index b061fc1b41a..89c93dedc33 100644
--- a/airflow-core/src/airflow/ui/src/hooks/useDateRangeFilter.ts
+++ b/airflow-core/src/airflow/ui/src/hooks/useDateRangeFilter.ts
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { useEffect, useState, type ChangeEvent } from "react";
+import { useCallback, useEffect, useState, type ChangeEvent } from "react";
 
 import dayjs from "dayjs";
 import timezone from "dayjs/plugin/timezone";
@@ -101,6 +101,23 @@ const validateDateRange = (startDate?: string, endDate?: 
string): boolean => {
   return start.isBefore(end) || start.isSame(end);
 };
 
+// The inputs are minute-granular ("HH:mm"), so the commit comparison ignores
+// sub-minute differences: re-deriving an unchanged value (e.g. a 
calendar-picked
+// "end of day" 23:59:59.999 from a displayed 23:59) must not fire onChange.
+const isSameMinuteValue = (next?: string, current?: string): boolean => {
+  const nextIsAbsent = next === undefined || next === "";
+  const currentIsAbsent = current === undefined || current === "";
+
+  if (nextIsAbsent || currentIsAbsent) {
+    return nextIsAbsent && currentIsAbsent;
+  }
+
+  const nextDate = dayjs(next);
+  const currentDate = dayjs(current);
+
+  return nextDate.isValid() && currentDate.isValid() && 
nextDate.isSame(currentDate, "minute");
+};
+
 export const combineDateAndTime = (
   dateStr: string,
   timeStr: string,
@@ -137,13 +154,25 @@ export const useDateRangeFilter = ({ onChange, translate, 
value }: UseDateRangeF
   const startDateValue = isValidDateValue(value.startDate) ? 
dayjs(value.startDate) : undefined;
   const endDateValue = isValidDateValue(value.endDate) ? dayjs(value.endDate) 
: undefined;
 
+  // The inputs are labeled with and committed in the selected timezone, so 
they
+  // must be filled in that timezone too — filling in the browser timezone 
would
+  // make opening and closing the picker shift the value.
+  const formatInputDate = useCallback(
+    (date: dayjs.Dayjs | undefined) => 
date?.tz(selectedTimezone).format(DATE_INPUT_FORMAT) ?? "",
+    [selectedTimezone],
+  );
+  const formatInputTime = useCallback(
+    (date: dayjs.Dayjs | undefined) => 
date?.tz(selectedTimezone).format(TIME_INPUT_FORMAT) ?? "",
+    [selectedTimezone],
+  );
+
   const [editingState, setEditingState] = useState<DateRangeEditingState>(() 
=> ({
     currentMonth: startDateValue ?? endDateValue ?? dayjs(),
     inputs: {
-      end: endDateValue?.format(DATE_INPUT_FORMAT) ?? "",
-      endTime: endDateValue?.format(TIME_INPUT_FORMAT) ?? "",
-      start: startDateValue?.format(DATE_INPUT_FORMAT) ?? "",
-      startTime: startDateValue?.format(TIME_INPUT_FORMAT) ?? "",
+      end: formatInputDate(endDateValue),
+      endTime: formatInputTime(endDateValue),
+      start: formatInputDate(startDateValue),
+      startTime: formatInputTime(startDateValue),
     },
     selectionTarget: undefined,
     validationErrors: [],
@@ -213,13 +242,13 @@ export const useDateRangeFilter = ({ onChange, translate, 
value }: UseDateRangeF
     setEditingState((prev) => ({
       ...prev,
       inputs: {
-        end: endVal?.format(DATE_INPUT_FORMAT) ?? "",
-        endTime: endVal?.format(TIME_INPUT_FORMAT) ?? "",
-        start: startVal?.format(DATE_INPUT_FORMAT) ?? "",
-        startTime: startVal?.format(TIME_INPUT_FORMAT) ?? "",
+        end: formatInputDate(endVal),
+        endTime: formatInputTime(endVal),
+        start: formatInputDate(startVal),
+        startTime: formatInputTime(startVal),
       },
     }));
-  }, [value.startDate, value.endDate]);
+  }, [formatInputDate, formatInputTime, value.endDate, value.startDate]);
 
   const handleDateClick = (clickedDate: dayjs.Dayjs) => {
     const currentTarget = editingState.selectionTarget;
@@ -227,20 +256,25 @@ export const useDateRangeFilter = ({ onChange, translate, 
value }: UseDateRangeF
     let newStartDate: string | undefined = value.startDate;
     let newEndDate: string | undefined = value.endDate;
 
+    // Interpret the clicked grid day as a calendar date in the selected 
timezone,
+    // matching how typed inputs are committed, so closing the picker 
afterwards
+    // re-derives the same value instead of shifting it.
+    const clickedDay = dayjs.tz(clickedDate.format("YYYY-MM-DD"), 
selectedTimezone);
+
     if (currentTarget === "start" || (!startDateValue && !endDateValue)) {
       // Set start date with start of day time
-      newStartDate = clickedDate.startOf("day").toISOString();
+      newStartDate = clickedDay.startOf("day").toISOString();
 
       if (endDateValue && clickedDate.isAfter(endDateValue, "day")) {
         newEndDate = undefined;
       }
     } else {
       // Set end date with end of day time
-      newEndDate = clickedDate.endOf("day").toISOString();
+      newEndDate = clickedDay.endOf("day").toISOString();
 
       if (startDateValue && clickedDate.isBefore(startDateValue, "day")) {
-        newStartDate = clickedDate.startOf("day").toISOString();
-        newEndDate = clickedDate.endOf("day").toISOString();
+        newStartDate = clickedDay.startOf("day").toISOString();
+        newEndDate = clickedDay.endOf("day").toISOString();
       }
       nextTarget = undefined;
     }
@@ -250,8 +284,11 @@ export const useDateRangeFilter = ({ onChange, translate, 
value }: UseDateRangeF
       // Update inputs to reflect the new date values
       const newInputs = {
         ...prev.inputs,
-        end: newEndDate === undefined ? "" : 
dayjs(newEndDate).format(DATE_INPUT_FORMAT),
-        start: newStartDate === undefined ? "" : 
dayjs(newStartDate).format(DATE_INPUT_FORMAT),
+        end: newEndDate === undefined ? "" : 
dayjs(newEndDate).tz(selectedTimezone).format(DATE_INPUT_FORMAT),
+        start:
+          newStartDate === undefined
+            ? ""
+            : 
dayjs(newStartDate).tz(selectedTimezone).format(DATE_INPUT_FORMAT),
       };
       // Revalidate with the new inputs
       const validationErrors = validateInputs(newInputs);
@@ -271,27 +308,13 @@ export const useDateRangeFilter = ({ onChange, translate, 
value }: UseDateRangeF
       const inputValue = event.target.value;
       const inputKey = inputType === "date" ? field : (`${field}Time` as 
const);
 
+      // Keep the change local: committing on every keystroke updates the URL
+      // search params, and the sync effect below then rewrites the inputs from
+      // the committed value mid-typing, which makes the picker unusable.
       setEditingState((prev) => {
         const newInputs = { ...prev.inputs, [inputKey]: inputValue };
         const validationErrors = validateInputs(newInputs);
 
-        const dateStr = field === "start" ? newInputs.start : newInputs.end;
-        const timeStr = field === "start" ? newInputs.startTime : 
newInputs.endTime;
-
-        if (dayjs(dateStr, DATE_INPUT_FORMAT, true).isValid()) {
-          const combinedDateTime = combineDateAndTime(dateStr, timeStr, {
-            endOfDay: field === "end",
-            timezone: selectedTimezone,
-          });
-
-          if (Boolean(combinedDateTime)) {
-            onChange({
-              ...value,
-              [field === "start" ? "startDate" : "endDate"]: combinedDateTime,
-            });
-          }
-        }
-
         return {
           ...prev,
           inputs: newInputs,
@@ -300,6 +323,33 @@ export const useDateRangeFilter = ({ onChange, translate, 
value }: UseDateRangeF
       });
     };
 
+  const commitEditingState = () => {
+    const { inputs } = editingState;
+
+    if (validateInputs(inputs).length > 0) {
+      return;
+    }
+
+    const nextStartDate = combineDateAndTime(inputs.start, inputs.startTime, {
+      timezone: selectedTimezone,
+    });
+    const nextEndDate = combineDateAndTime(inputs.end, inputs.endTime, {
+      endOfDay: true,
+      timezone: selectedTimezone,
+    });
+    const nextValue = {
+      endDate: nextEndDate || undefined,
+      startDate: nextStartDate || undefined,
+    };
+
+    if (
+      !isSameMinuteValue(nextValue.startDate, value.startDate) ||
+      !isSameMinuteValue(nextValue.endDate, value.endDate)
+    ) {
+      onChange(nextValue);
+    }
+  };
+
   const formatDateTime = (date: dayjs.Dayjs) => {
     const dateStr = date.tz(selectedTimezone).format("MMM DD, YYYY");
 
@@ -337,6 +387,7 @@ export const useDateRangeFilter = ({ onChange, translate, 
value }: UseDateRangeF
   const hasValidationErrors = editingState.validationErrors.length > 0;
 
   return {
+    commitEditingState,
     editingState,
     endDateValue,
     formatDisplayValue,

Reply via email to