1- import { useState , useMemo } from 'react' ;
2- import { Select } from './Select' ;
1+ import { useState , useMemo , useRef } from 'react' ;
2+ import {
3+ useFloating ,
4+ useClick ,
5+ useDismiss ,
6+ useRole ,
7+ useListNavigation ,
8+ useInteractions ,
9+ offset ,
10+ flip ,
11+ size ,
12+ autoUpdate ,
13+ FloatingPortal ,
14+ FloatingFocusManager ,
15+ } from '@floating-ui/react' ;
316
417function App ( ) {
518 const options = [
@@ -21,24 +34,125 @@ function App() {
2134 { value :
'ZA' , label :
'南アフリカ' , thumb :
'https://cdn.jsdelivr.net/npm/[email protected] /flags/4x3/za.svg' } , 2235 ] ;
2336
37+ const [ isOpen , setIsOpen ] = useState < boolean > ( false ) ;
38+ const [ activeIndex , setActiveIndex ] = useState < number | null > ( null ) ;
2439 const [ selectedIndex , setSelectedIndex ] = useState < number | null > ( null ) ;
2540
26- const selectedValue = useMemo ( ( ) => {
41+ const { refs, floatingStyles, context } = useFloating < HTMLElement > ( {
42+ placement : "bottom-start" ,
43+ open : isOpen ,
44+ onOpenChange : setIsOpen ,
45+ whileElementsMounted : autoUpdate ,
46+ middleware : [
47+ offset ( 5 ) ,
48+ flip ( { padding : 10 } ) ,
49+ size ( {
50+ apply ( { rects, elements, availableHeight } ) {
51+ Object . assign ( elements . floating . style , {
52+ maxHeight : `${ availableHeight } px` ,
53+ minWidth : `${ rects . reference . width } px` ,
54+ } ) ;
55+ } ,
56+ padding : 10 ,
57+ } ) ,
58+ ] ,
59+ } ) ;
60+
61+ const listRef = useRef < ( HTMLElement | null ) [ ] > ( [ ] ) ;
62+
63+ const click = useClick ( context , { event : "mousedown" } ) ;
64+ const dismiss = useDismiss ( context ) ;
65+ const listNav = useListNavigation ( context , {
66+ listRef,
67+ activeIndex,
68+ selectedIndex,
69+ onNavigate : setActiveIndex ,
70+ loop : true ,
71+ } ) ;
72+ const role = useRole ( context , { role : "listbox" } ) ;
73+
74+ const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions ( [
75+ click , dismiss , listNav , role
76+ ] ) ;
77+
78+ const handleSelect = ( index : number ) => {
79+ setSelectedIndex ( index ) ;
80+ setIsOpen ( false ) ;
81+ } ;
82+
83+ const selectedLabel = useMemo ( ( ) => {
2784 if ( selectedIndex === null ) return null ;
28- return options [ selectedIndex ] ?. value ?? null ;
85+ return options [ selectedIndex ] ?. label ?? null ;
2986 } , [ selectedIndex , options ] ) ;
3087
3188 return (
3289 < >
3390 Basic React App< br />
34- 選択値(index): { selectedIndex } < br />
35- 選択値(value): { selectedValue ?? 'なし' } < br />
36- < Select
37- label = "国を選択"
38- selectedIndex = { selectedIndex }
39- options = { options }
40- onChange = { setSelectedIndex }
41- />
91+ 選択値: { selectedIndex } < br />
92+ < button
93+ type = "button"
94+ ref = { refs . setReference }
95+ aria-label = "国を選択"
96+ onClick = { ( ) => refs . domReference . current ?. focus ( ) }
97+ { ...getReferenceProps ( ) }
98+ >
99+ { selectedLabel ?? "選択してください" }
100+ </ button >
101+
102+ { isOpen && (
103+ < FloatingPortal >
104+ < FloatingFocusManager context = { context } modal = { false } >
105+ < div
106+ ref = { refs . setFloating }
107+ style = { {
108+ ...floatingStyles ,
109+ overflowY : "auto" ,
110+ background : "#eee" ,
111+ minWidth : 100 ,
112+ borderRadius : 8 ,
113+ } }
114+ { ...getFloatingProps ( ) }
115+ >
116+ { options . map ( ( { value, label, thumb } , i ) => (
117+ < button
118+ key = { value }
119+ ref = { ( node ) => {
120+ listRef . current [ i ] = node ;
121+ } }
122+ type = "button"
123+ tabIndex = { i === activeIndex ? 0 : - 1 }
124+ style = { {
125+ display : "flex" ,
126+ gap : 8 ,
127+ width : "100%" ,
128+ border : 0 ,
129+ textAlign : "left" ,
130+ background : i === activeIndex ? "cyan" : "" ,
131+ } }
132+ { ...getItemProps ( {
133+ onClick ( ) {
134+ handleSelect ( i ) ;
135+ } ,
136+ onKeyDown ( event ) {
137+ if (
138+ event . key === "Enter" ||
139+ event . key === " "
140+ ) {
141+ event . preventDefault ( ) ;
142+ handleSelect ( i ) ;
143+ }
144+ } ,
145+ } ) }
146+ >
147+ < img src = { thumb } alt = "" width = "16" />
148+ { label }
149+ { i === selectedIndex && "✅" }
150+ </ button >
151+ ) ) }
152+ </ div >
153+ </ FloatingFocusManager >
154+ </ FloatingPortal >
155+ ) }
42156 < div > 後続のコンテンツ</ div >
43157 </ >
44158 ) ;
0 commit comments