From 2890d7082f1f213d17568828cfd6aad2d4ca5a2e Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 12:22:03 +0000 Subject: [PATCH] feat: add accessible clear buttons to app search inputs Adds a semantic `X` clear button (imported from `lucide-react`) to the search and filter inputs in `Launcher.tsx`, `ProjectsApp.tsx`, and `BlogApp.tsx`. The button includes an `aria-label` for screen readers, `focus-visible` classes for keyboard navigation in dark mode, and correctly manages focus by returning it to the input field after clearing the query. Co-authored-by: schmug <38227427+schmug@users.noreply.github.com> --- src/components/os/Launcher.tsx | 14 ++++++++++++++ src/components/os/apps/BlogApp.tsx | 18 +++++++++++++++++- src/components/os/apps/ProjectsApp.tsx | 18 +++++++++++++++++- 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/components/os/Launcher.tsx b/src/components/os/Launcher.tsx index 236ccf3..d563c06 100644 --- a/src/components/os/Launcher.tsx +++ b/src/components/os/Launcher.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useRef, useState } from 'react'; +import { X } from 'lucide-react'; import { type AppManifest } from '../../apps/registry'; import { renderIcon } from '../../apps/iconUtils'; import { useAllApps } from '../../hooks/useAllApps'; @@ -88,6 +89,19 @@ export function Launcher({ open, onClose }: Props) { spellCheck={false} maxLength={100} // Security: prevent DoS via extremely long input strings /> + {query && ( + + )} Esc diff --git a/src/components/os/apps/BlogApp.tsx b/src/components/os/apps/BlogApp.tsx index a0c0450..63cb894 100644 --- a/src/components/os/apps/BlogApp.tsx +++ b/src/components/os/apps/BlogApp.tsx @@ -1,4 +1,5 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useState, useRef } from 'react'; +import { X } from 'lucide-react'; import { relativeTime } from '../../../hooks/useProjects'; type BlogPost = { @@ -25,6 +26,7 @@ export default function BlogApp() { const [payload, setPayload] = useState(null); const [error, setError] = useState(null); const [query, setQuery] = useState(''); + const inputRef = useRef(null); useEffect(() => { let cancelled = false; @@ -71,6 +73,7 @@ export default function BlogApp() {
setQuery(e.target.value)} placeholder="Filter by title, tag, or summary" @@ -78,6 +81,19 @@ export default function BlogApp() { aria-label="Filter posts" maxLength={100} // Security: prevent DoS via extremely long input strings /> + {query && ( + + )}
diff --git a/src/components/os/apps/ProjectsApp.tsx b/src/components/os/apps/ProjectsApp.tsx index 4ad173b..a64191b 100644 --- a/src/components/os/apps/ProjectsApp.tsx +++ b/src/components/os/apps/ProjectsApp.tsx @@ -1,9 +1,11 @@ -import { useMemo, useState } from 'react'; +import { useMemo, useState, useRef } from 'react'; +import { X } from 'lucide-react'; import { useProjects, relativeTime } from '../../../hooks/useProjects'; export default function ProjectsApp() { const { payload, error } = useProjects(); const [query, setQuery] = useState(''); + const inputRef = useRef(null); const filtered = useMemo(() => { if (!payload) return []; @@ -35,6 +37,7 @@ export default function ProjectsApp() {
setQuery(e.target.value)} placeholder="Filter by name, language, or description" @@ -42,6 +45,19 @@ export default function ProjectsApp() { aria-label="Filter projects" maxLength={100} // Security: prevent DoS via extremely long input strings /> + {query && ( + + )}