1- import React , { useMemo , useRef , useState } from "react" ;
1+ import React , { useEffect , useMemo , useRef , useState } from "react" ;
22import type { Account } from "../../domain/types" ;
3+ import type { Ref } from "react" ;
34import { formatHandle } from "../utils/account" ;
45import { useClickOutside } from "../hooks/useClickOutside" ;
56import { AccountLabel } from "./AccountLabel" ;
@@ -8,15 +9,24 @@ export const AccountSelector = ({
89 accounts,
910 activeAccountId,
1011 setActiveAccount,
12+ onSelectionDone,
13+ summaryRef,
14+ summaryTitle,
1115 variant = "panel"
1216} : {
1317 accounts : Account [ ] ;
1418 activeAccountId : string | null ;
1519 setActiveAccount : ( id : string ) => void ;
20+ onSelectionDone ?: ( ) => void ;
21+ summaryRef ?: Ref < HTMLElement > ;
22+ summaryTitle ?: string ;
1623 variant ?: "panel" | "inline" ;
1724} ) => {
1825 const [ dropdownOpen , setDropdownOpen ] = useState ( false ) ;
26+ const [ highlightedAccountId , setHighlightedAccountId ] = useState < string | null > ( null ) ;
27+ const detailsRef = useRef < HTMLDetailsElement | null > ( null ) ;
1928 const dropdownRef = useRef < HTMLDivElement | null > ( null ) ;
29+ const selectionChangeRef = useRef ( false ) ;
2030
2131 useClickOutside ( dropdownRef , dropdownOpen , ( ) => setDropdownOpen ( false ) ) ;
2232
@@ -25,6 +35,75 @@ export const AccountSelector = ({
2535 [ accounts , activeAccountId ]
2636 ) ;
2737
38+ useEffect ( ( ) => {
39+ if ( ! dropdownOpen ) {
40+ setHighlightedAccountId ( null ) ;
41+ return ;
42+ }
43+ setHighlightedAccountId ( activeAccountId ?? accounts [ 0 ] ?. id ?? null ) ;
44+ } , [ activeAccountId , accounts , dropdownOpen ] ) ;
45+
46+ useEffect ( ( ) => {
47+ if ( ! dropdownOpen && selectionChangeRef . current ) {
48+ selectionChangeRef . current = false ;
49+ onSelectionDone ?.( ) ;
50+ }
51+ } , [ dropdownOpen , onSelectionDone ] ) ;
52+
53+ useEffect ( ( ) => {
54+ if ( ! dropdownOpen ) {
55+ return ;
56+ }
57+
58+ const handleKeyDown = ( event : KeyboardEvent ) => {
59+ if ( ! dropdownOpen ) {
60+ return ;
61+ }
62+ if ( ! detailsRef . current ?. contains ( document . activeElement ) ) {
63+ return ;
64+ }
65+ if ( accounts . length === 0 ) {
66+ return ;
67+ }
68+
69+ const currentIndex = Math . max (
70+ 0 ,
71+ accounts . findIndex ( ( account ) => account . id === ( highlightedAccountId ?? activeAccountId ) )
72+ ) ;
73+
74+ if ( event . key === "ArrowDown" || event . key === "ArrowUp" ) {
75+ event . preventDefault ( ) ;
76+ const offset = event . key === "ArrowDown" ? 1 : - 1 ;
77+ const nextIndex = ( currentIndex + offset + accounts . length ) % accounts . length ;
78+ const nextAccount = accounts [ nextIndex ] ;
79+ if ( nextAccount ) {
80+ setHighlightedAccountId ( nextAccount . id ) ;
81+ selectionChangeRef . current = true ;
82+ setActiveAccount ( nextAccount . id ) ;
83+ }
84+ return ;
85+ }
86+
87+ if ( event . key === "Enter" ) {
88+ event . preventDefault ( ) ;
89+ if ( highlightedAccountId ) {
90+ selectionChangeRef . current = true ;
91+ setActiveAccount ( highlightedAccountId ) ;
92+ }
93+ setDropdownOpen ( false ) ;
94+ return ;
95+ }
96+
97+ if ( event . key === "Escape" ) {
98+ event . preventDefault ( ) ;
99+ setDropdownOpen ( false ) ;
100+ }
101+ } ;
102+
103+ window . addEventListener ( "keydown" , handleKeyDown ) ;
104+ return ( ) => window . removeEventListener ( "keydown" , handleKeyDown ) ;
105+ } , [ accounts , activeAccountId , dropdownOpen , highlightedAccountId , setActiveAccount ] ) ;
106+
28107 const wrapperClassName =
29108 variant === "panel" ? "panel account-selector-panel" : "account-selector-inline" ;
30109 const Wrapper = variant === "panel" ? "section" : "div" ;
@@ -33,11 +112,16 @@ export const AccountSelector = ({
33112 < Wrapper className = { wrapperClassName } >
34113 < div className = "account-selector-header" >
35114 < details
115+ ref = { detailsRef }
36116 className = "account-selector"
37117 open = { dropdownOpen }
38118 onToggle = { ( event ) => setDropdownOpen ( event . currentTarget . open ) }
39119 >
40- < summary className = "account-selector-summary" >
120+ < summary
121+ ref = { summaryRef }
122+ className = "account-selector-summary"
123+ title = { summaryTitle ?? "계정 선택 (Ctrl+Shift+A)" }
124+ >
41125 { activeAccount ? (
42126 < AccountLabel
43127 avatarUrl = { activeAccount . avatarUrl }
@@ -59,11 +143,22 @@ export const AccountSelector = ({
59143 < ul className = "account-list" >
60144 { accounts . map ( ( account ) => {
61145 const isActiveAccount = account . id === activeAccountId ;
146+ const classNames = [ ] as string [ ] ;
147+ if ( account . id === highlightedAccountId ) {
148+ classNames . push ( "is-highlighted" ) ;
149+ }
150+ if ( isActiveAccount ) {
151+ classNames . push ( "active" ) ;
152+ }
62153 return (
63- < li key = { account . id } className = { isActiveAccount ? "active" : "" } >
154+ < li
155+ key = { account . id }
156+ className = { classNames . join ( " " ) }
157+ >
64158 < button
65159 type = "button"
66160 onClick = { ( ) => {
161+ selectionChangeRef . current = true ;
67162 setActiveAccount ( account . id ) ;
68163 setDropdownOpen ( false ) ;
69164 } }
0 commit comments