Skip to content

Commit 9227931

Browse files
Copilotintel352
andauthored
fix: address component review feedback — types, a11y, SSR, and key stability (#10)
* Initial plan * fix: address PR review feedback on components - DataTable: import type ReactNode, fix React.ReactNode refs, add keyboard-accessible sort buttons - Sidebar: import type KeyboardEvent, fix React.KeyboardEvent ref - Modal: useId() for unique title IDs, SSR guard before createPortal, stable action keys - ErrorBoundary.test: explicitly import beforeEach/afterEach from vitest Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>
1 parent af1d9ce commit 9227931

5 files changed

Lines changed: 65 additions & 28 deletions

File tree

package-lock.json

Lines changed: 19 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/DataTable.tsx

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, type CSSProperties } from 'react';
1+
import { useState, type CSSProperties, type ReactNode } from 'react';
22
import { colors, baseStyles } from '../theme';
33

44
export interface DataTableColumn<T> {
@@ -9,7 +9,7 @@ export interface DataTableColumn<T> {
99
/** Whether this column is sortable. Default: false */
1010
sortable?: boolean;
1111
/** Optional custom cell renderer. */
12-
render?: (value: T[keyof T], row: T) => React.ReactNode;
12+
render?: (value: T[keyof T], row: T) => ReactNode;
1313
/** Optional column header style override. */
1414
headerStyle?: CSSProperties;
1515
/** Optional cell style override. */
@@ -114,6 +114,7 @@ export default function DataTable<T extends Record<string, unknown>>({
114114
fontFamily: "'Inter', 'Segoe UI', system-ui, sans-serif",
115115
...style,
116116
};
117+
const thPadding = baseStyles.th.padding;
117118

118119
return (
119120
<div style={containerStyle}>
@@ -126,10 +127,9 @@ export default function DataTable<T extends Record<string, unknown>>({
126127
key={col.key}
127128
style={{
128129
...baseStyles.th,
129-
cursor: col.sortable ? 'pointer' : 'default',
130+
padding: 0,
130131
...col.headerStyle,
131132
}}
132-
onClick={() => handleSort(col)}
133133
aria-sort={
134134
sortState?.key === col.key
135135
? sortState.direction === 'asc'
@@ -138,12 +138,32 @@ export default function DataTable<T extends Record<string, unknown>>({
138138
: undefined
139139
}
140140
>
141-
{col.label}
142-
{col.sortable && (
143-
<SortIcon
144-
direction={sortState?.key === col.key ? sortState.direction : undefined}
145-
active={sortState?.key === col.key}
146-
/>
141+
{col.sortable ? (
142+
<button
143+
onClick={() => handleSort(col)}
144+
style={{
145+
background: 'none',
146+
border: 'none',
147+
cursor: 'pointer',
148+
padding: thPadding,
149+
width: '100%',
150+
textAlign: 'left',
151+
font: 'inherit',
152+
color: 'inherit',
153+
display: 'flex',
154+
alignItems: 'center',
155+
}}
156+
>
157+
{col.label}
158+
<SortIcon
159+
direction={sortState?.key === col.key ? sortState.direction : undefined}
160+
active={sortState?.key === col.key}
161+
/>
162+
</button>
163+
) : (
164+
<span style={{ display: 'block', padding: thPadding }}>
165+
{col.label}
166+
</span>
147167
)}
148168
</th>
149169
))}
@@ -173,7 +193,7 @@ export default function DataTable<T extends Record<string, unknown>>({
173193
<td key={col.key} style={{ ...baseStyles.td, ...col.cellStyle }}>
174194
{col.render
175195
? col.render(row[col.key], row)
176-
: (row[col.key] as React.ReactNode)}
196+
: (row[col.key] as ReactNode)}
177197
</td>
178198
))}
179199
</tr>

src/components/ErrorBoundary.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, expect, vi } from 'vitest';
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
22
import { render, screen } from '@testing-library/react';
33
import userEvent from '@testing-library/user-event';
44
import ErrorBoundary from './ErrorBoundary';

src/components/Modal.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
useEffect,
3+
useId,
34
useRef,
45
type CSSProperties,
56
type ReactNode,
@@ -9,6 +10,8 @@ import { createPortal } from 'react-dom';
910
import { colors, baseStyles } from '../theme';
1011

1112
export interface ModalAction {
13+
/** Stable unique identifier for this action button (used as React key). */
14+
id?: string;
1215
/** Button label. */
1316
label: string;
1417
/** Click handler. */
@@ -46,6 +49,7 @@ export default function Modal({
4649
style,
4750
}: ModalProps) {
4851
const dialogRef = useRef<HTMLDivElement>(null);
52+
const titleId = useId();
4953

5054
// Focus trap: focus the dialog on open.
5155
useEffect(() => {
@@ -136,7 +140,7 @@ export default function Modal({
136140
ref={dialogRef}
137141
role="dialog"
138142
aria-modal="true"
139-
aria-labelledby={title ? 'modal-title' : undefined}
143+
aria-labelledby={title ? titleId : undefined}
140144
tabIndex={-1}
141145
style={dialogStyle}
142146
onClick={(e) => e.stopPropagation()}
@@ -145,7 +149,7 @@ export default function Modal({
145149
{(title != null) && (
146150
<div style={headerStyle}>
147151
<h2
148-
id="modal-title"
152+
id={titleId}
149153
style={{
150154
margin: 0,
151155
fontSize: '18px',
@@ -179,9 +183,9 @@ export default function Modal({
179183

180184
{actions && actions.length > 0 && (
181185
<div style={footerStyle}>
182-
{actions.map((action) => (
186+
{actions.map((action, idx) => (
183187
<button
184-
key={action.label}
188+
key={action.id ?? idx}
185189
onClick={action.onClick}
186190
disabled={action.disabled}
187191
style={{
@@ -199,5 +203,9 @@ export default function Modal({
199203
</div>
200204
);
201205

206+
if (typeof document === 'undefined') {
207+
return null;
208+
}
209+
202210
return createPortal(modal, document.body);
203211
}

src/components/Sidebar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { CSSProperties, ReactNode } from 'react';
1+
import type { CSSProperties, ReactNode, KeyboardEvent } from 'react';
22
import { colors } from '../theme';
33

44
export interface SidebarItem {
@@ -184,7 +184,7 @@ function SidebarNavItem({ item, collapsed, onNavigate }: SidebarNavItemProps) {
184184
if (!isDisabled) onNavigate(item);
185185
}
186186

187-
function handleKeyDown(e: React.KeyboardEvent) {
187+
function handleKeyDown(e: KeyboardEvent<HTMLDivElement>) {
188188
if (e.key === 'Enter' || e.key === ' ') {
189189
e.preventDefault();
190190
handleClick();

0 commit comments

Comments
 (0)