Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .stylelintrc
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"rules": {
"declaration-empty-line-before": null,
"declaration-property-unit-whitelist": {
"/.*/": ["rem", "deg", "fr", "ms", "%", "px", "vw"]
"/.*/": ["rem", "deg", "fr", "ms", "%", "px", "vw", "vh"]
},
"declaration-property-value-blacklist": {
"/.*/": ["(\\d+[1]+px|[^1]+px)"]
Expand Down
95 changes: 95 additions & 0 deletions src/components/experimental/Table/Table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {
Table as BaseTable,
TableProps,
Cell as BaseCell,
Column as BaseColumn,
Row as BaseRow,
TableBody,
TableHeader
} from 'react-aria-components';
import styled from 'styled-components';
import { get } from '../../../utils/experimental/themeGet';
import { textStyles } from '../Text/Text';
import { getSemanticValue } from '../../../essentials/experimental';

const Table = styled(BaseTable)`
border-collapse: collapse;
border-spacing: 0;
position: relative;
width: 100%;
max-height: 100vh;
background: ${getSemanticValue('surface')};
color: ${getSemanticValue('on-surface')};
` as typeof BaseTable;

const Cell = styled(BaseCell)`
padding: 0 ${get('space.3')};
position: relative;

&::before {
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
content: '';
border-radius: inherit;
opacity: 0;
transition: opacity ease 200ms;
}

&:first-of-type {
border-radius: ${get('radii.4')} 0 0 ${get('radii.4')};
}

&:last-of-type {
border-radius: 0 ${get('radii.4')} ${get('radii.4')} 0;
}

&[data-focused] {
outline: 0;
}
` as typeof BaseCell;

const Column = styled(BaseColumn)`
position: sticky;
top: 0;
z-index: 1; /* for sticky headers to be on top of cells */
padding: 0 ${get('space.3')};
height: 3rem;
background: ${getSemanticValue('surface')};
border-bottom: 1px solid ${getSemanticValue('divider')};
text-align: start;
white-space: nowrap;
outline: 0;
${textStyles.variants.title2}
` as typeof BaseColumn;

const Row = styled(BaseRow)`
height: 3rem;
border-bottom: 1px solid ${getSemanticValue('divider')};
border-radius: ${get('radii.4')};
${textStyles.variants.body1}

&[data-hovered] td::before {
background: ${getSemanticValue('on-surface')};
opacity: 0.08;
}

&[data-selected] {
background: ${getSemanticValue('interactive-container')};
}

&[data-focused] {
outline: 0.125rem solid ${getSemanticValue('accent')};
outline-offset: -0.125rem;
}
` as typeof BaseRow;

const Skeleton = styled.div`
height: 1rem;
border-radius: ${get('radii.2')};
background: ${getSemanticValue('surface-variant')};
`;

export { Table, TableProps, Cell, Column, Row, TableBody, TableHeader, Skeleton };
169 changes: 169 additions & 0 deletions src/components/experimental/Table/docs/Table.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import React from 'react';
import { StoryObj, Meta } from '@storybook/react';
import { Table, TableHeader, TableBody, Row, Cell, Column, Skeleton } from '../Table';
import { Text } from '../../Text/Text';

const meta: Meta = {
title: 'Experimental/Components/Table',
component: Table,
parameters: {
layout: 'centered'
},
args: {
label: 'Files'
}
};

export default meta;

type Story = StoryObj<typeof Table>;

export const Default: Story = {
render: () => {
const columns: Array<{ id: string; name: string; isRowHeader?: boolean }> = [
{ name: 'Name', id: 'name', isRowHeader: true },
{ name: 'Type', id: 'type' },
{ name: 'Date Modified', id: 'date' }
];

const rows: Array<{ id: number; name: string; date: string; type: string }> = [
{ id: 1, name: 'Games', date: '6/7/2020', type: 'File folder' },
{ id: 2, name: 'Program Files', date: '4/7/2021', type: 'File folder' },
{ id: 3, name: 'bootmgr', date: '11/20/2010', type: 'System file' },
{ id: 4, name: 'log.txt', date: '1/18/2016', type: 'Text Document' },
{ id: 5, name: 'log.txt', date: '1/18/2016', type: 'Text Document' },
{ id: 6, name: 'log.txt', date: '1/18/2016', type: 'Text Document' },
{ id: 7, name: 'log.txt', date: '1/18/2016', type: 'Text Document' }
];

return (
<Table aria-label="Files" selectionMode="multiple" selectionBehavior="replace">
<TableHeader columns={columns}>
{column => <Column isRowHeader={column.isRowHeader}>{column.name}</Column>}
</TableHeader>
<TableBody items={rows}>
{item => <Row columns={columns}>{column => <Cell>{item[column.id]}</Cell>}</Row>}
</TableBody>
</Table>
);
}
};

export const Loading: Story = {
render: () => {
const columns: Array<{ id: string; name: string; isRowHeader?: boolean }> = [
{ name: 'Name', id: 'name', isRowHeader: true },
{ name: 'Type', id: 'type' },
{ name: 'Date Modified', id: 'date' }
];

return (
<Table aria-label="Files">
<TableHeader columns={columns}>
{column => <Column isRowHeader={column.isRowHeader}>{column.name}</Column>}
</TableHeader>
<TableBody items={[{ id: 1 }, { id: 2 }, { id: 3 }]}>
{() => (
<Row columns={columns}>
{() => (
<Cell>
<Skeleton />
</Cell>
)}
</Row>
)}
</TableBody>
</Table>
);
}
};

export const Empty: Story = {
render: () => {
const columns: Array<{ id: string; name: string; isRowHeader?: boolean }> = [
{ name: 'Name', id: 'name', isRowHeader: true },
{ name: 'Type', id: 'type' },
{ name: 'Date Modified', id: 'date' }
];

return (
<Table aria-label="Files">
<TableHeader columns={columns}>
{column => <Column isRowHeader={column.isRowHeader}>{column.name}</Column>}
</TableHeader>
<TableBody
items={[]}
renderEmptyState={() => (
<div style={{ padding: '1rem', textAlign: 'center' }}>
<Text variant="body1">No results found</Text>
</div>
)}
>
{[]}
</TableBody>
</Table>
);
}
};

export const Async: Story = {
render: () => {
type Character = { name: string; height: number; mass: number; birth_year: string };
const emptyCharacter: Character = {
name: '',
height: 0,
mass: 0,
birth_year: ''
};
const pageSize = 10;
const [isLoading, setIsLoading] = React.useState(true);
const [items, setItems] = React.useState<Character[]>(
/* eslint-disable-next-line unicorn/no-new-array */
new Array(pageSize).fill(emptyCharacter).map((value, idx) => ({ ...value, name: idx.toString() }))
);

React.useEffect(() => {
let ignore = false;

async function startFetching() {
const res = await fetch(`https://swapi.py4e.com/api/people`);
const json = await res.json();

if (!ignore) {
setItems(json.results);
}

setIsLoading(false);
}

// eslint-disable-next-line no-void
void startFetching();

return () => {
ignore = true;
};
}, []);

const columns: Array<{ id: string; name: string; isRowHeader?: boolean }> = [
{ name: 'Name', id: 'name', isRowHeader: true },
{ name: 'Height', id: 'height' },
{ name: 'Mass', id: 'mass' },
{ name: 'Birth Year', id: 'birth_year' }
];

return (
<Table aria-label="Star Wars Characters">
<TableHeader columns={columns}>
{column => <Column isRowHeader={column.isRowHeader}>{column.name}</Column>}
</TableHeader>
<TableBody items={items}>
{item => (
<Row id={item.name} columns={columns}>
{column => <Cell>{isLoading ? <Skeleton /> : item[column.id]}</Cell>}
</Row>
)}
</TableBody>
</Table>
);
}
};
1 change: 1 addition & 0 deletions src/components/experimental/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export { Label } from './Label/Label';
export { ListBox, ListBoxItem } from './ListBox/ListBox';
export { Popover } from './Popover/Popover';
export { Select } from './Select/Select';
export { Table } from './Table/Table';
export { Text } from './Text/Text';
export { TextField } from './TextField/TextField';
export { TimeField } from './TimeField/TimeField';
Expand Down