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

choo121600 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 ecb95734544 UI: Fix select filter pills staying open in Safari (#76013)
ecb95734544 is described below

commit ecb957345445feba9265310d60fac3b1b0106e98
Author: Yuseok Jo <[email protected]>
AuthorDate: Fri Oct 30 23:55:28 2026 +0900

    UI: Fix select filter pills staying open in Safari (#76013)
---
 .../ui/src/components/FilterBar/FilterBar.test.tsx | 20 ++++++++++++++++
 .../components/FilterBar/filters/LoopFilter.tsx    | 17 +++++---------
 .../FilterBar/filters/RunStateFilter.tsx           | 27 ++++++++++------------
 .../components/FilterBar/filters/SelectFilter.tsx  | 21 ++++++++---------
 .../src/airflow/ui/tests/e2e/pages/DagsPage.ts     |  4 ++--
 5 files changed, 49 insertions(+), 40 deletions(-)

diff --git 
a/airflow-core/src/airflow/ui/src/components/FilterBar/FilterBar.test.tsx 
b/airflow-core/src/airflow/ui/src/components/FilterBar/FilterBar.test.tsx
index 0e865dc450d..8f880f64337 100644
--- a/airflow-core/src/airflow/ui/src/components/FilterBar/FilterBar.test.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FilterBar/FilterBar.test.tsx
@@ -145,6 +145,26 @@ describe("FilterBar select filters", () => {
     expect(valueText).not.toBeNull();
     expect(globalThis.getComputedStyle(valueText as 
HTMLElement).overflow).toBe("visible");
   });
+
+  it("collapses the pill after a selection even when focus stays inside it", 
async () => {
+    // WebKit hands focus back to the select trigger, so blurring the active 
element leaves the pill open.
+    const blur = vi.spyOn(HTMLElement.prototype, "blur").mockImplementation(() 
=> undefined);
+    const selectConfig: FilterConfig = {
+      key: "state",
+      label: "State",
+      options: [{ label: "Failed", value: "failed" }],
+      type: "select",
+    };
+
+    render(<FilterBar configs={[selectConfig]} onFiltersChange={vi.fn()} />, { 
wrapper });
+
+    fireEvent.click(screen.getByTestId("add-filter-button"));
+    fireEvent.click(await screen.findByTestId("add-filter-state"));
+    fireEvent.click(await screen.findByTestId("state-filter-failed"));
+
+    await waitFor(() => 
expect(screen.getByTestId("state-pill")).toBeInTheDocument());
+    blur.mockRestore();
+  });
 });
 
 describe("FilterBar multiselect filters", () => {
diff --git 
a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/LoopFilter.tsx 
b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/LoopFilter.tsx
index 0e45a6a9aa2..a48473c0c9e 100644
--- 
a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/LoopFilter.tsx
+++ 
b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/LoopFilter.tsx
@@ -37,14 +37,6 @@ import { FilterPill } from "../FilterPill";
 import type { FilterPluginProps } from "../types";
 import { isLoopFilterValue } from "./loopParams";
 
-const collapsePill = () => {
-  setTimeout(() => {
-    const activeElement = document.activeElement as HTMLElement;
-
-    activeElement.blur();
-  }, 0);
-};
-
 /**
  * One pill, one list: every loop in the run and the passes it ran, as single 
choices.
  *
@@ -80,7 +72,6 @@ export const LoopFilter = ({ filter, onChange, onRemove }: 
FilterPluginProps) =>
     }
     hasJustSelected.current = true;
     onChange(decodeLoopOption(choice));
-    collapsePill();
   };
 
   return (
@@ -130,8 +121,12 @@ export const LoopFilter = ({ filter, onChange, onRemove }: 
FilterPluginProps) =>
             flex="1"
             h="full"
             onOpenChange={({ open }) => {
-              if (!open && !hasJustSelected.current) {
-                onRequestClose();
+              if (!open) {
+                if (hasJustSelected.current) {
+                  setTimeout(onRequestClose, 0);
+                } else {
+                  onRequestClose();
+                }
               }
               hasJustSelected.current = false;
             }}
diff --git 
a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/RunStateFilter.tsx
 
b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/RunStateFilter.tsx
index bd99830d15b..3e9a59c7c34 100644
--- 
a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/RunStateFilter.tsx
+++ 
b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/RunStateFilter.tsx
@@ -45,14 +45,6 @@ type RunStateFilterConfig = {
   options: Array<SelectOption>;
 };
 
-const collapsePill = () => {
-  setTimeout(() => {
-    const activeElement = document.activeElement as HTMLElement;
-
-    activeElement.blur();
-  }, 0);
-};
-
 /**
  * One pill filtering Dags on a run state matched within a lookback: the 
latest run only, a
  * recent time window, or the whole run history. Committed as a composite 
value that the
@@ -79,7 +71,6 @@ export const RunStateFilter = ({ filter, onChange, onRemove 
}: FilterPluginProps
     }
     hasJustSelected.current = true;
     onChange({ lookback: value?.lookback ?? pendingLookback, state: newState 
});
-    collapsePill();
   };
 
   const handleLookbackChange = ({ value: selected }: { value: Array<string> }) 
=> {
@@ -92,7 +83,6 @@ export const RunStateFilter = ({ filter, onChange, onRemove 
}: FilterPluginProps
     setPendingLookback(newLookback);
     if (value !== undefined) {
       onChange({ lookback: newLookback, state: value.state });
-      collapsePill();
     }
   };
 
@@ -164,12 +154,16 @@ export const RunStateFilter = ({ filter, onChange, 
onRemove }: FilterPluginProps
             defaultOpen={value === undefined}
             flex="1"
             h="full"
-            // Dismissing the menu hands focus back to the trigger inside the 
pill, so no blur
-            // fires and the pill would stay in edit mode. A close that 
follows a selection is
-            // left alone: the blur that selection triggers collapses the pill 
instead.
+            // Closing the menu hands focus back to the trigger inside the 
pill, so no blur fires
+            // and the pill would stay in edit mode. A close that follows a 
selection waits a tick,
+            // so the pill sees the chosen value instead of dropping the 
filter as abandoned.
             onOpenChange={({ open }) => {
-              if (!open && !hasJustSelected.current) {
-                onRequestClose();
+              if (!open) {
+                if (hasJustSelected.current) {
+                  setTimeout(onRequestClose, 0);
+                } else {
+                  onRequestClose();
+                }
               }
               hasJustSelected.current = false;
             }}
@@ -199,6 +193,9 @@ export const RunStateFilter = ({ filter, onChange, onRemove 
}: FilterPluginProps
               // Unlike the state select, closing this one without picking 
anything is not an
               // abandoned filter: the state select is the pill's primary 
control.
               if (!open) {
+                if (hasJustSelected.current && value !== undefined) {
+                  setTimeout(onRequestClose, 0);
+                }
                 hasJustSelected.current = false;
               }
             }}
diff --git 
a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/SelectFilter.tsx 
b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/SelectFilter.tsx
index e1b50eaa0c4..877166d6c6c 100644
--- 
a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/SelectFilter.tsx
+++ 
b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/SelectFilter.tsx
@@ -46,13 +46,6 @@ export const SelectFilter = ({ filter, onChange, onRemove }: 
FilterPluginProps)
 
     hasJustSelected.current = true;
     onChange(newValue);
-
-    // Trigger blur to close the editing mode after selection
-    setTimeout(() => {
-      const activeElement = document.activeElement as HTMLElement;
-
-      activeElement.blur();
-    }, 0);
   };
 
   const hasValue = filter.value !== null && filter.value !== undefined && 
filter.value !== "";
@@ -107,12 +100,16 @@ export const SelectFilter = ({ filter, onChange, onRemove 
}: FilterPluginProps)
             // open straight onto the options instead of making the user click 
again.
             defaultOpen={!hasValue}
             h="full"
-            // Dismissing the menu hands focus back to the trigger inside the 
pill, so no blur
-            // fires and the pill would stay in edit mode. A close that 
follows a selection is
-            // left alone: the blur that selection triggers collapses the pill 
instead.
+            // Closing the menu hands focus back to the trigger inside the 
pill, so no blur fires
+            // and the pill would stay in edit mode. A close that follows a 
selection waits a tick,
+            // so the pill sees the chosen value instead of dropping the 
filter as abandoned.
             onOpenChange={({ open }) => {
-              if (!open && !hasJustSelected.current) {
-                onRequestClose();
+              if (!open) {
+                if (hasJustSelected.current) {
+                  setTimeout(onRequestClose, 0);
+                } else {
+                  onRequestClose();
+                }
               }
               hasJustSelected.current = false;
             }}
diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts 
b/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts
index cc8dc68096b..97fff31b87c 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts
@@ -126,8 +126,8 @@ export class DagsPage extends BasePage {
         await this.page.getByTestId("add-filter-run_state").click();
       }
       await this.page.getByTestId(`run_state-filter-${status}`).click();
-      // Selecting blurs the pill, which collapses it ~150ms later. Hand back 
a settled bar
-      // instead of one mid-transition.
+      // Selecting collapses the pill a tick later. Hand back a settled bar 
instead of one
+      // mid-transition.
       await expect(this.runStatePill).toBeVisible({ timeout: 5000 });
     }
     await responsePromise;

Reply via email to