Skip to content

Commit 2beb757

Browse files
authored
feat(table): add align prop to Head and Cell (#342)
* feat(table): add align prop to head and cell * fix(table): apply explicit left alignment classes
1 parent 1c36c7a commit 2beb757

5 files changed

Lines changed: 92 additions & 8 deletions

File tree

.changeset/bright-tables-wave.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@tailor-platform/app-shell": minor
3+
---
4+
5+
Add `align?: "left" | "right"` to `Table.Head` and `Table.Cell` so raw table primitives can align numeric columns without relying on consumer Tailwind utility overrides.
6+
7+
```tsx
8+
<Table.Head align="right">Amount</Table.Head>
9+
<Table.Cell align="right">123</Table.Cell>
10+
```

docs/components/table.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ All other standard HTML `<table>` props are accepted.
6060

6161
All other sub-components (`Table.Header`, `Table.Body`, `Table.Footer`, `Table.Row`, `Table.Head`, `Table.Cell`, `Table.Caption`) accept `className` and their corresponding standard HTML element props.
6262

63+
`Table.Head` and `Table.Cell` also accept `align?: "left" | "right"` for semantic text alignment without relying on utility-class overrides.
64+
6365
## Examples
6466

6567
### With Footer
@@ -114,6 +116,25 @@ All other sub-components (`Table.Header`, `Table.Body`, `Table.Footer`, `Table.R
114116
</Table.Root>
115117
```
116118

119+
### Right-aligned numeric columns
120+
121+
```tsx
122+
<Table.Root>
123+
<Table.Header>
124+
<Table.Row>
125+
<Table.Head>Item</Table.Head>
126+
<Table.Head align="right">Amount</Table.Head>
127+
</Table.Row>
128+
</Table.Header>
129+
<Table.Body>
130+
<Table.Row>
131+
<Table.Cell>Widget A</Table.Cell>
132+
<Table.Cell align="right">123</Table.Cell>
133+
</Table.Row>
134+
</Table.Body>
135+
</Table.Root>
136+
```
137+
117138
### Constrained Height with Scroll
118139

119140
```tsx
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

3-
exports[`Table > snapshots > basic table with header and body 1`] = `"<div data-slot="table-container" class="astw:relative astw:w-full astw:overflow-x-auto"><table data-slot="table" class="astw:w-full astw:caption-bottom astw:text-sm"><thead data-slot="table-header" class="astw:[&amp;_tr]:border-b astw:[&amp;_tr]:border-border"><tr data-slot="table-row" class="astw:hover:bg-muted/50 astw:data-[state=selected]:bg-muted astw:border-b astw:border-border astw:transition-colors"><th data-slot="table-head" class="astw:text-foreground astw:h-10 astw:px-2 astw:first:pl-6 astw:last:pr-6 astw:text-left astw:align-middle astw:font-medium astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0">Name</th><th data-slot="table-head" class="astw:text-foreground astw:h-10 astw:px-2 astw:first:pl-6 astw:last:pr-6 astw:text-left astw:align-middle astw:font-medium astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0">Status</th></tr></thead><tbody data-slot="table-body" class="astw:[&amp;_tr:last-child]:border-0"><tr data-slot="table-row" class="astw:hover:bg-muted/50 astw:data-[state=selected]:bg-muted astw:border-b astw:border-border astw:transition-colors"><td data-slot="table-cell" class="astw:px-2 astw:py-2 astw:first:pl-6 astw:last:pr-6 astw:align-middle astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0">Item 1</td><td data-slot="table-cell" class="astw:px-2 astw:py-2 astw:first:pl-6 astw:last:pr-6 astw:align-middle astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0">Active</td></tr></tbody></table></div>"`;
3+
exports[`Table > snapshots > basic table with header and body 1`] = `"<div data-slot="table-container" class="astw:relative astw:w-full astw:overflow-x-auto"><table data-slot="table" class="astw:w-full astw:caption-bottom astw:text-sm"><thead data-slot="table-header" class="astw:[&amp;_tr]:border-b astw:[&amp;_tr]:border-border"><tr data-slot="table-row" class="astw:hover:bg-muted/50 astw:data-[state=selected]:bg-muted astw:border-b astw:border-border astw:transition-colors"><th data-slot="table-head" class="astw:text-foreground astw:h-10 astw:px-2 astw:first:pl-6 astw:last:pr-6 astw:align-middle astw:font-medium astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0 astw:text-left">Name</th><th data-slot="table-head" class="astw:text-foreground astw:h-10 astw:px-2 astw:first:pl-6 astw:last:pr-6 astw:align-middle astw:font-medium astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0 astw:text-left">Status</th></tr></thead><tbody data-slot="table-body" class="astw:[&amp;_tr:last-child]:border-0"><tr data-slot="table-row" class="astw:hover:bg-muted/50 astw:data-[state=selected]:bg-muted astw:border-b astw:border-border astw:transition-colors"><td data-slot="table-cell" class="astw:px-2 astw:py-2 astw:first:pl-6 astw:last:pr-6 astw:align-middle astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0 astw:text-left">Item 1</td><td data-slot="table-cell" class="astw:px-2 astw:py-2 astw:first:pl-6 astw:last:pr-6 astw:align-middle astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0 astw:text-left">Active</td></tr></tbody></table></div>"`;
44

5-
exports[`Table > snapshots > empty table 1`] = `"<div data-slot="table-container" class="astw:relative astw:w-full astw:overflow-x-auto"><table data-slot="table" class="astw:w-full astw:caption-bottom astw:text-sm"><thead data-slot="table-header" class="astw:[&amp;_tr]:border-b astw:[&amp;_tr]:border-border"><tr data-slot="table-row" class="astw:hover:bg-muted/50 astw:data-[state=selected]:bg-muted astw:border-b astw:border-border astw:transition-colors"><th data-slot="table-head" class="astw:text-foreground astw:h-10 astw:px-2 astw:first:pl-6 astw:last:pr-6 astw:text-left astw:align-middle astw:font-medium astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0">Name</th><th data-slot="table-head" class="astw:text-foreground astw:h-10 astw:px-2 astw:first:pl-6 astw:last:pr-6 astw:text-left astw:align-middle astw:font-medium astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0">Status</th></tr></thead><tbody data-slot="table-body" class="astw:[&amp;_tr:last-child]:border-0"></tbody></table></div>"`;
5+
exports[`Table > snapshots > empty table 1`] = `"<div data-slot="table-container" class="astw:relative astw:w-full astw:overflow-x-auto"><table data-slot="table" class="astw:w-full astw:caption-bottom astw:text-sm"><thead data-slot="table-header" class="astw:[&amp;_tr]:border-b astw:[&amp;_tr]:border-border"><tr data-slot="table-row" class="astw:hover:bg-muted/50 astw:data-[state=selected]:bg-muted astw:border-b astw:border-border astw:transition-colors"><th data-slot="table-head" class="astw:text-foreground astw:h-10 astw:px-2 astw:first:pl-6 astw:last:pr-6 astw:align-middle astw:font-medium astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0 astw:text-left">Name</th><th data-slot="table-head" class="astw:text-foreground astw:h-10 astw:px-2 astw:first:pl-6 astw:last:pr-6 astw:align-middle astw:font-medium astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0 astw:text-left">Status</th></tr></thead><tbody data-slot="table-body" class="astw:[&amp;_tr:last-child]:border-0"></tbody></table></div>"`;
66

7-
exports[`Table > snapshots > table with caption 1`] = `"<div data-slot="table-container" class="astw:relative astw:w-full astw:overflow-x-auto"><table data-slot="table" class="astw:w-full astw:caption-bottom astw:text-sm"><caption data-slot="table-caption" class="astw:text-muted-foreground astw:mt-4 astw:text-sm">A list of items</caption><thead data-slot="table-header" class="astw:[&amp;_tr]:border-b astw:[&amp;_tr]:border-border"><tr data-slot="table-row" class="astw:hover:bg-muted/50 astw:data-[state=selected]:bg-muted astw:border-b astw:border-border astw:transition-colors"><th data-slot="table-head" class="astw:text-foreground astw:h-10 astw:px-2 astw:first:pl-6 astw:last:pr-6 astw:text-left astw:align-middle astw:font-medium astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0">Name</th></tr></thead><tbody data-slot="table-body" class="astw:[&amp;_tr:last-child]:border-0"><tr data-slot="table-row" class="astw:hover:bg-muted/50 astw:data-[state=selected]:bg-muted astw:border-b astw:border-border astw:transition-colors"><td data-slot="table-cell" class="astw:px-2 astw:py-2 astw:first:pl-6 astw:last:pr-6 astw:align-middle astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0">Item</td></tr></tbody></table></div>"`;
7+
exports[`Table > snapshots > table with caption 1`] = `"<div data-slot="table-container" class="astw:relative astw:w-full astw:overflow-x-auto"><table data-slot="table" class="astw:w-full astw:caption-bottom astw:text-sm"><caption data-slot="table-caption" class="astw:text-muted-foreground astw:mt-4 astw:text-sm">A list of items</caption><thead data-slot="table-header" class="astw:[&amp;_tr]:border-b astw:[&amp;_tr]:border-border"><tr data-slot="table-row" class="astw:hover:bg-muted/50 astw:data-[state=selected]:bg-muted astw:border-b astw:border-border astw:transition-colors"><th data-slot="table-head" class="astw:text-foreground astw:h-10 astw:px-2 astw:first:pl-6 astw:last:pr-6 astw:align-middle astw:font-medium astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0 astw:text-left">Name</th></tr></thead><tbody data-slot="table-body" class="astw:[&amp;_tr:last-child]:border-0"><tr data-slot="table-row" class="astw:hover:bg-muted/50 astw:data-[state=selected]:bg-muted astw:border-b astw:border-border astw:transition-colors"><td data-slot="table-cell" class="astw:px-2 astw:py-2 astw:first:pl-6 astw:last:pr-6 astw:align-middle astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0 astw:text-left">Item</td></tr></tbody></table></div>"`;
88

9-
exports[`Table > snapshots > table with containerClassName 1`] = `"<div data-slot="table-container" class="astw:relative astw:w-full astw:overflow-x-auto custom-container"><table data-slot="table" class="astw:w-full astw:caption-bottom astw:text-sm"><thead data-slot="table-header" class="astw:[&amp;_tr]:border-b astw:[&amp;_tr]:border-border"><tr data-slot="table-row" class="astw:hover:bg-muted/50 astw:data-[state=selected]:bg-muted astw:border-b astw:border-border astw:transition-colors"><th data-slot="table-head" class="astw:text-foreground astw:h-10 astw:px-2 astw:first:pl-6 astw:last:pr-6 astw:text-left astw:align-middle astw:font-medium astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0">Name</th></tr></thead><tbody data-slot="table-body" class="astw:[&amp;_tr:last-child]:border-0"><tr data-slot="table-row" class="astw:hover:bg-muted/50 astw:data-[state=selected]:bg-muted astw:border-b astw:border-border astw:transition-colors"><td data-slot="table-cell" class="astw:px-2 astw:py-2 astw:first:pl-6 astw:last:pr-6 astw:align-middle astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0">Item</td></tr></tbody></table></div>"`;
9+
exports[`Table > snapshots > table with containerClassName 1`] = `"<div data-slot="table-container" class="astw:relative astw:w-full astw:overflow-x-auto custom-container"><table data-slot="table" class="astw:w-full astw:caption-bottom astw:text-sm"><thead data-slot="table-header" class="astw:[&amp;_tr]:border-b astw:[&amp;_tr]:border-border"><tr data-slot="table-row" class="astw:hover:bg-muted/50 astw:data-[state=selected]:bg-muted astw:border-b astw:border-border astw:transition-colors"><th data-slot="table-head" class="astw:text-foreground astw:h-10 astw:px-2 astw:first:pl-6 astw:last:pr-6 astw:align-middle astw:font-medium astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0 astw:text-left">Name</th></tr></thead><tbody data-slot="table-body" class="astw:[&amp;_tr:last-child]:border-0"><tr data-slot="table-row" class="astw:hover:bg-muted/50 astw:data-[state=selected]:bg-muted astw:border-b astw:border-border astw:transition-colors"><td data-slot="table-cell" class="astw:px-2 astw:py-2 astw:first:pl-6 astw:last:pr-6 astw:align-middle astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0 astw:text-left">Item</td></tr></tbody></table></div>"`;
1010

11-
exports[`Table > snapshots > table with footer 1`] = `"<div data-slot="table-container" class="astw:relative astw:w-full astw:overflow-x-auto"><table data-slot="table" class="astw:w-full astw:caption-bottom astw:text-sm"><thead data-slot="table-header" class="astw:[&amp;_tr]:border-b astw:[&amp;_tr]:border-border"><tr data-slot="table-row" class="astw:hover:bg-muted/50 astw:data-[state=selected]:bg-muted astw:border-b astw:border-border astw:transition-colors"><th data-slot="table-head" class="astw:text-foreground astw:h-10 astw:px-2 astw:first:pl-6 astw:last:pr-6 astw:text-left astw:align-middle astw:font-medium astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0">Product</th><th data-slot="table-head" class="astw:text-foreground astw:h-10 astw:px-2 astw:first:pl-6 astw:last:pr-6 astw:text-left astw:align-middle astw:font-medium astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0">Price</th></tr></thead><tbody data-slot="table-body" class="astw:[&amp;_tr:last-child]:border-0"><tr data-slot="table-row" class="astw:hover:bg-muted/50 astw:data-[state=selected]:bg-muted astw:border-b astw:border-border astw:transition-colors"><td data-slot="table-cell" class="astw:px-2 astw:py-2 astw:first:pl-6 astw:last:pr-6 astw:align-middle astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0">Widget</td><td data-slot="table-cell" class="astw:px-2 astw:py-2 astw:first:pl-6 astw:last:pr-6 astw:align-middle astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0">$10</td></tr></tbody><tfoot data-slot="table-footer" class="astw:border-t astw:border-border astw:font-medium astw:[&amp;>tr]:last:border-b-0"><tr data-slot="table-row" class="astw:hover:bg-muted/50 astw:data-[state=selected]:bg-muted astw:border-b astw:border-border astw:transition-colors"><td data-slot="table-cell" class="astw:px-2 astw:py-2 astw:first:pl-6 astw:last:pr-6 astw:align-middle astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0">Total</td><td data-slot="table-cell" class="astw:px-2 astw:py-2 astw:first:pl-6 astw:last:pr-6 astw:align-middle astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0">$10</td></tr></tfoot></table></div>"`;
11+
exports[`Table > snapshots > table with footer 1`] = `"<div data-slot="table-container" class="astw:relative astw:w-full astw:overflow-x-auto"><table data-slot="table" class="astw:w-full astw:caption-bottom astw:text-sm"><thead data-slot="table-header" class="astw:[&amp;_tr]:border-b astw:[&amp;_tr]:border-border"><tr data-slot="table-row" class="astw:hover:bg-muted/50 astw:data-[state=selected]:bg-muted astw:border-b astw:border-border astw:transition-colors"><th data-slot="table-head" class="astw:text-foreground astw:h-10 astw:px-2 astw:first:pl-6 astw:last:pr-6 astw:align-middle astw:font-medium astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0 astw:text-left">Product</th><th data-slot="table-head" class="astw:text-foreground astw:h-10 astw:px-2 astw:first:pl-6 astw:last:pr-6 astw:align-middle astw:font-medium astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0 astw:text-left">Price</th></tr></thead><tbody data-slot="table-body" class="astw:[&amp;_tr:last-child]:border-0"><tr data-slot="table-row" class="astw:hover:bg-muted/50 astw:data-[state=selected]:bg-muted astw:border-b astw:border-border astw:transition-colors"><td data-slot="table-cell" class="astw:px-2 astw:py-2 astw:first:pl-6 astw:last:pr-6 astw:align-middle astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0 astw:text-left">Widget</td><td data-slot="table-cell" class="astw:px-2 astw:py-2 astw:first:pl-6 astw:last:pr-6 astw:align-middle astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0 astw:text-left">$10</td></tr></tbody><tfoot data-slot="table-footer" class="astw:border-t astw:border-border astw:font-medium astw:[&amp;>tr]:last:border-b-0"><tr data-slot="table-row" class="astw:hover:bg-muted/50 astw:data-[state=selected]:bg-muted astw:border-b astw:border-border astw:transition-colors"><td data-slot="table-cell" class="astw:px-2 astw:py-2 astw:first:pl-6 astw:last:pr-6 astw:align-middle astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0 astw:text-left">Total</td><td data-slot="table-cell" class="astw:px-2 astw:py-2 astw:first:pl-6 astw:last:pr-6 astw:align-middle astw:whitespace-nowrap astw:[&amp;:has([role=checkbox])]:pr-0 astw:text-left">$10</td></tr></tfoot></table></div>"`;

packages/core/src/components/table.test.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,53 @@ afterEach(() => {
77
});
88

99
describe("Table", () => {
10+
describe("alignment", () => {
11+
it("right-aligns header cells via align prop", () => {
12+
const { container } = render(
13+
<Table.Root>
14+
<Table.Header>
15+
<Table.Row>
16+
<Table.Head align="right">Amount</Table.Head>
17+
</Table.Row>
18+
</Table.Header>
19+
</Table.Root>,
20+
);
21+
const head = container.querySelector('[data-slot="table-head"]');
22+
expect(head?.className).toContain("astw:text-right");
23+
expect(head?.className).not.toContain("astw:text-left");
24+
});
25+
26+
it("right-aligns data cells via align prop", () => {
27+
const { container } = render(
28+
<Table.Root>
29+
<Table.Body>
30+
<Table.Row>
31+
<Table.Cell align="right">123</Table.Cell>
32+
</Table.Row>
33+
</Table.Body>
34+
</Table.Root>,
35+
);
36+
const cell = container.querySelector('[data-slot="table-cell"]');
37+
expect(cell?.className).toContain("astw:text-right");
38+
expect(cell?.className).not.toContain("astw:text-left");
39+
});
40+
41+
it("left-aligns data cells by default", () => {
42+
const { container } = render(
43+
<Table.Root>
44+
<Table.Body>
45+
<Table.Row>
46+
<Table.Cell>123</Table.Cell>
47+
</Table.Row>
48+
</Table.Body>
49+
</Table.Root>,
50+
);
51+
const cell = container.querySelector('[data-slot="table-cell"]');
52+
expect(cell?.className).toContain("astw:text-left");
53+
expect(cell?.className).not.toContain("astw:text-right");
54+
});
55+
});
56+
1057
describe("snapshots", () => {
1158
it("basic table with header and body", () => {
1259
const { container } = render(

packages/core/src/components/table.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ type RootProps = React.ComponentProps<"table"> & {
77
containerClassName?: string;
88
};
99

10+
type CellAlign = "left" | "right";
11+
type HeadProps = React.ComponentProps<"th"> & { align?: CellAlign };
12+
type CellProps = React.ComponentProps<"td"> & { align?: CellAlign };
13+
1014
/**
1115
* The root table element with a horizontally scrollable container.
1216
*
@@ -101,12 +105,13 @@ function Row({ className, ...props }: React.ComponentProps<"tr">) {
101105
Row.displayName = "Table.Row";
102106

103107
/** A table header cell (`<th>`). */
104-
function Head({ className, ...props }: React.ComponentProps<"th">) {
108+
function Head({ className, align = "left", ...props }: HeadProps) {
105109
return (
106110
<th
107111
data-slot="table-head"
108112
className={cn(
109-
"astw:text-foreground astw:h-10 astw:px-2 astw:first:pl-6 astw:last:pr-6 astw:text-left astw:align-middle astw:font-medium astw:whitespace-nowrap astw:[&:has([role=checkbox])]:pr-0",
113+
"astw:text-foreground astw:h-10 astw:px-2 astw:first:pl-6 astw:last:pr-6 astw:align-middle astw:font-medium astw:whitespace-nowrap astw:[&:has([role=checkbox])]:pr-0",
114+
align === "right" ? "astw:text-right" : "astw:text-left",
110115
className,
111116
)}
112117
{...props}
@@ -116,12 +121,13 @@ function Head({ className, ...props }: React.ComponentProps<"th">) {
116121
Head.displayName = "Table.Head";
117122

118123
/** A table data cell (`<td>`). */
119-
function Cell({ className, ...props }: React.ComponentProps<"td">) {
124+
function Cell({ className, align = "left", ...props }: CellProps) {
120125
return (
121126
<td
122127
data-slot="table-cell"
123128
className={cn(
124129
"astw:px-2 astw:py-2 astw:first:pl-6 astw:last:pr-6 astw:align-middle astw:whitespace-nowrap astw:[&:has([role=checkbox])]:pr-0",
130+
align === "right" ? "astw:text-right" : "astw:text-left",
125131
className,
126132
)}
127133
{...props}

0 commit comments

Comments
 (0)