Skip to content
Merged
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
Expand Up @@ -106,6 +106,10 @@
**Learning:** Found multiple interactive `<button>` elements in user-facing components (`UserManagementPanel`, `UserMenuWidget`) that lacked keyboard focus indicators. This makes navigation via keyboard difficult for users relying on alternative inputs.
**Action:** When creating or maintaining new buttons, always append `focus-visible:ring-1 focus-visible:ring-[color] outline-none` to ensure standard keyboard accessibility across the UI.

## 2026-05-15 - Added Missing ARIA Labels to Collapsible Sections
**Learning:** In complex widget panels (like JS8Call `ListeningPost` and `KiwiNodeBrowser`), collapsible sections triggered by icon buttons or entire rows often lack proper ARIA semantics for expand/collapse states (`aria-expanded`, `aria-controls`), which leaves screen reader users unaware of whether the section is open or what content it toggles. Furthermore, non-semantic icons inside these buttons (like Chevrons) are sometimes not hidden from screen readers.
**Action:** When implementing or fixing custom collapsible components, always pair the toggle button with `aria-expanded={isOpen}`, link it to the content container via `aria-controls="content-id"`, and ensure keyboard visibility with `focus-visible:ring-*`. Also, mark purely visual indicator icons with `aria-hidden="true"`.

## 2026-05-18 - Missing Focus States and ARIA Expansion Logic on NWS Alerts Dropdown
**Learning:** The NWS Alerts Widget had a custom dropdown button that toggled the display of weather alerts. While it functioned correctly with mouse clicks, it missed two crucial accessibility features: `aria-expanded` was not bound to the `expanded` state, meaning screen readers couldn't announce its toggle state, and it lacked the standard `focus-visible` ring utility classes, rendering it invisible to keyboard navigators when tabbed to.
**Action:** Always ensure custom dropdown or toggle buttons have `aria-expanded` and `aria-controls` explicitly set, and include keyboard focus indicators (`focus-visible:ring-1 focus-visible:ring-[color] outline-none`) that match the surrounding UI components.
Expand Down
10 changes: 6 additions & 4 deletions frontend/src/components/js8call/KiwiNodeBrowser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -600,18 +600,20 @@ export default function KiwiNodeBrowser({
<div className="border-t border-slate-800">
<button
onClick={() => setShowManual(!showManual)}
className="w-full flex items-center gap-2 px-4 py-2 hover:bg-slate-800/40 transition-colors text-[10px] font-bold text-slate-500 uppercase tracking-widest"
aria-expanded={showManual}
aria-controls="manual-entry-content"
className="w-full flex items-center gap-2 px-4 py-2 hover:bg-slate-800/40 transition-colors text-[10px] font-bold text-slate-500 uppercase tracking-widest focus-visible:ring-1 focus-visible:ring-indigo-400 outline-none"
>
{showManual ? (
<ChevronDown className="w-3 h-3" />
<ChevronDown className="w-3 h-3" aria-hidden="true" />
) : (
<ChevronRight className="w-3 h-3" />
<ChevronRight className="w-3 h-3" aria-hidden="true" />
)}
Manual Entry β€” Private / Unlisted Nodes
</button>

{showManual && (
<div className="px-4 pb-4 space-y-3">
<div id="manual-entry-content" className="px-4 pb-4 space-y-3">
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<label htmlFor="manual-host" className="text-[9px] uppercase tracking-tighter text-slate-600 font-bold ml-1">
Expand Down
15 changes: 9 additions & 6 deletions frontend/src/components/js8call/ListeningPost.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
Zap,
Lock,
} from "lucide-react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import React, { useId, useCallback, useEffect, useRef, useState } from "react";
import { getToken } from "../../api/auth";
import { useAuth } from "../../hooks/useAuth";
import {
Expand Down Expand Up @@ -52,28 +52,31 @@ function CollapsibleSection({
isOpen,
onToggle,
}: CollapsibleSectionProps) {
const contentId = useId();
return (
<div className="border-b border-[#1a2b36] last:border-0">
<button
onClick={onToggle}
className="w-full px-5 py-4 flex items-center justify-between hover:bg-white/5 transition-colors group"
aria-expanded={isOpen}
aria-controls={contentId}
className="w-full px-5 py-4 flex items-center justify-between hover:bg-white/5 transition-colors group focus-visible:ring-1 focus-visible:ring-cyan-500 outline-none"
>
<div className="flex items-center gap-2">
{Icon && (
<Icon className="w-3.5 h-3.5 text-slate-500 group-hover:text-cyan-400 transition-colors" />
<Icon className="w-3.5 h-3.5 text-slate-500 group-hover:text-cyan-400 transition-colors" aria-hidden="true" />
)}
<span className="text-[10px] text-slate-400 font-bold uppercase tracking-widest group-hover:text-slate-200">
{title}
</span>
</div>
{isOpen ? (
<ChevronDown className="w-3.5 h-3.5 text-slate-600" />
<ChevronDown className="w-3.5 h-3.5 text-slate-600" aria-hidden="true" />
) : (
<ChevronRight className="w-3.5 h-3.5 text-slate-600" />
<ChevronRight className="w-3.5 h-3.5 text-slate-600" aria-hidden="true" />
)}
</button>
{isOpen && (
<div className="px-5 pb-5 space-y-5 animate-in fade-in slide-in-from-top-1 duration-200">
<div id={contentId} className="px-5 pb-5 space-y-5 animate-in fade-in slide-in-from-top-1 duration-200">
{children}
</div>
)}
Expand Down