screen reader and keyboard usability pass
This commit is contained in:
@@ -38,11 +38,18 @@ export default function DatePicker({ value, onChange, className }: Props) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const triggerRef = useRef<HTMLButtonElement>(null)
|
||||
const calRef = useRef<HTMLDivElement>(null)
|
||||
const dayRefs = useRef<(HTMLButtonElement | null)[]>([])
|
||||
const [focusDay, setFocusDay] = useState<number | null>(null)
|
||||
const { year, month } = parse(value)
|
||||
const [viewYear, setViewYear] = useState(year)
|
||||
const [viewMonth, setViewMonth] = useState(month)
|
||||
const [pos, setPos] = useState({ top: 0, left: 0 })
|
||||
|
||||
// the day of the selected value, when it falls inside the current view
|
||||
const selected = parse(value)
|
||||
const selectedInView =
|
||||
selected.year === viewYear && selected.month === viewMonth ? selected.day : 1
|
||||
|
||||
const updatePos = useCallback(() => {
|
||||
if (!triggerRef.current) return
|
||||
const rect = triggerRef.current.getBoundingClientRect()
|
||||
@@ -60,7 +67,7 @@ export default function DatePicker({ value, onChange, className }: Props) {
|
||||
}
|
||||
}
|
||||
function onKey(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') setOpen(false)
|
||||
if (e.key === 'Escape') { setFocusDay(null); setOpen(false); triggerRef.current?.focus() }
|
||||
}
|
||||
window.addEventListener('scroll', updatePos, true)
|
||||
document.addEventListener('mousedown', onDown)
|
||||
@@ -87,12 +94,38 @@ export default function DatePicker({ value, onChange, className }: Props) {
|
||||
|
||||
const pick = (d: number) => {
|
||||
onChange(fmt(viewYear, viewMonth, d))
|
||||
setFocusDay(null)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const focusDayRef = (d: number) => {
|
||||
setFocusDay(d)
|
||||
dayRefs.current[d - 1]?.focus()
|
||||
}
|
||||
|
||||
const onGridKeyDown = (e: { key: string; preventDefault(): void }) => {
|
||||
const cur = focusDay != null ? focusDay : selectedInView
|
||||
let next = cur
|
||||
if (e.key === 'ArrowRight') next = cur + 1
|
||||
else if (e.key === 'ArrowLeft') next = cur - 1
|
||||
else if (e.key === 'ArrowDown') next = cur + 7
|
||||
else if (e.key === 'ArrowUp') next = cur - 7
|
||||
else return
|
||||
e.preventDefault()
|
||||
if (next < 1 || next > days) return
|
||||
focusDayRef(next)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const d = focusDay != null && focusDay >= 1 && focusDay <= days ? focusDay : selectedInView
|
||||
dayRefs.current[d - 1]?.focus()
|
||||
}, [open, days, selectedInView])
|
||||
|
||||
return (
|
||||
<div className={`cv-datepicker ${className || ''}`}>
|
||||
<button className="cv-select-trigger" ref={triggerRef} onClick={() => setOpen(!open)} type="button">
|
||||
<button className="cv-select-trigger" ref={triggerRef} onClick={() => setOpen(!open)} type="button"
|
||||
aria-haspopup="dialog" aria-expanded={open}>
|
||||
<span>{value}</span>
|
||||
<IconChevronDownFilled size={10} style={{ transform: open ? 'rotate(180deg)' : 'none', transition: 'transform 150ms' }} />
|
||||
</button>
|
||||
@@ -107,7 +140,7 @@ export default function DatePicker({ value, onChange, className }: Props) {
|
||||
<div className="cv-cal-days" role="grid">
|
||||
{WEEKDAYS.map(d => <span key={d} className="cv-cal-dayname">{d}</span>)}
|
||||
</div>
|
||||
<div className="cv-cal-grid" role="grid">
|
||||
<div className="cv-cal-grid" role="grid" onKeyDown={onGridKeyDown}>
|
||||
{Array.from({ length: offset }, (_, i) => <span key={`pad-${i}`} />)}
|
||||
{Array.from({ length: days }, (_, i) => {
|
||||
const d = i + 1
|
||||
@@ -117,8 +150,10 @@ export default function DatePicker({ value, onChange, className }: Props) {
|
||||
return (
|
||||
<button
|
||||
key={d}
|
||||
ref={el => { dayRefs.current[d - 1] = el }}
|
||||
className={`cv-cal-day ${isSelected ? 'cv-cal-day-selected' : ''} ${isToday ? 'cv-cal-day-today' : ''}`}
|
||||
onClick={() => pick(d)}
|
||||
onFocus={() => setFocusDay(d)}
|
||||
type="button"
|
||||
aria-selected={isSelected}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user