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

kasiazjc pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 4d95a8d0348 feat(listview): compact filter pills with popover for CRUD 
views (#40169)
4d95a8d0348 is described below

commit 4d95a8d0348ff6cbbe3d1e5ee6fa61ddef4e5ab2
Author: Kasia <[email protected]>
AuthorDate: Sat May 30 10:30:40 2026 +0200

    feat(listview): compact filter pills with popover for CRUD views (#40169)
---
 scripts/oxlint.sh                                  |   2 +-
 superset-frontend/spec/helpers/testing-library.tsx |  30 ++
 .../src/components/Chart/chartAction.ts            |   7 +-
 .../components/ListView/CardSortSelect.test.tsx    | 102 ++++++
 .../src/components/ListView/CardSortSelect.tsx     |  76 ++--
 .../ListView/Filters/CompactFilterTrigger.test.tsx | 145 ++++++++
 .../ListView/Filters/CompactFilterTrigger.tsx      | 198 +++++++++++
 .../ListView/Filters/CompactSelectPanel.test.tsx   | 339 ++++++++++++++++++
 .../ListView/Filters/CompactSelectPanel.tsx        | 318 +++++++++++++++++
 .../src/components/ListView/Filters/DateRange.tsx  | 112 ------
 .../ListView/Filters/FilterPopoverContent.test.tsx |  80 +++++
 .../ListView/Filters/FilterPopoverContent.tsx      |  74 ++++
 .../components/ListView/Filters/Select.test.tsx    | 267 --------------
 .../src/components/ListView/Filters/Select.tsx     | 154 ---------
 .../components/ListView/Filters/TimeRange.test.tsx | 251 ++++++++++++++
 .../src/components/ListView/Filters/TimeRange.tsx  | 291 ++++++++++++++++
 .../src/components/ListView/Filters/index.test.tsx | 335 +++++++++++++++++-
 .../src/components/ListView/Filters/index.tsx      | 385 ++++++++++++++++-----
 .../src/components/ListView/ListView.test.tsx      |  21 +-
 .../src/components/ListView/ListView.tsx           |  81 ++++-
 .../src/pages/ChartList/ChartList.test.tsx         |   9 +-
 .../DashboardList/DashboardList.cardview.test.tsx  |  16 +-
 .../src/pages/DashboardList/DashboardList.test.tsx |   8 +-
 .../DatasetList/DatasetList.integration.test.tsx   |   6 +-
 .../DatasetList/DatasetList.listview.test.tsx      |  23 +-
 .../src/pages/DatasetList/DatasetList.test.tsx     |   7 +-
 .../src/pages/GroupsList/GroupsList.test.tsx       |  16 +-
 .../src/pages/RolesList/RolesList.test.tsx         |   7 +-
 .../RowLevelSecurityList.test.tsx                  |   9 +-
 .../src/pages/UsersList/UsersList.test.tsx         |  20 +-
 30 files changed, 2649 insertions(+), 740 deletions(-)

diff --git a/scripts/oxlint.sh b/scripts/oxlint.sh
index 95f48afabb6..9baf026ddf2 100755
--- a/scripts/oxlint.sh
+++ b/scripts/oxlint.sh
@@ -55,7 +55,7 @@ if [ ${#js_ts_files[@]} -gt 0 ]; then
     echo "$output" >&2
     exit 1
   }
-  [ -n "$output" ] && echo "$output"
+  if [ -n "$output" ]; then echo "$output"; fi
 else
   echo "No JavaScript/TypeScript files to lint"
 fi
diff --git a/superset-frontend/spec/helpers/testing-library.tsx 
b/superset-frontend/spec/helpers/testing-library.tsx
index c55f2991c0e..be4211bd72a 100644
--- a/superset-frontend/spec/helpers/testing-library.tsx
+++ b/superset-frontend/spec/helpers/testing-library.tsx
@@ -152,3 +152,33 @@ export async function selectOption(option: string, 
selectName?: string) {
   );
   await userEvent.click(item);
 }
+
+/**
+ * Select an option from a compact pill filter (new UI that replaced 
comboboxes).
+ * Clicks the pill button matching the label, then clicks the option in the 
panel.
+ */
+export async function selectPillOption(option: string, pillLabel?: string) {
+  let pill: HTMLElement;
+  if (pillLabel) {
+    // Find the pill whose text content includes the label
+    pill = await waitFor(() => {
+      const pills = screen.getAllByTestId('compact-filter-pill');
+      const match = pills.find(p => p.textContent?.includes(pillLabel));
+      if (!match)
+        throw new Error(`Could not find pill with label "${pillLabel}"`);
+      return match;
+    });
+  } else {
+    pill = await screen.findByTestId('compact-filter-pill');
+  }
+  await userEvent.click(pill);
+  // Wait for the option list to appear and click the item
+  const item = await waitFor(() => {
+    const listbox = document.querySelector('[role="listbox"]');
+    if (!listbox) throw new Error('No listbox found');
+    const opt = within(listbox as HTMLElement).getByText(option);
+    if (!opt) throw new Error(`Option "${option}" not found`);
+    return opt;
+  });
+  await userEvent.click(item);
+}
diff --git a/superset-frontend/src/components/Chart/chartAction.ts 
b/superset-frontend/src/components/Chart/chartAction.ts
index c44c3ccd341..53caea51e88 100644
--- a/superset-frontend/src/components/Chart/chartAction.ts
+++ b/superset-frontend/src/components/Chart/chartAction.ts
@@ -817,8 +817,11 @@ export function exploreJSON(
             ),
         );
         (queriesResponse as QueryData[]).forEach(response => {
-          if (response.warning) {
-            dispatch(addWarningToast(response.warning, { noDuplicate: true }));
+          const { warning } = response as QueryData & {
+            warning?: string | null;
+          };
+          if (warning) {
+            dispatch(addWarningToast(warning, { noDuplicate: true }));
           }
         });
         return dispatch(
diff --git a/superset-frontend/src/components/ListView/CardSortSelect.test.tsx 
b/superset-frontend/src/components/ListView/CardSortSelect.test.tsx
new file mode 100644
index 00000000000..6e00683999e
--- /dev/null
+++ b/superset-frontend/src/components/ListView/CardSortSelect.test.tsx
@@ -0,0 +1,102 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { render, screen } from 'spec/helpers/testing-library';
+import userEvent from '@testing-library/user-event';
+import { CardSortSelect } from './CardSortSelect';
+
+const options = [
+  { desc: false, id: 'title', label: 'Alphabetical', value: 'alphabetical' },
+  {
+    desc: true,
+    id: 'changed_on',
+    label: 'Recently modified',
+    value: 'recently_modified',
+  },
+  {
+    desc: false,
+    id: 'changed_on',
+    label: 'Least recently modified',
+    value: 'least_recently_modified',
+  },
+];
+
+test('pill always shows "Sort" label with no value suffix and no clear 
button', () => {
+  render(
+    <CardSortSelect
+      options={options}
+      onChange={jest.fn()}
+      initialSort={[{ id: 'title', desc: false }]}
+    />,
+  );
+  expect(screen.getByText('Sort')).toBeInTheDocument();
+  expect(screen.queryByText(/sort.*alphabetical/i)).not.toBeInTheDocument();
+  expect(screen.queryByTestId('compact-filter-clear')).not.toBeInTheDocument();
+  expect(screen.getByTestId('compact-filter-pill')).toHaveAttribute(
+    'aria-expanded',
+    'false',
+  );
+});
+
+test('no clear button even when a non-default sort is active', () => {
+  render(
+    <CardSortSelect
+      options={options}
+      onChange={jest.fn()}
+      initialSort={[{ id: 'changed_on', desc: true }]}
+    />,
+  );
+  expect(screen.getByText('Sort')).toBeInTheDocument();
+  expect(screen.queryByTestId('compact-filter-clear')).not.toBeInTheDocument();
+});
+
+test('clicking a sort option from the panel calls onChange with the correct id 
and desc', async () => {
+  const onChange = jest.fn();
+  render(
+    <CardSortSelect
+      options={options}
+      onChange={onChange}
+      initialSort={[{ id: 'title', desc: false }]}
+    />,
+  );
+
+  await userEvent.click(screen.getByTestId('compact-filter-pill'));
+  expect(screen.getByText('Recently modified')).toBeInTheDocument();
+
+  await userEvent.click(screen.getByText('Recently modified'));
+
+  expect(onChange).toHaveBeenCalledWith([{ id: 'changed_on', desc: true }]);
+  // Pill label stays "Sort" — value is in tooltip, not the label
+  expect(screen.getByText('Sort')).toBeInTheDocument();
+});
+
+test('selecting a different option from the panel calls onChange with correct 
args', async () => {
+  const onChange = jest.fn();
+  render(
+    <CardSortSelect
+      options={options}
+      onChange={onChange}
+      initialSort={[{ id: 'title', desc: false }]}
+    />,
+  );
+
+  await userEvent.click(screen.getByTestId('compact-filter-pill'));
+  await userEvent.click(screen.getByText('Least recently modified'));
+
+  expect(onChange).toHaveBeenCalledWith([{ id: 'changed_on', desc: false }]);
+});
diff --git a/superset-frontend/src/components/ListView/CardSortSelect.tsx 
b/superset-frontend/src/components/ListView/CardSortSelect.tsx
index 57617fc2f25..96c71f81103 100644
--- a/superset-frontend/src/components/ListView/CardSortSelect.tsx
+++ b/superset-frontend/src/components/ListView/CardSortSelect.tsx
@@ -16,20 +16,13 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { useState, useMemo } from 'react';
+import { useRef, useState } from 'react';
 import { t } from '@apache-superset/core/translation';
-import { styled } from '@apache-superset/core/theme';
-import { FormLabel, Select } from '@superset-ui/core/components';
-import { SELECT_WIDTH } from './utils';
+import type { SelectOption } from './types';
 import { CardSortSelectOption, SortColumn } from './types';
-
-const SortContainer = styled.div`
-  display: inline-flex;
-  font-size: ${({ theme }) => theme.fontSizeSM}px;
-  align-items: center;
-  text-align: left;
-  width: ${SELECT_WIDTH}px;
-`;
+import CompactFilterTrigger from './Filters/CompactFilterTrigger';
+import CompactSelectPanel from './Filters/CompactSelectPanel';
+import type { FilterHandler } from './Filters/types';
 
 interface CardViewSelectSortProps {
   onChange: (value: SortColumn[]) => void;
@@ -42,6 +35,8 @@ export const CardSortSelect = ({
   onChange,
   options,
 }: CardViewSelectSortProps) => {
+  const panelRef = useRef<FilterHandler>(null);
+
   const defaultSort =
     (initialSort &&
       options.find(
@@ -50,44 +45,41 @@ export const CardSortSelect = ({
       )) ||
     options[0];
 
-  const [value, setValue] = useState({
+  const [currentValue, setCurrentValue] = useState<SelectOption>({
     label: defaultSort.label,
     value: defaultSort.value,
   });
 
-  const formattedOptions = useMemo(
-    () => options.map(option => ({ label: option.label, value: option.value 
})),
-    [options],
-  );
+  const selectOptions = options.map(o => ({ label: o.label, value: o.value }));
 
-  const handleOnChange = (selected: { label: string; value: string }) => {
-    setValue(selected);
-    const originalOption = options.find(
-      ({ value }) => value === selected.value,
-    );
-    if (originalOption) {
-      const sortBy = [
-        {
-          id: originalOption.id,
-          desc: originalOption.desc,
-        },
-      ];
-      onChange(sortBy);
+  const handleSelect = (option: SelectOption | undefined) => {
+    if (!option) return;
+    const original = options.find(o => o.value === option.value);
+    if (original) {
+      setCurrentValue({ label: original.label, value: original.value });
+      onChange([{ id: original.id, desc: original.desc }]);
     }
   };
 
   return (
-    <SortContainer>
-      <Select
-        ariaLabel={t('Sort')}
-        header={<FormLabel>{t('Sort')}</FormLabel>}
-        labelInValue
-        onChange={handleOnChange}
-        options={formattedOptions}
-        showSearch
-        value={value}
-        data-test="card-sort-select"
-      />
-    </SortContainer>
+    <span data-test="card-sort-select">
+      <CompactFilterTrigger
+        label={t('Sort')}
+        hasValue={false}
+        onClear={() => {}}
+        tooltipTitle={String(currentValue.label)}
+      >
+        {({ isOpen, onClose }) => (
+          <CompactSelectPanel
+            ref={panelRef}
+            selects={selectOptions}
+            value={currentValue}
+            onSelect={handleSelect}
+            isOpen={isOpen}
+            onClose={onClose}
+          />
+        )}
+      </CompactFilterTrigger>
+    </span>
   );
 };
diff --git 
a/superset-frontend/src/components/ListView/Filters/CompactFilterTrigger.test.tsx
 
b/superset-frontend/src/components/ListView/Filters/CompactFilterTrigger.test.tsx
new file mode 100644
index 00000000000..4267f51d795
--- /dev/null
+++ 
b/superset-frontend/src/components/ListView/Filters/CompactFilterTrigger.test.tsx
@@ -0,0 +1,145 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { render, screen } from 'spec/helpers/testing-library';
+import userEvent from '@testing-library/user-event';
+import CompactFilterTrigger from './CompactFilterTrigger';
+
+// Base props without children — pass children as JSX to avoid 
no-children-prop lint rule.
+const baseProps = {
+  label: 'Owner',
+  hasValue: false,
+  onClear: jest.fn(),
+};
+
+const defaultChildren = jest.fn(() => (
+  <div data-testid="filter-content">Filter content</div>
+));
+
+function renderTrigger(
+  props: Partial<
+    typeof baseProps & {
+      hasValue: boolean;
+      tooltipTitle?: string;
+      popupType?: 'listbox' | 'dialog';
+    }
+  > = {},
+  children = defaultChildren,
+) {
+  return render(
+    <CompactFilterTrigger {...baseProps} {...props}>
+      {children}
+    </CompactFilterTrigger>,
+  );
+}
+
+beforeEach(() => {
+  jest.clearAllMocks();
+});
+
+test('renders the label', () => {
+  renderTrigger();
+  expect(screen.getByText('Owner')).toBeInTheDocument();
+});
+
+test('renders as inactive pill with down chevron when hasValue is false', () 
=> {
+  renderTrigger();
+  const pill = screen.getByTestId('compact-filter-pill');
+  expect(pill).toBeInTheDocument();
+  // No clear button when inactive
+  expect(screen.queryByTestId('compact-filter-clear')).not.toBeInTheDocument();
+});
+
+test('renders active state with clear icon when hasValue is true', () => {
+  renderTrigger({ hasValue: true });
+  expect(screen.getByTestId('compact-filter-clear')).toBeInTheDocument();
+});
+
+test('clear icon has descriptive aria-label matching the filter name', () => {
+  renderTrigger({ hasValue: true });
+  const clearIcon = screen.getByTestId('compact-filter-clear');
+  expect(clearIcon).toHaveAttribute('aria-label', 'Clear Owner filter');
+});
+
+test('clear icon is rendered inside the pill button', () => {
+  renderTrigger({ hasValue: true });
+  const pill = screen.getByTestId('compact-filter-pill');
+  const clearIcon = screen.getByTestId('compact-filter-clear');
+  expect(pill).toContainElement(clearIcon);
+});
+
+test('toggles aria-expanded when pill is clicked', async () => {
+  renderTrigger();
+  const pill = screen.getByTestId('compact-filter-pill');
+  expect(pill).toHaveAttribute('aria-expanded', 'false');
+  await userEvent.click(pill);
+  expect(pill).toHaveAttribute('aria-expanded', 'true');
+});
+
+test('calls onClear when clear icon is clicked', async () => {
+  const onClear = jest.fn();
+  renderTrigger({ hasValue: true, onClear } as any);
+  const clearIcon = screen.getByTestId('compact-filter-clear');
+  await userEvent.click(clearIcon);
+  expect(onClear).toHaveBeenCalledTimes(1);
+});
+
+test('does not render tooltip wrapper when tooltipTitle is absent', () => {
+  const { container } = renderTrigger();
+  expect(container.querySelector('.ant-tooltip')).not.toBeInTheDocument();
+});
+
+test('shows active state indicators when hasValue and tooltipTitle are set', 
() => {
+  renderTrigger({ hasValue: true, tooltipTitle: 'Some Owner' });
+  expect(screen.getByTestId('compact-filter-clear')).toBeInTheDocument();
+  expect(screen.getByTestId('compact-filter-pill')).toHaveAttribute(
+    'aria-expanded',
+    'false',
+  );
+});
+
+test('calls children render prop with isOpen and onClose', async () => {
+  const children = jest.fn(() => <div data-testid="panel-content">panel</div>);
+  renderTrigger({}, children);
+  const pill = screen.getByTestId('compact-filter-pill');
+  await userEvent.click(pill);
+  expect(children).toHaveBeenCalledWith(
+    expect.objectContaining({ isOpen: true, onClose: expect.any(Function) }),
+  );
+});
+
+test('sets aria-haspopup to listbox by default', () => {
+  renderTrigger();
+  const pill = screen.getByTestId('compact-filter-pill');
+  expect(pill).toHaveAttribute('aria-haspopup', 'listbox');
+});
+
+test('sets aria-haspopup to dialog when popupType is dialog', () => {
+  renderTrigger({ popupType: 'dialog' });
+  const pill = screen.getByTestId('compact-filter-pill');
+  expect(pill).toHaveAttribute('aria-haspopup', 'dialog');
+});
+
+test('closing dropdown resets aria-expanded to false', async () => {
+  renderTrigger();
+  const pill = screen.getByTestId('compact-filter-pill');
+  await userEvent.click(pill);
+  expect(pill).toHaveAttribute('aria-expanded', 'true');
+  await userEvent.click(pill);
+  expect(pill).toHaveAttribute('aria-expanded', 'false');
+});
diff --git 
a/superset-frontend/src/components/ListView/Filters/CompactFilterTrigger.tsx 
b/superset-frontend/src/components/ListView/Filters/CompactFilterTrigger.tsx
new file mode 100644
index 00000000000..4863e9abbbe
--- /dev/null
+++ b/superset-frontend/src/components/ListView/Filters/CompactFilterTrigger.tsx
@@ -0,0 +1,198 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {
+  useEffect,
+  useRef,
+  useState,
+  type ReactNode,
+  type MouseEvent,
+} from 'react';
+import { t } from '@apache-superset/core/translation';
+import { useTheme, styled, css } from '@apache-superset/core/theme';
+import { Dropdown, Tooltip, Icons } from '@superset-ui/core/components';
+
+export type FilterPanelRenderProps = {
+  isOpen: boolean;
+  onClose: () => void;
+};
+
+interface CompactFilterTriggerProps {
+  label: ReactNode;
+  hasValue: boolean;
+  onClear: () => void;
+  /** Render prop: receives { isOpen, onClose } and returns the panel content. 
*/
+  children: (props: FilterPanelRenderProps) => ReactNode;
+  /** Shown as a hover tooltip when a value is selected (e.g. the selected 
label). */
+  tooltipTitle?: string;
+  /** ARIA popup role for the trigger button. Use 'listbox' for option panels,
+   *  'dialog' for form panels (date range, numerical range). */
+  popupType?: 'listbox' | 'dialog';
+}
+
+const FilterPill = styled.button<{ $active: boolean }>`
+  ${({ theme, $active }) => css`
+    display: inline-flex;
+    align-items: center;
+    gap: ${theme.sizeUnit}px;
+    height: ${theme.controlHeight}px;
+    padding: 0 ${theme.sizeUnit * 3}px;
+    border-radius: ${theme.borderRadius}px;
+    border: 1px solid ${$active ? theme.colorPrimary : theme.colorBorder};
+    background: ${$active ? theme.colorPrimaryBg : theme.colorBgContainer};
+    color: ${$active ? theme.colorPrimary : theme.colorText};
+    font-size: ${theme.fontSizeSM}px;
+    font-weight: ${$active ? 600 : 400};
+    cursor: pointer;
+    white-space: nowrap;
+    line-height: 1;
+    transition:
+      border-color 0.2s,
+      background 0.2s,
+      color 0.2s;
+
+    /* AntD anticon spans carry vertical-align: -0.125em from global styles.
+       align-self centers the span within the pill; the inner flex+align-items
+       centers the svg within the span. */
+    .anticon {
+      display: flex;
+      align-items: center;
+      align-self: center;
+      line-height: 0;
+    }
+
+    &:hover {
+      border-color: ${theme.colorPrimary};
+      background: ${$active ? theme.colorPrimaryBgHover : 
theme.colorFillAlter};
+    }
+
+    &:focus-visible {
+      outline: 2px solid ${theme.colorPrimary};
+      outline-offset: 2px;
+    }
+  `}
+`;
+
+const ActiveDot = styled.span`
+  ${({ theme }) => css`
+    width: 6px;
+    height: 6px;
+    border-radius: 50%;
+    background: ${theme.colorPrimary};
+    flex-shrink: 0;
+  `}
+`;
+
+export default function CompactFilterTrigger({
+  label,
+  hasValue,
+  onClear,
+  children,
+  tooltipTitle,
+  popupType = 'listbox',
+}: CompactFilterTriggerProps) {
+  const [open, setOpen] = useState(false);
+  const [tooltipOpen, setTooltipOpen] = useState(false);
+  const theme = useTheme();
+  // Tracks whether tooltip should be suppressed after dropdown close.
+  // Brave (and some other browsers) fire a synthetic mouseover on 
newly-exposed
+  // elements when a popup disappears, triggering Tooltip onOpenChange(true)
+  // without real user intent. We suppress until the cursor actually leaves the
+  // pill (onMouseLeave), which is the first reliable "hover reset" signal.
+  const tooltipSuppressedRef = useRef(false);
+
+  // Close dropdown on window resize — AntD Dropdown doesn't reposition
+  // itself on resize so the panel ends up detached from the pill.
+  useEffect(() => {
+    if (!open) return;
+    const handleResize = () => setOpen(false);
+    window.addEventListener('resize', handleResize);
+    return () => window.removeEventListener('resize', handleResize);
+  }, [open]);
+
+  const handleClear = (e: MouseEvent) => {
+    e.stopPropagation();
+    onClear();
+    setOpen(false);
+    tooltipSuppressedRef.current = true;
+    setTooltipOpen(false);
+  };
+
+  return (
+    <Dropdown
+      open={open}
+      onOpenChange={visible => {
+        setOpen(visible);
+        if (!visible) {
+          tooltipSuppressedRef.current = true;
+          setTooltipOpen(false);
+        }
+      }}
+      trigger={['click']}
+      popupRender={() =>
+        children({ isOpen: open, onClose: () => setOpen(false) })
+      }
+      placement="bottomLeft"
+      destroyPopupOnHide
+    >
+      <Tooltip
+        title={tooltipTitle}
+        open={!!tooltipTitle && !open && tooltipOpen}
+        onOpenChange={visible => {
+          if (visible && tooltipSuppressedRef.current) return;
+          setTooltipOpen(visible && !!tooltipTitle && !open);
+        }}
+        mouseEnterDelay={0.5}
+        mouseLeaveDelay={0}
+      >
+        <FilterPill
+          $active={hasValue}
+          type="button"
+          data-test="compact-filter-pill"
+          aria-haspopup={popupType}
+          aria-expanded={open}
+          aria-label={typeof label === 'string' ? label : undefined}
+          onMouseLeave={() => {
+            tooltipSuppressedRef.current = false;
+          }}
+        >
+          {hasValue && <ActiveDot />}
+          <span>{label}</span>
+          {hasValue ? (
+            <Icons.CloseOutlined
+              iconSize="s"
+              iconColor={theme.colorPrimary}
+              onClick={handleClear}
+              data-test="compact-filter-clear"
+              aria-label={
+                typeof label === 'string'
+                  ? t('Clear %s filter', label)
+                  : undefined
+              }
+            />
+          ) : (
+            <Icons.DownOutlined
+              iconSize="s"
+              iconColor={theme.colorTextSecondary}
+            />
+          )}
+        </FilterPill>
+      </Tooltip>
+    </Dropdown>
+  );
+}
diff --git 
a/superset-frontend/src/components/ListView/Filters/CompactSelectPanel.test.tsx 
b/superset-frontend/src/components/ListView/Filters/CompactSelectPanel.test.tsx
new file mode 100644
index 00000000000..01010a574c8
--- /dev/null
+++ 
b/superset-frontend/src/components/ListView/Filters/CompactSelectPanel.test.tsx
@@ -0,0 +1,339 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { createRef, act } from 'react';
+import { render, screen, waitFor } from 'spec/helpers/testing-library';
+import userEvent from '@testing-library/user-event';
+import CompactSelectPanel from './CompactSelectPanel';
+import type { FilterHandler } from './types';
+
+const SMALL_SELECTS = [
+  { label: 'Alice', value: 1 },
+  { label: 'Bob', value: 2 },
+  { label: 'Charlie', value: 3 },
+];
+
+const LARGE_SELECTS = [
+  { label: 'Alice', value: 1 },
+  { label: 'Bob', value: 2 },
+  { label: 'Charlie', value: 3 },
+  { label: 'David', value: 4 },
+  { label: 'Eve', value: 5 },
+  { label: 'Frank', value: 6 },
+  { label: 'Grace', value: 7 },
+];
+
+beforeEach(() => {
+  jest.clearAllMocks();
+});
+
+test('renders options from selects prop', () => {
+  render(
+    <CompactSelectPanel
+      selects={SMALL_SELECTS}
+      value={undefined}
+      onSelect={jest.fn()}
+    />,
+  );
+  expect(screen.getByText('Alice')).toBeInTheDocument();
+  expect(screen.getByText('Bob')).toBeInTheDocument();
+  expect(screen.getByText('Charlie')).toBeInTheDocument();
+});
+
+test('hides search input when selects.length is 6 or fewer', () => {
+  render(
+    <CompactSelectPanel
+      selects={SMALL_SELECTS}
+      value={undefined}
+      onSelect={jest.fn()}
+    />,
+  );
+  expect(screen.queryByPlaceholderText('Search')).not.toBeInTheDocument();
+});
+
+test('shows search input when selects.length exceeds 6', () => {
+  render(
+    <CompactSelectPanel
+      selects={LARGE_SELECTS}
+      value={undefined}
+      onSelect={jest.fn()}
+    />,
+  );
+  expect(screen.getByPlaceholderText('Search')).toBeInTheDocument();
+});
+
+test('shows search input when fetchSelects is provided', () => {
+  const fetchSelects = jest.fn().mockResolvedValue({ data: [], totalCount: 0 
});
+  render(
+    <CompactSelectPanel
+      fetchSelects={fetchSelects}
+      value={undefined}
+      onSelect={jest.fn()}
+    />,
+  );
+  expect(screen.getByPlaceholderText('Search')).toBeInTheDocument();
+});
+
+test('filters static options by search term', async () => {
+  render(
+    <CompactSelectPanel
+      selects={LARGE_SELECTS}
+      value={undefined}
+      onSelect={jest.fn()}
+    />,
+  );
+  await userEvent.type(screen.getByPlaceholderText('Search'), 'ali');
+  expect(screen.getByText('Alice')).toBeInTheDocument();
+  expect(screen.queryByText('Bob')).not.toBeInTheDocument();
+});
+
+test('calls onSelect with normalized option when an option is clicked', async 
() => {
+  const onSelect = jest.fn();
+  render(
+    <CompactSelectPanel
+      selects={SMALL_SELECTS}
+      value={undefined}
+      onSelect={onSelect}
+    />,
+  );
+  await userEvent.click(screen.getByText('Alice'));
+  expect(onSelect).toHaveBeenCalledWith({ label: 'Alice', value: 1 }, false);
+});
+
+test('calls onSelect with undefined when same option is clicked twice 
(deselect)', async () => {
+  const onSelect = jest.fn();
+  render(
+    <CompactSelectPanel
+      selects={SMALL_SELECTS}
+      value={{ label: 'Alice', value: 1 }}
+      onSelect={onSelect}
+    />,
+  );
+  await userEvent.click(screen.getByText('Alice'));
+  expect(onSelect).toHaveBeenCalledWith(undefined, true);
+});
+
+test('shows checkmark icon on selected option', () => {
+  render(
+    <CompactSelectPanel
+      selects={SMALL_SELECTS}
+      value={{ label: 'Alice', value: 1 }}
+      onSelect={jest.fn()}
+    />,
+  );
+  const aliceOption = screen
+    .getByText('Alice')
+    .closest('[role="option"]') as HTMLElement;
+  expect(aliceOption).toHaveAttribute('aria-selected', 'true');
+});
+
+test('unselected options have aria-selected false', () => {
+  render(
+    <CompactSelectPanel
+      selects={SMALL_SELECTS}
+      value={{ label: 'Alice', value: 1 }}
+      onSelect={jest.fn()}
+    />,
+  );
+  const bobOption = screen
+    .getByText('Bob')
+    .closest('[role="option"]') as HTMLElement;
+  expect(bobOption).toHaveAttribute('aria-selected', 'false');
+});
+
+test('calls onClose after a selection is made', async () => {
+  const onClose = jest.fn();
+  render(
+    <CompactSelectPanel
+      selects={SMALL_SELECTS}
+      value={undefined}
+      onSelect={jest.fn()}
+      onClose={onClose}
+    />,
+  );
+  await userEvent.click(screen.getByText('Alice'));
+  expect(onClose).toHaveBeenCalledTimes(1);
+});
+
+test('clearFilter via ref resets selection and calls onSelect(undefined, 
true)', () => {
+  const onSelect = jest.fn();
+  const ref = createRef<FilterHandler>();
+  const { rerender } = render(
+    <CompactSelectPanel
+      ref={ref}
+      selects={SMALL_SELECTS}
+      value={{ label: 'Alice', value: 1 }}
+      onSelect={onSelect}
+    />,
+  );
+  expect(screen.getByText('Alice').closest('[role="option"]')).toHaveAttribute(
+    'aria-selected',
+    'true',
+  );
+
+  act(() => {
+    ref.current?.clearFilter();
+  });
+
+  expect(onSelect).toHaveBeenCalledWith(undefined, true);
+  // Component is fully controlled — visual deselection follows when the
+  // parent passes value={undefined} after receiving the onSelect callback.
+  rerender(
+    <CompactSelectPanel
+      ref={ref}
+      selects={SMALL_SELECTS}
+      value={undefined}
+      onSelect={onSelect}
+    />,
+  );
+  expect(screen.getByText('Alice').closest('[role="option"]')).toHaveAttribute(
+    'aria-selected',
+    'false',
+  );
+});
+
+test('shows Loading text when loading prop is true', () => {
+  render(
+    <CompactSelectPanel
+      selects={SMALL_SELECTS}
+      value={undefined}
+      onSelect={jest.fn()}
+      loading
+    />,
+  );
+  expect(screen.getByText('Loading...')).toBeInTheDocument();
+});
+
+test('shows No results when displayOptions is empty', () => {
+  render(
+    <CompactSelectPanel selects={[]} value={undefined} onSelect={jest.fn()} />,
+  );
+  expect(screen.getByText('No results')).toBeInTheDocument();
+});
+
+test('renders options list with listbox role and accessible label', () => {
+  render(
+    <CompactSelectPanel
+      selects={SMALL_SELECTS}
+      value={undefined}
+      onSelect={jest.fn()}
+    />,
+  );
+  const listbox = screen.getByRole('listbox');
+  expect(listbox).toBeInTheDocument();
+  expect(listbox).toHaveAttribute('aria-label', 'Filter options');
+});
+
+test('option items have option role', () => {
+  render(
+    <CompactSelectPanel
+      selects={SMALL_SELECTS}
+      value={undefined}
+      onSelect={jest.fn()}
+    />,
+  );
+  const options = screen.getAllByRole('option');
+  expect(options).toHaveLength(3);
+});
+
+test('fetches and displays remote options via fetchSelects on mount', async () 
=> {
+  const fetchSelects = jest.fn().mockResolvedValue({
+    data: [{ label: 'Remote User', value: 99 }],
+    totalCount: 1,
+  });
+  render(
+    <CompactSelectPanel
+      fetchSelects={fetchSelects}
+      value={undefined}
+      onSelect={jest.fn()}
+    />,
+  );
+  expect(screen.getByText('Loading...')).toBeInTheDocument();
+  await waitFor(() => {
+    expect(screen.getByText('Remote User')).toBeInTheDocument();
+  });
+  expect(fetchSelects).toHaveBeenCalledWith('', 0, 200);
+});
+
+test('shows No results when fetchSelects returns empty data', async () => {
+  const fetchSelects = jest.fn().mockResolvedValue({ data: [], totalCount: 0 
});
+  render(
+    <CompactSelectPanel
+      fetchSelects={fetchSelects}
+      value={undefined}
+      onSelect={jest.fn()}
+    />,
+  );
+  await waitFor(() => {
+    expect(screen.getByText('No results')).toBeInTheDocument();
+  });
+});
+
+test('shows No results when fetchSelects rejects', async () => {
+  const fetchSelects = jest.fn().mockRejectedValue(new Error('network error'));
+  render(
+    <CompactSelectPanel
+      fetchSelects={fetchSelects}
+      value={undefined}
+      onSelect={jest.fn()}
+    />,
+  );
+  await waitFor(() => {
+    expect(screen.getByText('No results')).toBeInTheDocument();
+  });
+});
+
+test('selects option via keyboard Enter key', async () => {
+  const onSelect = jest.fn();
+  render(
+    <CompactSelectPanel
+      selects={SMALL_SELECTS}
+      value={undefined}
+      onSelect={onSelect}
+    />,
+  );
+  const aliceOption = screen.getByText('Alice').closest('[role="option"]')!;
+  await userEvent.type(aliceOption, '{Enter}');
+  expect(onSelect).toHaveBeenCalledWith({ label: 'Alice', value: 1 }, false);
+});
+
+test('syncs selected state when external value prop changes', () => {
+  const { rerender } = render(
+    <CompactSelectPanel
+      selects={SMALL_SELECTS}
+      value={{ label: 'Alice', value: 1 }}
+      onSelect={jest.fn()}
+    />,
+  );
+  expect(screen.getByText('Alice').closest('[role="option"]')).toHaveAttribute(
+    'aria-selected',
+    'true',
+  );
+
+  rerender(
+    <CompactSelectPanel
+      selects={SMALL_SELECTS}
+      value={undefined}
+      onSelect={jest.fn()}
+    />,
+  );
+  expect(screen.getByText('Alice').closest('[role="option"]')).toHaveAttribute(
+    'aria-selected',
+    'false',
+  );
+});
diff --git 
a/superset-frontend/src/components/ListView/Filters/CompactSelectPanel.tsx 
b/superset-frontend/src/components/ListView/Filters/CompactSelectPanel.tsx
new file mode 100644
index 00000000000..872b90e65cf
--- /dev/null
+++ b/superset-frontend/src/components/ListView/Filters/CompactSelectPanel.tsx
@@ -0,0 +1,318 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {
+  forwardRef,
+  useImperativeHandle,
+  useMemo,
+  useRef,
+  useState,
+  useEffect,
+  type CSSProperties,
+  type RefObject,
+} from 'react';
+import { debounce } from 'lodash';
+import { t } from '@apache-superset/core/translation';
+import { useTheme, styled, css } from '@apache-superset/core/theme';
+import {
+  Icons,
+  Input,
+  Constants,
+  type InputRef,
+} from '@superset-ui/core/components';
+import type { SelectOption, ListViewFilter as Filter } from '../types';
+import type { FilterHandler } from './types';
+
+// Show search box when there are more than this many static options.
+const SEARCH_THRESHOLD = 6;
+
+// Page size for async select fetches — large enough to avoid most pagination
+// issues while still being a bounded request. Full infinite-load pagination
+// is a future improvement.
+const ASYNC_PAGE_SIZE = 200;
+
+interface CompactSelectPanelProps {
+  selects?: Filter['selects'];
+  fetchSelects?: Filter['fetchSelects'];
+  value?: SelectOption;
+  onSelect: (option: SelectOption | undefined, isClear?: boolean) => void;
+  onClose?: () => void;
+  isOpen?: boolean;
+  /** Forwarded from the filter config's popupStyle for per-filter width 
overrides */
+  panelStyle?: CSSProperties;
+  /** External loading state from filter config */
+  loading?: boolean;
+}
+
+const PanelContainer = styled.div`
+  ${({ theme }) => css`
+    min-width: 220px;
+    max-width: 320px;
+    max-height: 320px;
+    display: flex;
+    flex-direction: column;
+    border-radius: ${theme.borderRadiusLG}px;
+    background: ${theme.colorBgElevated};
+    box-shadow: ${theme.boxShadowSecondary};
+    padding: 0 0 ${theme.paddingXXS}px;
+  `}
+`;
+
+const SearchRow = styled.div`
+  ${({ theme }) => css`
+    padding: ${theme.sizeUnit * 2}px ${theme.sizeUnit * 2}px
+      ${theme.paddingXXS}px;
+  `}
+`;
+
+const OptionList = styled.ul`
+  ${({ theme }) => css`
+    margin: 0;
+    padding: ${theme.paddingXXS}px 0;
+    overflow-y: auto;
+    flex: 1;
+    list-style: none;
+  `}
+`;
+
+const OptionItem = styled.li<{ $active: boolean }>`
+  ${({ theme, $active }) => css`
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: ${(theme.controlHeight - theme.fontSize * theme.lineHeight) / 2}px
+      ${theme.controlPaddingHorizontal}px;
+    line-height: ${theme.lineHeight};
+    cursor: pointer;
+    font-size: ${theme.fontSize}px;
+    color: ${theme.colorText};
+    border-radius: ${theme.borderRadiusSM}px;
+    background: ${$active ? theme.colorPrimaryBg : 'transparent'};
+    transition: background 0.15s;
+
+    &:hover {
+      background: ${$active
+        ? theme.colorPrimaryBgHover
+        : theme.colorFillTertiary};
+      outline: 2px solid ${theme.colorPrimary};
+      outline-offset: -2px;
+    }
+  `}
+`;
+
+const OptionLabel = styled.span`
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  max-width: 240px;
+`;
+
+const StatusText = styled.div`
+  ${({ theme }) => css`
+    padding: ${theme.sizeUnit * 2}px ${theme.sizeUnit * 3}px;
+    text-align: center;
+    color: ${theme.colorTextDisabled};
+    font-size: ${theme.fontSizeSM}px;
+  `}
+`;
+
+function CompactSelectPanel(
+  {
+    selects = [],
+    fetchSelects,
+    value,
+    onSelect,
+    onClose,
+    isOpen,
+    loading: externalLoading,
+    panelStyle,
+  }: CompactSelectPanelProps,
+  ref: RefObject<FilterHandler>,
+) {
+  const theme = useTheme();
+  const inputRef = useRef<InputRef>(null);
+  const [search, setSearch] = useState('');
+  const [debouncedSearch, setDebouncedSearch] = useState('');
+  const [remoteOptions, setRemoteOptions] = useState<SelectOption[]>([]);
+  const [internalLoading, setInternalLoading] = useState(false);
+
+  const isLoading = externalLoading || internalLoading;
+
+  const debouncedSetSearch = useMemo(
+    () => debounce(setDebouncedSearch, Constants.FAST_DEBOUNCE),
+    [],
+  );
+
+  useEffect(
+    () => () => {
+      debouncedSetSearch.cancel();
+    },
+    [debouncedSetSearch],
+  );
+
+  // Focus search input when dropdown opens; reset search when it closes
+  useEffect(() => {
+    let timeoutId: ReturnType<typeof setTimeout>;
+    if (isOpen) {
+      timeoutId = setTimeout(() => {
+        inputRef.current?.input?.focus({ preventScroll: true });
+      }, 100);
+    } else {
+      setSearch('');
+      setDebouncedSearch('');
+    }
+    return () => {
+      if (timeoutId) clearTimeout(timeoutId);
+    };
+  }, [isOpen]);
+
+  // Fetch remote options when debounced search changes
+  useEffect(() => {
+    if (!fetchSelects) return;
+    let cancelled = false;
+    setInternalLoading(true);
+    fetchSelects(debouncedSearch, 0, ASYNC_PAGE_SIZE)
+      .then(result => {
+        if (!cancelled) setRemoteOptions(result?.data ?? []);
+      })
+      .catch(() => {
+        if (!cancelled) setRemoteOptions([]);
+      })
+      .finally(() => {
+        if (!cancelled) setInternalLoading(false);
+      });
+    return () => {
+      cancelled = true;
+    };
+  }, [debouncedSearch, fetchSelects]);
+
+  useImperativeHandle(ref, () => ({
+    clearFilter: () => {
+      setSearch('');
+      setDebouncedSearch('');
+      onSelect(undefined, true);
+    },
+  }));
+
+  const displayOptions = (
+    fetchSelects
+      ? remoteOptions
+      : selects.filter(o => {
+          const label = typeof o.label === 'string' ? o.label : 
String(o.value);
+          return label.toLowerCase().includes(search.toLowerCase());
+        })
+  ).filter(o => o != null);
+
+  const showSearch = !!fetchSelects || selects.length > SEARCH_THRESHOLD;
+
+  const handleSelect = (opt: SelectOption, displayText?: string) => {
+    const isDeselect = value?.value === opt.value;
+    // Normalize to a plain string label for URL serialization:
+    // 1. String labels pass through unchanged.
+    // 2. ReactNode labels with a `title` field use that (set by callers for
+    //    options like owner-select where label contains name + email JSX).
+    // 3. Fall back to DOM text content, then stringified value.
+    const label =
+      typeof opt.label === 'string'
+        ? opt.label
+        : (opt.title ?? displayText ?? String(opt.value ?? ''));
+    const next = isDeselect ? undefined : { label, value: opt.value };
+    onSelect(next, isDeselect);
+    onClose?.();
+  };
+
+  return (
+    <PanelContainer style={panelStyle}>
+      {showSearch && (
+        <SearchRow>
+          <Input
+            ref={inputRef}
+            prefix={
+              <Icons.SearchOutlined iconSize="l" iconColor={theme.colorIcon} />
+            }
+            placeholder={t('Search')}
+            value={search}
+            onChange={e => {
+              setSearch(e.target.value);
+              debouncedSetSearch(e.target.value);
+            }}
+            allowClear
+            css={css`
+              width: 100%;
+              box-shadow: none;
+            `}
+          />
+        </SearchRow>
+      )}
+      <OptionList role="listbox" aria-label={t('Filter options')}>
+        {isLoading ? (
+          <StatusText>{t('Loading...')}</StatusText>
+        ) : displayOptions.length === 0 ? (
+          <StatusText>{t('No results')}</StatusText>
+        ) : (
+          displayOptions.map((opt, i) => {
+            const isActive = value?.value === opt.value;
+            const getDisplayText = (el: HTMLElement) =>
+              el.textContent?.trim() || undefined;
+            const isFirst = i === 0;
+            const isLast = i === displayOptions.length - 1;
+            return (
+              <OptionItem
+                key={opt.value}
+                $active={isActive}
+                role="option"
+                aria-selected={isActive}
+                tabIndex={0}
+                onClick={e =>
+                  handleSelect(opt, getDisplayText(e.currentTarget))
+                }
+                onKeyDown={e => {
+                  if (e.key === 'Enter' || e.key === ' ') {
+                    e.preventDefault();
+                    handleSelect(opt, getDisplayText(e.currentTarget));
+                  } else if (e.key === 'ArrowDown' && !isLast) {
+                    e.preventDefault();
+                    (
+                      e.currentTarget.nextElementSibling as HTMLElement | null
+                    )?.focus();
+                  } else if (e.key === 'ArrowUp' && !isFirst) {
+                    e.preventDefault();
+                    (
+                      e.currentTarget
+                        .previousElementSibling as HTMLElement | null
+                    )?.focus();
+                  }
+                }}
+              >
+                <OptionLabel>{opt.label}</OptionLabel>
+                {isActive && (
+                  <Icons.CheckOutlined
+                    iconSize="s"
+                    iconColor={theme.colorPrimary}
+                  />
+                )}
+              </OptionItem>
+            );
+          })
+        )}
+      </OptionList>
+    </PanelContainer>
+  );
+}
+
+export default forwardRef(CompactSelectPanel);
diff --git a/superset-frontend/src/components/ListView/Filters/DateRange.tsx 
b/superset-frontend/src/components/ListView/Filters/DateRange.tsx
deleted file mode 100644
index 1b735a5c9b0..00000000000
--- a/superset-frontend/src/components/ListView/Filters/DateRange.tsx
+++ /dev/null
@@ -1,112 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import {
-  useState,
-  useMemo,
-  forwardRef,
-  useImperativeHandle,
-  RefObject,
-} from 'react';
-
-import { t } from '@apache-superset/core/translation';
-import { Dayjs } from 'dayjs';
-import { useLocale } from 'src/hooks/useLocale';
-import { extendedDayjs } from '@superset-ui/core/utils/dates';
-import {
-  AntdThemeProvider,
-  Loading,
-  FormLabel,
-  RangePicker,
-} from '@superset-ui/core/components';
-import type { BaseFilter, FilterHandler } from './types';
-import { FilterContainer } from './Base';
-import { RANGE_WIDTH } from '../utils';
-
-interface DateRangeFilterProps extends BaseFilter {
-  onSubmit: (val: number[] | string[]) => void;
-  name: string;
-  dateFilterValueType?: 'unix' | 'iso';
-}
-
-type ValueState = [number, number] | [string, string] | null;
-
-function DateRangeFilter(
-  {
-    Header,
-    initialValue,
-    onSubmit,
-    dateFilterValueType = 'unix',
-  }: DateRangeFilterProps,
-  ref: RefObject<FilterHandler>,
-) {
-  const [value, setValue] = useState<ValueState | null>(initialValue ?? null);
-  const dayjsValue = useMemo((): [Dayjs, Dayjs] | null => {
-    if (!value || (Array.isArray(value) && !value.length)) return null;
-    return [extendedDayjs(value[0]), extendedDayjs(value[1])];
-  }, [value]);
-
-  const locale = useLocale();
-
-  useImperativeHandle(ref, () => ({
-    clearFilter: () => {
-      setValue(null);
-      onSubmit([]);
-    },
-  }));
-
-  if (locale === null) {
-    return <Loading position="inline-centered" />;
-  }
-  return (
-    <AntdThemeProvider locale={locale}>
-      <FilterContainer
-        data-test="date-range-filter-container"
-        vertical
-        justify="center"
-        align="start"
-        width={RANGE_WIDTH}
-      >
-        <FormLabel>{Header}</FormLabel>
-        <RangePicker
-          placeholder={[t('Start date'), t('End date')]}
-          showTime
-          value={dayjsValue}
-          onCalendarChange={(dayjsRange: [Dayjs, Dayjs]) => {
-            if (!dayjsRange?.[0]?.valueOf() || !dayjsRange?.[1]?.valueOf()) {
-              setValue(null);
-              onSubmit([]);
-              return;
-            }
-            const changeValue =
-              dateFilterValueType === 'iso'
-                ? [dayjsRange[0].toISOString(), dayjsRange[1].toISOString()]
-                : [
-                    dayjsRange[0]?.valueOf() ?? 0,
-                    dayjsRange[1]?.valueOf() ?? 0,
-                  ];
-            setValue(changeValue as ValueState);
-            onSubmit(changeValue);
-          }}
-        />
-      </FilterContainer>
-    </AntdThemeProvider>
-  );
-}
-
-export default forwardRef(DateRangeFilter);
diff --git 
a/superset-frontend/src/components/ListView/Filters/FilterPopoverContent.test.tsx
 
b/superset-frontend/src/components/ListView/Filters/FilterPopoverContent.test.tsx
new file mode 100644
index 00000000000..f863f78d498
--- /dev/null
+++ 
b/superset-frontend/src/components/ListView/Filters/FilterPopoverContent.test.tsx
@@ -0,0 +1,80 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { render, screen } from 'spec/helpers/testing-library';
+import userEvent from '@testing-library/user-event';
+import FilterPopoverContent from './FilterPopoverContent';
+
+beforeEach(() => {
+  jest.clearAllMocks();
+});
+
+test('renders children inside the wrapper', () => {
+  render(
+    <FilterPopoverContent>
+      <div data-test="inner-content">Inner content</div>
+    </FilterPopoverContent>,
+  );
+  expect(screen.getByTestId('inner-content')).toBeInTheDocument();
+});
+
+test('renders the Apply button', () => {
+  render(
+    <FilterPopoverContent>
+      <div>content</div>
+    </FilterPopoverContent>,
+  );
+  expect(screen.getByRole('button', { name: /apply/i })).toBeInTheDocument();
+});
+
+test('calls onClose when Apply button is clicked', async () => {
+  const onClose = jest.fn();
+  render(
+    <FilterPopoverContent onClose={onClose}>
+      <div>content</div>
+    </FilterPopoverContent>,
+  );
+  await userEvent.click(screen.getByRole('button', { name: /apply/i }));
+  expect(onClose).toHaveBeenCalledTimes(1);
+});
+
+test('renders without onClose and clicking Apply does not throw', async () => {
+  render(
+    <FilterPopoverContent>
+      <div>content</div>
+    </FilterPopoverContent>,
+  );
+  // No onClose prop — click should not throw
+  await userEvent.click(screen.getByRole('button', { name: /apply/i }));
+  expect(screen.getByRole('button', { name: /apply/i })).toBeInTheDocument();
+});
+
+test('visually hides label elements so pills remain accessible', () => {
+  render(
+    <FilterPopoverContent>
+      <label htmlFor="input">Date range</label>
+      <input id="input" />
+    </FilterPopoverContent>,
+  );
+  const label = screen.getByText('Date range');
+  // The label must be in the DOM for screen readers but visually hidden via 
CSS
+  expect(label).toBeInTheDocument();
+  const computedStyle = window.getComputedStyle(label);
+  // clip / overflow hidden pattern applied; position absolute is the key 
indicator
+  expect(computedStyle.position).toBe('absolute');
+});
diff --git 
a/superset-frontend/src/components/ListView/Filters/FilterPopoverContent.tsx 
b/superset-frontend/src/components/ListView/Filters/FilterPopoverContent.tsx
new file mode 100644
index 00000000000..4d9ea3ae032
--- /dev/null
+++ b/superset-frontend/src/components/ListView/Filters/FilterPopoverContent.tsx
@@ -0,0 +1,74 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import type { ReactNode } from 'react';
+import { t } from '@apache-superset/core/translation';
+import { styled, css } from '@apache-superset/core/theme';
+import { Button } from '@superset-ui/core/components';
+
+interface FilterPopoverContentProps {
+  children: ReactNode;
+  onClose?: () => void;
+}
+
+const Wrapper = styled.div`
+  ${({ theme }) => css`
+    padding: ${theme.sizeUnit * 2}px;
+    display: flex;
+    flex-direction: column;
+    gap: ${theme.sizeUnit * 2}px;
+    background: ${theme.colorBgElevated};
+    border-radius: ${theme.borderRadiusLG}px;
+    box-shadow: ${theme.boxShadowSecondary};
+
+    /* Visually hide the redundant label — the pill already shows it, but keep 
it
+       accessible to screen readers so filter inputs have a named context. */
+    label {
+      position: absolute !important;
+      width: 1px;
+      height: 1px;
+      padding: 0;
+      margin: -1px;
+      overflow: hidden;
+      clip: rect(0, 0, 0, 0);
+      white-space: nowrap;
+      border: 0;
+    }
+  `}
+`;
+
+const Footer = styled.div`
+  display: flex;
+  justify-content: flex-end;
+`;
+
+export default function FilterPopoverContent({
+  children,
+  onClose,
+}: FilterPopoverContentProps) {
+  return (
+    <Wrapper>
+      {children}
+      <Footer>
+        <Button size="small" buttonStyle="primary" onClick={onClose}>
+          {t('Apply')}
+        </Button>
+      </Footer>
+    </Wrapper>
+  );
+}
diff --git a/superset-frontend/src/components/ListView/Filters/Select.test.tsx 
b/superset-frontend/src/components/ListView/Filters/Select.test.tsx
deleted file mode 100644
index a0bedb32f20..00000000000
--- a/superset-frontend/src/components/ListView/Filters/Select.test.tsx
+++ /dev/null
@@ -1,267 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import { createRef } from 'react';
-import {
-  render,
-  screen,
-  selectOption,
-  waitFor,
-} from 'spec/helpers/testing-library';
-import { ListViewFilterOperator } from '../types';
-import UIFilters from './index';
-import SelectFilter from './Select';
-import type { FilterHandler } from './types';
-
-const mockUpdateFilterValue = jest.fn();
-
-beforeEach(() => {
-  mockUpdateFilterValue.mockClear();
-});
-
-test('select filter with ReactNode label uses option title when serializing 
selection', async () => {
-  // Regression for sc-104554: the chart-list Owner filter renders options
-  // with ReactNode labels (name + email). The value passed to
-  // updateFilterValue is serialized into URL / filter state and re-used to
-  // render the filter pill on return. It must carry the plain-text name
-  // (from `title`) and not fall back to the numeric user id.
-  const ReactNodeLabel = (
-    <div>
-      <span>John Doe</span>
-      <span>[email protected]</span>
-    </div>
-  );
-
-  const fetchSelects = jest.fn().mockResolvedValue({
-    data: [
-      {
-        label: ReactNodeLabel,
-        value: 42,
-        title: 'John Doe',
-      },
-    ],
-    totalCount: 1,
-  });
-
-  const filters = [
-    {
-      Header: 'Owner',
-      key: 'owner',
-      id: 'owners',
-      input: 'select' as const,
-      operator: ListViewFilterOperator.RelationManyMany,
-      unfilteredLabel: 'All',
-      fetchSelects,
-      paginate: true,
-    },
-  ];
-
-  render(
-    <UIFilters
-      filters={filters}
-      internalFilters={[]}
-      updateFilterValue={mockUpdateFilterValue}
-    />,
-  );
-
-  await selectOption('John Doe', 'Owner');
-
-  await waitFor(() => {
-    expect(mockUpdateFilterValue).toHaveBeenCalledWith(0, {
-      label: 'John Doe',
-      value: 42,
-    });
-  });
-});
-
-test('select filter falls back to stringified value when no string label or 
title is available', async () => {
-  const fetchSelects = jest.fn().mockResolvedValue({
-    data: [
-      {
-        label: <span>123</span>,
-        value: 123,
-      },
-    ],
-    totalCount: 1,
-  });
-
-  const filters = [
-    {
-      Header: 'Something',
-      key: 'something',
-      id: 'something',
-      input: 'select' as const,
-      operator: ListViewFilterOperator.RelationOneMany,
-      unfilteredLabel: 'All',
-      fetchSelects,
-    },
-  ];
-
-  render(
-    <UIFilters
-      filters={filters}
-      internalFilters={[]}
-      updateFilterValue={mockUpdateFilterValue}
-    />,
-  );
-
-  await selectOption('123', 'Something');
-
-  await waitFor(() => {
-    expect(mockUpdateFilterValue).toHaveBeenCalledWith(0, {
-      label: '123',
-      value: 123,
-    });
-  });
-});
-
-test('plain select with string label passes label through unchanged', async () 
=> {
-  // Happy-path coverage for the typeof-string branch in onChange, exercised
-  // through the non-async Select wrapper (selects array, no fetchSelects).
-  const filters = [
-    {
-      Header: 'Status',
-      key: 'status',
-      id: 'status',
-      input: 'select' as const,
-      operator: ListViewFilterOperator.Equals,
-      unfilteredLabel: 'All',
-      selects: [
-        { label: 'Published', value: 7 },
-        { label: 'Draft', value: 8 },
-      ],
-    },
-  ];
-
-  render(
-    <UIFilters
-      filters={filters}
-      internalFilters={[]}
-      updateFilterValue={mockUpdateFilterValue}
-    />,
-  );
-
-  await selectOption('Published', 'Status');
-
-  await waitFor(() => {
-    expect(mockUpdateFilterValue).toHaveBeenCalledWith(0, {
-      label: 'Published',
-      value: 7,
-    });
-  });
-});
-
-test('plain select with ReactNode label uses option title when serializing 
selection', async () => {
-  // Parallel coverage to the AsyncSelect ReactNode-with-title test, against
-  // the non-async Select wrapper. Guards against the two wrappers ever
-  // diverging on antd's two-arg onChange shape.
-  const ReactNodeLabel = (
-    <div>
-      <span>Jane Roe</span>
-      <span>[email protected]</span>
-    </div>
-  );
-
-  const filters = [
-    {
-      Header: 'Owner',
-      key: 'owner',
-      id: 'owners',
-      input: 'select' as const,
-      operator: ListViewFilterOperator.RelationManyMany,
-      unfilteredLabel: 'All',
-      selects: [{ label: ReactNodeLabel, value: 99, title: 'Jane Roe' }],
-    },
-  ];
-
-  render(
-    <UIFilters
-      filters={filters}
-      internalFilters={[]}
-      updateFilterValue={mockUpdateFilterValue}
-    />,
-  );
-
-  await selectOption('Jane Roe', 'Owner');
-
-  await waitFor(() => {
-    expect(mockUpdateFilterValue).toHaveBeenCalledWith(0, {
-      label: 'Jane Roe',
-      value: 99,
-    });
-  });
-});
-
-test('clearFilter notifies onSelect with undefined and isClear=true', () => {
-  // The isClear flag is what allows the parent (Filters/index) to suppress
-  // onFilterUpdate side-effects when the user clears the filter rather than
-  // picking a new value. Lock that contract in.
-  const mockOnSelect = jest.fn();
-  const ref = createRef<FilterHandler>();
-
-  render(
-    <SelectFilter
-      Header="Owner"
-      initialValue={{ label: 'John Doe', value: 42 }}
-      onSelect={mockOnSelect}
-      selects={[{ label: 'John Doe', value: 42, title: 'John Doe' }]}
-      ref={ref}
-    />,
-  );
-
-  ref.current?.clearFilter();
-
-  expect(mockOnSelect).toHaveBeenCalledWith(undefined, true);
-});
-
-test('rehydrates filter pill from initialValue with plain-string label', async 
() => {
-  // The user-visible regression: after URL/state rehydration the filter pill
-  // must render the human-readable name, not the numeric user id. The fix
-  // ensures the persisted label is a string; this test asserts that string
-  // is what surfaces in the rendered combobox selection.
-  const filters = [
-    {
-      Header: 'Owner',
-      key: 'owner',
-      id: 'owners',
-      input: 'select' as const,
-      operator: ListViewFilterOperator.RelationManyMany,
-      unfilteredLabel: 'All',
-      fetchSelects: jest.fn().mockResolvedValue({ data: [], totalCount: 0 }),
-      paginate: true,
-    },
-  ];
-
-  render(
-    <UIFilters
-      filters={filters}
-      internalFilters={[
-        {
-          id: 'owners',
-          operator: ListViewFilterOperator.RelationManyMany,
-          value: { label: 'John Doe', value: 42 },
-        },
-      ]}
-      updateFilterValue={mockUpdateFilterValue}
-    />,
-  );
-
-  await waitFor(() => {
-    expect(screen.getByText('John Doe')).toBeInTheDocument();
-  });
-});
diff --git a/superset-frontend/src/components/ListView/Filters/Select.tsx 
b/superset-frontend/src/components/ListView/Filters/Select.tsx
deleted file mode 100644
index cecee38a274..00000000000
--- a/superset-frontend/src/components/ListView/Filters/Select.tsx
+++ /dev/null
@@ -1,154 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import {
-  useState,
-  useMemo,
-  forwardRef,
-  useImperativeHandle,
-  type RefObject,
-} from 'react';
-
-import { t } from '@apache-superset/core/translation';
-import { Select, AsyncSelect, FormLabel } from '@superset-ui/core/components';
-import { ListViewFilter as Filter, SelectOption } from '../types';
-import type { BaseFilter, FilterHandler } from './types';
-import { FilterContainer } from './Base';
-import { SELECT_WIDTH } from '../utils';
-
-interface SelectFilterProps extends BaseFilter {
-  fetchSelects?: Filter['fetchSelects'];
-  name?: string;
-  onSelect: (selected: SelectOption | undefined, isClear?: boolean) => void;
-  optionFilterProps?: string[];
-  paginate?: boolean;
-  selects: Filter['selects'];
-  loading?: boolean;
-  dropdownStyle?: React.CSSProperties;
-}
-
-function SelectFilter(
-  {
-    Header,
-    name,
-    fetchSelects,
-    initialValue,
-    onSelect,
-    optionFilterProps,
-    selects = [],
-    loading = false,
-    dropdownStyle,
-  }: SelectFilterProps,
-  ref: RefObject<FilterHandler>,
-) {
-  const [selectedOption, setSelectedOption] = useState(initialValue);
-
-  const onChange = (selected: SelectOption, option?: SelectOption) => {
-    // antd's `onChange` (with `labelInValue`) passes the `{label, value}`
-    // labeled-value as the first arg and the full option (which carries
-    // `title` and any other fields) as the second. Options may supply a
-    // ReactNode label (e.g. OwnerSelectLabel for the chart list Owner
-    // filter). Since this object is serialized into the URL and rehydrated
-    // as the filter pill on return, we need a plain string. Prefer `title`
-    // (set by callers to the human-readable name) before falling back to
-    // the value.
-    onSelect(
-      selected
-        ? {
-            label:
-              typeof selected.label === 'string'
-                ? selected.label
-                : (option?.title ?? String(selected.value)),
-            value: selected.value,
-          }
-        : undefined,
-    );
-    setSelectedOption(selected);
-  };
-
-  const onClear = () => {
-    onSelect(undefined, true);
-    setSelectedOption(undefined);
-  };
-
-  useImperativeHandle(ref, () => ({
-    clearFilter: () => {
-      onClear();
-    },
-  }));
-
-  const fetchAndFormatSelects = useMemo(
-    () => async (inputValue: string, page: number, pageSize: number) => {
-      if (fetchSelects) {
-        const selectValues = await fetchSelects(inputValue, page, pageSize);
-        return {
-          data: selectValues.data,
-          totalCount: selectValues.totalCount,
-        };
-      }
-      return {
-        data: [],
-        totalCount: 0,
-      };
-    },
-    [fetchSelects],
-  );
-  const placeholder = t('Choose...');
-  return (
-    <FilterContainer
-      data-test="select-filter-container"
-      width={SELECT_WIDTH}
-      vertical
-      justify="center"
-      align="start"
-    >
-      <FormLabel>{Header}</FormLabel>
-      {fetchSelects ? (
-        <AsyncSelect
-          allowClear
-          ariaLabel={typeof Header === 'string' ? Header : name || t('Filter')}
-          data-test="filters-select"
-          onChange={onChange}
-          onClear={onClear}
-          options={fetchAndFormatSelects}
-          optionFilterProps={optionFilterProps}
-          placeholder={placeholder}
-          dropdownStyle={dropdownStyle}
-          showSearch
-          value={selectedOption}
-        />
-      ) : (
-        <Select
-          allowClear
-          ariaLabel={typeof Header === 'string' ? Header : name || t('Filter')}
-          data-test="filters-select"
-          labelInValue
-          onChange={onChange}
-          onClear={onClear}
-          options={selects}
-          placeholder={placeholder}
-          dropdownStyle={dropdownStyle}
-          showSearch
-          value={selectedOption}
-          loading={loading}
-        />
-      )}
-    </FilterContainer>
-  );
-}
-export default forwardRef(SelectFilter);
diff --git 
a/superset-frontend/src/components/ListView/Filters/TimeRange.test.tsx 
b/superset-frontend/src/components/ListView/Filters/TimeRange.test.tsx
new file mode 100644
index 00000000000..63b315f7f5b
--- /dev/null
+++ b/superset-frontend/src/components/ListView/Filters/TimeRange.test.tsx
@@ -0,0 +1,251 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { createRef, act } from 'react';
+import { render, screen, waitFor } from 'spec/helpers/testing-library';
+import userEvent from '@testing-library/user-event';
+import { NO_TIME_RANGE, SupersetClient } from '@superset-ui/core';
+import TimeRangeFilter from './TimeRange';
+import type { FilterHandler } from './types';
+
+// Suppress debounced evaluation — the initial useEffect handles the committed
+// value; the debounced path is an optimistic UX enhancement, not a contract.
+jest.mock('src/explore/exploreUtils', () => ({
+  ...jest.requireActual('src/explore/exploreUtils'),
+  useDebouncedEffect: jest.fn(),
+}));
+
+jest.mock('src/explore/components/controls/DateFilterControl/utils', () => ({
+  FRAME_OPTIONS: [
+    { label: 'No filter', value: 'No filter' },
+    { label: 'Custom', value: 'Custom' },
+  ],
+  guessFrame: jest.fn().mockReturnValue('Custom'),
+  // 'No filter' is the string value of NO_TIME_RANGE constant
+  useDefaultTimeFilter: jest.fn().mockReturnValue('No filter'),
+}));
+
+jest.mock(
+  'src/explore/components/controls/DateFilterControl/components',
+  () => ({
+    AdvancedFrame: () => <div data-test="advanced-frame" />,
+    CalendarFrame: () => <div data-test="calendar-frame" />,
+    CommonFrame: () => <div data-test="common-frame" />,
+    CustomFrame: ({ value }: { value: string }) => (
+      <div data-test="custom-frame">{value}</div>
+    ),
+  }),
+);
+
+jest.mock(
+  
'src/explore/components/controls/DateFilterControl/components/CurrentCalendarFrame',
+  () => ({
+    CurrentCalendarFrame: () => <div data-testid="current-calendar-frame" />,
+  }),
+);
+
+const VALID_RANGE = '2024-01-01 : 2024-01-31';
+
+// Default successful response that fetchTimeRange and the Apply handler both 
use
+const MOCK_TIME_RANGE_RESULT = {
+  json: {
+    result: [{ since: '2024-01-01T00:00:00', until: '2024-01-31T23:59:59' }],
+  },
+};
+
+let getSpy: jest.SpyInstance;
+
+beforeEach(() => {
+  getSpy = jest
+    .spyOn(SupersetClient, 'get')
+    .mockResolvedValue(MOCK_TIME_RANGE_RESULT as any);
+});
+
+afterEach(() => {
+  getSpy.mockRestore();
+});
+
+function renderFilter(
+  props: Partial<{
+    value: string;
+    onSubmit: jest.Mock;
+    onClose: jest.Mock;
+  }> = {},
+) {
+  const onSubmit = props.onSubmit ?? jest.fn();
+  const onClose = props.onClose ?? jest.fn();
+  return render(
+    <TimeRangeFilter
+      value={props.value ?? VALID_RANGE}
+      onSubmit={onSubmit}
+      onClose={onClose}
+    />,
+  );
+}
+
+test('renders range type label, actual time range section, and footer 
buttons', () => {
+  renderFilter();
+  expect(screen.getByText('Range type')).toBeInTheDocument();
+  expect(screen.getByText('Actual time range')).toBeInTheDocument();
+  expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
+  expect(screen.getByRole('button', { name: /apply/i })).toBeInTheDocument();
+});
+
+test('shows the custom frame when guessFrame returns Custom', () => {
+  renderFilter();
+  expect(screen.getByTestId('custom-frame')).toBeInTheDocument();
+});
+
+test('Apply is disabled until the API validates the initial value', async () 
=> {
+  // Block resolution so we can observe disabled state
+  let resolve: (v: typeof MOCK_TIME_RANGE_RESULT) => void;
+  getSpy.mockReturnValue(
+    new Promise(res => {
+      resolve = res;
+    }),
+  );
+
+  renderFilter();
+  const apply = screen.getByRole('button', { name: /apply/i });
+  expect(apply).toBeDisabled();
+
+  act(() => {
+    resolve!(MOCK_TIME_RANGE_RESULT);
+  });
+
+  await waitFor(() => {
+    expect(apply).not.toBeDisabled();
+  });
+});
+
+test('Apply is enabled when the API returns a valid result', async () => {
+  renderFilter();
+  const apply = screen.getByRole('button', { name: /apply/i });
+  await waitFor(() => {
+    expect(apply).not.toBeDisabled();
+  });
+});
+
+test('Apply is disabled when the API returns an error response', async () => {
+  getSpy.mockRejectedValue(new Error('Bad request'));
+  renderFilter();
+  const apply = screen.getByRole('button', { name: /apply/i });
+  // Give fetchTimeRange time to reject and set validTimeRange=false
+  await waitFor(() => {
+    expect(apply).toBeDisabled();
+  });
+});
+
+test('Cancel button calls onClose', async () => {
+  const onClose = jest.fn();
+  renderFilter({ onClose });
+  await userEvent.click(screen.getByRole('button', { name: /cancel/i }));
+  expect(onClose).toHaveBeenCalledTimes(1);
+});
+
+test('Apply calls onSubmit([since, until]) and onClose when API succeeds', 
async () => {
+  const onSubmit = jest.fn();
+  const onClose = jest.fn();
+
+  renderFilter({ onSubmit, onClose });
+
+  const apply = screen.getByRole('button', { name: /apply/i });
+  await waitFor(() => {
+    expect(apply).not.toBeDisabled();
+  });
+
+  await userEvent.click(apply);
+
+  await waitFor(() => {
+    expect(onSubmit).toHaveBeenCalledWith([
+      '2024-01-01T00:00:00',
+      '2024-01-31T23:59:59',
+    ]);
+  });
+  expect(onClose).toHaveBeenCalledTimes(1);
+});
+
+test('Apply calls onClose but not onSubmit when the API call throws', async () 
=> {
+  const onSubmit = jest.fn();
+  const onClose = jest.fn();
+
+  // fetchTimeRange succeeds (for validTimeRange), but the Apply API call fails
+  getSpy
+    .mockResolvedValueOnce(MOCK_TIME_RANGE_RESULT as any) // fetchTimeRange in 
useEffect
+    .mockRejectedValueOnce(new Error('network')); // Apply button API call
+
+  renderFilter({ onSubmit, onClose });
+
+  const apply = screen.getByRole('button', { name: /apply/i });
+  await waitFor(() => {
+    expect(apply).not.toBeDisabled();
+  });
+
+  await userEvent.click(apply);
+
+  await waitFor(() => {
+    expect(onClose).toHaveBeenCalledTimes(1);
+  });
+  expect(onSubmit).not.toHaveBeenCalled();
+});
+
+test('Apply with NO_TIME_RANGE calls onSubmit(undefined) and onClose without 
an API call', async () => {
+  const onSubmit = jest.fn();
+  const onClose = jest.fn();
+
+  render(
+    <TimeRangeFilter
+      value={NO_TIME_RANGE}
+      onSubmit={onSubmit}
+      onClose={onClose}
+    />,
+  );
+
+  const apply = screen.getByRole('button', { name: /apply/i });
+  await waitFor(() => {
+    expect(apply).not.toBeDisabled();
+  });
+
+  const callsBefore = getSpy.mock.calls.length;
+  await userEvent.click(apply);
+
+  expect(onSubmit).toHaveBeenCalledWith(undefined);
+  expect(onClose).toHaveBeenCalledTimes(1);
+  // No extra API call for NO_TIME_RANGE — the button short-circuits
+  expect(getSpy.mock.calls.length).toBe(callsBefore);
+});
+
+test('clearFilter via ref calls onSubmit(undefined)', async () => {
+  const onSubmit = jest.fn();
+  const ref = createRef<FilterHandler>();
+
+  render(
+    <TimeRangeFilter
+      ref={ref}
+      value={VALID_RANGE}
+      onSubmit={onSubmit}
+      onClose={jest.fn()}
+    />,
+  );
+
+  act(() => {
+    ref.current?.clearFilter();
+  });
+
+  expect(onSubmit).toHaveBeenCalledWith(undefined);
+});
diff --git a/superset-frontend/src/components/ListView/Filters/TimeRange.tsx 
b/superset-frontend/src/components/ListView/Filters/TimeRange.tsx
new file mode 100644
index 00000000000..8ecb3009588
--- /dev/null
+++ b/superset-frontend/src/components/ListView/Filters/TimeRange.tsx
@@ -0,0 +1,291 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {
+  forwardRef,
+  useEffect,
+  useImperativeHandle,
+  useMemo,
+  useState,
+  type RefObject,
+} from 'react';
+import { t } from '@apache-superset/core/translation';
+import {
+  NO_TIME_RANGE,
+  SupersetClient,
+  fetchTimeRange,
+} from '@superset-ui/core';
+import rison from 'rison';
+import { css, styled, useTheme } from '@apache-superset/core/theme';
+import {
+  Button,
+  Constants,
+  Divider,
+  Icons,
+  Select,
+} from '@superset-ui/core/components';
+import { useDebouncedEffect } from 'src/explore/exploreUtils';
+import {
+  FRAME_OPTIONS,
+  guessFrame,
+  useDefaultTimeFilter,
+} from 'src/explore/components/controls/DateFilterControl/utils';
+import {
+  AdvancedFrame,
+  CalendarFrame,
+  CommonFrame,
+  CustomFrame,
+} from 'src/explore/components/controls/DateFilterControl/components';
+import { CurrentCalendarFrame } from 
'src/explore/components/controls/DateFilterControl/components/CurrentCalendarFrame';
+import type { FrameType } from 
'src/explore/components/controls/DateFilterControl/types';
+import type { FilterHandler } from './types';
+
+interface TimeRangeFilterProps {
+  value?: string;
+  onSubmit: (value: [string, string] | undefined) => void;
+  onClose: () => void;
+}
+
+const StyledRangeType = styled(Select)`
+  width: 272px;
+`;
+
+const ContentWrapper = styled.div`
+  ${({ theme }) => css`
+    width: 600px;
+    padding: ${theme.sizeUnit * 3}px;
+    background: ${theme.colorBgElevated};
+    border-radius: ${theme.borderRadiusLG}px;
+    box-shadow: ${theme.boxShadowSecondary};
+
+    .ant-row {
+      margin-top: 8px;
+    }
+
+    .ant-picker {
+      padding: 4px 17px 4px;
+      border-radius: 4px;
+    }
+
+    .ant-divider-horizontal {
+      margin: 16px 0;
+    }
+
+    .control-label {
+      font-size: ${theme.fontSizeSM}px;
+      line-height: 16px;
+      margin: 8px 0;
+    }
+
+    .section-title {
+      font-style: normal;
+      font-weight: ${theme.fontWeightStrong};
+      font-size: 15px;
+      line-height: 24px;
+      margin-bottom: 8px;
+    }
+
+    .control-anchor-to {
+      margin-top: 16px;
+    }
+
+    .control-anchor-to-datetime {
+      width: 217px;
+    }
+
+    .footer {
+      text-align: right;
+    }
+  `}
+`;
+
+const IconWrapper = styled.span`
+  span {
+    margin-right: ${({ theme }) => 2 * theme.sizeUnit}px;
+    vertical-align: middle;
+  }
+  .text {
+    vertical-align: middle;
+  }
+  .error {
+    color: ${({ theme }) => theme.colorError};
+  }
+`;
+
+function TimeRangeFilter(
+  { value: valueProp, onSubmit, onClose }: TimeRangeFilterProps,
+  ref: RefObject<FilterHandler>,
+) {
+  const defaultTimeFilter = useDefaultTimeFilter();
+  const value = valueProp ?? defaultTimeFilter;
+  const theme = useTheme();
+
+  // guessedFrame is only used for the initial useState — value is stable at
+  // mount because CompactFilterTrigger uses destroyPopupOnHide, so the panel
+  // always mounts fresh with the current committed value.
+  const guessedFrame = useMemo(() => guessFrame(value), [value]);
+  const [frame, setFrame] = useState<FrameType>(guessedFrame);
+  const [timeRangeValue, setTimeRangeValue] = useState(value);
+  const [evalResponse, setEvalResponse] = useState(value);
+  const [validTimeRange, setValidTimeRange] = useState(false);
+  const [lastFetched, setLastFetched] = useState(value);
+
+  // Evaluate the committed value shown in "Actual time range".
+  useEffect(() => {
+    if (value === NO_TIME_RANGE) {
+      setEvalResponse(NO_TIME_RANGE);
+      setValidTimeRange(true);
+      return;
+    }
+    fetchTimeRange(value).then(({ value: actual, error }) => {
+      if (error) {
+        setEvalResponse(error ?? '');
+        setValidTimeRange(false);
+      } else {
+        setEvalResponse(actual ?? value);
+        setValidTimeRange(true);
+      }
+      setLastFetched(value);
+    });
+  }, [value]);
+
+  // Debounced evaluation of the in-progress selection (drives "Actual time 
range").
+  useDebouncedEffect(
+    () => {
+      if (timeRangeValue === NO_TIME_RANGE) {
+        setEvalResponse(NO_TIME_RANGE);
+        setLastFetched(NO_TIME_RANGE);
+        setValidTimeRange(true);
+        return;
+      }
+      if (lastFetched !== timeRangeValue) {
+        fetchTimeRange(timeRangeValue).then(({ value: actual, error }) => {
+          if (error) {
+            setEvalResponse(error ?? '');
+            setValidTimeRange(false);
+          } else {
+            setEvalResponse(actual ?? '');
+            setValidTimeRange(true);
+          }
+          setLastFetched(timeRangeValue);
+        });
+      }
+    },
+    Constants.SLOW_DEBOUNCE,
+    [timeRangeValue],
+  );
+
+  useImperativeHandle(ref, () => ({
+    clearFilter: () => {
+      onSubmit(undefined);
+    },
+  }));
+
+  function onChangeFrame(val: FrameType) {
+    if (val === NO_TIME_RANGE) {
+      setTimeRangeValue(NO_TIME_RANGE);
+    }
+    setFrame(val);
+  }
+
+  return (
+    <ContentWrapper>
+      <div className="control-label">{t('Range type')}</div>
+      <StyledRangeType
+        ariaLabel={t('Range type')}
+        options={FRAME_OPTIONS}
+        value={frame}
+        onChange={onChangeFrame}
+      />
+      {frame !== 'No filter' && <Divider />}
+      {frame === 'Common' && (
+        <CommonFrame value={timeRangeValue} onChange={setTimeRangeValue} />
+      )}
+      {frame === 'Calendar' && (
+        <CalendarFrame value={timeRangeValue} onChange={setTimeRangeValue} />
+      )}
+      {frame === 'Current' && (
+        <CurrentCalendarFrame
+          value={timeRangeValue}
+          onChange={setTimeRangeValue}
+        />
+      )}
+      {frame === 'Advanced' && (
+        <AdvancedFrame value={timeRangeValue} onChange={setTimeRangeValue} />
+      )}
+      {frame === 'Custom' && (
+        <CustomFrame value={timeRangeValue} onChange={setTimeRangeValue} />
+      )}
+      <Divider />
+      <div>
+        <div className="section-title">{t('Actual time range')}</div>
+        {validTimeRange && (
+          <div>
+            {evalResponse === NO_TIME_RANGE ? t('No filter') : evalResponse}
+          </div>
+        )}
+        {!validTimeRange && (
+          <IconWrapper className="warning">
+            <Icons.ExclamationCircleOutlined iconColor={theme.colorError} />
+            <span className="text error">{evalResponse}</span>
+          </IconWrapper>
+        )}
+      </div>
+      <Divider />
+      <div className="footer">
+        <Button buttonStyle="secondary" cta key="cancel" onClick={onClose}>
+          {t('CANCEL')}
+        </Button>
+        <Button
+          buttonStyle="primary"
+          cta
+          disabled={!validTimeRange}
+          key="apply"
+          onClick={async () => {
+            if (timeRangeValue === NO_TIME_RANGE) {
+              onSubmit(undefined);
+              onClose();
+              return;
+            }
+            // fetchTimeRange returns a formatted display string ("X ≤ col < 
Y"),
+            // not the raw since/until strings. Call the API directly to get 
them.
+            try {
+              const response = await SupersetClient.get({
+                endpoint: 
`/api/v1/time_range/?q=${rison.encode_uri(timeRangeValue)}`,
+              });
+              const since: string | undefined =
+                response?.json?.result[0]?.since;
+              const until: string | undefined =
+                response?.json?.result[0]?.until;
+              if (since !== undefined && until !== undefined) {
+                onSubmit([since, until]);
+              }
+            } catch {
+              // leave filter unchanged on error
+            }
+            onClose();
+          }}
+        >
+          {t('APPLY')}
+        </Button>
+      </div>
+    </ContentWrapper>
+  );
+}
+
+export default forwardRef(TimeRangeFilter);
diff --git a/superset-frontend/src/components/ListView/Filters/index.test.tsx 
b/superset-frontend/src/components/ListView/Filters/index.test.tsx
index 1d75dbe9178..c93e3540b4b 100644
--- a/superset-frontend/src/components/ListView/Filters/index.test.tsx
+++ b/superset-frontend/src/components/ListView/Filters/index.test.tsx
@@ -17,6 +17,7 @@
  * under the License.
  */
 import { render, screen } from 'spec/helpers/testing-library';
+import userEvent from '@testing-library/user-event';
 import { ListViewFilterOperator } from '../types';
 import UIFilters from './index';
 
@@ -97,7 +98,335 @@ test('search filter passes autoComplete prop correctly', () 
=> {
   expect(input.autocomplete).toBe('new-password');
 });
 
-test('renders multiple search filters with different inputName values', () => {
+test('renders a compact pill trigger for select filters', () => {
+  const filters = [
+    {
+      Header: 'Owner',
+      key: 'owner',
+      id: 'owner',
+      input: 'select' as const,
+      operator: ListViewFilterOperator.RelationOneMany,
+      selects: [
+        { label: 'Alice', value: 1 },
+        { label: 'Bob', value: 2 },
+      ],
+    },
+  ];
+
+  render(
+    <UIFilters
+      filters={filters}
+      internalFilters={[]}
+      updateFilterValue={mockUpdateFilterValue}
+    />,
+  );
+
+  expect(screen.getByTestId('compact-filter-pill')).toBeInTheDocument();
+  expect(screen.getByText('Owner')).toBeInTheDocument();
+});
+
+test('select pill shows active state (clear button) when a value is selected', 
() => {
+  const filters = [
+    {
+      Header: 'Owner',
+      key: 'owner',
+      id: 'owner',
+      input: 'select' as const,
+      operator: ListViewFilterOperator.RelationOneMany,
+      selects: [{ label: 'Alice', value: 1 }],
+    },
+  ];
+
+  render(
+    <UIFilters
+      filters={filters}
+      internalFilters={[
+        {
+          id: 'owner',
+          operator: ListViewFilterOperator.RelationOneMany,
+          value: { label: 'Alice', value: 1 },
+        },
+      ]}
+      updateFilterValue={mockUpdateFilterValue}
+    />,
+  );
+
+  expect(
+    screen.getByRole('button', { name: /clear owner filter/i }),
+  ).toBeInTheDocument();
+});
+
+test('select pill tooltip falls back to static selects on cold URL load (no 
cached label)', () => {
+  const filters = [
+    {
+      Header: 'Owner',
+      key: 'owner',
+      id: 'owner',
+      input: 'select' as const,
+      operator: ListViewFilterOperator.RelationOneMany,
+      selects: [
+        { label: 'Alice', value: 1 },
+        { label: 'Bob', value: 2 },
+      ],
+    },
+  ];
+
+  // Simulate cold URL load: value has only numeric value, no label in cache
+  render(
+    <UIFilters
+      filters={filters}
+      internalFilters={[
+        {
+          id: 'owner',
+          operator: ListViewFilterOperator.RelationOneMany,
+          value: { value: 1 } as any,
+        },
+      ]}
+      updateFilterValue={mockUpdateFilterValue}
+    />,
+  );
+
+  // The pill should be active (clear button visible) and the static label
+  // should be resolved as the tooltip source
+  expect(
+    screen.getByRole('button', { name: /clear owner filter/i }),
+  ).toBeInTheDocument();
+});
+
+test('datetime_range filter renders as CompactFilterTrigger with dialog 
aria-haspopup', () => {
+  const filters = [
+    {
+      Header: 'Time range',
+      key: 'time_range',
+      id: 'time_range',
+      input: 'datetime_range' as const,
+      operator: ListViewFilterOperator.Between,
+    },
+  ];
+
+  render(
+    <UIFilters
+      filters={filters}
+      internalFilters={[]}
+      updateFilterValue={mockUpdateFilterValue}
+    />,
+  );
+
+  const pill = screen.getByTestId('compact-filter-pill');
+  expect(pill).toBeInTheDocument();
+  expect(pill).toHaveAttribute('aria-haspopup', 'dialog');
+  expect(screen.getByText('Time range')).toBeInTheDocument();
+});
+
+test('datetime_range pill shows active state when a time range string is set', 
() => {
+  const filters = [
+    {
+      Header: 'Time range',
+      key: 'time_range',
+      id: 'time_range',
+      input: 'datetime_range' as const,
+      operator: ListViewFilterOperator.Between,
+    },
+  ];
+
+  render(
+    <UIFilters
+      filters={filters}
+      internalFilters={[
+        {
+          id: 'time_range',
+          operator: ListViewFilterOperator.Between,
+          value: 'Last week',
+        },
+      ]}
+      updateFilterValue={mockUpdateFilterValue}
+    />,
+  );
+
+  // Clear icon is inside the pill (not a separate button)
+  const pill = screen.getByTestId('compact-filter-pill');
+  const clearIcon = screen.getByTestId('compact-filter-clear');
+  expect(clearIcon).toBeInTheDocument();
+  expect(pill).toContainElement(clearIcon);
+});
+
+test('datetime_range pill is inactive when value is NO_TIME_RANGE', () => {
+  const filters = [
+    {
+      Header: 'Time range',
+      key: 'time_range',
+      id: 'time_range',
+      input: 'datetime_range' as const,
+      operator: ListViewFilterOperator.Between,
+    },
+  ];
+
+  render(
+    <UIFilters
+      filters={filters}
+      internalFilters={[
+        {
+          id: 'time_range',
+          operator: ListViewFilterOperator.Between,
+          value: 'No filter',
+        },
+      ]}
+      updateFilterValue={mockUpdateFilterValue}
+    />,
+  );
+
+  expect(screen.queryByTestId('compact-filter-clear')).not.toBeInTheDocument();
+});
+
+test('datetime_range pill shows the time range string as tooltip title', () => 
{
+  const filters = [
+    {
+      Header: 'Time range',
+      key: 'time_range',
+      id: 'time_range',
+      input: 'datetime_range' as const,
+      operator: ListViewFilterOperator.Between,
+    },
+  ];
+
+  render(
+    <UIFilters
+      filters={filters}
+      internalFilters={[
+        {
+          id: 'time_range',
+          operator: ListViewFilterOperator.Between,
+          value: 'Last month',
+        },
+      ]}
+      updateFilterValue={mockUpdateFilterValue}
+    />,
+  );
+
+  // Pill is active and clear icon is inside
+  expect(screen.getByTestId('compact-filter-clear')).toBeInTheDocument();
+});
+
+test('numerical_range filter renders as CompactFilterTrigger with dialog 
aria-haspopup', () => {
+  const filters = [
+    {
+      Header: 'Age range',
+      key: 'age_range',
+      id: 'age_range',
+      input: 'numerical_range' as const,
+      operator: ListViewFilterOperator.Between,
+    },
+  ];
+
+  render(
+    <UIFilters
+      filters={filters}
+      internalFilters={[]}
+      updateFilterValue={mockUpdateFilterValue}
+    />,
+  );
+
+  const pill = screen.getByTestId('compact-filter-pill');
+  expect(pill).toBeInTheDocument();
+  expect(pill).toHaveAttribute('aria-haspopup', 'dialog');
+  expect(screen.getByText('Age range')).toBeInTheDocument();
+});
+
+test('numerical_range pill shows active state when value is set', () => {
+  const filters = [
+    {
+      Header: 'Age range',
+      key: 'age_range',
+      id: 'age_range',
+      input: 'numerical_range' as const,
+      operator: ListViewFilterOperator.Between,
+    },
+  ];
+
+  render(
+    <UIFilters
+      filters={filters}
+      internalFilters={[
+        {
+          id: 'age_range',
+          operator: ListViewFilterOperator.Between,
+          value: [18, 65],
+        },
+      ]}
+      updateFilterValue={mockUpdateFilterValue}
+    />,
+  );
+
+  expect(
+    screen.getByRole('button', { name: /clear age range filter/i }),
+  ).toBeInTheDocument();
+});
+
+test('datetime_range onClear calls updateFilterValue with undefined directly', 
async () => {
+  const updateFilterValue = jest.fn();
+  const filters = [
+    {
+      Header: 'Time range',
+      key: 'time_range',
+      id: 'time_range',
+      input: 'datetime_range' as const,
+      operator: ListViewFilterOperator.Between,
+    },
+  ];
+
+  render(
+    <UIFilters
+      filters={filters}
+      internalFilters={[
+        {
+          id: 'time_range',
+          operator: ListViewFilterOperator.Between,
+          value: 'Last week',
+        },
+      ]}
+      updateFilterValue={updateFilterValue}
+    />,
+  );
+
+  const clearIcon = screen.getByTestId('compact-filter-clear');
+  await userEvent.click(clearIcon);
+  expect(updateFilterValue).toHaveBeenCalledWith(0, undefined);
+});
+
+test('numerical_range onClear calls updateFilterValue with undefined 
directly', async () => {
+  const updateFilterValue = jest.fn();
+  const filters = [
+    {
+      Header: 'Age range',
+      key: 'age_range',
+      id: 'age_range',
+      input: 'numerical_range' as const,
+      operator: ListViewFilterOperator.Between,
+    },
+  ];
+
+  render(
+    <UIFilters
+      filters={filters}
+      internalFilters={[
+        {
+          id: 'age_range',
+          operator: ListViewFilterOperator.Between,
+          value: [18, 65],
+        },
+      ]}
+      updateFilterValue={updateFilterValue}
+    />,
+  );
+
+  const clearBtn = screen.getByRole('button', {
+    name: /clear age range filter/i,
+  });
+  await userEvent.click(clearBtn);
+  expect(updateFilterValue).toHaveBeenCalledWith(0, undefined);
+});
+
+test('renders only the first search filter when multiple search filters are 
configured', () => {
   const filters = [
     {
       Header: 'Name',
@@ -125,8 +454,8 @@ test('renders multiple search filters with different 
inputName values', () => {
     />,
   );
 
+  // Only the first search filter renders — one search box per page
   const inputs = screen.getAllByTestId('filters-search') as HTMLInputElement[];
-  expect(inputs).toHaveLength(2);
+  expect(inputs).toHaveLength(1);
   expect(inputs[0].name).toBe('filter_name_search');
-  expect(inputs[1].name).toBe('description');
 });
diff --git a/superset-frontend/src/components/ListView/Filters/index.tsx 
b/superset-frontend/src/components/ListView/Filters/index.tsx
index b76c9e4bc2c..07642aa3275 100644
--- a/superset-frontend/src/components/ListView/Filters/index.tsx
+++ b/superset-frontend/src/components/ListView/Filters/index.tsx
@@ -19,12 +19,16 @@
 import {
   createRef,
   forwardRef,
+  useCallback,
+  useEffect,
   useImperativeHandle,
   useMemo,
+  useState,
   RefObject,
 } from 'react';
 
 import { withTheme } from '@apache-superset/core/theme';
+import { t } from '@apache-superset/core/translation';
 
 import type {
   ListViewFilterValue as FilterValue,
@@ -33,10 +37,13 @@ import type {
   SelectOption,
 } from '../types';
 import type { FilterHandler } from './types';
+import { NO_TIME_RANGE } from '@superset-ui/core';
 import SearchFilter from './Search';
-import SelectFilter from './Select';
-import DateRangeFilter from './DateRange';
 import NumericalRangeFilter from './NumericalRange';
+import TimeRangeFilter from './TimeRange';
+import CompactFilterTrigger from './CompactFilterTrigger';
+import CompactSelectPanel from './CompactSelectPanel';
+import FilterPopoverContent from './FilterPopoverContent';
 
 interface UIFiltersProps {
   filters: Filters;
@@ -46,7 +53,10 @@ interface UIFiltersProps {
 
 function UIFilters(
   { filters, internalFilters = [], updateFilterValue }: UIFiltersProps,
-  ref: RefObject<{ clearFilters: () => void }>,
+  ref: RefObject<{
+    clearFilters: () => void;
+    clearFilterById: (id: string) => void;
+  }>,
 ) {
   const filterRefs = useMemo(
     () =>
@@ -54,125 +64,320 @@ function UIFilters(
     [filters.length],
   );
 
+  // Cache display labels for select filters so tooltip works after URL 
round-trip
+  // (URL serialization strips the label, leaving only the value).
+  const [tooltipLabels, setTooltipLabels] = useState<Record<number, string>>(
+    {},
+  );
+
+  // Evaluated human-readable labels for datetime_range pills (e.g. 
"2024-05-01 : 2024-05-31").
+  const [timeRangeTooltips, setTimeRangeTooltips] = useState<
+    Record<number, string>
+  >({});
+
+  // On cold load, URL params restore values but not labels for fetchSelects 
filters.
+  // Fetch the first page of options and cache the matching label so the 
tooltip works.
+  useEffect(() => {
+    filters.forEach((filter, index) => {
+      if (filter.input !== 'select' || !filter.fetchSelects) return;
+      if (tooltipLabels[index]) return;
+      const val = internalFilters?.[index]?.value as SelectOption | undefined;
+      if (!val?.value) return;
+      filter.fetchSelects('', 0, 500).then(result => {
+        const match = result?.data?.find(
+          (s: SelectOption) => s.value === val.value,
+        );
+        if (match) {
+          const lbl =
+            typeof match.label === 'string'
+              ? match.label
+              : String(match.value ?? '');
+          setTooltipLabels(prev => ({ ...prev, [index]: lbl }));
+        }
+      });
+    });
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [internalFilters]);
+
+  // Build datetime_range tooltips from the resolved [start, end] array value.
+  // Handles both ISO strings and unix-ms numbers.
+  useEffect(() => {
+    filters.forEach((filter, index) => {
+      if (filter.input !== 'datetime_range') return;
+      const val = internalFilters?.[index]?.value;
+      if (Array.isArray(val) && val.length === 2) {
+        const fmt = (v: unknown) => {
+          const d = new Date(v as string | number);
+          return isNaN(d.getTime())
+            ? String(v)
+            : d.toISOString().replace('T', ' ').slice(0, 19);
+        };
+        const tooltip = `${fmt(val[0])} – ${fmt(val[1])}`;
+        setTimeRangeTooltips(prev =>
+          prev[index] === tooltip ? prev : { ...prev, [index]: tooltip },
+        );
+      } else {
+        setTimeRangeTooltips(prev => {
+          if (!(index in prev)) return prev;
+          const next = { ...prev };
+          delete next[index];
+          return next;
+        });
+      }
+    });
+  }, [filters, internalFilters]);
+
+  const clearFilterAtIndex = useCallback(
+    (index: number) => {
+      filterRefs[index]?.current?.clearFilter?.();
+      updateFilterValue(index, undefined);
+      setTooltipLabels(prev => {
+        const next = { ...prev };
+        delete next[index];
+        return next;
+      });
+    },
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+    [updateFilterValue],
+  );
+
   useImperativeHandle(ref, () => ({
     clearFilters: () => {
-      filterRefs.forEach((filter: any) => {
-        filter.current?.clearFilter?.();
+      filterRefs.forEach((_, index) => {
+        filterRefs[index]?.current?.clearFilter?.();
+        updateFilterValue(index, undefined);
       });
+      setTooltipLabels({});
+      setTimeRangeTooltips({});
     },
     clearFilterById: (id: string) => {
       const index = filters.findIndex(f => f.id === id);
       if (index >= 0) {
-        filterRefs[index]?.current?.clearFilter?.();
+        clearFilterAtIndex(index);
       }
     },
   }));
 
-  return (
-    <>
-      {filters.map(
-        (
-          {
-            Header,
-            fetchSelects,
-            key,
-            id,
-            input,
-            optionFilterProps,
-            paginate,
-            selects,
-            toolTipDescription,
-            onFilterUpdate,
-            loading,
-            dateFilterValueType,
-            min,
-            max,
-            popupStyle,
-            autoComplete,
-            inputName,
-          },
-          index,
-        ) => {
-          const initialValue = internalFilters?.[index]?.value;
-          if (input === 'select') {
-            return (
-              <SelectFilter
+  // Search always leads the filter bar regardless of declaration order.
+  // Only the first search filter renders; subsequent ones are skipped (see 
note below).
+  // NOTE: This means secondary search fields (e.g. Email/Username on Users,
+  // Group Key on RLS) are not currently accessible via the filter bar. Those
+  // pages previously relied on multiple inline inputs. This is a known UX
+  // trade-off — revisit if admin workflows require additional search fields.
+  let searchFilterRendered = false;
+
+  // Render in two passes: search first, then all other filter types.
+  const renderFilter = (_: (typeof filters)[number], index: number) => {
+    const {
+      Header,
+      fetchSelects,
+      key,
+      id,
+      input,
+      selects,
+      toolTipDescription,
+      onFilterUpdate,
+      loading,
+      min,
+      max,
+      autoComplete,
+      inputName,
+      popupStyle,
+      dateFilterValueType,
+    } = filters[index];
+    const initialValue = internalFilters?.[index]?.value;
+    if (input === 'select') {
+      const selectValue = initialValue as SelectOption | undefined;
+      // Prefer cached label (survives URL round-trips where only the value
+      // is preserved). Fall back to the static selects list for cold loads.
+      const cachedLabel = tooltipLabels[index];
+      const staticFallback = cachedLabel
+        ? undefined
+        : selects?.find(s => s.value === selectValue?.value)?.label;
+      const tooltipTitle = !!selectValue
+        ? cachedLabel ||
+          (typeof staticFallback === 'string' ? staticFallback : undefined)
+        : t('Choose...');
+      return (
+        <span key={key} data-test="select-filter-container">
+          <CompactFilterTrigger
+            label={Header}
+            hasValue={!!selectValue}
+            tooltipTitle={tooltipTitle}
+            onClear={() => clearFilterAtIndex(index)}
+          >
+            {({ isOpen, onClose }) => (
+              <CompactSelectPanel
                 ref={filterRefs[index]}
-                Header={Header}
+                selects={selects}
                 fetchSelects={fetchSelects}
-                initialValue={initialValue}
-                key={key}
-                name={id}
+                value={initialValue as SelectOption | undefined}
+                loading={loading ?? false}
+                isOpen={isOpen}
+                onClose={onClose}
+                panelStyle={popupStyle}
                 onSelect={(
                   option: SelectOption | undefined,
                   isClear?: boolean,
                 ) => {
-                  if (onFilterUpdate) {
-                    // Filter change triggers both onChange AND onClear, only 
want to track onChange
-                    if (!isClear) {
-                      onFilterUpdate(option);
-                    }
+                  if (option && !isClear) {
+                    setTooltipLabels(prev => ({
+                      ...prev,
+                      [index]:
+                        typeof option.label === 'string'
+                          ? option.label
+                          : String(option.value ?? ''),
+                    }));
+                  }
+                  if (onFilterUpdate && !isClear) {
+                    onFilterUpdate(option);
                   }
-
                   updateFilterValue(index, option);
                 }}
-                optionFilterProps={optionFilterProps}
-                paginate={paginate}
-                selects={selects}
-                loading={loading ?? false}
-                dropdownStyle={popupStyle}
               />
-            );
-          }
-          if (input === 'search' && typeof Header === 'string') {
-            return (
-              <SearchFilter
-                ref={filterRefs[index]}
-                Header={Header}
-                initialValue={initialValue}
-                key={key}
-                name={inputName ?? id}
-                toolTipDescription={toolTipDescription}
-                onSubmit={(value: string) => {
-                  if (onFilterUpdate) {
-                    onFilterUpdate(value);
-                  }
+            )}
+          </CompactFilterTrigger>
+        </span>
+      );
+    }
+    if (input === 'search' && typeof Header === 'string') {
+      if (searchFilterRendered) return null;
+      searchFilterRendered = true;
+      return (
+        <SearchFilter
+          ref={filterRefs[index]}
+          Header={Header}
+          initialValue={initialValue}
+          key={key}
+          name={inputName ?? id}
+          toolTipDescription={toolTipDescription}
+          onSubmit={(value: string) => {
+            if (onFilterUpdate) {
+              onFilterUpdate(value);
+            }
 
-                  updateFilterValue(index, value);
-                }}
-                autoComplete={autoComplete}
-              />
-            );
-          }
-          if (input === 'datetime_range') {
-            return (
-              <DateRangeFilter
-                ref={filterRefs[index]}
-                Header={Header}
-                initialValue={initialValue}
-                key={key}
-                name={id}
-                onSubmit={value => updateFilterValue(index, value)}
-                dateFilterValueType={dateFilterValueType || 'unix'}
-              />
-            );
+            updateFilterValue(index, value);
+          }}
+          autoComplete={autoComplete}
+        />
+      );
+    }
+    if (input === 'datetime_range') {
+      // dateFilterValueType absent or 'unix': column stores unix ms (e.g. 
Query History start_time).
+      // 'iso': column stores ISO date strings (e.g. UsersList created_on, 
ActionLog dttm).
+      const isUnixType = !dateFilterValueType || dateFilterValueType === 
'unix';
+
+      // initialValue may be [ms, ms] (unix), ["iso","iso"] (iso), or legacy 
string.
+      // Always reconstruct panelValue as "ISO : ISO" so the TimeRange panel
+      // can parse it as a Custom date range regardless of storage type.
+      let resolvedIsoRange: [string, string] | null = null;
+      if (Array.isArray(initialValue) && initialValue.length === 2) {
+        if (typeof initialValue[0] === 'number') {
+          resolvedIsoRange = [
+            new Date(initialValue[0]).toISOString(),
+            new Date(initialValue[1] as number).toISOString(),
+          ];
+        } else if (typeof initialValue[0] === 'string') {
+          resolvedIsoRange = initialValue as [string, string];
+        }
+      }
+      const legacyStringVal =
+        !resolvedIsoRange &&
+        typeof initialValue === 'string' &&
+        initialValue !== NO_TIME_RANGE
+          ? initialValue
+          : null;
+      const hasTimeValue = !!(resolvedIsoRange || legacyStringVal);
+      const panelValue =
+        resolvedIsoRange?.join(' : ') ?? legacyStringVal ?? undefined;
+      return (
+        <CompactFilterTrigger
+          key={key}
+          label={Header}
+          hasValue={hasTimeValue}
+          tooltipTitle={
+            hasTimeValue ? (timeRangeTooltips[index] ?? panelValue) : undefined
           }
-          if (input === 'numerical_range') {
-            return (
+          popupType="dialog"
+          onClear={() => {
+            updateFilterValue(index, undefined);
+          }}
+        >
+          {({ onClose }) => (
+            <TimeRangeFilter
+              ref={filterRefs[index]}
+              value={panelValue}
+              onClose={onClose}
+              onSubmit={value => {
+                if (!value) {
+                  updateFilterValue(index, undefined);
+                } else if (isUnixType) {
+                  // Convert ISO strings to unix ms for numeric columns
+                  updateFilterValue(index, [
+                    new Date(value[0]).getTime(),
+                    new Date(value[1]).getTime(),
+                  ]);
+                } else {
+                  updateFilterValue(index, value);
+                }
+              }}
+            />
+          )}
+        </CompactFilterTrigger>
+      );
+    }
+    if (input === 'numerical_range') {
+      const hasRangeValue =
+        Array.isArray(initialValue) &&
+        initialValue.some(v => v !== null && v !== undefined);
+      const rangeTooltip = hasRangeValue
+        ? (initialValue as (number | null | undefined)[])
+            .filter(v => v !== null && v !== undefined)
+            .join(' – ')
+        : undefined;
+      return (
+        <CompactFilterTrigger
+          key={key}
+          label={Header}
+          hasValue={hasRangeValue}
+          tooltipTitle={rangeTooltip}
+          popupType="dialog"
+          onClear={() => {
+            updateFilterValue(index, undefined);
+          }}
+        >
+          {({ onClose }) => (
+            <FilterPopoverContent onClose={onClose}>
               <NumericalRangeFilter
                 ref={filterRefs[index]}
                 Header={Header}
                 initialValue={initialValue}
                 min={min}
                 max={max}
-                key={key}
                 name={id}
                 onSubmit={value => updateFilterValue(index, value)}
               />
-            );
-          }
-          return null;
-        },
+            </FilterPopoverContent>
+          )}
+        </CompactFilterTrigger>
+      );
+    }
+    return null;
+  };
+
+  return (
+    <>
+      {/* Search first */}
+      {filters.map((_, index) =>
+        filters[index].input === 'search'
+          ? renderFilter(filters[index], index)
+          : null,
+      )}
+      {/* Then all other filter types */}
+      {filters.map((_, index) =>
+        filters[index].input !== 'search'
+          ? renderFilter(filters[index], index)
+          : null,
       )}
     </>
   );
diff --git a/superset-frontend/src/components/ListView/ListView.test.tsx 
b/superset-frontend/src/components/ListView/ListView.test.tsx
index f278e607ac6..0d08bcf9fd9 100644
--- a/superset-frontend/src/components/ListView/ListView.test.tsx
+++ b/superset-frontend/src/components/ListView/ListView.test.tsx
@@ -301,15 +301,19 @@ describe('ListView', () => {
   });
 
   test('renders UI filters', () => {
-    const filterControls = screen.getAllByRole('combobox');
-    expect(filterControls).toHaveLength(2);
+    // select and datetime_range filters render as compact pill buttons;
+    // search filter renders as a text input
+    const filterPills = screen.getAllByTestId('compact-filter-pill');
+    expect(filterPills).toHaveLength(3); // ID, Age, Time
   });
 
   test('calls fetchData on filter', async () => {
-    // Handle select filter
-    const selectFilter = screen.getAllByRole('combobox')[0];
-    await userEvent.click(selectFilter);
-    const option = screen.getByText('foo');
+    // Click the ID compact pill to open its option panel
+    const idPill = screen.getByRole('button', { name: 'ID' });
+    await userEvent.click(idPill);
+
+    // Wait for and click the 'foo' option in the dropdown panel
+    const option = await screen.findByRole('option', { name: 'foo' });
     await userEvent.click(option);
 
     // Handle search filter
@@ -341,7 +345,10 @@ describe('ListView', () => {
       initialSort: [{ id: 'something' }],
     });
 
-    const sortSelect = screen.getByTestId('card-sort-select');
+    const sortSelectContainer = screen.getByTestId('card-sort-select');
+    const sortSelect = sortSelectContainer.querySelector(
+      '[data-test="compact-filter-pill"]',
+    ) as HTMLElement;
     await userEvent.click(sortSelect);
 
     const sortOption = screen.getByText('Alphabetical');
diff --git a/superset-frontend/src/components/ListView/ListView.tsx 
b/superset-frontend/src/components/ListView/ListView.tsx
index 9c1f313bd68..b77c402606e 100644
--- a/superset-frontend/src/components/ListView/ListView.tsx
+++ b/superset-frontend/src/components/ListView/ListView.tsx
@@ -65,13 +65,43 @@ const ListViewStyles = styled.div`
 
       .header {
         display: flex;
+        align-items: center;
         padding-bottom: ${theme.sizeUnit * 4}px;
 
         & .controls {
           display: flex;
           flex-wrap: wrap;
-          column-gap: ${theme.sizeUnit * 7}px;
-          row-gap: ${theme.sizeUnit * 4}px;
+          align-items: center;
+          column-gap: ${theme.sizeUnit * 2}px;
+          row-gap: ${theme.sizeUnit * 2}px;
+
+          /* Search input — fixed width/height matching pill height, label 
hidden */
+          [data-test='search-filter-container'] {
+            width: ${theme.sizeUnit * 44}px;
+            flex-shrink: 0;
+            height: ${theme.controlHeight}px;
+            align-self: center;
+            /* Hide the FormLabel Flex wrapper entirely so it doesn't affect
+               the column's justify-content centering calculation. */
+            > .ant-flex {
+              display: none;
+            }
+            label {
+              display: none;
+            }
+            .ant-input-affix-wrapper {
+              width: 100%;
+              height: 100%;
+            }
+          }
+
+          /* Select filter pill wrappers — make them proper flex items so the
+             inline-flex button inside doesn't introduce line-box quirks. */
+          [data-test='select-filter-container'] {
+            display: flex;
+            align-items: center;
+            align-self: center;
+          }
         }
       }
 
@@ -167,7 +197,6 @@ const bulkSelectColumnConfig = {
 const ViewModeContainer = styled.div`
   ${({ theme }) => `
     padding-right: ${theme.sizeUnit * 4}px;
-    margin-top: ${theme.sizeUnit * 5 + 1}px;
     white-space: nowrap;
     display: inline-block;
 
@@ -192,6 +221,29 @@ const ViewModeContainer = styled.div`
   `}
 `;
 
+const ClearAllButton = styled.button`
+  ${({ theme }) => `
+    background: none;
+    border: none;
+    padding: 0 ${theme.sizeUnit}px;
+    color: ${theme.colorPrimary};
+    font-size: ${theme.fontSizeSM}px;
+    cursor: pointer;
+    white-space: nowrap;
+    line-height: ${theme.controlHeight}px;
+
+    &:hover:not(:disabled) {
+      color: ${theme.colorPrimaryHover};
+      text-decoration: underline;
+    }
+
+    &:disabled {
+      color: ${theme.colorTextDisabled};
+      cursor: not-allowed;
+    }
+  `}
+`;
+
 const EmptyWrapper = styled.div`
   ${({ theme }) => `
     padding: ${theme.sizeUnit * 40}px 0;
@@ -356,6 +408,14 @@ export function ListView<T extends object = any>({
     clearFilterById: (id: string) => void;
   }>(null);
 
+  const hasActiveFilters = internalFilters.some(f => {
+    if (f.value === null || f.value === undefined || f.value === '')
+      return false;
+    if (Array.isArray(f.value))
+      return f.value.some(v => v !== null && v !== undefined && v !== '');
+    return true;
+  });
+
   // Wire the optional external filtersRef to our internal filterControlsRef.
   // useLayoutEffect fires synchronously after DOM mutations, guaranteeing the
   // ref is populated before the first paint and after every update.
@@ -421,6 +481,21 @@ export function ListView<T extends object = any>({
                 options={cardSortSelectOptions}
               />
             )}
+            {filterable && (
+              <Tooltip
+                title={!hasActiveFilters ? t('No filters applied') : undefined}
+              >
+                <span>
+                  <ClearAllButton
+                    type="button"
+                    disabled={!hasActiveFilters}
+                    onClick={() => filterControlsRef.current?.clearFilters()}
+                  >
+                    {t('Clear all')}
+                  </ClearAllButton>
+                </span>
+              </Tooltip>
+            )}
           </div>
         </div>
         <div className={`body ${rows.length === 0 ? 'empty' : ''} `}>
diff --git a/superset-frontend/src/pages/ChartList/ChartList.test.tsx 
b/superset-frontend/src/pages/ChartList/ChartList.test.tsx
index 6f9dedc5a09..1b16c7674b7 100644
--- a/superset-frontend/src/pages/ChartList/ChartList.test.tsx
+++ b/superset-frontend/src/pages/ChartList/ChartList.test.tsx
@@ -57,9 +57,12 @@ const mockUser = {
 const findFilterByLabel = (labelText: string) => {
   const containers = screen.getAllByTestId('select-filter-container');
   for (const container of containers) {
-    const label = container.querySelector('label');
-    if (label?.textContent === labelText) {
-      return container.querySelector('[role="combobox"], .ant-select');
+    // Compact pill filters show the label as button text
+    const pill = container.querySelector(
+      '[data-test="compact-filter-pill"]',
+    ) as HTMLElement | null;
+    if (pill && pill.textContent?.includes(labelText)) {
+      return pill;
     }
   }
   return null;
diff --git 
a/superset-frontend/src/pages/DashboardList/DashboardList.cardview.test.tsx 
b/superset-frontend/src/pages/DashboardList/DashboardList.cardview.test.tsx
index c03299cfc43..7733251176d 100644
--- a/superset-frontend/src/pages/DashboardList/DashboardList.cardview.test.tsx
+++ b/superset-frontend/src/pages/DashboardList/DashboardList.cardview.test.tsx
@@ -156,18 +156,16 @@ describe('DashboardList Card View Tests', () => {
       ).toBeInTheDocument();
     });
 
-    // Find the sort select by its testId, then the combobox within it
+    // Find the sort select by its testId, then the pill button within it
     const sortContainer = screen.getByTestId('card-sort-select');
-    const sortCombobox = within(sortContainer).getByRole('combobox');
-    await userEvent.click(sortCombobox);
+    // eslint-disable-next-line testing-library/no-node-access
+    const sortPill = sortContainer.querySelector(
+      '[data-test="compact-filter-pill"]',
+    ) as HTMLElement;
+    await userEvent.click(sortPill);
 
     // Select "Alphabetical" from the dropdown
-    const alphabeticalOption = await waitFor(() =>
-      within(
-        // eslint-disable-next-line testing-library/no-node-access
-        document.querySelector('.rc-virtual-list')!,
-      ).getByText('Alphabetical'),
-    );
+    const alphabeticalOption = await screen.findByText('Alphabetical');
     await userEvent.click(alphabeticalOption);
 
     await waitFor(() => {
diff --git a/superset-frontend/src/pages/DashboardList/DashboardList.test.tsx 
b/superset-frontend/src/pages/DashboardList/DashboardList.test.tsx
index 2f4b8f9dc5e..87c974df801 100644
--- a/superset-frontend/src/pages/DashboardList/DashboardList.test.tsx
+++ b/superset-frontend/src/pages/DashboardList/DashboardList.test.tsx
@@ -20,7 +20,7 @@ import fetchMock from 'fetch-mock';
 import { isFeatureEnabled } from '@superset-ui/core';
 import {
   screen,
-  selectOption,
+  selectPillOption,
   waitFor,
   fireEvent,
 } from 'spec/helpers/testing-library';
@@ -200,7 +200,7 @@ test('selecting Status filter encodes published=true in API 
call', async () => {
     ).toBeInTheDocument();
   });
 
-  await selectOption('Published', 'Status');
+  await selectPillOption('Published', 'Status');
 
   await waitFor(() => {
     const latest = getLatestDashboardApiCall();
@@ -242,7 +242,7 @@ test('selecting Owner filter encodes rel_m_m owner in API 
call', async () => {
     ).toBeInTheDocument();
   });
 
-  await selectOption('Admin User', 'Owner');
+  await selectPillOption('Admin User', 'Owner');
 
   await waitFor(() => {
     const latest = getLatestDashboardApiCall();
@@ -287,7 +287,7 @@ test('selecting Modified by filter encodes rel_o_m 
changed_by in API call', asyn
     ).toBeInTheDocument();
   });
 
-  await selectOption('Admin User', 'Modified by');
+  await selectPillOption('Admin User', 'Modified by');
 
   await waitFor(() => {
     const latest = getLatestDashboardApiCall();
diff --git 
a/superset-frontend/src/pages/DatasetList/DatasetList.integration.test.tsx 
b/superset-frontend/src/pages/DatasetList/DatasetList.integration.test.tsx
index 2a28b7bb11c..ee654a77458 100644
--- a/superset-frontend/src/pages/DatasetList/DatasetList.integration.test.tsx
+++ b/superset-frontend/src/pages/DatasetList/DatasetList.integration.test.tsx
@@ -20,7 +20,7 @@ import { act, screen, waitFor, within } from 
'@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import fetchMock from 'fetch-mock';
 import rison from 'rison';
-import { selectOption } from 'spec/helpers/testing-library';
+import { selectPillOption } from 'spec/helpers/testing-library';
 import {
   setupMocks,
   renderDatasetList,
@@ -102,11 +102,11 @@ test('ListView provider correctly merges filter + sort + 
pagination state on ref
     ).toBeGreaterThan(callsBeforeSort);
   });
 
-  // 2. Apply a filter using selectOption helper
+  // 2. Apply a filter using selectPillOption helper (compact pill UI)
   const beforeFilterCallCount = fetchMock.callHistory.calls(
     API_ENDPOINTS.DATASOURCE_COMBINED,
   ).length;
-  await selectOption('Virtual', 'Type');
+  await selectPillOption('Virtual', 'Type');
 
   // Wait for filter API call to complete
   await waitFor(() => {
diff --git 
a/superset-frontend/src/pages/DatasetList/DatasetList.listview.test.tsx 
b/superset-frontend/src/pages/DatasetList/DatasetList.listview.test.tsx
index 883658156c9..6ad588a8128 100644
--- a/superset-frontend/src/pages/DatasetList/DatasetList.listview.test.tsx
+++ b/superset-frontend/src/pages/DatasetList/DatasetList.listview.test.tsx
@@ -27,7 +27,7 @@ import userEvent from '@testing-library/user-event';
 import fetchMock from 'fetch-mock';
 import rison from 'rison';
 import { SupersetClient } from '@superset-ui/core';
-import { selectOption } from 'spec/helpers/testing-library';
+import { selectPillOption } from 'spec/helpers/testing-library';
 import {
   setupMocks,
   renderDatasetList,
@@ -1510,11 +1510,8 @@ test('bulk selection clears when filter changes', async 
() => {
     API_ENDPOINTS.DATASOURCE_COMBINED,
   ).length;
 
-  // Wait for filter combobox to be ready before applying filter
-  await screen.findByRole('combobox', { name: 'Type' });
-
-  // Apply a filter using selectOption helper
-  await selectOption('Virtual', 'Type');
+  // Apply a filter using selectPillOption helper (compact pill UI)
+  await selectPillOption('Virtual', 'Type');
 
   // Wait for filter API call to complete
   await waitFor(() => {
@@ -1556,16 +1553,13 @@ test('type filter API call includes correct filter 
parameter', async () => {
     expect(screen.getByTestId('listview-table')).toBeInTheDocument();
   });
 
-  // Wait for Type filter combobox
-  await screen.findByRole('combobox', { name: 'Type' });
-
   // Snapshot call count before filter
   const callsBeforeFilter = fetchMock.callHistory.calls(
     API_ENDPOINTS.DATASOURCE_COMBINED,
   ).length;
 
-  // Apply Type filter
-  await selectOption('Virtual', 'Type');
+  // Apply Type filter using compact pill UI
+  await selectPillOption('Virtual', 'Type');
 
   // Wait for filter API call to complete
   await waitFor(() => {
@@ -1606,16 +1600,13 @@ test('type filter persists after duplicating a 
dataset', async () => {
     expect(screen.getByTestId('listview-table')).toBeInTheDocument();
   });
 
-  // Wait for Type filter combobox
-  await screen.findByRole('combobox', { name: 'Type' });
-
   // Snapshot call count before filter
   const callsBeforeFilter = fetchMock.callHistory.calls(
     API_ENDPOINTS.DATASOURCE_COMBINED,
   ).length;
 
-  // Apply Type filter
-  await selectOption('Virtual', 'Type');
+  // Apply Type filter using compact pill UI
+  await selectPillOption('Virtual', 'Type');
 
   // Wait for filter API call to complete
   await waitFor(() => {
diff --git a/superset-frontend/src/pages/DatasetList/DatasetList.test.tsx 
b/superset-frontend/src/pages/DatasetList/DatasetList.test.tsx
index 2d1c4752e9b..ee4c1257aae 100644
--- a/superset-frontend/src/pages/DatasetList/DatasetList.test.tsx
+++ b/superset-frontend/src/pages/DatasetList/DatasetList.test.tsx
@@ -200,8 +200,8 @@ test('renders Name search filter', async () => {
 test('renders Type filter (Virtual/Physical dropdown)', async () => {
   renderDatasetList(mockAdminUser);
 
-  // Filter dropdowns should be present
-  const filters = await screen.findAllByRole('combobox');
+  // Filter pills should be present (compact pill UI)
+  const filters = await screen.findAllByTestId('compact-filter-pill');
   expect(filters.length).toBeGreaterThan(0);
 });
 
@@ -445,7 +445,8 @@ test('selecting Database filter triggers API call with 
database relation filter'
 
   await waitForDatasetsPageReady();
 
-  const filtersContainers = screen.getAllByRole('combobox');
+  // Filter pills should be present (compact pill UI replaces comboboxes)
+  const filtersContainers = screen.getAllByTestId('compact-filter-pill');
   expect(filtersContainers.length).toBeGreaterThan(0);
 });
 
diff --git a/superset-frontend/src/pages/GroupsList/GroupsList.test.tsx 
b/superset-frontend/src/pages/GroupsList/GroupsList.test.tsx
index 785066048e8..81bd86f8078 100644
--- a/superset-frontend/src/pages/GroupsList/GroupsList.test.tsx
+++ b/superset-frontend/src/pages/GroupsList/GroupsList.test.tsx
@@ -121,13 +121,17 @@ describe('GroupsList', () => {
 
   test('renders the filters correctly', async () => {
     await renderComponent();
-    const filtersSelect = screen.getAllByTestId('filters-select')[0];
 
-    expect(within(filtersSelect).getByText(/name/i)).toBeInTheDocument();
-    expect(within(filtersSelect).getByText(/label/i)).toBeInTheDocument();
-    
expect(within(filtersSelect).getByText(/description/i)).toBeInTheDocument();
-    expect(within(filtersSelect).getByText(/roles/i)).toBeInTheDocument();
-    expect(within(filtersSelect).getByText(/users/i)).toBeInTheDocument();
+    // The compact filter UI renders the first search filter as an input,
+    // and select filters as pill buttons. Only "Name" search renders inline;
+    // "Label" and "Description" searches are hidden (one search box per page).
+    expect(screen.getByTestId('filters-search')).toBeInTheDocument();
+
+    // Select filters render as compact pill buttons
+    const pills = screen.getAllByTestId('compact-filter-pill');
+    const pillLabels = pills.map(p => p.textContent ?? '');
+    expect(pillLabels.some(l => /roles/i.test(l))).toBe(true);
+    expect(pillLabels.some(l => /users/i.test(l))).toBe(true);
   });
 
   test('renders correct columns in the table', async () => {
diff --git a/superset-frontend/src/pages/RolesList/RolesList.test.tsx 
b/superset-frontend/src/pages/RolesList/RolesList.test.tsx
index 39d8069429c..6a179238d4e 100644
--- a/superset-frontend/src/pages/RolesList/RolesList.test.tsx
+++ b/superset-frontend/src/pages/RolesList/RolesList.test.tsx
@@ -151,8 +151,11 @@ describe('RolesList', () => {
   test('renders filters options', async () => {
     await renderAndWait();
 
-    const typeFilter = screen.queryAllByTestId('filters-select');
-    expect(typeFilter).toHaveLength(4);
+    // Compact filter UI: one search input for "Name" and 3 select pills
+    // (Users, Permissions, Groups).
+    expect(screen.getByTestId('filters-search')).toBeInTheDocument();
+    const selectContainers = screen.getAllByTestId('select-filter-container');
+    expect(selectContainers).toHaveLength(3);
   });
 
   test('renders correct list columns', async () => {
diff --git 
a/superset-frontend/src/pages/RowLevelSecurityList/RowLevelSecurityList.test.tsx
 
b/superset-frontend/src/pages/RowLevelSecurityList/RowLevelSecurityList.test.tsx
index b23b2421461..538e7ae41b1 100644
--- 
a/superset-frontend/src/pages/RowLevelSecurityList/RowLevelSecurityList.test.tsx
+++ 
b/superset-frontend/src/pages/RowLevelSecurityList/RowLevelSecurityList.test.tsx
@@ -166,11 +166,14 @@ describe('RuleList RTL', () => {
   test('renders filter options', async () => {
     await renderAndWait();
 
+    // Compact filter UI: only the first search filter renders (Name),
+    // subsequent search filters (Group Key) are hidden — one search box per 
page.
     const searchFilters = screen.queryAllByTestId('filters-search');
-    expect(searchFilters).toHaveLength(2);
+    expect(searchFilters).toHaveLength(1);
 
-    const typeFilter = screen.queryAllByTestId('filters-select');
-    expect(typeFilter).toHaveLength(3); // Update to expect 3 select filters
+    // Select filters render as compact pill buttons (Filter Type, Modified by)
+    const selectContainers = 
screen.queryAllByTestId('select-filter-container');
+    expect(selectContainers).toHaveLength(2);
   });
 
   test('renders correct list columns', async () => {
diff --git a/superset-frontend/src/pages/UsersList/UsersList.test.tsx 
b/superset-frontend/src/pages/UsersList/UsersList.test.tsx
index e994bd485cc..63539113eed 100644
--- a/superset-frontend/src/pages/UsersList/UsersList.test.tsx
+++ b/superset-frontend/src/pages/UsersList/UsersList.test.tsx
@@ -138,16 +138,16 @@ describe('UsersList', () => {
   test('renders filters options', async () => {
     await renderAndWait();
 
-    const submenu = screen.queryAllByTestId('filters-select')[0];
-    expect(within(submenu).getByText(/first name/i)).toBeInTheDocument();
-    expect(within(submenu).getByText(/last name/i)).toBeInTheDocument();
-    expect(within(submenu).getByText(/email/i)).toBeInTheDocument();
-    expect(within(submenu).getByText(/username/i)).toBeInTheDocument();
-    expect(within(submenu).getByText(/roles/i)).toBeInTheDocument();
-    expect(within(submenu).getByText(/is active?/i)).toBeInTheDocument();
-    expect(within(submenu).getByText(/created on/i)).toBeInTheDocument();
-    expect(within(submenu).getByText(/changed on/i)).toBeInTheDocument();
-    expect(within(submenu).getByText(/last login/i)).toBeInTheDocument();
+    // The compact filter UI shows: only the first search filter as an input,
+    // and select/datetime filters as pill buttons. Only "First name" search
+    // renders (subsequent search filters are hidden — one search box per 
page).
+    expect(screen.getByTestId('filters-search')).toBeInTheDocument();
+
+    // Select and datetime filters render as compact pill buttons
+    const pills = screen.getAllByTestId('compact-filter-pill');
+    const pillLabels = pills.map(p => p.textContent ?? '');
+    expect(pillLabels.some(l => /roles/i.test(l))).toBe(true);
+    expect(pillLabels.some(l => /is active\?/i.test(l))).toBe(true);
   });
 
   test('renders correct list columns', async () => {

Reply via email to