Skip to content

Add Table component#15

Merged
lifeiscontent merged 18 commits into
mainfrom
feat/table
Jun 16, 2026
Merged

Add Table component#15
lifeiscontent merged 18 commits into
mainfrom
feat/table

Conversation

@lifeiscontent

@lifeiscontent lifeiscontent commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Component

Adds the Table compound component to @plane/propel, rebuilt from Figma node 4017-653 ("Table", header/data cells 38px, px-4 py-2, subtle row dividers).

Parts / subcomponents

A compound API of plain semantic elements, each Omiting className/style from its intrinsic props:

  • Table (<table>), root, renders on surface-1 with text-13 primary body text
  • TableHeader (<thead>)
  • TableBody (<tbody>)
  • TableRow (<tr>), subtle bottom divider, hover tint, last-row divider hidden
  • TableHead (<th>), variant default (bg-layer-1 / text-secondary, text-12 semibold) vs sortable
  • TableCell (<td>)

Stories declare these via the subcomponents meta (mirrors the avatar-group pattern).

Sortable header

TableHead variant="sortable" renders the label as an interactive button with a lucide sort chevron (ChevronsUpDown / ChevronUp / ChevronDown, aria-hidden) and reflects order through aria-sort (none/ascending/descending) via the sort/onSort props.

Figma

Tokens mapped from node 4017-653 master components ("Table header" 4032-7901, "Table cell" 4032-7892): bg-surface-1, bg-layer-1, border-subtle, text-primary/text-secondary/text-tertiary, text-icon-secondary, text-12/text-13, 38px height, px-4 py-2. No arbitrary hex; cva + semantic propel utilities only.

Verification

  • vp install, ok
  • vp check, all formatted, no lint/type errors
  • vp run -r test (Playwright chromium), 14 passed, incl. 2 new Table tests (rows/cells/columnheader roles; sortable header toggles aria-sort on click)
  • vp run -r build, build complete, attw: No problems found (pre-existing publint ./hooks/* warnings are unrelated)

Dependencies

Blocked by: #16 (Dropdown, editable cells). Optionally composes: #30 (Pagination). Changes requested; the Table + Spreadsheet variants + editable cells are queued behind #16.

Copilot AI review requested due to automatic review settings June 8, 2026 14:00
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown

📚 Storybook preview: https://pr-15-propel-storybook.vamsi-906.workers.dev

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a new semantic, compound Table component to @plane/propel, including Storybook stories that exercise basic rendering and sortable header behavior (including aria-sort).

Changes:

  • Adds Table compound component primitives (Table, TableHeader, TableBody, TableRow, TableHead, TableCell) with Figma-aligned styling tokens and a sortable header variant.
  • Implements sortable header affordance via a button + lucide chevrons, reflecting state through aria-sort.
  • Adds Storybook stories with interaction tests covering table roles and sortable header toggling.

Reviewed changes

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

File Description
packages/propel/src/components/table/index.tsx Implements the Table compound component API and sortable header behavior.
packages/propel/src/components/table/table.stories.tsx Adds Storybook stories + play tests validating roles and sortable interactions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/propel/src/components/table/index.tsx Outdated
Comment thread packages/propel/src/components/table/index.tsx Outdated
Comment thread packages/propel/src/components/table/index.tsx
@lifeiscontent

Copy link
Copy Markdown
Collaborator Author

Removed defaultVariants; TableHead variant now required.

@bhaveshraja bhaveshraja left a comment

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.

Figma Design

Added examples of 2 types of tables.

We also need capability to click a cell and update the value with the help of dropdown component.

Page nation is another component, can be used in table if required.

@lifeiscontent lifeiscontent requested a review from bhaveshraja June 9, 2026 17:30
This was referenced Jun 9, 2026
lifeiscontent added a commit that referenced this pull request Jun 10, 2026
Address design review on PR #15 (Figma 4017-653):

- Two variants on the root `Table` via a required `variant` prop, read by the
  cells through context: `table` (row dividers only) and `spreadsheet` (full
  grid — every cell bordered). Both share the Figma cell metrics (38px, px-3
  py-2, header on layer-1, body on surface-1) and a rounded outer border.
- `TableEditableCell`: a click-to-edit cell whose value + trailing chevron is the
  trigger for a propel Dropdown, picking a new value that updates the row in
  place (Figma "Account type" editable cell). Owns the Dropdown root + trigger;
  the consumer passes the DropdownContent.
- Stories: Default (Table), Spreadsheet, Sortable, EditableCells (click-cell →
  dropdown → value updates), all with the 4017-653 design link.

Pagination is intentionally left as a separate component to compose, per review.
@lifeiscontent

lifeiscontent commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator Author

@bhaveshraja addressed the review (Figma 4017-653):

  1. Two table types, Table + Spreadsheet. Modeled as a required variant prop on the root <Table> (per the prop vocabulary, not size/type), read by the cells via context so you set it once:
  • variant="table", standard table: rounded outer border, header underline + subtle row dividers only (no vertical rules).
  • variant="spreadsheet", denser grid: every header/body cell fully bordered (0.5px border/subtle), matching the Figma "Spreadsheet" frame.
    Both share the same Figma cell metrics (38px tall, px-3 py-2, header on layer-1, body on surface-1).
  1. Click a cell to update its value via the Dropdown component. New TableEditableCell sub-component: the cell's value + trailing chevron is the trigger for a propel Dropdown (@plane/propel/components/dropdown); picking an item updates the row in place, the Figma "Account type" editable cell. It owns the Dropdown root + trigger, so you just pass a DropdownContent. Demo: the new EditableCells story (click cell, dropdown, value updates, with a play-test asserting the round-trip). Works in both variants.

  2. Pagination left as a separate component to compose later, not built into the table, as you noted.

Stories: Default (Table), Spreadsheet, Sortable, EditableCells, all linked to 4017-653. Kept proper semantics (<table>/<thead>/<tbody>/<th scope>/<td>, sortable headers reflect aria-sort). Verified against Figma in LTR and RTL (logical utilities mirror columns/alignment; the chevron is vertical so needs no mirroring). Gates green: vp check, vp run -r test (axe a11y, 91 passed), vp run -r build (attw clean), vp run -r build-storybook.

@lifeiscontent

Copy link
Copy Markdown
Collaborator Author

Added keyboard play tests: Enter/Space sorts (aria-sort cycles), editable cell opens the Dropdown via keyboard and Escape closes it.

@lifeiscontent

lifeiscontent commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator Author

@bhaveshraja ready for re-review, all the changes-requested items are addressed (CI green):

  • 2 table types: variant="table" (rounded border, header underline + row dividers) and variant="spreadsheet" (full grid).
  • Editable cells: click a cell, propel Dropdown updates the value (new TableEditableCell + an EditableCells demo + keyboard test).
  • Pagination kept as a separate component (not built into the table), per your note.

Could you take another look and clear the changes-requested?

lifeiscontent added a commit that referenced this pull request Jun 10, 2026
Designer follow-up on #15: pagination can be used with the table. The Table and
Pagination stay separate components, so the story keeps the full dataset, renders
only the current page of rows, and lets the Pagination below drive page and page
size (changing the size resets to page 1 and updates the range label).

Verified in Storybook (table variants, editable cell, and pagination) plus a play
test that advances a page and asserts the rows swap.
@lifeiscontent

Copy link
Copy Markdown
Collaborator Author

All three points are covered now:

  1. Two table types: the variant prop drives both layouts, table (row dividers only) and spreadsheet (full grid). See the Default and Spreadsheet stories.
  2. Click a cell to edit via Dropdown: TableEditableCell renders the value plus a chevron as the trigger and opens a propel Dropdown to pick a new value, updating the row in place. See the EditableCells story (the Account type column).
  3. Pagination: added a WithPagination story that composes the Table with the Pagination component. They stay separate, the table renders the current page of rows and the pagination below drives page and page size.

Rebased onto main so it picks up the merged Dropdown and Pagination. Checked all of it in Storybook (both variants, the editable-cell dropdown, and paging through the rows). vp check and the play tests are green.

@bhaveshraja bhaveshraja left a comment

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.

  1. There should be a border: borer-subtle  around the table.
  2. Changing to a different page using pagination is resizing the table's width. The table's width shouldn't change.
  3. Incase of long vertical scroll, headers have to be sticking to the top. And for long horizontal scroll, Option to make first and last column sticky.
  4. Option to have leading and trailing icons in cells. Can be an avatar too. Kind of slot.
  5. Cell can also have only icon, eg. like more icon - to have options related to the row item.
  6. Row height should be 44px, current with editable cell its much bigger.
  7. Hover state and selected state should be added for editable cell.

Few of these are tittle tricky to show in figma due to its capabilities. Happy to get on to a call and explain.

lifeiscontent added a commit that referenced this pull request Jun 11, 2026
…cell states

Addresses design review on #15:
- Rows are 44px. The editable cell was forcing rows to ~55px because its p-0 override
  lost to the shared cell padding under cx; padding now lives on each cell type, so the
  editable cell is truly full-bleed and its 44px button sets the height.
- Editable cells get an explicit open/selected state (data-[popup-open]) next to the
  existing hover and focus states.
- The WithPagination demo gets a fixed width so the table no longer resizes between
  pages (it is w-full and was shrink-to-fitting each page's content under centered
  layout). The table already has the border-subtle outer border.
@lifeiscontent

Copy link
Copy Markdown
Collaborator Author

Got the straightforward ones done:

  • Rows are 44px now. The editable cell was the cause of the taller rows: its full-bleed override wasn't actually winning, so those rows came out around 55px. Fixed it so the editable cell is genuinely full-bleed and the row is a clean 44px.
  • Editable cells have a selected state on top of the hover one now, the cell fills with a background while its menu is open.
  • The table no longer resizes when you page through. The pagination example had no set width, so it was shrinking to fit each page's content; pinned it.
  • The border-subtle around the table is already there, so let me know if it's reading differently somewhere for you.

The other three I'd rather do with you on that call, since they're the ones that are tricky to read off Figma: leading/trailing slots in cells (icon or avatar), an icon-only action cell for the per-row more menu, and the sticky header plus sticky first/last column behavior on scroll. Whenever works for you.

lifeiscontent added a commit that referenced this pull request Jun 15, 2026
…on cell (#15)

Addresses the design review (Figma 5196-4084):

1. The outer border-subtle now shows: the table is wrapped in a rounded, bordered,
   scrollable frame (a <table> can't clip its own rounded corners, so the border was
   invisible before).
2. Sticky header on vertical scroll, plus an opt-in pinned first/last column on
   horizontal scroll via each cell's pinned start/end. The frame is keyboard-scrollable
   (tabindex) only while it overflows, so it adds no stray tab stop and satisfies the
   axe scrollable-region rule.
3. TableCell gains inlineStartNode/inlineEndNode slots for a leading/trailing icon or
   Avatar (the Name column).
4. New TableActionCell: an icon-only (default ellipsis) row-options cell that opens a
   Dropdown, mirroring TableEditableCell.
5. Rows sit on layer-2 / layer-2-hover; actionable cells are layer-transparent /
   layer-transparent-hover so the cell hover reads distinctly over the row hover.

Closes #15
@bhaveshraja

Copy link
Copy Markdown
Collaborator
  1. Double border for spreadsheet variant. also small issue with border radius.
Screenshot 2026-06-15 at 9 33 58 PM
  1. Is this related to avatar component? The text is not aligned to centre here in table.
Screenshot 2026-06-15 at 9 34 53 PM
  1. For header text token should be text-tertiary
Screenshot 2026-06-15 at 9 37 57 PM
  1. Can we control the scrollbar, instead of it cutting the header/taking a space for itself. can it be on top of the items, like an overlay?
Screenshot 2026-06-16 at 12 45 03 AM

lifeiscontent added a commit that referenced this pull request Jun 16, 2026
…erlay scrollbar, selected cell)

Round of fixes from the PR #15 design review:

- Spreadsheet variant no longer draws a double border. The scroll frame owns the
  single outer ring, so cells only draw interior dividers (inline-end + bottom, with
  the last column/row dropping theirs). This also clears the squared-off corners that
  poked past the rounded frame.
- Header text now uses text-tertiary to match the Figma "Table header" token (was
  text-secondary on plain headers).
- Scroll frame is now a Base UI ScrollArea, so the scrollbar overlays the content
  instead of reserving a 12px gutter. The header is no longer clipped and the table
  width stays constant whether or not the scrollbar is showing (also keeps width
  stable across pagination). Sticky header + pinned columns still work because the
  ScrollArea Viewport is the real scroll container; it also gets tabIndex=0 only while
  it overflows, which keeps the scrollable-region-focusable a11y rule satisfied
  without the manual ResizeObserver.
- Pinned columns draw a hairline on their inline edge so they read as separated from
  the content scrolling under them (logical border, RTL-safe).
- Editable cell gains a selected prop: a persistent layer-transparent-selected tint a
  step above hover, for marking the active cell. The story wires it to the last-opened
  cell.

Verified in the browser across light/dark and LTR/RTL: single clean grid, tertiary
header, centered avatars, 44px rows, overlay scrollbar (0 reserved space), constant
width across pages, sticky header + pinned columns. vp check and the full test suite
(736 tests, axe gate across 4 themes) pass.
@lifeiscontent

lifeiscontent commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the detailed pass. Went through all the points against the current branch and pushed the fixes. Here's where each one landed (newest first):

  1. Spreadsheet double border + radius: fixed. The scroll frame already draws the single outer border, so the grid cells were doubling it on every edge (and squaring off the rounded corners). Cells now only draw interior dividers; the last column and last row drop theirs, so the frame's ring stays single and the corners round cleanly.

  2. Avatar not vertically centered in a cell: this was already centered in the current code (the cell uses items-center and the Avatar centers its own initials). Confirmed in the browser, the avatar and the text sit on the same center line. Not the Avatar component or the cell, both were fine, so no change needed here. Shout if you're still seeing it on a specific row and I'll dig in.

  3. Header text token: fixed, headers now use text-tertiary (was text-secondary).

  4. Scrollbar should overlay, not take space: fixed. The frame is now an overlay scrollbar, so it sits on top of the content instead of reserving a gutter. The header is no longer clipped and the table width no longer shifts when the scrollbar appears.

  5. border-subtle around the table: already in place, the frame carries a border-subtle ring. Left as is.

  6. Pagination must not resize the table width: confirmed stable now, the width holds constant across pages (measured 758px on page 1 and page 2). The overlay scrollbar from Add Badge component #4 also helps here since it no longer steals width when rows overflow.

  7. Sticky header + sticky first/last column: already supported and kept working through the scrollbar change. Header sticks on vertical scroll; columns take an opt-in pinned="start" / pinned="end" on the header and body cells. Verified both stick correctly, including in RTL where start pins to the right edge.

  8. Leading/trailing slots in cells (icon or Avatar): already there via inlineStartNode / inlineEndNode on the cell. Left as is.

  9. Icon-only cell (e.g. a "more" action): already there as the action cell with the trailing ... menu. Left as is.

  10. Row height 44px: confirmed, body rows are 44px including with the editable cell (the editable trigger fills the full 44px cell).

  11. Hover + selected state for the editable cell: hover/open were already there; added a persistent selected state (a stronger tint than hover) for marking the active cell.

  12. Click a cell to edit via the Dropdown: already implemented, the editable cell opens a propel Dropdown to pick a value and updates in place. Left as is.

  13. Pagination as a separate component: already the case, the table renders the current page's rows and the Pagination component drives page/pageSize alongside it (see the With Pagination story).

Verified everything in the browser in light and dark, LTR and RTL. Full test suite passes (axe a11y gate runs across all four themes).

The only one I'd flag for your eyes is 2., since I couldn't reproduce the misalignment. If you can point me at the exact screenshot/row, happy to take another look.

The TableHead variant axis (default/sortable) no longer defaults to
"default" via cva. variant is now a required prop, so every header cell
declares its visual treatment explicitly. sort/onSort remain optional
(additive, only meaningful for sortable headers).
Address design review on PR #15 (Figma 4017-653):

- Two variants on the root `Table` via a required `variant` prop, read by the
  cells through context: `table` (row dividers only) and `spreadsheet` (full
  grid — every cell bordered). Both share the Figma cell metrics (38px, px-3
  py-2, header on layer-1, body on surface-1) and a rounded outer border.
- `TableEditableCell`: a click-to-edit cell whose value + trailing chevron is the
  trigger for a propel Dropdown, picking a new value that updates the row in
  place (Figma "Account type" editable cell). Owns the Dropdown root + trigger;
  the consumer passes the DropdownContent.
- Stories: Default (Table), Spreadsheet, Sortable, EditableCells (click-cell →
  dropdown → value updates), all with the 4017-653 design link.

Pagination is intentionally left as a separate component to compose, per review.
SortableKeyboard: Tab to the sortable header button, then Enter/Space
cycle the sort (none -> ascending -> descending -> none) with aria-sort
and onSort following along; asserts the th scope=col semantics.

EditableCellKeyboard: Tab+Enter opens the portaled Dropdown, Arrow Down +
Enter selects a value and the cell updates in place, and Escape closes the
menu and returns focus to the cell trigger. The portaled menu is queried
from the document body by unique item text with waitFor for open/close.

Both tagged [!dev,!autodocs,!manifest] so they run under test only.
Designer follow-up on #15: pagination can be used with the table. The Table and
Pagination stay separate components, so the story keeps the full dataset, renders
only the current page of rows, and lets the Pagination below drive page and page
size (changing the size resets to page 1 and updates the range label).

Verified in Storybook (table variants, editable cell, and pagination) plus a play
test that advances a page and asserts the rows swap.
Same Markdown-in-autodocs issue as Popover/Tabs/Dropdown: the indented JSX example
let the wrapper tag leak out of the code box. Wrap it in a ```tsx fence.
…cell states

Addresses design review on #15:
- Rows are 44px. The editable cell was forcing rows to ~55px because its p-0 override
  lost to the shared cell padding under cx; padding now lives on each cell type, so the
  editable cell is truly full-bleed and its 44px button sets the height.
- Editable cells get an explicit open/selected state (data-[popup-open]) next to the
  existing hover and focus states.
- The WithPagination demo gets a fixed width so the table no longer resizes between
  pages (it is w-full and was shrink-to-fitting each page's content under centered
  layout). The table already has the border-subtle outer border.
…on cell (#15)

Addresses the design review (Figma 5196-4084):

1. The outer border-subtle now shows: the table is wrapped in a rounded, bordered,
   scrollable frame (a <table> can't clip its own rounded corners, so the border was
   invisible before).
2. Sticky header on vertical scroll, plus an opt-in pinned first/last column on
   horizontal scroll via each cell's pinned start/end. The frame is keyboard-scrollable
   (tabindex) only while it overflows, so it adds no stray tab stop and satisfies the
   axe scrollable-region rule.
3. TableCell gains inlineStartNode/inlineEndNode slots for a leading/trailing icon or
   Avatar (the Name column).
4. New TableActionCell: an icon-only (default ellipsis) row-options cell that opens a
   Dropdown, mirroring TableEditableCell.
5. Rows sit on layer-2 / layer-2-hover; actionable cells are layer-transparent /
   layer-transparent-hover so the cell hover reads distinctly over the row hover.

Closes #15
The RichRows action-menu test asserted toBeVisible right after findByRole, racing
the menu's open scale-fade in CI. Wrap it in waitFor like the other menu tests.
…erlay scrollbar, selected cell)

Round of fixes from the PR #15 design review:

- Spreadsheet variant no longer draws a double border. The scroll frame owns the
  single outer ring, so cells only draw interior dividers (inline-end + bottom, with
  the last column/row dropping theirs). This also clears the squared-off corners that
  poked past the rounded frame.
- Header text now uses text-tertiary to match the Figma "Table header" token (was
  text-secondary on plain headers).
- Scroll frame is now a Base UI ScrollArea, so the scrollbar overlays the content
  instead of reserving a 12px gutter. The header is no longer clipped and the table
  width stays constant whether or not the scrollbar is showing (also keeps width
  stable across pagination). Sticky header + pinned columns still work because the
  ScrollArea Viewport is the real scroll container; it also gets tabIndex=0 only while
  it overflows, which keeps the scrollable-region-focusable a11y rule satisfied
  without the manual ResizeObserver.
- Pinned columns draw a hairline on their inline edge so they read as separated from
  the content scrolling under them (logical border, RTL-safe).
- Editable cell gains a selected prop: a persistent layer-transparent-selected tint a
  step above hover, for marking the active cell. The story wires it to the last-opened
  cell.

Verified in the browser across light/dark and LTR/RTL: single clean grid, tertiary
header, centered avatars, 44px rows, overlay scrollbar (0 reserved space), constant
width across pages, sticky header + pinned columns. vp check and the full test suite
(736 tests, axe gate across 4 themes) pass.
Copilot AI review requested due to automatic review settings June 16, 2026 13:00

Copilot AI left a comment

Copy link
Copy Markdown

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 2 out of 2 changed files in this pull request and generated 3 comments.

Comment thread packages/propel/src/components/table/index.tsx
Comment thread packages/propel/src/components/table/index.tsx Outdated
Comment thread packages/propel/src/components/table/index.tsx Outdated
Copilot AI review requested due to automatic review settings June 16, 2026 17:23
@lifeiscontent lifeiscontent enabled auto-merge (squash) June 16, 2026 17:23

Copilot AI left a comment

Copy link
Copy Markdown

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 2 out of 2 changed files in this pull request and generated 4 comments.

Comment thread packages/propel/src/components/table/table.stories.tsx
Comment thread packages/propel/src/components/table/table.stories.tsx
Comment thread packages/propel/src/components/table/index.tsx Outdated
Comment thread packages/propel/src/components/table/index.tsx
Copilot AI review requested due to automatic review settings June 16, 2026 17:50
@lifeiscontent lifeiscontent merged commit 40cc8c6 into main Jun 16, 2026
3 checks passed
@lifeiscontent lifeiscontent deleted the feat/table branch June 16, 2026 17:53

Copilot AI left a comment

Copy link
Copy Markdown

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 2 out of 2 changed files in this pull request and generated 1 comment.

Comment on lines +182 to +186
/**
* Visual treatment. `sortable` renders a clickable button with a sort chevron;
* set it whenever you pass `sort`.
*/
variant: NonNullable<VariantProps<typeof tableHeadVariants>["variant"]>;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants