Skip to content

Commit bb837d2

Browse files
authored
Merge pull request #193 from hypothesis/table-updates
Add `emptyItemsMessage`, reduce CSS specificity for `Table` (`table` pattern)
2 parents 32e2999 + a50b437 commit bb837d2

File tree

6 files changed

+158
-48
lines changed

6 files changed

+158
-48
lines changed

src/components/Table.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { Scrollbox } from './containers';
1919
* @prop {string} [classes] - Extra CSS classes to apply to the <table>
2020
* @prop {string} [containerClasses] - Extra CSS classes to apply to the outermost
2121
* element, which is a <Scrollbox> div
22+
* @prop {import("preact").ComponentChildren} [emptyItemsMessage] - Optional message to display if
23+
* there are no `items`. Will only display when the Table is not loading.
2224
* @prop {TableHeader[]} tableHeaders - The columns to display in this table
2325
* @prop {boolean} [isLoading] - Show an indicator that data for the table is
2426
* currently being fetched
@@ -73,6 +75,7 @@ export function Table({
7375
accessibleLabel,
7476
classes,
7577
containerClasses,
78+
emptyItemsMessage,
7679
isLoading = false,
7780
items,
7881
onSelectItem,
@@ -176,7 +179,7 @@ export function Table({
176179
{tableHeaders.map(({ classes, label }, index) => (
177180
<th
178181
key={`${label}-${index}`}
179-
className={classnames(classes)}
182+
className={classnames('Hyp-Table__header', classes)}
180183
scope="col"
181184
>
182185
{label}
@@ -209,6 +212,14 @@ export function Table({
209212
<Spinner size="large" />
210213
</div>
211214
)}
215+
{!isLoading && items.length === 0 && emptyItemsMessage && (
216+
<div
217+
className="Hyp-Table-Scrollbox__message"
218+
data-testid="empty-items-message"
219+
>
220+
{emptyItemsMessage}
221+
</div>
222+
)}
212223
</Scrollbox>
213224
);
214225
}

src/components/test/Table-test.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,19 @@ describe('Table', () => {
6666
const columns = wrapper.find('thead th').map(col => col.text());
6767
assert.deepEqual(columns, ['Name', 'Size']);
6868
});
69+
70+
it('does not show an empty items message', () => {
71+
const wrapper = createComponent({
72+
isLoading: true,
73+
tableHeaders: [{ label: 'Name' }, { label: 'Size' }],
74+
items: [],
75+
emptyItemsMessage: 'There is nothing here',
76+
});
77+
78+
assert.isFalse(
79+
wrapper.find('[data-testid="empty-items-message"]').exists()
80+
);
81+
});
6982
});
7083

7184
it('renders column headings', () => {
@@ -91,6 +104,15 @@ describe('Table', () => {
91104
assert.isTrue(wrapper.contains(<td>Three</td>));
92105
});
93106

107+
it('shows provided empty message if there are no items', () => {
108+
const wrapper = createComponent({
109+
items: [],
110+
emptyItemsMessage: 'There is nothing here',
111+
});
112+
113+
assert.isTrue(wrapper.find('[data-testid="empty-items-message"]').exists());
114+
});
115+
94116
['click', 'mousedown'].forEach(event => {
95117
it(`selects item on ${event}`, () => {
96118
const onSelectItem = sinon.stub();

src/pattern-library/components/patterns/TableComponents.js

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ const renderCallback = file => (
1313
</>
1414
);
1515

16+
const customizedRenderCallback = file => (
17+
<>
18+
<td className="hyp-u-color--grey-6">{file.displayName}</td>
19+
<td>{file.updated}</td>
20+
</>
21+
);
22+
1623
const { tableHeaders, items } = sampleTableContent();
1724

1825
function TableExample() {
@@ -64,7 +71,13 @@ function ScrollboxTableExample() {
6471
scroll if it overflows. Apply height/width constraints to an appropriate
6572
parent elements to enable this. Height will not change when loading.
6673
</p>
67-
<p>In this example, the last item in the table is pre-selected.</p>
74+
<p>
75+
In this example, the last item in the table is pre-selected. Also in
76+
this example: an additional style is added to the first <code>td</code>{' '}
77+
in each row to make its foreground color different (NB: the example here
78+
would not meet ARIA contrast requirements). This demonstrates style
79+
extension/override.
80+
</p>
6881
<Library.Demo withSource>
6982
<div className="hyp-u-padding--5">
7083
<LabeledButton onClick={() => setIsLoading(!isLoading)}>
@@ -82,7 +95,7 @@ function ScrollboxTableExample() {
8295
selectedItem={selectedFile}
8396
onSelectItem={file => setSelectedFile(file)}
8497
onUseItem={file => alert(`Selected ${file.displayName}`)}
85-
renderItem={file => renderCallback(file)}
98+
renderItem={file => customizedRenderCallback(file)}
8699
tableHeaders={tableHeaders}
87100
/>
88101
</div>
@@ -91,12 +104,57 @@ function ScrollboxTableExample() {
91104
);
92105
}
93106

107+
function EmptyTableExample() {
108+
const [isLoading, setIsLoading] = useState(false);
109+
const items = [];
110+
const [selectedFile, setSelectedFile] = useState(
111+
/** @type {null|object} */ (items[items.length - 1])
112+
);
113+
114+
const emptyItemsMessage = (
115+
<p>
116+
There are no files available to show.{' '}
117+
<a href="https://www.example.com">Learn more.</a>
118+
</p>
119+
);
120+
121+
return (
122+
<Library.Example title="Constrained Table" variant="wide">
123+
<p>
124+
This Table has no items (it is empty). When not in loading state, the
125+
provided <code>emptyItemsMessage</code> will render centered in the
126+
table.
127+
</p>
128+
<Library.Demo withSource>
129+
<div className="hyp-u-padding--5">
130+
<LabeledButton onClick={() => setIsLoading(!isLoading)}>
131+
Toggle Loading
132+
</LabeledButton>
133+
</div>
134+
135+
<Table
136+
accessibleLabel="File list"
137+
emptyItemsMessage={emptyItemsMessage}
138+
isLoading={isLoading}
139+
items={items}
140+
selectedItem={selectedFile}
141+
onSelectItem={file => setSelectedFile(file)}
142+
onUseItem={file => alert(`Selected ${file.displayName}`)}
143+
renderItem={file => renderCallback(file)}
144+
tableHeaders={tableHeaders}
145+
/>
146+
</Library.Demo>
147+
</Library.Example>
148+
);
149+
}
150+
94151
export default function TableComponents() {
95152
return (
96153
<Library.Page title="Table">
97154
<Library.Pattern title="Table">
98155
<TableExample />
99156
<ScrollboxTableExample />
157+
<EmptyTableExample />
100158
</Library.Pattern>
101159
</Library.Page>
102160
);

src/pattern-library/components/patterns/TablePatterns.js

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,12 @@ export default function TablePatterns() {
2424
<table className="hyp-table">
2525
<thead>
2626
<tr>
27-
<th scope="col">Column A</th>
28-
<th scope="col">Column B</th>
27+
<th scope="col" className="hyp-table__header">
28+
Column A
29+
</th>
30+
<th scope="col" className="hyp-table__header">
31+
Column B
32+
</th>
2933
</tr>
3034
</thead>
3135
<SampleTableBody />
@@ -42,10 +46,18 @@ export default function TablePatterns() {
4246
<table className="hyp-table">
4347
<thead>
4448
<tr>
45-
<th scope="col" style="width:30%">
49+
<th
50+
scope="col"
51+
className="hyp-table__header"
52+
style="width:30%"
53+
>
4654
Column A
4755
</th>
48-
<th scope="col" style="width:70%">
56+
<th
57+
scope="col"
58+
className="hyp-table__header"
59+
style="width:70%"
60+
>
4961
Column B
5062
</th>
5163
</tr>
@@ -68,8 +80,12 @@ export default function TablePatterns() {
6880
<table className="hyp-table">
6981
<thead>
7082
<tr>
71-
<th scope="col">Column A</th>
72-
<th scope="col">Column B</th>
83+
<th scope="col" className="hyp-table__header">
84+
Column A
85+
</th>
86+
<th scope="col" className="hyp-table__header">
87+
Column B
88+
</th>
7389
</tr>
7490
</thead>
7591
<SampleTableBody />

styles/components/Table.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ $-row-height: var.$touch-target-size;
1818
// Adjust vertical position to account for header, which takes up one "row"
1919
margin-top: math.div($-row-height, 2);
2020
}
21+
22+
&__message {
23+
@include layout.absolute-centered;
24+
top: $-row-height * 2;
25+
}
2126
}
2227

2328
.Hyp-Table {

styles/mixins/patterns/_tables.scss

Lines changed: 37 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -38,56 +38,54 @@ $-min-row-height: var.$touch-target-size;
3838
th,
3939
td {
4040
@include layout.padding;
41+
// Prevent extra vertical height with <th> elements
42+
// FIXME: Review after typography patterns introduced
43+
line-height: 1;
4144
}
4245

4346
td {
4447
@include atoms.border(top);
4548
}
4649

47-
thead {
48-
// Prevent extra vertical height with <th> elements
49-
// FIXME: Review after typography patterns introduced
50-
line-height: 1;
51-
52-
th {
53-
@include containers.sticky-header;
54-
border-bottom: 1px solid $-color-header-border;
55-
background-color: $-color-header-background;
56-
// Ensure the header is displayed above content in the table when it is
57-
// scrolled, including any content which establishes a new stacking context.
58-
z-index: 1;
59-
text-align: left;
60-
}
50+
&__header {
51+
@include containers.sticky-header;
52+
border-bottom: 1px solid $-color-header-border;
53+
background-color: $-color-header-background;
54+
// Ensure the header is displayed above content in the table when it is
55+
// scrolled, including any content which establishes a new stacking context.
56+
z-index: 1;
57+
text-align: left;
6158
}
6259

6360
tbody {
6461
// Make table content look interact-able
6562
cursor: pointer;
63+
}
64+
65+
tr {
66+
height: $-min-row-height;
67+
@include layout.padding;
68+
}
69+
70+
// No border on top of first row's <td> elements, to eliminate a
71+
// double border with the <th>s
72+
tr:first-child td {
73+
border-top: none;
74+
}
75+
76+
tr.is-selected td {
77+
background-color: $-color-background--inverted;
78+
color: $-color-text--inverted;
79+
}
80+
81+
tr:hover:not(.is-selected) {
82+
background-color: $-color-background-hover;
83+
}
6684

67-
// No border on top of first row's <td> elements, to eliminate a
68-
// double border with the <th>s
69-
& tr:first-child td {
70-
border-top: none;
71-
}
72-
73-
& tr:nth-child(odd) {
74-
// Need a low-opacity black versus a named greyscale color because
75-
// this background needs to be very low opacity so as not to obscure
76-
// scroll-hinting shadows
77-
background-color: rgba(0, 0, 0, 0.025);
78-
}
79-
80-
& tr {
81-
height: $-min-row-height;
82-
83-
&.is-selected {
84-
background-color: $-color-background--inverted;
85-
color: $-color-text--inverted;
86-
}
87-
}
88-
89-
& tr:hover:not(.is-selected) {
90-
background-color: $-color-background-hover;
91-
}
85+
tr:nth-child(odd):not(.is-selected) {
86+
// Need a low-opacity black versus a named greyscale color because
87+
// this background needs to be very low opacity so as not to obscure
88+
// scroll-hinting shadows
89+
background-color: rgba(0, 0, 0, 0.025);
9290
}
9391
}

0 commit comments

Comments
 (0)