Skip to content

Commit

Permalink
fixup! Add Table component
Browse files Browse the repository at this point in the history
  • Loading branch information
lyzadanger committed Aug 31, 2021
1 parent 09f5c36 commit 1e708e0
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 84 deletions.
50 changes: 31 additions & 19 deletions src/components/Table.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,33 @@ import { Scrollbox } from './containers';
import { Spinner } from './Spinner';

/**
* @typedef TableColumn
* @prop {string} label - Header label for the column
* @typedef TableHeader
* @prop {string} label
* @prop {string} [classes] - Additional CSS classes for the column's `<th>` element
*/

/**
* @template Item
* @typedef TableProps
* @prop {string} accessibleLabel - An accessible label for the table
* @prop {string} [classes] - Extra CSS classes to apply to the outermost
* @prop {string} [classes] - Extra CSS classes to apply to the <table>
* @prop {string} [containerClasses] - Extra CSS classes to apply to the outermost
* element, which is a <Scrollbox> div
* @prop {TableColumn[]} columns - The columns to display in this table
* @prop {TableHeader[]} tableHeaders - The columns to display in this table
* @prop {boolean} [isLoading] - Show an indicator that data for the table is
* currently being fetched
* @prop {Item[]} items -
* The items to display in this table, one per row. `renderItem` defines how
* information from each item is represented as a series of table cells.
* @prop {(it: Item, selected: boolean) => any} renderItem -
* A function called to render each item as the contents of a table row.
* The result should be a list of `<td>` elements (one per column) wrapped inside a Fragment.
* A function to render an item as a table row. It should return
* a `<td>` element for each `tableHeader` column, wrapped in a `Fragment`
* @prop {Item|null} selectedItem - The currently selected item from `items`
* @prop {(it: Item) => void} onSelectItem -
* Callback invoked when the user changes the selected item
* @prop {(it: Item) => void} onUseItem -
* Callback invoked when a user chooses to use an item by double-clicking it
* or pressing Enter while it is selected
* @prop {string} [tableClasses] - Extra CSS classes to apply to the <table>
*/

/**
Expand All @@ -47,19 +47,20 @@ import { Spinner } from './Spinner';
*/
function nextItem(items, currentItem, step) {
const index = currentItem ? items.indexOf(currentItem) : -1;
const delta = index + step;
if (index < 0) {
return items[0];
}

if (index + step < 0) {
if (delta < 0) {
return items[0];
}

if (index + step >= items.length) {
if (delta >= items.length) {
return items[items.length - 1];
}

return items[index + step];
return items[delta];
}

/**
Expand All @@ -71,19 +72,20 @@ function nextItem(items, currentItem, step) {
export function Table({
accessibleLabel,
classes,
columns,
containerClasses,
isLoading = false,
items,
onSelectItem,
onUseItem,
renderItem,
tableClasses,
selectedItem,
tableHeaders,
}) {
const rowRefs = useRef(/** @type {(HTMLElement|null)[]} */ ([]));
const scrollboxRef = useRef(/** @type {HTMLElement|null} */ (null));
const headerRef = useRef(/** @type {HTMLElement|null} */ (null));

/** @param {Item} item */
const onKeyboardSelect = item => {
const rowEl = rowRefs.current[items.indexOf(item)];
if (rowEl) {
Expand All @@ -92,6 +94,7 @@ export function Table({
onSelectItem(item);
};

/** @param {KeyboardEvent} event */
const onKeyDown = event => {
let handled = false;
switch (event.key) {
Expand Down Expand Up @@ -137,10 +140,19 @@ export function Table({
// The top of the selected row, relative to the top of the Scrollbox frame
const rowOffsetFromScrollbox = rowEl.offsetTop - scrollboxEl.scrollTop;

if (rowOffsetFromScrollbox >= scrollboxEl.clientHeight) {
// The `selectedItem` is in a table row that is not visible because it
// is below the visible content in the `scrollbox`. This is most likely
// to occur if a `Table` is rendered with an initial `selectedItem` that
// is towards the bottom of the table (later in the `items` array).
// Scroll it into view.
rowEl.scrollIntoView();
}

// If the offset position is smaller than the height of the header,
// the row is partially or fully obscured by the header. Scroll just
// enough to make the full row visible beneath the header.
if (rowOffsetFromScrollbox <= headingHeight) {
if (rowOffsetFromScrollbox < headingHeight) {
scrollboxEl.scrollBy(0, rowOffsetFromScrollbox - headingHeight);
}
}
Expand All @@ -149,25 +161,25 @@ export function Table({
return (
<Scrollbox
withHeader
classes={classnames('Hyp-Table-Scrollbox', classes)}
classes={classnames('Hyp-Table-Scrollbox', containerClasses)}
containerRef={scrollboxRef}
>
<table
aria-label={accessibleLabel}
className={classnames('Hyp-Table', tableClasses)}
className={classnames('Hyp-Table', classes)}
tabIndex={0}
role="grid"
onKeyDown={onKeyDown}
>
<thead ref={downcastRef(headerRef)}>
<tr>
{columns.map(column => (
{tableHeaders.map(({ classes, label }, index) => (
<th
key={column.label}
className={classnames(column.classes)}
key={`${label}-${index}`}
className={classnames(classes)}
scope="col"
>
{column.label}
{label}
</th>
))}
</tr>
Expand Down
21 changes: 12 additions & 9 deletions src/components/test/Table-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,37 @@ describe('Table', () => {
mount(
<Table
accessibleLabel="Test table"
columns={[{ label: 'Item' }]}
tableHeaders={[{ label: 'Item' }]}
items={[]}
renderItem={item => item}
renderItem={item => <td>{item}</td>}
onSelectItem={sinon.stub()}
onUseItem={sinon.stub()}
selectedItem={null}
{...props}
/>
);

it('renders column headings', () => {
const wrapper = renderTable({
columns: [{ label: 'Name' }, { label: 'Size' }],
tableHeaders: [{ label: 'Name' }, { label: 'Size' }],
});
const columns = wrapper.find('thead th').map(col => col.text());
assert.deepEqual(columns, ['Name', 'Size']);
});

it('renders items', () => {
const wrapper = renderTable({
columns: [{ label: 'Item' }],
tableHeaders: [{ label: 'Item' }],
items: ['One', 'Two', 'Three'],
// eslint-disable-next-line react/display-name
renderItem: item => <span>{item}</span>,
renderItem: item => <td>{item}</td>,
});

const items = wrapper.find('tr > span');
const items = wrapper.find('tr > td');
assert.equal(items.length, 3);
assert.isTrue(wrapper.contains(<span>One</span>));
assert.isTrue(wrapper.contains(<span>Two</span>));
assert.isTrue(wrapper.contains(<span>Three</span>));
assert.isTrue(wrapper.contains(<td>One</td>));
assert.isTrue(wrapper.contains(<td>Two</td>));
assert.isTrue(wrapper.contains(<td>Three</td>));
});

['click', 'mousedown'].forEach(event => {
Expand Down
65 changes: 9 additions & 56 deletions src/pattern-library/components/patterns/TableComponents.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,57 +5,7 @@ import { LabeledButton, Table } from '../../../';

import Library from '../Library';

const columns = [
{
label: 'Name',
},
{
label: 'Last modified',
},
];

const files = [
{
displayName: 'Persnickety.pdf',
updated: 'Jul 28, 2021',
},
{
displayName: 'Albumen.pdf',
updated: 'Jul 20, 2021',
},
{
displayName: 'Yams-and-sauce.pdf',
updated: 'Aug 04, 2021',
},
{
displayName: 'Coneflowers-and-their-allies.pdf',
updated: 'Aug 01, 2021',
},
{
displayName: 'Dollars-and-sense.pdf',
updated: 'Aug 22, 2021',
},
{
displayName: 'Mendicant Friars.PDF',
updated: 'Jul 20, 2021',
},
{
displayName: 'Paleogeography.pdf',
updated: 'Aug 04, 2021',
},
{
displayName: 'Foregone conclusions.pdf',
updated: 'Aug 01, 2021',
},
{
displayName: 'Forklifts-and-bananas.pdf',
updated: 'Aug 01, 2021',
},
{
displayName: 'Coracles.pdf',
updated: 'Aug 05, 2021',
},
];
import { sampleTableContent } from './samples';

const renderCallback = file => (
<Fragment>
Expand All @@ -64,6 +14,8 @@ const renderCallback = file => (
</Fragment>
);

const { tableHeaders, items } = sampleTableContent();

function TableExample() {
const [isLoading, setIsLoading] = useState(false);
const [selectedFile, setSelectedFile] = useState(
Expand All @@ -86,13 +38,13 @@ function TableExample() {
</div>
<Table
accessibleLabel="File list"
columns={columns}
isLoading={isLoading}
items={files}
items={items}
selectedItem={selectedFile}
onSelectItem={file => setSelectedFile(file)}
onUseItem={file => alert(`Selected ${file.displayName}`)}
renderItem={file => renderCallback(file)}
tableHeaders={tableHeaders}
/>
</Library.Demo>
</Library.Example>
Expand All @@ -102,7 +54,7 @@ function TableExample() {
function ScrollboxTableExample() {
const [isLoading, setIsLoading] = useState(false);
const [selectedFile, setSelectedFile] = useState(
/** @type {null|object} */ (null)
/** @type {null|object} */ (items[items.length - 1])
);

return (
Expand All @@ -113,6 +65,7 @@ function ScrollboxTableExample() {
scroll if it overflows. Apply height/width constraints to an appropriate
parent elements to enable this. Height will not change when loading.
</p>
<p>In this example, the last item in the table is pre-selected.</p>
<Library.Demo withSource>
<div className="hyp-u-padding--5">
<LabeledButton onClick={() => setIsLoading(!isLoading)}>
Expand All @@ -125,13 +78,13 @@ function ScrollboxTableExample() {
>
<Table
accessibleLabel="File list"
columns={columns}
isLoading={isLoading}
items={files}
items={isLoading ? [] : items}
selectedItem={selectedFile}
onSelectItem={file => setSelectedFile(file)}
onUseItem={file => alert(`Selected ${file.displayName}`)}
renderItem={file => renderCallback(file)}
tableHeaders={tableHeaders}
/>
</div>
</Library.Demo>
Expand Down
55 changes: 55 additions & 0 deletions src/pattern-library/components/patterns/samples.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,58 @@ export function LoremIpsum() {
</p>
);
}

export function sampleTableContent() {
return {
tableHeaders: [
{
label: 'Name',
},
{
label: 'Last modified',
},
],
items: [
{
displayName: 'Persnickety.pdf',
updated: 'Jul 28, 2021',
},
{
displayName: 'Albumen.pdf',
updated: 'Jul 20, 2021',
},
{
displayName: 'Yams-and-sauce.pdf',
updated: 'Aug 04, 2021',
},
{
displayName: 'Coneflowers-and-their-allies.pdf',
updated: 'Aug 01, 2021',
},
{
displayName: 'Dollars-and-sense.pdf',
updated: 'Aug 22, 2021',
},
{
displayName: 'Mendicant Friars.PDF',
updated: 'Jul 20, 2021',
},
{
displayName: 'Paleogeography.pdf',
updated: 'Aug 04, 2021',
},
{
displayName: 'Foregone conclusions.pdf',
updated: 'Aug 01, 2021',
},
{
displayName: 'Forklifts-and-bananas.pdf',
updated: 'Aug 01, 2021',
},
{
displayName: 'Coracles.pdf',
updated: 'Aug 05, 2021',
},
],
};
}

0 comments on commit 1e708e0

Please sign in to comment.