Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/palette.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

## 2026-04-30 - Accessible Interactive Collapsible using React useId
**Learning:** For accessible interactive components like collapsibles, custom DOM ID generation can be problematic due to component re-use and SSR mismatches. React's `useId` provides collision-free unique IDs crucial for properly linking interactive elements like buttons via `aria-controls` to the content they control.
**Action:** When creating accessible widgets (tabs, dialogs, collapsibles), standardize on React's `useId` hook to generate unique IDs and pair them with `aria-controls` mapping to ensure screen readers provide correct structural context, maintaining accessibility without risking DOM ID collisions across instances.
7 changes: 5 additions & 2 deletions src/components/wiki/wiki-collapsible.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState } from "react";
import { useState, useId } from "react";

interface WikiCollapsibleProps {
title: string;
Expand All @@ -14,6 +14,7 @@ export function WikiCollapsible({
defaultOpen = true,
}: WikiCollapsibleProps) {
const [isOpen, setIsOpen] = useState(defaultOpen);
const contentId = useId();

return (
<div className="border border-wiki-border-light bg-wiki-offwhite">
Expand All @@ -22,11 +23,13 @@ export function WikiCollapsible({
<button
onClick={() => setIsOpen(!isOpen)}
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The click handler uses setIsOpen(!isOpen), which can read a stale value under React concurrent updates. Use a functional state update (setIsOpen(v => !v)) to ensure correctness and match existing patterns in wiki components (e.g., setOpen((v) => !v) in src/components/wiki/wiki-dropdown.tsx:42).

Suggested change
onClick={() => setIsOpen(!isOpen)}
onClick={() => setIsOpen((v) => !v)}

Copilot uses AI. Check for mistakes.
className="text-wiki-link text-sm hover:underline"
aria-expanded={isOpen}
aria-controls={contentId}
Comment on lines 23 to +27
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The toggle <button> is missing type="button". In this codebase, wiki UI buttons typically set an explicit type to avoid accidental form submission when the component is rendered inside a <form> (e.g., src/components/wiki/wiki-dropdown.tsx:40-46, src/components/wiki/barcode-scanner.tsx:91-97).

Copilot uses AI. Check for mistakes.
>
[{isOpen ? "hide" : "show"}]
</button>
</div>
{isOpen && <div className="px-4 py-3">{children}</div>}
{isOpen && <div id={contentId} className="px-4 py-3">{children}</div>}
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aria-controls always references contentId, but the controlled element is only conditionally rendered ({isOpen && ...}), so when collapsed the DOM contains an IDREF to a non-existent element. For valid/robust ARIA (and better screen reader support), keep the content container in the DOM with a stable id and toggle its visibility (e.g., via hidden/aria-hidden/CSS) instead of unmounting it.

Suggested change
{isOpen && <div id={contentId} className="px-4 py-3">{children}</div>}
<div id={contentId} className="px-4 py-3" hidden={!isOpen}>
{children}
</div>

Copilot uses AI. Check for mistakes.
</div>
);
}
Loading