Skip to content

Commit 8980a56

Browse files
authored
table component (#88)
* table component * fixes * test fix * clean up * requested changes * header color
1 parent 10d493e commit 8980a56

4 files changed

Lines changed: 680 additions & 0 deletions

File tree

registry.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,19 @@
148148
}
149149
]
150150
},
151+
{
152+
"name": "table",
153+
"type": "registry:ui",
154+
"title": "Table",
155+
"description": "Responsive data table with Nebari header, cell, row, footer, caption, and hover styling.",
156+
"registryDependencies": ["utils", "theme"],
157+
"files": [
158+
{
159+
"path": "registry/nebari/ui/table.tsx",
160+
"type": "registry:ui"
161+
}
162+
]
163+
},
151164
{
152165
"name": "alert",
153166
"type": "registry:ui",

registry/nebari/ui/table.tsx

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
// biome-ignore-all lint/a11y/noNoninteractiveTabindex: table scroll containers need keyboard access when content overflows.
2+
import type * as React from 'react';
3+
import { cn } from '@/lib/utils';
4+
5+
type TableProps = React.ComponentProps<'table'> & {
6+
/** Accessible name for the keyboard-focusable horizontal scroll container. */
7+
scrollContainerLabel?: string;
8+
/** Additional classes for the horizontal scroll container. */
9+
scrollContainerClassName?: string;
10+
/** Props forwarded to the horizontal scroll container. */
11+
scrollContainerProps?: Omit<React.ComponentProps<'section'>, 'children'>;
12+
};
13+
14+
type TableHeadProps = Omit<
15+
React.ComponentProps<'th'>,
16+
'onClick' | 'onKeyDown'
17+
> & {
18+
/** Makes the header sortable by rendering its contents in a real button. */
19+
onClick?: React.MouseEventHandler<HTMLButtonElement>;
20+
onKeyDown?: React.KeyboardEventHandler<HTMLButtonElement>;
21+
};
22+
23+
/** Responsive table frame with Nebari border, radius, and surface styling. */
24+
function Table({
25+
className,
26+
scrollContainerClassName,
27+
scrollContainerLabel,
28+
scrollContainerProps,
29+
'aria-label': ariaLabel,
30+
'aria-labelledby': ariaLabelledBy,
31+
...props
32+
}: TableProps) {
33+
const {
34+
className: scrollContainerPropsClassName,
35+
'aria-label': scrollContainerAriaLabel,
36+
'aria-labelledby': scrollContainerAriaLabelledBy,
37+
tabIndex: scrollContainerTabIndex,
38+
...resolvedScrollContainerProps
39+
} = scrollContainerProps ?? {};
40+
const resolvedScrollContainerLabel =
41+
scrollContainerAriaLabel ??
42+
scrollContainerLabel ??
43+
(ariaLabel === undefined
44+
? 'Table scroll area'
45+
: `${ariaLabel} scroll area`);
46+
47+
return (
48+
<section
49+
{...resolvedScrollContainerProps}
50+
aria-label={
51+
scrollContainerAriaLabelledBy === undefined
52+
? resolvedScrollContainerLabel
53+
: undefined
54+
}
55+
aria-labelledby={scrollContainerAriaLabelledBy}
56+
data-slot="table-container"
57+
tabIndex={scrollContainerTabIndex ?? 0}
58+
className={cn(
59+
'relative w-full overflow-x-auto rounded-md border border-border bg-card outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',
60+
scrollContainerClassName,
61+
scrollContainerPropsClassName,
62+
)}
63+
>
64+
<table
65+
aria-label={ariaLabel}
66+
aria-labelledby={ariaLabelledBy}
67+
data-slot="table"
68+
className={cn(
69+
'w-full border-collapse bg-card caption-bottom text-left text-sm',
70+
className,
71+
)}
72+
{...props}
73+
/>
74+
</section>
75+
);
76+
}
77+
78+
function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
79+
return (
80+
<thead
81+
data-slot="table-header"
82+
className={cn('bg-muted', className)}
83+
{...props}
84+
/>
85+
);
86+
}
87+
88+
function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
89+
return (
90+
<tbody
91+
data-slot="table-body"
92+
className={cn('[&_tr:last-child]:border-0', className)}
93+
{...props}
94+
/>
95+
);
96+
}
97+
98+
function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) {
99+
return (
100+
<tfoot
101+
data-slot="table-footer"
102+
className={cn(
103+
'border-t border-border bg-card font-medium [&>tr]:last:border-b-0',
104+
className,
105+
)}
106+
{...props}
107+
/>
108+
);
109+
}
110+
111+
function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
112+
return (
113+
<tr
114+
data-slot="table-row"
115+
className={cn(
116+
'border-b border-border hover:bg-muted/50 data-[state=selected]:bg-muted data-[state=selected]:hover:bg-muted motion-safe:transition-[color,background-color] motion-safe:duration-[--duration-fast] motion-safe:ease-[--ease-standard]',
117+
className,
118+
)}
119+
{...props}
120+
/>
121+
);
122+
}
123+
124+
function TableHead({
125+
className,
126+
children,
127+
onClick,
128+
onKeyDown,
129+
tabIndex,
130+
...props
131+
}: TableHeadProps) {
132+
const isInteractive = onClick != null;
133+
const ariaSort = props['aria-sort'] ?? (isInteractive ? 'none' : undefined);
134+
135+
return (
136+
<th
137+
{...props}
138+
aria-sort={ariaSort}
139+
data-slot="table-head"
140+
tabIndex={isInteractive ? undefined : tabIndex}
141+
className={cn(
142+
'relative h-10 text-left align-middle font-medium text-foreground text-sm leading-5 tracking-normal whitespace-nowrap [&:has([role=checkbox])]:pr-0',
143+
isInteractive ? 'p-0' : 'px-4',
144+
className,
145+
)}
146+
>
147+
{isInteractive ? (
148+
<button
149+
className="inline-flex h-10 w-full cursor-pointer items-center justify-start gap-1 bg-transparent px-4 text-left font-medium text-foreground text-sm leading-5 tracking-normal underline-offset-4 outline-none hover:bg-muted-foreground/10 hover:underline focus-visible:bg-muted-foreground/10 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset motion-safe:transition-[color,background-color] motion-safe:duration-[--duration-fast] motion-safe:ease-[--ease-standard]"
150+
data-slot="table-head-button"
151+
onClick={onClick}
152+
onKeyDown={onKeyDown}
153+
tabIndex={tabIndex}
154+
type="button"
155+
>
156+
{children}
157+
</button>
158+
) : (
159+
children
160+
)}
161+
</th>
162+
);
163+
}
164+
165+
function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
166+
return (
167+
<td
168+
data-slot="table-cell"
169+
className={cn(
170+
'h-12 px-4 py-3 align-middle text-foreground text-sm leading-5 [&:has([role=checkbox])]:pr-0',
171+
className,
172+
)}
173+
{...props}
174+
/>
175+
);
176+
}
177+
178+
function TableCaption({
179+
className,
180+
...props
181+
}: React.ComponentProps<'caption'>) {
182+
return (
183+
<caption
184+
data-slot="table-caption"
185+
className={cn('px-4 py-3 text-muted-foreground text-xs', className)}
186+
{...props}
187+
/>
188+
);
189+
}
190+
191+
export type { TableHeadProps, TableProps };
192+
export {
193+
Table,
194+
TableBody,
195+
TableCaption,
196+
TableCell,
197+
TableFooter,
198+
TableHead,
199+
TableHeader,
200+
TableRow,
201+
};

0 commit comments

Comments
 (0)