foldkit 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -55
- package/dist/fieldValidation/index.d.ts +39 -30
- package/dist/fieldValidation/index.d.ts.map +1 -1
- package/dist/fieldValidation/index.js +25 -30
- package/dist/fieldValidation/public.d.ts +2 -2
- package/dist/fieldValidation/public.d.ts.map +1 -1
- package/dist/fieldValidation/public.js +1 -1
- package/dist/html/index.d.ts +44 -9
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +15 -3
- package/dist/html/lazy.d.ts +12 -0
- package/dist/html/lazy.d.ts.map +1 -0
- package/dist/html/lazy.js +35 -0
- package/dist/html/public.d.ts +2 -1
- package/dist/html/public.d.ts.map +1 -1
- package/dist/html/public.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/managedResource/index.d.ts +38 -0
- package/dist/managedResource/index.d.ts.map +1 -0
- package/dist/managedResource/index.js +20 -0
- package/dist/managedResource/public.d.ts +5 -0
- package/dist/managedResource/public.d.ts.map +1 -0
- package/dist/managedResource/public.js +2 -0
- package/dist/runtime/managedResource.d.ts +114 -0
- package/dist/runtime/managedResource.d.ts.map +1 -0
- package/dist/runtime/managedResource.js +92 -0
- package/dist/runtime/public.d.ts +2 -2
- package/dist/runtime/public.d.ts.map +1 -1
- package/dist/runtime/public.js +1 -1
- package/dist/runtime/runtime.d.ts +79 -90
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +95 -19
- package/dist/runtime/subscription.d.ts +25 -0
- package/dist/runtime/subscription.d.ts.map +1 -0
- package/dist/runtime/subscription.js +7 -0
- package/dist/struct/index.d.ts +2 -0
- package/dist/struct/index.d.ts.map +1 -1
- package/dist/struct/index.js +4 -0
- package/dist/struct/public.d.ts +1 -1
- package/dist/struct/public.d.ts.map +1 -1
- package/dist/struct/public.js +1 -1
- package/dist/subscription/public.d.ts +3 -0
- package/dist/subscription/public.d.ts.map +1 -0
- package/dist/subscription/public.js +1 -0
- package/dist/ui/anchor.d.ts +19 -0
- package/dist/ui/anchor.d.ts.map +1 -0
- package/dist/ui/{menu/anchor.js → anchor.js} +3 -2
- package/dist/ui/disclosure/index.d.ts.map +1 -1
- package/dist/ui/disclosure/index.js +3 -2
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +2 -0
- package/dist/ui/listbox/multi.d.ts +172 -0
- package/dist/ui/listbox/multi.d.ts.map +1 -0
- package/dist/ui/listbox/multi.js +25 -0
- package/dist/ui/listbox/multiPublic.d.ts +3 -0
- package/dist/ui/listbox/multiPublic.d.ts.map +1 -0
- package/dist/ui/listbox/multiPublic.js +1 -0
- package/dist/ui/listbox/public.d.ts +7 -3
- package/dist/ui/listbox/public.d.ts.map +1 -1
- package/dist/ui/listbox/public.js +4 -1
- package/dist/ui/listbox/{index.d.ts → shared.d.ts} +78 -27
- package/dist/ui/listbox/shared.d.ts.map +1 -0
- package/dist/ui/listbox/{index.js → shared.js} +208 -199
- package/dist/ui/listbox/single.d.ts +172 -0
- package/dist/ui/listbox/single.d.ts.map +1 -0
- package/dist/ui/listbox/single.js +29 -0
- package/dist/ui/menu/index.d.ts +1 -4
- package/dist/ui/menu/index.d.ts.map +1 -1
- package/dist/ui/menu/index.js +2 -3
- package/dist/ui/menu/public.d.ts +3 -2
- package/dist/ui/menu/public.d.ts.map +1 -1
- package/dist/ui/menu/public.js +2 -1
- package/dist/ui/popover/index.d.ts +75 -0
- package/dist/ui/popover/index.d.ts.map +1 -0
- package/dist/ui/popover/index.js +237 -0
- package/dist/ui/popover/public.d.ts +5 -0
- package/dist/ui/popover/public.d.ts.map +1 -0
- package/dist/ui/popover/public.js +2 -0
- package/dist/ui/switch/index.d.ts +47 -0
- package/dist/ui/switch/index.d.ts.map +1 -0
- package/dist/ui/switch/index.js +66 -0
- package/dist/ui/switch/public.d.ts +3 -0
- package/dist/ui/switch/public.d.ts.map +1 -0
- package/dist/ui/switch/public.js +1 -0
- package/dist/ui/transition.d.ts +5 -0
- package/dist/ui/transition.d.ts.map +1 -0
- package/dist/ui/transition.js +3 -0
- package/package.json +17 -1
- package/dist/ui/listbox/index.d.ts.map +0 -1
- package/dist/ui/menu/anchor.d.ts +0 -18
- package/dist/ui/menu/anchor.d.ts.map +0 -1
|
@@ -2,37 +2,49 @@ import { Array, Effect, Match as M, Option, Predicate, Schema as S, String as St
|
|
|
2
2
|
import { OptionExt } from '../../effectExtensions';
|
|
3
3
|
import { html } from '../../html';
|
|
4
4
|
import { m } from '../../message';
|
|
5
|
-
import {
|
|
5
|
+
import { makeConstrainedEvo } from '../../struct';
|
|
6
6
|
import * as Task from '../../task';
|
|
7
|
+
import { anchorHooks } from '../anchor';
|
|
7
8
|
import { groupContiguous } from '../group';
|
|
8
9
|
import { findFirstEnabledIndex, isPrintableKey, keyToIndex } from '../keyboard';
|
|
9
|
-
import {
|
|
10
|
+
import { TransitionState } from '../transition';
|
|
10
11
|
import { resolveTypeaheadMatch } from '../typeahead';
|
|
11
12
|
export { resolveTypeaheadMatch };
|
|
12
13
|
// MODEL
|
|
13
14
|
/** Schema for the activation trigger — whether the user interacted via mouse or keyboard. */
|
|
14
15
|
export const ActivationTrigger = S.Literal('Pointer', 'Keyboard');
|
|
15
|
-
/** Schema for the transition animation state, tracking enter/leave phases for CSS transition coordination. */
|
|
16
|
-
export const TransitionState = S.Literal('Idle', 'EnterStart', 'EnterAnimating', 'LeaveStart', 'LeaveAnimating');
|
|
17
16
|
/** Schema for the listbox orientation — whether items flow vertically or horizontally. */
|
|
18
17
|
export const Orientation = S.Literal('Vertical', 'Horizontal');
|
|
19
|
-
/** Schema
|
|
20
|
-
export const
|
|
18
|
+
/** Schema fields shared by all listbox variants (single-select and multi-select). Spread into each variant's `S.Struct` to avoid duplicating field definitions. */
|
|
19
|
+
export const BaseModel = S.Struct({
|
|
21
20
|
id: S.String,
|
|
22
21
|
isOpen: S.Boolean,
|
|
23
22
|
isAnimated: S.Boolean,
|
|
24
23
|
isModal: S.Boolean,
|
|
25
|
-
isMultiple: S.Boolean,
|
|
26
24
|
orientation: Orientation,
|
|
27
25
|
transitionState: TransitionState,
|
|
28
26
|
maybeActiveItemIndex: S.OptionFromSelf(S.Number),
|
|
29
27
|
activationTrigger: ActivationTrigger,
|
|
30
28
|
searchQuery: S.String,
|
|
31
29
|
searchVersion: S.Number,
|
|
32
|
-
selectedItems: S.Array(S.String),
|
|
33
30
|
maybeLastPointerPosition: S.OptionFromSelf(S.Struct({ screenX: S.Number, screenY: S.Number })),
|
|
34
31
|
maybeLastButtonPointerType: S.OptionFromSelf(S.String),
|
|
35
32
|
});
|
|
33
|
+
/** Creates the shared base fields for a listbox model from a config. Each variant spreads this and adds its selection field. */
|
|
34
|
+
export const baseInit = (config) => ({
|
|
35
|
+
id: config.id,
|
|
36
|
+
isOpen: false,
|
|
37
|
+
isAnimated: config.isAnimated ?? false,
|
|
38
|
+
isModal: config.isModal ?? false,
|
|
39
|
+
orientation: config.orientation ?? 'Vertical',
|
|
40
|
+
transitionState: 'Idle',
|
|
41
|
+
maybeActiveItemIndex: Option.none(),
|
|
42
|
+
activationTrigger: 'Keyboard',
|
|
43
|
+
searchQuery: '',
|
|
44
|
+
searchVersion: 0,
|
|
45
|
+
maybeLastPointerPosition: Option.none(),
|
|
46
|
+
maybeLastButtonPointerType: Option.none(),
|
|
47
|
+
});
|
|
36
48
|
// MESSAGE
|
|
37
49
|
/** Sent when the listbox opens via button click or keyboard. Contains an optional initial active item index — None for pointer, Some for keyboard. */
|
|
38
50
|
export const Opened = m('Opened', {
|
|
@@ -83,30 +95,19 @@ export const PressedPointerOnButton = m('PressedPointerOnButton', {
|
|
|
83
95
|
});
|
|
84
96
|
/** Union of all messages the listbox component can produce. */
|
|
85
97
|
export const Message = S.Union(Opened, Closed, ClosedByTab, ActivatedItem, DeactivatedItem, SelectedItem, MovedPointerOverItem, RequestedItemClick, Searched, ClearedSearch, NoOp, AdvancedTransitionFrame, EndedTransition, DetectedButtonMovement, PressedPointerOnButton);
|
|
86
|
-
//
|
|
87
|
-
const SEARCH_DEBOUNCE_MILLISECONDS = 350;
|
|
88
|
-
const LEFT_MOUSE_BUTTON = 0;
|
|
89
|
-
|
|
90
|
-
export const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
transitionState: 'Idle',
|
|
98
|
-
maybeActiveItemIndex: Option.none(),
|
|
99
|
-
activationTrigger: 'Keyboard',
|
|
100
|
-
searchQuery: '',
|
|
101
|
-
searchVersion: 0,
|
|
102
|
-
selectedItems: config.selectedItems ?? [],
|
|
103
|
-
maybeLastPointerPosition: Option.none(),
|
|
104
|
-
maybeLastButtonPointerType: Option.none(),
|
|
105
|
-
});
|
|
106
|
-
// UPDATE
|
|
107
|
-
const closedModel = (model) => evo(model, {
|
|
98
|
+
// CONSTANTS
|
|
99
|
+
export const SEARCH_DEBOUNCE_MILLISECONDS = 350;
|
|
100
|
+
export const LEFT_MOUSE_BUTTON = 0;
|
|
101
|
+
// SELECTORS
|
|
102
|
+
export const buttonSelector = (id) => `#${id}-button`;
|
|
103
|
+
export const itemsSelector = (id) => `#${id}-items`;
|
|
104
|
+
export const itemSelector = (id, index) => `#${id}-item-${index}`;
|
|
105
|
+
export const itemId = (id, index) => `${id}-item-${index}`;
|
|
106
|
+
// HELPERS
|
|
107
|
+
const constrainedEvo = makeConstrainedEvo();
|
|
108
|
+
export const closedModel = (model) => constrainedEvo(model, {
|
|
108
109
|
isOpen: () => false,
|
|
109
|
-
transitionState: () =>
|
|
110
|
+
transitionState: () => model.isAnimated ? 'LeaveStart' : 'Idle',
|
|
110
111
|
maybeActiveItemIndex: () => Option.none(),
|
|
111
112
|
activationTrigger: () => 'Keyboard',
|
|
112
113
|
searchQuery: () => '',
|
|
@@ -114,182 +115,187 @@ const closedModel = (model) => evo(model, {
|
|
|
114
115
|
maybeLastPointerPosition: () => Option.none(),
|
|
115
116
|
maybeLastButtonPointerType: () => Option.none(),
|
|
116
117
|
});
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
maybeNextFrame,
|
|
155
|
-
maybeUnlockScroll,
|
|
156
|
-
maybeRestoreInert,
|
|
157
|
-
]), Array.prepend(focusButton)),
|
|
158
|
-
],
|
|
159
|
-
ClosedByTab: () => [
|
|
160
|
-
closedModel(model),
|
|
161
|
-
Array.getSomes([maybeNextFrame, maybeUnlockScroll, maybeRestoreInert]),
|
|
162
|
-
],
|
|
163
|
-
ActivatedItem: ({ index, activationTrigger }) => [
|
|
164
|
-
evo(model, {
|
|
165
|
-
maybeActiveItemIndex: () => Option.some(index),
|
|
166
|
-
activationTrigger: () => activationTrigger,
|
|
167
|
-
}),
|
|
168
|
-
activationTrigger === 'Keyboard'
|
|
169
|
-
? [
|
|
170
|
-
Task.scrollIntoView(itemSelector(model.id, index)).pipe(Effect.ignore, Effect.as(NoOp())),
|
|
171
|
-
]
|
|
172
|
-
: [],
|
|
173
|
-
],
|
|
174
|
-
MovedPointerOverItem: ({ index, screenX, screenY }) => {
|
|
175
|
-
const isSamePosition = Option.exists(model.maybeLastPointerPosition, position => position.screenX === screenX && position.screenY === screenY);
|
|
176
|
-
if (isSamePosition) {
|
|
177
|
-
return [model, []];
|
|
178
|
-
}
|
|
179
|
-
return [
|
|
180
|
-
evo(model, {
|
|
181
|
-
maybeActiveItemIndex: () => Option.some(index),
|
|
182
|
-
activationTrigger: () => 'Pointer',
|
|
183
|
-
maybeLastPointerPosition: () => Option.some({ screenX, screenY }),
|
|
184
|
-
}),
|
|
185
|
-
[],
|
|
186
|
-
];
|
|
187
|
-
},
|
|
188
|
-
DeactivatedItem: () => model.activationTrigger === 'Pointer'
|
|
189
|
-
? [evo(model, { maybeActiveItemIndex: () => Option.none() }), []]
|
|
190
|
-
: [model, []],
|
|
191
|
-
SelectedItem: ({ item }) => {
|
|
192
|
-
if (model.isMultiple) {
|
|
193
|
-
const nextSelectedItems = Array.contains(model.selectedItems, item)
|
|
194
|
-
? Array.filter(model.selectedItems, selected => selected !== item)
|
|
195
|
-
: Array.append(model.selectedItems, item);
|
|
196
|
-
return [evo(model, { selectedItems: () => nextSelectedItems }), []];
|
|
197
|
-
}
|
|
198
|
-
return [
|
|
199
|
-
evo(closedModel(model), {
|
|
200
|
-
selectedItems: () => [item],
|
|
201
|
-
}),
|
|
118
|
+
export const makeUpdate = (handleSelectedItem) => {
|
|
119
|
+
const withUpdateReturn = M.withReturnType();
|
|
120
|
+
return (model, message) => {
|
|
121
|
+
const maybeNextFrame = OptionExt.when(model.isAnimated, Task.nextFrame.pipe(Effect.as(AdvancedTransitionFrame())));
|
|
122
|
+
const maybeLockScroll = OptionExt.when(model.isModal, Task.lockScroll.pipe(Effect.as(NoOp())));
|
|
123
|
+
const maybeUnlockScroll = OptionExt.when(model.isModal, Task.unlockScroll.pipe(Effect.as(NoOp())));
|
|
124
|
+
const maybeInertOthers = OptionExt.when(model.isModal, Task.inertOthers(model.id, [
|
|
125
|
+
buttonSelector(model.id),
|
|
126
|
+
itemsSelector(model.id),
|
|
127
|
+
]).pipe(Effect.as(NoOp())));
|
|
128
|
+
const maybeRestoreInert = OptionExt.when(model.isModal, Task.restoreInert(model.id).pipe(Effect.as(NoOp())));
|
|
129
|
+
const focusButton = Task.focus(buttonSelector(model.id)).pipe(Effect.ignore, Effect.as(NoOp()));
|
|
130
|
+
return M.value(message).pipe(withUpdateReturn, M.tagsExhaustive({
|
|
131
|
+
Opened: ({ maybeActiveItemIndex }) => {
|
|
132
|
+
const nextModel = constrainedEvo(model, {
|
|
133
|
+
isOpen: () => true,
|
|
134
|
+
transitionState: () => model.isAnimated ? 'EnterStart' : 'Idle',
|
|
135
|
+
maybeActiveItemIndex: () => maybeActiveItemIndex,
|
|
136
|
+
activationTrigger: () => Option.match(maybeActiveItemIndex, {
|
|
137
|
+
onNone: () => 'Pointer',
|
|
138
|
+
onSome: () => 'Keyboard',
|
|
139
|
+
}),
|
|
140
|
+
searchQuery: () => '',
|
|
141
|
+
searchVersion: () => 0,
|
|
142
|
+
maybeLastPointerPosition: () => Option.none(),
|
|
143
|
+
});
|
|
144
|
+
return [
|
|
145
|
+
nextModel,
|
|
146
|
+
pipe(Array.getSomes([
|
|
147
|
+
maybeNextFrame,
|
|
148
|
+
maybeLockScroll,
|
|
149
|
+
maybeInertOthers,
|
|
150
|
+
]), Array.prepend(Task.focus(itemsSelector(model.id)).pipe(Effect.ignore, Effect.as(NoOp())))),
|
|
151
|
+
];
|
|
152
|
+
},
|
|
153
|
+
Closed: () => [
|
|
154
|
+
closedModel(model),
|
|
202
155
|
pipe(Array.getSomes([
|
|
203
156
|
maybeNextFrame,
|
|
204
157
|
maybeUnlockScroll,
|
|
205
158
|
maybeRestoreInert,
|
|
206
159
|
]), Array.prepend(focusButton)),
|
|
207
|
-
];
|
|
208
|
-
},
|
|
209
|
-
RequestedItemClick: ({ index }) => [
|
|
210
|
-
model,
|
|
211
|
-
[
|
|
212
|
-
Task.clickElement(itemSelector(model.id, index)).pipe(Effect.ignore, Effect.as(NoOp())),
|
|
213
160
|
],
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
161
|
+
ClosedByTab: () => [
|
|
162
|
+
closedModel(model),
|
|
163
|
+
Array.getSomes([
|
|
164
|
+
maybeNextFrame,
|
|
165
|
+
maybeUnlockScroll,
|
|
166
|
+
maybeRestoreInert,
|
|
167
|
+
]),
|
|
168
|
+
],
|
|
169
|
+
ActivatedItem: ({ index, activationTrigger }) => [
|
|
170
|
+
constrainedEvo(model, {
|
|
171
|
+
maybeActiveItemIndex: () => Option.some(index),
|
|
172
|
+
activationTrigger: () => activationTrigger,
|
|
223
173
|
}),
|
|
174
|
+
activationTrigger === 'Keyboard'
|
|
175
|
+
? [
|
|
176
|
+
Task.scrollIntoView(itemSelector(model.id, index)).pipe(Effect.ignore, Effect.as(NoOp())),
|
|
177
|
+
]
|
|
178
|
+
: [],
|
|
179
|
+
],
|
|
180
|
+
MovedPointerOverItem: ({ index, screenX, screenY }) => {
|
|
181
|
+
const isSamePosition = Option.exists(model.maybeLastPointerPosition, position => position.screenX === screenX && position.screenY === screenY);
|
|
182
|
+
if (isSamePosition) {
|
|
183
|
+
return [model, []];
|
|
184
|
+
}
|
|
185
|
+
return [
|
|
186
|
+
constrainedEvo(model, {
|
|
187
|
+
maybeActiveItemIndex: () => Option.some(index),
|
|
188
|
+
activationTrigger: () => 'Pointer',
|
|
189
|
+
maybeLastPointerPosition: () => Option.some({ screenX, screenY }),
|
|
190
|
+
}),
|
|
191
|
+
[],
|
|
192
|
+
];
|
|
193
|
+
},
|
|
194
|
+
DeactivatedItem: () => model.activationTrigger === 'Pointer'
|
|
195
|
+
? [
|
|
196
|
+
constrainedEvo(model, {
|
|
197
|
+
maybeActiveItemIndex: () => Option.none(),
|
|
198
|
+
}),
|
|
199
|
+
[],
|
|
200
|
+
]
|
|
201
|
+
: [model, []],
|
|
202
|
+
SelectedItem: ({ item }) => handleSelectedItem(model, item, {
|
|
203
|
+
focusButton,
|
|
204
|
+
maybeNextFrame,
|
|
205
|
+
maybeUnlockScroll,
|
|
206
|
+
maybeRestoreInert,
|
|
207
|
+
}),
|
|
208
|
+
RequestedItemClick: ({ index }) => [
|
|
209
|
+
model,
|
|
224
210
|
[
|
|
225
|
-
Task.
|
|
211
|
+
Task.clickElement(itemSelector(model.id, index)).pipe(Effect.ignore, Effect.as(NoOp())),
|
|
226
212
|
],
|
|
227
|
-
];
|
|
228
|
-
},
|
|
229
|
-
ClearedSearch: ({ version }) => {
|
|
230
|
-
if (version !== model.searchVersion) {
|
|
231
|
-
return [model, []];
|
|
232
|
-
}
|
|
233
|
-
return [evo(model, { searchQuery: () => '' }), []];
|
|
234
|
-
},
|
|
235
|
-
AdvancedTransitionFrame: () => M.value(model.transitionState).pipe(withUpdateReturn, M.when('EnterStart', () => [
|
|
236
|
-
evo(model, { transitionState: () => 'EnterAnimating' }),
|
|
237
|
-
[
|
|
238
|
-
Task.waitForTransitions(itemsSelector(model.id)).pipe(Effect.as(EndedTransition())),
|
|
239
|
-
],
|
|
240
|
-
]), M.when('LeaveStart', () => [
|
|
241
|
-
evo(model, { transitionState: () => 'LeaveAnimating' }),
|
|
242
|
-
[
|
|
243
|
-
Effect.raceFirst(Task.detectElementMovement(buttonSelector(model.id)).pipe(Effect.as(DetectedButtonMovement())), Task.waitForTransitions(itemsSelector(model.id)).pipe(Effect.as(EndedTransition()))),
|
|
244
213
|
],
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
214
|
+
Searched: ({ key, maybeTargetIndex }) => {
|
|
215
|
+
const nextSearchQuery = model.searchQuery + key;
|
|
216
|
+
const nextSearchVersion = model.searchVersion + 1;
|
|
217
|
+
return [
|
|
218
|
+
constrainedEvo(model, {
|
|
219
|
+
searchQuery: () => nextSearchQuery,
|
|
220
|
+
searchVersion: () => nextSearchVersion,
|
|
221
|
+
maybeActiveItemIndex: () => Option.orElse(maybeTargetIndex, () => model.maybeActiveItemIndex),
|
|
222
|
+
}),
|
|
223
|
+
[
|
|
224
|
+
Task.delay(SEARCH_DEBOUNCE_MILLISECONDS).pipe(Effect.as(ClearedSearch({ version: nextSearchVersion }))),
|
|
225
|
+
],
|
|
226
|
+
];
|
|
227
|
+
},
|
|
228
|
+
ClearedSearch: ({ version }) => {
|
|
229
|
+
if (version !== model.searchVersion) {
|
|
230
|
+
return [model, []];
|
|
231
|
+
}
|
|
232
|
+
return [constrainedEvo(model, { searchQuery: () => '' }), []];
|
|
233
|
+
},
|
|
234
|
+
AdvancedTransitionFrame: () => M.value(model.transitionState).pipe(withUpdateReturn, M.when('EnterStart', () => [
|
|
235
|
+
constrainedEvo(model, {
|
|
236
|
+
transitionState: () => 'EnterAnimating',
|
|
237
|
+
}),
|
|
238
|
+
[
|
|
239
|
+
Task.waitForTransitions(itemsSelector(model.id)).pipe(Effect.as(EndedTransition())),
|
|
240
|
+
],
|
|
241
|
+
]), M.when('LeaveStart', () => [
|
|
242
|
+
constrainedEvo(model, {
|
|
243
|
+
transitionState: () => 'LeaveAnimating',
|
|
244
|
+
}),
|
|
245
|
+
[
|
|
246
|
+
Effect.raceFirst(Task.detectElementMovement(buttonSelector(model.id)).pipe(Effect.as(DetectedButtonMovement())), Task.waitForTransitions(itemsSelector(model.id)).pipe(Effect.as(EndedTransition()))),
|
|
247
|
+
],
|
|
248
|
+
]), M.orElse(() => [model, []])),
|
|
249
|
+
EndedTransition: () => M.value(model.transitionState).pipe(withUpdateReturn, M.whenOr('EnterAnimating', 'LeaveAnimating', () => [
|
|
250
|
+
constrainedEvo(model, { transitionState: () => 'Idle' }),
|
|
251
|
+
[],
|
|
252
|
+
]), M.orElse(() => [model, []])),
|
|
253
|
+
DetectedButtonMovement: () => M.value(model.transitionState).pipe(withUpdateReturn, M.when('LeaveAnimating', () => [
|
|
254
|
+
constrainedEvo(model, { transitionState: () => 'Idle' }),
|
|
255
|
+
[],
|
|
256
|
+
]), M.orElse(() => [model, []])),
|
|
257
|
+
PressedPointerOnButton: ({ pointerType, button }) => {
|
|
258
|
+
const withPointerType = constrainedEvo(model, {
|
|
259
|
+
maybeLastButtonPointerType: () => Option.some(pointerType),
|
|
260
|
+
});
|
|
261
|
+
if (pointerType !== 'mouse' || button !== LEFT_MOUSE_BUTTON) {
|
|
262
|
+
return [withPointerType, []];
|
|
263
|
+
}
|
|
264
|
+
if (model.isOpen) {
|
|
265
|
+
return [
|
|
266
|
+
closedModel(withPointerType),
|
|
267
|
+
pipe(Array.getSomes([
|
|
268
|
+
maybeNextFrame,
|
|
269
|
+
maybeUnlockScroll,
|
|
270
|
+
maybeRestoreInert,
|
|
271
|
+
]), Array.prepend(focusButton)),
|
|
272
|
+
];
|
|
273
|
+
}
|
|
274
|
+
const nextModel = constrainedEvo(withPointerType, {
|
|
275
|
+
isOpen: () => true,
|
|
276
|
+
transitionState: () => model.isAnimated ? 'EnterStart' : 'Idle',
|
|
277
|
+
maybeActiveItemIndex: () => Option.none(),
|
|
278
|
+
activationTrigger: () => 'Pointer',
|
|
279
|
+
searchQuery: () => '',
|
|
280
|
+
searchVersion: () => 0,
|
|
281
|
+
maybeLastPointerPosition: () => Option.none(),
|
|
282
|
+
});
|
|
262
283
|
return [
|
|
263
|
-
|
|
284
|
+
nextModel,
|
|
264
285
|
pipe(Array.getSomes([
|
|
265
286
|
maybeNextFrame,
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
]), Array.prepend(
|
|
287
|
+
maybeLockScroll,
|
|
288
|
+
maybeInertOthers,
|
|
289
|
+
]), Array.prepend(Task.focus(itemsSelector(model.id)).pipe(Effect.ignore, Effect.as(NoOp())))),
|
|
269
290
|
];
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
maybeActiveItemIndex: () => Option.none(),
|
|
275
|
-
activationTrigger: () => 'Pointer',
|
|
276
|
-
searchQuery: () => '',
|
|
277
|
-
searchVersion: () => 0,
|
|
278
|
-
maybeLastPointerPosition: () => Option.none(),
|
|
279
|
-
});
|
|
280
|
-
return [
|
|
281
|
-
nextModel,
|
|
282
|
-
pipe(Array.getSomes([maybeNextFrame, maybeLockScroll, maybeInertOthers]), Array.prepend(Task.focus(itemsSelector(model.id)).pipe(Effect.ignore, Effect.as(NoOp())))),
|
|
283
|
-
];
|
|
284
|
-
},
|
|
285
|
-
NoOp: () => [model, []],
|
|
286
|
-
}));
|
|
291
|
+
},
|
|
292
|
+
NoOp: () => [model, []],
|
|
293
|
+
}));
|
|
294
|
+
};
|
|
287
295
|
};
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const { div, input, AriaActiveDescendant, AriaControls, AriaDisabled, AriaExpanded, AriaHasPopup, AriaLabelledBy, AriaOrientation, AriaSelected, Attribute, Class, DataAttribute, Id, Name, OnBlur, OnClick, OnDestroy, OnInsert, OnKeyDownPreventDefault, OnKeyUpPreventDefault, OnPointerDown, OnPointerLeave, OnPointerMove, Role, Style, Tabindex, Type, Value, keyed, } = html();
|
|
292
|
-
const { model: { id, isOpen, isMultiple, orientation, transitionState, maybeActiveItemIndex, selectedItems, searchQuery, maybeLastButtonPointerType, }, toMessage, items, itemToConfig, isItemDisabled, isButtonDisabled, buttonContent, buttonClassName, itemsClassName, backdropClassName, className, itemGroupKey, groupToHeading, groupClassName, separatorClassName, anchor, name, form, isDisabled, isInvalid, } = config;
|
|
296
|
+
export const makeView = (behavior) => (config) => {
|
|
297
|
+
const { div, input, AriaActiveDescendant, AriaControls, AriaDisabled, AriaExpanded, AriaHasPopup, AriaLabelledBy, AriaMultiSelectable, AriaOrientation, AriaSelected, Attribute, Class, DataAttribute, Id, Name, OnBlur, OnClick, OnDestroy, OnInsert, OnKeyDownPreventDefault, OnKeyUpPreventDefault, OnPointerDown, OnPointerLeave, OnPointerMove, Role, Style, Tabindex, Type, Value, keyed, } = html();
|
|
298
|
+
const { model: { id, isOpen, orientation, transitionState, maybeActiveItemIndex, searchQuery, maybeLastButtonPointerType, }, toMessage, items, itemToConfig, isItemDisabled, isButtonDisabled, buttonContent, buttonClassName, itemsClassName, backdropClassName, className, itemGroupKey, groupToHeading, groupClassName, separatorClassName, anchor, name, form, isDisabled, isInvalid, } = config;
|
|
293
299
|
const itemToValue = config.itemToValue ?? (item => String(item));
|
|
294
300
|
const itemToSearchText = config.itemToSearchText ?? (item => itemToValue(item));
|
|
295
301
|
const isLeaving = transitionState === 'LeaveStart' || transitionState === 'LeaveAnimating';
|
|
@@ -325,7 +331,7 @@ export const view = (config) => {
|
|
|
325
331
|
const isNavigationKey = (key) => Array.contains(navigationKeys, key);
|
|
326
332
|
const firstEnabledIndex = findFirstEnabledIndex(items.length, 0, isItemDisabledByIndex)(0, 1);
|
|
327
333
|
const lastEnabledIndex = findFirstEnabledIndex(items.length, 0, isItemDisabledByIndex)(items.length - 1, -1);
|
|
328
|
-
const selectedItemIndex =
|
|
334
|
+
const selectedItemIndex = behavior.selectedItemIndex(config.model, items, itemToValue);
|
|
329
335
|
const handleButtonKeyDown = (key) => M.value(key).pipe(M.whenOr('Enter', ' ', 'ArrowDown', () => Option.some(toMessage(Opened({
|
|
330
336
|
maybeActiveItemIndex: Option.orElse(selectedItemIndex, () => Option.some(firstEnabledIndex)),
|
|
331
337
|
})))), M.when('ArrowUp', () => Option.some(toMessage(Opened({
|
|
@@ -399,7 +405,7 @@ export const view = (config) => {
|
|
|
399
405
|
Id(`${id}-items`),
|
|
400
406
|
Role('listbox'),
|
|
401
407
|
AriaOrientation(Str.toLowerCase(orientation)),
|
|
402
|
-
...(
|
|
408
|
+
...(behavior.ariaMultiSelectable ? [AriaMultiSelectable(true)] : []),
|
|
403
409
|
AriaLabelledBy(`${id}-button`),
|
|
404
410
|
...maybeActiveDescendant,
|
|
405
411
|
Tabindex(0),
|
|
@@ -417,7 +423,7 @@ export const view = (config) => {
|
|
|
417
423
|
const listboxItems = Array.map(items, (item, index) => {
|
|
418
424
|
const isActiveItem = Option.exists(maybeActiveItemIndex, activeIndex => activeIndex === index);
|
|
419
425
|
const isDisabledItem = isItemDisabledByIndex(index);
|
|
420
|
-
const isSelectedItem =
|
|
426
|
+
const isSelectedItem = behavior.isItemSelected(config.model, itemToValue(item));
|
|
421
427
|
const itemConfig = itemToConfig(item, {
|
|
422
428
|
isActive: isActiveItem,
|
|
423
429
|
isDisabled: isDisabledItem,
|
|
@@ -484,13 +490,16 @@ export const view = (config) => {
|
|
|
484
490
|
keyed('div')(`${id}-items-container`, itemsContainerAttributes, renderedItems),
|
|
485
491
|
];
|
|
486
492
|
const formAttribute = form ? [Attribute('form', form)] : [];
|
|
493
|
+
const selectedValues = pipe(items, Array.filter(item => behavior.isItemSelected(config.model, itemToValue(item))), Array.map(itemToValue));
|
|
487
494
|
const hiddenInputs = name
|
|
488
|
-
? Array.match(
|
|
489
|
-
onEmpty: () => [
|
|
490
|
-
|
|
495
|
+
? Array.match(selectedValues, {
|
|
496
|
+
onEmpty: () => [
|
|
497
|
+
input([Type('hidden'), Name(name), ...formAttribute]),
|
|
498
|
+
],
|
|
499
|
+
onNonEmpty: Array.map(selectedValue => input([
|
|
491
500
|
Type('hidden'),
|
|
492
501
|
Name(name),
|
|
493
|
-
Value(
|
|
502
|
+
Value(selectedValue),
|
|
494
503
|
...formAttribute,
|
|
495
504
|
])),
|
|
496
505
|
})
|