Skip to content

Commit 24bb8a8

Browse files
author
QuantCode Agent
committed
fix: resolve four cross-package test failures in monorepo
- utils: export useSearchDebounce (was renamed); reconcile api.ts import - ui/Button: forward aria-label; guarantee accessible name for icon-only buttons and only mark icon decorative when other name exists (WCAG 4.1.2) - ui/DataTable: fix stale-closure sort by merging key+dir into one atomic functional state update so second click toggles descending - utils/formatDate: day-first en-AU format without zero-padded day (1/03/2024)
1 parent 7e2198e commit 24bb8a8

5 files changed

Lines changed: 69 additions & 49 deletions

File tree

‎apps/web/src/lib/api.ts‎

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
11
/**
22
* API client utilities for the web app.
3-
*
4-
* BUG: imports `useThrottle` from @e2e/utils, but that hook was renamed to
5-
* `useDebounce`. This causes a TypeScript error and a runtime crash.
6-
*
7-
* Fix: change the import to `useDebounce`.
83
*/
94

10-
// BUG: useThrottle no longer exists — was renamed to useDebounce
11-
import { useThrottle } from "@e2e/utils"
12-
import { formatDate, formatAUD } from "@e2e/utils"
5+
import { useSearchDebounce, formatDate, formatAUD } from "@e2e/utils"
136

147
export const BASE_URL = process.env.API_URL ?? "http://localhost:3000"
158

@@ -28,5 +21,5 @@ export async function fetchPosts() {
2821
// Re-export formatting utilities used throughout the app
2922
export { formatDate, formatAUD }
3023

31-
// Re-export the debounce hook (currently broken import)
32-
export { useThrottle as useSearchDebounce }
24+
// Re-export the debounce hook used for search input
25+
export { useSearchDebounce }

‎packages/ui/src/components/Button/Button.tsx‎

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,24 @@ type Props = {
1717
/**
1818
* Button component.
1919
*
20-
* BUG: When `iconOnly` is true, the button renders without visible text.
21-
* An `aria-label` is required for screen reader accessibility (WCAG 2.1 SC 4.1.2),
22-
* but the component does not enforce or warn about its absence.
20+
* Every button must expose an accessible name to satisfy WCAG 2.2 SC 4.1.2
21+
* (Name, Role, Value). The name comes from the first available of:
2322
*
24-
* The test in Button.test.tsx checks that an icon-only button has an accessible name.
25-
* Fix: throw/warn in development when `iconOnly && !aria-label`, or always render
26-
* the aria-label attribute when iconOnly is true.
23+
* 1. an explicit `aria-label`,
24+
* 2. visible text children,
25+
* 3. the icon's own text content,
26+
* 4. a generic fallback, used only for `iconOnly` buttons whose icon is
27+
* decorative and therefore hidden from assistive technology.
28+
*
29+
* The icon is only marked `aria-hidden` when one of (1) or (2) already supplies
30+
* a name — hiding it otherwise would leave the button anonymous.
2731
*/
32+
const FALLBACK_LABEL = "Button"
33+
34+
function hasRenderableChildren(children: React.ReactNode): boolean {
35+
return children !== undefined && children !== null && children !== false && children !== ""
36+
}
37+
2838
export function Button({
2939
children,
3040
icon,
@@ -34,15 +44,35 @@ export function Button({
3444
onClick,
3545
"aria-label": ariaLabel,
3646
}: Props) {
47+
if (iconOnly && !ariaLabel && process.env.NODE_ENV !== "production") {
48+
console.warn(
49+
"Button: `iconOnly` buttons require an `aria-label` to provide an accessible name (WCAG 2.2 SC 4.1.2).",
50+
)
51+
}
52+
53+
// Children are only rendered — and so only contribute a name — when not icon-only.
54+
const showsChildren = !iconOnly && hasRenderableChildren(children)
55+
56+
// Fall back to a generic name only for explicit icon-only buttons: their icon is
57+
// treated as decorative, so nothing else can name them. When `iconOnly` is not
58+
// set the icon stays exposed and provides the name from its own text content.
59+
const accessibleLabel = iconOnly ? (ariaLabel ?? FALLBACK_LABEL) : ariaLabel
60+
61+
// Safe to hide the icon only once a label or visible text supplies the name.
62+
const iconIsDecorative = Boolean(accessibleLabel) || showsChildren
63+
3764
return (
3865
<button
3966
className={`btn btn-${variant}`}
4067
disabled={disabled}
4168
onClick={onClick}
42-
// BUG: aria-label is not applied when iconOnly is true and no ariaLabel is passed
43-
// The component should enforce aria-label for icon-only buttons
69+
aria-label={accessibleLabel}
4470
>
45-
{icon && <span className="btn-icon">{icon}</span>}
71+
{icon && (
72+
<span className="btn-icon" aria-hidden={iconIsDecorative || undefined}>
73+
{icon}
74+
</span>
75+
)}
4676
{!iconOnly && children}
4777
</button>
4878
)

‎packages/ui/src/components/DataTable/DataTable.tsx‎

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,23 @@ type Props<T extends Record<string, unknown>> = {
1616
/**
1717
* DataTable with client-side sorting.
1818
*
19-
* BUG: The sort handler has a stale closure — it captures `sortDir` at the
20-
* time the handler is created, so toggling sort direction does not work
21-
* correctly after the first click. The second click always sorts in the same
22-
* direction as the first.
23-
*
24-
* Fix: use the functional form of setState — `setSortDir(prev => ...)` —
25-
* so the toggle always reads the current value.
19+
* Sorting state is updated via the functional form of setState so the toggle
20+
* always reads the committed value rather than the value captured when the
21+
* handler was created.
2622
*/
2723
export function DataTable<T extends Record<string, unknown>>({ data, columns }: Props<T>) {
28-
const [sortKey, setSortKey] = useState<keyof T | null>(null)
29-
const [sortDir, setSortDir] = useState<SortDir>("asc")
24+
const [sort, setSort] = useState<{ key: keyof T | null; dir: SortDir }>({
25+
key: null,
26+
dir: "asc",
27+
})
28+
const { key: sortKey, dir: sortDir } = sort
3029

31-
// BUG: stale closure — sortDir is captured at handler creation time
3230
const handleSort = (key: keyof T) => {
33-
if (sortKey === key) {
34-
setSortDir(sortDir === "asc" ? "desc" : "asc") // BUG: reads stale sortDir
35-
} else {
36-
setSortKey(key)
37-
setSortDir("asc")
38-
}
31+
setSort((prev) =>
32+
prev.key === key
33+
? { key, dir: prev.dir === "asc" ? "desc" : "asc" }
34+
: { key, dir: "asc" },
35+
)
3936
}
4037

4138
const sorted = sortKey

‎packages/utils/src/format/date.ts‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
/**
22
* Date formatting utilities.
33
*
4-
* BUG: formatDate passes `'en-AU'` as the locale but then uses a US-style
5-
* format string option (`month: 'numeric'` before `day: 'numeric'`), which
6-
* produces MM/DD/YYYY output instead of DD/MM/YYYY for Australian dates.
7-
*
8-
* Fix: use `dateStyle: 'short'` with `'en-AU'` locale, which correctly
9-
* produces DD/MM/YYYY, or explicitly set `day: 'numeric', month: 'numeric', year: 'numeric'`
10-
* and rely on the locale to order them correctly.
4+
* Australian date convention is day-first (D/MM/YYYY). The `en-AU` locale
5+
* orders the fields correctly but zero-pads a single-digit day, so the day
6+
* part is un-padded via `formatToParts` — giving "1/03/2024" rather than
7+
* "01/03/2024", while the month stays two-digit and the year four-digit.
118
*/
9+
const DATE_PARTS_FORMATTER = new Intl.DateTimeFormat("en-AU", {
10+
day: "numeric",
11+
month: "2-digit",
12+
year: "numeric",
13+
})
14+
1215
export function formatDate(date: Date): string {
13-
// BUG: explicit field order overrides locale ordering — produces M/D/YYYY not D/M/YYYY
14-
return new Intl.DateTimeFormat("en-AU", {
15-
month: "numeric",
16-
day: "numeric",
17-
year: "numeric",
18-
}).format(date)
16+
return DATE_PARTS_FORMATTER.formatToParts(date)
17+
.map((part) => (part.type === "day" ? String(Number(part.value)) : part.value))
18+
.join("")
1919
}
2020

2121
export function formatDateTime(date: Date): string {

‎packages/utils/src/index.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export { useDebounce } from "./hooks/useDebounce"
1+
export { useDebounce, useDebounce as useSearchDebounce } from "./hooks/useDebounce"
22
export { usePagination } from "./hooks/usePagination"
33
export { formatAUD } from "./format/currency"
44
export { formatDate, formatDateTime } from "./format/date"

0 commit comments

Comments
 (0)