allow for keyboard navigation and adjusted card height to match regardless of text inside

This commit is contained in:
findthelorax
2026-04-20 23:57:28 -04:00
committed by Remy
parent adb2088752
commit da287d459c
+34 -7
View File
@@ -1,4 +1,4 @@
import { useState } from 'react' import { useRef, useState, type KeyboardEvent } from 'react'
import { toast } from 'sonner' import { toast } from 'sonner'
import { Check } from 'lucide-react' import { Check } from 'lucide-react'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
@@ -14,17 +14,21 @@ interface ThemeCardProps {
themeId: ThemeId themeId: ThemeId
selected: boolean selected: boolean
onClick: () => void onClick: () => void
onKeyDown?: (event: KeyboardEvent<HTMLButtonElement>) => void
buttonRef?: (element: HTMLButtonElement | null) => void
} }
function ThemeCard({ themeId, selected, onClick }: ThemeCardProps) { function ThemeCard({ themeId, selected, onClick, onKeyDown, buttonRef }: ThemeCardProps) {
const preset = THEMES[themeId] const preset = THEMES[themeId]
const c = preset.colors const c = preset.colors
return ( return (
<button <button
ref={buttonRef}
type="button" type="button"
onClick={onClick} onClick={onClick}
className="relative rounded-xl border-2 p-3 text-left transition-all duration-150 focus:outline-none w-full" onKeyDown={onKeyDown}
className="relative rounded-xl border-2 p-3 text-left transition-all duration-150 focus:outline-none w-full h-full flex flex-col"
style={{ style={{
borderColor: selected ? c.nodeAccents.isp.border : c.handleBackground, borderColor: selected ? c.nodeAccents.isp.border : c.handleBackground,
background: c.canvasBackground, background: c.canvasBackground,
@@ -76,7 +80,7 @@ function ThemeCard({ themeId, selected, onClick }: ThemeCardProps) {
{preset.label} {preset.label}
</div> </div>
<div <div
className="text-xs leading-snug mt-1 line-clamp-3 whitespace-normal wrap-break-word overflow-hidden" className="text-xs leading-snug mt-1 line-clamp-3 whitespace-normal wrap-break-word overflow-hidden min-h-12"
style={{ color: c.nodeSubtextColor }} style={{ color: c.nodeSubtextColor }}
> >
{preset.description} {preset.description}
@@ -93,6 +97,7 @@ interface ThemeModalProps {
export function ThemeModal({ open, onClose }: ThemeModalProps) { export function ThemeModal({ open, onClose }: ThemeModalProps) {
const { activeTheme, setTheme } = useThemeStore() const { activeTheme, setTheme } = useThemeStore()
const { markUnsaved } = useCanvasStore() const { markUnsaved } = useCanvasStore()
const cardRefs = useRef<Array<HTMLButtonElement | null>>([])
// Capture the theme that was active when the modal opened // Capture the theme that was active when the modal opened
const [originalTheme] = useState<ThemeId>(activeTheme) const [originalTheme] = useState<ThemeId>(activeTheme)
@@ -104,6 +109,24 @@ export function ThemeModal({ open, onClose }: ThemeModalProps) {
setTheme(id) setTheme(id)
} }
const handleCardKeyDown = (index: number) => (event: KeyboardEvent<HTMLButtonElement>) => {
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return
event.preventDefault()
const direction = event.key === 'ArrowRight' ? 1 : -1
const nextIndex = (index + direction + THEME_ORDER.length) % THEME_ORDER.length
const nextTheme = THEME_ORDER[nextIndex]
setSelected(nextTheme)
setTheme(nextTheme)
const nextCard = cardRefs.current[nextIndex]
if (!nextCard) return
nextCard.focus({ preventScroll: true })
nextCard.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' })
}
const handleApply = () => { const handleApply = () => {
setTheme(selected) setTheme(selected)
markUnsaved() markUnsaved()
@@ -126,13 +149,17 @@ export function ThemeModal({ open, onClose }: ThemeModalProps) {
<DialogTitle className="text-sm font-semibold">Choose Canvas Style</DialogTitle> <DialogTitle className="text-sm font-semibold">Choose Canvas Style</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="flex flex-nowrap gap-3 py-1 overflow-x-auto overflow-y-hidden pb-2 pr-1"> <div className="flex items-stretch flex-nowrap gap-3 py-1 overflow-x-auto overflow-y-hidden pb-2 pr-1">
{THEME_ORDER.map((id) => ( {THEME_ORDER.map((id, index) => (
<div key={id} className="shrink-0 w-30 md:w-24"> <div key={id} className="shrink-0 w-30 md:w-24 h-full">
<ThemeCard <ThemeCard
themeId={id} themeId={id}
selected={selected === id} selected={selected === id}
onClick={() => handleSelect(id)} onClick={() => handleSelect(id)}
onKeyDown={handleCardKeyDown(index)}
buttonRef={(element) => {
cardRefs.current[index] = element
}}
/> />
</div> </div>
))} ))}