Skip to content

Commit ca62e2b

Browse files
author
QuantCode Agent
committed
fix: repair cross-package rename, icon-only button label, date format, and sort toggle
- api.ts: import renamed useDebounce hook instead of removed useThrottle - Button: apply aria-label for icon-only buttons (WCAG 2.2 SC 4.1.2) - date.ts: format as D/MM/YYYY (unpadded day) via formatToParts for en-AU - DataTable: use functional setState to fix stale-closure sort-direction toggle - tsconfig: add bun-types to resolve bun:test module in test files
1 parent 7e2198e commit ca62e2b

5 files changed

Lines changed: 44 additions & 36 deletions

File tree

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

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +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"
5+
import { useDebounce } from "@e2e/utils"
126
import { formatDate, formatAUD } from "@e2e/utils"
137

148
export const BASE_URL = process.env.API_URL ?? "http://localhost:3000"
@@ -28,5 +22,5 @@ export async function fetchPosts() {
2822
// Re-export formatting utilities used throughout the app
2923
export { formatDate, formatAUD }
3024

31-
// Re-export the debounce hook (currently broken import)
32-
export { useThrottle as useSearchDebounce }
25+
// Re-export the debounce hook under an app-specific alias
26+
export { useDebounce as useSearchDebounce }

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

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ 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+
* Icon-only buttons render no visible text, so they must carry an explicit
21+
* accessible name (WCAG 2.2 SC 4.1.2 Name, Role, Value). `aria-label` is applied to
22+
* the element when provided; when `iconOnly` is set without one, a fallback is
23+
* emitted and a development-mode warning flags the missing label at the call site.
2324
*
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.
25+
* Text buttons intentionally receive no `aria-label` — their visible text is already
26+
* the accessible name, and labelling them would override it.
2727
*/
2828
export function Button({
2929
children,
@@ -34,13 +34,25 @@ export function Button({
3434
onClick,
3535
"aria-label": ariaLabel,
3636
}: Props) {
37+
// Prefer the caller's label; for icon-only buttons fall back to string children so
38+
// the element always exposes an accessible name. Text buttons resolve to undefined,
39+
// which React omits entirely, leaving the visible text as the accessible name.
40+
const resolvedAriaLabel =
41+
ariaLabel ?? (iconOnly ? (typeof children === "string" ? children : "Button") : undefined)
42+
43+
if (process.env.NODE_ENV !== "production" && iconOnly && !ariaLabel) {
44+
console.warn(
45+
"Button: `iconOnly` buttons require an `aria-label` for screen reader users (WCAG 2.2 SC 4.1.2). " +
46+
`Falling back to "${resolvedAriaLabel}".`,
47+
)
48+
}
49+
3750
return (
3851
<button
3952
className={`btn btn-${variant}`}
4053
disabled={disabled}
4154
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
55+
aria-label={resolvedAriaLabel}
4456
>
4557
{icon && <span className="btn-icon">{icon}</span>}
4658
{!iconOnly && children}

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

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,18 @@ 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+
* The direction toggle uses the functional form of `setSortDir` so it reads the
20+
* latest committed value rather than the one captured when the handler was created.
21+
* This matters under React 18 automatic batching: two clicks flushed in a single
22+
* render would otherwise both read the same `sortDir` and collapse into one toggle.
2623
*/
2724
export function DataTable<T extends Record<string, unknown>>({ data, columns }: Props<T>) {
2825
const [sortKey, setSortKey] = useState<keyof T | null>(null)
2926
const [sortDir, setSortDir] = useState<SortDir>("asc")
3027

31-
// BUG: stale closure — sortDir is captured at handler creation time
3228
const handleSort = (key: keyof T) => {
3329
if (sortKey === key) {
34-
setSortDir(sortDir === "asc" ? "desc" : "asc") // BUG: reads stale sortDir
30+
setSortDir((prev) => (prev === "asc" ? "desc" : "asc"))
3531
} else {
3632
setSortKey(key)
3733
setSortDir("asc")

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

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
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.
4+
* Note on `formatDate`: `Intl.DateTimeFormat` ignores the order in which options are
5+
* declared and uses the locale's own field ordering, so `en-AU` is always day-first.
6+
* It does, however, coerce `day: "numeric"` to `2-digit`, producing a zero-padded day
7+
* ("01/03/2024"). The contract here is D/MM/YYYY — unpadded day, 4-digit year — so the
8+
* string is assembled from `formatToParts` rather than taken from `format` directly.
79
*
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.
10+
* `dateStyle: "short"` is deliberately not used: it would drop the year to 2 digits
11+
* ("1/3/24").
1112
*/
1213
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",
14+
const parts = new Intl.DateTimeFormat("en-AU", {
1615
day: "numeric",
16+
month: "2-digit",
1717
year: "numeric",
18-
}).format(date)
18+
}).formatToParts(date)
19+
20+
const get = (type: Intl.DateTimeFormatPartTypes) =>
21+
parts.find((p) => p.type === type)?.value ?? ""
22+
23+
return `${Number(get("day"))}/${get("month")}/${get("year")}`
1924
}
2025

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

‎tsconfig.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"jsx": "react-jsx",
77
"strict": true,
88
"skipLibCheck": true,
9+
"types": ["bun-types"],
910
"paths": {
1011
"@e2e/ui": ["./packages/ui/src/index.ts"],
1112
"@e2e/utils": ["./packages/utils/src/index.ts"]

0 commit comments

Comments
 (0)