foldkit 0.101.0 → 0.102.1

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