Skip to content

feat(table): unified filter pill UI with overflow strip#9638

Merged
kirangadhave merged 9 commits into
mainfrom
kg/table-filter-popover
May 26, 2026
Merged

feat(table): unified filter pill UI with overflow strip#9638
kirangadhave merged 9 commits into
mainfrom
kg/table-filter-popover

Conversation

@kirangadhave
Copy link
Copy Markdown
Member

@kirangadhave kirangadhave commented May 21, 2026

Summary

Replaces the column-header value-picker submenu with a unified pill-based filter strip below the table. Users add, edit, and remove filters via a single editor popover anchored to the pills row.

UI

  • New + button on the pills strip opens the editor. The column-header ··· menu routes through the same editor.
  • Persistent filter icon on column headers whose column has an active filter.
  • Filter pills format date / datetime / time values using the same helpers as column cells, so a pill matches what the cell shows.
  • "Filter by values" is shown only on text and numeric columns.
  • When pills overflow the strip width, the row clips with a soft right-edge fade and a "See all" button appears. Clicking it opens a draggable popover that lists every active filter. Overflow detection is a scrollWidth > clientWidth check on the strip itself with a ResizeObserver, re-measured on filter content change.
Screenshot 2026-05-22 at 3 46 08 PM
  • Value truncation with a full-segments tooltip and inline is_in / not_in chip rendering via a shared chip primitive reused by the editor picker.

Filter by values editor

Screenshot 2026-05-22 at 3 47 39 PM

Filter pill + Tooltip

Screenshot 2026-05-22 at 3 47 03 PM

Internals

  • Filter logic split out of filters.ts into a filters/ folder (types, operators, guards, builders, defaults, serialize, format).
    • Filters code blew up with all new additions, now the code is organized by concepts, so adding new filters or features should be easier.
  • Removes the select filter type. Text and number columns carry their own in / not_in membership shape directly. supportsMembership is promoted to a type guard (isMembershipFilterType) used at every call site.
    • select was incorrect conceptually. select is a filter operator rather than a type.
  • Operator groups (NULLISH, MEMBERSHIP, COMPARISON, TEXT_SCALAR, BOOLEAN_VALUE, EMPTY) consolidated so OPERATORS_WITHOUT_VALUE and per-type lists derive from one source.
    • Grouping operators by shape and purpose makes sense rather than just data type. We compose data type filters from the shape/purpose primitives.
    • For e.g. number column can have nullish, membership, comparision operators, but not text_scalar, empty or boolean operators
  • Local-ISO date formatters moved to utils/dates and renamed dateToLocalISO* to make local-time semantics explicit (vs. Date.toISOString).

Demo

Screen.Recording.2026-05-22.at.4.11.59.PM.mov

Test plan

  • Open mo.ui.table(df) with text, number, date, datetime, time, and boolean columns. Column ··· menu shows "Filter by values" only on text and numeric columns.
  • + button on the pill strip and the column-header ··· Filter item both open the same editor.
  • Filtered columns show the persistent filter icon next to the header.
  • Add enough filters that the strip overflows: right edge fades, "See all" appears, popover lists every filter.
  • Resize the table narrower / wider: "See all" appears / disappears as overflow changes.
  • Date / datetime / time pill text matches the column-cell formatting for the same value.
  • Pasting "X and Y", "X to Y", "X - Y", "X – Y" into a date range input all parse into the two date fields.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
marimo-docs Ready Ready Preview, Comment May 26, 2026 6:21am

Request Review

cubic-dev-ai[bot]

This comment was marked as resolved.

@kirangadhave kirangadhave added the enhancement New feature or request label May 21, 2026
@kirangadhave kirangadhave force-pushed the kg/table-filter-popover branch from 459bdd8 to 9a009bd Compare May 21, 2026 23:23
@kirangadhave kirangadhave marked this pull request as ready for review May 21, 2026 23:23
Copilot AI review requested due to automatic review settings May 21, 2026 23:23

This comment was marked as resolved.

@kirangadhave
Copy link
Copy Markdown
Member Author

@cubic-dev-ai

@cubic-dev-ai
Copy link
Copy Markdown
Contributor

cubic-dev-ai Bot commented May 22, 2026

@cubic-dev-ai

@kirangadhave I have started the AI code review. It will take a few minutes to complete.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 17 files

Architecture diagram
sequenceDiagram
    participant Table as DataTable
    participant Ctx as FilterEditorProvider
    participant Header as ColumnHeader
    participant Pills as FilterPills
    participant Ghost as Ghost Row (hidden)
    participant AddBtn as AddFilterButton
    participant Editor as FilterPillEditor
    participant ChipPicker as CompactChipRow / Chip
    participant Overflow as Overflow Popover

    Note over Table,Overflow: Unified filter pill UI with overflow strip

    Table->>Ctx: Provide requestAddFilter callback
    Table->>Pills: Render with filters, addFilterSnapshot, onAddFilterSnapshotChange
    Table->>Pills: Pass table, calculateTopKRows

    Header->>Ctx: useFilterEditor()
    Ctx-->>Header: requestAddFilter

    alt User clicks "Filter" in column header
        Header->>+Ctx: requestAddFilter({ columnId })
        Ctx->>Table: setAddFilterSnapshot(buildEditorSnapshot(column))
        Table->>Pills: update addFilterSnapshot prop
        Pills->>AddBtn: snapshot != null
        AddBtn->>Editor: render FilterPillEditor with snapshot
    else User clicks "Filter by values" in column header
        Header->>+Ctx: requestAddFilter({ columnId, operator: "in" })
        Ctx->>Table: setAddFilterSnapshot(buildEditorSnapshot(column, { operator: "in" }))
        Table->>Pills: update addFilterSnapshot prop
        Pills->>AddBtn: snapshot != null
        AddBtn->>Editor: render FilterPillEditor with "in" operator
    end

    alt User clicks "+" button on pills strip
        AddBtn->>AddBtn: buildEditorSnapshot(first editable column)
        AddBtn->>AddBtn: set open = true
        AddBtn->>Editor: render FilterPillEditor
    else User clicks existing filter pill
        Pills->>Editor: open FilterPillEditor with pill's snapshot & editIndex
    end

    Editor->>Editor: User modifies operator/value
    Editor->>Table: onApply → setColumnFilters(updater)
    alt editIndex undefined (new filter)
        Table->>Table: prepend { id: draftColumnId, value } to filters
    else editIndex defined (edit existing)
        Table->>Table: replace filters[editIndex] = { id: draftColumnId, value }
    end
    Editor->>Pills: onClose → close popover

    Note over Pills,Ghost: Overflow detection uses hidden ghost row
    Pills->>Ghost: Always render all pills at natural width (hidden, not displayed)
    Ghost-->>Pills: Provide scrollWidth measurement
    Pills->>Pills: Compare scrollWidth > clientWidth via ResizeObserver
    alt hasOverflow
        Pills->>Pills: Apply CSS mask gradient on container
        Pills->>Pills: Show +N button at right edge
        Pills->>Overflow: open DraggablePopover with full filter list
        Overflow-->>Table: "Clear all" → setColumnFilters([])
    end

    Note over Pills,ChipPicker: Pill rendering
    Pills->>ChipPicker: Format value (scalar or multi-value)
    alt scalar value
        ChipPicker-->>Pills: Render truncated text with tooltip on hover
    else multi-value (in/not_in)
        ChipPicker->>ChipPicker: CompactChipRow with max 3 visible chips
        ChipPicker-->>Pills: Render chips with +N overflow indicator
    end

    Note over Table: Column header shows persistent filter indicator
    Header->>Header: hasFilter → show FilterIcon next to header text
Loading

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread frontend/src/components/data-table/filter-pills.tsx
Comment thread frontend/src/components/data-table/column-header.tsx Outdated
Introduces the filter pill strip with a + button to add filters and an overflow popover with a 'See all' button gated by scrollWidth detection. Pills open the filter editor directly from a column-header menu, show a persistent indicator on filtered headers, and 'Filter by values' extends to numeric, date, and time columns. Adds value truncation with a full-segments tooltip, inline is_in/not_in chip rendering, and a shared chip primitive reused by the editor picker.
Pills now use the same dateToISODate, exactDateTime, and dateToISOTime helpers as the column cells, and hide 'Filter by values' on date, datetime, and time columns since those should use the range picker. parsePastedRange also accepts 'and' as a separator alongside hyphen, en-dash, em-dash, and 'to'.
Also consolidates the filter test harness and adds column-header menu coverage.
Splits filters.ts into a filters/ folder by concern (types, operators, guards, builders, defaults, serialize, format). Removes the 'select' filter type; text and number columns now carry their own in/not_in membership shape. Consolidates operator groups so OPERATORS_WITHOUT_VALUE and per-type op lists derive from one source, and promotes supportsMembership to a type guard (isMembershipFilterType) used at all call sites. columnEditableType now throws on invalid filterType instead of silently falling back to text, and is reused in handleColumnChange. Drops the trivial getEditableType wrapper and the ColumnFilterValue cast in defaultFilterValueFor's membership branch. Renames renderSortFilterIcon to renderSortIcon and narrows filterType once in context-menu. Editor defaults for date/datetime are now '=='.
dateToISODate/Time/DateTime were generic Date-to-string helpers stranded
in filters/serialize.ts after the folder split. Move them to utils/dates
alongside the existing date formatters, rename to make the local-time
semantics explicit (dateToLocalISO*), and add docstrings clarifying when
to use them versus Date.toISOString().
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 28 out of 28 changed files in this pull request and generated 2 comments.

Comment thread frontend/src/components/data-table/filter-pills.tsx Outdated
Comment thread frontend/src/components/data-table/data-table.tsx Outdated
Copy link
Copy Markdown
Collaborator

@Light2Dark Light2Dark left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Couple of observations, some are not important now.

  1. The tooltip seems to appear unexpectedly based on your video. One instance i can reproduce is clicking Accept triggers it to open.
Screen.Recording.2026-05-25.at.12.57.06.PM.mov
  1. Future: It'd be nice if the blue icon is clickable / redirects to the modal?
Image
  1. Tiny issue, validation only happens after I select another element / escape out. So, it takes me 2 clicks to apply filter.
Image
  1. Do you think its possible for 'Filter by values' to automatically open up the list to filter by? Previously was 1 click to filter, now its 2 clicks.

For the PR, I really like the keyboard navigation, pretty sweet.

Copy link
Copy Markdown
Collaborator

@Light2Dark Light2Dark left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks great!

items: value.values
.map((v) => stringifyUnknownValue({ value: v }))
.toSorted((a, b) => a.localeCompare(b)),
};
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may add

      default:
        logNever(value.operator);

to all the case statements here

@kirangadhave
Copy link
Copy Markdown
Member Author

The tooltip seems to appear unexpectedly based on your video. One instance i can reproduce is clicking Accept triggers it to open.

I'll take a look, looks like some bug

Future: It'd be nice if the blue icon is clickable / redirects to the modal?

That's a good idea

Tiny issue, validation only happens after I select another element / escape out. So, it takes me 2 clicks to apply filter.

Ah i see, i'll fix this as well

Do you think its possible for 'Filter by values' to automatically open up the list to filter by? Previously was 1 click to filter, now its 2 clicks.

This should be easy to do. good suggestion

Prevent the pill tooltip from reappearing when the editor popover closes by
disabling focus restore on close, and bump the pill tooltip's hover delay so
quick interactions don't trigger it.
NumberField commits via onChange only on blur, leaving Apply disabled while
the user is still typing. Expose a live onInputText hook so the filter pill
editor can validate the in-progress text and enable Apply on the first
keystroke that parses.
Open the values list immediately when the editor mounts with no chosen
values, so 'Filter by values' takes one click instead of two.
@kirangadhave kirangadhave merged commit 4f7706e into main May 26, 2026
31 checks passed
@kirangadhave kirangadhave deleted the kg/table-filter-popover branch May 26, 2026 15:08
@github-actions
Copy link
Copy Markdown

🚀 Development release published. You may be able to view the changes at https://marimo.app?v=0.23.9-dev4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants