@@ -10,9 +10,10 @@ import { WoltlabCoreListItemElement } from "./woltlab-core-list-item";
1010
1111 // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
1212 class WoltlabCoreListBoxElement extends HTMLParsedElement {
13+ #position = - 1 ;
1314 #selected = "" ;
1415 readonly #formInput: HTMLInputElement ;
15- readonly #items: Set < WoltlabCoreListItemElement > = new Set ( ) ;
16+ readonly #knownItems: WeakSet < WoltlabCoreListItemElement > = new WeakSet ( ) ;
1617 readonly #shadow: ShadowRoot ;
1718 readonly #slot: HTMLSlotElement ;
1819
@@ -44,12 +45,47 @@ import { WoltlabCoreListItemElement } from "./woltlab-core-list-item";
4445
4546 this . #formInput = document . createElement ( "input" ) ;
4647 this . #formInput. type = "hidden" ;
48+
49+ this . addEventListener ( "focus" , ( ) => {
50+ const items = this . #getItems( ) ;
51+ if ( items . length === 0 ) {
52+ return ;
53+ }
54+
55+ let position = items . findIndex ( ( item ) => item . selected ) ;
56+ if ( position === - 1 ) {
57+ position = 0 ;
58+ }
59+
60+ this . #setFocus( items , position ) ;
61+ } ) ;
62+
63+ this . addEventListener ( "keydown" , ( event ) => {
64+ switch ( event . key ) {
65+ case "ArrowDown" :
66+ event . preventDefault ( ) ;
67+ this . #focusNextItem( ) ;
68+ break ;
69+
70+ case "ArrowUp" :
71+ event . preventDefault ( ) ;
72+ this . #focusPreviousItem( ) ;
73+ break ;
74+
75+ case "Enter" :
76+ event . preventDefault ( ) ;
77+ this . #selectItem( ) ;
78+ break ;
79+ }
80+ } ) ;
4781 }
4882
4983 parsedCallback ( ) {
84+ this . classList . add ( "listBox" ) ;
5085 this . role = "listbox" ;
51- this . setAttribute ( "aria-multiselectable" , "false" ) ;
52- this . setAttribute ( "aria-orientation" , "vertical" ) ;
86+ this . ariaMultiSelectable = "false" ;
87+ this . ariaOrientation = "vertical" ;
88+ this . tabIndex = 0 ;
5389
5490 const selected = this . getAttribute ( "selected" ) || this . #selected;
5591 this . removeAttribute ( "selected" ) ;
@@ -72,8 +108,8 @@ import { WoltlabCoreListItemElement } from "./woltlab-core-list-item";
72108 element . selected = false ;
73109 }
74110
75- if ( ! this . #items . has ( element ) ) {
76- this . #items . add ( element ) ;
111+ if ( ! this . #knownItems . has ( element ) ) {
112+ this . #knownItems . add ( element ) ;
77113
78114 element . addEventListener ( "change" , ( event ) => {
79115 if ( event . detail . selected ) {
@@ -93,7 +129,7 @@ import { WoltlabCoreListItemElement } from "./woltlab-core-list-item";
93129 this . #selected = value ;
94130 this . setAttribute ( "selected" , value ) ;
95131
96- for ( const item of this . #items ) {
132+ for ( const item of this . #getItems ( ) ) {
97133 if ( item . selected ) {
98134 if ( item . value !== value ) {
99135 item . selected = false ;
@@ -115,6 +151,67 @@ import { WoltlabCoreListItemElement } from "./woltlab-core-list-item";
115151 this . dispatchEvent ( event ) ;
116152 }
117153
154+ #focusNextItem( ) : void {
155+ const items = this . #getItems( ) ;
156+ const size = items . length ;
157+ if ( size === 0 ) {
158+ return ;
159+ }
160+
161+ let position = this . #position + 1 ;
162+ if ( position >= size ) {
163+ position = size - 1 ;
164+ }
165+
166+ if ( position === this . #position) {
167+ return ;
168+ }
169+
170+ this . #setFocus( items , position ) ;
171+ }
172+
173+ #focusPreviousItem( ) : void {
174+ const items = this . #getItems( ) ;
175+ if ( items . length === 0 ) {
176+ return ;
177+ }
178+
179+ let position = this . #position - 1 ;
180+ if ( position < 0 ) {
181+ position = 0 ;
182+ }
183+
184+ if ( position === this . #position) {
185+ return ;
186+ }
187+
188+ this . #setFocus( items , position ) ;
189+ }
190+
191+ #setFocus( items : WoltlabCoreListItemElement [ ] , position : number ) : void {
192+ for ( let i = 0 , length = items . length ; i < length ; i ++ ) {
193+ const item = items [ i ] ;
194+
195+ if ( i === position ) {
196+ this . setAttribute ( "aria-activedescendant" , item . id ) ;
197+ item . focused = true ;
198+ } else {
199+ item . focused = false ;
200+ }
201+ }
202+
203+ this . #position = position ;
204+ }
205+
206+ #selectItem( ) : void {
207+ const item = this . #getItems( ) [ this . #position] ;
208+ if ( item === undefined ) {
209+ return ;
210+ }
211+
212+ this . #changeSelection( item . value ) ;
213+ }
214+
118215 #updateFormInput( name : string ) : void {
119216 if ( name === "" ) {
120217 this . removeAttribute ( "name" ) ;
@@ -127,6 +224,12 @@ import { WoltlabCoreListItemElement } from "./woltlab-core-list-item";
127224 }
128225 }
129226
227+ #getItems( ) : WoltlabCoreListItemElement [ ] {
228+ return Array . from ( this . #slot. assignedElements ( ) ) . filter (
229+ ( element ) => element instanceof WoltlabCoreListItemElement ,
230+ ) ;
231+ }
232+
130233 get selected ( ) : string {
131234 return this . #selected;
132235 }
0 commit comments