foldkit 0.17.0 → 0.19.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 (87) hide show
  1. package/README.md +11 -16
  2. package/dist/effectExtensions/optionExtensions.d.ts +4 -0
  3. package/dist/effectExtensions/optionExtensions.d.ts.map +1 -1
  4. package/dist/effectExtensions/optionExtensions.js +2 -1
  5. package/dist/fieldValidation/index.d.ts +8 -8
  6. package/dist/fieldValidation/index.d.ts.map +1 -1
  7. package/dist/fieldValidation/index.js +9 -8
  8. package/dist/html/index.d.ts +57 -1
  9. package/dist/html/index.d.ts.map +1 -1
  10. package/dist/html/index.js +69 -7
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +1 -0
  14. package/dist/message/index.d.ts +2 -0
  15. package/dist/message/index.d.ts.map +1 -0
  16. package/dist/message/index.js +1 -0
  17. package/dist/message/public.d.ts +2 -0
  18. package/dist/message/public.d.ts.map +1 -0
  19. package/dist/message/public.js +1 -0
  20. package/dist/route/index.d.ts +1 -0
  21. package/dist/route/index.d.ts.map +1 -1
  22. package/dist/route/index.js +1 -0
  23. package/dist/route/parser.js +17 -17
  24. package/dist/route/public.d.ts +1 -1
  25. package/dist/route/public.d.ts.map +1 -1
  26. package/dist/route/public.js +1 -1
  27. package/dist/runtime/browserListeners.d.ts.map +1 -1
  28. package/dist/runtime/browserListeners.js +2 -2
  29. package/dist/runtime/runtime.d.ts +1 -1
  30. package/dist/runtime/runtime.d.ts.map +1 -1
  31. package/dist/runtime/runtime.js +8 -8
  32. package/dist/runtime/urlRequest.d.ts +4 -4
  33. package/dist/runtime/urlRequest.d.ts.map +1 -1
  34. package/dist/runtime/urlRequest.js +3 -2
  35. package/dist/schema/index.d.ts +44 -10
  36. package/dist/schema/index.d.ts.map +1 -1
  37. package/dist/schema/index.js +19 -15
  38. package/dist/schema/public.d.ts +1 -0
  39. package/dist/schema/public.d.ts.map +1 -1
  40. package/dist/task/dom.d.ts +59 -0
  41. package/dist/task/dom.d.ts.map +1 -0
  42. package/dist/task/dom.js +113 -0
  43. package/dist/task/index.d.ts +6 -85
  44. package/dist/task/index.d.ts.map +1 -1
  45. package/dist/task/index.js +6 -131
  46. package/dist/task/inert.d.ts +25 -0
  47. package/dist/task/inert.d.ts.map +1 -0
  48. package/dist/task/inert.js +88 -0
  49. package/dist/task/public.d.ts +1 -1
  50. package/dist/task/public.d.ts.map +1 -1
  51. package/dist/task/public.js +1 -1
  52. package/dist/task/random.d.ts +12 -0
  53. package/dist/task/random.d.ts.map +1 -0
  54. package/dist/task/random.js +11 -0
  55. package/dist/task/scrollLock.d.ts +24 -0
  56. package/dist/task/scrollLock.d.ts.map +1 -0
  57. package/dist/task/scrollLock.js +48 -0
  58. package/dist/task/time.d.ts +42 -0
  59. package/dist/task/time.d.ts.map +1 -0
  60. package/dist/task/time.js +54 -0
  61. package/dist/task/timing.d.ts +36 -0
  62. package/dist/task/timing.d.ts.map +1 -0
  63. package/dist/task/timing.js +55 -0
  64. package/dist/ui/dialog/index.d.ts +4 -4
  65. package/dist/ui/dialog/index.d.ts.map +1 -1
  66. package/dist/ui/dialog/index.js +8 -8
  67. package/dist/ui/disclosure/index.d.ts +4 -4
  68. package/dist/ui/disclosure/index.d.ts.map +1 -1
  69. package/dist/ui/disclosure/index.js +8 -8
  70. package/dist/ui/index.d.ts +1 -0
  71. package/dist/ui/index.d.ts.map +1 -1
  72. package/dist/ui/index.js +1 -0
  73. package/dist/ui/keyboard.d.ts +4 -0
  74. package/dist/ui/keyboard.d.ts.map +1 -0
  75. package/dist/ui/keyboard.js +7 -0
  76. package/dist/ui/menu/index.d.ts +189 -0
  77. package/dist/ui/menu/index.d.ts.map +1 -0
  78. package/dist/ui/menu/index.js +502 -0
  79. package/dist/ui/menu/public.d.ts +3 -0
  80. package/dist/ui/menu/public.d.ts.map +1 -0
  81. package/dist/ui/menu/public.js +1 -0
  82. package/dist/ui/tabs/index.d.ts +8 -10
  83. package/dist/ui/tabs/index.d.ts.map +1 -1
  84. package/dist/ui/tabs/index.js +17 -28
  85. package/dist/url/index.d.ts +1 -1
  86. package/dist/url/index.js +4 -4
  87. package/package.json +14 -1
@@ -0,0 +1,502 @@
1
+ import { Array, Match as M, Option, Schema as S, String as Str, pipe, } from 'effect';
2
+ import { OptionExt } from '../../effectExtensions';
3
+ import { html } from '../../html';
4
+ import { m } from '../../message';
5
+ import { evo } from '../../struct';
6
+ import * as Task from '../../task';
7
+ import { findFirstEnabledIndex, keyToIndex, wrapIndex } from '../keyboard';
8
+ // MODEL
9
+ /** Schema for the activation trigger — whether the user interacted via mouse or keyboard. */
10
+ export const ActivationTrigger = S.Literal('Pointer', 'Keyboard');
11
+ /** Schema for the transition animation state, tracking enter/leave phases for CSS transition coordination. */
12
+ export const TransitionState = S.Literal('Idle', 'EnterStart', 'EnterAnimating', 'LeaveStart', 'LeaveAnimating');
13
+ const PointerOrigin = S.Struct({
14
+ screenX: S.Number,
15
+ screenY: S.Number,
16
+ timeStamp: S.Number,
17
+ });
18
+ /** Schema for the menu component's state, tracking open/closed status, active item, activation trigger, and typeahead search. */
19
+ export const Model = S.Struct({
20
+ id: S.String,
21
+ isOpen: S.Boolean,
22
+ isAnimated: S.Boolean,
23
+ isModal: S.Boolean,
24
+ transitionState: TransitionState,
25
+ maybeActiveItemIndex: S.OptionFromSelf(S.Number),
26
+ activationTrigger: ActivationTrigger,
27
+ searchQuery: S.String,
28
+ searchVersion: S.Number,
29
+ maybeLastPointerPosition: S.OptionFromSelf(S.Struct({ screenX: S.Number, screenY: S.Number })),
30
+ maybeLastButtonPointerType: S.OptionFromSelf(S.String),
31
+ maybePointerOrigin: S.OptionFromSelf(PointerOrigin),
32
+ });
33
+ // MESSAGE
34
+ /** Sent when the menu opens via button click or keyboard. Contains an optional initial active item index — None for pointer, Some for keyboard. */
35
+ export const Opened = m('Opened', {
36
+ maybeActiveItemIndex: S.OptionFromSelf(S.Number),
37
+ });
38
+ /** Sent when the menu closes via Escape key or backdrop click. */
39
+ export const Closed = m('Closed');
40
+ /** Sent when focus leaves the menu items container via Tab key. */
41
+ export const ClosedByTab = m('ClosedByTab');
42
+ /** Sent when an item is highlighted via arrow keys or mouse hover. Includes activation trigger. */
43
+ export const ActivatedItem = m('ActivatedItem', {
44
+ index: S.Number,
45
+ activationTrigger: ActivationTrigger,
46
+ });
47
+ /** Sent when the mouse leaves an enabled item. */
48
+ export const DeactivatedItem = m('DeactivatedItem');
49
+ /** Sent when an item is selected via Enter, Space, or click. */
50
+ export const SelectedItem = m('SelectedItem', { index: S.Number });
51
+ /** Sent when Enter or Space is pressed on the active item, triggering a programmatic click on the DOM element. */
52
+ export const RequestedItemClick = m('RequestedItemClick', {
53
+ index: S.Number,
54
+ });
55
+ /** Sent when a printable character is typed for typeahead search. */
56
+ export const Searched = m('Searched', {
57
+ key: S.String,
58
+ maybeTargetIndex: S.OptionFromSelf(S.Number),
59
+ });
60
+ /** Sent after the search debounce period to clear the accumulated query. */
61
+ export const ClearedSearch = m('ClearedSearch', { version: S.Number });
62
+ /** Sent when the pointer moves over a menu item, carrying screen coordinates for tracked-pointer comparison. */
63
+ export const MovedPointerOverItem = m('MovedPointerOverItem', {
64
+ index: S.Number,
65
+ screenX: S.Number,
66
+ screenY: S.Number,
67
+ });
68
+ /** Placeholder message used when no action is needed. */
69
+ export const NoOp = m('NoOp');
70
+ /** Sent internally when a double-rAF completes, advancing the transition to its animating phase. */
71
+ export const AdvancedTransitionFrame = m('AdvancedTransitionFrame');
72
+ /** Sent internally when all CSS transitions on the menu items container have completed. */
73
+ export const EndedTransition = m('EndedTransition');
74
+ /** Sent when the user presses a pointer device on the menu button. Records pointer type and toggles for mouse. */
75
+ export const PressedPointerOnButton = m('PressedPointerOnButton', {
76
+ pointerType: S.String,
77
+ button: S.Number,
78
+ screenX: S.Number,
79
+ screenY: S.Number,
80
+ timeStamp: S.Number,
81
+ });
82
+ /** Sent when the user releases a pointer on the items container, enabling drag-to-select for mouse. */
83
+ export const ReleasedPointerOnItems = m('ReleasedPointerOnItems', {
84
+ screenX: S.Number,
85
+ screenY: S.Number,
86
+ timeStamp: S.Number,
87
+ });
88
+ /** Union of all messages the menu component can produce. */
89
+ export const Message = S.Union(Opened, Closed, ClosedByTab, ActivatedItem, DeactivatedItem, SelectedItem, MovedPointerOverItem, RequestedItemClick, Searched, ClearedSearch, NoOp, AdvancedTransitionFrame, EndedTransition, PressedPointerOnButton, ReleasedPointerOnItems);
90
+ // INIT
91
+ const SEARCH_DEBOUNCE_MILLISECONDS = 350;
92
+ const POINTER_HOLD_THRESHOLD_MILLISECONDS = 200;
93
+ const POINTER_MOVEMENT_THRESHOLD_PIXELS = 5;
94
+ /** Creates an initial menu model from a config. Defaults to closed with no active item. */
95
+ export const init = (config) => ({
96
+ id: config.id,
97
+ isOpen: false,
98
+ isAnimated: config.isAnimated ?? false,
99
+ isModal: config.isModal ?? true,
100
+ transitionState: 'Idle',
101
+ maybeActiveItemIndex: Option.none(),
102
+ activationTrigger: 'Keyboard',
103
+ searchQuery: '',
104
+ searchVersion: 0,
105
+ maybeLastPointerPosition: Option.none(),
106
+ maybeLastButtonPointerType: Option.none(),
107
+ maybePointerOrigin: Option.none(),
108
+ });
109
+ // UPDATE
110
+ const closedModel = (model) => evo(model, {
111
+ isOpen: () => false,
112
+ transitionState: () => (model.isAnimated ? 'LeaveStart' : 'Idle'),
113
+ maybeActiveItemIndex: () => Option.none(),
114
+ activationTrigger: () => 'Keyboard',
115
+ searchQuery: () => '',
116
+ searchVersion: () => 0,
117
+ maybeLastPointerPosition: () => Option.none(),
118
+ maybeLastButtonPointerType: () => Option.none(),
119
+ maybePointerOrigin: () => Option.none(),
120
+ });
121
+ const buttonSelector = (id) => `#${id}-button`;
122
+ const itemsSelector = (id) => `#${id}-items`;
123
+ const itemSelector = (id, index) => `#${id}-item-${index}`;
124
+ /** Processes a menu message and returns the next model and commands. */
125
+ export const update = (model, message) => {
126
+ const maybeNextFrameCommand = OptionExt.when(model.isAnimated, Task.nextFrame(() => AdvancedTransitionFrame()));
127
+ const maybeLockScrollCommand = OptionExt.when(model.isModal, Task.lockScroll(() => NoOp()));
128
+ const maybeUnlockScrollCommand = OptionExt.when(model.isModal, Task.unlockScroll(() => NoOp()));
129
+ const maybeInertOthersCommand = OptionExt.when(model.isModal, Task.inertOthers(model.id, [buttonSelector(model.id), itemsSelector(model.id)], () => NoOp()));
130
+ const maybeRestoreInertCommand = OptionExt.when(model.isModal, Task.restoreInert(model.id, () => NoOp()));
131
+ return M.value(message).pipe(M.withReturnType(), M.tagsExhaustive({
132
+ Opened: ({ maybeActiveItemIndex }) => {
133
+ const nextModel = evo(model, {
134
+ isOpen: () => true,
135
+ transitionState: () => (model.isAnimated ? 'EnterStart' : 'Idle'),
136
+ maybeActiveItemIndex: () => maybeActiveItemIndex,
137
+ activationTrigger: () => Option.match(maybeActiveItemIndex, {
138
+ onNone: () => 'Pointer',
139
+ onSome: () => 'Keyboard',
140
+ }),
141
+ searchQuery: () => '',
142
+ searchVersion: () => 0,
143
+ maybeLastPointerPosition: () => Option.none(),
144
+ });
145
+ return [
146
+ nextModel,
147
+ pipe(Array.getSomes([
148
+ maybeNextFrameCommand,
149
+ maybeLockScrollCommand,
150
+ maybeInertOthersCommand,
151
+ ]), Array.prepend(Task.focus(itemsSelector(model.id), () => NoOp()))),
152
+ ];
153
+ },
154
+ Closed: () => [
155
+ closedModel(model),
156
+ pipe(Array.getSomes([
157
+ maybeNextFrameCommand,
158
+ maybeUnlockScrollCommand,
159
+ maybeRestoreInertCommand,
160
+ ]), Array.prepend(Task.focus(buttonSelector(model.id), () => NoOp()))),
161
+ ],
162
+ ClosedByTab: () => [
163
+ closedModel(model),
164
+ Array.getSomes([
165
+ maybeNextFrameCommand,
166
+ maybeUnlockScrollCommand,
167
+ maybeRestoreInertCommand,
168
+ ]),
169
+ ],
170
+ ActivatedItem: ({ index, activationTrigger }) => [
171
+ evo(model, {
172
+ maybeActiveItemIndex: () => Option.some(index),
173
+ activationTrigger: () => activationTrigger,
174
+ }),
175
+ activationTrigger === 'Keyboard'
176
+ ? [Task.scrollIntoView(itemSelector(model.id, index), () => NoOp())]
177
+ : [],
178
+ ],
179
+ MovedPointerOverItem: ({ index, screenX, screenY }) => {
180
+ const isSamePosition = Option.exists(model.maybeLastPointerPosition, position => position.screenX === screenX && position.screenY === screenY);
181
+ if (isSamePosition) {
182
+ return [model, []];
183
+ }
184
+ return [
185
+ evo(model, {
186
+ maybeActiveItemIndex: () => Option.some(index),
187
+ activationTrigger: () => 'Pointer',
188
+ maybeLastPointerPosition: () => Option.some({ screenX, screenY }),
189
+ }),
190
+ [],
191
+ ];
192
+ },
193
+ DeactivatedItem: () => model.activationTrigger === 'Pointer'
194
+ ? [evo(model, { maybeActiveItemIndex: () => Option.none() }), []]
195
+ : [model, []],
196
+ SelectedItem: () => [
197
+ closedModel(model),
198
+ pipe(Array.getSomes([
199
+ maybeNextFrameCommand,
200
+ maybeUnlockScrollCommand,
201
+ maybeRestoreInertCommand,
202
+ ]), Array.prepend(Task.focus(buttonSelector(model.id), () => NoOp()))),
203
+ ],
204
+ RequestedItemClick: ({ index }) => [
205
+ model,
206
+ [Task.clickElement(itemSelector(model.id, index), () => NoOp())],
207
+ ],
208
+ Searched: ({ key, maybeTargetIndex }) => {
209
+ const nextSearchQuery = model.searchQuery + key;
210
+ const nextSearchVersion = model.searchVersion + 1;
211
+ return [
212
+ evo(model, {
213
+ searchQuery: () => nextSearchQuery,
214
+ searchVersion: () => nextSearchVersion,
215
+ maybeActiveItemIndex: () => Option.orElse(maybeTargetIndex, () => model.maybeActiveItemIndex),
216
+ }),
217
+ [
218
+ Task.delay(SEARCH_DEBOUNCE_MILLISECONDS, () => ClearedSearch({ version: nextSearchVersion })),
219
+ ],
220
+ ];
221
+ },
222
+ ClearedSearch: ({ version }) => {
223
+ if (version !== model.searchVersion) {
224
+ return [model, []];
225
+ }
226
+ return [evo(model, { searchQuery: () => '' }), []];
227
+ },
228
+ AdvancedTransitionFrame: () => M.value(model.transitionState).pipe(M.withReturnType(), M.when('EnterStart', () => [
229
+ evo(model, { transitionState: () => 'EnterAnimating' }),
230
+ [
231
+ Task.waitForTransitions(itemsSelector(model.id), () => EndedTransition()),
232
+ ],
233
+ ]), M.when('LeaveStart', () => [
234
+ evo(model, { transitionState: () => 'LeaveAnimating' }),
235
+ [
236
+ Task.waitForTransitions(itemsSelector(model.id), () => EndedTransition()),
237
+ ],
238
+ ]), M.orElse(() => [model, []])),
239
+ EndedTransition: () => M.value(model.transitionState).pipe(M.withReturnType(), M.whenOr('EnterAnimating', 'LeaveAnimating', () => [
240
+ evo(model, { transitionState: () => 'Idle' }),
241
+ [],
242
+ ]), M.orElse(() => [model, []])),
243
+ PressedPointerOnButton: ({ pointerType, button, screenX, screenY, timeStamp, }) => {
244
+ const withPointerType = evo(model, {
245
+ maybeLastButtonPointerType: () => Option.some(pointerType),
246
+ });
247
+ if (pointerType !== 'mouse' || button !== 0) {
248
+ return [withPointerType, []];
249
+ }
250
+ if (model.isOpen) {
251
+ return [
252
+ closedModel(withPointerType),
253
+ pipe(Array.getSomes([
254
+ maybeNextFrameCommand,
255
+ maybeUnlockScrollCommand,
256
+ maybeRestoreInertCommand,
257
+ ]), Array.prepend(Task.focus(buttonSelector(model.id), () => NoOp()))),
258
+ ];
259
+ }
260
+ const nextModel = evo(withPointerType, {
261
+ isOpen: () => true,
262
+ transitionState: () => (model.isAnimated ? 'EnterStart' : 'Idle'),
263
+ maybeActiveItemIndex: () => Option.none(),
264
+ activationTrigger: () => 'Pointer',
265
+ searchQuery: () => '',
266
+ searchVersion: () => 0,
267
+ maybeLastPointerPosition: () => Option.none(),
268
+ maybePointerOrigin: () => Option.some({ screenX, screenY, timeStamp }),
269
+ });
270
+ return [
271
+ nextModel,
272
+ pipe(Array.getSomes([
273
+ maybeNextFrameCommand,
274
+ maybeLockScrollCommand,
275
+ maybeInertOthersCommand,
276
+ ]), Array.prepend(Task.focus(itemsSelector(model.id), () => NoOp()))),
277
+ ];
278
+ },
279
+ ReleasedPointerOnItems: ({ screenX, screenY, timeStamp }) => {
280
+ const hasNoOrigin = Option.isNone(model.maybePointerOrigin);
281
+ const hasNoActiveItem = Option.isNone(model.maybeActiveItemIndex);
282
+ const isMovementBelowThreshold = Option.exists(model.maybePointerOrigin, origin => Math.abs(screenX - origin.screenX) <
283
+ POINTER_MOVEMENT_THRESHOLD_PIXELS &&
284
+ Math.abs(screenY - origin.screenY) <
285
+ POINTER_MOVEMENT_THRESHOLD_PIXELS);
286
+ const isHoldTimeBelowThreshold = Option.exists(model.maybePointerOrigin, origin => timeStamp - origin.timeStamp < POINTER_HOLD_THRESHOLD_MILLISECONDS);
287
+ if (hasNoOrigin ||
288
+ isMovementBelowThreshold ||
289
+ isHoldTimeBelowThreshold ||
290
+ hasNoActiveItem) {
291
+ return [model, []];
292
+ }
293
+ return [
294
+ model,
295
+ [
296
+ Task.clickElement(itemSelector(model.id, model.maybeActiveItemIndex.value), () => NoOp()),
297
+ ],
298
+ ];
299
+ },
300
+ NoOp: () => [model, []],
301
+ }));
302
+ };
303
+ export const groupContiguous = (items, toKey) => {
304
+ const tagged = Array.map(items, (item, index) => ({
305
+ key: toKey(item, index),
306
+ item,
307
+ }));
308
+ return Array.chop(tagged, nonEmpty => {
309
+ const key = Array.headNonEmpty(nonEmpty).key;
310
+ const [matching, rest] = Array.span(nonEmpty, tagged => tagged.key === key);
311
+ return [{ key, items: Array.map(matching, ({ item }) => item) }, rest];
312
+ });
313
+ };
314
+ const itemId = (id, index) => `${id}-item-${index}`;
315
+ /** Finds the first enabled item whose search text starts with the query, searching forward from the active item and wrapping around. On a fresh search, starts after the active item; on a refinement, includes the active item. */
316
+ export const resolveTypeaheadMatch = (items, query, maybeActiveItemIndex, isDisabled, itemToSearchText, isRefinement) => {
317
+ const lowerQuery = Str.toLowerCase(query);
318
+ const offset = isRefinement ? 0 : 1;
319
+ const startIndex = Option.match(maybeActiveItemIndex, {
320
+ onNone: () => 0,
321
+ onSome: index => index + offset,
322
+ });
323
+ const isEnabledMatch = (index) => !isDisabled(index) &&
324
+ pipe(items, Array.get(index), Option.exists(item => pipe(itemToSearchText(item, index), Str.toLowerCase, Str.startsWith(lowerQuery))));
325
+ return pipe(items.length, Array.makeBy(step => wrapIndex(startIndex + step, items.length)), Array.findFirst(isEnabledMatch));
326
+ };
327
+ /** Renders a headless menu with typeahead search, keyboard navigation, and aria-activedescendant focus management. */
328
+ export const view = (config) => {
329
+ const { div, AriaActiveDescendant, AriaControls, AriaDisabled, AriaExpanded, AriaHasPopup, AriaLabelledBy, Class, DataAttribute, Id, OnBlur, OnClick, OnKeyDownPreventDefault, OnKeyUpPreventDefault, OnPointerDown, OnPointerLeave, OnPointerMove, OnPointerUp, Role, Tabindex, Type, keyed, } = html();
330
+ const { model: { id, isOpen, transitionState, maybeActiveItemIndex, searchQuery, maybeLastButtonPointerType, }, toMessage, items, itemToConfig, isItemDisabled, itemToSearchText = (item) => item, isButtonDisabled, buttonContent, buttonClassName, itemsClassName, backdropClassName, className, itemGroupKey, groupToHeading, groupClassName, separatorClassName, } = config;
331
+ const isLeaving = transitionState === 'LeaveStart' || transitionState === 'LeaveAnimating';
332
+ const isVisible = isOpen || isLeaving;
333
+ const transitionAttributes = M.value(transitionState).pipe(M.when('EnterStart', () => [
334
+ DataAttribute('closed', ''),
335
+ DataAttribute('enter', ''),
336
+ DataAttribute('transition', ''),
337
+ ]), M.when('EnterAnimating', () => [
338
+ DataAttribute('enter', ''),
339
+ DataAttribute('transition', ''),
340
+ ]), M.when('LeaveStart', () => [
341
+ DataAttribute('leave', ''),
342
+ DataAttribute('transition', ''),
343
+ ]), M.when('LeaveAnimating', () => [
344
+ DataAttribute('closed', ''),
345
+ DataAttribute('leave', ''),
346
+ DataAttribute('transition', ''),
347
+ ]), M.orElse(() => []));
348
+ const isDisabled = (index) => !!isItemDisabled &&
349
+ pipe(items, Array.get(index), Option.exists(item => isItemDisabled(item, index)));
350
+ const firstEnabledIndex = findFirstEnabledIndex(items.length, 0, isDisabled)(0, 1);
351
+ const lastEnabledIndex = findFirstEnabledIndex(items.length, 0, isDisabled)(items.length - 1, -1);
352
+ const handleButtonKeyDown = (key) => M.value(key).pipe(M.whenOr('Enter', ' ', 'ArrowDown', () => Option.some(toMessage(Opened({
353
+ maybeActiveItemIndex: Option.some(firstEnabledIndex),
354
+ })))), M.when('ArrowUp', () => Option.some(toMessage(Opened({
355
+ maybeActiveItemIndex: Option.some(lastEnabledIndex),
356
+ })))), M.orElse(() => Option.none()));
357
+ const handleButtonPointerDown = (pointerType, button, screenX, screenY, timeStamp) => Option.some(toMessage(PressedPointerOnButton({
358
+ pointerType,
359
+ button,
360
+ screenX,
361
+ screenY,
362
+ timeStamp,
363
+ })));
364
+ const handleButtonClick = () => {
365
+ const isMouse = Option.exists(maybeLastButtonPointerType, type => type === 'mouse');
366
+ if (isMouse) {
367
+ return toMessage(NoOp());
368
+ }
369
+ else if (isOpen) {
370
+ return toMessage(Closed());
371
+ }
372
+ else {
373
+ return toMessage(Opened({ maybeActiveItemIndex: Option.none() }));
374
+ }
375
+ };
376
+ const handleSpaceKeyUp = (key) => OptionExt.when(key === ' ', toMessage(NoOp()));
377
+ const resolveActiveIndex = keyToIndex('ArrowDown', 'ArrowUp', items.length, Option.getOrElse(maybeActiveItemIndex, () => 0), isDisabled);
378
+ const searchForKey = (key) => {
379
+ const nextQuery = searchQuery + key;
380
+ const maybeTargetIndex = resolveTypeaheadMatch(items, nextQuery, maybeActiveItemIndex, isDisabled, itemToSearchText, Str.isNonEmpty(searchQuery));
381
+ return Option.some(toMessage(Searched({ key, maybeTargetIndex })));
382
+ };
383
+ const handleItemsKeyDown = (key) => M.value(key).pipe(M.when('Escape', () => Option.some(toMessage(Closed()))), M.when('Enter', () => Option.map(maybeActiveItemIndex, index => toMessage(RequestedItemClick({ index })))), M.when(' ', () => Str.isNonEmpty(searchQuery)
384
+ ? searchForKey(' ')
385
+ : Option.map(maybeActiveItemIndex, index => toMessage(RequestedItemClick({ index })))), M.whenOr('ArrowDown', 'ArrowUp', 'Home', 'End', 'PageUp', 'PageDown', () => Option.some(toMessage(ActivatedItem({
386
+ index: resolveActiveIndex(key),
387
+ activationTrigger: 'Keyboard',
388
+ })))), M.when(key => key.length === 1, () => searchForKey(key)), M.orElse(() => Option.none()));
389
+ const handleItemsPointerUp = (screenX, screenY, pointerType, timeStamp) => OptionExt.when(pointerType === 'mouse', toMessage(ReleasedPointerOnItems({ screenX, screenY, timeStamp })));
390
+ const buttonAttributes = [
391
+ Id(`${id}-button`),
392
+ Type('button'),
393
+ Class(buttonClassName),
394
+ AriaHasPopup('menu'),
395
+ AriaExpanded(isVisible),
396
+ AriaControls(`${id}-items`),
397
+ ...(isButtonDisabled
398
+ ? [AriaDisabled(true), DataAttribute('disabled', '')]
399
+ : [
400
+ OnPointerDown(handleButtonPointerDown),
401
+ OnKeyDownPreventDefault(handleButtonKeyDown),
402
+ OnKeyUpPreventDefault(handleSpaceKeyUp),
403
+ OnClick(handleButtonClick()),
404
+ ]),
405
+ ...(isVisible ? [DataAttribute('open', '')] : []),
406
+ ];
407
+ const maybeActiveDescendant = Option.match(maybeActiveItemIndex, {
408
+ onNone: () => [],
409
+ onSome: index => [AriaActiveDescendant(itemId(id, index))],
410
+ });
411
+ const itemsContainerAttributes = [
412
+ Id(`${id}-items`),
413
+ Role('menu'),
414
+ AriaLabelledBy(`${id}-button`),
415
+ ...maybeActiveDescendant,
416
+ Tabindex(0),
417
+ Class(itemsClassName),
418
+ ...transitionAttributes,
419
+ ...(isLeaving
420
+ ? []
421
+ : [
422
+ OnKeyDownPreventDefault(handleItemsKeyDown),
423
+ OnKeyUpPreventDefault(handleSpaceKeyUp),
424
+ OnPointerUp(handleItemsPointerUp),
425
+ OnBlur(toMessage(ClosedByTab())),
426
+ ]),
427
+ ];
428
+ const menuItems = Array.map(items, (item, index) => {
429
+ const isActiveItem = Option.exists(maybeActiveItemIndex, activeIndex => activeIndex === index);
430
+ const isDisabledItem = isDisabled(index);
431
+ const itemConfig = itemToConfig(item, {
432
+ isActive: isActiveItem,
433
+ isDisabled: isDisabledItem,
434
+ });
435
+ const isInteractive = !isDisabledItem && !isLeaving;
436
+ return keyed('div')(itemId(id, index), [
437
+ Id(itemId(id, index)),
438
+ Role('menuitem'),
439
+ Tabindex(-1),
440
+ Class(itemConfig.className),
441
+ ...(isActiveItem ? [DataAttribute('active', '')] : []),
442
+ ...(isDisabledItem
443
+ ? [AriaDisabled(true), DataAttribute('disabled', '')]
444
+ : []),
445
+ ...(isInteractive
446
+ ? [
447
+ OnClick(toMessage(SelectedItem({ index }))),
448
+ OnPointerMove((screenX, screenY, pointerType) => OptionExt.when(pointerType !== 'touch', toMessage(MovedPointerOverItem({ index, screenX, screenY })))),
449
+ OnPointerLeave(pointerType => OptionExt.when(pointerType !== 'touch', toMessage(DeactivatedItem()))),
450
+ ]
451
+ : []),
452
+ ], [itemConfig.content]);
453
+ });
454
+ const renderGroupedItems = () => {
455
+ if (!itemGroupKey) {
456
+ return menuItems;
457
+ }
458
+ const segments = groupContiguous(menuItems, (_, index) => Array.get(items, index).pipe(Option.match({
459
+ onNone: () => '',
460
+ onSome: item => itemGroupKey(item, index),
461
+ })));
462
+ return Array.flatMap(segments, (segment, segmentIndex) => {
463
+ const maybeHeading = Option.fromNullable(groupToHeading && groupToHeading(segment.key));
464
+ const headingId = `${id}-heading-${segment.key}`;
465
+ const headingElement = Option.match(maybeHeading, {
466
+ onNone: () => [],
467
+ onSome: heading => [
468
+ keyed('div')(headingId, [Id(headingId), Role('presentation'), Class(heading.className)], [heading.content]),
469
+ ],
470
+ });
471
+ const groupContent = [...headingElement, ...segment.items];
472
+ const groupElement = keyed('div')(`${id}-group-${segment.key}`, [
473
+ Role('group'),
474
+ ...(Option.isSome(maybeHeading) ? [AriaLabelledBy(headingId)] : []),
475
+ ...(groupClassName ? [Class(groupClassName)] : []),
476
+ ], groupContent);
477
+ const separator = segmentIndex > 0 && separatorClassName
478
+ ? [
479
+ keyed('div')(`${id}-separator-${segmentIndex}`, [Role('separator'), Class(separatorClassName)], []),
480
+ ]
481
+ : [];
482
+ return [...separator, groupElement];
483
+ });
484
+ };
485
+ const backdrop = keyed('div')(`${id}-backdrop`, [
486
+ Class(backdropClassName),
487
+ ...(isLeaving ? [] : [OnClick(toMessage(Closed()))]),
488
+ ], []);
489
+ const renderedItems = renderGroupedItems();
490
+ const visibleContent = [
491
+ backdrop,
492
+ keyed('div')(`${id}-items-container`, itemsContainerAttributes, renderedItems),
493
+ ];
494
+ const wrapperAttributes = [
495
+ ...(className ? [Class(className)] : []),
496
+ ...(isVisible ? [DataAttribute('open', '')] : []),
497
+ ];
498
+ return div(wrapperAttributes, [
499
+ keyed('button')(`${id}-button`, buttonAttributes, [buttonContent]),
500
+ ...(isVisible ? visibleContent : []),
501
+ ]);
502
+ };
@@ -0,0 +1,3 @@
1
+ export { init, update, view, Model, Message, TransitionState } from './index';
2
+ export type { ActivationTrigger, Opened, Closed, ClosedByTab, ActivatedItem, DeactivatedItem, SelectedItem, MovedPointerOverItem, Searched, ClearedSearch, NoOp, AdvancedTransitionFrame, EndedTransition, InitConfig, ViewConfig, ItemConfig, GroupHeading, } from './index';
3
+ //# sourceMappingURL=public.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../../src/ui/menu/public.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAE7E,YAAY,EACV,iBAAiB,EACjB,MAAM,EACN,MAAM,EACN,WAAW,EACX,aAAa,EACb,eAAe,EACf,YAAY,EACZ,oBAAoB,EACpB,QAAQ,EACR,aAAa,EACb,IAAI,EACJ,uBAAuB,EACvB,eAAe,EACf,UAAU,EACV,UAAU,EACV,UAAU,EACV,YAAY,GACb,MAAM,SAAS,CAAA"}
@@ -0,0 +1 @@
1
+ export { init, update, view, Model, Message, TransitionState } from './index';
@@ -1,6 +1,7 @@
1
1
  import { Schema as S } from 'effect';
2
2
  import type { Html, TagName } from '../../html';
3
3
  import type { Command } from '../../runtime/runtime';
4
+ export { wrapIndex, findFirstEnabledIndex, keyToIndex } from '../keyboard';
4
5
  /** Controls the tab list layout direction and which arrow keys navigate between tabs. */
5
6
  export declare const Orientation: S.Literal<["Horizontal", "Vertical"]>;
6
7
  export type Orientation = typeof Orientation.Type;
@@ -17,21 +18,21 @@ export declare const Model: S.Struct<{
17
18
  }>;
18
19
  export type Model = typeof Model.Type;
19
20
  /** Sent when a tab is selected via click or keyboard. Updates both the active and focused indices. */
20
- export declare const TabSelected: S.TaggedStruct<"TabSelected", {
21
+ export declare const TabSelected: import("../../schema").CallableTaggedStruct<"TabSelected", {
21
22
  index: typeof S.Number;
22
23
  }>;
23
24
  /** Sent when a tab receives keyboard focus in `Manual` mode without being activated. */
24
- export declare const TabFocused: S.TaggedStruct<"TabFocused", {
25
+ export declare const TabFocused: import("../../schema").CallableTaggedStruct<"TabFocused", {
25
26
  index: typeof S.Number;
26
27
  }>;
27
28
  /** Placeholder message used when no action is needed, such as after a focus command completes. */
28
- export declare const NoOp: S.TaggedStruct<"NoOp", {}>;
29
+ export declare const NoOp: import("../../schema").CallableTaggedStruct<"NoOp", {}>;
29
30
  /** Union of all messages the tabs component can produce. */
30
- export declare const Message: S.Union<[S.TaggedStruct<"TabSelected", {
31
+ export declare const Message: S.Union<[import("../../schema").CallableTaggedStruct<"TabSelected", {
31
32
  index: typeof S.Number;
32
- }>, S.TaggedStruct<"TabFocused", {
33
+ }>, import("../../schema").CallableTaggedStruct<"TabFocused", {
33
34
  index: typeof S.Number;
34
- }>, S.TaggedStruct<"NoOp", {}>]>;
35
+ }>, import("../../schema").CallableTaggedStruct<"NoOp", {}>]>;
35
36
  export type TabSelected = typeof TabSelected.Type;
36
37
  export type TabFocused = typeof TabFocused.Type;
37
38
  export type NoOp = typeof NoOp.Type;
@@ -47,9 +48,6 @@ export type InitConfig = Readonly<{
47
48
  export declare const init: (config: InitConfig) => Model;
48
49
  /** Processes a tabs message and returns the next model and commands. */
49
50
  export declare const update: (model: Model, message: Message) => [Model, ReadonlyArray<Command<Message>>];
50
- export declare const wrapIndex: (index: number, length: number) => number;
51
- export declare const findFirstEnabledIndex: (tabCount: number, focusedIndex: number, isDisabled: (index: number) => boolean) => (startIndex: number, direction: 1 | -1) => number;
52
- export declare const keyToIndex: (nextKey: string, previousKey: string, tabCount: number, focusedIndex: number, isDisabled: (index: number) => boolean) => ((key: string) => number);
53
51
  /** Configuration for an individual tab's button and panel content. */
54
52
  export type TabConfig = Readonly<{
55
53
  buttonClassName: string;
@@ -60,7 +58,7 @@ export type TabConfig = Readonly<{
60
58
  /** Configuration for rendering a tab group with `view`. */
61
59
  export type ViewConfig<Message, Tab extends string> = Readonly<{
62
60
  model: Model;
63
- toMessage: (message: TabSelected | TabFocused | NoOp) => Message;
61
+ toMessage: (message: TabSelected | TabFocused) => Message;
64
62
  tabs: ReadonlyArray<Tab>;
65
63
  tabToConfig: (tab: Tab, context: {
66
64
  isActive: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/tabs/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/tabs/index.ts"],"names":[],"mappings":"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"}