Skip to content

Commit 31e0c21

Browse files
Elena RashkovanLena Rashkovan
andauthored
feat(table): add experimental component (#493)
* feat(table): add experimental component * feat(table): add skeleton * docs(table): add examples * fix(table): fix lost generic props * chore: update stylelint whitelist * style(table): move comment --------- Co-authored-by: Lena Rashkovan <[email protected]>
1 parent d960e0a commit 31e0c21

File tree

4 files changed

+267
-1
lines changed

4 files changed

+267
-1
lines changed

.stylelintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"rules": {
1111
"declaration-empty-line-before": null,
1212
"declaration-property-unit-whitelist": {
13-
"/.*/": ["rem", "deg", "fr", "ms", "%", "px", "vw"]
13+
"/.*/": ["rem", "deg", "fr", "ms", "%", "px", "vw", "vh"]
1414
},
1515
"declaration-property-value-blacklist": {
1616
"/.*/": ["(\\d+[1]+px|[^1]+px)"]
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import {
2+
Table as BaseTable,
3+
TableProps,
4+
Cell as BaseCell,
5+
Column as BaseColumn,
6+
Row as BaseRow,
7+
TableBody,
8+
TableHeader
9+
} from 'react-aria-components';
10+
import styled from 'styled-components';
11+
import { get } from '../../../utils/experimental/themeGet';
12+
import { textStyles } from '../Text/Text';
13+
import { getSemanticValue } from '../../../essentials/experimental';
14+
15+
const Table = styled(BaseTable)`
16+
border-collapse: collapse;
17+
border-spacing: 0;
18+
position: relative;
19+
width: 100%;
20+
max-height: 100vh;
21+
background: ${getSemanticValue('surface')};
22+
color: ${getSemanticValue('on-surface')};
23+
` as typeof BaseTable;
24+
25+
const Cell = styled(BaseCell)`
26+
padding: 0 ${get('space.3')};
27+
position: relative;
28+
29+
&::before {
30+
position: absolute;
31+
top: 0;
32+
right: 0;
33+
left: 0;
34+
bottom: 0;
35+
content: '';
36+
border-radius: inherit;
37+
opacity: 0;
38+
transition: opacity ease 200ms;
39+
}
40+
41+
&:first-of-type {
42+
border-radius: ${get('radii.4')} 0 0 ${get('radii.4')};
43+
}
44+
45+
&:last-of-type {
46+
border-radius: 0 ${get('radii.4')} ${get('radii.4')} 0;
47+
}
48+
49+
&[data-focused] {
50+
outline: 0;
51+
}
52+
` as typeof BaseCell;
53+
54+
/* Z-Index is needed for sticky header cells to be on top of other cells */
55+
const Column = styled(BaseColumn)`
56+
position: sticky;
57+
top: 0;
58+
z-index: 1;
59+
padding: 0 ${get('space.3')};
60+
height: 3rem;
61+
background: ${getSemanticValue('surface')};
62+
border-bottom: 1px solid ${getSemanticValue('divider')};
63+
text-align: start;
64+
white-space: nowrap;
65+
outline: 0;
66+
${textStyles.variants.title2}
67+
` as typeof BaseColumn;
68+
69+
const Row = styled(BaseRow)`
70+
height: 3rem;
71+
border-bottom: 1px solid ${getSemanticValue('divider')};
72+
border-radius: ${get('radii.4')};
73+
${textStyles.variants.body1}
74+
75+
&[data-hovered] td::before {
76+
background: ${getSemanticValue('on-surface')};
77+
opacity: 0.08;
78+
}
79+
80+
&[data-selected] {
81+
background: ${getSemanticValue('interactive-container')};
82+
}
83+
84+
&[data-focused] {
85+
outline: 0.125rem solid ${getSemanticValue('accent')};
86+
outline-offset: -0.125rem;
87+
}
88+
` as typeof BaseRow;
89+
90+
const Skeleton = styled.div`
91+
height: 1rem;
92+
border-radius: ${get('radii.2')};
93+
background: ${getSemanticValue('surface-variant')};
94+
`;
95+
96+
export { Table, TableProps, Cell, Column, Row, TableBody, TableHeader, Skeleton };
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import React from 'react';
2+
import { StoryObj, Meta } from '@storybook/react';
3+
import { Table, TableHeader, TableBody, Row, Cell, Column, Skeleton } from '../Table';
4+
import { Text } from '../../Text/Text';
5+
6+
const meta: Meta = {
7+
title: 'Experimental/Components/Table',
8+
component: Table,
9+
parameters: {
10+
layout: 'centered'
11+
},
12+
args: {
13+
label: 'Files'
14+
}
15+
};
16+
17+
export default meta;
18+
19+
type Story = StoryObj<typeof Table>;
20+
21+
export const Default: Story = {
22+
render: () => {
23+
const columns: Array<{ id: string; name: string; isRowHeader?: boolean }> = [
24+
{ name: 'Name', id: 'name', isRowHeader: true },
25+
{ name: 'Type', id: 'type' },
26+
{ name: 'Date Modified', id: 'date' }
27+
];
28+
29+
const rows: Array<{ id: number; name: string; date: string; type: string }> = [
30+
{ id: 1, name: 'Games', date: '6/7/2020', type: 'File folder' },
31+
{ id: 2, name: 'Program Files', date: '4/7/2021', type: 'File folder' },
32+
{ id: 3, name: 'bootmgr', date: '11/20/2010', type: 'System file' },
33+
{ id: 4, name: 'log.txt', date: '1/18/2016', type: 'Text Document' },
34+
{ id: 5, name: 'log.txt', date: '1/18/2016', type: 'Text Document' },
35+
{ id: 6, name: 'log.txt', date: '1/18/2016', type: 'Text Document' },
36+
{ id: 7, name: 'log.txt', date: '1/18/2016', type: 'Text Document' }
37+
];
38+
39+
return (
40+
<Table aria-label="Files" selectionMode="multiple" selectionBehavior="replace">
41+
<TableHeader columns={columns}>
42+
{column => <Column isRowHeader={column.isRowHeader}>{column.name}</Column>}
43+
</TableHeader>
44+
<TableBody items={rows}>
45+
{item => <Row columns={columns}>{column => <Cell>{item[column.id]}</Cell>}</Row>}
46+
</TableBody>
47+
</Table>
48+
);
49+
}
50+
};
51+
52+
export const Loading: Story = {
53+
render: () => {
54+
const columns: Array<{ id: string; name: string; isRowHeader?: boolean }> = [
55+
{ name: 'Name', id: 'name', isRowHeader: true },
56+
{ name: 'Type', id: 'type' },
57+
{ name: 'Date Modified', id: 'date' }
58+
];
59+
60+
return (
61+
<Table aria-label="Files">
62+
<TableHeader columns={columns}>
63+
{column => <Column isRowHeader={column.isRowHeader}>{column.name}</Column>}
64+
</TableHeader>
65+
<TableBody items={[{ id: 1 }, { id: 2 }, { id: 3 }]}>
66+
{() => (
67+
<Row columns={columns}>
68+
{() => (
69+
<Cell>
70+
<Skeleton />
71+
</Cell>
72+
)}
73+
</Row>
74+
)}
75+
</TableBody>
76+
</Table>
77+
);
78+
}
79+
};
80+
81+
export const Empty: Story = {
82+
render: () => {
83+
const columns: Array<{ id: string; name: string; isRowHeader?: boolean }> = [
84+
{ name: 'Name', id: 'name', isRowHeader: true },
85+
{ name: 'Type', id: 'type' },
86+
{ name: 'Date Modified', id: 'date' }
87+
];
88+
89+
return (
90+
<Table aria-label="Files">
91+
<TableHeader columns={columns}>
92+
{column => <Column isRowHeader={column.isRowHeader}>{column.name}</Column>}
93+
</TableHeader>
94+
<TableBody
95+
items={[]}
96+
renderEmptyState={() => (
97+
<div style={{ padding: '1rem', textAlign: 'center' }}>
98+
<Text variant="body1">No results found</Text>
99+
</div>
100+
)}
101+
>
102+
{[]}
103+
</TableBody>
104+
</Table>
105+
);
106+
}
107+
};
108+
109+
export const Async: Story = {
110+
render: () => {
111+
type Character = { name: string; height: number; mass: number; birth_year: string };
112+
const emptyCharacter: Character = {
113+
name: '',
114+
height: 0,
115+
mass: 0,
116+
birth_year: ''
117+
};
118+
const pageSize = 10;
119+
const [isLoading, setIsLoading] = React.useState(true);
120+
const [items, setItems] = React.useState<Character[]>(
121+
/* eslint-disable-next-line unicorn/no-new-array */
122+
new Array(pageSize).fill(emptyCharacter).map((value, idx) => ({ ...value, name: idx.toString() }))
123+
);
124+
125+
React.useEffect(() => {
126+
let ignore = false;
127+
128+
async function startFetching() {
129+
const res = await fetch(`https://swapi.py4e.com/api/people`);
130+
const json = await res.json();
131+
132+
if (!ignore) {
133+
setItems(json.results);
134+
}
135+
136+
setIsLoading(false);
137+
}
138+
139+
// eslint-disable-next-line no-void
140+
void startFetching();
141+
142+
return () => {
143+
ignore = true;
144+
};
145+
}, []);
146+
147+
const columns: Array<{ id: string; name: string; isRowHeader?: boolean }> = [
148+
{ name: 'Name', id: 'name', isRowHeader: true },
149+
{ name: 'Height', id: 'height' },
150+
{ name: 'Mass', id: 'mass' },
151+
{ name: 'Birth Year', id: 'birth_year' }
152+
];
153+
154+
return (
155+
<Table aria-label="Star Wars Characters">
156+
<TableHeader columns={columns}>
157+
{column => <Column isRowHeader={column.isRowHeader}>{column.name}</Column>}
158+
</TableHeader>
159+
<TableBody items={items}>
160+
{item => (
161+
<Row id={item.name} columns={columns}>
162+
{column => <Cell>{isLoading ? <Skeleton /> : item[column.id]}</Cell>}
163+
</Row>
164+
)}
165+
</TableBody>
166+
</Table>
167+
);
168+
}
169+
};

src/components/experimental/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export { Label } from './Label/Label';
1111
export { ListBox, ListBoxItem } from './ListBox/ListBox';
1212
export { Popover } from './Popover/Popover';
1313
export { Select } from './Select/Select';
14+
export { Table } from './Table/Table';
1415
export { Text } from './Text/Text';
1516
export { TextField } from './TextField/TextField';
1617
export { TimeField } from './TimeField/TimeField';

0 commit comments

Comments
 (0)