11import { createContext , h } from 'preact' ;
22import { useCallback , useContext } from 'preact/hooks' ;
3- import { signal , useComputed , useSignal } from '@preact/signals' ;
3+ import { signal , useComputed } from '@preact/signals' ;
44import { usePlatformName } from '../../types.js' ;
5- import { eventToIntention , invariant } from '../../utils.js' ;
5+ import { eventToIntention } from '../../utils.js' ;
66import { useHistoryServiceDispatch , useResultsData } from './HistoryServiceProvider.js' ;
7+ import { useSelectionStateApi } from '../hooks/useSelectionState.js' ;
78
89/**
910 * @typedef {(s: (d: Set<number>) => Set<number>, reason: string) => void } UpdateSelected
1011 * @typedef {import("../../utils.js").Intention } Intention
11- * @typedef {{ focusedIndex: number|null; anchorIndex: number|null; lastShiftRange: { end:number|null; start:number|null }} } SelectionState
12+ * @typedef {import('../hooks/useSelectionState.js').Action } Action
13+ * @typedef {import('../hooks/useSelectionState.js').SelectionState } SelectionState
1214 * @import { ReadonlySignal } from '@preact/signals'
1315 */
1416
15- /**
16- * @typedef {{kind: 'select-index', index: number, reason?: string}
17- * | {kind: 'toggle-index'; index: number; reason?: string}
18- * | {kind: 'expand-selected-to-index'; index: number; reason?: string}
19- * | {kind: 'inc-or-dec-selected'; nextIndex: number; reason?: string}
20- * | {kind: 'move-selection'; direction: 'up' | 'down'; total: number; reason?: string}
21- * | {kind: 'increment-selection'; direction: 'up' | 'down'; total: number; reason?: string}
22- * | {kind: 'reset'; reason?: string}
23- * } Action
24- */
2517const SelectionDispatchContext = createContext ( /** @type {(a: Action) => void } */ ( ( a ) => { } ) ) ;
26- const SelectionContext = createContext ( /** @type {ReadonlySignal<Set<number>> } */ ( signal ( new Set ( [ ] ) ) ) ) ;
27- const FocussedContext = createContext ( /** @type {ReadonlySignal<number|null> } */ ( signal ( null ) ) ) ;
18+ const SelectionStateContext = createContext ( /** @type {ReadonlySignal<SelectionState> } */ ( signal ( { } ) ) ) ;
2819
2920/**
3021 * Provides a context for the selections + state for managing updates (like keyboard+clicks)
@@ -33,196 +24,33 @@ const FocussedContext = createContext(/** @type {ReadonlySignal<number|null>} */
3324 * @param {import("preact").ComponentChild } props.children - The child components that will consume the history service context.
3425 */
3526export function SelectionProvider ( { children } ) {
36- const state = useSignal ( defaultState ) ;
37- const selected = useComputed ( ( ) => state . value . selected ) ;
38- const focussed = useComputed ( ( ) => state . value . focusedIndex ) ;
39- /**
40- * @param {Action } evt
41- */
42- function dispatch ( evt ) {
43- console . log ( evt ) ;
44- const next = reducer ( state . value , evt ) ;
45- state . value = next ;
46-
47- // after-effects?
48- if ( evt . kind === 'move-selection' || evt . kind === 'increment-selection' ) {
49- const match = document . querySelector ( `[aria-selected][data-index="${ state . value . focusedIndex } "]` ) ;
50- match ?. scrollIntoView ( { block : 'nearest' , inline : 'nearest' } ) ;
51- }
52- }
53- const dispatcher = useCallback ( dispatch , [ state , selected ] ) ;
27+ const { dispatch, state } = useSelectionStateApi ( ) ;
5428
5529 return (
56- < SelectionContext . Provider value = { selected } >
57- < FocussedContext . Provider value = { focussed } >
58- < SelectionDispatchContext . Provider value = { dispatcher } > { children } </ SelectionDispatchContext . Provider >
59- </ FocussedContext . Provider >
60- </ SelectionContext . Provider >
30+ < SelectionStateContext . Provider value = { state } >
31+ < SelectionDispatchContext . Provider value = { dispatch } > { children } </ SelectionDispatchContext . Provider >
32+ </ SelectionStateContext . Provider >
6133 ) ;
6234}
6335
64- // Hook for consuming the context
36+ export function useSelectionState ( ) {
37+ return useContext ( SelectionStateContext ) ;
38+ }
39+
6540export function useSelected ( ) {
66- return useContext ( SelectionContext ) ;
41+ const state = useContext ( SelectionStateContext ) ;
42+ return useComputed ( ( ) => state . value . selected ) ;
6743}
6844
69- function useFocussedIndex ( ) {
70- return useContext ( FocussedContext ) ;
45+ export function useFocussedIndex ( ) {
46+ const state = useContext ( SelectionStateContext ) ;
47+ return useComputed ( ( ) => state . value . focusedIndex ) ;
7148}
7249
7350export function useSelectionDispatch ( ) {
7451 return useContext ( SelectionDispatchContext ) ;
7552}
7653
77- const defaultState = {
78- anchorIndex : /** @type {null|number } */ ( null ) ,
79- /** @type {{start: null|number; end: null|number} } */
80- lastShiftRange : {
81- start : null ,
82- end : null ,
83- } ,
84- focusedIndex : /** @type {null|number } */ ( null ) ,
85- selected : new Set ( /** @type {number[] } */ ( [ ] ) ) ,
86- } ;
87-
88- /**
89- * @param {typeof defaultState } prev
90- * @param {Action } evt
91- * @return {typeof defaultState }
92- */
93- function reducer ( prev , evt ) {
94- switch ( evt . kind ) {
95- case 'reset' : {
96- return { ...defaultState } ;
97- }
98- case 'move-selection' : {
99- const { focusedIndex } = prev ;
100- invariant ( focusedIndex !== null ) ;
101- const delta = evt . direction === 'up' ? - 1 : 1 ;
102- // either the last item, or current + 1
103- const max = Math . min ( evt . total - 1 , focusedIndex + delta ) ;
104- const newIndex = Math . max ( 0 , max ) ;
105- const newSelected = new Set ( [ newIndex ] ) ;
106- return {
107- anchorIndex : newIndex ,
108- focusedIndex : newIndex ,
109- lastShiftRange : { start : null , end : null } ,
110- selected : newSelected ,
111- } ;
112- }
113- case 'select-index' : {
114- const newSelected = new Set ( [ evt . index ] ) ;
115- return {
116- anchorIndex : evt . index ,
117- focusedIndex : evt . index ,
118- lastShiftRange : { start : null , end : null } ,
119- selected : newSelected ,
120- } ;
121- }
122- case 'toggle-index' : {
123- const newSelected = new Set ( prev . selected ) ;
124- if ( newSelected . has ( evt . index ) ) {
125- newSelected . delete ( evt . index ) ;
126- } else {
127- newSelected . add ( evt . index ) ;
128- }
129- return {
130- anchorIndex : evt . index ,
131- lastShiftRange : { start : null , end : null } ,
132- focusedIndex : evt . index ,
133- selected : newSelected ,
134- } ;
135- }
136- case 'expand-selected-to-index' : {
137- const { anchorIndex, lastShiftRange } = prev ;
138- const newSelected = new Set ( prev . selected ) ;
139-
140- // If there was a previous shift selection, remove it first
141- if ( lastShiftRange . start !== null && lastShiftRange . end !== null ) {
142- for ( let i = lastShiftRange . start ; i <= lastShiftRange . end ; i ++ ) {
143- newSelected . delete ( i ) ;
144- }
145- }
146-
147- // Calculate new range bounds from the anchor point
148- const start = Math . min ( anchorIndex ?? 0 , evt . index ) ;
149- const end = Math . max ( anchorIndex ?? 0 , evt . index ) ;
150-
151- // Add all items in new range to selection
152- for ( let i = start ; i <= end ; i ++ ) {
153- newSelected . add ( i ) ;
154- }
155-
156- return {
157- ...prev ,
158- lastShiftRange : { start, end } ,
159- focusedIndex : evt . index ,
160- selected : newSelected ,
161- } ;
162- }
163- case 'inc-or-dec-selected' : {
164- const { anchorIndex, lastShiftRange } = prev ;
165- // Handle shift+arrow selection
166- const newSelected = new Set ( prev . selected ) ;
167-
168- // Remove previous shift range
169- if ( lastShiftRange . start !== null && lastShiftRange . end !== null ) {
170- for ( let i = lastShiftRange . start ; i <= lastShiftRange . end ; i ++ ) {
171- newSelected . delete ( i ) ;
172- }
173- }
174-
175- // Calculate new range
176- const start = Math . min ( anchorIndex ?? evt . nextIndex , evt . nextIndex ) ;
177- const end = Math . max ( anchorIndex ?? evt . nextIndex , evt . nextIndex ) ;
178-
179- // Add new range
180- for ( let i = start ; i <= end ; i ++ ) {
181- newSelected . add ( i ) ;
182- }
183- return {
184- focusedIndex : evt . nextIndex ,
185- lastShiftRange : { start, end } ,
186- anchorIndex : anchorIndex === null ? evt . nextIndex : anchorIndex ,
187- selected : newSelected ,
188- } ;
189- }
190- case 'increment-selection' : {
191- const { focusedIndex, anchorIndex, lastShiftRange } = prev ;
192- invariant ( focusedIndex !== null ) ;
193- const delta = evt . direction === 'up' ? - 1 : 1 ;
194- const newIndex = Math . max ( 0 , Math . min ( evt . total - 1 , focusedIndex + delta ) ) ;
195-
196- // Handle shift+arrow selection
197- const newSelected = new Set ( prev . selected ) ;
198-
199- // Remove previous shift range
200- if ( lastShiftRange . start !== null && lastShiftRange . end !== null ) {
201- for ( let i = lastShiftRange . start ; i <= lastShiftRange . end ; i ++ ) {
202- newSelected . delete ( i ) ;
203- }
204- }
205-
206- // Calculate new range
207- const start = Math . min ( anchorIndex ?? newIndex , newIndex ) ;
208- const end = Math . max ( anchorIndex ?? newIndex , newIndex ) ;
209-
210- // Add new range
211- for ( let i = start ; i <= end ; i ++ ) {
212- newSelected . add ( i ) ;
213- }
214- return {
215- focusedIndex : newIndex ,
216- lastShiftRange : { start, end } ,
217- anchorIndex : anchorIndex === null ? newIndex : anchorIndex ,
218- selected : newSelected ,
219- } ;
220- }
221- default :
222- return prev ;
223- }
224- }
225-
22654/**
22755 * Handle onClick + keydown events to support most interactions with the list.
22856 * @param {import('preact/hooks').MutableRef<HTMLElement|null> } mainRef
0 commit comments