1+ import React , { useState , useEffect , useRef } from 'react' ;
2+
3+ export const Modal = ( { isOpen, onClose, title, children } : any ) => {
4+ const ref = useRef < HTMLDivElement > ( null ) ;
5+
6+ useEffect ( ( ) => {
7+ if ( ! isOpen ) return ;
8+ const el = ref . current ;
9+ if ( ! el ) return ;
10+
11+ // Focus trap implementation
12+ const focusable = Array . from ( el . querySelectorAll < HTMLElement > (
13+ 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
14+ ) ) ;
15+ const first = focusable [ 0 ] ;
16+ const last = focusable [ focusable . length - 1 ] ;
17+
18+ const handleKeyDown = ( e : KeyboardEvent ) => {
19+ if ( e . key === 'Tab' ) {
20+ if ( e . shiftKey ) {
21+ if ( document . activeElement === first ) {
22+ e . preventDefault ( ) ;
23+ last ?. focus ( ) ;
24+ }
25+ } else {
26+ if ( document . activeElement === last ) {
27+ e . preventDefault ( ) ;
28+ first ?. focus ( ) ;
29+ }
30+ }
31+ } else if ( e . key === 'Escape' ) {
32+ onClose ( ) ;
33+ }
34+ } ;
35+
36+ el . addEventListener ( 'keydown' , handleKeyDown ) ;
37+ first ?. focus ( ) ;
38+ return ( ) => el . removeEventListener ( 'keydown' , handleKeyDown ) ;
39+ } , [ isOpen , onClose ] ) ;
40+
41+ if ( ! isOpen ) return null ;
42+
43+ return (
44+ < div role = "dialog" aria-modal = "true" aria-labelledby = "modal-title" ref = { ref } >
45+ < h2 id = "modal-title" > { title } </ h2 >
46+ { children }
47+ < button onClick = { onClose } aria-label = "Close modal" > Close</ button >
48+ </ div >
49+ ) ;
50+ } ;
51+
52+ export const Dropdown = ( { options } : { options : string [ ] } ) => {
53+ const [ isOpen , setIsOpen ] = useState ( false ) ;
54+ const [ selected , setSelected ] = useState ( options [ 0 ] ) ;
55+
56+ return (
57+ < div >
58+ < button
59+ aria-haspopup = "listbox"
60+ aria-expanded = { isOpen }
61+ onClick = { ( ) => setIsOpen ( ! isOpen ) }
62+ >
63+ { selected }
64+ </ button >
65+ { isOpen && (
66+ < ul role = "listbox" aria-activedescendant = { selected } >
67+ { options . map ( ( opt ) => (
68+ < li
69+ key = { opt }
70+ role = "option"
71+ id = { opt }
72+ aria-selected = { selected === opt }
73+ onClick = { ( ) => { setSelected ( opt ) ; setIsOpen ( false ) ; } }
74+ >
75+ { opt }
76+ </ li >
77+ ) ) }
78+ </ ul >
79+ ) }
80+ </ div >
81+ ) ;
82+ } ;
83+
84+ export const Tabs = ( { tabs } : { tabs : { id : string , label : string , content : React . ReactNode } [ ] } ) => {
85+ const [ activeIndex , setActiveIndex ] = useState ( 0 ) ;
86+ const tabsRef = useRef < ( HTMLButtonElement | null ) [ ] > ( [ ] ) ;
87+
88+ const handleKeyDown = ( e : React . KeyboardEvent , index : number ) => {
89+ let newIndex = activeIndex ;
90+ if ( e . key === 'ArrowRight' ) {
91+ newIndex = ( index + 1 ) % tabs . length ;
92+ } else if ( e . key === 'ArrowLeft' ) {
93+ newIndex = ( index - 1 + tabs . length ) % tabs . length ;
94+ } else {
95+ return ;
96+ }
97+ setActiveIndex ( newIndex ) ;
98+ tabsRef . current [ newIndex ] ?. focus ( ) ;
99+ } ;
100+
101+ return (
102+ < div >
103+ < div role = "tablist" aria-label = "Sample Tabs" >
104+ { tabs . map ( ( tab , i ) => (
105+ < button
106+ key = { tab . id }
107+ role = "tab"
108+ aria-selected = { activeIndex === i }
109+ aria-controls = { `panel-${ tab . id } ` }
110+ id = { `tab-${ tab . id } ` }
111+ ref = { ( el ) => { tabsRef . current [ i ] = el ; } }
112+ onClick = { ( ) => setActiveIndex ( i ) }
113+ onKeyDown = { ( e ) => handleKeyDown ( e , i ) }
114+ tabIndex = { activeIndex === i ? 0 : - 1 }
115+ >
116+ { tab . label }
117+ </ button >
118+ ) ) }
119+ </ div >
120+ { tabs . map ( ( tab , i ) => (
121+ < div
122+ key = { tab . id }
123+ role = "tabpanel"
124+ id = { `panel-${ tab . id } ` }
125+ aria-labelledby = { `tab-${ tab . id } ` }
126+ hidden = { activeIndex !== i }
127+ tabIndex = { 0 }
128+ >
129+ { tab . content }
130+ </ div >
131+ ) ) }
132+ </ div >
133+ ) ;
134+ } ;
135+
136+ export const Form = ( ) => (
137+ < form noValidate onSubmit = { ( e ) => e . preventDefault ( ) } >
138+ < label htmlFor = "username" > Username</ label >
139+ < input id = "username" type = "text" aria-required = "true" required />
140+ < label htmlFor = "email" > Email</ label >
141+ < input id = "email" type = "email" />
142+ < button type = "submit" > Submit</ button >
143+ </ form >
144+ ) ;
145+
146+ export const Toast = ( { message, type = 'info' } : { message : string , type ?: 'info' | 'error' } ) => (
147+ < div role = { type === 'error' ? 'alert' : 'status' } aria-live = { type === 'error' ? 'assertive' : 'polite' } >
148+ { message }
149+ </ div >
150+ ) ;
0 commit comments