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.
Files changed (208) hide show
  1. package/README.md +2 -1
  2. package/dist/canvas/view.d.ts +1 -1
  3. package/dist/canvas/view.d.ts.map +1 -1
  4. package/dist/canvas/view.js +5 -5
  5. package/dist/command/index.d.ts +71 -0
  6. package/dist/command/index.d.ts.map +1 -1
  7. package/dist/command/index.js +34 -1
  8. package/dist/command/public.d.ts +1 -1
  9. package/dist/command/public.d.ts.map +1 -1
  10. package/dist/command/public.js +1 -1
  11. package/dist/devTools/overlay.d.ts.map +1 -1
  12. package/dist/devTools/overlay.js +137 -110
  13. package/dist/dom/dom.d.ts +8 -11
  14. package/dist/dom/dom.d.ts.map +1 -1
  15. package/dist/dom/dom.js +8 -11
  16. package/dist/dom/elementMovement.d.ts +1 -3
  17. package/dist/dom/elementMovement.d.ts.map +1 -1
  18. package/dist/dom/elementMovement.js +1 -3
  19. package/dist/dom/inert.d.ts +2 -4
  20. package/dist/dom/inert.d.ts.map +1 -1
  21. package/dist/dom/inert.js +2 -4
  22. package/dist/dom/scrollLock.d.ts +2 -2
  23. package/dist/dom/scrollLock.js +2 -2
  24. package/dist/dom/waitForAnimation.d.ts +1 -1
  25. package/dist/dom/waitForAnimation.js +1 -1
  26. package/dist/html/boundary.d.ts +98 -0
  27. package/dist/html/boundary.d.ts.map +1 -0
  28. package/dist/html/boundary.js +176 -0
  29. package/dist/html/childAttribute.d.ts +44 -0
  30. package/dist/html/childAttribute.d.ts.map +1 -0
  31. package/dist/html/childAttribute.js +34 -0
  32. package/dist/html/index.d.ts +70 -23
  33. package/dist/html/index.d.ts.map +1 -1
  34. package/dist/html/index.js +639 -575
  35. package/dist/html/lazy.d.ts +12 -7
  36. package/dist/html/lazy.d.ts.map +1 -1
  37. package/dist/html/lazy.js +30 -11
  38. package/dist/html/public.d.ts +2 -2
  39. package/dist/html/public.d.ts.map +1 -1
  40. package/dist/html/public.js +1 -1
  41. package/dist/html/runtimeSingleton.d.ts +72 -0
  42. package/dist/html/runtimeSingleton.d.ts.map +1 -0
  43. package/dist/html/runtimeSingleton.js +112 -0
  44. package/dist/html/submodel.d.ts +98 -0
  45. package/dist/html/submodel.d.ts.map +1 -0
  46. package/dist/html/submodel.js +190 -0
  47. package/dist/index.d.ts +1 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +1 -0
  50. package/dist/render/render.d.ts +1 -1
  51. package/dist/render/render.js +1 -1
  52. package/dist/runtime/messagePriority.d.ts +5 -1
  53. package/dist/runtime/messagePriority.d.ts.map +1 -1
  54. package/dist/runtime/messagePriority.js +25 -4
  55. package/dist/runtime/runtime.d.ts +1 -1
  56. package/dist/runtime/runtime.d.ts.map +1 -1
  57. package/dist/runtime/runtime.js +115 -61
  58. package/dist/submodel/public.d.ts +4 -0
  59. package/dist/submodel/public.d.ts.map +1 -0
  60. package/dist/submodel/public.js +1 -0
  61. package/dist/submodel/submodel.d.ts +32 -0
  62. package/dist/submodel/submodel.d.ts.map +1 -0
  63. package/dist/submodel/submodel.js +1 -0
  64. package/dist/test/apps/disabledButton.d.ts +4 -5
  65. package/dist/test/apps/disabledButton.d.ts.map +1 -1
  66. package/dist/test/apps/disabledButton.js +16 -16
  67. package/dist/test/scene.d.ts +8 -8
  68. package/dist/test/scene.d.ts.map +1 -1
  69. package/dist/test/scene.js +25 -13
  70. package/dist/test/story.d.ts +15 -8
  71. package/dist/test/story.d.ts.map +1 -1
  72. package/dist/test/story.js +21 -9
  73. package/dist/ui/animation/index.d.ts +30 -14
  74. package/dist/ui/animation/index.d.ts.map +1 -1
  75. package/dist/ui/animation/index.js +9 -19
  76. package/dist/ui/animation/public.d.ts +2 -2
  77. package/dist/ui/animation/public.d.ts.map +1 -1
  78. package/dist/ui/animation/public.js +1 -1
  79. package/dist/ui/calendar/index.d.ts +199 -84
  80. package/dist/ui/calendar/index.d.ts.map +1 -1
  81. package/dist/ui/calendar/index.js +129 -140
  82. package/dist/ui/calendar/public.d.ts +2 -2
  83. package/dist/ui/calendar/public.d.ts.map +1 -1
  84. package/dist/ui/calendar/public.js +1 -1
  85. package/dist/ui/checkbox/index.d.ts +93 -21
  86. package/dist/ui/checkbox/index.d.ts.map +1 -1
  87. package/dist/ui/checkbox/index.js +62 -33
  88. package/dist/ui/checkbox/public.d.ts +2 -2
  89. package/dist/ui/checkbox/public.d.ts.map +1 -1
  90. package/dist/ui/checkbox/public.js +1 -1
  91. package/dist/ui/combobox/multi.d.ts +35 -91
  92. package/dist/ui/combobox/multi.d.ts.map +1 -1
  93. package/dist/ui/combobox/multi.js +34 -17
  94. package/dist/ui/combobox/multiPublic.d.ts +2 -2
  95. package/dist/ui/combobox/multiPublic.d.ts.map +1 -1
  96. package/dist/ui/combobox/multiPublic.js +1 -1
  97. package/dist/ui/combobox/public.d.ts +3 -3
  98. package/dist/ui/combobox/public.d.ts.map +1 -1
  99. package/dist/ui/combobox/public.js +2 -2
  100. package/dist/ui/combobox/shared.d.ts +56 -31
  101. package/dist/ui/combobox/shared.d.ts.map +1 -1
  102. package/dist/ui/combobox/shared.js +333 -322
  103. package/dist/ui/combobox/single.d.ts +46 -93
  104. package/dist/ui/combobox/single.d.ts.map +1 -1
  105. package/dist/ui/combobox/single.js +44 -17
  106. package/dist/ui/datePicker/index.d.ts +256 -48
  107. package/dist/ui/datePicker/index.d.ts.map +1 -1
  108. package/dist/ui/datePicker/index.js +149 -104
  109. package/dist/ui/datePicker/public.d.ts +2 -2
  110. package/dist/ui/datePicker/public.d.ts.map +1 -1
  111. package/dist/ui/datePicker/public.js +1 -1
  112. package/dist/ui/dialog/index.d.ts +95 -39
  113. package/dist/ui/dialog/index.d.ts.map +1 -1
  114. package/dist/ui/dialog/index.js +71 -62
  115. package/dist/ui/dialog/public.d.ts +2 -2
  116. package/dist/ui/dialog/public.d.ts.map +1 -1
  117. package/dist/ui/dialog/public.js +1 -1
  118. package/dist/ui/disclosure/index.d.ts +71 -31
  119. package/dist/ui/disclosure/index.d.ts.map +1 -1
  120. package/dist/ui/disclosure/index.js +57 -62
  121. package/dist/ui/disclosure/public.d.ts +2 -2
  122. package/dist/ui/disclosure/public.d.ts.map +1 -1
  123. package/dist/ui/disclosure/public.js +1 -1
  124. package/dist/ui/dragAndDrop/index.d.ts +6 -6
  125. package/dist/ui/dragAndDrop/index.d.ts.map +1 -1
  126. package/dist/ui/dragAndDrop/index.js +7 -7
  127. package/dist/ui/dragAndDrop/public.d.ts +1 -1
  128. package/dist/ui/dragAndDrop/public.d.ts.map +1 -1
  129. package/dist/ui/dragAndDrop/public.js +1 -1
  130. package/dist/ui/fileDrop/index.d.ts +42 -46
  131. package/dist/ui/fileDrop/index.d.ts.map +1 -1
  132. package/dist/ui/fileDrop/index.js +30 -46
  133. package/dist/ui/fileDrop/public.d.ts +2 -2
  134. package/dist/ui/fileDrop/public.d.ts.map +1 -1
  135. package/dist/ui/fileDrop/public.js +1 -1
  136. package/dist/ui/listbox/multi.d.ts +39 -84
  137. package/dist/ui/listbox/multi.d.ts.map +1 -1
  138. package/dist/ui/listbox/multi.js +38 -20
  139. package/dist/ui/listbox/multiPublic.d.ts +2 -2
  140. package/dist/ui/listbox/multiPublic.d.ts.map +1 -1
  141. package/dist/ui/listbox/multiPublic.js +1 -1
  142. package/dist/ui/listbox/public.d.ts +3 -3
  143. package/dist/ui/listbox/public.d.ts.map +1 -1
  144. package/dist/ui/listbox/public.js +2 -2
  145. package/dist/ui/listbox/shared.d.ts +71 -30
  146. package/dist/ui/listbox/shared.d.ts.map +1 -1
  147. package/dist/ui/listbox/shared.js +319 -296
  148. package/dist/ui/listbox/single.d.ts +57 -85
  149. package/dist/ui/listbox/single.d.ts.map +1 -1
  150. package/dist/ui/listbox/single.js +48 -24
  151. package/dist/ui/menu/index.d.ts +80 -36
  152. package/dist/ui/menu/index.d.ts.map +1 -1
  153. package/dist/ui/menu/index.js +117 -86
  154. package/dist/ui/menu/public.d.ts +2 -2
  155. package/dist/ui/menu/public.d.ts.map +1 -1
  156. package/dist/ui/menu/public.js +1 -1
  157. package/dist/ui/popover/index.d.ts +117 -44
  158. package/dist/ui/popover/index.d.ts.map +1 -1
  159. package/dist/ui/popover/index.js +88 -101
  160. package/dist/ui/popover/public.d.ts +2 -2
  161. package/dist/ui/popover/public.d.ts.map +1 -1
  162. package/dist/ui/popover/public.js +1 -1
  163. package/dist/ui/radioGroup/index.d.ts +122 -45
  164. package/dist/ui/radioGroup/index.d.ts.map +1 -1
  165. package/dist/ui/radioGroup/index.js +111 -72
  166. package/dist/ui/radioGroup/public.d.ts +2 -2
  167. package/dist/ui/radioGroup/public.d.ts.map +1 -1
  168. package/dist/ui/radioGroup/public.js +1 -1
  169. package/dist/ui/slider/index.d.ts +72 -34
  170. package/dist/ui/slider/index.d.ts.map +1 -1
  171. package/dist/ui/slider/index.js +40 -49
  172. package/dist/ui/slider/public.d.ts +2 -2
  173. package/dist/ui/slider/public.d.ts.map +1 -1
  174. package/dist/ui/slider/public.js +1 -1
  175. package/dist/ui/switch/index.d.ts +74 -21
  176. package/dist/ui/switch/index.d.ts.map +1 -1
  177. package/dist/ui/switch/index.js +62 -33
  178. package/dist/ui/switch/public.d.ts +2 -2
  179. package/dist/ui/switch/public.d.ts.map +1 -1
  180. package/dist/ui/switch/public.js +1 -1
  181. package/dist/ui/tabs/index.d.ts +107 -45
  182. package/dist/ui/tabs/index.d.ts.map +1 -1
  183. package/dist/ui/tabs/index.js +99 -81
  184. package/dist/ui/tabs/public.d.ts +2 -2
  185. package/dist/ui/tabs/public.d.ts.map +1 -1
  186. package/dist/ui/tabs/public.js +1 -1
  187. package/dist/ui/toast/index.d.ts +93 -109
  188. package/dist/ui/toast/index.d.ts.map +1 -1
  189. package/dist/ui/toast/index.js +16 -29
  190. package/dist/ui/toast/schema.d.ts +15 -4
  191. package/dist/ui/toast/schema.d.ts.map +1 -1
  192. package/dist/ui/toast/schema.js +11 -4
  193. package/dist/ui/toast/update.d.ts +36 -18
  194. package/dist/ui/toast/update.d.ts.map +1 -1
  195. package/dist/ui/toast/update.js +33 -14
  196. package/dist/ui/tooltip/index.d.ts +94 -42
  197. package/dist/ui/tooltip/index.d.ts.map +1 -1
  198. package/dist/ui/tooltip/index.js +64 -73
  199. package/dist/ui/tooltip/public.d.ts +2 -2
  200. package/dist/ui/tooltip/public.d.ts.map +1 -1
  201. package/dist/ui/tooltip/public.js +1 -1
  202. package/dist/ui/virtualList/index.d.ts +18 -41
  203. package/dist/ui/virtualList/index.d.ts.map +1 -1
  204. package/dist/ui/virtualList/index.js +17 -37
  205. package/dist/ui/virtualList/public.d.ts +2 -2
  206. package/dist/ui/virtualList/public.d.ts.map +1 -1
  207. package/dist/ui/virtualList/public.js +1 -1
  208. package/package.json +1 -1
@@ -2,7 +2,7 @@ import { Array, Effect, Match as M, Option, Predicate, Result, Schema as S, pipe
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';
@@ -16,7 +16,7 @@ import { groupContiguous } from '../group.js';
16
16
  import { findFirstEnabledIndex, keyToIndex } from '../keyboard.js';
17
17
  export { groupContiguous };
18
18
  // MODEL
19
- /** Schema for the activation trigger whether the user interacted via mouse or keyboard. */
19
+ /** Schema for the activation trigger: whether the user interacted via mouse or keyboard. */
20
20
  export const ActivationTrigger = S.Literals(['Pointer', 'Keyboard']);
21
21
  /** Schema fields shared by all combobox variants (single-select and multi-select). Spread into each variant's `S.Struct` to avoid duplicating field definitions. */
22
22
  export const BaseModel = S.Struct({
@@ -85,9 +85,9 @@ export const CompletedLockScroll = m('CompletedLockScroll');
85
85
  /** Sent when the scroll unlock command completes. */
86
86
  export const CompletedUnlockScroll = m('CompletedUnlockScroll');
87
87
  /** Sent when the inert-others command completes. */
88
- export const CompletedSetupInert = m('CompletedSetupInert');
88
+ export const CompletedInertOthers = m('CompletedInertOthers');
89
89
  /** Sent when the restore-inert command completes. */
90
- export const CompletedTeardownInert = m('CompletedTeardownInert');
90
+ export const CompletedRestoreInert = m('CompletedRestoreInert');
91
91
  /** Sent when the focus-input command completes. */
92
92
  export const CompletedFocusInput = m('CompletedFocusInput');
93
93
  /** Sent when the scroll-into-view command completes after keyboard activation. */
@@ -124,8 +124,8 @@ export const Message = S.Union([
124
124
  RequestedItemClick,
125
125
  CompletedLockScroll,
126
126
  CompletedUnlockScroll,
127
- CompletedSetupInert,
128
- CompletedTeardownInert,
127
+ CompletedInertOthers,
128
+ CompletedRestoreInert,
129
129
  CompletedFocusInput,
130
130
  CompletedScrollIntoView,
131
131
  CompletedClickItem,
@@ -137,6 +137,14 @@ export const Message = S.Union([
137
137
  UpdatedInputValue,
138
138
  PressedToggleButton,
139
139
  ]);
140
+ // OUT MESSAGE
141
+ /** 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. */
142
+ export const Selected = m('Selected', {
143
+ value: S.String,
144
+ wasAdded: S.Boolean,
145
+ });
146
+ /** 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. */
147
+ export const OutMessage = S.Union([Selected]);
140
148
  // SELECTORS
141
149
  export const inputSelector = (id) => `#${id}-input`;
142
150
  export const inputWrapperSelector = (id) => `#${id}-input-wrapper`;
@@ -145,7 +153,7 @@ export const itemSelector = (id, index) => `#${id}-item-${index}`;
145
153
  export const itemId = (id, index) => `${id}-item-${index}`;
146
154
  // HELPERS
147
155
  const constrainedEvo = makeConstrainedEvo();
148
- /** Resets only shared base fields to their closed state. Does not touch inputValue or selection those are variant-specific. */
156
+ /** Resets only shared base fields to their closed state. Does not touch inputValue or selection. Those are variant-specific. */
149
157
  export const closedBaseModel = (model) => constrainedEvo(model, {
150
158
  isOpen: () => false,
151
159
  maybeActiveItemIndex: () => Option.none(),
@@ -157,9 +165,9 @@ export const LockScroll = Command.define('LockScroll', CompletedLockScroll)(Dom.
157
165
  /** Re-enables page scrolling after the combobox popup closes. */
158
166
  export const UnlockScroll = Command.define('UnlockScroll', CompletedUnlockScroll)(Dom.unlockScroll.pipe(Effect.as(CompletedUnlockScroll())));
159
167
  /** Marks all elements outside the combobox as inert for modal behavior. */
160
- export const InertOthers = Command.define('InertOthers', { id: S.String }, CompletedSetupInert)(({ id }) => Dom.inertOthers(id, [inputWrapperSelector(id), itemsSelector(id)]).pipe(Effect.as(CompletedSetupInert())));
168
+ export const InertOthers = Command.define('InertOthers', { id: S.String }, CompletedInertOthers)(({ id }) => Dom.inertOthers(id, [inputWrapperSelector(id), itemsSelector(id)]).pipe(Effect.as(CompletedInertOthers())));
161
169
  /** Removes the inert attribute from elements outside the combobox. */
162
- export const RestoreInert = Command.define('RestoreInert', { id: S.String }, CompletedTeardownInert)(({ id }) => Dom.restoreInert(id).pipe(Effect.as(CompletedTeardownInert())));
170
+ export const RestoreInert = Command.define('RestoreInert', { id: S.String }, CompletedRestoreInert)(({ id }) => Dom.restoreInert(id).pipe(Effect.as(CompletedRestoreInert())));
163
171
  /** Moves focus to the combobox input after selection or close. */
164
172
  export const FocusInput = Command.define('FocusInput', { id: S.String }, CompletedFocusInput)(({ id }) => Dom.focus(inputSelector(id)).pipe(Effect.ignore, Effect.as(CompletedFocusInput())));
165
173
  /** Scrolls the active combobox item into view after keyboard navigation. */
@@ -170,7 +178,7 @@ export const ClickItem = Command.define('ClickItem', { id: S.String, index: S.Nu
170
178
  export const DetectMovementOrAnimationEnd = Command.define('DetectMovementOrAnimationEnd', { id: S.String }, GotAnimationMessage)(({ id }) => Effect.raceFirst(Dom.detectElementMovement(inputWrapperSelector(id)).pipe(Effect.as(GotAnimationMessage({ message: AnimationEndedAnimation() }))), Dom.waitForAnimationSettled(itemsSelector(id)).pipe(Effect.as(GotAnimationMessage({ message: AnimationEndedAnimation() })))));
171
179
  const delegateToAnimation = (model, animationMessage) => {
172
180
  const [nextAnimation, animationCommands, maybeOutMessage] = animationUpdate(model.animation, animationMessage);
173
- const mappedCommands = animationCommands.map(Command.mapEffect(Effect.map(message => GotAnimationMessage({ message }))));
181
+ const mappedCommands = Command.mapMessages(animationCommands, message => GotAnimationMessage({ message }));
174
182
  const additionalCommands = Option.match(maybeOutMessage, {
175
183
  onNone: () => [],
176
184
  onSome: M.type().pipe(M.tagsExhaustive({
@@ -183,12 +191,13 @@ const delegateToAnimation = (model, animationMessage) => {
183
191
  return [
184
192
  constrainedEvo(model, { animation: () => nextAnimation }),
185
193
  [...mappedCommands, ...additionalCommands],
194
+ Option.none(),
186
195
  ];
187
196
  };
188
197
  /** Creates a combobox update function from variant-specific handlers. Shared logic (open, close, activate, transition) is handled internally; only close, selection, and immediate-activation behavior varies by variant. */
189
198
  export const makeUpdate = (handlers) => {
190
199
  const withUpdateReturn = M.withReturnType();
191
- return (model, message) => {
200
+ const internalUpdate = (model, message) => {
192
201
  const maybeLockScroll = OptionExt.when(model.isModal, LockScroll());
193
202
  const maybeUnlockScroll = OptionExt.when(model.isModal, UnlockScroll());
194
203
  const maybeInertOthers = OptionExt.when(model.isModal, InertOthers({ id: model.id }));
@@ -203,22 +212,24 @@ export const makeUpdate = (handlers) => {
203
212
  ...Array.getSomes([maybeLockScroll, maybeInertOthers]),
204
213
  ...animationCommands,
205
214
  ],
215
+ Option.none(),
206
216
  ];
207
217
  }
208
218
  return [
209
219
  constrainedEvo(baseModel, { isOpen: () => true }),
210
220
  Array.getSomes([maybeLockScroll, maybeInertOthers]),
221
+ Option.none(),
211
222
  ];
212
223
  };
213
- const closeCombobox = (baseModel, commands) => {
224
+ const closeCombobox = (baseModel, commands, maybeOutMessage = Option.none()) => {
214
225
  const closed = handlers.handleClose(baseModel);
215
226
  if (model.isAnimated) {
216
227
  const [nextModel, animationCommands] = delegateToAnimation(closed, AnimationHid());
217
- return [nextModel, [...commands, ...animationCommands]];
228
+ return [nextModel, [...commands, ...animationCommands], maybeOutMessage];
218
229
  }
219
- return [closed, commands];
230
+ return [closed, commands, maybeOutMessage];
220
231
  };
221
- return M.value(message).pipe(withUpdateReturn, M.tagsExhaustive({
232
+ return M.value(message).pipe(withUpdateReturn, M.tag('CompletedLockScroll', 'CompletedUnlockScroll', 'CompletedInertOthers', 'CompletedRestoreInert', 'CompletedFocusInput', 'CompletedScrollIntoView', 'CompletedClickItem', 'CompletedAnchorCombobox', 'CompletedAttachComboboxPreventBlur', 'CompletedAttachComboboxSelectOnFocus', 'CompletedPortalComboboxBackdrop', () => [model, [], Option.none()]), M.tagsExhaustive({
222
233
  Opened: ({ maybeActiveItemIndex }) => openCombobox(constrainedEvo(model, {
223
234
  maybeActiveItemIndex: () => maybeActiveItemIndex,
224
235
  activationTrigger: () => Option.match(maybeActiveItemIndex, {
@@ -243,12 +254,13 @@ export const makeUpdate = (handlers) => {
243
254
  activationTrigger === 'Keyboard'
244
255
  ? [ScrollIntoView({ id: model.id, index })]
245
256
  : [],
257
+ Option.none(),
246
258
  ];
247
259
  },
248
260
  MovedPointerOverItem: ({ index, screenX, screenY }) => {
249
261
  const isSamePosition = Option.exists(model.maybeLastPointerPosition, position => position.screenX === screenX && position.screenY === screenY);
250
262
  if (isSamePosition) {
251
- return [model, []];
263
+ return [model, [], Option.none()];
252
264
  }
253
265
  return [
254
266
  constrainedEvo(model, {
@@ -257,6 +269,7 @@ export const makeUpdate = (handlers) => {
257
269
  maybeLastPointerPosition: () => Option.some({ screenX, screenY }),
258
270
  }),
259
271
  [],
272
+ Option.none(),
260
273
  ];
261
274
  },
262
275
  DeactivatedItem: () => model.activationTrigger === 'Pointer'
@@ -265,23 +278,29 @@ export const makeUpdate = (handlers) => {
265
278
  maybeActiveItemIndex: () => Option.none(),
266
279
  }),
267
280
  [],
281
+ Option.none(),
268
282
  ]
269
- : [model, []],
283
+ : [model, [], Option.none()],
270
284
  SelectedItem: ({ item, displayText }) => {
271
- const [nextModel, commands] = handlers.handleSelectedItem(model, item, displayText, {
285
+ const [nextModel, commands, maybeOutMessage] = handlers.handleSelectedItem(model, item, displayText, {
272
286
  focusInput,
273
287
  maybeUnlockScroll,
274
288
  maybeRestoreInert,
275
289
  });
276
290
  if (model.isOpen && !nextModel.isOpen && model.isAnimated) {
277
291
  const [transitionedModel, animationCommands] = delegateToAnimation(nextModel, AnimationHid());
278
- return [transitionedModel, [...commands, ...animationCommands]];
292
+ return [
293
+ transitionedModel,
294
+ [...commands, ...animationCommands],
295
+ maybeOutMessage,
296
+ ];
279
297
  }
280
- return [nextModel, commands];
298
+ return [nextModel, commands, maybeOutMessage];
281
299
  },
282
300
  RequestedItemClick: ({ index }) => [
283
301
  model,
284
302
  [ClickItem({ id: model.id, index })],
303
+ Option.none(),
285
304
  ],
286
305
  UpdatedInputValue: ({ value }) => {
287
306
  if (model.isOpen) {
@@ -292,6 +311,7 @@ export const makeUpdate = (handlers) => {
292
311
  activationTrigger: () => 'Keyboard',
293
312
  }),
294
313
  [],
314
+ Option.none(),
295
315
  ];
296
316
  }
297
317
  return openCombobox(constrainedEvo(model, {
@@ -310,22 +330,12 @@ export const makeUpdate = (handlers) => {
310
330
  activationTrigger: () => 'Pointer',
311
331
  maybeLastPointerPosition: () => Option.none(),
312
332
  }));
313
- return [nextModel, [focusInput, ...commands]];
333
+ return [nextModel, [focusInput, ...commands], Option.none()];
314
334
  },
315
335
  GotAnimationMessage: ({ message: animationMessage }) => delegateToAnimation(model, animationMessage),
316
- CompletedLockScroll: () => [model, []],
317
- CompletedUnlockScroll: () => [model, []],
318
- CompletedSetupInert: () => [model, []],
319
- CompletedTeardownInert: () => [model, []],
320
- CompletedFocusInput: () => [model, []],
321
- CompletedScrollIntoView: () => [model, []],
322
- CompletedClickItem: () => [model, []],
323
- CompletedAnchorCombobox: () => [model, []],
324
- CompletedAttachComboboxPreventBlur: () => [model, []],
325
- CompletedAttachComboboxSelectOnFocus: () => [model, []],
326
- CompletedPortalComboboxBackdrop: () => [model, []],
327
336
  }));
328
337
  };
338
+ return internalUpdate;
329
339
  };
330
340
  /** The anchor-positioning Mount this Combobox renders when an anchor is
331
341
  * configured. Exposed so Scene tests can call
@@ -391,303 +401,304 @@ export const PortalComboboxBackdrop = Mount.define('PortalComboboxBackdrop', Com
391
401
  return CompletedPortalComboboxBackdrop();
392
402
  }));
393
403
  /** 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. */
394
- export const makeView = (behavior) => (config) => {
395
- const h = html();
396
- const { model: { id, isOpen, immediate, animation: { transitionState }, maybeActiveItemIndex, }, toParentMessage, onSelectedItem, 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, } = config;
397
- const dispatchSelectedItem = (item, index) => onSelectedItem
398
- ? onSelectedItem(itemToValue(item, index))
399
- : toParentMessage(SelectedItem({
400
- item: itemToValue(item, index),
401
- displayText: itemToDisplayText(item, index),
402
- }));
403
- const isLeaving = transitionState === 'LeaveStart' || transitionState === 'LeaveAnimating';
404
- const isVisible = isOpen || isLeaving;
405
- const animationAttributes = M.value(transitionState).pipe(M.when('EnterStart', () => [
406
- h.DataAttribute('closed', ''),
407
- h.DataAttribute('enter', ''),
408
- h.DataAttribute('transition', ''),
409
- ]), M.when('EnterAnimating', () => [
410
- h.DataAttribute('enter', ''),
411
- h.DataAttribute('transition', ''),
412
- ]), M.when('LeaveStart', () => [
413
- h.DataAttribute('leave', ''),
414
- h.DataAttribute('transition', ''),
415
- ]), M.when('LeaveAnimating', () => [
416
- h.DataAttribute('closed', ''),
417
- h.DataAttribute('leave', ''),
418
- h.DataAttribute('transition', ''),
419
- ]), M.orElse(() => []));
420
- const isDisabledAtIndex = (index) => Predicate.isNotUndefined(isItemDisabled) &&
421
- pipe(items, Array.get(index), Option.exists(item => isItemDisabled(item, index)));
422
- const firstEnabledIndex = findFirstEnabledIndex(items.length, 0, isDisabledAtIndex)(0, 1);
423
- const lastEnabledIndex = findFirstEnabledIndex(items.length, 0, isDisabledAtIndex)(items.length - 1, -1);
424
- const resolveActiveIndex = keyToIndex('ArrowDown', 'ArrowUp', items.length, Option.getOrElse(maybeActiveItemIndex, () => -1), isDisabledAtIndex);
425
- const resolveImmediateSelection = (targetIndex) => OptionExt.when(immediate, pipe(items, Array.get(targetIndex), Option.match({
426
- onNone: () => ({ item: '', displayText: '' }),
427
- onSome: targetItem => ({
428
- item: itemToValue(targetItem, targetIndex),
429
- displayText: itemToDisplayText(targetItem, targetIndex),
430
- }),
431
- })));
432
- const handleInputKeyDown = (key) => M.value(key).pipe(M.when('ArrowDown', () => {
433
- if (!isOpen) {
434
- return Option.some(toParentMessage(Opened({
435
- maybeActiveItemIndex: Option.some(firstEnabledIndex),
436
- })));
437
- }
438
- const targetIndex = resolveActiveIndex('ArrowDown');
439
- return Option.some(toParentMessage(ActivatedItem({
440
- index: targetIndex,
441
- activationTrigger: 'Keyboard',
442
- maybeImmediateSelection: resolveImmediateSelection(targetIndex),
443
- })));
444
- }), M.when('ArrowUp', () => {
445
- if (!isOpen) {
446
- return Option.some(toParentMessage(Opened({
447
- maybeActiveItemIndex: Option.some(lastEnabledIndex),
448
- })));
449
- }
450
- const targetIndex = resolveActiveIndex('ArrowUp');
451
- return Option.some(toParentMessage(ActivatedItem({
452
- index: targetIndex,
453
- activationTrigger: 'Keyboard',
454
- maybeImmediateSelection: resolveImmediateSelection(targetIndex),
404
+ export const makeView = (behavior) => {
405
+ const impl = defineView((model, viewInputs) => {
406
+ const h = html();
407
+ const { id, isOpen, immediate, animation: { transitionState }, maybeActiveItemIndex, } = model;
408
+ 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, } = viewInputs;
409
+ const isLeaving = transitionState === 'LeaveStart' || transitionState === 'LeaveAnimating';
410
+ const isVisible = isOpen || isLeaving;
411
+ const animationAttributes = M.value(transitionState).pipe(M.when('EnterStart', () => [
412
+ h.DataAttribute('closed', ''),
413
+ h.DataAttribute('enter', ''),
414
+ h.DataAttribute('transition', ''),
415
+ ]), M.when('EnterAnimating', () => [
416
+ h.DataAttribute('enter', ''),
417
+ h.DataAttribute('transition', ''),
418
+ ]), M.when('LeaveStart', () => [
419
+ h.DataAttribute('leave', ''),
420
+ h.DataAttribute('transition', ''),
421
+ ]), M.when('LeaveAnimating', () => [
422
+ h.DataAttribute('closed', ''),
423
+ h.DataAttribute('leave', ''),
424
+ h.DataAttribute('transition', ''),
425
+ ]), M.orElse(() => []));
426
+ const isDisabledAtIndex = (index) => Predicate.isNotUndefined(isItemDisabled) &&
427
+ pipe(items, Array.get(index), Option.exists(item => isItemDisabled(item, index)));
428
+ const firstEnabledIndex = findFirstEnabledIndex(items.length, 0, isDisabledAtIndex)(0, 1);
429
+ const lastEnabledIndex = findFirstEnabledIndex(items.length, 0, isDisabledAtIndex)(items.length - 1, -1);
430
+ const resolveActiveIndex = keyToIndex('ArrowDown', 'ArrowUp', items.length, Option.getOrElse(maybeActiveItemIndex, () => -1), isDisabledAtIndex);
431
+ const resolveImmediateSelection = (targetIndex) => OptionExt.when(immediate, pipe(items, Array.get(targetIndex), Option.match({
432
+ onNone: () => ({ item: '', displayText: '' }),
433
+ onSome: targetItem => ({
434
+ item: itemToValue(targetItem, targetIndex),
435
+ displayText: itemToDisplayText(targetItem, targetIndex),
436
+ }),
455
437
  })));
456
- }), M.when('Enter', () => {
457
- if (!isOpen) {
458
- return Option.none();
459
- }
460
- return Option.map(maybeActiveItemIndex, index => toParentMessage(RequestedItemClick({ index })));
461
- }), M.when('Escape', () => {
462
- if (!isOpen) {
463
- return Option.none();
464
- }
465
- return Option.some(toParentMessage(Closed()));
466
- }), M.whenOr('Home', 'End', () => {
467
- if (!isOpen) {
468
- return Option.none();
469
- }
470
- const targetIndex = resolveActiveIndex(key);
471
- return Option.some(toParentMessage(ActivatedItem({
472
- index: targetIndex,
473
- activationTrigger: 'Keyboard',
474
- maybeImmediateSelection: resolveImmediateSelection(targetIndex),
475
- })));
476
- }), M.orElse(() => Option.none()));
477
- const maybeActiveDescendant = Option.match(maybeActiveItemIndex, {
478
- onNone: () => [],
479
- onSome: index => [h.AriaActiveDescendant(itemId(id, index))],
480
- });
481
- const resolvedInputAttributes = [
482
- h.Id(`${id}-input`),
483
- h.Role('combobox'),
484
- h.AriaExpanded(isVisible),
485
- h.AriaControls(`${id}-items`),
486
- h.Attribute('aria-autocomplete', 'list'),
487
- h.Attribute('aria-haspopup', 'listbox'),
488
- h.Autocomplete('off'),
489
- h.Value(config.model.inputValue),
490
- ...maybeActiveDescendant,
491
- ...(inputPlaceholder ? [h.Placeholder(inputPlaceholder)] : []),
492
- ...(isDisabled
493
- ? [h.AriaDisabled(true), h.DataAttribute('disabled', '')]
494
- : [
495
- h.OnInput(value => toParentMessage(UpdatedInputValue({ value }))),
496
- h.OnKeyDownPreventDefault(handleInputKeyDown),
497
- h.OnBlur(toParentMessage(BlurredInput())),
498
- ...(openOnFocus
438
+ const handleInputKeyDown = (key) => M.value(key).pipe(M.when('ArrowDown', () => {
439
+ if (!isOpen) {
440
+ return Option.some(Opened({
441
+ maybeActiveItemIndex: Option.some(firstEnabledIndex),
442
+ }));
443
+ }
444
+ const targetIndex = resolveActiveIndex('ArrowDown');
445
+ return Option.some(ActivatedItem({
446
+ index: targetIndex,
447
+ activationTrigger: 'Keyboard',
448
+ maybeImmediateSelection: resolveImmediateSelection(targetIndex),
449
+ }));
450
+ }), M.when('ArrowUp', () => {
451
+ if (!isOpen) {
452
+ return Option.some(Opened({
453
+ maybeActiveItemIndex: Option.some(lastEnabledIndex),
454
+ }));
455
+ }
456
+ const targetIndex = resolveActiveIndex('ArrowUp');
457
+ return Option.some(ActivatedItem({
458
+ index: targetIndex,
459
+ activationTrigger: 'Keyboard',
460
+ maybeImmediateSelection: resolveImmediateSelection(targetIndex),
461
+ }));
462
+ }), M.when('Enter', () => {
463
+ if (!isOpen) {
464
+ return Option.none();
465
+ }
466
+ return Option.map(maybeActiveItemIndex, index => RequestedItemClick({ index }));
467
+ }), M.when('Escape', () => {
468
+ if (!isOpen) {
469
+ return Option.none();
470
+ }
471
+ return Option.some(Closed());
472
+ }), M.whenOr('Home', 'End', () => {
473
+ if (!isOpen) {
474
+ return Option.none();
475
+ }
476
+ const targetIndex = resolveActiveIndex(key);
477
+ return Option.some(ActivatedItem({
478
+ index: targetIndex,
479
+ activationTrigger: 'Keyboard',
480
+ maybeImmediateSelection: resolveImmediateSelection(targetIndex),
481
+ }));
482
+ }), M.orElse(() => Option.none()));
483
+ const maybeActiveDescendant = Option.match(maybeActiveItemIndex, {
484
+ onNone: () => [],
485
+ onSome: index => [h.AriaActiveDescendant(itemId(id, index))],
486
+ });
487
+ const resolvedInputAttributes = [
488
+ h.Id(`${id}-input`),
489
+ h.Role('combobox'),
490
+ h.AriaExpanded(isVisible),
491
+ h.AriaControls(`${id}-items`),
492
+ h.Attribute('aria-autocomplete', 'list'),
493
+ h.Attribute('aria-haspopup', 'listbox'),
494
+ h.Autocomplete('off'),
495
+ h.Value(model.inputValue),
496
+ ...maybeActiveDescendant,
497
+ ...(inputPlaceholder ? [h.Placeholder(inputPlaceholder)] : []),
498
+ ...(isDisabled
499
+ ? [h.AriaDisabled(true), h.DataAttribute('disabled', '')]
500
+ : [
501
+ h.OnInput(value => UpdatedInputValue({ value })),
502
+ h.OnKeyDownPreventDefault(handleInputKeyDown),
503
+ h.OnBlur(BlurredInput()),
504
+ ...(openOnFocus
505
+ ? [h.OnFocus(Opened({ maybeActiveItemIndex: Option.none() }))]
506
+ : []),
507
+ ]),
508
+ ...(isInvalid
509
+ ? [h.AriaInvalid(true), h.DataAttribute('invalid', '')]
510
+ : []),
511
+ ...(isVisible ? [h.DataAttribute('open', '')] : []),
512
+ ...(model.selectInputOnFocus
513
+ ? [h.OnMount(AttachComboboxSelectOnFocus())]
514
+ : []),
515
+ ...(inputClassName ? [h.Class(inputClassName)] : []),
516
+ ...inputAttributes,
517
+ ];
518
+ const anchorAttributes = anchor
519
+ ? [
520
+ h.Style({
521
+ position: 'absolute',
522
+ margin: '0',
523
+ visibility: 'hidden',
524
+ }),
525
+ h.OnMount(AnchorCombobox({
526
+ buttonId: `${id}-input-wrapper`,
527
+ anchor,
528
+ })),
529
+ ]
530
+ : [h.OnMount(AttachComboboxPreventBlur())];
531
+ const itemsContainerAttributes = [
532
+ h.Id(`${id}-items`),
533
+ h.Role('listbox'),
534
+ ...(behavior.ariaMultiSelectable ? [h.AriaMultiSelectable(true)] : []),
535
+ h.AriaLabelledBy(`${id}-input`),
536
+ h.Tabindex(-1),
537
+ ...anchorAttributes,
538
+ ...animationAttributes,
539
+ ...(itemsClassName ? [h.Class(itemsClassName)] : []),
540
+ ...itemsAttributes,
541
+ ];
542
+ const comboboxItems = Array.map(items, (item, index) => {
543
+ const isActiveItem = Option.exists(maybeActiveItemIndex, activeIndex => activeIndex === index);
544
+ const isDisabledItem = isDisabledAtIndex(index);
545
+ const isSelectedItem = behavior.isItemSelected(model, itemToValue(item, index));
546
+ const itemConfig = itemToConfig(item, {
547
+ isActive: isActiveItem,
548
+ isDisabled: isDisabledItem,
549
+ isSelected: isSelectedItem,
550
+ });
551
+ const isInteractive = !isDisabledItem && !isLeaving;
552
+ return h.keyed('div')(itemId(id, index), [
553
+ h.Id(itemId(id, index)),
554
+ h.Role('option'),
555
+ h.AriaSelected(isSelectedItem),
556
+ ...(isActiveItem ? [h.DataAttribute('active', '')] : []),
557
+ ...(isSelectedItem ? [h.DataAttribute('selected', '')] : []),
558
+ ...(isDisabledItem
559
+ ? [h.AriaDisabled(true), h.DataAttribute('disabled', '')]
560
+ : []),
561
+ ...(isInteractive
499
562
  ? [
500
- h.OnFocus(toParentMessage(Opened({ maybeActiveItemIndex: Option.none() }))),
563
+ h.OnClick(SelectedItem({
564
+ item: itemToValue(item, index),
565
+ displayText: itemToDisplayText(item, index),
566
+ })),
567
+ ...(isActiveItem
568
+ ? []
569
+ : [
570
+ h.OnPointerMove((screenX, screenY, pointerType) => OptionExt.when(pointerType !== 'touch', MovedPointerOverItem({ index, screenX, screenY }))),
571
+ ]),
572
+ h.OnPointerLeave(pointerType => OptionExt.when(pointerType !== 'touch', DeactivatedItem())),
501
573
  ]
502
574
  : []),
503
- ]),
504
- ...(isInvalid
505
- ? [h.AriaInvalid(true), h.DataAttribute('invalid', '')]
506
- : []),
507
- ...(isVisible ? [h.DataAttribute('open', '')] : []),
508
- ...(config.model.selectInputOnFocus
575
+ ...(itemConfig.className ? [h.Class(itemConfig.className)] : []),
576
+ ], [itemConfig.content]);
577
+ });
578
+ const renderGroupedItems = () => {
579
+ if (!itemGroupKey) {
580
+ return comboboxItems;
581
+ }
582
+ const segments = groupContiguous(comboboxItems, (_, index) => Array.get(items, index).pipe(Option.match({
583
+ onNone: () => '',
584
+ onSome: item => itemGroupKey(item, index),
585
+ })));
586
+ return Array.flatMap(segments, (segment, segmentIndex) => {
587
+ const maybeHeading = Option.fromNullishOr(groupToHeading && groupToHeading(segment.key));
588
+ const headingId = `${id}-heading-${segment.key}`;
589
+ const headingElement = Option.match(maybeHeading, {
590
+ onNone: () => [],
591
+ onSome: heading => [
592
+ h.keyed('div')(headingId, [
593
+ h.Id(headingId),
594
+ h.Role('presentation'),
595
+ ...(heading.className ? [h.Class(heading.className)] : []),
596
+ ], [heading.content]),
597
+ ],
598
+ });
599
+ const groupContent = [...headingElement, ...segment.items];
600
+ const groupElement = h.keyed('div')(`${id}-group-${segment.key}`, [
601
+ h.Role('group'),
602
+ ...(Option.isSome(maybeHeading)
603
+ ? [h.AriaLabelledBy(headingId)]
604
+ : []),
605
+ ...(groupClassName ? [h.Class(groupClassName)] : []),
606
+ ...groupAttributes,
607
+ ], groupContent);
608
+ const separator = segmentIndex > 0 &&
609
+ (separatorClassName ||
610
+ Array.isReadonlyArrayNonEmpty(separatorAttributes))
611
+ ? [
612
+ h.keyed('div')(`${id}-separator-${segmentIndex}`, [
613
+ h.Role('separator'),
614
+ ...(separatorClassName
615
+ ? [h.Class(separatorClassName)]
616
+ : []),
617
+ ...separatorAttributes,
618
+ ], []),
619
+ ]
620
+ : [];
621
+ return [...separator, groupElement];
622
+ });
623
+ };
624
+ const backdrop = h.keyed('div')(`${id}-backdrop`, [
625
+ h.OnMount(PortalComboboxBackdrop()),
626
+ ...(isLeaving ? [] : [h.OnClick(Closed())]),
627
+ ...(backdropClassName ? [h.Class(backdropClassName)] : []),
628
+ ...backdropAttributes,
629
+ ], []);
630
+ const renderedItems = renderGroupedItems();
631
+ const scrollableItems = itemsScrollClassName ||
632
+ Array.isReadonlyArrayNonEmpty(itemsScrollAttributes)
509
633
  ? [
510
- h.OnMount(Mount.mapMessage(AttachComboboxSelectOnFocus(), toParentMessage)),
634
+ h.div([
635
+ ...(itemsScrollClassName
636
+ ? [h.Class(itemsScrollClassName)]
637
+ : []),
638
+ ...itemsScrollAttributes,
639
+ ], renderedItems),
511
640
  ]
512
- : []),
513
- ...(inputClassName ? [h.Class(inputClassName)] : []),
514
- ...inputAttributes,
515
- ];
516
- const anchorAttributes = anchor
517
- ? [
518
- h.Style({ position: 'absolute', margin: '0', visibility: 'hidden' }),
519
- h.OnMount(Mount.mapMessage(AnchorCombobox({
520
- buttonId: `${id}-input-wrapper`,
521
- anchor,
522
- }), toParentMessage)),
523
- ]
524
- : [
525
- h.OnMount(Mount.mapMessage(AttachComboboxPreventBlur(), toParentMessage)),
641
+ : renderedItems;
642
+ const visibleContent = [
643
+ backdrop,
644
+ h.keyed('div')(`${id}-items-container`, itemsContainerAttributes, scrollableItems),
526
645
  ];
527
- const itemsContainerAttributes = [
528
- h.Id(`${id}-items`),
529
- h.Role('listbox'),
530
- ...(behavior.ariaMultiSelectable ? [h.AriaMultiSelectable(true)] : []),
531
- h.AriaLabelledBy(`${id}-input`),
532
- h.Tabindex(-1),
533
- ...anchorAttributes,
534
- ...animationAttributes,
535
- ...(itemsClassName ? [h.Class(itemsClassName)] : []),
536
- ...itemsAttributes,
537
- ];
538
- const comboboxItems = Array.map(items, (item, index) => {
539
- const isActiveItem = Option.exists(maybeActiveItemIndex, activeIndex => activeIndex === index);
540
- const isDisabledItem = isDisabledAtIndex(index);
541
- const isSelectedItem = behavior.isItemSelected(config.model, itemToValue(item, index));
542
- const itemConfig = itemToConfig(item, {
543
- isActive: isActiveItem,
544
- isDisabled: isDisabledItem,
545
- isSelected: isSelectedItem,
546
- });
547
- const isInteractive = !isDisabledItem && !isLeaving;
548
- return h.keyed('div')(itemId(id, index), [
549
- h.Id(itemId(id, index)),
550
- h.Role('option'),
551
- h.AriaSelected(isSelectedItem),
552
- ...(isActiveItem ? [h.DataAttribute('active', '')] : []),
553
- ...(isSelectedItem ? [h.DataAttribute('selected', '')] : []),
554
- ...(isDisabledItem
555
- ? [h.AriaDisabled(true), h.DataAttribute('disabled', '')]
556
- : []),
557
- ...(isInteractive
558
- ? [
559
- h.OnClick(dispatchSelectedItem(item, index)),
560
- ...(isActiveItem
561
- ? []
562
- : [
563
- h.OnPointerMove((screenX, screenY, pointerType) => OptionExt.when(pointerType !== 'touch', toParentMessage(MovedPointerOverItem({ index, screenX, screenY })))),
564
- ]),
565
- h.OnPointerLeave(pointerType => OptionExt.when(pointerType !== 'touch', toParentMessage(DeactivatedItem()))),
566
- ]
646
+ const resolvedInputWrapperAttributes = [
647
+ h.Id(`${id}-input-wrapper`),
648
+ ...(inputWrapperClassName ? [h.Class(inputWrapperClassName)] : []),
649
+ ...inputWrapperAttributes,
650
+ ];
651
+ const toggleButton = buttonContent
652
+ ? [
653
+ h.keyed('button')(`${id}-button`, [
654
+ h.Id(`${id}-button`),
655
+ h.Type('button'),
656
+ h.Tabindex(-1),
657
+ h.AriaControls(`${id}-items`),
658
+ h.AriaExpanded(isVisible),
659
+ h.Attribute('aria-haspopup', 'listbox'),
660
+ ...(isDisabled
661
+ ? [h.AriaDisabled(true), h.DataAttribute('disabled', '')]
662
+ : [h.OnClick(PressedToggleButton())]),
663
+ h.OnMount(AttachComboboxPreventBlur()),
664
+ ...(buttonClassName ? [h.Class(buttonClassName)] : []),
665
+ ...buttonAttributes,
666
+ ], [buttonContent]),
667
+ ]
668
+ : [];
669
+ const selectedValues = pipe(items, Array.filterMap((item, index) => {
670
+ const value = itemToValue(item, index);
671
+ return Result.fromOption(OptionExt.when(behavior.isItemSelected(model, value), value), () => undefined);
672
+ }));
673
+ const hiddenInputs = formName
674
+ ? Array.match(selectedValues, {
675
+ onEmpty: () => [h.input([h.Type('hidden'), h.Name(formName)])],
676
+ onNonEmpty: Array.map(selectedValue => h.input([
677
+ h.Type('hidden'),
678
+ h.Name(formName),
679
+ h.Value(selectedValue),
680
+ ])),
681
+ })
682
+ : [];
683
+ const wrapperAttributes = [
684
+ ...(className ? [h.Class(className)] : []),
685
+ ...attributes,
686
+ ...(isVisible ? [h.DataAttribute('open', '')] : []),
687
+ ...(isDisabled ? [h.DataAttribute('disabled', '')] : []),
688
+ ...(isInvalid ? [h.DataAttribute('invalid', '')] : []),
689
+ ];
690
+ return h.div(wrapperAttributes, [
691
+ h.div(resolvedInputWrapperAttributes, [
692
+ h.input(resolvedInputAttributes),
693
+ ...toggleButton,
694
+ ]),
695
+ ...(isVisible && Array.isReadonlyArrayNonEmpty(items)
696
+ ? visibleContent
567
697
  : []),
568
- ...(itemConfig.className ? [h.Class(itemConfig.className)] : []),
569
- ], [itemConfig.content]);
698
+ ...hiddenInputs,
699
+ ]);
570
700
  });
571
- const renderGroupedItems = () => {
572
- if (!itemGroupKey) {
573
- return comboboxItems;
574
- }
575
- const segments = groupContiguous(comboboxItems, (_, index) => Array.get(items, index).pipe(Option.match({
576
- onNone: () => '',
577
- onSome: item => itemGroupKey(item, index),
578
- })));
579
- return Array.flatMap(segments, (segment, segmentIndex) => {
580
- const maybeHeading = Option.fromNullishOr(groupToHeading && groupToHeading(segment.key));
581
- const headingId = `${id}-heading-${segment.key}`;
582
- const headingElement = Option.match(maybeHeading, {
583
- onNone: () => [],
584
- onSome: heading => [
585
- h.keyed('div')(headingId, [
586
- h.Id(headingId),
587
- h.Role('presentation'),
588
- ...(heading.className ? [h.Class(heading.className)] : []),
589
- ], [heading.content]),
590
- ],
591
- });
592
- const groupContent = [...headingElement, ...segment.items];
593
- const groupElement = h.keyed('div')(`${id}-group-${segment.key}`, [
594
- h.Role('group'),
595
- ...(Option.isSome(maybeHeading)
596
- ? [h.AriaLabelledBy(headingId)]
597
- : []),
598
- ...(groupClassName ? [h.Class(groupClassName)] : []),
599
- ...groupAttributes,
600
- ], groupContent);
601
- const separator = segmentIndex > 0 &&
602
- (separatorClassName ||
603
- Array.isReadonlyArrayNonEmpty(separatorAttributes))
604
- ? [
605
- h.keyed('div')(`${id}-separator-${segmentIndex}`, [
606
- h.Role('separator'),
607
- ...(separatorClassName
608
- ? [h.Class(separatorClassName)]
609
- : []),
610
- ...separatorAttributes,
611
- ], []),
612
- ]
613
- : [];
614
- return [...separator, groupElement];
615
- });
616
- };
617
- const backdrop = h.keyed('div')(`${id}-backdrop`, [
618
- h.OnMount(Mount.mapMessage(PortalComboboxBackdrop(), toParentMessage)),
619
- ...(isLeaving ? [] : [h.OnClick(toParentMessage(Closed()))]),
620
- ...(backdropClassName ? [h.Class(backdropClassName)] : []),
621
- ...backdropAttributes,
622
- ], []);
623
- const renderedItems = renderGroupedItems();
624
- const scrollableItems = itemsScrollClassName ||
625
- Array.isReadonlyArrayNonEmpty(itemsScrollAttributes)
626
- ? [
627
- h.div([
628
- ...(itemsScrollClassName
629
- ? [h.Class(itemsScrollClassName)]
630
- : []),
631
- ...itemsScrollAttributes,
632
- ], renderedItems),
633
- ]
634
- : renderedItems;
635
- const visibleContent = [
636
- backdrop,
637
- h.keyed('div')(`${id}-items-container`, itemsContainerAttributes, scrollableItems),
638
- ];
639
- const resolvedInputWrapperAttributes = [
640
- h.Id(`${id}-input-wrapper`),
641
- ...(inputWrapperClassName ? [h.Class(inputWrapperClassName)] : []),
642
- ...inputWrapperAttributes,
643
- ];
644
- const toggleButton = buttonContent
645
- ? [
646
- h.keyed('button')(`${id}-button`, [
647
- h.Id(`${id}-button`),
648
- h.Type('button'),
649
- h.Tabindex(-1),
650
- h.AriaControls(`${id}-items`),
651
- h.AriaExpanded(isVisible),
652
- h.Attribute('aria-haspopup', 'listbox'),
653
- ...(isDisabled
654
- ? [h.AriaDisabled(true), h.DataAttribute('disabled', '')]
655
- : [h.OnClick(toParentMessage(PressedToggleButton()))]),
656
- h.OnMount(Mount.mapMessage(AttachComboboxPreventBlur(), toParentMessage)),
657
- ...(buttonClassName ? [h.Class(buttonClassName)] : []),
658
- ...buttonAttributes,
659
- ], [buttonContent]),
660
- ]
661
- : [];
662
- const selectedValues = pipe(items, Array.filterMap((item, index) => {
663
- const value = itemToValue(item, index);
664
- return Result.fromOption(OptionExt.when(behavior.isItemSelected(config.model, value), value), () => undefined);
665
- }));
666
- const hiddenInputs = formName
667
- ? Array.match(selectedValues, {
668
- onEmpty: () => [h.input([h.Type('hidden'), h.Name(formName)])],
669
- onNonEmpty: Array.map(selectedValue => h.input([
670
- h.Type('hidden'),
671
- h.Name(formName),
672
- h.Value(selectedValue),
673
- ])),
674
- })
675
- : [];
676
- const wrapperAttributes = [
677
- ...(className ? [h.Class(className)] : []),
678
- ...attributes,
679
- ...(isVisible ? [h.DataAttribute('open', '')] : []),
680
- ...(isDisabled ? [h.DataAttribute('disabled', '')] : []),
681
- ...(isInvalid ? [h.DataAttribute('invalid', '')] : []),
682
- ];
683
- return h.div(wrapperAttributes, [
684
- h.div(resolvedInputWrapperAttributes, [
685
- h.input(resolvedInputAttributes),
686
- ...toggleButton,
687
- ]),
688
- ...(isVisible && Array.isReadonlyArrayNonEmpty(items)
689
- ? visibleContent
690
- : []),
691
- ...hiddenInputs,
692
- ]);
701
+ return () =>
702
+ /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
703
+ impl;
693
704
  };