Skip to content

Commit 558a0ee

Browse files
committed
Table fixes
1 parent 1b39148 commit 558a0ee

4 files changed

Lines changed: 109 additions & 5 deletions

File tree

src/components/Table/Table.module.scss

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,15 @@
1717

1818
.toolbar {
1919
display: flex;
20+
align-items: center;
2021
justify-content: flex-end;
22+
gap: var(--space-3);
23+
}
24+
25+
.resultCount {
26+
margin-right: auto;
27+
font-size: var(--font-size-sm);
28+
color: var(--color-text-secondary);
2129
}
2230

2331
.search {
@@ -69,6 +77,16 @@
6977
font-weight: var(--font-weight-semibold);
7078
white-space: nowrap;
7179
user-select: none;
80+
81+
/* Subtle tint on the active sort column */
82+
&[aria-sort='ascending'],
83+
&[aria-sort='descending'] {
84+
background-color: color-mix(
85+
in srgb,
86+
var(--table-accent, var(--color-primary)) 8%,
87+
var(--table-header-bg, var(--color-surface))
88+
);
89+
}
7290
}
7391

7492
.thContent {
@@ -157,6 +175,15 @@
157175
color: var(--color-text-secondary);
158176
}
159177

178+
/* Error content shown inside the empty-state cell */
179+
180+
.errorContent {
181+
display: inline-flex;
182+
align-items: center;
183+
gap: var(--space-2);
184+
color: var(--color-error);
185+
}
186+
160187
/* Inline "Clear search" link inside the empty state */
161188

162189
.clearSearchButton {
@@ -181,7 +208,12 @@
181208
}
182209

183210
.selected {
211+
/* !important needed to override striped-row specificity */
184212
background-color: color-mix(in srgb, var(--table-accent, var(--color-primary)) 12%, transparent) !important;
213+
214+
&:hover {
215+
background-color: color-mix(in srgb, var(--table-accent, var(--color-primary)) 18%, transparent) !important;
216+
}
185217
}
186218

187219
/* Focus ring on selectable rows */
@@ -204,7 +236,16 @@
204236
}
205237

206238
.skeletonRow {
239+
border-bottom: 1px solid var(--color-border);
207240
pointer-events: none;
241+
242+
/* Vary cell widths to mimic realistic data distribution */
243+
td:nth-child(1) .skeletonCell { width: 40%; }
244+
td:nth-child(2) .skeletonCell { width: 78%; }
245+
td:nth-child(3) .skeletonCell { width: 55%; }
246+
td:nth-child(4) .skeletonCell { width: 85%; }
247+
td:nth-child(5) .skeletonCell { width: 45%; }
248+
td:nth-child(n+6) .skeletonCell { width: 65%; }
208249
}
209250

210251
.skeletonCell {
@@ -297,7 +338,6 @@
297338
color: var(--color-text-secondary);
298339
}
299340

300-
.showingInfo {
301-
color: var(--color-text-secondary);
302-
}
341+
/* Styling hook for the "Showing X–Y of Z" span; inherits color from .pageInfo */
342+
.showingInfo {}
303343

src/components/Table/Table.stories.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,16 @@ export const ErrorState: Story = {
128128
},
129129
};
130130

131+
/** Error state with selectable rows disabled, loading finished. */
132+
export const ErrorStateWithContext: Story = {
133+
args: {
134+
columns,
135+
data: [],
136+
searchable: true,
137+
error: 'Could not connect to the server. Check your network and retry.',
138+
},
139+
};
140+
131141
/** Full-featured table with header color, selectable rows, search, pagination. */
132142
export const KitchenSink: Story = {
133143
args: {

src/components/Table/Table.test.tsx

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,42 @@ describe('Table', () => {
141141
expect(screen.getByText('Failed to load')).toBeInTheDocument();
142142
});
143143

144-
/* --- Row selection (keyboard + mouse) --- */
144+
it('wraps error content in an error-styled container', () => {
145+
render(<Table columns={columns} data={[]} error="Something went wrong" />);
146+
const errorText = screen.getByText('Something went wrong');
147+
expect(errorText.closest('[class*="errorContent"]')).toBeInTheDocument();
148+
});
149+
150+
it('sets role="grid" on the table when selectable', () => {
151+
render(<Table columns={columns} data={data} selectable />);
152+
expect(screen.getByRole('grid')).toBeInTheDocument();
153+
});
154+
155+
it('does not set role="grid" when not selectable', () => {
156+
render(<Table columns={columns} data={data} />);
157+
expect(screen.queryByRole('grid')).not.toBeInTheDocument();
158+
expect(screen.getByRole('table')).toBeInTheDocument();
159+
});
160+
161+
/* --- Visible result count in toolbar --- */
162+
163+
it('shows result count in toolbar when search is active', async () => {
164+
render(<Table columns={columns} data={data} searchable />);
165+
await userEvent.type(screen.getByRole('searchbox'), 'Alice');
166+
expect(screen.getByText('1 result')).toBeInTheDocument();
167+
});
168+
169+
it('shows plural result count for multiple matches', async () => {
170+
render(<Table columns={columns} data={data} searchable />);
171+
await userEvent.type(screen.getByRole('searchbox'), 'a');
172+
// The visible toolbar span shows e.g. "2 results" (live region shows "2 results found")
173+
expect(screen.getByText(/^\d+ results$/)).toBeInTheDocument();
174+
});
175+
176+
it('hides result count when search is empty', () => {
177+
render(<Table columns={columns} data={data} searchable />);
178+
expect(screen.queryByText(/result/)).not.toBeInTheDocument();
179+
});
145180

146181
it('selects a row on click when selectable', async () => {
147182
render(<Table columns={columns} data={data} selectable />);

src/components/Table/Table.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,16 @@ function ChevronRightIcon() {
5656
);
5757
}
5858

59+
function WarningIcon() {
60+
return (
61+
<svg aria-hidden="true" focusable="false" width="16" height="16" viewBox="0 0 16 16" fill="none">
62+
<path d="M8 2L14.5 13H1.5L8 2Z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" />
63+
<path d="M8 6.5V9.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
64+
<circle cx="8" cy="11.5" r="0.75" fill="currentColor" />
65+
</svg>
66+
);
67+
}
68+
5969
/* ---- Types ---- */
6070

6171
export type SortDirection = 'asc' | 'desc' | 'none';
@@ -317,6 +327,11 @@ export function Table<T extends Record<string, unknown> = Record<string, unknown
317327

318328
{searchable && (
319329
<div className={styles.toolbar}>
330+
{search.trim() && (
331+
<span className={styles.resultCount}>
332+
{sorted.length} {sorted.length === 1 ? 'result' : 'results'}
333+
</span>
334+
)}
320335
<input
321336
type="search"
322337
className={styles.search}
@@ -339,6 +354,7 @@ export function Table<T extends Record<string, unknown> = Record<string, unknown
339354
.join(' ')}
340355
style={tableStyle}
341356
aria-busy={loading || undefined}
357+
role={selectable ? 'grid' : undefined}
342358
>
343359
{caption && <caption className={styles.caption}>{caption}</caption>}
344360
<thead className={styles.thead}>
@@ -410,7 +426,10 @@ export function Table<T extends Record<string, unknown> = Record<string, unknown
410426
<tr>
411427
<td className={styles.empty} colSpan={columns.length}>
412428
{error ? (
413-
error
429+
<span className={styles.errorContent}>
430+
<WarningIcon />
431+
{error}
432+
</span>
414433
) : search.trim() ? (
415434
<>
416435
No results for &ldquo;{search}&rdquo;.{' '}

0 commit comments

Comments
 (0)