foldkit 0.33.6 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +19 -19
  2. package/dist/devtools/overlay.d.ts.map +1 -1
  3. package/dist/devtools/overlay.js +76 -33
  4. package/dist/html/index.d.ts +431 -5
  5. package/dist/html/index.d.ts.map +1 -1
  6. package/dist/html/index.js +1 -0
  7. package/dist/html/public.d.ts +1 -2
  8. package/dist/html/public.d.ts.map +1 -1
  9. package/dist/html/public.js +1 -2
  10. package/dist/task/dom.d.ts +6 -6
  11. package/dist/task/dom.js +6 -6
  12. package/dist/task/inert.d.ts +2 -2
  13. package/dist/task/inert.js +2 -2
  14. package/dist/task/scrollLock.d.ts +2 -2
  15. package/dist/task/scrollLock.js +2 -2
  16. package/dist/ui/checkbox/index.d.ts +5 -9
  17. package/dist/ui/checkbox/index.d.ts.map +1 -1
  18. package/dist/ui/checkbox/index.js +7 -10
  19. package/dist/ui/checkbox/public.d.ts +1 -1
  20. package/dist/ui/checkbox/public.d.ts.map +1 -1
  21. package/dist/ui/combobox/multi.d.ts +31 -10
  22. package/dist/ui/combobox/multi.d.ts.map +1 -1
  23. package/dist/ui/combobox/multi.js +1 -1
  24. package/dist/ui/combobox/public.d.ts +1 -1
  25. package/dist/ui/combobox/public.d.ts.map +1 -1
  26. package/dist/ui/combobox/shared.d.ts +53 -14
  27. package/dist/ui/combobox/shared.d.ts.map +1 -1
  28. package/dist/ui/combobox/shared.js +72 -28
  29. package/dist/ui/combobox/single.d.ts +31 -10
  30. package/dist/ui/combobox/single.d.ts.map +1 -1
  31. package/dist/ui/combobox/single.js +1 -1
  32. package/dist/ui/dialog/index.d.ts +14 -8
  33. package/dist/ui/dialog/index.d.ts.map +1 -1
  34. package/dist/ui/dialog/index.js +21 -12
  35. package/dist/ui/dialog/public.d.ts +1 -1
  36. package/dist/ui/dialog/public.d.ts.map +1 -1
  37. package/dist/ui/dialog/public.js +1 -1
  38. package/dist/ui/disclosure/index.d.ts +11 -8
  39. package/dist/ui/disclosure/index.d.ts.map +1 -1
  40. package/dist/ui/disclosure/index.js +20 -18
  41. package/dist/ui/disclosure/public.d.ts +1 -1
  42. package/dist/ui/disclosure/public.d.ts.map +1 -1
  43. package/dist/ui/listbox/multi.d.ts +34 -9
  44. package/dist/ui/listbox/multi.d.ts.map +1 -1
  45. package/dist/ui/listbox/multi.js +1 -1
  46. package/dist/ui/listbox/public.d.ts +1 -1
  47. package/dist/ui/listbox/public.d.ts.map +1 -1
  48. package/dist/ui/listbox/shared.d.ts +57 -13
  49. package/dist/ui/listbox/shared.d.ts.map +1 -1
  50. package/dist/ui/listbox/shared.js +77 -27
  51. package/dist/ui/listbox/single.d.ts +34 -9
  52. package/dist/ui/listbox/single.d.ts.map +1 -1
  53. package/dist/ui/listbox/single.js +1 -1
  54. package/dist/ui/menu/index.d.ts +39 -11
  55. package/dist/ui/menu/index.d.ts.map +1 -1
  56. package/dist/ui/menu/index.js +81 -32
  57. package/dist/ui/menu/public.d.ts +1 -1
  58. package/dist/ui/menu/public.d.ts.map +1 -1
  59. package/dist/ui/popover/index.d.ts +28 -12
  60. package/dist/ui/popover/index.d.ts.map +1 -1
  61. package/dist/ui/popover/index.js +50 -24
  62. package/dist/ui/popover/public.d.ts +1 -1
  63. package/dist/ui/popover/public.d.ts.map +1 -1
  64. package/dist/ui/radioGroup/index.d.ts +8 -7
  65. package/dist/ui/radioGroup/index.d.ts.map +1 -1
  66. package/dist/ui/radioGroup/index.js +12 -9
  67. package/dist/ui/radioGroup/public.d.ts +1 -1
  68. package/dist/ui/radioGroup/public.d.ts.map +1 -1
  69. package/dist/ui/switch/index.d.ts +5 -9
  70. package/dist/ui/switch/index.d.ts.map +1 -1
  71. package/dist/ui/switch/index.js +7 -10
  72. package/dist/ui/switch/public.d.ts +1 -1
  73. package/dist/ui/switch/public.d.ts.map +1 -1
  74. package/dist/ui/tabs/index.d.ts +9 -7
  75. package/dist/ui/tabs/index.d.ts.map +1 -1
  76. package/dist/ui/tabs/index.js +27 -17
  77. package/dist/ui/tabs/public.d.ts +1 -1
  78. package/dist/ui/tabs/public.d.ts.map +1 -1
  79. package/package.json +3 -3
@@ -1,7 +1,6 @@
1
1
  import { Array, Effect, Match as M, Option, Predicate, Schema as S, String as Str, pipe, } from 'effect';
2
2
  import { OptionExt } from '../../effectExtensions';
3
- import { html } from '../../html';
4
- import { createLazy } from '../../html/lazy';
3
+ import { createLazy, html } from '../../html';
5
4
  import { m } from '../../message';
6
5
  import { evo } from '../../struct';
7
6
  import * as Task from '../../task';
@@ -68,8 +67,28 @@ export const MovedPointerOverItem = m('MovedPointerOverItem', {
68
67
  screenX: S.Number,
69
68
  screenY: S.Number,
70
69
  });
71
- /** Placeholder message used when no action is needed. */
72
- export const NoOp = m('NoOp');
70
+ /** Sent when the focus-items command completes after opening the menu. */
71
+ export const CompletedItemsFocus = m('CompletedItemsFocus');
72
+ /** Sent when the focus-button command completes after closing or selecting. */
73
+ export const CompletedButtonFocus = m('CompletedButtonFocus');
74
+ /** Sent when the scroll lock command completes. */
75
+ export const CompletedScrollLock = m('CompletedScrollLock');
76
+ /** Sent when the scroll unlock command completes. */
77
+ export const CompletedScrollUnlock = m('CompletedScrollUnlock');
78
+ /** Sent when the inert-others command completes. */
79
+ export const CompletedInertSetup = m('CompletedInertSetup');
80
+ /** Sent when the restore-inert command completes. */
81
+ export const CompletedInertTeardown = m('CompletedInertTeardown');
82
+ /** Sent when the scroll-into-view command completes after keyboard activation. */
83
+ export const CompletedScrollIntoView = m('CompletedScrollIntoView');
84
+ /** Sent when the programmatic click command completes. */
85
+ export const CompletedItemClick = m('CompletedItemClick');
86
+ /** Sent when the advance-focus command completes. */
87
+ export const CompletedFocusAdvance = m('CompletedFocusAdvance');
88
+ /** Sent when a mouse click on the button is ignored because pointer-down already handled the toggle. */
89
+ export const IgnoredMouseClick = m('IgnoredMouseClick');
90
+ /** Sent when a Space key-up is captured to prevent page scrolling. */
91
+ export const SuppressedSpaceScroll = m('SuppressedSpaceScroll');
73
92
  /** Sent internally when a double-rAF completes, advancing the transition to its animating phase. */
74
93
  export const AdvancedTransitionFrame = m('AdvancedTransitionFrame');
75
94
  /** Sent internally when all CSS transitions on the menu items container have completed. */
@@ -91,7 +110,7 @@ export const ReleasedPointerOnItems = m('ReleasedPointerOnItems', {
91
110
  timeStamp: S.Number,
92
111
  });
93
112
  /** Union of all messages the menu component can produce. */
94
- export const Message = S.Union(Opened, Closed, ClosedByTab, ActivatedItem, DeactivatedItem, SelectedItem, MovedPointerOverItem, RequestedItemClick, Searched, ClearedSearch, NoOp, AdvancedTransitionFrame, EndedTransition, DetectedButtonMovement, PressedPointerOnButton, ReleasedPointerOnItems);
113
+ export const Message = S.Union(Opened, Closed, ClosedByTab, ActivatedItem, DeactivatedItem, SelectedItem, MovedPointerOverItem, RequestedItemClick, Searched, ClearedSearch, CompletedItemsFocus, CompletedButtonFocus, CompletedScrollLock, CompletedScrollUnlock, CompletedInertSetup, CompletedInertTeardown, CompletedScrollIntoView, CompletedItemClick, CompletedFocusAdvance, IgnoredMouseClick, SuppressedSpaceScroll, AdvancedTransitionFrame, EndedTransition, DetectedButtonMovement, PressedPointerOnButton, ReleasedPointerOnItems);
95
114
  // INIT
96
115
  const SEARCH_DEBOUNCE_MILLISECONDS = 350;
97
116
  const LEFT_MOUSE_BUTTON = 0;
@@ -131,13 +150,13 @@ const withUpdateReturn = M.withReturnType();
131
150
  /** Processes a menu message and returns the next model and commands. */
132
151
  export const update = (model, message) => {
133
152
  const maybeNextFrame = OptionExt.when(model.isAnimated, Task.nextFrame.pipe(Effect.as(AdvancedTransitionFrame())));
134
- const maybeLockScroll = OptionExt.when(model.isModal, Task.lockScroll.pipe(Effect.as(NoOp())));
135
- const maybeUnlockScroll = OptionExt.when(model.isModal, Task.unlockScroll.pipe(Effect.as(NoOp())));
153
+ const maybeLockScroll = OptionExt.when(model.isModal, Task.lockScroll.pipe(Effect.as(CompletedScrollLock())));
154
+ const maybeUnlockScroll = OptionExt.when(model.isModal, Task.unlockScroll.pipe(Effect.as(CompletedScrollUnlock())));
136
155
  const maybeInertOthers = OptionExt.when(model.isModal, Task.inertOthers(model.id, [
137
156
  buttonSelector(model.id),
138
157
  itemsSelector(model.id),
139
- ]).pipe(Effect.as(NoOp())));
140
- const maybeRestoreInert = OptionExt.when(model.isModal, Task.restoreInert(model.id).pipe(Effect.as(NoOp())));
158
+ ]).pipe(Effect.as(CompletedInertSetup())));
159
+ const maybeRestoreInert = OptionExt.when(model.isModal, Task.restoreInert(model.id).pipe(Effect.as(CompletedInertTeardown())));
141
160
  return M.value(message).pipe(withUpdateReturn, M.tagsExhaustive({
142
161
  Opened: ({ maybeActiveItemIndex }) => {
143
162
  const nextModel = evo(model, {
@@ -154,7 +173,7 @@ export const update = (model, message) => {
154
173
  });
155
174
  return [
156
175
  nextModel,
157
- pipe(Array.getSomes([maybeNextFrame, maybeLockScroll, maybeInertOthers]), Array.prepend(Task.focus(itemsSelector(model.id)).pipe(Effect.ignore, Effect.as(NoOp())))),
176
+ pipe(Array.getSomes([maybeNextFrame, maybeLockScroll, maybeInertOthers]), Array.prepend(Task.focus(itemsSelector(model.id)).pipe(Effect.ignore, Effect.as(CompletedItemsFocus())))),
158
177
  ];
159
178
  },
160
179
  Closed: () => [
@@ -163,7 +182,7 @@ export const update = (model, message) => {
163
182
  maybeNextFrame,
164
183
  maybeUnlockScroll,
165
184
  maybeRestoreInert,
166
- ]), Array.prepend(Task.focus(buttonSelector(model.id)).pipe(Effect.ignore, Effect.as(NoOp())))),
185
+ ]), Array.prepend(Task.focus(buttonSelector(model.id)).pipe(Effect.ignore, Effect.as(CompletedButtonFocus())))),
167
186
  ],
168
187
  ClosedByTab: () => [
169
188
  closedModel(model),
@@ -176,7 +195,7 @@ export const update = (model, message) => {
176
195
  }),
177
196
  activationTrigger === 'Keyboard'
178
197
  ? [
179
- Task.scrollIntoView(itemSelector(model.id, index)).pipe(Effect.ignore, Effect.as(NoOp())),
198
+ Task.scrollIntoView(itemSelector(model.id, index)).pipe(Effect.ignore, Effect.as(CompletedScrollIntoView())),
180
199
  ]
181
200
  : [],
182
201
  ],
@@ -203,12 +222,12 @@ export const update = (model, message) => {
203
222
  maybeNextFrame,
204
223
  maybeUnlockScroll,
205
224
  maybeRestoreInert,
206
- ]), Array.prepend(Task.focus(buttonSelector(model.id)).pipe(Effect.ignore, Effect.as(NoOp())))),
225
+ ]), Array.prepend(Task.focus(buttonSelector(model.id)).pipe(Effect.ignore, Effect.as(CompletedButtonFocus())))),
207
226
  ],
208
227
  RequestedItemClick: ({ index }) => [
209
228
  model,
210
229
  [
211
- Task.clickElement(itemSelector(model.id, index)).pipe(Effect.ignore, Effect.as(NoOp())),
230
+ Task.clickElement(itemSelector(model.id, index)).pipe(Effect.ignore, Effect.as(CompletedItemClick())),
212
231
  ],
213
232
  ],
214
233
  Searched: ({ key, maybeTargetIndex }) => {
@@ -264,7 +283,7 @@ export const update = (model, message) => {
264
283
  maybeNextFrame,
265
284
  maybeUnlockScroll,
266
285
  maybeRestoreInert,
267
- ]), Array.prepend(Task.focus(buttonSelector(model.id)).pipe(Effect.ignore, Effect.as(NoOp())))),
286
+ ]), Array.prepend(Task.focus(buttonSelector(model.id)).pipe(Effect.ignore, Effect.as(CompletedButtonFocus())))),
268
287
  ];
269
288
  }
270
289
  const nextModel = evo(withPointerType, {
@@ -279,7 +298,7 @@ export const update = (model, message) => {
279
298
  });
280
299
  return [
281
300
  nextModel,
282
- pipe(Array.getSomes([maybeNextFrame, maybeLockScroll, maybeInertOthers]), Array.prepend(Task.focus(itemsSelector(model.id)).pipe(Effect.ignore, Effect.as(NoOp())))),
301
+ pipe(Array.getSomes([maybeNextFrame, maybeLockScroll, maybeInertOthers]), Array.prepend(Task.focus(itemsSelector(model.id)).pipe(Effect.ignore, Effect.as(CompletedItemsFocus())))),
283
302
  ];
284
303
  },
285
304
  ReleasedPointerOnItems: ({ screenX, screenY, timeStamp }) => {
@@ -299,11 +318,21 @@ export const update = (model, message) => {
299
318
  return [
300
319
  model,
301
320
  [
302
- Task.clickElement(itemSelector(model.id, model.maybeActiveItemIndex.value)).pipe(Effect.ignore, Effect.as(NoOp())),
321
+ Task.clickElement(itemSelector(model.id, model.maybeActiveItemIndex.value)).pipe(Effect.ignore, Effect.as(CompletedItemClick())),
303
322
  ],
304
323
  ];
305
324
  },
306
- NoOp: () => [model, []],
325
+ CompletedItemsFocus: () => [model, []],
326
+ CompletedButtonFocus: () => [model, []],
327
+ CompletedScrollLock: () => [model, []],
328
+ CompletedScrollUnlock: () => [model, []],
329
+ CompletedInertSetup: () => [model, []],
330
+ CompletedInertTeardown: () => [model, []],
331
+ CompletedScrollIntoView: () => [model, []],
332
+ CompletedItemClick: () => [model, []],
333
+ CompletedFocusAdvance: () => [model, []],
334
+ IgnoredMouseClick: () => [model, []],
335
+ SuppressedSpaceScroll: () => [model, []],
307
336
  }));
308
337
  };
309
338
  export { groupContiguous, resolveTypeaheadMatch };
@@ -311,7 +340,7 @@ const itemId = (id, index) => `${id}-item-${index}`;
311
340
  /** Renders a headless menu with typeahead search, keyboard navigation, and aria-activedescendant focus management. */
312
341
  export const view = (config) => {
313
342
  const { div, AriaActiveDescendant, AriaControls, AriaDisabled, AriaExpanded, AriaHasPopup, AriaLabelledBy, Class, DataAttribute, Id, OnBlur, OnClick, OnDestroy, OnInsert, OnKeyDownPreventDefault, OnKeyUpPreventDefault, OnPointerDown, OnPointerLeave, OnPointerMove, OnPointerUp, Role, Style, Tabindex, Type, keyed, } = html();
314
- const { model: { id, isOpen, transitionState, maybeActiveItemIndex, searchQuery, maybeLastButtonPointerType, }, toMessage, items, itemToConfig, isItemDisabled, itemToSearchText = (item) => item, isButtonDisabled, buttonContent, buttonClassName, itemsClassName, itemsScrollClassName, backdropClassName, className, itemGroupKey, groupToHeading, groupClassName, separatorClassName, anchor, } = config;
343
+ const { model: { id, isOpen, transitionState, maybeActiveItemIndex, searchQuery, maybeLastButtonPointerType, }, toMessage, items, itemToConfig, isItemDisabled, itemToSearchText = (item) => item, isButtonDisabled, buttonContent, buttonClassName, buttonAttributes = [], itemsClassName, itemsAttributes = [], itemsScrollClassName, itemsScrollAttributes = [], backdropClassName, backdropAttributes = [], className, attributes = [], itemGroupKey, groupToHeading, groupClassName, groupAttributes = [], separatorClassName, separatorAttributes = [], anchor, } = config;
315
344
  const isLeaving = transitionState === 'LeaveStart' || transitionState === 'LeaveAnimating';
316
345
  const isVisible = isOpen || isLeaving;
317
346
  const transitionAttributes = M.value(transitionState).pipe(M.when('EnterStart', () => [
@@ -348,7 +377,7 @@ export const view = (config) => {
348
377
  const handleButtonClick = () => {
349
378
  const isMouse = Option.exists(maybeLastButtonPointerType, type => type === 'mouse');
350
379
  if (isMouse) {
351
- return toMessage(NoOp());
380
+ return toMessage(IgnoredMouseClick());
352
381
  }
353
382
  else if (isOpen) {
354
383
  return toMessage(Closed());
@@ -357,7 +386,7 @@ export const view = (config) => {
357
386
  return toMessage(Opened({ maybeActiveItemIndex: Option.none() }));
358
387
  }
359
388
  };
360
- const handleSpaceKeyUp = (key) => OptionExt.when(key === ' ', toMessage(NoOp()));
389
+ const handleSpaceKeyUp = (key) => OptionExt.when(key === ' ', toMessage(SuppressedSpaceScroll()));
361
390
  const resolveActiveIndex = keyToIndex('ArrowDown', 'ArrowUp', items.length, Option.getOrElse(maybeActiveItemIndex, () => 0), isDisabled);
362
391
  const searchForKey = (key) => {
363
392
  const nextQuery = searchQuery + key;
@@ -371,10 +400,9 @@ export const view = (config) => {
371
400
  activationTrigger: 'Keyboard',
372
401
  })))), M.when(isPrintableKey, () => searchForKey(key)), M.orElse(() => Option.none()));
373
402
  const handleItemsPointerUp = (screenX, screenY, pointerType, timeStamp) => OptionExt.when(pointerType === 'mouse', toMessage(ReleasedPointerOnItems({ screenX, screenY, timeStamp })));
374
- const buttonAttributes = [
403
+ const resolvedButtonAttributes = [
375
404
  Id(`${id}-button`),
376
405
  Type('button'),
377
- Class(buttonClassName),
378
406
  AriaHasPopup('menu'),
379
407
  AriaExpanded(isVisible),
380
408
  AriaControls(`${id}-items`),
@@ -387,6 +415,8 @@ export const view = (config) => {
387
415
  OnClick(handleButtonClick()),
388
416
  ]),
389
417
  ...(isVisible ? [DataAttribute('open', '')] : []),
418
+ ...(buttonClassName ? [Class(buttonClassName)] : []),
419
+ ...buttonAttributes,
390
420
  ];
391
421
  const maybeActiveDescendant = Option.match(maybeActiveItemIndex, {
392
422
  onNone: () => [],
@@ -408,7 +438,6 @@ export const view = (config) => {
408
438
  AriaLabelledBy(`${id}-button`),
409
439
  ...maybeActiveDescendant,
410
440
  Tabindex(0),
411
- Class(itemsClassName),
412
441
  ...anchorAttributes,
413
442
  ...transitionAttributes,
414
443
  ...(isLeaving
@@ -419,6 +448,8 @@ export const view = (config) => {
419
448
  OnPointerUp(handleItemsPointerUp),
420
449
  OnBlur(toMessage(ClosedByTab())),
421
450
  ]),
451
+ ...(itemsClassName ? [Class(itemsClassName)] : []),
452
+ ...itemsAttributes,
422
453
  ];
423
454
  const menuItems = Array.map(items, (item, index) => {
424
455
  const isActiveItem = Option.exists(maybeActiveItemIndex, activeIndex => activeIndex === index);
@@ -431,7 +462,6 @@ export const view = (config) => {
431
462
  return keyed('div')(itemId(id, index), [
432
463
  Id(itemId(id, index)),
433
464
  Role('menuitem'),
434
- Class(itemConfig.className),
435
465
  ...(isActiveItem ? [DataAttribute('active', '')] : []),
436
466
  ...(isDisabledItem
437
467
  ? [AriaDisabled(true), DataAttribute('disabled', '')]
@@ -447,6 +477,7 @@ export const view = (config) => {
447
477
  OnPointerLeave(pointerType => OptionExt.when(pointerType !== 'touch', toMessage(DeactivatedItem()))),
448
478
  ]
449
479
  : []),
480
+ ...(itemConfig.className ? [Class(itemConfig.className)] : []),
450
481
  ], [itemConfig.content]);
451
482
  });
452
483
  const renderGroupedItems = () => {
@@ -463,7 +494,11 @@ export const view = (config) => {
463
494
  const headingElement = Option.match(maybeHeading, {
464
495
  onNone: () => [],
465
496
  onSome: heading => [
466
- keyed('div')(headingId, [Id(headingId), Role('presentation'), Class(heading.className)], [heading.content]),
497
+ keyed('div')(headingId, [
498
+ Id(headingId),
499
+ Role('presentation'),
500
+ ...(heading.className ? [Class(heading.className)] : []),
501
+ ], [heading.content]),
467
502
  ],
468
503
  });
469
504
  const groupContent = [...headingElement, ...segment.items];
@@ -471,22 +506,35 @@ export const view = (config) => {
471
506
  Role('group'),
472
507
  ...(Option.isSome(maybeHeading) ? [AriaLabelledBy(headingId)] : []),
473
508
  ...(groupClassName ? [Class(groupClassName)] : []),
509
+ ...groupAttributes,
474
510
  ], groupContent);
475
- const separator = segmentIndex > 0 && separatorClassName
511
+ const separator = segmentIndex > 0 &&
512
+ (separatorClassName ||
513
+ Array.isNonEmptyReadonlyArray(separatorAttributes))
476
514
  ? [
477
- keyed('div')(`${id}-separator-${segmentIndex}`, [Role('separator'), Class(separatorClassName)], []),
515
+ keyed('div')(`${id}-separator-${segmentIndex}`, [
516
+ Role('separator'),
517
+ ...(separatorClassName ? [Class(separatorClassName)] : []),
518
+ ...separatorAttributes,
519
+ ], []),
478
520
  ]
479
521
  : [];
480
522
  return [...separator, groupElement];
481
523
  });
482
524
  };
483
525
  const backdrop = keyed('div')(`${id}-backdrop`, [
484
- Class(backdropClassName),
485
526
  ...(isLeaving ? [] : [OnClick(toMessage(Closed()))]),
527
+ ...(backdropClassName ? [Class(backdropClassName)] : []),
528
+ ...backdropAttributes,
486
529
  ], []);
487
530
  const renderedItems = renderGroupedItems();
488
- const scrollableItems = itemsScrollClassName
489
- ? [div([Class(itemsScrollClassName)], renderedItems)]
531
+ const scrollableItems = itemsScrollClassName || Array.isNonEmptyReadonlyArray(itemsScrollAttributes)
532
+ ? [
533
+ div([
534
+ ...(itemsScrollClassName ? [Class(itemsScrollClassName)] : []),
535
+ ...itemsScrollAttributes,
536
+ ], renderedItems),
537
+ ]
490
538
  : renderedItems;
491
539
  const visibleContent = [
492
540
  backdrop,
@@ -494,10 +542,11 @@ export const view = (config) => {
494
542
  ];
495
543
  const wrapperAttributes = [
496
544
  ...(className ? [Class(className)] : []),
545
+ ...attributes,
497
546
  ...(isVisible ? [DataAttribute('open', '')] : []),
498
547
  ];
499
548
  return div(wrapperAttributes, [
500
- keyed('button')(`${id}-button`, buttonAttributes, [buttonContent]),
549
+ keyed('button')(`${id}-button`, resolvedButtonAttributes, [buttonContent]),
501
550
  ...(isVisible ? visibleContent : []),
502
551
  ]);
503
552
  };
@@ -1,5 +1,5 @@
1
1
  export { init, update, view, lazy, Model, Message } from './index';
2
2
  export { TransitionState } from '../transition';
3
- export type { ActivationTrigger, Opened, Closed, ClosedByTab, ActivatedItem, DeactivatedItem, SelectedItem, MovedPointerOverItem, RequestedItemClick, Searched, ClearedSearch, PressedPointerOnButton, ReleasedPointerOnItems, NoOp, AdvancedTransitionFrame, EndedTransition, DetectedButtonMovement, InitConfig, ViewConfig, ItemConfig, GroupHeading, } from './index';
3
+ export type { ActivationTrigger, Opened, Closed, ClosedByTab, ActivatedItem, DeactivatedItem, SelectedItem, MovedPointerOverItem, RequestedItemClick, Searched, ClearedSearch, PressedPointerOnButton, ReleasedPointerOnItems, IgnoredMouseClick, SuppressedSpaceScroll, AdvancedTransitionFrame, EndedTransition, DetectedButtonMovement, InitConfig, ViewConfig, ItemConfig, GroupHeading, } from './index';
4
4
  export type { AnchorConfig } from '../anchor';
5
5
  //# sourceMappingURL=public.d.ts.map
@@ -1 +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,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAElE,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAE/C,YAAY,EACV,iBAAiB,EACjB,MAAM,EACN,MAAM,EACN,WAAW,EACX,aAAa,EACb,eAAe,EACf,YAAY,EACZ,oBAAoB,EACpB,kBAAkB,EAClB,QAAQ,EACR,aAAa,EACb,sBAAsB,EACtB,sBAAsB,EACtB,IAAI,EACJ,uBAAuB,EACvB,eAAe,EACf,sBAAsB,EACtB,UAAU,EACV,UAAU,EACV,UAAU,EACV,YAAY,GACb,MAAM,SAAS,CAAA;AAEhB,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA"}
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,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAElE,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAE/C,YAAY,EACV,iBAAiB,EACjB,MAAM,EACN,MAAM,EACN,WAAW,EACX,aAAa,EACb,eAAe,EACf,YAAY,EACZ,oBAAoB,EACpB,kBAAkB,EAClB,QAAQ,EACR,aAAa,EACb,sBAAsB,EACtB,sBAAsB,EACtB,iBAAiB,EACjB,qBAAqB,EACrB,uBAAuB,EACvB,eAAe,EACf,sBAAsB,EACtB,UAAU,EACV,UAAU,EACV,UAAU,EACV,YAAY,GACb,MAAM,SAAS,CAAA;AAEhB,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA"}
@@ -1,6 +1,6 @@
1
1
  import { Schema as S } from 'effect';
2
2
  import type { Command } from '../../command';
3
- import { type Html } from '../../html';
3
+ import { type Attribute, type Html } from '../../html';
4
4
  import type { AnchorConfig } from '../anchor';
5
5
  /** Schema for the popover component's state, tracking open/closed status and transition animation. */
6
6
  export declare const Model: S.Struct<{
@@ -23,8 +23,22 @@ export declare const PressedPointerOnButton: import("../../schema").CallableTagg
23
23
  pointerType: typeof S.String;
24
24
  button: typeof S.Number;
25
25
  }>;
26
- /** Placeholder message used when no action is needed. */
27
- export declare const NoOp: import("../../schema").CallableTaggedStruct<"NoOp", {}>;
26
+ /** Sent when the focus-panel command completes after opening the popover. */
27
+ export declare const CompletedPanelFocus: import("../../schema").CallableTaggedStruct<"CompletedPanelFocus", {}>;
28
+ /** Sent when the focus-button command completes after closing. */
29
+ export declare const CompletedButtonFocus: import("../../schema").CallableTaggedStruct<"CompletedButtonFocus", {}>;
30
+ /** Sent when the scroll lock command completes. */
31
+ export declare const CompletedScrollLock: import("../../schema").CallableTaggedStruct<"CompletedScrollLock", {}>;
32
+ /** Sent when the scroll unlock command completes. */
33
+ export declare const CompletedScrollUnlock: import("../../schema").CallableTaggedStruct<"CompletedScrollUnlock", {}>;
34
+ /** Sent when the inert-others command completes. */
35
+ export declare const CompletedInertSetup: import("../../schema").CallableTaggedStruct<"CompletedInertSetup", {}>;
36
+ /** Sent when the restore-inert command completes. */
37
+ export declare const CompletedInertTeardown: import("../../schema").CallableTaggedStruct<"CompletedInertTeardown", {}>;
38
+ /** Sent when a mouse click on the button is ignored because pointer-down already handled the toggle. */
39
+ export declare const IgnoredMouseClick: import("../../schema").CallableTaggedStruct<"IgnoredMouseClick", {}>;
40
+ /** Sent when a Space key-up is captured to prevent page scrolling. */
41
+ export declare const SuppressedSpaceScroll: import("../../schema").CallableTaggedStruct<"SuppressedSpaceScroll", {}>;
28
42
  /** Sent internally when a double-rAF completes, advancing the transition to its animating phase. */
29
43
  export declare const AdvancedTransitionFrame: import("../../schema").CallableTaggedStruct<"AdvancedTransitionFrame", {}>;
30
44
  /** Sent internally when all CSS transitions on the popover panel have completed. */
@@ -35,15 +49,13 @@ export declare const DetectedButtonMovement: import("../../schema").CallableTagg
35
49
  export declare const Message: S.Union<[import("../../schema").CallableTaggedStruct<"Opened", {}>, import("../../schema").CallableTaggedStruct<"Closed", {}>, import("../../schema").CallableTaggedStruct<"ClosedByTab", {}>, import("../../schema").CallableTaggedStruct<"PressedPointerOnButton", {
36
50
  pointerType: typeof S.String;
37
51
  button: typeof S.Number;
38
- }>, import("../../schema").CallableTaggedStruct<"NoOp", {}>, import("../../schema").CallableTaggedStruct<"AdvancedTransitionFrame", {}>, import("../../schema").CallableTaggedStruct<"EndedTransition", {}>, import("../../schema").CallableTaggedStruct<"DetectedButtonMovement", {}>]>;
52
+ }>, import("../../schema").CallableTaggedStruct<"CompletedPanelFocus", {}>, import("../../schema").CallableTaggedStruct<"CompletedButtonFocus", {}>, import("../../schema").CallableTaggedStruct<"CompletedScrollLock", {}>, import("../../schema").CallableTaggedStruct<"CompletedScrollUnlock", {}>, import("../../schema").CallableTaggedStruct<"CompletedInertSetup", {}>, import("../../schema").CallableTaggedStruct<"CompletedInertTeardown", {}>, import("../../schema").CallableTaggedStruct<"IgnoredMouseClick", {}>, import("../../schema").CallableTaggedStruct<"SuppressedSpaceScroll", {}>, import("../../schema").CallableTaggedStruct<"AdvancedTransitionFrame", {}>, import("../../schema").CallableTaggedStruct<"EndedTransition", {}>, import("../../schema").CallableTaggedStruct<"DetectedButtonMovement", {}>]>;
39
53
  export type Opened = typeof Opened.Type;
40
54
  export type Closed = typeof Closed.Type;
41
55
  export type ClosedByTab = typeof ClosedByTab.Type;
42
56
  export type PressedPointerOnButton = typeof PressedPointerOnButton.Type;
43
- export type NoOp = typeof NoOp.Type;
44
- export type AdvancedTransitionFrame = typeof AdvancedTransitionFrame.Type;
45
- export type EndedTransition = typeof EndedTransition.Type;
46
- export type DetectedButtonMovement = typeof DetectedButtonMovement.Type;
57
+ export type IgnoredMouseClick = typeof IgnoredMouseClick.Type;
58
+ export type SuppressedSpaceScroll = typeof SuppressedSpaceScroll.Type;
47
59
  export type Message = typeof Message.Type;
48
60
  /** Configuration for creating a popover model with `init`. `isAnimated` enables CSS transition coordination (default `false`). `isModal` locks page scroll and inerts other elements when open (default `false`). */
49
61
  export type InitConfig = Readonly<{
@@ -59,15 +71,19 @@ export declare const update: (model: Model, message: Message) => UpdateReturn;
59
71
  /** Configuration for rendering a popover with `view`. */
60
72
  export type ViewConfig<Message> = Readonly<{
61
73
  model: Model;
62
- toMessage: (message: Opened | Closed | ClosedByTab | PressedPointerOnButton | NoOp) => Message;
74
+ toMessage: (message: Opened | Closed | ClosedByTab | PressedPointerOnButton | IgnoredMouseClick | SuppressedSpaceScroll) => Message;
63
75
  anchor: AnchorConfig;
64
76
  buttonContent: Html;
65
- buttonClassName: string;
77
+ buttonClassName?: string;
78
+ buttonAttributes?: ReadonlyArray<Attribute<Message>>;
66
79
  panelContent: Html;
67
- panelClassName: string;
68
- backdropClassName: string;
80
+ panelClassName?: string;
81
+ panelAttributes?: ReadonlyArray<Attribute<Message>>;
82
+ backdropClassName?: string;
83
+ backdropAttributes?: ReadonlyArray<Attribute<Message>>;
69
84
  isDisabled?: boolean;
70
85
  className?: string;
86
+ attributes?: ReadonlyArray<Attribute<Message>>;
71
87
  }>;
72
88
  /** Renders a headless popover with a trigger button and a floating panel. Uses the disclosure ARIA pattern (aria-expanded + aria-controls) with no role on the panel. */
73
89
  export declare const view: <Message>(config: ViewConfig<Message>) => Html;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/popover/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/popover/index.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,6 @@
1
1
  import { Array, Effect, Match as M, Option, Schema as S, pipe } from 'effect';
2
2
  import { OptionExt } from '../../effectExtensions';
3
- import { html } from '../../html';
4
- import { createLazy } from '../../html/lazy';
3
+ import { createLazy, html } from '../../html';
5
4
  import { m } from '../../message';
6
5
  import { evo } from '../../struct';
7
6
  import * as Task from '../../task';
@@ -29,8 +28,22 @@ export const PressedPointerOnButton = m('PressedPointerOnButton', {
29
28
  pointerType: S.String,
30
29
  button: S.Number,
31
30
  });
32
- /** Placeholder message used when no action is needed. */
33
- export const NoOp = m('NoOp');
31
+ /** Sent when the focus-panel command completes after opening the popover. */
32
+ export const CompletedPanelFocus = m('CompletedPanelFocus');
33
+ /** Sent when the focus-button command completes after closing. */
34
+ export const CompletedButtonFocus = m('CompletedButtonFocus');
35
+ /** Sent when the scroll lock command completes. */
36
+ export const CompletedScrollLock = m('CompletedScrollLock');
37
+ /** Sent when the scroll unlock command completes. */
38
+ export const CompletedScrollUnlock = m('CompletedScrollUnlock');
39
+ /** Sent when the inert-others command completes. */
40
+ export const CompletedInertSetup = m('CompletedInertSetup');
41
+ /** Sent when the restore-inert command completes. */
42
+ export const CompletedInertTeardown = m('CompletedInertTeardown');
43
+ /** Sent when a mouse click on the button is ignored because pointer-down already handled the toggle. */
44
+ export const IgnoredMouseClick = m('IgnoredMouseClick');
45
+ /** Sent when a Space key-up is captured to prevent page scrolling. */
46
+ export const SuppressedSpaceScroll = m('SuppressedSpaceScroll');
34
47
  /** Sent internally when a double-rAF completes, advancing the transition to its animating phase. */
35
48
  export const AdvancedTransitionFrame = m('AdvancedTransitionFrame');
36
49
  /** Sent internally when all CSS transitions on the popover panel have completed. */
@@ -38,7 +51,7 @@ export const EndedTransition = m('EndedTransition');
38
51
  /** Sent internally when the popover button moves in the viewport during a leave transition, cancelling the animation. */
39
52
  export const DetectedButtonMovement = m('DetectedButtonMovement');
40
53
  /** Union of all messages the popover component can produce. */
41
- export const Message = S.Union(Opened, Closed, ClosedByTab, PressedPointerOnButton, NoOp, AdvancedTransitionFrame, EndedTransition, DetectedButtonMovement);
54
+ export const Message = S.Union(Opened, Closed, ClosedByTab, PressedPointerOnButton, CompletedPanelFocus, CompletedButtonFocus, CompletedScrollLock, CompletedScrollUnlock, CompletedInertSetup, CompletedInertTeardown, IgnoredMouseClick, SuppressedSpaceScroll, AdvancedTransitionFrame, EndedTransition, DetectedButtonMovement);
42
55
  // INIT
43
56
  const LEFT_MOUSE_BUTTON = 0;
44
57
  /** Creates an initial popover model from a config. Defaults to closed. */
@@ -62,13 +75,13 @@ const withUpdateReturn = M.withReturnType();
62
75
  /** Processes a popover message and returns the next model and commands. */
63
76
  export const update = (model, message) => {
64
77
  const maybeNextFrame = OptionExt.when(model.isAnimated, Task.nextFrame.pipe(Effect.as(AdvancedTransitionFrame())));
65
- const maybeLockScroll = OptionExt.when(model.isModal, Task.lockScroll.pipe(Effect.as(NoOp())));
66
- const maybeUnlockScroll = OptionExt.when(model.isModal, Task.unlockScroll.pipe(Effect.as(NoOp())));
78
+ const maybeLockScroll = OptionExt.when(model.isModal, Task.lockScroll.pipe(Effect.as(CompletedScrollLock())));
79
+ const maybeUnlockScroll = OptionExt.when(model.isModal, Task.unlockScroll.pipe(Effect.as(CompletedScrollUnlock())));
67
80
  const maybeInertOthers = OptionExt.when(model.isModal, Task.inertOthers(model.id, [
68
81
  buttonSelector(model.id),
69
82
  panelSelector(model.id),
70
- ]).pipe(Effect.as(NoOp())));
71
- const maybeRestoreInert = OptionExt.when(model.isModal, Task.restoreInert(model.id).pipe(Effect.as(NoOp())));
83
+ ]).pipe(Effect.as(CompletedInertSetup())));
84
+ const maybeRestoreInert = OptionExt.when(model.isModal, Task.restoreInert(model.id).pipe(Effect.as(CompletedInertTeardown())));
72
85
  return M.value(message).pipe(withUpdateReturn, M.tagsExhaustive({
73
86
  Opened: () => {
74
87
  const nextModel = evo(model, {
@@ -77,7 +90,7 @@ export const update = (model, message) => {
77
90
  });
78
91
  return [
79
92
  nextModel,
80
- pipe(Array.getSomes([maybeNextFrame, maybeLockScroll, maybeInertOthers]), Array.prepend(Task.focus(panelSelector(model.id)).pipe(Effect.ignore, Effect.as(NoOp())))),
93
+ pipe(Array.getSomes([maybeNextFrame, maybeLockScroll, maybeInertOthers]), Array.prepend(Task.focus(panelSelector(model.id)).pipe(Effect.ignore, Effect.as(CompletedPanelFocus())))),
81
94
  ];
82
95
  },
83
96
  Closed: () => [
@@ -86,7 +99,7 @@ export const update = (model, message) => {
86
99
  maybeNextFrame,
87
100
  maybeUnlockScroll,
88
101
  maybeRestoreInert,
89
- ]), Array.prepend(Task.focus(buttonSelector(model.id)).pipe(Effect.ignore, Effect.as(NoOp())))),
102
+ ]), Array.prepend(Task.focus(buttonSelector(model.id)).pipe(Effect.ignore, Effect.as(CompletedButtonFocus())))),
90
103
  ],
91
104
  ClosedByTab: () => [
92
105
  closedModel(model),
@@ -106,7 +119,7 @@ export const update = (model, message) => {
106
119
  maybeNextFrame,
107
120
  maybeUnlockScroll,
108
121
  maybeRestoreInert,
109
- ]), Array.prepend(Task.focus(buttonSelector(model.id)).pipe(Effect.ignore, Effect.as(NoOp())))),
122
+ ]), Array.prepend(Task.focus(buttonSelector(model.id)).pipe(Effect.ignore, Effect.as(CompletedButtonFocus())))),
110
123
  ];
111
124
  }
112
125
  const nextModel = evo(withPointerType, {
@@ -115,7 +128,7 @@ export const update = (model, message) => {
115
128
  });
116
129
  return [
117
130
  nextModel,
118
- pipe(Array.getSomes([maybeNextFrame, maybeLockScroll, maybeInertOthers]), Array.prepend(Task.focus(panelSelector(model.id)).pipe(Effect.ignore, Effect.as(NoOp())))),
131
+ pipe(Array.getSomes([maybeNextFrame, maybeLockScroll, maybeInertOthers]), Array.prepend(Task.focus(panelSelector(model.id)).pipe(Effect.ignore, Effect.as(CompletedPanelFocus())))),
119
132
  ];
120
133
  },
121
134
  AdvancedTransitionFrame: () => M.value(model.transitionState).pipe(withUpdateReturn, M.when('EnterStart', () => [
@@ -137,13 +150,20 @@ export const update = (model, message) => {
137
150
  evo(model, { transitionState: () => 'Idle' }),
138
151
  [],
139
152
  ]), M.orElse(() => [model, []])),
140
- NoOp: () => [model, []],
153
+ CompletedPanelFocus: () => [model, []],
154
+ CompletedButtonFocus: () => [model, []],
155
+ CompletedScrollLock: () => [model, []],
156
+ CompletedScrollUnlock: () => [model, []],
157
+ CompletedInertSetup: () => [model, []],
158
+ CompletedInertTeardown: () => [model, []],
159
+ IgnoredMouseClick: () => [model, []],
160
+ SuppressedSpaceScroll: () => [model, []],
141
161
  }));
142
162
  };
143
163
  /** Renders a headless popover with a trigger button and a floating panel. Uses the disclosure ARIA pattern (aria-expanded + aria-controls) with no role on the panel. */
144
164
  export const view = (config) => {
145
165
  const { div, AriaControls, AriaDisabled, AriaExpanded, Class, DataAttribute, Id, OnBlur, OnClick, OnDestroy, OnInsert, OnKeyDownPreventDefault, OnKeyUpPreventDefault, OnPointerDown, Style, Tabindex, Type, keyed, } = html();
146
- const { model: { id, isOpen, transitionState, maybeLastButtonPointerType }, toMessage, anchor, buttonContent, buttonClassName, panelContent, panelClassName, backdropClassName, isDisabled, className, } = config;
166
+ const { model: { id, isOpen, transitionState, maybeLastButtonPointerType }, toMessage, anchor, buttonContent, buttonClassName, buttonAttributes = [], panelContent, panelClassName, panelAttributes = [], backdropClassName, backdropAttributes = [], isDisabled, className, attributes = [], } = config;
147
167
  const isLeaving = transitionState === 'LeaveStart' || transitionState === 'LeaveAnimating';
148
168
  const isVisible = isOpen || isLeaving;
149
169
  const transitionAttributes = M.value(transitionState).pipe(M.when('EnterStart', () => [
@@ -169,7 +189,7 @@ export const view = (config) => {
169
189
  const handleButtonClick = () => {
170
190
  const isMouse = Option.exists(maybeLastButtonPointerType, type => type === 'mouse');
171
191
  if (isMouse) {
172
- return toMessage(NoOp());
192
+ return toMessage(IgnoredMouseClick());
173
193
  }
174
194
  else if (isOpen) {
175
195
  return toMessage(Closed());
@@ -178,12 +198,11 @@ export const view = (config) => {
178
198
  return toMessage(Opened());
179
199
  }
180
200
  };
181
- const handleSpaceKeyUp = (key) => OptionExt.when(key === ' ', toMessage(NoOp()));
201
+ const handleSpaceKeyUp = (key) => OptionExt.when(key === ' ', toMessage(SuppressedSpaceScroll()));
182
202
  const handlePanelKeyDown = (key) => M.value(key).pipe(M.when('Escape', () => Option.some(toMessage(Closed()))), M.orElse(() => Option.none()));
183
- const buttonAttributes = [
203
+ const resolvedButtonAttributes = [
184
204
  Id(`${id}-button`),
185
205
  Type('button'),
186
- Class(buttonClassName),
187
206
  AriaExpanded(isVisible),
188
207
  AriaControls(`${id}-panel`),
189
208
  ...(isDisabled
@@ -195,6 +214,8 @@ export const view = (config) => {
195
214
  OnClick(handleButtonClick()),
196
215
  ]),
197
216
  ...(isVisible ? [DataAttribute('open', '')] : []),
217
+ ...(buttonClassName ? [Class(buttonClassName)] : []),
218
+ ...buttonAttributes,
198
219
  ];
199
220
  const hooks = anchorHooks({
200
221
  buttonId: `${id}-button`,
@@ -206,10 +227,9 @@ export const view = (config) => {
206
227
  OnInsert(hooks.onInsert),
207
228
  OnDestroy(hooks.onDestroy),
208
229
  ];
209
- const panelAttributes = [
230
+ const resolvedPanelAttributes = [
210
231
  Id(`${id}-panel`),
211
232
  Tabindex(0),
212
- Class(panelClassName),
213
233
  ...anchorAttributes,
214
234
  ...transitionAttributes,
215
235
  ...(isLeaving
@@ -218,21 +238,27 @@ export const view = (config) => {
218
238
  OnKeyDownPreventDefault(handlePanelKeyDown),
219
239
  OnBlur(toMessage(ClosedByTab())),
220
240
  ]),
241
+ ...(panelClassName ? [Class(panelClassName)] : []),
242
+ ...panelAttributes,
221
243
  ];
222
244
  const backdrop = keyed('div')(`${id}-backdrop`, [
223
- Class(backdropClassName),
224
245
  ...(isLeaving ? [] : [OnClick(toMessage(Closed()))]),
246
+ ...(backdropClassName ? [Class(backdropClassName)] : []),
247
+ ...backdropAttributes,
225
248
  ], []);
226
249
  const visibleContent = [
227
250
  backdrop,
228
- keyed('div')(`${id}-panel-container`, panelAttributes, [panelContent]),
251
+ keyed('div')(`${id}-panel-container`, resolvedPanelAttributes, [
252
+ panelContent,
253
+ ]),
229
254
  ];
230
255
  const wrapperAttributes = [
231
256
  ...(className ? [Class(className)] : []),
257
+ ...attributes,
232
258
  ...(isVisible ? [DataAttribute('open', '')] : []),
233
259
  ];
234
260
  return div(wrapperAttributes, [
235
- keyed('button')(`${id}-button`, buttonAttributes, [buttonContent]),
261
+ keyed('button')(`${id}-button`, resolvedButtonAttributes, [buttonContent]),
236
262
  ...(isVisible ? visibleContent : []),
237
263
  ]);
238
264
  };
@@ -1,5 +1,5 @@
1
1
  export { init, update, view, lazy, Model, Message } from './index';
2
2
  export { TransitionState } from '../transition';
3
- export type { Opened, Closed, ClosedByTab, PressedPointerOnButton, NoOp, AdvancedTransitionFrame, EndedTransition, DetectedButtonMovement, InitConfig, ViewConfig, } from './index';
3
+ export type { Opened, Closed, ClosedByTab, PressedPointerOnButton, IgnoredMouseClick, SuppressedSpaceScroll, InitConfig, ViewConfig, } from './index';
4
4
  export type { AnchorConfig } from '../anchor';
5
5
  //# sourceMappingURL=public.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../../src/ui/popover/public.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAElE,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAE/C,YAAY,EACV,MAAM,EACN,MAAM,EACN,WAAW,EACX,sBAAsB,EACtB,IAAI,EACJ,uBAAuB,EACvB,eAAe,EACf,sBAAsB,EACtB,UAAU,EACV,UAAU,GACX,MAAM,SAAS,CAAA;AAEhB,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA"}
1
+ {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../../src/ui/popover/public.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAElE,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAE/C,YAAY,EACV,MAAM,EACN,MAAM,EACN,WAAW,EACX,sBAAsB,EACtB,iBAAiB,EACjB,qBAAqB,EACrB,UAAU,EACV,UAAU,GACX,MAAM,SAAS,CAAA;AAEhB,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA"}