Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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