Skip to content

Commit 55a2f0b

Browse files
authored
feat(components): add Table (#1365)
* feat(components): add `Table` * fix: doc * chore: add changeset * fix: resizable align * fix: remove menu * fix: test
1 parent c610769 commit 55a2f0b

File tree

10 files changed

+615
-1
lines changed

10 files changed

+615
-1
lines changed

.changeset/tame-mangos-greet.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@launchpad-ui/components": patch
3+
---
4+
5+
Add `Table`
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { render, screen } from '../../../test/utils';
4+
import { Cell, Column, ResizableTableContainer, Row, Table, TableBody, TableHeader } from '../src';
5+
6+
describe('Table', () => {
7+
it('renders', () => {
8+
render(
9+
<ResizableTableContainer>
10+
<Table>
11+
<TableHeader>
12+
<Column isRowHeader>Col 1</Column>
13+
<Column>Col 2</Column>
14+
</TableHeader>
15+
<TableBody>
16+
<Row>
17+
<Cell>Cell 1</Cell>
18+
<Cell>Cell 2</Cell>
19+
</Row>
20+
</TableBody>
21+
</Table>
22+
</ResizableTableContainer>,
23+
);
24+
expect(screen.getByRole('grid')).toBeVisible();
25+
});
26+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { ForwardedRef } from 'react';
2+
import type { DropIndicatorProps } from 'react-aria-components';
3+
4+
import { cva } from 'class-variance-authority';
5+
import { forwardRef } from 'react';
6+
import { DropIndicator as AriaDropIndicator, composeRenderProps } from 'react-aria-components';
7+
8+
import styles from './styles/DropIndicator.module.css';
9+
10+
const indicator = cva(styles.indicator);
11+
12+
const _DropIndicator = (props: DropIndicatorProps, ref: ForwardedRef<HTMLElement>) => {
13+
return (
14+
<AriaDropIndicator
15+
{...props}
16+
ref={ref}
17+
className={composeRenderProps(props.className, (className, renderProps) =>
18+
indicator({ ...renderProps, className }),
19+
)}
20+
/>
21+
);
22+
};
23+
24+
/**
25+
* A DropIndicator is rendered between items in a collection to indicate where dropped data will be inserted.
26+
*/
27+
const DropIndicator = forwardRef(_DropIndicator);
28+
29+
export { DropIndicator };
30+
export type { DropIndicatorProps };

packages/components/src/Table.tsx

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
import type { forwardRefType } from '@react-types/shared';
2+
import type { ForwardedRef } from 'react';
3+
import type {
4+
CellProps,
5+
ColumnProps,
6+
ColumnResizerProps,
7+
ResizableTableContainerProps,
8+
RowProps,
9+
TableBodyProps,
10+
TableHeaderProps,
11+
TableProps,
12+
} from 'react-aria-components';
13+
14+
import { Icon } from '@launchpad-ui/icons';
15+
import { cva } from 'class-variance-authority';
16+
import { createContext, forwardRef, useContext } from 'react';
17+
import { VisuallyHidden } from 'react-aria';
18+
import {
19+
Cell as AriaCell,
20+
Column as AriaColumn,
21+
ColumnResizer as AriaColumnResizer,
22+
ResizableTableContainer as AriaResizableTableContainer,
23+
Row as AriaRow,
24+
Table as AriaTable,
25+
TableBody as AriaTableBody,
26+
TableHeader as AriaTableHeader,
27+
Collection,
28+
Provider,
29+
composeRenderProps,
30+
useTableOptions,
31+
} from 'react-aria-components';
32+
33+
import { Checkbox } from './Checkbox';
34+
import { IconButton } from './IconButton';
35+
import styles from './styles/Table.module.css';
36+
37+
const table = cva(styles.table);
38+
const column = cva(styles.column);
39+
const header = cva(styles.header);
40+
const body = cva(styles.body);
41+
const row = cva(styles.row);
42+
const cell = cva(styles.cell);
43+
const resizer = cva(styles.resizer);
44+
45+
const ResizableTableContainerContext = createContext<{ resizable: boolean } | null>({
46+
resizable: false,
47+
});
48+
49+
const _Table = (props: TableProps, ref: ForwardedRef<HTMLTableElement>) => {
50+
return (
51+
<AriaTable
52+
{...props}
53+
ref={ref}
54+
className={composeRenderProps(props.className, (className, renderProps) =>
55+
table({ ...renderProps, className }),
56+
)}
57+
/>
58+
);
59+
};
60+
61+
/**
62+
* A table displays data in rows and columns and enables a user to navigate its contents via directional navigation keys, and optionally supports row selection and sorting.
63+
*
64+
* https://react-spectrum.adobe.com/react-aria/Table.html
65+
*/
66+
const Table = forwardRef(_Table);
67+
68+
const _Column = (props: ColumnProps, ref: ForwardedRef<HTMLTableCellElement>) => {
69+
const ctx = useContext(ResizableTableContainerContext);
70+
return (
71+
<AriaColumn
72+
{...props}
73+
ref={ref}
74+
className={composeRenderProps(props.className, (className, renderProps) =>
75+
column({ ...renderProps, className }),
76+
)}
77+
>
78+
{composeRenderProps(props.children, (children, { allowsSorting, sortDirection }) => (
79+
<div className={styles.flex}>
80+
<span className={styles.truncate}>{children}</span>
81+
{allowsSorting && sortDirection && (
82+
<Icon name={sortDirection === 'ascending' ? 'caret-up' : 'caret-down'} size="small" />
83+
)}
84+
{ctx?.resizable && <ColumnResizer />}
85+
</div>
86+
))}
87+
</AriaColumn>
88+
);
89+
};
90+
91+
/**
92+
* A column within a `<Table>`.
93+
*
94+
* https://react-spectrum.adobe.com/react-aria/Table.html
95+
*/
96+
const Column = forwardRef(_Column);
97+
98+
const _TableHeader = <T extends object>(
99+
{ className, ...props }: TableHeaderProps<T>,
100+
ref: ForwardedRef<HTMLTableSectionElement>,
101+
) => {
102+
const { selectionBehavior, selectionMode, allowsDragging } = useTableOptions();
103+
return (
104+
<AriaTableHeader {...props} ref={ref} className={header({ className })}>
105+
{allowsDragging && (
106+
<Column>
107+
<VisuallyHidden>Drag</VisuallyHidden>
108+
</Column>
109+
)}
110+
{selectionBehavior === 'toggle' && (
111+
<AriaColumn>{selectionMode === 'multiple' && <Checkbox slot="selection" />}</AriaColumn>
112+
)}
113+
<Collection items={props.columns}>{props.children}</Collection>
114+
</AriaTableHeader>
115+
);
116+
};
117+
118+
/**
119+
* A header within a `<Table>`, containing the table columns.
120+
*
121+
* https://react-spectrum.adobe.com/react-aria/Table.html
122+
*/
123+
const TableHeader = (forwardRef as forwardRefType)(_TableHeader);
124+
125+
const _TableBody = <T extends object>(
126+
props: TableBodyProps<T>,
127+
ref: ForwardedRef<HTMLTableSectionElement>,
128+
) => {
129+
return (
130+
<AriaTableBody
131+
{...props}
132+
ref={ref}
133+
className={composeRenderProps(props.className, (className, renderProps) =>
134+
body({ ...renderProps, className }),
135+
)}
136+
/>
137+
);
138+
};
139+
140+
/**
141+
* The body of a `<Table>`, containing the table rows.
142+
*
143+
* https://react-spectrum.adobe.com/react-aria/Table.html
144+
*/
145+
const TableBody = (forwardRef as forwardRefType)(_TableBody);
146+
147+
const _Row = <T extends object>(
148+
{ columns, children, ...props }: RowProps<T>,
149+
ref: ForwardedRef<HTMLTableRowElement>,
150+
) => {
151+
const { selectionBehavior, allowsDragging } = useTableOptions();
152+
153+
return (
154+
<AriaRow
155+
{...props}
156+
ref={ref}
157+
className={composeRenderProps(props.className, (className, renderProps) =>
158+
row({ ...renderProps, className }),
159+
)}
160+
>
161+
{allowsDragging && (
162+
<Cell>
163+
{/* @ts-ignore RAC adds label */}
164+
<IconButton slot="drag" icon="grip-horiz" variant="minimal" size="small" />
165+
</Cell>
166+
)}
167+
{selectionBehavior === 'toggle' && (
168+
<Cell>
169+
<Checkbox slot="selection" />
170+
</Cell>
171+
)}
172+
<Collection items={columns}>{children}</Collection>
173+
</AriaRow>
174+
);
175+
};
176+
177+
/**
178+
* A row within a `<Table>`.
179+
*
180+
* https://react-spectrum.adobe.com/react-aria/Table.html
181+
*/
182+
const Row = (forwardRef as forwardRefType)(_Row);
183+
184+
const _Cell = (props: CellProps, ref: ForwardedRef<HTMLTableCellElement>) => {
185+
return (
186+
<AriaCell
187+
{...props}
188+
ref={ref}
189+
className={composeRenderProps(props.className, (className, renderProps) =>
190+
cell({ ...renderProps, className }),
191+
)}
192+
/>
193+
);
194+
};
195+
196+
/**
197+
* A cell within a table row.
198+
*
199+
* https://react-spectrum.adobe.com/react-aria/Table.html
200+
*/
201+
const Cell = forwardRef(_Cell);
202+
203+
const _ColumnResizer = (props: ColumnResizerProps, ref: ForwardedRef<HTMLDivElement>) => {
204+
return (
205+
<AriaColumnResizer
206+
{...props}
207+
ref={ref}
208+
className={composeRenderProps(props.className, (className, renderProps) =>
209+
resizer({ ...renderProps, className }),
210+
)}
211+
/>
212+
);
213+
};
214+
215+
const ColumnResizer = forwardRef(_ColumnResizer);
216+
217+
const _ResizableTableContainer = (
218+
{ children, ...props }: ResizableTableContainerProps,
219+
ref: ForwardedRef<HTMLDivElement>,
220+
) => {
221+
return (
222+
<AriaResizableTableContainer {...props} ref={ref}>
223+
<Provider values={[[ResizableTableContainerContext, { resizable: true }]]}>
224+
{children}
225+
</Provider>
226+
</AriaResizableTableContainer>
227+
);
228+
};
229+
230+
const ResizableTableContainer = forwardRef(_ResizableTableContainer);
231+
232+
export { Cell, Column, ColumnResizer, ResizableTableContainer, Row, Table, TableBody, TableHeader };
233+
export type {
234+
CellProps,
235+
ColumnProps,
236+
ColumnResizerProps,
237+
ResizableTableContainerProps,
238+
RowProps,
239+
TableProps,
240+
TableBodyProps,
241+
TableHeaderProps,
242+
};

packages/components/src/index.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export type { ComboBoxProps } from './ComboBox';
1919
export type { DateFieldProps, DateInputProps, DateSegmentProps, TimeFieldProps } from './DateField';
2020
export type { DatePickerProps, DateRangePickerProps } from './DatePicker';
2121
export type { DialogProps, DialogTriggerProps } from './Dialog';
22+
export type { DropIndicatorProps } from './DropIndicator';
2223
export type { FieldErrorProps } from './FieldError';
2324
export type { FieldGroupProps } from './FieldGroup';
2425
export type { FileTriggerProps } from './FileTrigger';
@@ -47,6 +48,16 @@ export type { SectionProps } from './Section';
4748
export type { SelectProps, SelectValueProps } from './Select';
4849
export type { SeparatorProps } from './Separator';
4950
export type { SwitchProps } from './Switch';
51+
export type {
52+
CellProps,
53+
ColumnProps,
54+
ColumnResizerProps,
55+
ResizableTableContainerProps,
56+
RowProps,
57+
TableProps,
58+
TableBodyProps,
59+
TableHeaderProps,
60+
} from './Table';
5061
export type { TabProps, TabsProps, TabListProps, TabPanelProps } from './Tabs';
5162
export type { TagGroupProps, TagListProps, TagProps } from './TagGroup';
5263
export type { TextProps } from './Text';
@@ -77,6 +88,7 @@ export { ComboBox, ComboBoxClearButton } from './ComboBox';
7788
export { DateField, DateInput, DateSegment, TimeField } from './DateField';
7889
export { DatePicker, DateRangePicker } from './DatePicker';
7990
export { Dialog, DialogTrigger } from './Dialog';
91+
export { DropIndicator } from './DropIndicator';
8092
export { FieldError } from './FieldError';
8193
export { FieldGroup } from './FieldGroup';
8294
export { FileTrigger } from './FileTrigger';
@@ -108,6 +120,16 @@ export { Section } from './Section';
108120
export { Select, SelectValue } from './Select';
109121
export { Separator } from './Separator';
110122
export { Switch } from './Switch';
123+
export {
124+
Cell,
125+
Column,
126+
ColumnResizer,
127+
ResizableTableContainer,
128+
Row,
129+
Table,
130+
TableBody,
131+
TableHeader,
132+
} from './Table';
111133
export { Tab, Tabs, TabList, TabPanel } from './Tabs';
112134
export { TagGroup, TagList, Tag } from './TagGroup';
113135
export { Text } from './Text';
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.indicator {
2+
&[data-drop-target] {
3+
outline: 1px solid var(--lp-color-border-interactive-focus);
4+
transform: translateZ(0);
5+
}
6+
}

0 commit comments

Comments
 (0)