@foldkit/ui 0.127.0 → 0.128.1
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 -2
- package/dist/calendar/index.d.ts +17 -23
- package/dist/calendar/index.d.ts.map +1 -1
- package/dist/calendar/index.js +16 -25
- package/dist/calendar/public.d.ts +1 -1
- package/dist/calendar/public.d.ts.map +1 -1
- package/dist/calendar/public.js +1 -1
- package/dist/checkbox/index.d.ts +47 -104
- package/dist/checkbox/index.d.ts.map +1 -1
- package/dist/checkbox/index.js +48 -87
- package/dist/checkbox/public.d.ts +2 -2
- package/dist/checkbox/public.d.ts.map +1 -1
- package/dist/checkbox/public.js +1 -1
- package/dist/combobox/multi.d.ts +19 -30
- package/dist/combobox/multi.d.ts.map +1 -1
- package/dist/combobox/multi.js +32 -53
- package/dist/combobox/public.d.ts +2 -2
- package/dist/combobox/public.d.ts.map +1 -1
- package/dist/combobox/public.js +1 -1
- package/dist/combobox/shared.d.ts +59 -35
- package/dist/combobox/shared.d.ts.map +1 -1
- package/dist/combobox/shared.js +58 -45
- package/dist/combobox/single.d.ts +36 -48
- package/dist/combobox/single.d.ts.map +1 -1
- package/dist/combobox/single.js +49 -76
- package/dist/datePicker/index.d.ts +33 -41
- package/dist/datePicker/index.d.ts.map +1 -1
- package/dist/datePicker/index.js +31 -40
- package/dist/datePicker/public.d.ts +1 -1
- package/dist/datePicker/public.d.ts.map +1 -1
- package/dist/datePicker/public.js +1 -1
- package/dist/disclosure/index.d.ts +49 -113
- package/dist/disclosure/index.d.ts.map +1 -1
- package/dist/disclosure/index.js +35 -92
- package/dist/disclosure/public.d.ts +2 -2
- package/dist/disclosure/public.d.ts.map +1 -1
- package/dist/disclosure/public.js +1 -1
- package/dist/listbox/multi.d.ts +7 -21
- package/dist/listbox/multi.d.ts.map +1 -1
- package/dist/listbox/multi.js +12 -36
- package/dist/listbox/public.d.ts +1 -1
- package/dist/listbox/public.d.ts.map +1 -1
- package/dist/listbox/shared.d.ts +28 -20
- package/dist/listbox/shared.d.ts.map +1 -1
- package/dist/listbox/shared.js +6 -7
- package/dist/listbox/single.d.ts +17 -27
- package/dist/listbox/single.d.ts.map +1 -1
- package/dist/listbox/single.js +18 -30
- package/dist/radioGroup/index.d.ts +57 -142
- package/dist/radioGroup/index.d.ts.map +1 -1
- package/dist/radioGroup/index.js +44 -119
- package/dist/radioGroup/public.d.ts +2 -2
- package/dist/radioGroup/public.d.ts.map +1 -1
- package/dist/radioGroup/public.js +1 -1
- package/dist/slider/index.d.ts +51 -27
- package/dist/slider/index.d.ts.map +1 -1
- package/dist/slider/index.js +66 -66
- package/dist/slider/public.d.ts +1 -1
- package/dist/slider/public.d.ts.map +1 -1
- package/dist/slider/public.js +1 -1
- package/dist/switch/index.d.ts +45 -92
- package/dist/switch/index.d.ts.map +1 -1
- package/dist/switch/index.js +36 -81
- package/dist/switch/public.d.ts +2 -2
- package/dist/switch/public.d.ts.map +1 -1
- package/dist/switch/public.js +1 -1
- package/dist/tabs/index.d.ts +33 -33
- package/dist/tabs/index.d.ts.map +1 -1
- package/dist/tabs/index.js +36 -49
- package/dist/toast/index.d.ts +1 -0
- package/dist/toast/index.d.ts.map +1 -1
- package/dist/toast/index.js +1 -0
- package/dist/toast/public.d.ts +2 -0
- package/dist/toast/public.d.ts.map +1 -1
- package/dist/toast/public.js +1 -0
- package/dist/toast/test.d.ts +41 -0
- package/dist/toast/test.d.ts.map +1 -0
- package/dist/toast/test.js +36 -0
- package/package.json +5 -5
package/dist/combobox/shared.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Array, Effect, Match as M, Option, Predicate,
|
|
1
|
+
import { Array, Effect, Match as M, Option, Predicate, Schema as S, pipe, } from 'effect';
|
|
2
2
|
import * as Command from 'foldkit/command';
|
|
3
3
|
import * as Dom from 'foldkit/dom';
|
|
4
4
|
import { html } from 'foldkit/html';
|
|
@@ -55,22 +55,25 @@ export const baseInit = (config) => ({
|
|
|
55
55
|
export const Opened = m('Opened', {
|
|
56
56
|
maybeActiveItemIndex: S.Option(S.Number),
|
|
57
57
|
});
|
|
58
|
-
/** Sent when the combobox closes via Escape key or backdrop click. */
|
|
59
|
-
export const Closed = m('Closed');
|
|
60
|
-
/** Sent when the combobox input loses focus. */
|
|
61
|
-
export const BlurredInput = m('BlurredInput'
|
|
58
|
+
/** Sent when the combobox closes via Escape key or backdrop click. `restingInputValue` is what the input returns to on close (the parent-owned selection's display text, or empty), computed by the view from `ViewInputs.restingInputValue`. */
|
|
59
|
+
export const Closed = m('Closed', { restingInputValue: S.String });
|
|
60
|
+
/** Sent when the combobox input loses focus. `restingInputValue` is what the input returns to on close (the parent-owned selection's display text, or empty), computed by the view from `ViewInputs.restingInputValue`. */
|
|
61
|
+
export const BlurredInput = m('BlurredInput', {
|
|
62
|
+
restingInputValue: S.String,
|
|
63
|
+
});
|
|
62
64
|
/** Sent when an item is highlighted via arrow keys or mouse hover. Includes activation trigger and optional immediate selection info. */
|
|
63
65
|
export const ActivatedItem = m('ActivatedItem', {
|
|
64
66
|
index: S.Number,
|
|
65
67
|
activationTrigger: ActivationTrigger,
|
|
66
|
-
maybeImmediateSelection: S.Option(S.Struct({ item: S.String
|
|
68
|
+
maybeImmediateSelection: S.Option(S.Struct({ item: S.String })),
|
|
67
69
|
});
|
|
68
70
|
/** Sent when the mouse leaves an enabled item. */
|
|
69
71
|
export const DeactivatedItem = m('DeactivatedItem');
|
|
70
|
-
/** Sent when an item is selected via Enter or click.
|
|
72
|
+
/** Sent when an item is selected via Enter or click. `displayText` is the item's resting input text, and `wasSelected` reports whether the item was already in the parent-owned selection when activated, so nullable deselect logic works without the Model knowing the selection. */
|
|
71
73
|
export const SelectedItem = m('SelectedItem', {
|
|
72
74
|
item: S.String,
|
|
73
75
|
displayText: S.String,
|
|
76
|
+
wasSelected: S.Boolean,
|
|
74
77
|
});
|
|
75
78
|
/** Sent when the pointer moves over a combobox item. */
|
|
76
79
|
export const MovedPointerOverItem = m('MovedPointerOverItem', {
|
|
@@ -112,8 +115,10 @@ export const GotAnimationMessage = m('GotAnimationMessage', {
|
|
|
112
115
|
export const UpdatedInputValue = m('UpdatedInputValue', {
|
|
113
116
|
value: S.String,
|
|
114
117
|
});
|
|
115
|
-
/** Sent when the optional toggle button is clicked. */
|
|
116
|
-
export const PressedToggleButton = m('PressedToggleButton'
|
|
118
|
+
/** Sent when the optional toggle button is clicked. `restingInputValue` is what the input returns to when the press closes the combobox (the parent-owned selection's display text, or empty), computed by the view from `ViewInputs.restingInputValue`. */
|
|
119
|
+
export const PressedToggleButton = m('PressedToggleButton', {
|
|
120
|
+
restingInputValue: S.String,
|
|
121
|
+
});
|
|
117
122
|
/** Union of all messages the combobox component can produce. */
|
|
118
123
|
export const Message = S.Union([
|
|
119
124
|
Opened,
|
|
@@ -140,13 +145,14 @@ export const Message = S.Union([
|
|
|
140
145
|
PressedToggleButton,
|
|
141
146
|
]);
|
|
142
147
|
// OUT MESSAGE
|
|
143
|
-
/** Sent when
|
|
148
|
+
/** Sent when the user activates an item. Carries the neutral fact that the item was activated; the parent owns the selection and decides what it means (single-select stores the value, nullable single-select toggles it, multi-select toggles the value's membership). 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 the factory's `update` without casting. */
|
|
144
149
|
export const Selected = m('Selected', {
|
|
145
150
|
value: S.String,
|
|
146
|
-
wasAdded: S.Boolean,
|
|
147
151
|
});
|
|
148
|
-
/**
|
|
149
|
-
export const
|
|
152
|
+
/** Sent when a nullable combobox closes with an empty input, meaning the user cleared it. The parent clears the selection it owns. */
|
|
153
|
+
export const ClearedSelection = m('ClearedSelection');
|
|
154
|
+
/** Union of out-messages the combobox component can produce. The parent folds `Selected` into the selection it owns and clears that selection on `ClearedSelection`. */
|
|
155
|
+
export const OutMessage = S.Union([Selected, ClearedSelection]);
|
|
150
156
|
// SELECTORS
|
|
151
157
|
/** Returns the bare DOM id of the combobox input, derived from the
|
|
152
158
|
* combobox's base id. Use this to associate an external label with the
|
|
@@ -161,7 +167,7 @@ export const itemSelector = (id, index) => idSelector(`${id}-item-${index}`);
|
|
|
161
167
|
export const itemId = (id, index) => `${id}-item-${index}`;
|
|
162
168
|
// HELPERS
|
|
163
169
|
const constrainedEvo = makeConstrainedEvo();
|
|
164
|
-
/** Resets only shared base fields to their closed state. Does not touch inputValue
|
|
170
|
+
/** Resets only shared base fields to their closed state. Does not touch inputValue. That is variant-specific. */
|
|
165
171
|
export const closedBaseModel = (model) => constrainedEvo(model, {
|
|
166
172
|
isOpen: () => false,
|
|
167
173
|
maybeActiveItemIndex: () => Option.none(),
|
|
@@ -229,13 +235,17 @@ export const makeUpdate = (handlers) => {
|
|
|
229
235
|
Option.none(),
|
|
230
236
|
];
|
|
231
237
|
};
|
|
232
|
-
const closeCombobox = (baseModel, commands,
|
|
233
|
-
const closed = handlers.handleClose(baseModel);
|
|
238
|
+
const closeCombobox = (baseModel, commands, restingInputValue) => {
|
|
239
|
+
const [closed, maybeCloseOutMessage] = handlers.handleClose(baseModel, restingInputValue);
|
|
234
240
|
if (model.isAnimated) {
|
|
235
241
|
const [nextModel, animationCommands] = delegateToAnimation(closed, AnimationHid());
|
|
236
|
-
return [
|
|
242
|
+
return [
|
|
243
|
+
nextModel,
|
|
244
|
+
[...commands, ...animationCommands],
|
|
245
|
+
maybeCloseOutMessage,
|
|
246
|
+
];
|
|
237
247
|
}
|
|
238
|
-
return [closed, commands,
|
|
248
|
+
return [closed, commands, maybeCloseOutMessage];
|
|
239
249
|
};
|
|
240
250
|
return M.value(message).pipe(withUpdateReturn, M.tag('CompletedLockScroll', 'CompletedUnlockScroll', 'CompletedInertOthers', 'CompletedRestoreInert', 'CompletedFocusInput', 'CompletedScrollIntoView', 'CompletedClickItem', 'CompletedAnchorCombobox', 'CompletedAttachComboboxPreventBlur', 'CompletedAttachComboboxSelectOnFocus', 'CompletedPortalComboboxBackdrop', () => [model, [], Option.none()]), M.tagsExhaustive({
|
|
241
251
|
Opened: ({ maybeActiveItemIndex }) => openCombobox(constrainedEvo(model, {
|
|
@@ -246,23 +256,32 @@ export const makeUpdate = (handlers) => {
|
|
|
246
256
|
}),
|
|
247
257
|
maybeLastPointerPosition: () => Option.none(),
|
|
248
258
|
})),
|
|
249
|
-
|
|
250
|
-
|
|
259
|
+
// NOTE: guard on isOpen so a close-path message baked from a stale
|
|
260
|
+
// render (a sub-frame blur after a selection already closed the
|
|
261
|
+
// combobox) cannot rewrite inputValue with an outdated
|
|
262
|
+
// restingInputValue or re-emit ClearedSelection while closed.
|
|
263
|
+
Closed: ({ restingInputValue }) => model.isOpen
|
|
264
|
+
? closeCombobox(model, [focusInput], restingInputValue)
|
|
265
|
+
: [model, [], Option.none()],
|
|
266
|
+
BlurredInput: ({ restingInputValue }) => model.isOpen
|
|
267
|
+
? closeCombobox(model, [], restingInputValue)
|
|
268
|
+
: [model, [], Option.none()],
|
|
251
269
|
ActivatedItem: ({ index, activationTrigger, maybeImmediateSelection, }) => {
|
|
252
270
|
const highlightedModel = constrainedEvo(model, {
|
|
253
271
|
maybeActiveItemIndex: () => Option.some(index),
|
|
254
272
|
activationTrigger: () => activationTrigger,
|
|
255
273
|
});
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
274
|
+
const skippedActivation = [highlightedModel, Option.none()];
|
|
275
|
+
const [nextModel, maybeOutMessage] = Option.match(maybeImmediateSelection, {
|
|
276
|
+
onNone: () => skippedActivation,
|
|
277
|
+
onSome: ({ item }) => handlers.handleImmediateActivation(highlightedModel, item),
|
|
259
278
|
});
|
|
260
279
|
return [
|
|
261
280
|
nextModel,
|
|
262
281
|
activationTrigger === 'Keyboard'
|
|
263
282
|
? [ScrollIntoView({ id: model.id, index })]
|
|
264
283
|
: [],
|
|
265
|
-
|
|
284
|
+
maybeOutMessage,
|
|
266
285
|
];
|
|
267
286
|
},
|
|
268
287
|
MovedPointerOverItem: ({ index, screenX, screenY }) => {
|
|
@@ -289,8 +308,8 @@ export const makeUpdate = (handlers) => {
|
|
|
289
308
|
Option.none(),
|
|
290
309
|
]
|
|
291
310
|
: [model, [], Option.none()],
|
|
292
|
-
SelectedItem: ({ item, displayText }) => {
|
|
293
|
-
const [nextModel, commands, maybeOutMessage] = handlers.handleSelectedItem(model, item, displayText, {
|
|
311
|
+
SelectedItem: ({ item, displayText, wasSelected }) => {
|
|
312
|
+
const [nextModel, commands, maybeOutMessage] = handlers.handleSelectedItem(model, item, displayText, wasSelected, {
|
|
294
313
|
focusInput,
|
|
295
314
|
maybeUnlockScroll,
|
|
296
315
|
maybeRestoreInert,
|
|
@@ -329,9 +348,9 @@ export const makeUpdate = (handlers) => {
|
|
|
329
348
|
maybeLastPointerPosition: () => Option.none(),
|
|
330
349
|
}));
|
|
331
350
|
},
|
|
332
|
-
PressedToggleButton: () => {
|
|
351
|
+
PressedToggleButton: ({ restingInputValue }) => {
|
|
333
352
|
if (model.isOpen) {
|
|
334
|
-
return closeCombobox(model, [focusInput]);
|
|
353
|
+
return closeCombobox(model, [focusInput], restingInputValue);
|
|
335
354
|
}
|
|
336
355
|
const [nextModel, commands] = openCombobox(constrainedEvo(model, {
|
|
337
356
|
maybeActiveItemIndex: () => Option.none(),
|
|
@@ -413,12 +432,12 @@ export const PortalComboboxBackdrop = Mount.define('PortalComboboxBackdrop', Com
|
|
|
413
432
|
yield* Effect.acquireRelease(Effect.sync(() => portalToContainingRoot(element)), cleanup => Effect.sync(cleanup));
|
|
414
433
|
return CompletedPortalComboboxBackdrop();
|
|
415
434
|
}));
|
|
416
|
-
/** Creates a combobox view function from variant-specific behavior. Shared rendering logic (input, items, transitions, keyboard navigation) is handled internally; only
|
|
435
|
+
/** Creates a combobox view function from variant-specific behavior. Shared rendering logic (input, items, transitions, keyboard navigation) is handled internally; only listbox multi-select semantics vary by variant. */
|
|
417
436
|
export const makeView = (behavior) => {
|
|
418
437
|
const impl = defineView((model, viewInputs) => {
|
|
419
438
|
const h = html();
|
|
420
439
|
const { id, isOpen, immediate, animation: { transitionState }, maybeActiveItemIndex, } = model;
|
|
421
|
-
const { items, itemToConfig, itemToValue, itemToDisplayText, isItemDisabled, inputClassName, inputAttributes = [], inputPlaceholder, inputWrapperClassName, inputWrapperAttributes = [], itemsClassName, itemsAttributes = [], itemsScrollClassName, itemsScrollAttributes = [], backdropClassName, backdropAttributes = [], className, attributes = [], buttonContent, buttonClassName, buttonAttributes = [], formName, isDisabled, isInvalid, openOnFocus, itemGroupKey, groupToHeading, groupClassName, groupAttributes = [], separatorClassName, separatorAttributes = [], anchor = {}, ariaLabel, ariaLabelledBy, } = viewInputs;
|
|
440
|
+
const { items, selectedValues, restingInputValue, itemToConfig, itemToValue, itemToDisplayText, isItemDisabled, inputClassName, inputAttributes = [], inputPlaceholder, inputWrapperClassName, inputWrapperAttributes = [], itemsClassName, itemsAttributes = [], itemsScrollClassName, itemsScrollAttributes = [], backdropClassName, backdropAttributes = [], className, attributes = [], buttonContent, buttonClassName, buttonAttributes = [], formName, isDisabled, isInvalid, openOnFocus, itemGroupKey, groupToHeading, groupClassName, groupAttributes = [], separatorClassName, separatorAttributes = [], anchor = {}, ariaLabel, ariaLabelledBy, } = viewInputs;
|
|
422
441
|
const resolveInputLabel = () => {
|
|
423
442
|
if (Predicate.isNotUndefined(ariaLabel)) {
|
|
424
443
|
return [h.AriaLabel(ariaLabel)];
|
|
@@ -431,6 +450,7 @@ export const makeView = (behavior) => {
|
|
|
431
450
|
}
|
|
432
451
|
};
|
|
433
452
|
const inputLabelAttributes = resolveInputLabel();
|
|
453
|
+
const isValueSelected = (itemValue) => Array.contains(selectedValues, itemValue);
|
|
434
454
|
const isLeaving = transitionState === 'LeaveStart' || transitionState === 'LeaveAnimating';
|
|
435
455
|
const isVisible = isOpen || isLeaving;
|
|
436
456
|
const animationAttributes = M.value(transitionState).pipe(M.when('EnterStart', () => [
|
|
@@ -453,12 +473,8 @@ export const makeView = (behavior) => {
|
|
|
453
473
|
const firstEnabledIndex = findFirstEnabledIndex(items.length, 0, isDisabledAtIndex)(0, 1);
|
|
454
474
|
const lastEnabledIndex = findFirstEnabledIndex(items.length, 0, isDisabledAtIndex)(items.length - 1, -1);
|
|
455
475
|
const resolveActiveIndex = keyToIndex('ArrowDown', 'ArrowUp', items.length, Option.getOrElse(maybeActiveItemIndex, () => -1), isDisabledAtIndex);
|
|
456
|
-
const resolveImmediateSelection = (targetIndex) => OptionExt.when(immediate,
|
|
457
|
-
|
|
458
|
-
onSome: targetItem => ({
|
|
459
|
-
item: itemToValue(targetItem, targetIndex),
|
|
460
|
-
displayText: itemToDisplayText(targetItem, targetIndex),
|
|
461
|
-
}),
|
|
476
|
+
const resolveImmediateSelection = (targetIndex) => pipe(OptionExt.when(immediate, targetIndex), Option.flatMap(index => Array.get(items, index)), Option.map(targetItem => ({
|
|
477
|
+
item: itemToValue(targetItem, targetIndex),
|
|
462
478
|
})));
|
|
463
479
|
const handleInputKeyDown = (key) => M.value(key).pipe(M.when('ArrowDown', () => {
|
|
464
480
|
if (!isOpen) {
|
|
@@ -493,7 +509,7 @@ export const makeView = (behavior) => {
|
|
|
493
509
|
if (!isOpen) {
|
|
494
510
|
return Option.none();
|
|
495
511
|
}
|
|
496
|
-
return Option.some(Closed());
|
|
512
|
+
return Option.some(Closed({ restingInputValue }));
|
|
497
513
|
}), M.whenOr('Home', 'End', () => {
|
|
498
514
|
if (!isOpen) {
|
|
499
515
|
return Option.none();
|
|
@@ -526,7 +542,7 @@ export const makeView = (behavior) => {
|
|
|
526
542
|
: [
|
|
527
543
|
h.OnInput(value => UpdatedInputValue({ value })),
|
|
528
544
|
h.OnKeyDownPreventDefault(handleInputKeyDown),
|
|
529
|
-
h.OnBlur(BlurredInput()),
|
|
545
|
+
h.OnBlur(BlurredInput({ restingInputValue })),
|
|
530
546
|
...(openOnFocus
|
|
531
547
|
? [h.OnFocus(Opened({ maybeActiveItemIndex: Option.none() }))]
|
|
532
548
|
: []),
|
|
@@ -566,7 +582,7 @@ export const makeView = (behavior) => {
|
|
|
566
582
|
const comboboxItems = Array.map(items, (item, index) => {
|
|
567
583
|
const isActiveItem = Option.exists(maybeActiveItemIndex, activeIndex => activeIndex === index);
|
|
568
584
|
const isDisabledItem = isDisabledAtIndex(index);
|
|
569
|
-
const isSelectedItem =
|
|
585
|
+
const isSelectedItem = isValueSelected(itemToValue(item, index));
|
|
570
586
|
const itemConfig = itemToConfig(item, {
|
|
571
587
|
isActive: isActiveItem,
|
|
572
588
|
isDisabled: isDisabledItem,
|
|
@@ -587,6 +603,7 @@ export const makeView = (behavior) => {
|
|
|
587
603
|
h.OnClick(SelectedItem({
|
|
588
604
|
item: itemToValue(item, index),
|
|
589
605
|
displayText: itemToDisplayText(item, index),
|
|
606
|
+
wasSelected: isValueSelected(itemToValue(item, index)),
|
|
590
607
|
})),
|
|
591
608
|
...(isActiveItem
|
|
592
609
|
? []
|
|
@@ -647,7 +664,7 @@ export const makeView = (behavior) => {
|
|
|
647
664
|
};
|
|
648
665
|
const backdrop = h.keyed('div')(`${id}-backdrop`, [
|
|
649
666
|
h.OnMount(PortalComboboxBackdrop()),
|
|
650
|
-
...(isLeaving ? [] : [h.OnClick(Closed())]),
|
|
667
|
+
...(isLeaving ? [] : [h.OnClick(Closed({ restingInputValue }))]),
|
|
651
668
|
...(backdropClassName ? [h.Class(backdropClassName)] : []),
|
|
652
669
|
...backdropAttributes,
|
|
653
670
|
], []);
|
|
@@ -683,17 +700,13 @@ export const makeView = (behavior) => {
|
|
|
683
700
|
h.Attribute('aria-haspopup', 'listbox'),
|
|
684
701
|
...(isDisabled
|
|
685
702
|
? [h.AriaDisabled(true), h.DataAttribute('disabled', '')]
|
|
686
|
-
: [h.OnClick(PressedToggleButton())]),
|
|
703
|
+
: [h.OnClick(PressedToggleButton({ restingInputValue }))]),
|
|
687
704
|
h.OnMount(AttachComboboxPreventBlur()),
|
|
688
705
|
...(buttonClassName ? [h.Class(buttonClassName)] : []),
|
|
689
706
|
...buttonAttributes,
|
|
690
707
|
], [buttonContent]),
|
|
691
708
|
]
|
|
692
709
|
: [];
|
|
693
|
-
const selectedValues = pipe(items, Array.filterMap((item, index) => {
|
|
694
|
-
const value = itemToValue(item, index);
|
|
695
|
-
return Result.fromOption(OptionExt.when(behavior.isItemSelected(model, value), value), () => undefined);
|
|
696
|
-
}));
|
|
697
710
|
const hiddenInputs = formName
|
|
698
711
|
? Array.match(selectedValues, {
|
|
699
712
|
onEmpty: () => [h.input([h.Type('hidden'), h.Name(formName)])],
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { Option, Schema as S } from 'effect';
|
|
2
2
|
import type * as Command from 'foldkit/command';
|
|
3
|
-
import
|
|
4
|
-
import { type BaseInitConfig, type
|
|
5
|
-
/** Schema for the single-select combobox
|
|
3
|
+
import { type View as SubmodelView } from 'foldkit/submodel';
|
|
4
|
+
import { type BaseInitConfig, type BaseViewInputsCommon, type Message, type OutMessage } from './shared.js';
|
|
5
|
+
/** Schema for the single-select combobox's private interaction state (open/closed status, active item, activation trigger, typed input value). The selection is owned by the parent and passed in via `ViewInputs.maybeSelectedValue`. */
|
|
6
6
|
export declare const Model: S.Struct<{
|
|
7
|
-
readonly maybeSelectedItem: S.Option<S.String>;
|
|
8
|
-
readonly maybeSelectedDisplayText: S.Option<S.String>;
|
|
9
7
|
readonly id: S.String;
|
|
10
8
|
readonly isOpen: S.Boolean;
|
|
11
9
|
readonly isAnimated: S.Boolean;
|
|
@@ -27,17 +25,12 @@ export declare const Model: S.Struct<{
|
|
|
27
25
|
}>>;
|
|
28
26
|
}>;
|
|
29
27
|
export type Model = typeof Model.Type;
|
|
30
|
-
/** Configuration for creating a single-select combobox model with `init`. `isAnimated` enables CSS transition coordination (default `false`). `isModal` locks page scroll and inerts other elements when open (default `false`).
|
|
31
|
-
export type InitConfig = BaseInitConfig
|
|
32
|
-
|
|
33
|
-
selectedDisplayText?: string;
|
|
34
|
-
}>;
|
|
35
|
-
/** Creates an initial single-select combobox model from a config. Defaults to closed with no active item, empty input, and no selection. */
|
|
28
|
+
/** Configuration for creating a single-select combobox model with `init`. `isAnimated` enables CSS transition coordination (default `false`). `isModal` locks page scroll and inerts other elements when open (default `false`). */
|
|
29
|
+
export type InitConfig = BaseInitConfig;
|
|
30
|
+
/** Creates an initial single-select combobox model from a config. Defaults to closed with no active item and an empty input. */
|
|
36
31
|
export declare const init: (config: InitConfig) => Model;
|
|
37
|
-
/** Processes a combobox message and returns the next model and
|
|
32
|
+
/** Processes a combobox message and returns the next model, commands, and optional OutMessage. Closes the combobox on selection (single-select behavior); emits `Selected({ value })` for the parent to store, and `ClearedSelection` when a nullable combobox closes with an empty input. */
|
|
38
33
|
export declare const update: (model: S.Struct.ReadonlySide<{
|
|
39
|
-
readonly maybeSelectedItem: S.Option<S.String>;
|
|
40
|
-
readonly maybeSelectedDisplayText: S.Option<S.String>;
|
|
41
34
|
readonly id: S.String;
|
|
42
35
|
readonly isOpen: S.Boolean;
|
|
43
36
|
readonly isAnimated: S.Boolean;
|
|
@@ -58,8 +51,6 @@ export declare const update: (model: S.Struct.ReadonlySide<{
|
|
|
58
51
|
readonly screenY: S.Number;
|
|
59
52
|
}>>;
|
|
60
53
|
}, "Type">, message: Message) => readonly [S.Struct.ReadonlySide<{
|
|
61
|
-
readonly maybeSelectedItem: S.Option<S.String>;
|
|
62
|
-
readonly maybeSelectedDisplayText: S.Option<S.String>;
|
|
63
54
|
readonly id: S.String;
|
|
64
55
|
readonly isOpen: S.Boolean;
|
|
65
56
|
readonly isAnimated: S.Boolean;
|
|
@@ -90,20 +81,21 @@ export declare const update: (model: S.Struct.ReadonlySide<{
|
|
|
90
81
|
readonly _tag: S.tag<"CompletedInertOthers">;
|
|
91
82
|
}, "Type"> | S.Struct.ReadonlySide<{
|
|
92
83
|
readonly _tag: S.tag<"CompletedRestoreInert">;
|
|
93
|
-
}, "Type"> | S.Struct.ReadonlySide<{
|
|
94
|
-
readonly _tag: S.tag<"Closed">;
|
|
95
84
|
}, "Type"> | S.Struct.ReadonlySide<{
|
|
96
85
|
readonly _tag: S.tag<"Opened">;
|
|
97
86
|
maybeActiveItemIndex: S.Option<S.Number>;
|
|
87
|
+
}, "Type"> | S.Struct.ReadonlySide<{
|
|
88
|
+
readonly _tag: S.tag<"Closed">;
|
|
89
|
+
restingInputValue: S.String;
|
|
98
90
|
}, "Type"> | S.Struct.ReadonlySide<{
|
|
99
91
|
readonly _tag: S.tag<"BlurredInput">;
|
|
92
|
+
restingInputValue: S.String;
|
|
100
93
|
}, "Type"> | S.Struct.ReadonlySide<{
|
|
101
94
|
readonly _tag: S.tag<"ActivatedItem">;
|
|
102
95
|
index: S.Number;
|
|
103
96
|
activationTrigger: S.Literals<readonly ["Pointer", "Keyboard"]>;
|
|
104
97
|
maybeImmediateSelection: S.Option<S.Struct<{
|
|
105
98
|
readonly item: S.String;
|
|
106
|
-
readonly displayText: S.String;
|
|
107
99
|
}>>;
|
|
108
100
|
}, "Type"> | S.Struct.ReadonlySide<{
|
|
109
101
|
readonly _tag: S.tag<"DeactivatedItem">;
|
|
@@ -111,6 +103,7 @@ export declare const update: (model: S.Struct.ReadonlySide<{
|
|
|
111
103
|
readonly _tag: S.tag<"SelectedItem">;
|
|
112
104
|
item: S.String;
|
|
113
105
|
displayText: S.String;
|
|
106
|
+
wasSelected: S.Boolean;
|
|
114
107
|
}, "Type"> | S.Struct.ReadonlySide<{
|
|
115
108
|
readonly _tag: S.tag<"MovedPointerOverItem">;
|
|
116
109
|
index: S.Number;
|
|
@@ -130,6 +123,7 @@ export declare const update: (model: S.Struct.ReadonlySide<{
|
|
|
130
123
|
value: S.String;
|
|
131
124
|
}, "Type"> | S.Struct.ReadonlySide<{
|
|
132
125
|
readonly _tag: S.tag<"PressedToggleButton">;
|
|
126
|
+
restingInputValue: S.String;
|
|
133
127
|
}, "Type"> | S.Struct.ReadonlySide<{
|
|
134
128
|
readonly _tag: S.tag<"CompletedAnchorCombobox">;
|
|
135
129
|
}, "Type"> | S.Struct.ReadonlySide<{
|
|
@@ -142,49 +136,43 @@ export declare const update: (model: S.Struct.ReadonlySide<{
|
|
|
142
136
|
readonly _tag: S.tag<"GotAnimationMessage">;
|
|
143
137
|
message: S.Union<[import("foldkit/schema").CallableTaggedStruct<"Showed", {}>, import("foldkit/schema").CallableTaggedStruct<"Hid", {}>, import("foldkit/schema").CallableTaggedStruct<"AdvancedAnimationFrame", {}>, import("foldkit/schema").CallableTaggedStruct<"EndedAnimation", {}>]>;
|
|
144
138
|
}, "Type">, never, never>;
|
|
145
|
-
}>[], Option.Option<
|
|
146
|
-
readonly _tag: "Selected";
|
|
147
|
-
readonly value: string;
|
|
148
|
-
readonly wasAdded: boolean;
|
|
149
|
-
}>>];
|
|
139
|
+
}>[], Option.Option<OutMessage<string>>];
|
|
150
140
|
type UpdateReturn = ReturnType<typeof update>;
|
|
151
141
|
/** Programmatically opens the combobox, updating the model and returning
|
|
152
142
|
* focus and modal commands. Use this in domain-event handlers to open the combobox. */
|
|
153
143
|
export declare const open: (model: Model) => UpdateReturn;
|
|
154
144
|
/** Programmatically closes the combobox, updating the model and returning
|
|
155
|
-
* focus and modal commands.
|
|
156
|
-
|
|
157
|
-
|
|
145
|
+
* focus and modal commands. `restingInputValue` is the text the input
|
|
146
|
+
* returns to (the parent-owned selection's display text, or empty). Use
|
|
147
|
+
* this in domain-event handlers to close the combobox. */
|
|
148
|
+
export declare const close: (model: Model, restingInputValue: string) => UpdateReturn;
|
|
149
|
+
/** Programmatically selects an item in the single-select combobox, closing
|
|
150
|
+
* the combobox and emitting `Selected({ value })`. The Submodel treats the
|
|
151
|
+
* select as fresh (`wasSelected: false`), so the input rests on
|
|
152
|
+
* `displayText`; what the selection becomes is the parent's fold to decide,
|
|
153
|
+
* and a parent that toggles on its own state will still deselect an
|
|
154
|
+
* already-selected value. */
|
|
158
155
|
export declare const selectItem: (model: Model, item: string, displayText: string) => UpdateReturn;
|
|
159
|
-
/** Reflects an externally-sourced selection onto the model without
|
|
160
|
-
* emitting an OutMessage or running selection side effects (no close, no
|
|
161
|
-
* focus). Sets the selected item, its display text, and the input text
|
|
162
|
-
* together, so the input shows the reflected selection. Pass
|
|
163
|
-
* `Option.none()` to clear. Use this to mirror external truth (a URL
|
|
164
|
-
* parameter, restored storage, a server push) onto the combobox.
|
|
165
|
-
* Contrast with `selectItem`, a user/programmatic *choice* that emits
|
|
166
|
-
* `Selected`. Returns the model directly because it produces no commands
|
|
167
|
-
* and no OutMessage. */
|
|
168
|
-
export declare const reflectSelectedItem: Reflect<Model, Option.Option<{
|
|
169
|
-
readonly item: string;
|
|
170
|
-
readonly displayText: string;
|
|
171
|
-
}>>;
|
|
172
156
|
/** Per-render view inputs passed to the view via `h.submodel`'s `viewInputs` field. */
|
|
173
|
-
export type ViewInputs<Item extends string> =
|
|
157
|
+
export type ViewInputs<Item extends string> = BaseViewInputsCommon<Item> & Readonly<{
|
|
158
|
+
/** The selection the parent owns, passed in fresh on every render.
|
|
159
|
+
* `Option` because a single-select combobox may have no selection
|
|
160
|
+
* yet. Marks the selected item and drives the hidden form input
|
|
161
|
+
* submitted under `formName`. */
|
|
162
|
+
maybeSelectedValue: Option.Option<Item>;
|
|
163
|
+
}>;
|
|
174
164
|
/** Pairs the single-select combobox's `view` and `update` (and programmatic
|
|
175
165
|
* helpers) behind a single Item-typed entry point. See `Listbox.create`
|
|
176
166
|
* for the rationale; the combobox factory follows the same shape with
|
|
177
|
-
* `selectItem` taking both `item` and `displayText`.
|
|
167
|
+
* `selectItem` taking both `item` and `displayText`. `selectItem` emits
|
|
168
|
+
* `Selected({ value })` with the input resting on `displayText`; what the
|
|
169
|
+
* selection becomes is the parent's fold to decide. */
|
|
178
170
|
export declare const create: <Item extends string = string>() => Readonly<{
|
|
179
|
-
view: SubmodelView<Model, Message,
|
|
171
|
+
view: SubmodelView<Model, Message, ViewInputs<Item>>;
|
|
180
172
|
update: (model: Model, message: Message) => readonly [Model, ReadonlyArray<Command.Command<Message>>, Option.Option<OutMessage<Item>>];
|
|
181
173
|
selectItem: (model: Model, item: Item, displayText: string) => readonly [Model, ReadonlyArray<Command.Command<Message>>, Option.Option<OutMessage<Item>>];
|
|
182
174
|
open: (model: Model) => readonly [Model, ReadonlyArray<Command.Command<Message>>, Option.Option<OutMessage<Item>>];
|
|
183
|
-
close: (model: Model) => readonly [Model, ReadonlyArray<Command.Command<Message>>, Option.Option<OutMessage<Item>>];
|
|
184
|
-
reflectSelectedItem: Reflect<Model, Option.Option<{
|
|
185
|
-
readonly item: Item;
|
|
186
|
-
readonly displayText: string;
|
|
187
|
-
}>>;
|
|
175
|
+
close: (model: Model, restingInputValue: string) => readonly [Model, ReadonlyArray<Command.Command<Message>>, Option.Option<OutMessage<Item>>];
|
|
188
176
|
}>;
|
|
189
177
|
export {};
|
|
190
178
|
//# sourceMappingURL=single.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"single.d.ts","sourceRoot":"","sources":["../../src/combobox/single.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"single.d.ts","sourceRoot":"","sources":["../../src/combobox/single.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,EAAE,MAAM,IAAI,CAAC,EAAQ,MAAM,QAAQ,CAAA;AACzD,OAAO,KAAK,KAAK,OAAO,MAAM,iBAAiB,CAAA;AAE/C,OAAO,EAAE,KAAK,IAAI,IAAI,YAAY,EAAc,MAAM,kBAAkB,CAAA;AAExE,OAAO,EACL,KAAK,cAAc,EAEnB,KAAK,oBAAoB,EAGzB,KAAK,OAAO,EAEZ,KAAK,UAAU,EAOhB,MAAM,aAAa,CAAA;AAIpB,0OAA0O;AAC1O,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;EAEhB,CAAA;AAEF,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAA;AAIrC,oOAAoO;AACpO,MAAM,MAAM,UAAU,GAAG,cAAc,CAAA;AAEvC,gIAAgI;AAChI,eAAO,MAAM,IAAI,GAAI,QAAQ,UAAU,KAAG,KAAyB,CAAA;AAInE,8RAA8R;AAC9R,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wCAkCjB,CAAA;AAEF,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAA;AAE7C;wFACwF;AACxF,eAAO,MAAM,IAAI,GAAI,OAAO,KAAK,KAAG,YAC4B,CAAA;AAEhE;;;2DAG2D;AAC3D,eAAO,MAAM,KAAK,GAAI,OAAO,KAAK,EAAE,mBAAmB,MAAM,KAAG,YAClB,CAAA;AAE9C;;;;;8BAK8B;AAC9B,eAAO,MAAM,UAAU,GACrB,OAAO,KAAK,EACZ,MAAM,MAAM,EACZ,aAAa,MAAM,KAClB,YACqE,CAAA;AAIxE,uFAAuF;AACvF,MAAM,MAAM,UAAU,CAAC,IAAI,SAAS,MAAM,IAAI,oBAAoB,CAAC,IAAI,CAAC,GACtE,QAAQ,CAAC;IACP;;;sCAGkC;IAClC,kBAAkB,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;CACxC,CAAC,CAAA;AAIJ;;;;;wDAKwD;AACxD,eAAO,MAAM,MAAM,GAAI,IAAI,SAAS,MAAM,GAAG,MAAM,OAAK,QAAQ,CAAC;IAC/D,IAAI,EAAE,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,UAAU,CAAC,IAAI,CAAC,CAAC,CAAA;IACpD,MAAM,EAAE,CACN,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,OAAO,KACb,SAAS,CACZ,KAAK,EACL,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,EACvC,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAChC,CAAA;IACD,UAAU,EAAE,CACV,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,IAAI,EACV,WAAW,EAAE,MAAM,KAChB,SAAS,CACZ,KAAK,EACL,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,EACvC,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAChC,CAAA;IACD,IAAI,EAAE,CACJ,KAAK,EAAE,KAAK,KACT,SAAS,CACZ,KAAK,EACL,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,EACvC,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAChC,CAAA;IACD,KAAK,EAAE,CACL,KAAK,EAAE,KAAK,EACZ,iBAAiB,EAAE,MAAM,KACtB,SAAS,CACZ,KAAK,EACL,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,EACvC,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAChC,CAAA;CACF,CA6BA,CAAA"}
|
package/dist/combobox/single.js
CHANGED
|
@@ -1,106 +1,79 @@
|
|
|
1
|
-
import { Array,
|
|
1
|
+
import { Array, Option, Schema as S, pipe } from 'effect';
|
|
2
2
|
import { evo } from 'foldkit/struct';
|
|
3
|
-
import {
|
|
3
|
+
import { defineView } from 'foldkit/submodel';
|
|
4
|
+
import { BaseModel, ClearedSelection, Closed, Opened, SelectedItem, Selected as SharedSelected, baseInit, closedBaseModel, makeUpdate, makeView, } from './shared.js';
|
|
4
5
|
// MODEL
|
|
5
|
-
/** Schema for the single-select combobox
|
|
6
|
+
/** Schema for the single-select combobox's private interaction state (open/closed status, active item, activation trigger, typed input value). The selection is owned by the parent and passed in via `ViewInputs.maybeSelectedValue`. */
|
|
6
7
|
export const Model = S.Struct({
|
|
7
8
|
...BaseModel.fields,
|
|
8
|
-
maybeSelectedItem: S.Option(S.String),
|
|
9
|
-
maybeSelectedDisplayText: S.Option(S.String),
|
|
10
|
-
});
|
|
11
|
-
/** Creates an initial single-select combobox model from a config. Defaults to closed with no active item, empty input, and no selection. */
|
|
12
|
-
export const init = (config) => ({
|
|
13
|
-
...baseInit(config),
|
|
14
|
-
maybeSelectedItem: Option.fromNullishOr(config.selectedItem),
|
|
15
|
-
maybeSelectedDisplayText: Option.fromNullishOr(config.selectedDisplayText ?? config.selectedItem),
|
|
16
9
|
});
|
|
10
|
+
/** Creates an initial single-select combobox model from a config. Defaults to closed with no active item and an empty input. */
|
|
11
|
+
export const init = (config) => baseInit(config);
|
|
17
12
|
// UPDATE
|
|
18
|
-
/** Processes a combobox message and returns the next model and
|
|
13
|
+
/** Processes a combobox message and returns the next model, commands, and optional OutMessage. Closes the combobox on selection (single-select behavior); emits `Selected({ value })` for the parent to store, and `ClearedSelection` when a nullable combobox closes with an empty input. */
|
|
19
14
|
export const update = makeUpdate({
|
|
20
|
-
handleClose: model => {
|
|
15
|
+
handleClose: (model, restingInputValue) => {
|
|
21
16
|
if (model.nullable && model.inputValue === '') {
|
|
22
|
-
return
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
});
|
|
17
|
+
return [
|
|
18
|
+
evo(closedBaseModel(model), { inputValue: () => '' }),
|
|
19
|
+
Option.some(ClearedSelection()),
|
|
20
|
+
];
|
|
27
21
|
}
|
|
28
|
-
return
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
return [
|
|
23
|
+
evo(closedBaseModel(model), { inputValue: () => restingInputValue }),
|
|
24
|
+
Option.none(),
|
|
25
|
+
];
|
|
31
26
|
},
|
|
32
|
-
handleSelectedItem: (model, item, displayText, context) => {
|
|
33
|
-
const
|
|
34
|
-
const nullableDeselect = model.nullable && isAlreadySelected;
|
|
35
|
-
const nextModel = nullableDeselect
|
|
36
|
-
? evo(closedBaseModel(model), {
|
|
37
|
-
inputValue: () => '',
|
|
38
|
-
maybeSelectedItem: () => Option.none(),
|
|
39
|
-
maybeSelectedDisplayText: () => Option.none(),
|
|
40
|
-
})
|
|
41
|
-
: evo(closedBaseModel(model), {
|
|
42
|
-
inputValue: () => displayText,
|
|
43
|
-
maybeSelectedItem: () => Option.some(item),
|
|
44
|
-
maybeSelectedDisplayText: () => Option.some(displayText),
|
|
45
|
-
});
|
|
27
|
+
handleSelectedItem: (model, item, displayText, wasSelected, context) => {
|
|
28
|
+
const nullableDeselect = model.nullable && wasSelected;
|
|
46
29
|
return [
|
|
47
|
-
|
|
30
|
+
evo(closedBaseModel(model), {
|
|
31
|
+
inputValue: () => (nullableDeselect ? '' : displayText),
|
|
32
|
+
}),
|
|
48
33
|
pipe(Array.getSomes([context.maybeUnlockScroll, context.maybeRestoreInert]), Array.prepend(context.focusInput)),
|
|
49
|
-
Option.some(SharedSelected({ value: item
|
|
34
|
+
Option.some(SharedSelected({ value: item })),
|
|
50
35
|
];
|
|
51
36
|
},
|
|
52
|
-
handleImmediateActivation: (model, item
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
37
|
+
handleImmediateActivation: (model, item) => [
|
|
38
|
+
model,
|
|
39
|
+
Option.some(SharedSelected({ value: item })),
|
|
40
|
+
],
|
|
56
41
|
});
|
|
57
42
|
/** Programmatically opens the combobox, updating the model and returning
|
|
58
43
|
* focus and modal commands. Use this in domain-event handlers to open the combobox. */
|
|
59
44
|
export const open = (model) => update(model, Opened({ maybeActiveItemIndex: Option.none() }));
|
|
60
45
|
/** Programmatically closes the combobox, updating the model and returning
|
|
61
|
-
* focus and modal commands.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
export const
|
|
65
|
-
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
* and no OutMessage. */
|
|
74
|
-
export const reflectSelectedItem = Function.dual(2, (model, maybeSelection) => Option.match(maybeSelection, {
|
|
75
|
-
onNone: () => evo(model, {
|
|
76
|
-
maybeSelectedItem: () => Option.none(),
|
|
77
|
-
maybeSelectedDisplayText: () => Option.none(),
|
|
78
|
-
inputValue: () => '',
|
|
79
|
-
}),
|
|
80
|
-
onSome: ({ item, displayText }) => evo(model, {
|
|
81
|
-
maybeSelectedItem: () => Option.some(item),
|
|
82
|
-
maybeSelectedDisplayText: () => Option.some(displayText),
|
|
83
|
-
inputValue: () => displayText,
|
|
84
|
-
}),
|
|
85
|
-
}));
|
|
86
|
-
const internalView = makeView({
|
|
87
|
-
isItemSelected: (model, itemValue) => Option.exists(model.maybeSelectedItem, selectedItem => selectedItem === itemValue),
|
|
88
|
-
ariaMultiSelectable: false,
|
|
89
|
-
});
|
|
46
|
+
* focus and modal commands. `restingInputValue` is the text the input
|
|
47
|
+
* returns to (the parent-owned selection's display text, or empty). Use
|
|
48
|
+
* this in domain-event handlers to close the combobox. */
|
|
49
|
+
export const close = (model, restingInputValue) => update(model, Closed({ restingInputValue }));
|
|
50
|
+
/** Programmatically selects an item in the single-select combobox, closing
|
|
51
|
+
* the combobox and emitting `Selected({ value })`. The Submodel treats the
|
|
52
|
+
* select as fresh (`wasSelected: false`), so the input rests on
|
|
53
|
+
* `displayText`; what the selection becomes is the parent's fold to decide,
|
|
54
|
+
* and a parent that toggles on its own state will still deselect an
|
|
55
|
+
* already-selected value. */
|
|
56
|
+
export const selectItem = (model, item, displayText) => update(model, SelectedItem({ item, displayText, wasSelected: false }));
|
|
57
|
+
const internalView = makeView({ ariaMultiSelectable: false });
|
|
90
58
|
/** Pairs the single-select combobox's `view` and `update` (and programmatic
|
|
91
59
|
* helpers) behind a single Item-typed entry point. See `Listbox.create`
|
|
92
60
|
* for the rationale; the combobox factory follows the same shape with
|
|
93
|
-
* `selectItem` taking both `item` and `displayText`.
|
|
61
|
+
* `selectItem` taking both `item` and `displayText`. `selectItem` emits
|
|
62
|
+
* `Selected({ value })` with the input resting on `displayText`; what the
|
|
63
|
+
* selection becomes is the parent's fold to decide. */
|
|
94
64
|
export const create = () => {
|
|
95
65
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
96
66
|
const typedUpdate = update;
|
|
67
|
+
const arrayBasedView = internalView();
|
|
68
|
+
const view = defineView((model, { maybeSelectedValue, ...baseInputs }) => arrayBasedView(model, {
|
|
69
|
+
...baseInputs,
|
|
70
|
+
selectedValues: Option.toArray(maybeSelectedValue),
|
|
71
|
+
}));
|
|
97
72
|
return {
|
|
98
|
-
view
|
|
73
|
+
view,
|
|
99
74
|
update: typedUpdate,
|
|
100
|
-
selectItem: (model, item, displayText) => typedUpdate(model, SelectedItem({ item, displayText })),
|
|
75
|
+
selectItem: (model, item, displayText) => typedUpdate(model, SelectedItem({ item, displayText, wasSelected: false })),
|
|
101
76
|
open: model => typedUpdate(model, Opened({ maybeActiveItemIndex: Option.none() })),
|
|
102
|
-
close: model => typedUpdate(model, Closed()),
|
|
103
|
-
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
|
|
104
|
-
reflectSelectedItem: reflectSelectedItem,
|
|
77
|
+
close: (model, restingInputValue) => typedUpdate(model, Closed({ restingInputValue })),
|
|
105
78
|
};
|
|
106
79
|
};
|