Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
96 changes: 96 additions & 0 deletions src/components/experimental/Table/Table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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;

/* Z-Index is needed for sticky header cells to be on top of other cells */
const Column = styled(BaseColumn)`
position: sticky;
top: 0;
z-index: 1;
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