@@ -10,7 +10,7 @@ import {
1010 EyeIcon ,
1111 EyeSlashIcon ,
1212} from '@phosphor-icons/react' ;
13- import { useState } from 'react' ;
13+ import { useState , useEffect } from 'react' ;
1414import { Button } from '@/components/ui/button' ;
1515import {
1616 Card ,
@@ -72,6 +72,29 @@ export function RangeSelectionPopup({
7272 const [ selectedColor , setSelectedColor ] = useState < string > ( DEFAULT_ANNOTATION_VALUES . color ) ;
7373 const [ isPublic , setIsPublic ] = useState < boolean > ( DEFAULT_ANNOTATION_VALUES . isPublic ) ;
7474 const [ isSubmitting , setIsSubmitting ] = useState ( false ) ;
75+ const [ validationErrors , setValidationErrors ] = useState < string [ ] > ( [ ] ) ;
76+
77+ useEffect ( ( ) => {
78+ if ( ! isOpen ) return ;
79+
80+ const handleKeyDown = ( e : KeyboardEvent ) => {
81+ if ( e . key === 'Escape' ) {
82+ onClose ( ) ;
83+ } else if ( e . key === 'Enter' && e . ctrlKey && showAnnotationForm ) {
84+ e . preventDefault ( ) ;
85+ handleCreateAnnotation ( ) ;
86+ } else if ( e . key === 'z' && e . ctrlKey && ! showAnnotationForm ) {
87+ e . preventDefault ( ) ;
88+ handleZoom ( ) ;
89+ } else if ( e . key === 'a' && e . ctrlKey && ! showAnnotationForm ) {
90+ e . preventDefault ( ) ;
91+ setShowAnnotationForm ( true ) ;
92+ }
93+ } ;
94+
95+ document . addEventListener ( 'keydown' , handleKeyDown ) ;
96+ return ( ) => document . removeEventListener ( 'keydown' , handleKeyDown ) ;
97+ } , [ isOpen , showAnnotationForm ] ) ;
7598
7699 const handleZoom = ( ) => {
77100 onZoom ( dateRange ) ;
@@ -90,10 +113,11 @@ export function RangeSelectionPopup({
90113
91114 const validation = validateAnnotationForm ( formData ) ;
92115 if ( ! validation . isValid ) {
93- console . error ( 'Validation errors:' , validation . errors ) ;
116+ setValidationErrors ( validation . errors ) ;
94117 return ;
95118 }
96119
120+ setValidationErrors ( [ ] ) ;
97121 setIsSubmitting ( true ) ;
98122 try {
99123 await onCreateAnnotation ( {
@@ -133,18 +157,23 @@ export function RangeSelectionPopup({
133157 return (
134158 < div className = "fixed inset-0 z-50 flex items-center justify-center bg-black/20 backdrop-blur-sm" >
135159 < div className = "mx-4 w-full max-w-md" >
136- < Card className = "shadow-2xl border-2 bg-card/95 backdrop-blur-sm" >
160+ < Card
161+ className = "shadow-2xl border-2 bg-card/95 backdrop-blur-sm"
162+ role = "dialog"
163+ aria-labelledby = "range-selection-title"
164+ aria-describedby = "range-selection-description"
165+ >
137166 < CardHeader className = "pb-4" >
138167 < div className = "flex items-center justify-between" >
139168 < div className = "flex items-center gap-3" >
140169 < div className = "flex h-10 w-10 items-center justify-center rounded-full bg-primary/10" >
141170 < CalendarIcon className = "h-5 w-5 text-primary" />
142171 </ div >
143172 < div >
144- < CardTitle className = "text-xl" >
173+ < CardTitle id = "range-selection-title" className = "text-xl" >
145174 { showAnnotationForm ? 'Add Annotation' : 'Range Selected' }
146175 </ CardTitle >
147- < CardDescription className = "text-sm" >
176+ < CardDescription id = "range-selection-description" className = "text-sm" >
148177 { dateRange . startDate . toLocaleDateString ( 'en-US' , {
149178 month : 'short' ,
150179 day : 'numeric'
@@ -176,6 +205,7 @@ export function RangeSelectionPopup({
176205 className = "w-full h-auto py-4 flex items-center justify-start gap-4"
177206 variant = "outline"
178207 size = "lg"
208+ aria-label = "Zoom to range (Ctrl+Z)"
179209 >
180210 < div className = "flex h-12 w-12 items-center justify-center rounded-full bg-primary/10" >
181211 < MagnifyingGlassIcon className = "h-6 w-6 text-primary" />
@@ -186,13 +216,15 @@ export function RangeSelectionPopup({
186216 Focus on this period for detailed analysis
187217 </ div >
188218 </ div >
219+ < div className = "text-xs text-muted-foreground" > Ctrl+Z</ div >
189220 </ Button >
190221
191222 < Button
192223 onClick = { ( ) => setShowAnnotationForm ( true ) }
193224 className = "w-full h-auto py-4 flex items-center justify-start gap-4"
194225 variant = "outline"
195226 size = "lg"
227+ aria-label = "Add annotation (Ctrl+A)"
196228 >
197229 < div className = "flex h-12 w-12 items-center justify-center rounded-full bg-primary/10" >
198230 < NoteIcon className = "h-6 w-6 text-primary" />
@@ -203,6 +235,7 @@ export function RangeSelectionPopup({
203235 Mark this period with a note or label
204236 </ div >
205237 </ div >
238+ < div className = "text-xs text-muted-foreground" > Ctrl+A</ div >
206239 </ Button >
207240 </ div >
208241 </ >
@@ -235,11 +268,27 @@ export function RangeSelectionPopup({
235268 maxLength = { DEFAULT_ANNOTATION_VALUES . maxTextLength }
236269 className = "resize-none"
237270 disabled = { isSubmitting }
271+ autoFocus
272+ aria-describedby = "annotation-text-help annotation-text-count"
238273 />
239274 < div className = "flex justify-between items-center text-xs text-muted-foreground" >
240- < span > Keep it concise and descriptive</ span >
241- < span > { annotationText . length } /{ DEFAULT_ANNOTATION_VALUES . maxTextLength } </ span >
275+ < span id = "annotation-text-help" > Keep it concise and descriptive</ span >
276+ < span id = "annotation-text-count" className = { annotationText . length > DEFAULT_ANNOTATION_VALUES . maxTextLength * 0.9 ? 'text-warning' : '' } >
277+ { annotationText . length } /{ DEFAULT_ANNOTATION_VALUES . maxTextLength }
278+ </ span >
242279 </ div >
280+
281+ { /* Validation Errors */ }
282+ { validationErrors . length > 0 && (
283+ < div className = "space-y-1" >
284+ { validationErrors . map ( ( error , index ) => (
285+ < div key = { index } className = "text-xs text-destructive flex items-center gap-1" >
286+ < span className = "text-destructive" > ⚠</ span >
287+ { error }
288+ </ div >
289+ ) ) }
290+ </ div >
291+ ) }
243292 </ div >
244293
245294 { /* Tags */ }
@@ -370,24 +419,26 @@ export function RangeSelectionPopup({
370419 >
371420 Cancel
372421 </ Button >
373- < Button
374- onClick = { handleCreateAnnotation }
375- disabled = { ! annotationText . trim ( ) || isSubmitting }
376- className = "flex-1"
377- size = "lg"
378- >
379- { isSubmitting ? (
380- < >
381- < div className = "h-4 w-4 mr-2 animate-spin rounded-full border-2 border-current border-t-transparent" />
382- Creating...
383- </ >
384- ) : (
385- < >
386- < NoteIcon className = "h-4 w-4 mr-2" />
387- Create Annotation
388- </ >
389- ) }
390- </ Button >
422+ < Button
423+ onClick = { handleCreateAnnotation }
424+ disabled = { ! annotationText . trim ( ) || isSubmitting }
425+ className = "flex-1"
426+ size = "lg"
427+ aria-label = "Create annotation (Ctrl+Enter)"
428+ >
429+ { isSubmitting ? (
430+ < >
431+ < div className = "h-4 w-4 mr-2 animate-spin rounded-full border-2 border-current border-t-transparent" />
432+ Creating...
433+ </ >
434+ ) : (
435+ < >
436+ < NoteIcon className = "h-4 w-4 mr-2" />
437+ Create Annotation
438+ </ >
439+ ) }
440+ < span className = "text-xs ml-2 opacity-60" > Ctrl+Enter</ span >
441+ </ Button >
391442 </ div >
392443 </ div >
393444 </ >
0 commit comments