@foldkit/ui 0.112.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 (201) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +67 -0
  3. package/dist/anchor.d.ts +38 -0
  4. package/dist/anchor.d.ts.map +1 -0
  5. package/dist/anchor.js +142 -0
  6. package/dist/animation/index.d.ts +49 -0
  7. package/dist/animation/index.d.ts.map +1 -0
  8. package/dist/animation/index.js +75 -0
  9. package/dist/animation/public.d.ts +3 -0
  10. package/dist/animation/public.d.ts.map +1 -0
  11. package/dist/animation/public.js +1 -0
  12. package/dist/animation/schema.d.ts +43 -0
  13. package/dist/animation/schema.d.ts.map +1 -0
  14. package/dist/animation/schema.js +41 -0
  15. package/dist/animation/update.d.ts +24 -0
  16. package/dist/animation/update.d.ts.map +1 -0
  17. package/dist/animation/update.js +67 -0
  18. package/dist/button/index.d.ts +17 -0
  19. package/dist/button/index.d.ts.map +1 -0
  20. package/dist/button/index.js +22 -0
  21. package/dist/button/public.d.ts +3 -0
  22. package/dist/button/public.d.ts.map +1 -0
  23. package/dist/button/public.js +1 -0
  24. package/dist/calendar/index.d.ts +462 -0
  25. package/dist/calendar/index.d.ts.map +1 -0
  26. package/dist/calendar/index.js +825 -0
  27. package/dist/calendar/public.d.ts +3 -0
  28. package/dist/calendar/public.d.ts.map +1 -0
  29. package/dist/calendar/public.js +1 -0
  30. package/dist/checkbox/index.d.ts +119 -0
  31. package/dist/checkbox/index.d.ts.map +1 -0
  32. package/dist/checkbox/index.js +111 -0
  33. package/dist/checkbox/public.d.ts +3 -0
  34. package/dist/checkbox/public.d.ts.map +1 -0
  35. package/dist/checkbox/public.js +1 -0
  36. package/dist/combobox/multi.d.ts +183 -0
  37. package/dist/combobox/multi.d.ts.map +1 -0
  38. package/dist/combobox/multi.js +81 -0
  39. package/dist/combobox/multiPublic.d.ts +3 -0
  40. package/dist/combobox/multiPublic.d.ts.map +1 -0
  41. package/dist/combobox/multiPublic.js +1 -0
  42. package/dist/combobox/public.d.ts +7 -0
  43. package/dist/combobox/public.d.ts.map +1 -0
  44. package/dist/combobox/public.js +3 -0
  45. package/dist/combobox/shared.d.ts +423 -0
  46. package/dist/combobox/shared.d.ts.map +1 -0
  47. package/dist/combobox/shared.js +708 -0
  48. package/dist/combobox/single.d.ts +198 -0
  49. package/dist/combobox/single.d.ts.map +1 -0
  50. package/dist/combobox/single.js +106 -0
  51. package/dist/datePicker/index.d.ts +457 -0
  52. package/dist/datePicker/index.d.ts.map +1 -0
  53. package/dist/datePicker/index.js +318 -0
  54. package/dist/datePicker/public.d.ts +3 -0
  55. package/dist/datePicker/public.d.ts.map +1 -0
  56. package/dist/datePicker/public.js +1 -0
  57. package/dist/dialog/index.d.ts +160 -0
  58. package/dist/dialog/index.d.ts.map +1 -0
  59. package/dist/dialog/index.js +211 -0
  60. package/dist/dialog/public.d.ts +3 -0
  61. package/dist/dialog/public.d.ts.map +1 -0
  62. package/dist/dialog/public.js +1 -0
  63. package/dist/disclosure/index.d.ts +110 -0
  64. package/dist/disclosure/index.d.ts.map +1 -0
  65. package/dist/disclosure/index.js +111 -0
  66. package/dist/disclosure/public.d.ts +3 -0
  67. package/dist/disclosure/public.d.ts.map +1 -0
  68. package/dist/disclosure/public.js +1 -0
  69. package/dist/dragAndDrop/index.d.ts +540 -0
  70. package/dist/dragAndDrop/index.d.ts.map +1 -0
  71. package/dist/dragAndDrop/index.js +535 -0
  72. package/dist/dragAndDrop/public.d.ts +3 -0
  73. package/dist/dragAndDrop/public.d.ts.map +1 -0
  74. package/dist/dragAndDrop/public.js +1 -0
  75. package/dist/fieldset/index.d.ts +21 -0
  76. package/dist/fieldset/index.d.ts.map +1 -0
  77. package/dist/fieldset/index.js +25 -0
  78. package/dist/fieldset/public.d.ts +3 -0
  79. package/dist/fieldset/public.d.ts.map +1 -0
  80. package/dist/fieldset/public.js +1 -0
  81. package/dist/fileDrop/index.d.ts +109 -0
  82. package/dist/fileDrop/index.d.ts.map +1 -0
  83. package/dist/fileDrop/index.js +127 -0
  84. package/dist/fileDrop/public.d.ts +3 -0
  85. package/dist/fileDrop/public.d.ts.map +1 -0
  86. package/dist/fileDrop/public.js +1 -0
  87. package/dist/group.d.ts +8 -0
  88. package/dist/group.d.ts.map +1 -0
  89. package/dist/group.js +13 -0
  90. package/dist/index.d.ts +25 -0
  91. package/dist/index.d.ts.map +1 -0
  92. package/dist/index.js +24 -0
  93. package/dist/input/index.d.ts +26 -0
  94. package/dist/input/index.d.ts.map +1 -0
  95. package/dist/input/index.js +43 -0
  96. package/dist/input/public.d.ts +3 -0
  97. package/dist/input/public.d.ts.map +1 -0
  98. package/dist/input/public.js +1 -0
  99. package/dist/internal/optionExtensions.d.ts +6 -0
  100. package/dist/internal/optionExtensions.d.ts.map +1 -0
  101. package/dist/internal/optionExtensions.js +2 -0
  102. package/dist/keyboard.d.ts +6 -0
  103. package/dist/keyboard.d.ts.map +1 -0
  104. package/dist/keyboard.js +9 -0
  105. package/dist/listbox/multi.d.ts +189 -0
  106. package/dist/listbox/multi.d.ts.map +1 -0
  107. package/dist/listbox/multi.js +65 -0
  108. package/dist/listbox/multiPublic.d.ts +3 -0
  109. package/dist/listbox/multiPublic.d.ts.map +1 -0
  110. package/dist/listbox/multiPublic.js +1 -0
  111. package/dist/listbox/public.d.ts +7 -0
  112. package/dist/listbox/public.d.ts.map +1 -0
  113. package/dist/listbox/public.js +3 -0
  114. package/dist/listbox/shared.d.ts +432 -0
  115. package/dist/listbox/shared.d.ts.map +1 -0
  116. package/dist/listbox/shared.js +670 -0
  117. package/dist/listbox/single.d.ts +207 -0
  118. package/dist/listbox/single.d.ts.map +1 -0
  119. package/dist/listbox/single.js +73 -0
  120. package/dist/menu/index.d.ts +368 -0
  121. package/dist/menu/index.d.ts.map +1 -0
  122. package/dist/menu/index.js +682 -0
  123. package/dist/menu/public.d.ts +4 -0
  124. package/dist/menu/public.d.ts.map +1 -0
  125. package/dist/menu/public.js +1 -0
  126. package/dist/popover/index.d.ts +267 -0
  127. package/dist/popover/index.d.ts.map +1 -0
  128. package/dist/popover/index.js +346 -0
  129. package/dist/popover/public.d.ts +4 -0
  130. package/dist/popover/public.d.ts.map +1 -0
  131. package/dist/popover/public.js +1 -0
  132. package/dist/radioGroup/index.d.ts +169 -0
  133. package/dist/radioGroup/index.d.ts.map +1 -0
  134. package/dist/radioGroup/index.js +197 -0
  135. package/dist/radioGroup/public.d.ts +3 -0
  136. package/dist/radioGroup/public.d.ts.map +1 -0
  137. package/dist/radioGroup/public.js +1 -0
  138. package/dist/select/index.d.ts +24 -0
  139. package/dist/select/index.d.ts.map +1 -0
  140. package/dist/select/index.js +40 -0
  141. package/dist/select/public.d.ts +3 -0
  142. package/dist/select/public.d.ts.map +1 -0
  143. package/dist/select/public.js +1 -0
  144. package/dist/slider/index.d.ts +318 -0
  145. package/dist/slider/index.d.ts.map +1 -0
  146. package/dist/slider/index.js +337 -0
  147. package/dist/slider/public.d.ts +3 -0
  148. package/dist/slider/public.d.ts.map +1 -0
  149. package/dist/slider/public.js +1 -0
  150. package/dist/switch/index.d.ts +99 -0
  151. package/dist/switch/index.d.ts.map +1 -0
  152. package/dist/switch/index.js +107 -0
  153. package/dist/switch/public.d.ts +3 -0
  154. package/dist/switch/public.d.ts.map +1 -0
  155. package/dist/switch/public.js +1 -0
  156. package/dist/tabs/index.d.ts +155 -0
  157. package/dist/tabs/index.d.ts.map +1 -0
  158. package/dist/tabs/index.js +185 -0
  159. package/dist/tabs/public.d.ts +3 -0
  160. package/dist/tabs/public.d.ts.map +1 -0
  161. package/dist/tabs/public.js +1 -0
  162. package/dist/test/apps/disabledButton.d.ts +38 -0
  163. package/dist/test/apps/disabledButton.d.ts.map +1 -0
  164. package/dist/test/apps/disabledButton.js +71 -0
  165. package/dist/textarea/index.d.ts +26 -0
  166. package/dist/textarea/index.d.ts.map +1 -0
  167. package/dist/textarea/index.js +44 -0
  168. package/dist/textarea/public.d.ts +3 -0
  169. package/dist/textarea/public.d.ts.map +1 -0
  170. package/dist/textarea/public.js +1 -0
  171. package/dist/toast/index.d.ts +608 -0
  172. package/dist/toast/index.d.ts.map +1 -0
  173. package/dist/toast/index.js +146 -0
  174. package/dist/toast/public.d.ts +4 -0
  175. package/dist/toast/public.d.ts.map +1 -0
  176. package/dist/toast/public.js +1 -0
  177. package/dist/toast/schema.d.ts +154 -0
  178. package/dist/toast/schema.d.ts.map +1 -0
  179. package/dist/toast/schema.js +93 -0
  180. package/dist/toast/update.d.ts +510 -0
  181. package/dist/toast/update.d.ts.map +1 -0
  182. package/dist/toast/update.js +225 -0
  183. package/dist/tooltip/index.d.ts +170 -0
  184. package/dist/tooltip/index.d.ts.map +1 -0
  185. package/dist/tooltip/index.js +253 -0
  186. package/dist/tooltip/public.d.ts +4 -0
  187. package/dist/tooltip/public.d.ts.map +1 -0
  188. package/dist/tooltip/public.js +1 -0
  189. package/dist/typeahead.d.ts +4 -0
  190. package/dist/typeahead.d.ts.map +1 -0
  191. package/dist/typeahead.js +14 -0
  192. package/dist/virtualList/index.d.ts +203 -0
  193. package/dist/virtualList/index.d.ts.map +1 -0
  194. package/dist/virtualList/index.js +392 -0
  195. package/dist/virtualList/public.d.ts +3 -0
  196. package/dist/virtualList/public.d.ts.map +1 -0
  197. package/dist/virtualList/public.js +1 -0
  198. package/dist/vitest-setup.d.ts +2 -0
  199. package/dist/vitest-setup.d.ts.map +1 -0
  200. package/dist/vitest-setup.js +2 -0
  201. package/package.json +161 -0
@@ -0,0 +1,670 @@
1
+ import { Array, Effect, Equal, Match as M, Option, Predicate, Schema as S, String as Str, pipe, } from 'effect';
2
+ import * as Command from 'foldkit/command';
3
+ import * as Dom from 'foldkit/dom';
4
+ import { html } from 'foldkit/html';
5
+ import { m } from 'foldkit/message';
6
+ import * as Mount from 'foldkit/mount';
7
+ import { makeConstrainedEvo } from 'foldkit/struct';
8
+ import { defineView } from 'foldkit/submodel';
9
+ import { AnchorConfig, anchorSetup, portalToBody } from '../anchor.js';
10
+ // NOTE: Animation imports are split across schema + update to avoid a circular
11
+ // dependency: animation → html → runtime → devtools → listbox → animation.
12
+ // The barrel (../animation) imports from html, which starts the cycle.
13
+ import { EndedAnimation as AnimationEndedAnimation, Hid as AnimationHid, Message as AnimationMessage, Model as AnimationModel, Showed as AnimationShowed, init as animationInit, } from '../animation/schema.js';
14
+ import { update as animationUpdate } from '../animation/update.js';
15
+ import { groupContiguous } from '../group.js';
16
+ import * as OptionExt from '../internal/optionExtensions.js';
17
+ import { findFirstEnabledIndex, isPrintableKey, keyToIndex, } from '../keyboard.js';
18
+ import { resolveTypeaheadMatch } from '../typeahead.js';
19
+ export { resolveTypeaheadMatch };
20
+ // MODEL
21
+ /** Schema for the activation trigger: whether the user interacted via mouse or keyboard. */
22
+ export const ActivationTrigger = S.Literals(['Pointer', 'Keyboard']);
23
+ /** Schema for the listbox orientation: whether items flow vertically or horizontally. */
24
+ export const Orientation = S.Literals(['Vertical', 'Horizontal']);
25
+ /** Schema fields shared by all listbox variants (single-select and multi-select). Spread into each variant's `S.Struct` to avoid duplicating field definitions. */
26
+ export const BaseModel = S.Struct({
27
+ id: S.String,
28
+ isOpen: S.Boolean,
29
+ isAnimated: S.Boolean,
30
+ isModal: S.Boolean,
31
+ orientation: Orientation,
32
+ animation: AnimationModel,
33
+ maybeActiveItemIndex: S.Option(S.Number),
34
+ activationTrigger: ActivationTrigger,
35
+ searchQuery: S.String,
36
+ searchVersion: S.Number,
37
+ maybeLastPointerPosition: S.Option(S.Struct({ screenX: S.Number, screenY: S.Number })),
38
+ maybeLastButtonPointerType: S.Option(S.String),
39
+ });
40
+ /** Creates the shared base fields for a listbox model from a config. Each variant spreads this and adds its selection field. */
41
+ export const baseInit = (config) => ({
42
+ id: config.id,
43
+ isOpen: false,
44
+ isAnimated: config.isAnimated ?? false,
45
+ isModal: config.isModal ?? false,
46
+ orientation: config.orientation ?? 'Vertical',
47
+ animation: animationInit({ id: `${config.id}-listbox` }),
48
+ maybeActiveItemIndex: Option.none(),
49
+ activationTrigger: 'Keyboard',
50
+ searchQuery: '',
51
+ searchVersion: 0,
52
+ maybeLastPointerPosition: Option.none(),
53
+ maybeLastButtonPointerType: Option.none(),
54
+ });
55
+ // MESSAGE
56
+ /** Sent when the listbox opens via button click or keyboard. Contains an optional initial active item index: None for pointer, Some for keyboard. */
57
+ export const Opened = m('Opened', {
58
+ maybeActiveItemIndex: S.Option(S.Number),
59
+ });
60
+ /** Sent when the listbox closes via Escape key or backdrop click. */
61
+ export const Closed = m('Closed');
62
+ /** Sent when the listbox items container loses focus. */
63
+ export const BlurredItems = m('BlurredItems');
64
+ /** Sent when an item is highlighted via arrow keys or mouse hover. Includes activation trigger. */
65
+ export const ActivatedItem = m('ActivatedItem', {
66
+ index: S.Number,
67
+ activationTrigger: ActivationTrigger,
68
+ });
69
+ /** Sent when the mouse leaves an enabled item. */
70
+ export const DeactivatedItem = m('DeactivatedItem');
71
+ /** Sent when an item is selected via Enter, Space, or click. Contains the item's string value. */
72
+ export const SelectedItem = m('SelectedItem', { item: S.String });
73
+ /** Sent when Enter or Space is pressed on the active item, triggering a programmatic click on the DOM element. */
74
+ export const RequestedItemClick = m('RequestedItemClick', {
75
+ index: S.Number,
76
+ });
77
+ /** Sent when a printable character is typed for typeahead search. */
78
+ export const Searched = m('Searched', {
79
+ key: S.String,
80
+ maybeTargetIndex: S.Option(S.Number),
81
+ });
82
+ /** Sent after the search debounce period to clear the accumulated query. */
83
+ export const ClearedSearch = m('ClearedSearch', { version: S.Number });
84
+ /** Sent when the pointer moves over a listbox item, carrying screen coordinates for tracked-pointer comparison. */
85
+ export const MovedPointerOverItem = m('MovedPointerOverItem', {
86
+ index: S.Number,
87
+ screenX: S.Number,
88
+ screenY: S.Number,
89
+ });
90
+ /** Sent when the scroll lock command completes. */
91
+ export const CompletedLockScroll = m('CompletedLockScroll');
92
+ /** Sent when the scroll unlock command completes. */
93
+ export const CompletedUnlockScroll = m('CompletedUnlockScroll');
94
+ /** Sent when the inert-others command completes. */
95
+ export const CompletedInertOthers = m('CompletedInertOthers');
96
+ /** Sent when the restore-inert command completes. */
97
+ export const CompletedRestoreInert = m('CompletedRestoreInert');
98
+ /** Sent when the focus-button command completes after closing. */
99
+ export const CompletedFocusButton = m('CompletedFocusButton');
100
+ /** Sent when the focus-items command completes after opening. */
101
+ export const CompletedFocusItems = m('CompletedFocusItems');
102
+ /** Sent when the scroll-into-view command completes after keyboard activation. */
103
+ export const CompletedScrollIntoView = m('CompletedScrollIntoView');
104
+ /** Sent when the programmatic item click command completes. */
105
+ export const CompletedClickItem = m('CompletedClickItem');
106
+ /** Sent when a mouse click on the button is ignored because pointer-down already handled the toggle. */
107
+ export const IgnoredMouseClick = m('IgnoredMouseClick');
108
+ /** Sent when a Space key-up is captured to prevent page scrolling. */
109
+ export const SuppressedSpaceScroll = m('SuppressedSpaceScroll');
110
+ /** Sent when the listbox items panel mounts and Floating UI has positioned it. Update no-ops; surfaces the positioning side effect for DevTools. */
111
+ export const CompletedAnchorListbox = m('CompletedAnchorListbox');
112
+ /** Sent when the listbox backdrop mounts and is portaled to the document body. Update no-ops; surfaces the portal side effect for DevTools. */
113
+ export const CompletedPortalListboxBackdrop = m('CompletedPortalListboxBackdrop');
114
+ /** Wraps an Animation submodel message for delegation. */
115
+ export const GotAnimationMessage = m('GotAnimationMessage', {
116
+ message: AnimationMessage,
117
+ });
118
+ /** Sent when the user presses a pointer device on the listbox button. Records pointer type for click handling. */
119
+ export const PressedPointerOnButton = m('PressedPointerOnButton', {
120
+ pointerType: S.String,
121
+ button: S.Number,
122
+ });
123
+ /** Union of all messages the listbox component can produce. */
124
+ export const Message = S.Union([
125
+ Opened,
126
+ Closed,
127
+ BlurredItems,
128
+ ActivatedItem,
129
+ DeactivatedItem,
130
+ SelectedItem,
131
+ MovedPointerOverItem,
132
+ RequestedItemClick,
133
+ Searched,
134
+ ClearedSearch,
135
+ CompletedLockScroll,
136
+ CompletedUnlockScroll,
137
+ CompletedInertOthers,
138
+ CompletedRestoreInert,
139
+ CompletedFocusButton,
140
+ CompletedFocusItems,
141
+ CompletedScrollIntoView,
142
+ CompletedClickItem,
143
+ IgnoredMouseClick,
144
+ SuppressedSpaceScroll,
145
+ CompletedAnchorListbox,
146
+ CompletedPortalListboxBackdrop,
147
+ GotAnimationMessage,
148
+ PressedPointerOnButton,
149
+ ]);
150
+ // OUT MESSAGE
151
+ /** 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. */
152
+ export const Selected = m('Selected', {
153
+ value: S.String,
154
+ wasAdded: S.Boolean,
155
+ });
156
+ /** 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. */
157
+ export const OutMessage = S.Union([Selected]);
158
+ // CONSTANTS
159
+ export const SEARCH_DEBOUNCE_MILLISECONDS = 350;
160
+ export const LEFT_MOUSE_BUTTON = 0;
161
+ // SELECTORS
162
+ export const buttonSelector = (id) => `#${id}-button`;
163
+ export const itemsSelector = (id) => `#${id}-items`;
164
+ export const itemSelector = (id, index) => `#${id}-item-${index}`;
165
+ export const itemId = (id, index) => `${id}-item-${index}`;
166
+ // HELPERS
167
+ const constrainedEvo = makeConstrainedEvo();
168
+ export const closedModel = (model) => constrainedEvo(model, {
169
+ isOpen: () => false,
170
+ maybeActiveItemIndex: () => Option.none(),
171
+ searchQuery: () => '',
172
+ searchVersion: () => 0,
173
+ maybeLastPointerPosition: () => Option.none(),
174
+ maybeLastButtonPointerType: () => Option.none(),
175
+ });
176
+ /** Prevents page scrolling while the listbox is open in modal mode. */
177
+ export const LockScroll = Command.define('LockScroll', CompletedLockScroll)(Dom.lockScroll.pipe(Effect.as(CompletedLockScroll())));
178
+ /** Re-enables page scrolling after the listbox closes. */
179
+ export const UnlockScroll = Command.define('UnlockScroll', CompletedUnlockScroll)(Dom.unlockScroll.pipe(Effect.as(CompletedUnlockScroll())));
180
+ /** Marks all elements outside the listbox as inert for modal behavior. */
181
+ export const InertOthers = Command.define('InertOthers', { id: S.String }, CompletedInertOthers)(({ id }) => Dom.inertOthers(id, [buttonSelector(id), itemsSelector(id)]).pipe(Effect.as(CompletedInertOthers())));
182
+ /** Removes the inert attribute from elements outside the listbox. */
183
+ export const RestoreInert = Command.define('RestoreInert', { id: S.String }, CompletedRestoreInert)(({ id }) => Dom.restoreInert(id).pipe(Effect.as(CompletedRestoreInert())));
184
+ /** Moves focus back to the listbox button after closing. */
185
+ export const FocusButton = Command.define('FocusButton', { id: S.String }, CompletedFocusButton)(({ id }) => Dom.focus(buttonSelector(id)).pipe(Effect.ignore, Effect.as(CompletedFocusButton())));
186
+ /** Moves focus to the listbox items container after opening. */
187
+ export const FocusItems = Command.define('FocusItems', { id: S.String }, CompletedFocusItems)(({ id }) => Dom.focus(itemsSelector(id)).pipe(Effect.ignore, Effect.as(CompletedFocusItems())));
188
+ /** Scrolls the active listbox item into view after keyboard navigation. */
189
+ export const ScrollIntoView = Command.define('ScrollIntoView', { id: S.String, index: S.Number }, CompletedScrollIntoView)(({ id, index }) => Dom.scrollIntoView(itemSelector(id, index)).pipe(Effect.ignore, Effect.as(CompletedScrollIntoView())));
190
+ /** Programmatically clicks the active listbox item's DOM element. */
191
+ export const ClickItem = Command.define('ClickItem', { id: S.String, index: S.Number }, CompletedClickItem)(({ id, index }) => Dom.clickElement(itemSelector(id, index)).pipe(Effect.ignore, Effect.as(CompletedClickItem())));
192
+ /** Waits for the typeahead search debounce period before clearing the query. */
193
+ export const DelayClearSearch = Command.define('DelayClearSearch', { version: S.Number }, ClearedSearch)(({ version }) => Effect.sleep(SEARCH_DEBOUNCE_MILLISECONDS).pipe(Effect.as(ClearedSearch({ version }))));
194
+ /** Detects whether the listbox button moved or the leave animation ended. Whichever comes first; both outcomes signal the Animation submodel that leave is complete. */
195
+ export const DetectMovementOrAnimationEnd = Command.define('DetectMovementOrAnimationEnd', { id: S.String }, GotAnimationMessage)(({ id }) => Effect.raceFirst(Dom.detectElementMovement(buttonSelector(id)).pipe(Effect.as(GotAnimationMessage({ message: AnimationEndedAnimation() }))), Dom.waitForAnimationSettled(itemsSelector(id)).pipe(Effect.as(GotAnimationMessage({ message: AnimationEndedAnimation() })))));
196
+ export const makeUpdate = (handleSelectedItem) => {
197
+ const withUpdateReturn = M.withReturnType();
198
+ const delegateToAnimation = (model, animationMessage) => {
199
+ const [nextAnimation, animationCommands, maybeOutMessage] = animationUpdate(model.animation, animationMessage);
200
+ const mappedCommands = Command.mapMessages(animationCommands, message => GotAnimationMessage({ message }));
201
+ const additionalCommands = Option.match(maybeOutMessage, {
202
+ onNone: () => [],
203
+ onSome: M.type().pipe(M.tagsExhaustive({
204
+ StartedLeaveAnimating: () => [
205
+ DetectMovementOrAnimationEnd({ id: model.id }),
206
+ ],
207
+ TransitionedOut: () => [],
208
+ })),
209
+ });
210
+ return [
211
+ constrainedEvo(model, { animation: () => nextAnimation }),
212
+ [...mappedCommands, ...additionalCommands],
213
+ Option.none(),
214
+ ];
215
+ };
216
+ const openListbox = (baseModel, openCommands) => {
217
+ if (baseModel.isAnimated) {
218
+ const [nextModel, animationCommands] = delegateToAnimation(baseModel, AnimationShowed());
219
+ return [
220
+ constrainedEvo(nextModel, { isOpen: () => true }),
221
+ [...openCommands, ...animationCommands],
222
+ Option.none(),
223
+ ];
224
+ }
225
+ return [
226
+ constrainedEvo(baseModel, { isOpen: () => true }),
227
+ openCommands,
228
+ Option.none(),
229
+ ];
230
+ };
231
+ const closeListbox = (baseModel, commands, maybeOutMessage = Option.none()) => {
232
+ const closed = closedModel(baseModel);
233
+ if (baseModel.isAnimated) {
234
+ const [nextModel, animationCommands] = delegateToAnimation(closed, AnimationHid());
235
+ return [nextModel, [...commands, ...animationCommands], maybeOutMessage];
236
+ }
237
+ return [closed, commands, maybeOutMessage];
238
+ };
239
+ const internalUpdate = (model, message) => {
240
+ const maybeLockScroll = OptionExt.when(model.isModal, LockScroll());
241
+ const maybeUnlockScroll = OptionExt.when(model.isModal, UnlockScroll());
242
+ const maybeInertOthers = OptionExt.when(model.isModal, InertOthers({ id: model.id }));
243
+ const maybeRestoreInert = OptionExt.when(model.isModal, RestoreInert({ id: model.id }));
244
+ const focusButton = FocusButton({ id: model.id });
245
+ const focusItems = FocusItems({ id: model.id });
246
+ const openCommands = [
247
+ ...Array.getSomes([maybeLockScroll, maybeInertOthers]),
248
+ focusItems,
249
+ ];
250
+ const closeWithFocusCommands = [
251
+ focusButton,
252
+ ...Array.getSomes([maybeUnlockScroll, maybeRestoreInert]),
253
+ ];
254
+ const closeWithoutFocusCommands = Array.getSomes([
255
+ maybeUnlockScroll,
256
+ maybeRestoreInert,
257
+ ]);
258
+ return M.value(message).pipe(withUpdateReturn, M.tag('CompletedLockScroll', 'CompletedUnlockScroll', 'CompletedInertOthers', 'CompletedRestoreInert', 'CompletedFocusButton', 'CompletedFocusItems', 'CompletedScrollIntoView', 'CompletedClickItem', 'SuppressedSpaceScroll', 'CompletedAnchorListbox', 'CompletedPortalListboxBackdrop', () => [model, [], Option.none()]), M.tagsExhaustive({
259
+ Opened: ({ maybeActiveItemIndex }) => openListbox(constrainedEvo(model, {
260
+ maybeActiveItemIndex: () => maybeActiveItemIndex,
261
+ activationTrigger: () => Option.match(maybeActiveItemIndex, {
262
+ onNone: () => 'Pointer',
263
+ onSome: () => 'Keyboard',
264
+ }),
265
+ searchQuery: () => '',
266
+ searchVersion: () => 0,
267
+ maybeLastPointerPosition: () => Option.none(),
268
+ }), openCommands),
269
+ Closed: () => closeListbox(model, closeWithFocusCommands),
270
+ BlurredItems: () => {
271
+ if (Option.exists(model.maybeLastButtonPointerType, Equal.equals('mouse'))) {
272
+ return [model, [], Option.none()];
273
+ }
274
+ return closeListbox(model, closeWithoutFocusCommands);
275
+ },
276
+ ActivatedItem: ({ index, activationTrigger }) => [
277
+ constrainedEvo(model, {
278
+ maybeActiveItemIndex: () => Option.some(index),
279
+ activationTrigger: () => activationTrigger,
280
+ }),
281
+ activationTrigger === 'Keyboard'
282
+ ? [ScrollIntoView({ id: model.id, index })]
283
+ : [],
284
+ Option.none(),
285
+ ],
286
+ MovedPointerOverItem: ({ index, screenX, screenY }) => {
287
+ const isSamePosition = Option.exists(model.maybeLastPointerPosition, position => position.screenX === screenX && position.screenY === screenY);
288
+ if (isSamePosition) {
289
+ return [model, [], Option.none()];
290
+ }
291
+ return [
292
+ constrainedEvo(model, {
293
+ maybeActiveItemIndex: () => Option.some(index),
294
+ activationTrigger: () => 'Pointer',
295
+ maybeLastPointerPosition: () => Option.some({ screenX, screenY }),
296
+ }),
297
+ [],
298
+ Option.none(),
299
+ ];
300
+ },
301
+ DeactivatedItem: () => model.activationTrigger === 'Pointer'
302
+ ? [
303
+ constrainedEvo(model, {
304
+ maybeActiveItemIndex: () => Option.none(),
305
+ }),
306
+ [],
307
+ Option.none(),
308
+ ]
309
+ : [model, [], Option.none()],
310
+ SelectedItem: ({ item }) => handleSelectedItem(model, item, {
311
+ closeWithFocus: (closeModel, maybeOutMessage = Option.none()) => closeListbox(closeModel, closeWithFocusCommands, maybeOutMessage),
312
+ closeWithoutFocus: (closeModel, maybeOutMessage = Option.none()) => closeListbox(closeModel, closeWithoutFocusCommands, maybeOutMessage),
313
+ }),
314
+ RequestedItemClick: ({ index }) => [
315
+ model,
316
+ [ClickItem({ id: model.id, index })],
317
+ Option.none(),
318
+ ],
319
+ Searched: ({ key, maybeTargetIndex }) => {
320
+ const nextSearchQuery = model.searchQuery + key;
321
+ const nextSearchVersion = model.searchVersion + 1;
322
+ return [
323
+ constrainedEvo(model, {
324
+ searchQuery: () => nextSearchQuery,
325
+ searchVersion: () => nextSearchVersion,
326
+ maybeActiveItemIndex: () => Option.orElse(maybeTargetIndex, () => model.maybeActiveItemIndex),
327
+ }),
328
+ [DelayClearSearch({ version: nextSearchVersion })],
329
+ Option.none(),
330
+ ];
331
+ },
332
+ ClearedSearch: ({ version }) => {
333
+ if (version !== model.searchVersion) {
334
+ return [model, [], Option.none()];
335
+ }
336
+ return [
337
+ constrainedEvo(model, { searchQuery: () => '' }),
338
+ [],
339
+ Option.none(),
340
+ ];
341
+ },
342
+ GotAnimationMessage: ({ message: animationMessage }) => delegateToAnimation(model, animationMessage),
343
+ PressedPointerOnButton: ({ pointerType, button }) => {
344
+ const withPointerType = constrainedEvo(model, {
345
+ maybeLastButtonPointerType: () => Option.some(pointerType),
346
+ });
347
+ if (pointerType !== 'mouse' || button !== LEFT_MOUSE_BUTTON) {
348
+ return [withPointerType, [], Option.none()];
349
+ }
350
+ if (model.isOpen) {
351
+ const [closed, commands] = closeListbox(withPointerType, closeWithFocusCommands);
352
+ return [
353
+ constrainedEvo(closed, {
354
+ maybeLastButtonPointerType: () => Option.some(pointerType),
355
+ }),
356
+ commands,
357
+ Option.none(),
358
+ ];
359
+ }
360
+ return openListbox(constrainedEvo(withPointerType, {
361
+ maybeActiveItemIndex: () => Option.none(),
362
+ activationTrigger: () => 'Pointer',
363
+ searchQuery: () => '',
364
+ searchVersion: () => 0,
365
+ maybeLastPointerPosition: () => Option.none(),
366
+ }), openCommands);
367
+ },
368
+ IgnoredMouseClick: () => [
369
+ constrainedEvo(model, {
370
+ maybeLastButtonPointerType: () => Option.none(),
371
+ }),
372
+ [],
373
+ Option.none(),
374
+ ],
375
+ }));
376
+ };
377
+ return internalUpdate;
378
+ };
379
+ /** The anchor-positioning Mount this Listbox renders on its items panel.
380
+ * The panel is always anchored to the button via Floating UI and portaled
381
+ * to the document body (opt out of portaling with `anchor.portal: false`),
382
+ * so it escapes ancestor stacking contexts and overflow clipping. Exposed
383
+ * so Scene tests can call
384
+ * `Scene.Mount.resolve(AnchorListbox, CompletedAnchorListbox())`. */
385
+ export const AnchorListbox = Mount.define('AnchorListbox', { buttonId: S.String, anchor: AnchorConfig }, CompletedAnchorListbox)(({ buttonId, anchor }) => element => Effect.gen(function* () {
386
+ yield* Effect.acquireRelease(Effect.sync(() => anchorSetup({ buttonId, anchor })(element)), cleanup => Effect.sync(cleanup));
387
+ return CompletedAnchorListbox();
388
+ }));
389
+ /** The backdrop-portaling Mount this Listbox renders. Exposed so Scene tests can
390
+ * call `Scene.Mount.resolve(PortalListboxBackdrop, CompletedPortalListboxBackdrop())` to
391
+ * acknowledge the mount produced by the rendered backdrop. */
392
+ export const PortalListboxBackdrop = Mount.define('PortalListboxBackdrop', CompletedPortalListboxBackdrop)(element => Effect.gen(function* () {
393
+ yield* Effect.acquireRelease(Effect.sync(() => portalToBody(element)), cleanup => Effect.sync(cleanup));
394
+ return CompletedPortalListboxBackdrop();
395
+ }));
396
+ export const makeView = (behavior) => {
397
+ const impl = defineView((model, viewInputs) => {
398
+ const h = html();
399
+ const { id, isOpen, orientation, animation: { transitionState }, maybeActiveItemIndex, searchQuery, maybeLastButtonPointerType, } = model;
400
+ 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;
401
+ const itemToValue = viewInputs.itemToValue ?? ((item) => String(item));
402
+ const itemToSearchText = viewInputs.itemToSearchText ?? ((item) => itemToValue(item));
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 isItemDisabledByIndex = (index) => Predicate.isNotUndefined(isItemDisabled) &&
421
+ pipe(items, Array.get(index), Option.exists(item => isItemDisabled(item, index)));
422
+ const isButtonEffectivelyDisabled = isDisabled || isButtonDisabled;
423
+ const nextKey = orientation === 'Horizontal' ? 'ArrowRight' : 'ArrowDown';
424
+ const previousKey = orientation === 'Horizontal' ? 'ArrowLeft' : 'ArrowUp';
425
+ const navigationKeys = [
426
+ nextKey,
427
+ previousKey,
428
+ 'Home',
429
+ 'End',
430
+ 'PageUp',
431
+ 'PageDown',
432
+ ];
433
+ const isNavigationKey = (key) => Array.contains(navigationKeys, key);
434
+ const firstEnabledIndex = findFirstEnabledIndex(items.length, 0, isItemDisabledByIndex)(0, 1);
435
+ const lastEnabledIndex = findFirstEnabledIndex(items.length, 0, isItemDisabledByIndex)(items.length - 1, -1);
436
+ const selectedItemIndex = behavior.selectedItemIndex(model, items, itemToValue);
437
+ const handleButtonKeyDown = (key) => {
438
+ if (isOpen) {
439
+ return handleItemsKeyDown(key);
440
+ }
441
+ return M.value(key).pipe(M.whenOr('Enter', ' ', 'ArrowDown', () => Option.some(Opened({
442
+ maybeActiveItemIndex: Option.orElse(selectedItemIndex, () => Option.some(firstEnabledIndex)),
443
+ }))), M.when('ArrowUp', () => Option.some(Opened({
444
+ maybeActiveItemIndex: Option.orElse(selectedItemIndex, () => Option.some(lastEnabledIndex)),
445
+ }))), M.orElse(() => Option.none()));
446
+ };
447
+ const handleButtonPointerDown = (pointerType, button) => Option.some(PressedPointerOnButton({ pointerType, button }));
448
+ const handleButtonClick = () => {
449
+ const isMouse = Option.exists(maybeLastButtonPointerType, type => type === 'mouse');
450
+ if (isMouse) {
451
+ return IgnoredMouseClick();
452
+ }
453
+ else if (isOpen) {
454
+ return Closed();
455
+ }
456
+ else {
457
+ return Opened({ maybeActiveItemIndex: Option.none() });
458
+ }
459
+ };
460
+ const handleSpaceKeyUp = (key) => OptionExt.when(key === ' ', SuppressedSpaceScroll());
461
+ const resolveActiveIndex = (key) => Option.match(maybeActiveItemIndex, {
462
+ onNone: () => M.value(key).pipe(M.whenOr(previousKey, 'End', 'PageDown', () => lastEnabledIndex), M.orElse(() => firstEnabledIndex)),
463
+ onSome: activeIndex => keyToIndex(nextKey, previousKey, items.length, activeIndex, isItemDisabledByIndex)(key),
464
+ });
465
+ const searchForKey = (key) => {
466
+ const nextQuery = searchQuery + key;
467
+ const maybeTargetIndex = resolveTypeaheadMatch(items, nextQuery, maybeActiveItemIndex, isItemDisabledByIndex, itemToSearchText, Str.isNonEmpty(searchQuery));
468
+ return Option.some(Searched({ key, maybeTargetIndex }));
469
+ };
470
+ 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)
471
+ ? searchForKey(' ')
472
+ : Option.map(maybeActiveItemIndex, index => RequestedItemClick({ index }))), M.when(isNavigationKey, () => Option.some(ActivatedItem({
473
+ index: resolveActiveIndex(key),
474
+ activationTrigger: 'Keyboard',
475
+ }))), M.when(isPrintableKey, () => searchForKey(key)), M.orElse(() => Option.none()));
476
+ const resolvedButtonAttributes = [
477
+ h.Id(`${id}-button`),
478
+ h.Type('button'),
479
+ h.AriaHasPopup('listbox'),
480
+ h.AriaExpanded(isVisible),
481
+ h.AriaControls(`${id}-items`),
482
+ ...(isButtonEffectivelyDisabled
483
+ ? [h.AriaDisabled(true), h.DataAttribute('disabled', '')]
484
+ : [
485
+ h.OnPointerDown(handleButtonPointerDown),
486
+ h.OnKeyDownPreventDefault(handleButtonKeyDown),
487
+ h.OnKeyUpPreventDefault(handleSpaceKeyUp),
488
+ h.OnClick(handleButtonClick()),
489
+ ]),
490
+ ...(isVisible
491
+ ? [
492
+ h.DataAttribute('open', ''),
493
+ h.Style({ position: 'relative', zIndex: '1' }),
494
+ ]
495
+ : []),
496
+ ...(isInvalid ? [h.DataAttribute('invalid', '')] : []),
497
+ ...(buttonClassName ? [h.Class(buttonClassName)] : []),
498
+ ...buttonAttributes,
499
+ ];
500
+ const maybeActiveDescendant = Option.match(maybeActiveItemIndex, {
501
+ onNone: () => [],
502
+ onSome: index => [h.AriaActiveDescendant(itemId(id, index))],
503
+ });
504
+ const anchorAttributes = [
505
+ h.Style({
506
+ position: 'absolute',
507
+ margin: '0',
508
+ visibility: 'hidden',
509
+ }),
510
+ h.OnMount(AnchorListbox({ buttonId: `${id}-button`, anchor })),
511
+ ];
512
+ const itemsContainerAttributes = [
513
+ h.Id(`${id}-items`),
514
+ h.Role('listbox'),
515
+ h.AriaOrientation(Str.toLowerCase(orientation)),
516
+ ...(behavior.ariaMultiSelectable ? [h.AriaMultiSelectable(true)] : []),
517
+ h.AriaLabelledBy(`${id}-button`),
518
+ ...maybeActiveDescendant,
519
+ h.Tabindex(0),
520
+ ...anchorAttributes,
521
+ ...animationAttributes,
522
+ ...(isLeaving
523
+ ? []
524
+ : [
525
+ h.OnKeyDownPreventDefault(handleItemsKeyDown),
526
+ h.OnKeyUpPreventDefault(handleSpaceKeyUp),
527
+ h.OnBlur(BlurredItems()),
528
+ ]),
529
+ ...(itemsClassName ? [h.Class(itemsClassName)] : []),
530
+ ...itemsAttributes,
531
+ ];
532
+ const listboxItems = Array.map(items, (item, index) => {
533
+ const isActiveItem = Option.exists(maybeActiveItemIndex, activeIndex => activeIndex === index);
534
+ const isDisabledItem = isItemDisabledByIndex(index);
535
+ const isSelectedItem = behavior.isItemSelected(model, itemToValue(item));
536
+ const itemConfig = itemToConfig(item, {
537
+ isActive: isActiveItem,
538
+ isDisabled: isDisabledItem,
539
+ isSelected: isSelectedItem,
540
+ });
541
+ const isInteractive = !isDisabledItem && !isLeaving;
542
+ return h.keyed('div')(itemId(id, index), [
543
+ h.Id(itemId(id, index)),
544
+ h.Role('option'),
545
+ h.AriaSelected(isSelectedItem),
546
+ ...(isActiveItem ? [h.DataAttribute('active', '')] : []),
547
+ ...(isSelectedItem ? [h.DataAttribute('selected', '')] : []),
548
+ ...(isDisabledItem
549
+ ? [h.AriaDisabled(true), h.DataAttribute('disabled', '')]
550
+ : []),
551
+ ...(isInteractive
552
+ ? [
553
+ h.OnClick(SelectedItem({ item: itemToValue(item) })),
554
+ ...(isActiveItem
555
+ ? []
556
+ : [
557
+ h.OnPointerMove((screenX, screenY, pointerType) => OptionExt.when(pointerType !== 'touch', MovedPointerOverItem({
558
+ index,
559
+ screenX,
560
+ screenY,
561
+ }))),
562
+ ]),
563
+ h.OnPointerLeave(pointerType => OptionExt.when(pointerType !== 'touch', DeactivatedItem())),
564
+ ]
565
+ : []),
566
+ ...(itemConfig.className ? [h.Class(itemConfig.className)] : []),
567
+ ], [itemConfig.content]);
568
+ });
569
+ const renderGroupedItems = () => {
570
+ if (!itemGroupKey) {
571
+ return listboxItems;
572
+ }
573
+ const segments = groupContiguous(listboxItems, (_, index) => Array.get(items, index).pipe(Option.match({
574
+ onNone: () => '',
575
+ onSome: item => itemGroupKey(item, index),
576
+ })));
577
+ return Array.flatMap(segments, (segment, segmentIndex) => {
578
+ const maybeHeading = Option.fromNullishOr(groupToHeading?.(segment.key));
579
+ const headingId = `${id}-heading-${segment.key}`;
580
+ const headingElement = Option.match(maybeHeading, {
581
+ onNone: () => [],
582
+ onSome: heading => [
583
+ h.keyed('div')(headingId, [
584
+ h.Id(headingId),
585
+ h.Role('presentation'),
586
+ ...(heading.className ? [h.Class(heading.className)] : []),
587
+ ], [heading.content]),
588
+ ],
589
+ });
590
+ const groupContent = [...headingElement, ...segment.items];
591
+ const groupElement = h.keyed('div')(`${id}-group-${segment.key}`, [
592
+ h.Role('group'),
593
+ ...(Option.isSome(maybeHeading)
594
+ ? [h.AriaLabelledBy(headingId)]
595
+ : []),
596
+ ...(groupClassName ? [h.Class(groupClassName)] : []),
597
+ ...groupAttributes,
598
+ ], groupContent);
599
+ const separator = segmentIndex > 0 &&
600
+ (separatorClassName ||
601
+ Array.isReadonlyArrayNonEmpty(separatorAttributes))
602
+ ? [
603
+ h.keyed('div')(`${id}-separator-${segmentIndex}`, [
604
+ h.Role('separator'),
605
+ ...(separatorClassName
606
+ ? [h.Class(separatorClassName)]
607
+ : []),
608
+ ...separatorAttributes,
609
+ ], []),
610
+ ]
611
+ : [];
612
+ return [...separator, groupElement];
613
+ });
614
+ };
615
+ const backdrop = h.keyed('div')(`${id}-backdrop`, [
616
+ h.OnMount(PortalListboxBackdrop()),
617
+ ...(isLeaving ? [] : [h.OnClick(Closed())]),
618
+ ...(backdropClassName ? [h.Class(backdropClassName)] : []),
619
+ ...backdropAttributes,
620
+ ], []);
621
+ const renderedItems = renderGroupedItems();
622
+ const scrollableItems = itemsScrollClassName ||
623
+ Array.isReadonlyArrayNonEmpty(itemsScrollAttributes)
624
+ ? [
625
+ h.div([
626
+ ...(itemsScrollClassName
627
+ ? [h.Class(itemsScrollClassName)]
628
+ : []),
629
+ ...itemsScrollAttributes,
630
+ ], renderedItems),
631
+ ]
632
+ : renderedItems;
633
+ const visibleContent = [
634
+ backdrop,
635
+ h.keyed('div')(`${id}-items-container`, itemsContainerAttributes, scrollableItems),
636
+ ];
637
+ const formAttribute = form ? [h.Attribute('form', form)] : [];
638
+ const selectedValues = pipe(items, Array.filter(item => behavior.isItemSelected(model, itemToValue(item))), Array.map(itemToValue));
639
+ const hiddenInputs = name
640
+ ? Array.match(selectedValues, {
641
+ onEmpty: () => [
642
+ h.input([h.Type('hidden'), h.Name(name), ...formAttribute]),
643
+ ],
644
+ onNonEmpty: Array.map(selectedValue => h.input([
645
+ h.Type('hidden'),
646
+ h.Name(name),
647
+ h.Value(selectedValue),
648
+ ...formAttribute,
649
+ ])),
650
+ })
651
+ : [];
652
+ const wrapperAttributes = [
653
+ ...(className ? [h.Class(className)] : []),
654
+ ...attributes,
655
+ ...(isVisible ? [h.DataAttribute('open', '')] : []),
656
+ ...(isDisabled ? [h.DataAttribute('disabled', '')] : []),
657
+ ...(isInvalid ? [h.DataAttribute('invalid', '')] : []),
658
+ ];
659
+ return h.div(wrapperAttributes, [
660
+ h.keyed('button')(`${id}-button`, resolvedButtonAttributes, [
661
+ buttonContent,
662
+ ]),
663
+ ...hiddenInputs,
664
+ ...(isVisible ? visibleContent : []),
665
+ ]);
666
+ });
667
+ return () =>
668
+ /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
669
+ impl;
670
+ };