@foldkit/ui 0.127.0 → 0.128.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.
Files changed (79) hide show
  1. package/README.md +2 -2
  2. package/dist/calendar/index.d.ts +17 -23
  3. package/dist/calendar/index.d.ts.map +1 -1
  4. package/dist/calendar/index.js +16 -25
  5. package/dist/calendar/public.d.ts +1 -1
  6. package/dist/calendar/public.d.ts.map +1 -1
  7. package/dist/calendar/public.js +1 -1
  8. package/dist/checkbox/index.d.ts +47 -104
  9. package/dist/checkbox/index.d.ts.map +1 -1
  10. package/dist/checkbox/index.js +48 -87
  11. package/dist/checkbox/public.d.ts +2 -2
  12. package/dist/checkbox/public.d.ts.map +1 -1
  13. package/dist/checkbox/public.js +1 -1
  14. package/dist/combobox/multi.d.ts +19 -30
  15. package/dist/combobox/multi.d.ts.map +1 -1
  16. package/dist/combobox/multi.js +32 -53
  17. package/dist/combobox/public.d.ts +2 -2
  18. package/dist/combobox/public.d.ts.map +1 -1
  19. package/dist/combobox/public.js +1 -1
  20. package/dist/combobox/shared.d.ts +59 -35
  21. package/dist/combobox/shared.d.ts.map +1 -1
  22. package/dist/combobox/shared.js +58 -45
  23. package/dist/combobox/single.d.ts +36 -48
  24. package/dist/combobox/single.d.ts.map +1 -1
  25. package/dist/combobox/single.js +49 -76
  26. package/dist/datePicker/index.d.ts +33 -41
  27. package/dist/datePicker/index.d.ts.map +1 -1
  28. package/dist/datePicker/index.js +31 -40
  29. package/dist/datePicker/public.d.ts +1 -1
  30. package/dist/datePicker/public.d.ts.map +1 -1
  31. package/dist/datePicker/public.js +1 -1
  32. package/dist/disclosure/index.d.ts +49 -113
  33. package/dist/disclosure/index.d.ts.map +1 -1
  34. package/dist/disclosure/index.js +35 -92
  35. package/dist/disclosure/public.d.ts +2 -2
  36. package/dist/disclosure/public.d.ts.map +1 -1
  37. package/dist/disclosure/public.js +1 -1
  38. package/dist/listbox/multi.d.ts +7 -21
  39. package/dist/listbox/multi.d.ts.map +1 -1
  40. package/dist/listbox/multi.js +12 -36
  41. package/dist/listbox/public.d.ts +1 -1
  42. package/dist/listbox/public.d.ts.map +1 -1
  43. package/dist/listbox/shared.d.ts +28 -20
  44. package/dist/listbox/shared.d.ts.map +1 -1
  45. package/dist/listbox/shared.js +6 -7
  46. package/dist/listbox/single.d.ts +17 -27
  47. package/dist/listbox/single.d.ts.map +1 -1
  48. package/dist/listbox/single.js +18 -30
  49. package/dist/radioGroup/index.d.ts +57 -142
  50. package/dist/radioGroup/index.d.ts.map +1 -1
  51. package/dist/radioGroup/index.js +44 -119
  52. package/dist/radioGroup/public.d.ts +2 -2
  53. package/dist/radioGroup/public.d.ts.map +1 -1
  54. package/dist/radioGroup/public.js +1 -1
  55. package/dist/slider/index.d.ts +51 -27
  56. package/dist/slider/index.d.ts.map +1 -1
  57. package/dist/slider/index.js +66 -66
  58. package/dist/slider/public.d.ts +1 -1
  59. package/dist/slider/public.d.ts.map +1 -1
  60. package/dist/slider/public.js +1 -1
  61. package/dist/switch/index.d.ts +45 -92
  62. package/dist/switch/index.d.ts.map +1 -1
  63. package/dist/switch/index.js +36 -81
  64. package/dist/switch/public.d.ts +2 -2
  65. package/dist/switch/public.d.ts.map +1 -1
  66. package/dist/switch/public.js +1 -1
  67. package/dist/tabs/index.d.ts +33 -33
  68. package/dist/tabs/index.d.ts.map +1 -1
  69. package/dist/tabs/index.js +36 -49
  70. package/dist/toast/index.d.ts +1 -0
  71. package/dist/toast/index.d.ts.map +1 -1
  72. package/dist/toast/index.js +1 -0
  73. package/dist/toast/public.d.ts +2 -0
  74. package/dist/toast/public.d.ts.map +1 -1
  75. package/dist/toast/public.js +1 -0
  76. package/dist/toast/test.d.ts +41 -0
  77. package/dist/toast/test.d.ts.map +1 -0
  78. package/dist/toast/test.js +36 -0
  79. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- import { Array, Effect, Match as M, Option, Predicate, Result, Schema as S, pipe, } from 'effect';
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, displayText: 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. Includes display text for restoring input value on close. */
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 a single-select combobox commits a selection, or when a multi-select combobox toggles an item on. The `value` is the string key; consumers that need a richer domain type should look it up from their own state or, in the multi case, branch on `wasAdded` to distinguish add vs remove. */
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
- /** Union of out-messages the combobox component can produce. Single-select comboboxes always emit `wasAdded: true`. Multi-select comboboxes emit `wasAdded: true` when adding to the selection and `wasAdded: false` when toggling off. */
149
- export const OutMessage = S.Union([Selected]);
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 or selection. Those are variant-specific. */
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, maybeOutMessage = Option.none()) => {
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 [nextModel, [...commands, ...animationCommands], maybeOutMessage];
242
+ return [
243
+ nextModel,
244
+ [...commands, ...animationCommands],
245
+ maybeCloseOutMessage,
246
+ ];
237
247
  }
238
- return [closed, commands, maybeOutMessage];
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
- Closed: () => closeCombobox(model, [focusInput]),
250
- BlurredInput: () => closeCombobox(model, []),
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 nextModel = Option.match(maybeImmediateSelection, {
257
- onNone: () => highlightedModel,
258
- onSome: ({ item, displayText }) => handlers.handleImmediateActivation(highlightedModel, item, displayText),
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
- Option.none(),
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 selection display varies by variant. */
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, pipe(items, Array.get(targetIndex), Option.match({
457
- onNone: () => ({ item: '', displayText: '' }),
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 = behavior.isItemSelected(model, itemToValue(item, index));
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 type { Reflect, View as SubmodelView } from 'foldkit/submodel';
4
- import { type BaseInitConfig, type BaseViewInputs, type Message, type OutMessage } from './shared.js';
5
- /** Schema for the single-select combobox component's state, tracking open/closed status, active item, input value, selected item, and display text. */
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`). `selectedItem` sets the initial selection (default none). */
31
- export type InitConfig = BaseInitConfig & Readonly<{
32
- selectedItem?: string;
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 commands. Closes the combobox on selection (single-select behavior). */
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<Readonly<{
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. Use this in domain-event handlers to close the combobox. */
156
- export declare const close: (model: Model) => UpdateReturn;
157
- /** Programmatically selects an item in the single-select combobox. Emits `Selected({ value, wasAdded })`. */
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> = BaseViewInputs<Item>;
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, BaseViewInputs<Item>>;
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,EAAmB,MAAM,EAAE,MAAM,IAAI,CAAC,EAAQ,MAAM,QAAQ,CAAA;AACnE,OAAO,KAAK,KAAK,OAAO,MAAM,iBAAiB,CAAA;AAE/C,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,IAAI,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAErE,OAAO,EACL,KAAK,cAAc,EAEnB,KAAK,cAAc,EAEnB,KAAK,OAAO,EAEZ,KAAK,UAAU,EAOhB,MAAM,aAAa,CAAA;AAIpB,uJAAuJ;AACvJ,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;EAIhB,CAAA;AAEF,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAA;AAIrC,8RAA8R;AAC9R,MAAM,MAAM,UAAU,GAAG,cAAc,GACrC,QAAQ,CAAC;IACP,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAC7B,CAAC,CAAA;AAEJ,4IAA4I;AAC5I,eAAO,MAAM,IAAI,GAAI,QAAQ,UAAU,KAAG,KAMxC,CAAA;AAIF,uIAAuI;AACvI,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAkDjB,CAAA;AAEF,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAA;AAE7C;wFACwF;AACxF,eAAO,MAAM,IAAI,GAAI,OAAO,KAAK,KAAG,YAC4B,CAAA;AAEhE;yFACyF;AACzF,eAAO,MAAM,KAAK,GAAI,OAAO,KAAK,KAAG,YAAuC,CAAA;AAE5E,6GAA6G;AAC7G,eAAO,MAAM,UAAU,GACrB,OAAO,KAAK,EACZ,MAAM,MAAM,EACZ,aAAa,MAAM,KAClB,YAAkE,CAAA;AAErE;;;;;;;;yBAQyB;AACzB,eAAO,MAAM,mBAAmB,EAAE,OAAO,CACvC,KAAK,EACL,MAAM,CAAC,MAAM,CAAC;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC,CAwBvE,CAAA;AAID,uFAAuF;AACvF,MAAM,MAAM,UAAU,CAAC,IAAI,SAAS,MAAM,IAAI,cAAc,CAAC,IAAI,CAAC,CAAA;AAWlE;;;yDAGyD;AACzD,eAAO,MAAM,MAAM,GAAI,IAAI,SAAS,MAAM,GAAG,MAAM,OAAK,QAAQ,CAAC;IAC/D,IAAI,EAAE,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,cAAc,CAAC,IAAI,CAAC,CAAC,CAAA;IACxD,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,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,mBAAmB,EAAE,OAAO,CAC1B,KAAK,EACL,MAAM,CAAC,MAAM,CAAC;QAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;QAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC,CACrE,CAAA;CACF,CAsBA,CAAA"}
1
+ {"version":3,"file":"single.d.ts","sourceRoot":"","sources":["../../src/combobox/single.ts"],"names":[],"mappings":"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"}
@@ -1,106 +1,79 @@
1
- import { Array, Function, Option, Schema as S, pipe } from 'effect';
1
+ import { Array, Option, Schema as S, pipe } from 'effect';
2
2
  import { evo } from 'foldkit/struct';
3
- import { BaseModel, Closed, Opened, SelectedItem, Selected as SharedSelected, baseInit, closedBaseModel, makeUpdate, makeView, } from './shared.js';
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 component's state, tracking open/closed status, active item, input value, selected item, and display text. */
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 commands. Closes the combobox on selection (single-select behavior). */
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 evo(closedBaseModel(model), {
23
- maybeSelectedItem: () => Option.none(),
24
- maybeSelectedDisplayText: () => Option.none(),
25
- inputValue: () => '',
26
- });
17
+ return [
18
+ evo(closedBaseModel(model), { inputValue: () => '' }),
19
+ Option.some(ClearedSelection()),
20
+ ];
27
21
  }
28
- return evo(closedBaseModel(model), {
29
- inputValue: () => Option.getOrElse(model.maybeSelectedDisplayText, () => ''),
30
- });
22
+ return [
23
+ evo(closedBaseModel(model), { inputValue: () => restingInputValue }),
24
+ Option.none(),
25
+ ];
31
26
  },
32
- handleSelectedItem: (model, item, displayText, context) => {
33
- const isAlreadySelected = Option.exists(model.maybeSelectedItem, selectedItem => selectedItem === item);
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
- nextModel,
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, wasAdded: !nullableDeselect })),
34
+ Option.some(SharedSelected({ value: item })),
50
35
  ];
51
36
  },
52
- handleImmediateActivation: (model, item, displayText) => evo(model, {
53
- maybeSelectedItem: () => Option.some(item),
54
- maybeSelectedDisplayText: () => Option.some(displayText),
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. Use this in domain-event handlers to close the combobox. */
62
- export const close = (model) => update(model, Closed());
63
- /** Programmatically selects an item in the single-select combobox. Emits `Selected({ value, wasAdded })`. */
64
- export const selectItem = (model, item, displayText) => update(model, SelectedItem({ item, displayText }));
65
- /** Reflects an externally-sourced selection onto the model without
66
- * emitting an OutMessage or running selection side effects (no close, no
67
- * focus). Sets the selected item, its display text, and the input text
68
- * together, so the input shows the reflected selection. Pass
69
- * `Option.none()` to clear. Use this to mirror external truth (a URL
70
- * parameter, restored storage, a server push) onto the combobox.
71
- * Contrast with `selectItem`, a user/programmatic *choice* that emits
72
- * `Selected`. Returns the model directly because it produces no commands
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: internalView(),
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
  };