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