foldkit 0.101.0 → 0.102.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 +2 -1
- package/dist/canvas/view.d.ts +1 -1
- package/dist/canvas/view.d.ts.map +1 -1
- package/dist/canvas/view.js +5 -5
- package/dist/command/index.d.ts +71 -0
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js +34 -1
- package/dist/command/public.d.ts +1 -1
- package/dist/command/public.d.ts.map +1 -1
- package/dist/command/public.js +1 -1
- package/dist/devTools/overlay.d.ts.map +1 -1
- package/dist/devTools/overlay.js +137 -110
- package/dist/dom/dom.d.ts +8 -11
- package/dist/dom/dom.d.ts.map +1 -1
- package/dist/dom/dom.js +8 -11
- package/dist/dom/elementMovement.d.ts +1 -3
- package/dist/dom/elementMovement.d.ts.map +1 -1
- package/dist/dom/elementMovement.js +1 -3
- package/dist/dom/inert.d.ts +2 -4
- package/dist/dom/inert.d.ts.map +1 -1
- package/dist/dom/inert.js +2 -4
- package/dist/dom/scrollLock.d.ts +2 -2
- package/dist/dom/scrollLock.js +2 -2
- package/dist/dom/waitForAnimation.d.ts +1 -1
- package/dist/dom/waitForAnimation.js +1 -1
- package/dist/html/boundary.d.ts +98 -0
- package/dist/html/boundary.d.ts.map +1 -0
- package/dist/html/boundary.js +176 -0
- package/dist/html/childAttribute.d.ts +44 -0
- package/dist/html/childAttribute.d.ts.map +1 -0
- package/dist/html/childAttribute.js +34 -0
- package/dist/html/index.d.ts +70 -23
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +639 -575
- package/dist/html/lazy.d.ts +12 -7
- package/dist/html/lazy.d.ts.map +1 -1
- package/dist/html/lazy.js +30 -11
- package/dist/html/public.d.ts +2 -2
- package/dist/html/public.d.ts.map +1 -1
- package/dist/html/public.js +1 -1
- package/dist/html/runtimeSingleton.d.ts +72 -0
- package/dist/html/runtimeSingleton.d.ts.map +1 -0
- package/dist/html/runtimeSingleton.js +112 -0
- package/dist/html/submodel.d.ts +98 -0
- package/dist/html/submodel.d.ts.map +1 -0
- package/dist/html/submodel.js +190 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/render/render.d.ts +1 -1
- package/dist/render/render.js +1 -1
- package/dist/runtime/messagePriority.d.ts +5 -1
- package/dist/runtime/messagePriority.d.ts.map +1 -1
- package/dist/runtime/messagePriority.js +25 -4
- package/dist/runtime/runtime.d.ts +1 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +115 -61
- package/dist/submodel/public.d.ts +4 -0
- package/dist/submodel/public.d.ts.map +1 -0
- package/dist/submodel/public.js +1 -0
- package/dist/submodel/submodel.d.ts +32 -0
- package/dist/submodel/submodel.d.ts.map +1 -0
- package/dist/submodel/submodel.js +1 -0
- package/dist/test/apps/disabledButton.d.ts +4 -5
- package/dist/test/apps/disabledButton.d.ts.map +1 -1
- package/dist/test/apps/disabledButton.js +16 -16
- package/dist/test/scene.d.ts +8 -8
- package/dist/test/scene.d.ts.map +1 -1
- package/dist/test/scene.js +25 -13
- package/dist/test/story.d.ts +15 -8
- package/dist/test/story.d.ts.map +1 -1
- package/dist/test/story.js +21 -9
- package/dist/ui/animation/index.d.ts +30 -14
- package/dist/ui/animation/index.d.ts.map +1 -1
- package/dist/ui/animation/index.js +9 -19
- package/dist/ui/animation/public.d.ts +2 -2
- package/dist/ui/animation/public.d.ts.map +1 -1
- package/dist/ui/animation/public.js +1 -1
- package/dist/ui/calendar/index.d.ts +199 -84
- package/dist/ui/calendar/index.d.ts.map +1 -1
- package/dist/ui/calendar/index.js +129 -140
- package/dist/ui/calendar/public.d.ts +2 -2
- package/dist/ui/calendar/public.d.ts.map +1 -1
- package/dist/ui/calendar/public.js +1 -1
- package/dist/ui/checkbox/index.d.ts +93 -21
- package/dist/ui/checkbox/index.d.ts.map +1 -1
- package/dist/ui/checkbox/index.js +62 -33
- package/dist/ui/checkbox/public.d.ts +2 -2
- package/dist/ui/checkbox/public.d.ts.map +1 -1
- package/dist/ui/checkbox/public.js +1 -1
- package/dist/ui/combobox/multi.d.ts +35 -91
- package/dist/ui/combobox/multi.d.ts.map +1 -1
- package/dist/ui/combobox/multi.js +34 -17
- package/dist/ui/combobox/multiPublic.d.ts +2 -2
- package/dist/ui/combobox/multiPublic.d.ts.map +1 -1
- package/dist/ui/combobox/multiPublic.js +1 -1
- package/dist/ui/combobox/public.d.ts +3 -3
- package/dist/ui/combobox/public.d.ts.map +1 -1
- package/dist/ui/combobox/public.js +2 -2
- package/dist/ui/combobox/shared.d.ts +56 -31
- package/dist/ui/combobox/shared.d.ts.map +1 -1
- package/dist/ui/combobox/shared.js +333 -322
- package/dist/ui/combobox/single.d.ts +46 -93
- package/dist/ui/combobox/single.d.ts.map +1 -1
- package/dist/ui/combobox/single.js +44 -17
- package/dist/ui/datePicker/index.d.ts +256 -48
- package/dist/ui/datePicker/index.d.ts.map +1 -1
- package/dist/ui/datePicker/index.js +149 -104
- package/dist/ui/datePicker/public.d.ts +2 -2
- package/dist/ui/datePicker/public.d.ts.map +1 -1
- package/dist/ui/datePicker/public.js +1 -1
- package/dist/ui/dialog/index.d.ts +95 -39
- package/dist/ui/dialog/index.d.ts.map +1 -1
- package/dist/ui/dialog/index.js +71 -62
- package/dist/ui/dialog/public.d.ts +2 -2
- package/dist/ui/dialog/public.d.ts.map +1 -1
- package/dist/ui/dialog/public.js +1 -1
- package/dist/ui/disclosure/index.d.ts +71 -31
- package/dist/ui/disclosure/index.d.ts.map +1 -1
- package/dist/ui/disclosure/index.js +57 -62
- package/dist/ui/disclosure/public.d.ts +2 -2
- package/dist/ui/disclosure/public.d.ts.map +1 -1
- package/dist/ui/disclosure/public.js +1 -1
- package/dist/ui/dragAndDrop/index.d.ts +6 -6
- package/dist/ui/dragAndDrop/index.d.ts.map +1 -1
- package/dist/ui/dragAndDrop/index.js +7 -7
- package/dist/ui/dragAndDrop/public.d.ts +1 -1
- package/dist/ui/dragAndDrop/public.d.ts.map +1 -1
- package/dist/ui/dragAndDrop/public.js +1 -1
- package/dist/ui/fileDrop/index.d.ts +42 -46
- package/dist/ui/fileDrop/index.d.ts.map +1 -1
- package/dist/ui/fileDrop/index.js +30 -46
- package/dist/ui/fileDrop/public.d.ts +2 -2
- package/dist/ui/fileDrop/public.d.ts.map +1 -1
- package/dist/ui/fileDrop/public.js +1 -1
- package/dist/ui/listbox/multi.d.ts +39 -84
- package/dist/ui/listbox/multi.d.ts.map +1 -1
- package/dist/ui/listbox/multi.js +38 -20
- package/dist/ui/listbox/multiPublic.d.ts +2 -2
- package/dist/ui/listbox/multiPublic.d.ts.map +1 -1
- package/dist/ui/listbox/multiPublic.js +1 -1
- package/dist/ui/listbox/public.d.ts +3 -3
- package/dist/ui/listbox/public.d.ts.map +1 -1
- package/dist/ui/listbox/public.js +2 -2
- package/dist/ui/listbox/shared.d.ts +71 -30
- package/dist/ui/listbox/shared.d.ts.map +1 -1
- package/dist/ui/listbox/shared.js +319 -296
- package/dist/ui/listbox/single.d.ts +57 -85
- package/dist/ui/listbox/single.d.ts.map +1 -1
- package/dist/ui/listbox/single.js +48 -24
- package/dist/ui/menu/index.d.ts +80 -36
- package/dist/ui/menu/index.d.ts.map +1 -1
- package/dist/ui/menu/index.js +117 -86
- package/dist/ui/menu/public.d.ts +2 -2
- package/dist/ui/menu/public.d.ts.map +1 -1
- package/dist/ui/menu/public.js +1 -1
- package/dist/ui/popover/index.d.ts +117 -44
- package/dist/ui/popover/index.d.ts.map +1 -1
- package/dist/ui/popover/index.js +88 -101
- package/dist/ui/popover/public.d.ts +2 -2
- package/dist/ui/popover/public.d.ts.map +1 -1
- package/dist/ui/popover/public.js +1 -1
- package/dist/ui/radioGroup/index.d.ts +122 -45
- package/dist/ui/radioGroup/index.d.ts.map +1 -1
- package/dist/ui/radioGroup/index.js +111 -72
- package/dist/ui/radioGroup/public.d.ts +2 -2
- package/dist/ui/radioGroup/public.d.ts.map +1 -1
- package/dist/ui/radioGroup/public.js +1 -1
- package/dist/ui/slider/index.d.ts +72 -34
- package/dist/ui/slider/index.d.ts.map +1 -1
- package/dist/ui/slider/index.js +40 -49
- package/dist/ui/slider/public.d.ts +2 -2
- package/dist/ui/slider/public.d.ts.map +1 -1
- package/dist/ui/slider/public.js +1 -1
- package/dist/ui/switch/index.d.ts +74 -21
- package/dist/ui/switch/index.d.ts.map +1 -1
- package/dist/ui/switch/index.js +62 -33
- package/dist/ui/switch/public.d.ts +2 -2
- package/dist/ui/switch/public.d.ts.map +1 -1
- package/dist/ui/switch/public.js +1 -1
- package/dist/ui/tabs/index.d.ts +107 -45
- package/dist/ui/tabs/index.d.ts.map +1 -1
- package/dist/ui/tabs/index.js +99 -81
- package/dist/ui/tabs/public.d.ts +2 -2
- package/dist/ui/tabs/public.d.ts.map +1 -1
- package/dist/ui/tabs/public.js +1 -1
- package/dist/ui/toast/index.d.ts +93 -109
- package/dist/ui/toast/index.d.ts.map +1 -1
- package/dist/ui/toast/index.js +16 -29
- package/dist/ui/toast/schema.d.ts +15 -4
- package/dist/ui/toast/schema.d.ts.map +1 -1
- package/dist/ui/toast/schema.js +11 -4
- package/dist/ui/toast/update.d.ts +36 -18
- package/dist/ui/toast/update.d.ts.map +1 -1
- package/dist/ui/toast/update.js +33 -14
- package/dist/ui/tooltip/index.d.ts +94 -42
- package/dist/ui/tooltip/index.d.ts.map +1 -1
- package/dist/ui/tooltip/index.js +64 -73
- package/dist/ui/tooltip/public.d.ts +2 -2
- package/dist/ui/tooltip/public.d.ts.map +1 -1
- package/dist/ui/tooltip/public.js +1 -1
- package/dist/ui/virtualList/index.d.ts +18 -41
- package/dist/ui/virtualList/index.d.ts.map +1 -1
- package/dist/ui/virtualList/index.js +17 -37
- package/dist/ui/virtualList/public.d.ts +2 -2
- package/dist/ui/virtualList/public.d.ts.map +1 -1
- package/dist/ui/virtualList/public.js +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import { Array, Effect, Equal, Match as M, Option, Predicate, Schema as S, Strin
|
|
|
2
2
|
import * as Command from '../../command/index.js';
|
|
3
3
|
import * as Dom from '../../dom/index.js';
|
|
4
4
|
import { OptionExt } from '../../effectExtensions/index.js';
|
|
5
|
-
import { html } from '../../html/index.js';
|
|
5
|
+
import { defineView, html, } from '../../html/index.js';
|
|
6
6
|
import { m } from '../../message/index.js';
|
|
7
7
|
import * as Mount from '../../mount/index.js';
|
|
8
8
|
import { makeConstrainedEvo } from '../../struct/index.js';
|
|
@@ -17,9 +17,9 @@ import { findFirstEnabledIndex, isPrintableKey, keyToIndex, } from '../keyboard.
|
|
|
17
17
|
import { resolveTypeaheadMatch } from '../typeahead.js';
|
|
18
18
|
export { resolveTypeaheadMatch };
|
|
19
19
|
// MODEL
|
|
20
|
-
/** Schema for the activation trigger
|
|
20
|
+
/** Schema for the activation trigger: whether the user interacted via mouse or keyboard. */
|
|
21
21
|
export const ActivationTrigger = S.Literals(['Pointer', 'Keyboard']);
|
|
22
|
-
/** Schema for the listbox orientation
|
|
22
|
+
/** Schema for the listbox orientation: whether items flow vertically or horizontally. */
|
|
23
23
|
export const Orientation = S.Literals(['Vertical', 'Horizontal']);
|
|
24
24
|
/** Schema fields shared by all listbox variants (single-select and multi-select). Spread into each variant's `S.Struct` to avoid duplicating field definitions. */
|
|
25
25
|
export const BaseModel = S.Struct({
|
|
@@ -52,7 +52,7 @@ export const baseInit = (config) => ({
|
|
|
52
52
|
maybeLastButtonPointerType: Option.none(),
|
|
53
53
|
});
|
|
54
54
|
// MESSAGE
|
|
55
|
-
/** Sent when the listbox opens via button click or keyboard. Contains an optional initial active item index
|
|
55
|
+
/** Sent when the listbox opens via button click or keyboard. Contains an optional initial active item index: None for pointer, Some for keyboard. */
|
|
56
56
|
export const Opened = m('Opened', {
|
|
57
57
|
maybeActiveItemIndex: S.Option(S.Number),
|
|
58
58
|
});
|
|
@@ -91,9 +91,9 @@ export const CompletedLockScroll = m('CompletedLockScroll');
|
|
|
91
91
|
/** Sent when the scroll unlock command completes. */
|
|
92
92
|
export const CompletedUnlockScroll = m('CompletedUnlockScroll');
|
|
93
93
|
/** Sent when the inert-others command completes. */
|
|
94
|
-
export const
|
|
94
|
+
export const CompletedInertOthers = m('CompletedInertOthers');
|
|
95
95
|
/** Sent when the restore-inert command completes. */
|
|
96
|
-
export const
|
|
96
|
+
export const CompletedRestoreInert = m('CompletedRestoreInert');
|
|
97
97
|
/** Sent when the focus-button command completes after closing. */
|
|
98
98
|
export const CompletedFocusButton = m('CompletedFocusButton');
|
|
99
99
|
/** Sent when the focus-items command completes after opening. */
|
|
@@ -133,8 +133,8 @@ export const Message = S.Union([
|
|
|
133
133
|
ClearedSearch,
|
|
134
134
|
CompletedLockScroll,
|
|
135
135
|
CompletedUnlockScroll,
|
|
136
|
-
|
|
137
|
-
|
|
136
|
+
CompletedInertOthers,
|
|
137
|
+
CompletedRestoreInert,
|
|
138
138
|
CompletedFocusButton,
|
|
139
139
|
CompletedFocusItems,
|
|
140
140
|
CompletedScrollIntoView,
|
|
@@ -146,6 +146,14 @@ export const Message = S.Union([
|
|
|
146
146
|
GotAnimationMessage,
|
|
147
147
|
PressedPointerOnButton,
|
|
148
148
|
]);
|
|
149
|
+
// OUT MESSAGE
|
|
150
|
+
/** Sent when a single-select listbox commits a selection, or when a multi-select listbox toggles an item. Generic over `Value extends string`: the runtime schema stores `value: string`, but the type-level OutMessage exposes `value: Value` so consumers who supply `items: ReadonlyArray<MyUnion>` receive `value: MyUnion` from `update<MyUnion>` without casting. The cast is fenced inside this module's `update` return, sound because the value was extracted from the items array the consumer supplied. */
|
|
151
|
+
export const Selected = m('Selected', {
|
|
152
|
+
value: S.String,
|
|
153
|
+
wasAdded: S.Boolean,
|
|
154
|
+
});
|
|
155
|
+
/** Union of out-messages the listbox component can produce. Single-select listboxes always emit `wasAdded: true`. Multi-select listboxes emit `wasAdded: true` when adding to the selection and `wasAdded: false` when toggling off. */
|
|
156
|
+
export const OutMessage = S.Union([Selected]);
|
|
149
157
|
// CONSTANTS
|
|
150
158
|
export const SEARCH_DEBOUNCE_MILLISECONDS = 350;
|
|
151
159
|
export const LEFT_MOUSE_BUTTON = 0;
|
|
@@ -169,9 +177,9 @@ export const LockScroll = Command.define('LockScroll', CompletedLockScroll)(Dom.
|
|
|
169
177
|
/** Re-enables page scrolling after the listbox closes. */
|
|
170
178
|
export const UnlockScroll = Command.define('UnlockScroll', CompletedUnlockScroll)(Dom.unlockScroll.pipe(Effect.as(CompletedUnlockScroll())));
|
|
171
179
|
/** Marks all elements outside the listbox as inert for modal behavior. */
|
|
172
|
-
export const InertOthers = Command.define('InertOthers', { id: S.String },
|
|
180
|
+
export const InertOthers = Command.define('InertOthers', { id: S.String }, CompletedInertOthers)(({ id }) => Dom.inertOthers(id, [buttonSelector(id), itemsSelector(id)]).pipe(Effect.as(CompletedInertOthers())));
|
|
173
181
|
/** Removes the inert attribute from elements outside the listbox. */
|
|
174
|
-
export const RestoreInert = Command.define('RestoreInert', { id: S.String },
|
|
182
|
+
export const RestoreInert = Command.define('RestoreInert', { id: S.String }, CompletedRestoreInert)(({ id }) => Dom.restoreInert(id).pipe(Effect.as(CompletedRestoreInert())));
|
|
175
183
|
/** Moves focus back to the listbox button after closing. */
|
|
176
184
|
export const FocusButton = Command.define('FocusButton', { id: S.String }, CompletedFocusButton)(({ id }) => Dom.focus(buttonSelector(id)).pipe(Effect.ignore, Effect.as(CompletedFocusButton())));
|
|
177
185
|
/** Moves focus to the listbox items container after opening. */
|
|
@@ -188,7 +196,7 @@ export const makeUpdate = (handleSelectedItem) => {
|
|
|
188
196
|
const withUpdateReturn = M.withReturnType();
|
|
189
197
|
const delegateToAnimation = (model, animationMessage) => {
|
|
190
198
|
const [nextAnimation, animationCommands, maybeOutMessage] = animationUpdate(model.animation, animationMessage);
|
|
191
|
-
const mappedCommands =
|
|
199
|
+
const mappedCommands = Command.mapMessages(animationCommands, message => GotAnimationMessage({ message }));
|
|
192
200
|
const additionalCommands = Option.match(maybeOutMessage, {
|
|
193
201
|
onNone: () => [],
|
|
194
202
|
onSome: M.type().pipe(M.tagsExhaustive({
|
|
@@ -201,6 +209,7 @@ export const makeUpdate = (handleSelectedItem) => {
|
|
|
201
209
|
return [
|
|
202
210
|
constrainedEvo(model, { animation: () => nextAnimation }),
|
|
203
211
|
[...mappedCommands, ...additionalCommands],
|
|
212
|
+
Option.none(),
|
|
204
213
|
];
|
|
205
214
|
};
|
|
206
215
|
const openListbox = (baseModel, openCommands) => {
|
|
@@ -209,19 +218,24 @@ export const makeUpdate = (handleSelectedItem) => {
|
|
|
209
218
|
return [
|
|
210
219
|
constrainedEvo(nextModel, { isOpen: () => true }),
|
|
211
220
|
[...openCommands, ...animationCommands],
|
|
221
|
+
Option.none(),
|
|
212
222
|
];
|
|
213
223
|
}
|
|
214
|
-
return [
|
|
224
|
+
return [
|
|
225
|
+
constrainedEvo(baseModel, { isOpen: () => true }),
|
|
226
|
+
openCommands,
|
|
227
|
+
Option.none(),
|
|
228
|
+
];
|
|
215
229
|
};
|
|
216
|
-
const closeListbox = (baseModel, commands) => {
|
|
230
|
+
const closeListbox = (baseModel, commands, maybeOutMessage = Option.none()) => {
|
|
217
231
|
const closed = closedModel(baseModel);
|
|
218
232
|
if (baseModel.isAnimated) {
|
|
219
233
|
const [nextModel, animationCommands] = delegateToAnimation(closed, AnimationHid());
|
|
220
|
-
return [nextModel, [...commands, ...animationCommands]];
|
|
234
|
+
return [nextModel, [...commands, ...animationCommands], maybeOutMessage];
|
|
221
235
|
}
|
|
222
|
-
return [closed, commands];
|
|
236
|
+
return [closed, commands, maybeOutMessage];
|
|
223
237
|
};
|
|
224
|
-
|
|
238
|
+
const internalUpdate = (model, message) => {
|
|
225
239
|
const maybeLockScroll = OptionExt.when(model.isModal, LockScroll());
|
|
226
240
|
const maybeUnlockScroll = OptionExt.when(model.isModal, UnlockScroll());
|
|
227
241
|
const maybeInertOthers = OptionExt.when(model.isModal, InertOthers({ id: model.id }));
|
|
@@ -240,7 +254,7 @@ export const makeUpdate = (handleSelectedItem) => {
|
|
|
240
254
|
maybeUnlockScroll,
|
|
241
255
|
maybeRestoreInert,
|
|
242
256
|
]);
|
|
243
|
-
return M.value(message).pipe(withUpdateReturn, M.tagsExhaustive({
|
|
257
|
+
return M.value(message).pipe(withUpdateReturn, M.tag('CompletedLockScroll', 'CompletedUnlockScroll', 'CompletedInertOthers', 'CompletedRestoreInert', 'CompletedFocusButton', 'CompletedFocusItems', 'CompletedScrollIntoView', 'CompletedClickItem', 'SuppressedSpaceScroll', 'CompletedAnchorListbox', 'CompletedPortalListboxBackdrop', () => [model, [], Option.none()]), M.tagsExhaustive({
|
|
244
258
|
Opened: ({ maybeActiveItemIndex }) => openListbox(constrainedEvo(model, {
|
|
245
259
|
maybeActiveItemIndex: () => maybeActiveItemIndex,
|
|
246
260
|
activationTrigger: () => Option.match(maybeActiveItemIndex, {
|
|
@@ -254,7 +268,7 @@ export const makeUpdate = (handleSelectedItem) => {
|
|
|
254
268
|
Closed: () => closeListbox(model, closeWithFocusCommands),
|
|
255
269
|
BlurredItems: () => {
|
|
256
270
|
if (Option.exists(model.maybeLastButtonPointerType, Equal.equals('mouse'))) {
|
|
257
|
-
return [model, []];
|
|
271
|
+
return [model, [], Option.none()];
|
|
258
272
|
}
|
|
259
273
|
return closeListbox(model, closeWithoutFocusCommands);
|
|
260
274
|
},
|
|
@@ -266,11 +280,12 @@ export const makeUpdate = (handleSelectedItem) => {
|
|
|
266
280
|
activationTrigger === 'Keyboard'
|
|
267
281
|
? [ScrollIntoView({ id: model.id, index })]
|
|
268
282
|
: [],
|
|
283
|
+
Option.none(),
|
|
269
284
|
],
|
|
270
285
|
MovedPointerOverItem: ({ index, screenX, screenY }) => {
|
|
271
286
|
const isSamePosition = Option.exists(model.maybeLastPointerPosition, position => position.screenX === screenX && position.screenY === screenY);
|
|
272
287
|
if (isSamePosition) {
|
|
273
|
-
return [model, []];
|
|
288
|
+
return [model, [], Option.none()];
|
|
274
289
|
}
|
|
275
290
|
return [
|
|
276
291
|
constrainedEvo(model, {
|
|
@@ -279,6 +294,7 @@ export const makeUpdate = (handleSelectedItem) => {
|
|
|
279
294
|
maybeLastPointerPosition: () => Option.some({ screenX, screenY }),
|
|
280
295
|
}),
|
|
281
296
|
[],
|
|
297
|
+
Option.none(),
|
|
282
298
|
];
|
|
283
299
|
},
|
|
284
300
|
DeactivatedItem: () => model.activationTrigger === 'Pointer'
|
|
@@ -287,15 +303,17 @@ export const makeUpdate = (handleSelectedItem) => {
|
|
|
287
303
|
maybeActiveItemIndex: () => Option.none(),
|
|
288
304
|
}),
|
|
289
305
|
[],
|
|
306
|
+
Option.none(),
|
|
290
307
|
]
|
|
291
|
-
: [model, []],
|
|
308
|
+
: [model, [], Option.none()],
|
|
292
309
|
SelectedItem: ({ item }) => handleSelectedItem(model, item, {
|
|
293
|
-
closeWithFocus: closeModel => closeListbox(closeModel, closeWithFocusCommands),
|
|
294
|
-
closeWithoutFocus: closeModel => closeListbox(closeModel, closeWithoutFocusCommands),
|
|
310
|
+
closeWithFocus: (closeModel, maybeOutMessage = Option.none()) => closeListbox(closeModel, closeWithFocusCommands, maybeOutMessage),
|
|
311
|
+
closeWithoutFocus: (closeModel, maybeOutMessage = Option.none()) => closeListbox(closeModel, closeWithoutFocusCommands, maybeOutMessage),
|
|
295
312
|
}),
|
|
296
313
|
RequestedItemClick: ({ index }) => [
|
|
297
314
|
model,
|
|
298
315
|
[ClickItem({ id: model.id, index })],
|
|
316
|
+
Option.none(),
|
|
299
317
|
],
|
|
300
318
|
Searched: ({ key, maybeTargetIndex }) => {
|
|
301
319
|
const nextSearchQuery = model.searchQuery + key;
|
|
@@ -307,13 +325,18 @@ export const makeUpdate = (handleSelectedItem) => {
|
|
|
307
325
|
maybeActiveItemIndex: () => Option.orElse(maybeTargetIndex, () => model.maybeActiveItemIndex),
|
|
308
326
|
}),
|
|
309
327
|
[DelayClearSearch({ version: nextSearchVersion })],
|
|
328
|
+
Option.none(),
|
|
310
329
|
];
|
|
311
330
|
},
|
|
312
331
|
ClearedSearch: ({ version }) => {
|
|
313
332
|
if (version !== model.searchVersion) {
|
|
314
|
-
return [model, []];
|
|
333
|
+
return [model, [], Option.none()];
|
|
315
334
|
}
|
|
316
|
-
return [
|
|
335
|
+
return [
|
|
336
|
+
constrainedEvo(model, { searchQuery: () => '' }),
|
|
337
|
+
[],
|
|
338
|
+
Option.none(),
|
|
339
|
+
];
|
|
317
340
|
},
|
|
318
341
|
GotAnimationMessage: ({ message: animationMessage }) => delegateToAnimation(model, animationMessage),
|
|
319
342
|
PressedPointerOnButton: ({ pointerType, button }) => {
|
|
@@ -321,7 +344,7 @@ export const makeUpdate = (handleSelectedItem) => {
|
|
|
321
344
|
maybeLastButtonPointerType: () => Option.some(pointerType),
|
|
322
345
|
});
|
|
323
346
|
if (pointerType !== 'mouse' || button !== LEFT_MOUSE_BUTTON) {
|
|
324
|
-
return [withPointerType, []];
|
|
347
|
+
return [withPointerType, [], Option.none()];
|
|
325
348
|
}
|
|
326
349
|
if (model.isOpen) {
|
|
327
350
|
const [closed, commands] = closeListbox(withPointerType, closeWithFocusCommands);
|
|
@@ -330,6 +353,7 @@ export const makeUpdate = (handleSelectedItem) => {
|
|
|
330
353
|
maybeLastButtonPointerType: () => Option.some(pointerType),
|
|
331
354
|
}),
|
|
332
355
|
commands,
|
|
356
|
+
Option.none(),
|
|
333
357
|
];
|
|
334
358
|
}
|
|
335
359
|
return openListbox(constrainedEvo(withPointerType, {
|
|
@@ -340,25 +364,16 @@ export const makeUpdate = (handleSelectedItem) => {
|
|
|
340
364
|
maybeLastPointerPosition: () => Option.none(),
|
|
341
365
|
}), openCommands);
|
|
342
366
|
},
|
|
343
|
-
CompletedLockScroll: () => [model, []],
|
|
344
|
-
CompletedUnlockScroll: () => [model, []],
|
|
345
|
-
CompletedSetupInert: () => [model, []],
|
|
346
|
-
CompletedTeardownInert: () => [model, []],
|
|
347
|
-
CompletedFocusButton: () => [model, []],
|
|
348
|
-
CompletedFocusItems: () => [model, []],
|
|
349
|
-
CompletedScrollIntoView: () => [model, []],
|
|
350
|
-
CompletedClickItem: () => [model, []],
|
|
351
367
|
IgnoredMouseClick: () => [
|
|
352
368
|
constrainedEvo(model, {
|
|
353
369
|
maybeLastButtonPointerType: () => Option.none(),
|
|
354
370
|
}),
|
|
355
371
|
[],
|
|
372
|
+
Option.none(),
|
|
356
373
|
],
|
|
357
|
-
SuppressedSpaceScroll: () => [model, []],
|
|
358
|
-
CompletedAnchorListbox: () => [model, []],
|
|
359
|
-
CompletedPortalListboxBackdrop: () => [model, []],
|
|
360
374
|
}));
|
|
361
375
|
};
|
|
376
|
+
return internalUpdate;
|
|
362
377
|
};
|
|
363
378
|
/** The anchor-positioning Mount this Listbox renders when an anchor is
|
|
364
379
|
* configured. Exposed so Scene tests can call
|
|
@@ -374,272 +389,280 @@ export const PortalListboxBackdrop = Mount.define('PortalListboxBackdrop', Compl
|
|
|
374
389
|
yield* Effect.acquireRelease(Effect.sync(() => portalToBody(element)), cleanup => Effect.sync(cleanup));
|
|
375
390
|
return CompletedPortalListboxBackdrop();
|
|
376
391
|
}));
|
|
377
|
-
export const makeView = (behavior) =>
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
const handleSpaceKeyUp = (key) => OptionExt.when(key === ' ', toParentMessage(SuppressedSpaceScroll()));
|
|
446
|
-
const resolveActiveIndex = (key) => Option.match(maybeActiveItemIndex, {
|
|
447
|
-
onNone: () => M.value(key).pipe(M.whenOr(previousKey, 'End', 'PageDown', () => lastEnabledIndex), M.orElse(() => firstEnabledIndex)),
|
|
448
|
-
onSome: activeIndex => keyToIndex(nextKey, previousKey, items.length, activeIndex, isItemDisabledByIndex)(key),
|
|
449
|
-
});
|
|
450
|
-
const searchForKey = (key) => {
|
|
451
|
-
const nextQuery = searchQuery + key;
|
|
452
|
-
const maybeTargetIndex = resolveTypeaheadMatch(items, nextQuery, maybeActiveItemIndex, isItemDisabledByIndex, itemToSearchText, Str.isNonEmpty(searchQuery));
|
|
453
|
-
return Option.some(toParentMessage(Searched({ key, maybeTargetIndex })));
|
|
454
|
-
};
|
|
455
|
-
const handleItemsKeyDown = (key) => M.value(key).pipe(M.when('Escape', () => Option.some(toParentMessage(Closed()))), M.when('Enter', () => Option.map(maybeActiveItemIndex, index => toParentMessage(RequestedItemClick({ index })))), M.when(' ', () => Str.isNonEmpty(searchQuery)
|
|
456
|
-
? searchForKey(' ')
|
|
457
|
-
: Option.map(maybeActiveItemIndex, index => toParentMessage(RequestedItemClick({ index })))), M.when(isNavigationKey, () => Option.some(toParentMessage(ActivatedItem({
|
|
458
|
-
index: resolveActiveIndex(key),
|
|
459
|
-
activationTrigger: 'Keyboard',
|
|
460
|
-
})))), M.when(isPrintableKey, () => searchForKey(key)), M.orElse(() => Option.none()));
|
|
461
|
-
const resolvedButtonAttributes = [
|
|
462
|
-
h.Id(`${id}-button`),
|
|
463
|
-
h.Type('button'),
|
|
464
|
-
h.AriaHasPopup('listbox'),
|
|
465
|
-
h.AriaExpanded(isVisible),
|
|
466
|
-
h.AriaControls(`${id}-items`),
|
|
467
|
-
...(isButtonEffectivelyDisabled
|
|
468
|
-
? [h.AriaDisabled(true), h.DataAttribute('disabled', '')]
|
|
469
|
-
: [
|
|
470
|
-
h.OnPointerDown(handleButtonPointerDown),
|
|
471
|
-
h.OnKeyDownPreventDefault(handleButtonKeyDown),
|
|
472
|
-
h.OnKeyUpPreventDefault(handleSpaceKeyUp),
|
|
473
|
-
h.OnClick(handleButtonClick()),
|
|
474
|
-
]),
|
|
475
|
-
...(isVisible
|
|
476
|
-
? [
|
|
477
|
-
h.DataAttribute('open', ''),
|
|
478
|
-
h.Style({ position: 'relative', zIndex: '1' }),
|
|
479
|
-
]
|
|
480
|
-
: []),
|
|
481
|
-
...(isInvalid ? [h.DataAttribute('invalid', '')] : []),
|
|
482
|
-
...(buttonClassName ? [h.Class(buttonClassName)] : []),
|
|
483
|
-
...buttonAttributes,
|
|
484
|
-
];
|
|
485
|
-
const maybeActiveDescendant = Option.match(maybeActiveItemIndex, {
|
|
486
|
-
onNone: () => [],
|
|
487
|
-
onSome: index => [h.AriaActiveDescendant(itemId(id, index))],
|
|
488
|
-
});
|
|
489
|
-
const anchorAttributes = anchor
|
|
490
|
-
? [
|
|
491
|
-
h.Style({ position: 'absolute', margin: '0', visibility: 'hidden' }),
|
|
492
|
-
h.OnMount(Mount.mapMessage(AnchorListbox({ buttonId: `${id}-button`, anchor }), toParentMessage)),
|
|
493
|
-
]
|
|
494
|
-
: [];
|
|
495
|
-
const itemsContainerAttributes = [
|
|
496
|
-
h.Id(`${id}-items`),
|
|
497
|
-
h.Role('listbox'),
|
|
498
|
-
h.AriaOrientation(Str.toLowerCase(orientation)),
|
|
499
|
-
...(behavior.ariaMultiSelectable ? [h.AriaMultiSelectable(true)] : []),
|
|
500
|
-
h.AriaLabelledBy(`${id}-button`),
|
|
501
|
-
...maybeActiveDescendant,
|
|
502
|
-
h.Tabindex(0),
|
|
503
|
-
...anchorAttributes,
|
|
504
|
-
...animationAttributes,
|
|
505
|
-
...(isLeaving
|
|
506
|
-
? []
|
|
507
|
-
: [
|
|
508
|
-
h.OnKeyDownPreventDefault(handleItemsKeyDown),
|
|
509
|
-
h.OnKeyUpPreventDefault(handleSpaceKeyUp),
|
|
510
|
-
h.OnBlur(toParentMessage(BlurredItems())),
|
|
511
|
-
]),
|
|
512
|
-
...(itemsClassName ? [h.Class(itemsClassName)] : []),
|
|
513
|
-
...itemsAttributes,
|
|
514
|
-
];
|
|
515
|
-
const listboxItems = Array.map(items, (item, index) => {
|
|
516
|
-
const isActiveItem = Option.exists(maybeActiveItemIndex, activeIndex => activeIndex === index);
|
|
517
|
-
const isDisabledItem = isItemDisabledByIndex(index);
|
|
518
|
-
const isSelectedItem = behavior.isItemSelected(config.model, itemToValue(item));
|
|
519
|
-
const itemConfig = itemToConfig(item, {
|
|
520
|
-
isActive: isActiveItem,
|
|
521
|
-
isDisabled: isDisabledItem,
|
|
522
|
-
isSelected: isSelectedItem,
|
|
392
|
+
export const makeView = (behavior) => {
|
|
393
|
+
const impl = defineView((model, viewInputs) => {
|
|
394
|
+
const h = html();
|
|
395
|
+
const { id, isOpen, orientation, animation: { transitionState }, maybeActiveItemIndex, searchQuery, maybeLastButtonPointerType, } = model;
|
|
396
|
+
const { items, itemToConfig, isItemDisabled, isButtonDisabled, buttonContent, buttonClassName, buttonAttributes = [], itemsClassName, itemsAttributes = [], itemsScrollClassName, itemsScrollAttributes = [], backdropClassName, backdropAttributes = [], className, attributes = [], itemGroupKey, groupToHeading, groupClassName, groupAttributes = [], separatorClassName, separatorAttributes = [], anchor, name, form, isDisabled, isInvalid, } = viewInputs;
|
|
397
|
+
const itemToValue = viewInputs.itemToValue ?? ((item) => String(item));
|
|
398
|
+
const itemToSearchText = viewInputs.itemToSearchText ?? ((item) => itemToValue(item));
|
|
399
|
+
const isLeaving = transitionState === 'LeaveStart' || transitionState === 'LeaveAnimating';
|
|
400
|
+
const isVisible = isOpen || isLeaving;
|
|
401
|
+
const animationAttributes = M.value(transitionState).pipe(M.when('EnterStart', () => [
|
|
402
|
+
h.DataAttribute('closed', ''),
|
|
403
|
+
h.DataAttribute('enter', ''),
|
|
404
|
+
h.DataAttribute('transition', ''),
|
|
405
|
+
]), M.when('EnterAnimating', () => [
|
|
406
|
+
h.DataAttribute('enter', ''),
|
|
407
|
+
h.DataAttribute('transition', ''),
|
|
408
|
+
]), M.when('LeaveStart', () => [
|
|
409
|
+
h.DataAttribute('leave', ''),
|
|
410
|
+
h.DataAttribute('transition', ''),
|
|
411
|
+
]), M.when('LeaveAnimating', () => [
|
|
412
|
+
h.DataAttribute('closed', ''),
|
|
413
|
+
h.DataAttribute('leave', ''),
|
|
414
|
+
h.DataAttribute('transition', ''),
|
|
415
|
+
]), M.orElse(() => []));
|
|
416
|
+
const isItemDisabledByIndex = (index) => Predicate.isNotUndefined(isItemDisabled) &&
|
|
417
|
+
pipe(items, Array.get(index), Option.exists(item => isItemDisabled(item, index)));
|
|
418
|
+
const isButtonEffectivelyDisabled = isDisabled || isButtonDisabled;
|
|
419
|
+
const nextKey = orientation === 'Horizontal' ? 'ArrowRight' : 'ArrowDown';
|
|
420
|
+
const previousKey = orientation === 'Horizontal' ? 'ArrowLeft' : 'ArrowUp';
|
|
421
|
+
const navigationKeys = [
|
|
422
|
+
nextKey,
|
|
423
|
+
previousKey,
|
|
424
|
+
'Home',
|
|
425
|
+
'End',
|
|
426
|
+
'PageUp',
|
|
427
|
+
'PageDown',
|
|
428
|
+
];
|
|
429
|
+
const isNavigationKey = (key) => Array.contains(navigationKeys, key);
|
|
430
|
+
const firstEnabledIndex = findFirstEnabledIndex(items.length, 0, isItemDisabledByIndex)(0, 1);
|
|
431
|
+
const lastEnabledIndex = findFirstEnabledIndex(items.length, 0, isItemDisabledByIndex)(items.length - 1, -1);
|
|
432
|
+
const selectedItemIndex = behavior.selectedItemIndex(model, items, itemToValue);
|
|
433
|
+
const handleButtonKeyDown = (key) => {
|
|
434
|
+
if (isOpen) {
|
|
435
|
+
return handleItemsKeyDown(key);
|
|
436
|
+
}
|
|
437
|
+
return M.value(key).pipe(M.whenOr('Enter', ' ', 'ArrowDown', () => Option.some(Opened({
|
|
438
|
+
maybeActiveItemIndex: Option.orElse(selectedItemIndex, () => Option.some(firstEnabledIndex)),
|
|
439
|
+
}))), M.when('ArrowUp', () => Option.some(Opened({
|
|
440
|
+
maybeActiveItemIndex: Option.orElse(selectedItemIndex, () => Option.some(lastEnabledIndex)),
|
|
441
|
+
}))), M.orElse(() => Option.none()));
|
|
442
|
+
};
|
|
443
|
+
const handleButtonPointerDown = (pointerType, button) => Option.some(PressedPointerOnButton({ pointerType, button }));
|
|
444
|
+
const handleButtonClick = () => {
|
|
445
|
+
const isMouse = Option.exists(maybeLastButtonPointerType, type => type === 'mouse');
|
|
446
|
+
if (isMouse) {
|
|
447
|
+
return IgnoredMouseClick();
|
|
448
|
+
}
|
|
449
|
+
else if (isOpen) {
|
|
450
|
+
return Closed();
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
return Opened({ maybeActiveItemIndex: Option.none() });
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
const handleSpaceKeyUp = (key) => OptionExt.when(key === ' ', SuppressedSpaceScroll());
|
|
457
|
+
const resolveActiveIndex = (key) => Option.match(maybeActiveItemIndex, {
|
|
458
|
+
onNone: () => M.value(key).pipe(M.whenOr(previousKey, 'End', 'PageDown', () => lastEnabledIndex), M.orElse(() => firstEnabledIndex)),
|
|
459
|
+
onSome: activeIndex => keyToIndex(nextKey, previousKey, items.length, activeIndex, isItemDisabledByIndex)(key),
|
|
523
460
|
});
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
461
|
+
const searchForKey = (key) => {
|
|
462
|
+
const nextQuery = searchQuery + key;
|
|
463
|
+
const maybeTargetIndex = resolveTypeaheadMatch(items, nextQuery, maybeActiveItemIndex, isItemDisabledByIndex, itemToSearchText, Str.isNonEmpty(searchQuery));
|
|
464
|
+
return Option.some(Searched({ key, maybeTargetIndex }));
|
|
465
|
+
};
|
|
466
|
+
const handleItemsKeyDown = (key) => M.value(key).pipe(M.when('Escape', () => Option.some(Closed())), M.when('Enter', () => Option.map(maybeActiveItemIndex, index => RequestedItemClick({ index }))), M.when(' ', () => Str.isNonEmpty(searchQuery)
|
|
467
|
+
? searchForKey(' ')
|
|
468
|
+
: Option.map(maybeActiveItemIndex, index => RequestedItemClick({ index }))), M.when(isNavigationKey, () => Option.some(ActivatedItem({
|
|
469
|
+
index: resolveActiveIndex(key),
|
|
470
|
+
activationTrigger: 'Keyboard',
|
|
471
|
+
}))), M.when(isPrintableKey, () => searchForKey(key)), M.orElse(() => Option.none()));
|
|
472
|
+
const resolvedButtonAttributes = [
|
|
473
|
+
h.Id(`${id}-button`),
|
|
474
|
+
h.Type('button'),
|
|
475
|
+
h.AriaHasPopup('listbox'),
|
|
476
|
+
h.AriaExpanded(isVisible),
|
|
477
|
+
h.AriaControls(`${id}-items`),
|
|
478
|
+
...(isButtonEffectivelyDisabled
|
|
532
479
|
? [h.AriaDisabled(true), h.DataAttribute('disabled', '')]
|
|
533
|
-
: [
|
|
534
|
-
|
|
480
|
+
: [
|
|
481
|
+
h.OnPointerDown(handleButtonPointerDown),
|
|
482
|
+
h.OnKeyDownPreventDefault(handleButtonKeyDown),
|
|
483
|
+
h.OnKeyUpPreventDefault(handleSpaceKeyUp),
|
|
484
|
+
h.OnClick(handleButtonClick()),
|
|
485
|
+
]),
|
|
486
|
+
...(isVisible
|
|
535
487
|
? [
|
|
536
|
-
h.
|
|
537
|
-
|
|
538
|
-
? []
|
|
539
|
-
: [
|
|
540
|
-
h.OnPointerMove((screenX, screenY, pointerType) => OptionExt.when(pointerType !== 'touch', toParentMessage(MovedPointerOverItem({ index, screenX, screenY })))),
|
|
541
|
-
]),
|
|
542
|
-
h.OnPointerLeave(pointerType => OptionExt.when(pointerType !== 'touch', toParentMessage(DeactivatedItem()))),
|
|
488
|
+
h.DataAttribute('open', ''),
|
|
489
|
+
h.Style({ position: 'relative', zIndex: '1' }),
|
|
543
490
|
]
|
|
544
491
|
: []),
|
|
545
|
-
...(
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
492
|
+
...(isInvalid ? [h.DataAttribute('invalid', '')] : []),
|
|
493
|
+
...(buttonClassName ? [h.Class(buttonClassName)] : []),
|
|
494
|
+
...buttonAttributes,
|
|
495
|
+
];
|
|
496
|
+
const maybeActiveDescendant = Option.match(maybeActiveItemIndex, {
|
|
497
|
+
onNone: () => [],
|
|
498
|
+
onSome: index => [h.AriaActiveDescendant(itemId(id, index))],
|
|
499
|
+
});
|
|
500
|
+
const anchorAttributes = anchor
|
|
501
|
+
? [
|
|
502
|
+
h.Style({
|
|
503
|
+
position: 'absolute',
|
|
504
|
+
margin: '0',
|
|
505
|
+
visibility: 'hidden',
|
|
506
|
+
}),
|
|
507
|
+
h.OnMount(AnchorListbox({ buttonId: `${id}-button`, anchor })),
|
|
508
|
+
]
|
|
509
|
+
: [];
|
|
510
|
+
const itemsContainerAttributes = [
|
|
511
|
+
h.Id(`${id}-items`),
|
|
512
|
+
h.Role('listbox'),
|
|
513
|
+
h.AriaOrientation(Str.toLowerCase(orientation)),
|
|
514
|
+
...(behavior.ariaMultiSelectable ? [h.AriaMultiSelectable(true)] : []),
|
|
515
|
+
h.AriaLabelledBy(`${id}-button`),
|
|
516
|
+
...maybeActiveDescendant,
|
|
517
|
+
h.Tabindex(0),
|
|
518
|
+
...anchorAttributes,
|
|
519
|
+
...animationAttributes,
|
|
520
|
+
...(isLeaving
|
|
521
|
+
? []
|
|
522
|
+
: [
|
|
523
|
+
h.OnKeyDownPreventDefault(handleItemsKeyDown),
|
|
524
|
+
h.OnKeyUpPreventDefault(handleSpaceKeyUp),
|
|
525
|
+
h.OnBlur(BlurredItems()),
|
|
526
|
+
]),
|
|
527
|
+
...(itemsClassName ? [h.Class(itemsClassName)] : []),
|
|
528
|
+
...itemsAttributes,
|
|
529
|
+
];
|
|
530
|
+
const listboxItems = Array.map(items, (item, index) => {
|
|
531
|
+
const isActiveItem = Option.exists(maybeActiveItemIndex, activeIndex => activeIndex === index);
|
|
532
|
+
const isDisabledItem = isItemDisabledByIndex(index);
|
|
533
|
+
const isSelectedItem = behavior.isItemSelected(model, itemToValue(item));
|
|
534
|
+
const itemConfig = itemToConfig(item, {
|
|
535
|
+
isActive: isActiveItem,
|
|
536
|
+
isDisabled: isDisabledItem,
|
|
537
|
+
isSelected: isSelectedItem,
|
|
568
538
|
});
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
h.
|
|
572
|
-
|
|
573
|
-
|
|
539
|
+
const isInteractive = !isDisabledItem && !isLeaving;
|
|
540
|
+
return h.keyed('div')(itemId(id, index), [
|
|
541
|
+
h.Id(itemId(id, index)),
|
|
542
|
+
h.Role('option'),
|
|
543
|
+
h.AriaSelected(isSelectedItem),
|
|
544
|
+
...(isActiveItem ? [h.DataAttribute('active', '')] : []),
|
|
545
|
+
...(isSelectedItem ? [h.DataAttribute('selected', '')] : []),
|
|
546
|
+
...(isDisabledItem
|
|
547
|
+
? [h.AriaDisabled(true), h.DataAttribute('disabled', '')]
|
|
574
548
|
: []),
|
|
575
|
-
...(
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
]
|
|
589
|
-
]
|
|
590
|
-
: [];
|
|
591
|
-
return [...separator, groupElement];
|
|
592
|
-
});
|
|
593
|
-
};
|
|
594
|
-
const backdrop = h.keyed('div')(`${id}-backdrop`, [
|
|
595
|
-
h.OnMount(Mount.mapMessage(PortalListboxBackdrop(), toParentMessage)),
|
|
596
|
-
...(isLeaving ? [] : [h.OnClick(toParentMessage(Closed()))]),
|
|
597
|
-
...(backdropClassName ? [h.Class(backdropClassName)] : []),
|
|
598
|
-
...backdropAttributes,
|
|
599
|
-
], []);
|
|
600
|
-
const renderedItems = renderGroupedItems();
|
|
601
|
-
const scrollableItems = itemsScrollClassName ||
|
|
602
|
-
Array.isReadonlyArrayNonEmpty(itemsScrollAttributes)
|
|
603
|
-
? [
|
|
604
|
-
h.div([
|
|
605
|
-
...(itemsScrollClassName
|
|
606
|
-
? [h.Class(itemsScrollClassName)]
|
|
549
|
+
...(isInteractive
|
|
550
|
+
? [
|
|
551
|
+
h.OnClick(SelectedItem({ item: itemToValue(item) })),
|
|
552
|
+
...(isActiveItem
|
|
553
|
+
? []
|
|
554
|
+
: [
|
|
555
|
+
h.OnPointerMove((screenX, screenY, pointerType) => OptionExt.when(pointerType !== 'touch', MovedPointerOverItem({
|
|
556
|
+
index,
|
|
557
|
+
screenX,
|
|
558
|
+
screenY,
|
|
559
|
+
}))),
|
|
560
|
+
]),
|
|
561
|
+
h.OnPointerLeave(pointerType => OptionExt.when(pointerType !== 'touch', DeactivatedItem())),
|
|
562
|
+
]
|
|
607
563
|
: []),
|
|
608
|
-
...
|
|
609
|
-
],
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
564
|
+
...(itemConfig.className ? [h.Class(itemConfig.className)] : []),
|
|
565
|
+
], [itemConfig.content]);
|
|
566
|
+
});
|
|
567
|
+
const renderGroupedItems = () => {
|
|
568
|
+
if (!itemGroupKey) {
|
|
569
|
+
return listboxItems;
|
|
570
|
+
}
|
|
571
|
+
const segments = groupContiguous(listboxItems, (_, index) => Array.get(items, index).pipe(Option.match({
|
|
572
|
+
onNone: () => '',
|
|
573
|
+
onSome: item => itemGroupKey(item, index),
|
|
574
|
+
})));
|
|
575
|
+
return Array.flatMap(segments, (segment, segmentIndex) => {
|
|
576
|
+
const maybeHeading = Option.fromNullishOr(groupToHeading?.(segment.key));
|
|
577
|
+
const headingId = `${id}-heading-${segment.key}`;
|
|
578
|
+
const headingElement = Option.match(maybeHeading, {
|
|
579
|
+
onNone: () => [],
|
|
580
|
+
onSome: heading => [
|
|
581
|
+
h.keyed('div')(headingId, [
|
|
582
|
+
h.Id(headingId),
|
|
583
|
+
h.Role('presentation'),
|
|
584
|
+
...(heading.className ? [h.Class(heading.className)] : []),
|
|
585
|
+
], [heading.content]),
|
|
586
|
+
],
|
|
587
|
+
});
|
|
588
|
+
const groupContent = [...headingElement, ...segment.items];
|
|
589
|
+
const groupElement = h.keyed('div')(`${id}-group-${segment.key}`, [
|
|
590
|
+
h.Role('group'),
|
|
591
|
+
...(Option.isSome(maybeHeading)
|
|
592
|
+
? [h.AriaLabelledBy(headingId)]
|
|
593
|
+
: []),
|
|
594
|
+
...(groupClassName ? [h.Class(groupClassName)] : []),
|
|
595
|
+
...groupAttributes,
|
|
596
|
+
], groupContent);
|
|
597
|
+
const separator = segmentIndex > 0 &&
|
|
598
|
+
(separatorClassName ||
|
|
599
|
+
Array.isReadonlyArrayNonEmpty(separatorAttributes))
|
|
600
|
+
? [
|
|
601
|
+
h.keyed('div')(`${id}-separator-${segmentIndex}`, [
|
|
602
|
+
h.Role('separator'),
|
|
603
|
+
...(separatorClassName
|
|
604
|
+
? [h.Class(separatorClassName)]
|
|
605
|
+
: []),
|
|
606
|
+
...separatorAttributes,
|
|
607
|
+
], []),
|
|
608
|
+
]
|
|
609
|
+
: [];
|
|
610
|
+
return [...separator, groupElement];
|
|
611
|
+
});
|
|
612
|
+
};
|
|
613
|
+
const backdrop = h.keyed('div')(`${id}-backdrop`, [
|
|
614
|
+
h.OnMount(PortalListboxBackdrop()),
|
|
615
|
+
...(isLeaving ? [] : [h.OnClick(Closed())]),
|
|
616
|
+
...(backdropClassName ? [h.Class(backdropClassName)] : []),
|
|
617
|
+
...backdropAttributes,
|
|
618
|
+
], []);
|
|
619
|
+
const renderedItems = renderGroupedItems();
|
|
620
|
+
const scrollableItems = itemsScrollClassName ||
|
|
621
|
+
Array.isReadonlyArrayNonEmpty(itemsScrollAttributes)
|
|
622
|
+
? [
|
|
623
|
+
h.div([
|
|
624
|
+
...(itemsScrollClassName
|
|
625
|
+
? [h.Class(itemsScrollClassName)]
|
|
626
|
+
: []),
|
|
627
|
+
...itemsScrollAttributes,
|
|
628
|
+
], renderedItems),
|
|
629
|
+
]
|
|
630
|
+
: renderedItems;
|
|
631
|
+
const visibleContent = [
|
|
632
|
+
backdrop,
|
|
633
|
+
h.keyed('div')(`${id}-items-container`, itemsContainerAttributes, scrollableItems),
|
|
634
|
+
];
|
|
635
|
+
const formAttribute = form ? [h.Attribute('form', form)] : [];
|
|
636
|
+
const selectedValues = pipe(items, Array.filter(item => behavior.isItemSelected(model, itemToValue(item))), Array.map(itemToValue));
|
|
637
|
+
const hiddenInputs = name
|
|
638
|
+
? Array.match(selectedValues, {
|
|
639
|
+
onEmpty: () => [
|
|
640
|
+
h.input([h.Type('hidden'), h.Name(name), ...formAttribute]),
|
|
641
|
+
],
|
|
642
|
+
onNonEmpty: Array.map(selectedValue => h.input([
|
|
643
|
+
h.Type('hidden'),
|
|
644
|
+
h.Name(name),
|
|
645
|
+
h.Value(selectedValue),
|
|
646
|
+
...formAttribute,
|
|
647
|
+
])),
|
|
648
|
+
})
|
|
649
|
+
: [];
|
|
650
|
+
const wrapperAttributes = [
|
|
651
|
+
...(className ? [h.Class(className)] : []),
|
|
652
|
+
...attributes,
|
|
653
|
+
...(isVisible ? [h.DataAttribute('open', '')] : []),
|
|
654
|
+
...(isDisabled ? [h.DataAttribute('disabled', '')] : []),
|
|
655
|
+
...(isInvalid ? [h.DataAttribute('invalid', '')] : []),
|
|
656
|
+
];
|
|
657
|
+
return h.div(wrapperAttributes, [
|
|
658
|
+
h.keyed('button')(`${id}-button`, resolvedButtonAttributes, [
|
|
659
|
+
buttonContent,
|
|
660
|
+
]),
|
|
661
|
+
...hiddenInputs,
|
|
662
|
+
...(isVisible ? visibleContent : []),
|
|
663
|
+
]);
|
|
664
|
+
});
|
|
665
|
+
return () =>
|
|
666
|
+
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
667
|
+
impl;
|
|
645
668
|
};
|