Skip to content
Open
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
9 changes: 9 additions & 0 deletions .storybook/decorators.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Constrain from '../source/02-layouts/Constrain/Constrain';

const withGlobalWrapper = Story => (
<Constrain modifierClasses="spaced-4">
<Story />
</Constrain>
);

export { withGlobalWrapper };
1 change: 1 addition & 0 deletions source/00-config/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@

@layer config.usage {
@import 'vars/colors.css';
@import 'vars/dynamic.css';
@import 'vars/form.css';
}
3 changes: 2 additions & 1 deletion source/00-config/vars/colors.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@
--selection-text: var(--grayscale-white);

--table-background: var(--grayscale-white);
--tablebackground-foot: var(--grayscale-gray-1);
--table-background-foot: var(--grayscale-gray-1);
--table-background-head: var(--grayscale-gray-1);
--table-background-sorted: var(--brand-blue-light-2);
--table-border: var(--grayscale-gray-5);

--text-link: var(--brand-blue-base);
Expand Down
3 changes: 3 additions & 0 deletions source/00-config/vars/dynamic.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:root {
--scrollbar-width: 0px;
}
37 changes: 37 additions & 0 deletions source/01-global/icon/icons/Sort.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// organize-imports-ignore
// This component is automatically generated.
// SVGs should be added to icon/svgs.
// See the project documentation for more information.
// tslint:disable:ordered-imports
import clsx from 'clsx';
import * as React from 'react';
import type { SVGProps } from 'react';
interface SVGRProps {
title?: string;
titleId?: string;
isHidden?: boolean;
modifierClasses?: string | string[];
}
const SvgSort = ({
modifierClasses,
isHidden,
title,
titleId,
...props
}: SVGProps<SVGSVGElement> & SVGRProps) => {
return (
<svg
role={title ? 'img' : undefined}
aria-hidden={isHidden ? 'true' : 'false'}
viewBox="0 0 320 512"
xmlns="http://www.w3.org/2000/svg"
className={clsx('icon', modifierClasses)}
aria-labelledby={titleId}
{...props}
>
{title ? <title id={titleId}>{title}</title> : null}
<path d="m137.4 41.4c12.5-12.5 32.8-12.5 45.3 0l128 128c9.2 9.2 11.9 22.9 6.9 34.9s-16.6 19.8-29.6 19.8h-256c-12.9 0-24.6-7.8-29.6-19.8s-2.2-25.7 6.9-34.9l128-128zm0 429.3-128-128c-9.2-9.2-11.9-22.9-6.9-34.9s16.6-19.8 29.6-19.8h255.9c12.9 0 24.6 7.8 29.6 19.8s2.2 25.7-6.9 34.9l-128 128c-12.5 12.5-32.8 12.5-45.3 0z" />
</svg>
);
};
export default SvgSort;
37 changes: 37 additions & 0 deletions source/01-global/icon/icons/Sorted.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// organize-imports-ignore
// This component is automatically generated.
// SVGs should be added to icon/svgs.
// See the project documentation for more information.
// tslint:disable:ordered-imports
import clsx from 'clsx';
import * as React from 'react';
import type { SVGProps } from 'react';
interface SVGRProps {
title?: string;
titleId?: string;
isHidden?: boolean;
modifierClasses?: string | string[];
}
const SvgSorted = ({
modifierClasses,
isHidden,
title,
titleId,
...props
}: SVGProps<SVGSVGElement> & SVGRProps) => {
return (
<svg
role={title ? 'img' : undefined}
aria-hidden={isHidden ? 'true' : 'false'}
viewBox="0 0 320 512"
xmlns="http://www.w3.org/2000/svg"
className={clsx('icon', modifierClasses)}
aria-labelledby={titleId}
{...props}
>
{title ? <title id={titleId}>{title}</title> : null}
<path d="m182.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-128 128c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8h256c12.9 0 24.6-7.8 29.6-19.8s2.2-25.7-6.9-34.9l-128-128z" />
</svg>
);
};
export default SvgSorted;
6 changes: 6 additions & 0 deletions source/01-global/icon/icons/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ const Icons = {
Rss: dynamic(() => import('./Rss'), {
loading: () => <svg className="icon"></svg>,
}),
Sort: dynamic(() => import('./Sort'), {
loading: () => <svg className="icon"></svg>,
}),
Sorted: dynamic(() => import('./Sorted'), {
loading: () => <svg className="icon"></svg>,
}),
Twitter: dynamic(() => import('./Twitter'), {
loading: () => <svg className="icon"></svg>,
}),
Expand Down
1 change: 1 addition & 0 deletions source/01-global/icon/svgs/sort.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions source/01-global/icon/svgs/sorted.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 8 additions & 1 deletion source/02-layouts/SiteContainer/SiteContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
'use client';

import { GessoComponent } from 'gesso';
import { ReactNode } from 'react';
import { JSX, ReactNode, useEffect } from 'react';
import setScrollbarProperty from '../../06-utility/setScrollbarProperty';
import styles from './site-container.module.css';

interface SiteContainerProps extends GessoComponent {
children: ReactNode;
}

function SiteContainer({ children }: SiteContainerProps): JSX.Element {
useEffect(() => {
setScrollbarProperty();
}, []);

return <div className={styles['site-container']}>{children}</div>;
}

Expand Down
48 changes: 48 additions & 0 deletions source/03-components/Table/SortableHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { JSX, MouseEvent } from 'react';
import Sort from '../../01-global/icon/icons/Sort';
import Sorted from '../../01-global/icon/icons/Sorted';
import styles from './table.module.css';

interface SortableHeaderProps {
column: string;
label: string;
direction?: 'ascending' | 'descending';
updateSort: (newSortColumn: string) => void;
}

function SortableHeader({
column,
label,
direction,
updateSort,
}: SortableHeaderProps): JSX.Element {
const Icon = direction ? Sorted : Sort;

const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
updateSort(column);
};

return (
<th
scope="col"
data-sortable={true}
aria-label={`${label}, sortable column, currently ${direction ? `sorted ${direction}` : 'unsorted'}`}
aria-sort={direction}
>
<div className={styles['header-inner']}>
{label}
<button
className={styles['header-button']}
onClick={handleClick}
type="button"
title={`Click to sort by ${label} in ${direction === 'ascending' ? 'descending' : 'ascending'} order.`}
>
<Icon isHidden={true} />
</button>
</div>
</th>
);
}

export default SortableHeader;
189 changes: 189 additions & 0 deletions source/03-components/Table/Table.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { Meta, StoryObj } from '@storybook/react';
import clsx from 'clsx';
import { useMemo, useState } from 'react';
import { withGlobalWrapper } from '../../../.storybook/decorators';
import SiteContainer from '../../02-layouts/SiteContainer/SiteContainer';
import SortableHeader from './SortableHeader';
import tableData from './table.data.yml';
import styles from './table.module.css';

type ExampleData = {
city: string;
country: string;
population: number;
};

const settings: Meta<{
isScrollable?: boolean;
caption?: string;
data: ExampleData[];
}> = {
title: 'Components/Table',
decorators: [
Story => (
<SiteContainer>
<Story />
</SiteContainer>
),
withGlobalWrapper,
],
args: {
isScrollable: false,
caption: 'Table caption',
},
argTypes: {
isScrollable: {
type: 'boolean',
},
caption: {
type: 'string',
},
},
parameters: {
controls: {
include: ['isScrollable', 'caption'],
},
},
tags: ['autodocs'],
};

type Story = StoryObj<{
isScrollable?: boolean;
caption?: string;
data: ExampleData[];
}>;

const Default: Story = {
render: ({ isScrollable, caption, data }) => {
return (
<table
className={clsx({
[styles['is-scrollable']]: isScrollable,
})}
>
{caption && <caption>{caption}</caption>}
<thead>
<tr>
<th scope="col">City</th>
<th scope="col">Country</th>
<th scope="col">Population</th>
</tr>
</thead>
<tbody>
{data.map(row => (
<tr key={row.city}>
<td>{row.city}</td>
<td>{row.country}</td>
<td>{Number(row.population).toLocaleString('en-US')}</td>
</tr>
))}
</tbody>
</table>
);
},
args: {
...tableData,
},
};

const Sortable: Story = {
render: function SortableTable({ isScrollable, caption, data }) {
const [sortColumn, setSortColumn] = useState<
'city' | 'country' | 'population' | undefined
>(undefined);
const [sortDirection, setSortDirection] = useState<
'ascending' | 'descending'
>('ascending');
const sortedData = useMemo(
() =>
sortColumn
? [...data].sort((thisRow, nextRow) => {
const cellA =
sortDirection === 'ascending'
? thisRow[sortColumn]
: nextRow[sortColumn];
const cellB =
sortDirection === 'ascending'
? nextRow[sortColumn]
: thisRow[sortColumn];
if (typeof cellA === 'number' && typeof cellB === 'number') {
return cellA - cellB;
}
return cellA.toString().localeCompare(cellB.toString());
})
: data,
[data, sortColumn, sortDirection],
);

const updateSort = (newSortColumn: string) => {
if (sortColumn === newSortColumn) {
setSortDirection(prevState =>
prevState === 'ascending' ? 'descending' : 'ascending',
);
} else {
setSortColumn(newSortColumn as 'city' | 'country' | 'population');
setSortDirection('ascending');
}
};

return (
<>
<table
className={clsx(styles['is-sortable'], {
[styles['is-scrollable']]: isScrollable,
})}
>
{caption && <caption>{caption}</caption>}
<thead>
<tr>
<SortableHeader
key="city"
column="city"
label="City"
updateSort={updateSort}
direction={sortColumn === 'city' ? sortDirection : undefined}
/>
<SortableHeader
key="country"
column="country"
label="Country"
updateSort={updateSort}
direction={sortColumn === 'country' ? sortDirection : undefined}
/>
<SortableHeader
key="population"
column="population"
label="Population"
updateSort={updateSort}
direction={
sortColumn === 'population' ? sortDirection : undefined
}
/>
</tr>
</thead>
<tbody>
{sortedData.map(row => (
<tr key={row.city}>
<td>{row.city}</td>
<td>{row.country}</td>
<td>{Number(row.population).toLocaleString('en-US')}</td>
</tr>
))}
</tbody>
</table>
<div className={styles['announcement-region']} aria-live="polite">
{sortColumn
? `The table ${caption && `named "${caption}"`} is now sorted by
${sortColumn} in ${sortDirection} order.`
: ''}
</div>
</>
);
},
args: {
...tableData,
},
};

export default settings;
export { Default, Sortable };
Loading