- Add aria-label and role attributes to search TextField for screen readers - Add role="listbox" and aria-label to search results Paper - Add role="option", aria-selected, and minHeight to ListItemButton for accessibility - Update placeholder from "John 3:16" to "John 3" to match chapter-level search - Change parseReference abbreviation matching from === to startsWith() for consistency with searchBooks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
105 lines
3.0 KiB
TypeScript
105 lines
3.0 KiB
TypeScript
'use client'
|
|
import { useState, useEffect } from 'react'
|
|
import { Search, Close } from '@mui/icons-material'
|
|
import { Box, TextField, InputAdornment, Paper, List, ListItem, ListItemButton, Typography } from '@mui/material'
|
|
import { searchBooks, type SearchResult } from '@/lib/bible-search'
|
|
|
|
interface SearchNavigatorProps {
|
|
onNavigate: (bookId: number, chapter: number) => void
|
|
}
|
|
|
|
export function SearchNavigator({ onNavigate }: SearchNavigatorProps) {
|
|
const [query, setQuery] = useState('')
|
|
const [results, setResults] = useState<SearchResult[]>([])
|
|
const [isOpen, setIsOpen] = useState(false)
|
|
|
|
useEffect(() => {
|
|
if (query.trim()) {
|
|
setResults(searchBooks(query))
|
|
setIsOpen(true)
|
|
} else {
|
|
setResults([])
|
|
setIsOpen(false)
|
|
}
|
|
}, [query])
|
|
|
|
const handleSelect = (result: SearchResult) => {
|
|
onNavigate(result.bookId, result.chapter)
|
|
setQuery('')
|
|
setIsOpen(false)
|
|
}
|
|
|
|
return (
|
|
<Box sx={{ position: 'relative', width: '100%' }}>
|
|
<TextField
|
|
aria-label="Search Bible books and chapters"
|
|
role="searchbox"
|
|
placeholder="Search Bible (e.g., Genesis 1, John 3)"
|
|
value={query}
|
|
onChange={(e) => setQuery(e.target.value)}
|
|
onFocus={() => query && setIsOpen(true)}
|
|
InputProps={{
|
|
startAdornment: (
|
|
<InputAdornment position="start">
|
|
<Search sx={{ color: 'text.secondary' }} />
|
|
</InputAdornment>
|
|
),
|
|
endAdornment: query && (
|
|
<InputAdornment position="end">
|
|
<Close
|
|
sx={{ cursor: 'pointer', color: 'text.secondary' }}
|
|
onClick={() => setQuery('')}
|
|
/>
|
|
</InputAdornment>
|
|
),
|
|
}}
|
|
sx={{
|
|
width: '100%',
|
|
'& .MuiOutlinedInput-root': {
|
|
fontSize: '0.95rem',
|
|
'@media (max-width: 600px)': {
|
|
fontSize: '1rem' // Larger on mobile to avoid zoom
|
|
}
|
|
}
|
|
}}
|
|
/>
|
|
|
|
{isOpen && results.length > 0 && (
|
|
<Paper
|
|
role="listbox"
|
|
aria-label="Search results"
|
|
sx={{
|
|
position: 'absolute',
|
|
top: '100%',
|
|
left: 0,
|
|
right: 0,
|
|
zIndex: 10,
|
|
mt: 1,
|
|
maxHeight: 300,
|
|
overflow: 'auto'
|
|
}}
|
|
>
|
|
<List>
|
|
{results.map((result, idx) => (
|
|
<ListItem key={idx} disablePadding>
|
|
<ListItemButton
|
|
role="option"
|
|
aria-selected={false}
|
|
sx={{ minHeight: '44px', py: 1.5 }}
|
|
onClick={() => handleSelect(result)}
|
|
>
|
|
<Box>
|
|
<Typography variant="body2" fontWeight={500}>
|
|
{result.reference}
|
|
</Typography>
|
|
</Box>
|
|
</ListItemButton>
|
|
</ListItem>
|
|
))}
|
|
</List>
|
|
</Paper>
|
|
)}
|
|
</Box>
|
|
)
|
|
}
|