88
99import { OptionPattern } from './option' ;
1010import { KeyboardEventManager , PointerEventManager , Modifier } from '../behaviors/event-manager' ;
11- import { ListSelection , ListSelectionInputs } from '../behaviors/list-selection/list-selection' ;
12- import { ListTypeahead , ListTypeaheadInputs } from '../behaviors/list-typeahead/list-typeahead' ;
13- import { ListNavigation , ListNavigationInputs } from '../behaviors/list-navigation/list-navigation' ;
14- import { ListFocus , ListFocusInputs } from '../behaviors/list-focus/list-focus' ;
1511import { computed , signal } from '@angular/core' ;
1612import { SignalLike } from '../behaviors/signal-like/signal-like' ;
17-
18- /** The selection operations that the listbox can perform. */
19- interface SelectOptions {
20- toggle ?: boolean ;
21- selectOne ?: boolean ;
22- selectRange ?: boolean ;
23- anchor ?: boolean ;
24- }
13+ import { List , ListInputs } from '../behaviors/list/list' ;
2514
2615/** Represents the required inputs for a listbox. */
27- export type ListboxInputs < V > = ListNavigationInputs < OptionPattern < V > > &
28- ListSelectionInputs < OptionPattern < V > , V > &
29- ListTypeaheadInputs < OptionPattern < V > > &
30- ListFocusInputs < OptionPattern < V > > & {
31- readonly : SignalLike < boolean > ;
32- } ;
16+ export type ListboxInputs < V > = ListInputs < OptionPattern < V > , V > & {
17+ readonly : SignalLike < boolean > ;
18+ } ;
3319
3420/** Controls the state of a listbox. */
3521export class ListboxPattern < V > {
36- /** Controls navigation for the listbox. */
37- navigation : ListNavigation < OptionPattern < V > > ;
38-
39- /** Controls selection for the listbox. */
40- selection : ListSelection < OptionPattern < V > , V > ;
41-
42- /** Controls typeahead for the listbox. */
43- typeahead : ListTypeahead < OptionPattern < V > > ;
44-
45- /** Controls focus for the listbox. */
46- focusManager : ListFocus < OptionPattern < V > > ;
22+ listBehavior : List < OptionPattern < V > , V > ;
4723
4824 /** Whether the list is vertically or horizontally oriented. */
4925 orientation : SignalLike < 'vertical' | 'horizontal' > ;
5026
5127 /** Whether the listbox is disabled. */
52- disabled = computed ( ( ) => this . focusManager . isListDisabled ( ) ) ;
28+ disabled = computed ( ( ) => this . listBehavior . disabled ( ) ) ;
5329
5430 /** Whether the listbox is readonly. */
5531 readonly : SignalLike < boolean > ;
5632
5733 /** The tabindex of the listbox. */
58- tabindex = computed ( ( ) => this . focusManager . getListTabindex ( ) ) ;
34+ tabindex = computed ( ( ) => this . listBehavior . tabindex ( ) ) ;
5935
6036 /** The id of the current active item. */
61- activedescendant = computed ( ( ) => this . focusManager . getActiveDescendant ( ) ) ;
37+ activedescendant = computed ( ( ) => this . listBehavior . activedescendant ( ) ) ;
6238
6339 /** Whether multiple items in the list can be selected at once. */
6440 multi : SignalLike < boolean > ;
6541
6642 /** The number of items in the listbox. */
67- setsize = computed ( ( ) => this . navigation . inputs . items ( ) . length ) ;
43+ setsize = computed ( ( ) => this . inputs . items ( ) . length ) ;
6844
6945 /** Whether the listbox selection follows focus. */
7046 followFocus = computed ( ( ) => this . inputs . selectionMode ( ) === 'follow' ) ;
@@ -89,98 +65,84 @@ export class ListboxPattern<V> {
8965 } ) ;
9066
9167 /** Represents the space key. Does nothing when the user is actively using typeahead. */
92- dynamicSpaceKey = computed ( ( ) => ( this . typeahead . isTyping ( ) ? '' : ' ' ) ) ;
68+ dynamicSpaceKey = computed ( ( ) => ( this . listBehavior . isTyping ( ) ? '' : ' ' ) ) ;
9369
9470 /** The regexp used to decide if a key should trigger typeahead. */
9571 typeaheadRegexp = / ^ .$ / ; // TODO: Ignore spaces?
9672
97- /**
98- * The uncommitted index for selecting a range of options.
99- *
100- * NOTE: This is subtly distinct from the "rangeStartIndex" in the ListSelection behavior.
101- * The anchorIndex does not necessarily represent the start of a range, but represents the most
102- * recent index where the user showed intent to begin a range selection. Usually, this is wherever
103- * the user most recently pressed the "Shift" key, but if the user presses shift + space to select
104- * from the anchor, the user is not intending to start a new range from this index.
105- *
106- * In other words, "rangeStartIndex" is only set when a user commits to starting a range selection
107- * while "anchorIndex" is set whenever a user indicates they may be starting a range selection.
108- */
109- anchorIndex = signal ( 0 ) ;
110-
11173 /** The keydown event manager for the listbox. */
11274 keydown = computed ( ( ) => {
11375 const manager = new KeyboardEventManager ( ) ;
11476
11577 if ( this . readonly ( ) ) {
11678 return manager
117- . on ( this . prevKey , ( ) => this . prev ( ) )
118- . on ( this . nextKey , ( ) => this . next ( ) )
119- . on ( 'Home' , ( ) => this . first ( ) )
120- . on ( 'End' , ( ) => this . last ( ) )
121- . on ( this . typeaheadRegexp , e => this . search ( e . key ) ) ;
79+ . on ( this . prevKey , ( ) => this . listBehavior . prev ( ) )
80+ . on ( this . nextKey , ( ) => this . listBehavior . next ( ) )
81+ . on ( 'Home' , ( ) => this . listBehavior . first ( ) )
82+ . on ( 'End' , ( ) => this . listBehavior . last ( ) )
83+ . on ( this . typeaheadRegexp , e => this . listBehavior . search ( e . key ) ) ;
12284 }
12385
12486 if ( ! this . followFocus ( ) ) {
12587 manager
126- . on ( this . prevKey , ( ) => this . prev ( ) )
127- . on ( this . nextKey , ( ) => this . next ( ) )
128- . on ( 'Home' , ( ) => this . first ( ) )
129- . on ( 'End' , ( ) => this . last ( ) )
130- . on ( this . typeaheadRegexp , e => this . search ( e . key ) ) ;
88+ . on ( this . prevKey , ( ) => this . listBehavior . prev ( ) )
89+ . on ( this . nextKey , ( ) => this . listBehavior . next ( ) )
90+ . on ( 'Home' , ( ) => this . listBehavior . first ( ) )
91+ . on ( 'End' , ( ) => this . listBehavior . last ( ) )
92+ . on ( this . typeaheadRegexp , e => this . listBehavior . search ( e . key ) ) ;
13193 }
13294
13395 if ( this . followFocus ( ) ) {
13496 manager
135- . on ( this . prevKey , ( ) => this . prev ( { selectOne : true } ) )
136- . on ( this . nextKey , ( ) => this . next ( { selectOne : true } ) )
137- . on ( 'Home' , ( ) => this . first ( { selectOne : true } ) )
138- . on ( 'End' , ( ) => this . last ( { selectOne : true } ) )
139- . on ( this . typeaheadRegexp , e => this . search ( e . key , { selectOne : true } ) ) ;
97+ . on ( this . prevKey , ( ) => this . listBehavior . prev ( { selectOne : true } ) )
98+ . on ( this . nextKey , ( ) => this . listBehavior . next ( { selectOne : true } ) )
99+ . on ( 'Home' , ( ) => this . listBehavior . first ( { selectOne : true } ) )
100+ . on ( 'End' , ( ) => this . listBehavior . last ( { selectOne : true } ) )
101+ . on ( this . typeaheadRegexp , e => this . listBehavior . search ( e . key , { selectOne : true } ) ) ;
140102 }
141103
142104 if ( this . inputs . multi ( ) ) {
143105 manager
144- . on ( Modifier . Any , 'Shift' , ( ) => this . anchorIndex . set ( this . inputs . activeIndex ( ) ) )
145- . on ( Modifier . Shift , this . prevKey , ( ) => this . prev ( { selectRange : true } ) )
146- . on ( Modifier . Shift , this . nextKey , ( ) => this . next ( { selectRange : true } ) )
106+ . on ( Modifier . Any , 'Shift' , ( ) => this . listBehavior . anchor ( this . inputs . activeIndex ( ) ) )
107+ . on ( Modifier . Shift , this . prevKey , ( ) => this . listBehavior . prev ( { selectRange : true } ) )
108+ . on ( Modifier . Shift , this . nextKey , ( ) => this . listBehavior . next ( { selectRange : true } ) )
147109 . on ( [ Modifier . Ctrl | Modifier . Shift , Modifier . Meta | Modifier . Shift ] , 'Home' , ( ) =>
148- this . first ( { selectRange : true , anchor : false } ) ,
110+ this . listBehavior . first ( { selectRange : true , anchor : false } ) ,
149111 )
150112 . on ( [ Modifier . Ctrl | Modifier . Shift , Modifier . Meta | Modifier . Shift ] , 'End' , ( ) =>
151- this . last ( { selectRange : true , anchor : false } ) ,
113+ this . listBehavior . last ( { selectRange : true , anchor : false } ) ,
152114 )
153115 . on ( Modifier . Shift , 'Enter' , ( ) =>
154- this . _updateSelection ( { selectRange : true , anchor : false } ) ,
116+ this . listBehavior . updateSelection ( { selectRange : true , anchor : false } ) ,
155117 )
156118 . on ( Modifier . Shift , this . dynamicSpaceKey , ( ) =>
157- this . _updateSelection ( { selectRange : true , anchor : false } ) ,
119+ this . listBehavior . updateSelection ( { selectRange : true , anchor : false } ) ,
158120 ) ;
159121 }
160122
161123 if ( ! this . followFocus ( ) && this . inputs . multi ( ) ) {
162124 manager
163- . on ( this . dynamicSpaceKey , ( ) => this . selection . toggle ( ) )
164- . on ( 'Enter' , ( ) => this . selection . toggle ( ) )
165- . on ( [ Modifier . Ctrl , Modifier . Meta ] , 'A' , ( ) => this . selection . toggleAll ( ) ) ;
125+ . on ( this . dynamicSpaceKey , ( ) => this . listBehavior . toggle ( ) )
126+ . on ( 'Enter' , ( ) => this . listBehavior . toggle ( ) )
127+ . on ( [ Modifier . Ctrl , Modifier . Meta ] , 'A' , ( ) => this . listBehavior . toggleAll ( ) ) ;
166128 }
167129
168130 if ( ! this . followFocus ( ) && ! this . inputs . multi ( ) ) {
169- manager . on ( this . dynamicSpaceKey , ( ) => this . selection . toggleOne ( ) ) ;
170- manager . on ( 'Enter' , ( ) => this . selection . toggleOne ( ) ) ;
131+ manager . on ( this . dynamicSpaceKey , ( ) => this . listBehavior . toggleOne ( ) ) ;
132+ manager . on ( 'Enter' , ( ) => this . listBehavior . toggleOne ( ) ) ;
171133 }
172134
173135 if ( this . inputs . multi ( ) && this . followFocus ( ) ) {
174136 manager
175- . on ( [ Modifier . Ctrl , Modifier . Meta ] , this . prevKey , ( ) => this . prev ( ) )
176- . on ( [ Modifier . Ctrl , Modifier . Meta ] , this . nextKey , ( ) => this . next ( ) )
177- . on ( [ Modifier . Ctrl , Modifier . Meta ] , ' ' , ( ) => this . selection . toggle ( ) )
178- . on ( [ Modifier . Ctrl , Modifier . Meta ] , 'Enter' , ( ) => this . selection . toggle ( ) )
179- . on ( [ Modifier . Ctrl , Modifier . Meta ] , 'Home' , ( ) => this . first ( ) )
180- . on ( [ Modifier . Ctrl , Modifier . Meta ] , 'End' , ( ) => this . last ( ) )
137+ . on ( [ Modifier . Ctrl , Modifier . Meta ] , this . prevKey , ( ) => this . listBehavior . prev ( ) )
138+ . on ( [ Modifier . Ctrl , Modifier . Meta ] , this . nextKey , ( ) => this . listBehavior . next ( ) )
139+ . on ( [ Modifier . Ctrl , Modifier . Meta ] , ' ' , ( ) => this . listBehavior . toggle ( ) )
140+ . on ( [ Modifier . Ctrl , Modifier . Meta ] , 'Enter' , ( ) => this . listBehavior . toggle ( ) )
141+ . on ( [ Modifier . Ctrl , Modifier . Meta ] , 'Home' , ( ) => this . listBehavior . first ( ) )
142+ . on ( [ Modifier . Ctrl , Modifier . Meta ] , 'End' , ( ) => this . listBehavior . last ( ) )
181143 . on ( [ Modifier . Ctrl , Modifier . Meta ] , 'A' , ( ) => {
182- this . selection . toggleAll ( ) ;
183- this . selection . select ( ) ; // Ensure the currect option remains selected.
144+ this . listBehavior . toggleAll ( ) ;
145+ this . listBehavior . select ( ) ; // Ensure the currect option remains selected.
184146 } ) ;
185147 }
186148
@@ -192,29 +154,31 @@ export class ListboxPattern<V> {
192154 const manager = new PointerEventManager ( ) ;
193155
194156 if ( this . readonly ( ) ) {
195- return manager . on ( e => this . goto ( e ) ) ;
157+ return manager . on ( e => this . listBehavior . goto ( this . _getItem ( e ) ! ) ) ;
196158 }
197159
198160 if ( this . multi ( ) ) {
199- manager . on ( Modifier . Shift , e => this . goto ( e , { selectRange : true } ) ) ;
161+ manager . on ( Modifier . Shift , e =>
162+ this . listBehavior . goto ( this . _getItem ( e ) ! , { selectRange : true } ) ,
163+ ) ;
200164 }
201165
202166 if ( ! this . multi ( ) && this . followFocus ( ) ) {
203- return manager . on ( e => this . goto ( e , { selectOne : true } ) ) ;
167+ return manager . on ( e => this . listBehavior . goto ( this . _getItem ( e ) ! , { selectOne : true } ) ) ;
204168 }
205169
206170 if ( ! this . multi ( ) && ! this . followFocus ( ) ) {
207- return manager . on ( e => this . goto ( e , { toggle : true } ) ) ;
171+ return manager . on ( e => this . listBehavior . goto ( this . _getItem ( e ) ! , { toggle : true } ) ) ;
208172 }
209173
210174 if ( this . multi ( ) && this . followFocus ( ) ) {
211175 return manager
212- . on ( e => this . goto ( e , { selectOne : true } ) )
213- . on ( Modifier . Ctrl , e => this . goto ( e , { toggle : true } ) ) ;
176+ . on ( e => this . listBehavior . goto ( this . _getItem ( e ) ! , { selectOne : true } ) )
177+ . on ( Modifier . Ctrl , e => this . listBehavior . goto ( this . _getItem ( e ) ! , { toggle : true } ) ) ;
214178 }
215179
216180 if ( this . multi ( ) && ! this . followFocus ( ) ) {
217- return manager . on ( e => this . goto ( e , { toggle : true } ) ) ;
181+ return manager . on ( e => this . listBehavior . goto ( this . _getItem ( e ) ! , { toggle : true } ) ) ;
218182 }
219183
220184 return manager ;
@@ -225,14 +189,7 @@ export class ListboxPattern<V> {
225189 this . orientation = inputs . orientation ;
226190 this . multi = inputs . multi ;
227191
228- this . focusManager = new ListFocus ( inputs ) ;
229- this . selection = new ListSelection ( { ...inputs , focusManager : this . focusManager } ) ;
230- this . typeahead = new ListTypeahead ( { ...inputs , focusManager : this . focusManager } ) ;
231- this . navigation = new ListNavigation ( {
232- ...inputs ,
233- focusManager : this . focusManager ,
234- wrap : computed ( ( ) => this . wrap ( ) && this . inputs . wrap ( ) ) ,
235- } ) ;
192+ this . listBehavior = new List ( inputs ) ;
236193 }
237194
238195 /** Returns a set of violations */
@@ -270,37 +227,6 @@ export class ListboxPattern<V> {
270227 }
271228 }
272229
273- /** Navigates to the first option in the listbox. */
274- first ( opts ?: SelectOptions ) {
275- this . _navigate ( opts , ( ) => this . navigation . first ( ) ) ;
276- }
277-
278- /** Navigates to the last option in the listbox. */
279- last ( opts ?: SelectOptions ) {
280- this . _navigate ( opts , ( ) => this . navigation . last ( ) ) ;
281- }
282-
283- /** Navigates to the next option in the listbox. */
284- next ( opts ?: SelectOptions ) {
285- this . _navigate ( opts , ( ) => this . navigation . next ( ) ) ;
286- }
287-
288- /** Navigates to the previous option in the listbox. */
289- prev ( opts ?: SelectOptions ) {
290- this . _navigate ( opts , ( ) => this . navigation . prev ( ) ) ;
291- }
292-
293- /** Navigates to the given item in the listbox. */
294- goto ( event : PointerEvent , opts ?: SelectOptions ) {
295- const item = this . _getItem ( event ) ;
296- this . _navigate ( opts , ( ) => this . navigation . goto ( item ) ) ;
297- }
298-
299- /** Handles typeahead search navigation for the listbox. */
300- search ( char : string , opts ?: SelectOptions ) {
301- this . _navigate ( opts , ( ) => this . typeahead . search ( char ) ) ;
302- }
303-
304230 /**
305231 * Sets the listbox to it's default initial state.
306232 *
@@ -315,7 +241,7 @@ export class ListboxPattern<V> {
315241 let firstItem : OptionPattern < V > | null = null ;
316242
317243 for ( const item of this . inputs . items ( ) ) {
318- if ( this . focusManager . isFocusable ( item ) ) {
244+ if ( this . listBehavior . isFocusable ( item ) ) {
319245 if ( ! firstItem ) {
320246 firstItem = item ;
321247 }
@@ -331,46 +257,6 @@ export class ListboxPattern<V> {
331257 }
332258 }
333259
334- /**
335- * Safely performs a navigation operation.
336- *
337- * Handles conditionally disabling wrapping for when a navigation
338- * operation is occurring while the user is selecting a range of options.
339- *
340- * Handles boilerplate calling of focus & selection operations. Also ensures these
341- * additional operations are only called if the navigation operation moved focus to a new option.
342- */
343- private _navigate ( opts : SelectOptions = { } , operation : ( ) => boolean ) {
344- if ( opts ?. selectRange ) {
345- this . wrap . set ( false ) ;
346- this . selection . rangeStartIndex . set ( this . anchorIndex ( ) ) ;
347- }
348-
349- const moved = operation ( ) ;
350-
351- if ( moved ) {
352- this . _updateSelection ( opts ) ;
353- }
354-
355- this . wrap . set ( true ) ;
356- }
357-
358- /** Handles updating selection for the listbox. */
359- private _updateSelection ( opts : SelectOptions = { anchor : true } ) {
360- if ( opts . toggle ) {
361- this . selection . toggle ( ) ;
362- }
363- if ( opts . selectOne ) {
364- this . selection . selectOne ( ) ;
365- }
366- if ( opts . selectRange ) {
367- this . selection . selectRange ( ) ;
368- }
369- if ( ! opts . anchor ) {
370- this . anchorIndex . set ( this . selection . rangeStartIndex ( ) ) ;
371- }
372- }
373-
374260 private _getItem ( e : PointerEvent ) {
375261 if ( ! ( e . target instanceof HTMLElement ) ) {
376262 return ;
0 commit comments