ajo-ui 0.1.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 (103) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +126 -0
  3. package/dist/accordion.js +130 -0
  4. package/dist/avatar.js +54 -0
  5. package/dist/calendar.js +2 -0
  6. package/dist/carousel.js +239 -0
  7. package/dist/chart.js +790 -0
  8. package/dist/checkbox-group.js +70 -0
  9. package/dist/checkbox.js +77 -0
  10. package/dist/chunks/bar-CVafh6C1.js +99 -0
  11. package/dist/chunks/calendar-q8jZ8Cxr.js +1923 -0
  12. package/dist/chunks/collection-DtRB63U4.js +111 -0
  13. package/dist/chunks/data-table-DpkWv4y4.js +1039 -0
  14. package/dist/chunks/menu-B45IyHHC.js +704 -0
  15. package/dist/chunks/native-BJdhd9XJ.js +20 -0
  16. package/dist/chunks/popup-C8Bb3l_g.js +459 -0
  17. package/dist/chunks/popup-surface-BDCgtVU0.js +18 -0
  18. package/dist/chunks/position-D6_i_SRn.js +434 -0
  19. package/dist/chunks/virtual-list-B7hjGkjk.js +413 -0
  20. package/dist/collapsible.js +106 -0
  21. package/dist/command.js +263 -0
  22. package/dist/context-menu.js +112 -0
  23. package/dist/data-table.js +5 -0
  24. package/dist/dialog.js +207 -0
  25. package/dist/direction.js +22 -0
  26. package/dist/drawer.js +139 -0
  27. package/dist/field.js +26 -0
  28. package/dist/index.js +38 -0
  29. package/dist/input-date.js +994 -0
  30. package/dist/input-group.js +52 -0
  31. package/dist/input-otp.js +179 -0
  32. package/dist/menu.js +2 -0
  33. package/dist/menubar.js +236 -0
  34. package/dist/message-scroller.js +446 -0
  35. package/dist/navigation-menu.js +330 -0
  36. package/dist/popover.js +307 -0
  37. package/dist/progress.js +39 -0
  38. package/dist/radio-group.js +107 -0
  39. package/dist/resizable.js +172 -0
  40. package/dist/select.js +961 -0
  41. package/dist/sidebar.js +343 -0
  42. package/dist/slider.js +259 -0
  43. package/dist/switch.js +53 -0
  44. package/dist/tabs.js +182 -0
  45. package/dist/toast.js +492 -0
  46. package/dist/toggle-group.js +111 -0
  47. package/dist/toggle.js +52 -0
  48. package/dist/toolbar.js +127 -0
  49. package/dist/tooltip.js +196 -0
  50. package/dist/utils.js +104 -0
  51. package/dist/virtual-list.js +2 -0
  52. package/package.json +250 -0
  53. package/src/accordion.tsx +261 -0
  54. package/src/availability.ts +261 -0
  55. package/src/avatar.tsx +99 -0
  56. package/src/bar.ts +156 -0
  57. package/src/calendar.tsx +1441 -0
  58. package/src/carousel.tsx +424 -0
  59. package/src/chart.tsx +1194 -0
  60. package/src/checkbox-group.tsx +132 -0
  61. package/src/checkbox.tsx +130 -0
  62. package/src/collapsible.tsx +188 -0
  63. package/src/collection.ts +154 -0
  64. package/src/command.tsx +511 -0
  65. package/src/context-menu.tsx +233 -0
  66. package/src/data-table-contract.ts +143 -0
  67. package/src/data-table-model.ts +760 -0
  68. package/src/data-table.tsx +475 -0
  69. package/src/dialog.tsx +393 -0
  70. package/src/direction.tsx +45 -0
  71. package/src/drawer.tsx +251 -0
  72. package/src/field.tsx +61 -0
  73. package/src/index.ts +37 -0
  74. package/src/input-date.tsx +1539 -0
  75. package/src/input-group.tsx +142 -0
  76. package/src/input-otp.tsx +324 -0
  77. package/src/menu-cluster.ts +124 -0
  78. package/src/menu.tsx +1095 -0
  79. package/src/menubar.tsx +459 -0
  80. package/src/message-scroller.tsx +732 -0
  81. package/src/native.ts +26 -0
  82. package/src/navigation-menu.tsx +578 -0
  83. package/src/popover.tsx +519 -0
  84. package/src/popup-surface.tsx +31 -0
  85. package/src/popup.ts +569 -0
  86. package/src/position.ts +523 -0
  87. package/src/progress.tsx +70 -0
  88. package/src/radio-group.tsx +186 -0
  89. package/src/resizable.tsx +310 -0
  90. package/src/segments.ts +922 -0
  91. package/src/select.tsx +1501 -0
  92. package/src/sidebar.tsx +683 -0
  93. package/src/slider.tsx +424 -0
  94. package/src/switch.tsx +104 -0
  95. package/src/tabs.tsx +314 -0
  96. package/src/toast.tsx +923 -0
  97. package/src/toggle-group.tsx +249 -0
  98. package/src/toggle.tsx +91 -0
  99. package/src/toolbar.tsx +212 -0
  100. package/src/tooltip.tsx +359 -0
  101. package/src/utils.ts +204 -0
  102. package/src/virtual-list.tsx +205 -0
  103. package/src/virtual.ts +385 -0
package/src/native.ts ADDED
@@ -0,0 +1,26 @@
1
+ /** HTMLElement shape augmented by the native Popover API. */
2
+ export type PopoverElement = HTMLElement & {
3
+ hidePopover?: () => void
4
+ showPopover?: (options?: { source?: HTMLElement }) => void
5
+ }
6
+
7
+ /** Checks whether a native popover is currently open. */
8
+ export const popoverOpen = (element: HTMLElement) =>
9
+ typeof element.matches === 'function' && element.matches(':popover-open')
10
+
11
+ /** Opens a native popover and reports whether it is open. */
12
+ export const openPopover = (element: PopoverElement, source?: HTMLElement | null) => {
13
+ if (popoverOpen(element)) return true
14
+ if (typeof element.showPopover !== 'function') return false
15
+ if (source) element.showPopover({ source })
16
+ else element.showPopover()
17
+ return popoverOpen(element)
18
+ }
19
+
20
+ /** Closes a native popover and reports whether it is closed. */
21
+ export const closePopover = (element: PopoverElement) => {
22
+ if (!popoverOpen(element)) return true
23
+ if (typeof element.hidePopover !== 'function') return false
24
+ element.hidePopover()
25
+ return !popoverOpen(element)
26
+ }
@@ -0,0 +1,578 @@
1
+ import type { IntrinsicElements, Stateful, Stateless, WithChildren } from 'ajo'
2
+ import { callHandler, id, listen, statefulRootAttrs as rootAttrs } from 'ajo-cloves'
3
+ import { context } from 'ajo/context'
4
+ import { bar } from './bar'
5
+ import { contentAttrs, popup, type PopupView } from './popup'
6
+ import type { ReservedPositionArg } from './position'
7
+ import type { FixedArgs, OmitArg, PopupPosition } from './utils'
8
+ import { text, triggerAttrs } from './utils'
9
+ export type { PopupPlacement, PopupPosition } from './utils'
10
+
11
+ /** Stable identifier for an open navigation-menu item. */
12
+ export type NavigationMenuValue = string
13
+
14
+ /** Props for the navigation-menu root and its controlled state. */
15
+ export type NavigationMenuArgs = WithChildren<OmitArg<IntrinsicElements['nav'], 'onchange' | ReservedPositionArg> & PopupPosition & {
16
+ /** Controlled open item value. Empty string closes every content panel. */
17
+ value?: NavigationMenuValue
18
+ /** Initial open item value for uncontrolled usage. */
19
+ defaultValue?: NavigationMenuValue
20
+ /** Hover-intent delay before a panel opens, in milliseconds. */
21
+ openDelay?: number
22
+ /** Hover-intent delay before a panel closes after the pointer leaves, in milliseconds. */
23
+ closeDelay?: number
24
+ /** Called whenever the open item value changes. */
25
+ onValueChange?: (value: NavigationMenuValue, event?: Event) => void
26
+ /** Additional CSS classes. */
27
+ class?: string
28
+ }> & FixedArgs<'onchange' | ReservedPositionArg>
29
+
30
+ /** Props for the list containing navigation-menu items. */
31
+ export type NavigationMenuListArgs = WithChildren<IntrinsicElements['ul'] & {
32
+ /** Additional CSS classes. */
33
+ class?: string
34
+ }>
35
+
36
+ /** Props for a navigation-menu item and its stable value. */
37
+ export type NavigationMenuItemArgs = WithChildren<OmitArg<IntrinsicElements['li'], 'gap' | 'placement' | ReservedPositionArg> & {
38
+ /** Stable value used by controlled NavigationMenu state. */
39
+ value?: NavigationMenuValue
40
+ /** Disable this item and its trigger. */
41
+ disabled?: boolean
42
+ /** Additional CSS classes. */
43
+ class?: string
44
+ }> & FixedArgs<'gap' | 'placement' | ReservedPositionArg>
45
+
46
+ /** Props for a button that opens a navigation-menu panel. */
47
+ export type NavigationMenuTriggerArgs = WithChildren<IntrinsicElements['button'] & {
48
+ /** Plain-text label used for keyboard typeahead. */
49
+ textValue?: string
50
+ /** Additional CSS classes. */
51
+ class?: string
52
+ }>
53
+
54
+ /** Props for a floating navigation-menu panel. */
55
+ export type NavigationMenuContentArgs = WithChildren<OmitArg<IntrinsicElements['div'], 'aria-labelledby' | 'hidden' | 'id' | 'popover' | 'tabindex' | 'tabIndex' | ReservedPositionArg> & {
56
+ /** Additional CSS classes. */
57
+ class?: string
58
+ /** Inline CSS declarations composed with live positioning styles. */
59
+ style?: string
60
+ }> & FixedArgs<'aria-labelledby' | 'gap' | 'hidden' | 'id' | 'placement' | 'popover' | 'tabindex' | 'tabIndex' | ReservedPositionArg>
61
+
62
+ /** Props for an anchor or button rendered inside navigation-menu content. */
63
+ export type NavigationMenuLinkArgs = WithChildren<(IntrinsicElements['a'] & IntrinsicElements['button']) & {
64
+ /** Render as a native anchor or button. */
65
+ as?: 'a' | 'button'
66
+ /** Mark the link as active. */
67
+ active?: boolean
68
+ /** Additional CSS classes. */
69
+ class?: string
70
+ }>
71
+
72
+ type RootContextValue = {
73
+ close: (event?: Event) => void
74
+ closeDelay: number
75
+ follow: (value: string, event?: Event) => void
76
+ gap: PopupPosition['gap']
77
+ open: (value: string, event?: Event) => void
78
+ openDelay: number
79
+ placement: PopupPosition['placement']
80
+ /** One-shot: true when a keyboard open requested focus into this value's panel. */
81
+ takeFocus: (value: string) => boolean
82
+ value: string
83
+ }
84
+
85
+ type ItemContextValue = {
86
+ adoptTriggerId: PopupView['adoptTriggerId']
87
+ clickTrigger: (event: Event) => void
88
+ close: (event?: Event) => void
89
+ contentId: string
90
+ contentStyle: PopupView['contentStyle']
91
+ disabled: boolean
92
+ open: boolean
93
+ registerContentHover: (hovering: boolean, event: Event) => void
94
+ registerTriggerFocus: (event: FocusEvent) => void
95
+ registerTriggerHover: (hovering: boolean, event: Event) => void
96
+ setContent: (element: HTMLDivElement | null) => void
97
+ setTrigger: (element: HTMLButtonElement | null) => void
98
+ triggerId: string
99
+ value: string
100
+ }
101
+
102
+ const RootContext = context<RootContextValue | null>(null)
103
+ const ItemContext = context<ItemContextValue | null>(null)
104
+
105
+ const triggers = (root: HTMLElement) =>
106
+ Array.from(root.querySelectorAll<HTMLButtonElement>('[data-navigation-menu-trigger="true"]'))
107
+ .filter(trigger =>
108
+ !trigger.disabled
109
+ && trigger.offsetParent !== null
110
+ && trigger.closest('[data-slot="navigation-menu"]') === root)
111
+
112
+ const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
113
+
114
+ const NavigationMenuRoot: Stateful<NavigationMenuArgs, 'nav'> = function* ({ defaultValue, value }) {
115
+ let closeDelay = 300
116
+ const closeFocus = { current: null as HTMLElement | null }
117
+ let onValueChange: NavigationMenuArgs['onValueChange']
118
+ let openDelay = 200
119
+ let pendingFocus = ''
120
+
121
+ const state = bar(this, {
122
+ triggers: () => triggers(this),
123
+ initialValue: String(value ?? defaultValue ?? ''),
124
+ onValueChange: (next, event) => onValueChange?.(next, event),
125
+ })
126
+
127
+ const openTrigger = () =>
128
+ triggers(this).find(trigger => trigger.dataset.value === state.value)
129
+
130
+ const focusPanel = (trigger: HTMLElement) => {
131
+ const contentId = trigger.getAttribute('aria-controls')
132
+ const panel = contentId ? document.getElementById(contentId) : null
133
+ panel?.querySelector<HTMLElement>(FOCUSABLE)?.focus()
134
+ }
135
+
136
+ const close = (event?: Event, focus?: HTMLElement | null) => {
137
+ pendingFocus = ''
138
+ closeFocus.current = focus ?? null
139
+ state.close(event)
140
+ }
141
+
142
+ listen(this, 'keydown', (event: KeyboardEvent) => {
143
+ if (event.defaultPrevented) return
144
+ const target = event.target as HTMLElement | null
145
+ if (target?.closest('[data-slot="navigation-menu"]') !== this) return
146
+ const trigger = target?.closest<HTMLButtonElement>('[data-navigation-menu-trigger="true"]')
147
+
148
+ if (!trigger) {
149
+ // Escape inside an open panel returns focus to its trigger and
150
+ // closes; while closed the key passes through untouched (a hosting
151
+ // dialog keeps its Escape).
152
+ if (event.key === 'Escape' && state.value) {
153
+ event.preventDefault()
154
+ close(event, openTrigger())
155
+ }
156
+ return
157
+ }
158
+
159
+ if (state.handle(event)) return
160
+
161
+ if (event.key === 'ArrowDown' || event.key === 'Enter' || event.key === ' ') {
162
+ event.preventDefault()
163
+ const next = trigger.dataset.value ?? ''
164
+ if (state.value === next) {
165
+ // Already open: ArrowDown enters the panel, Enter/Space toggle closed.
166
+ if (event.key === 'ArrowDown') focusPanel(trigger)
167
+ else close(event)
168
+ } else {
169
+ pendingFocus = next
170
+ state.setValue(next, event)
171
+ }
172
+ }
173
+ })
174
+
175
+ // Focus-out close: panel links stay tabbable, so Tab walks through them;
176
+ // tabbing past the last one (or focus leaving the nav and its panels
177
+ // entirely) closes the open panel. Focus landing on another trigger runs
178
+ // the follow policy instead of closing.
179
+ listen(this, 'focusout', (event: FocusEvent) => {
180
+ if (!state.value) return
181
+ const next = event.relatedTarget as Node | null
182
+ if (next && this.contains(next)) return
183
+ // Window blur (alt-tab) fires focusout with a null relatedTarget while
184
+ // the focused element stays inside the nav: not a focus departure.
185
+ if (!next && this.contains(document.activeElement)) return
186
+ close(event)
187
+ })
188
+
189
+ for (const args of this) {
190
+ closeDelay = Math.max(0, Number(args.closeDelay ?? 300))
191
+ onValueChange = args.onValueChange
192
+ openDelay = Math.max(0, Number(args.openDelay ?? 200))
193
+ state.sync(args.value != null ? String(args.value ?? '') : undefined)
194
+ if (pendingFocus && pendingFocus !== state.value) pendingFocus = ''
195
+ if (closeFocus.current) {
196
+ const target = closeFocus.current
197
+ closeFocus.current = null
198
+ if (!state.value) queueMicrotask(() => {
199
+ if (!state.value) target.focus()
200
+ })
201
+ }
202
+
203
+ RootContext({
204
+ close,
205
+ closeDelay,
206
+ // Pointer-driven opens clear any stale keyboard focus request.
207
+ follow: (next, event) => {
208
+ pendingFocus = ''
209
+ state.follow(next, event)
210
+ },
211
+ gap: args.gap,
212
+ open: (next, event) => {
213
+ // An engine echo for the same keyboard-requested value must not
214
+ // invalidate its post-geometry focus token. Pointer opens have no
215
+ // matching token and clear any stale request.
216
+ if (pendingFocus !== next) pendingFocus = ''
217
+ state.setValue(next, event)
218
+ },
219
+ openDelay,
220
+ placement: args.placement,
221
+ takeFocus: value => {
222
+ if (!value || pendingFocus !== value) return false
223
+ pendingFocus = ''
224
+ return true
225
+ },
226
+ value: state.value,
227
+ })
228
+
229
+ yield <>{args.children}</>
230
+ }
231
+ }
232
+
233
+ NavigationMenuRoot.is = 'nav'
234
+
235
+ /** Unstyled root landmark and state provider for a navigation menu. */
236
+ const NavigationMenu: Stateless<NavigationMenuArgs> = ({
237
+ children,
238
+ class: classes,
239
+ closeDelay,
240
+ defaultValue,
241
+ gap,
242
+ onValueChange,
243
+ openDelay,
244
+ placement,
245
+ value,
246
+ ...attrs
247
+ }) => (
248
+ <NavigationMenuRoot
249
+ {...rootAttrs(attrs)}
250
+ closeDelay={closeDelay}
251
+ defaultValue={defaultValue}
252
+ gap={gap}
253
+ onValueChange={onValueChange}
254
+ openDelay={openDelay}
255
+ placement={placement}
256
+ value={value}
257
+ attr:class={classes}
258
+ attr:data-slot="navigation-menu"
259
+ >
260
+ {children}
261
+ </NavigationMenuRoot>
262
+ )
263
+
264
+ /** Unstyled horizontal list of navigation menu items. */
265
+ const NavigationMenuList: Stateless<NavigationMenuListArgs> = ({ children, class: classes, ...attrs }) => (
266
+ <ul {...attrs} class={classes} data-slot="navigation-menu-list">
267
+ {children}
268
+ </ul>
269
+ )
270
+
271
+ const NavigationMenuItemRoot: Stateful<NavigationMenuItemArgs, 'li'> = function* ({ value }) {
272
+ const fallback = value ?? id('navigation-menu-item')
273
+ let disabled = false
274
+ let itemValue = String(fallback)
275
+ let root: RootContextValue | null = null
276
+ let item: PopupView<HTMLButtonElement, HTMLDivElement>
277
+
278
+ item = popup<HTMLButtonElement, HTMLDivElement>(this, {
279
+ prefix: 'navigation-menu',
280
+ profile: 'navigation',
281
+ initialOpen: false,
282
+ disabled: () => disabled,
283
+ hover: {
284
+ openDelay: () => root?.openDelay ?? 200,
285
+ closeDelay: () => root?.closeDelay ?? 300,
286
+ },
287
+ onOpenChange: (next, event) => {
288
+ if (next) root?.open(itemValue, event)
289
+ else if (root?.value === itemValue) root.close(event)
290
+ },
291
+ reference: view => view.trigger,
292
+ source: view => view.trigger,
293
+ referenceHidden: 'close',
294
+ dismiss: {
295
+ escape: false,
296
+ outside: true,
297
+ inside: view => [view.trigger, view.content],
298
+ onDismiss: event => closeItem(event),
299
+ },
300
+ onPosition: () => {
301
+ // Keyboard focus is committed only after the current trigger/content
302
+ // tuple has real geometry and the panel is visible.
303
+ if (root?.takeFocus(itemValue)) item.content?.querySelector<HTMLElement>(FOCUSABLE)?.focus()
304
+ },
305
+ onSync: opened => {
306
+ if (!opened) cause = ''
307
+ },
308
+ })
309
+
310
+ // Open cause decides what a trigger press does to an open panel: only a
311
+ // hover-opened panel holds through the press that follows (the
312
+ // hover→click race, the recorded deviation); a press- or keyboard-opened
313
+ // panel closes.
314
+ let cause: 'hover' | 'press' | '' = ''
315
+
316
+ const registerTriggerHover = (hovering: boolean, event: Event) => {
317
+ if (hovering) {
318
+ if (!item.open) cause = 'hover'
319
+ item.hold('trigger', event)
320
+ // Open-follows-hover: an already-open bar moves between panels
321
+ // without re-running the open delay.
322
+ if (!disabled) root?.follow(itemValue, event)
323
+ } else {
324
+ if (!item.open && cause === 'hover') cause = ''
325
+ item.release('trigger', event)
326
+ }
327
+ }
328
+
329
+ const registerContentHover = (hovering: boolean, event: Event) =>
330
+ hovering ? item.hold('content', event) : item.release('content', event)
331
+
332
+ const registerTriggerFocus = (event: FocusEvent) => {
333
+ if (!disabled) root?.follow(itemValue, event)
334
+ }
335
+
336
+ const openItem = (event?: Event) => {
337
+ if (disabled) return
338
+ cause = 'press'
339
+ item.cancelHover()
340
+ if (root) root.open(itemValue, event)
341
+ else item.setOpen(true, event)
342
+ }
343
+
344
+ const closeItem = (event?: Event) =>
345
+ root ? root.close(event) : item.close(event)
346
+
347
+ const clickTrigger = (event: Event) => {
348
+ if (disabled) return
349
+ if (!item.open) {
350
+ openItem(event)
351
+ } else if (cause === 'hover') {
352
+ cause = 'press'
353
+ item.cancelHover()
354
+ } else {
355
+ closeItem(event)
356
+ }
357
+ }
358
+
359
+ for (const args of this) {
360
+ root = RootContext()
361
+ itemValue = String(args.value ?? fallback)
362
+ disabled = Boolean(args.disabled)
363
+ // Without a NavigationMenu ancestor the item degrades to an
364
+ // uncontrolled hover panel instead of a permanently-closed one.
365
+ const opened = item.sync(root ? root.value === itemValue : null, {
366
+ placement: root?.placement,
367
+ gap: root?.gap,
368
+ })
369
+ if (!opened) cause = ''
370
+
371
+ ItemContext({
372
+ adoptTriggerId: item.adoptTriggerId,
373
+ clickTrigger,
374
+ close: closeItem,
375
+ contentId: item.contentId,
376
+ contentStyle: item.contentStyle,
377
+ disabled,
378
+ open: opened,
379
+ registerContentHover,
380
+ registerTriggerFocus,
381
+ registerTriggerHover,
382
+ setContent: item.setContent,
383
+ setTrigger: item.setTrigger,
384
+ triggerId: item.triggerId,
385
+ value: itemValue,
386
+ })
387
+
388
+ yield <>{args.children}</>
389
+ }
390
+ }
391
+
392
+ NavigationMenuItemRoot.is = 'li'
393
+
394
+ /** Unstyled top-level item inside a NavigationMenuList. */
395
+ const NavigationMenuItem: Stateless<NavigationMenuItemArgs> = ({
396
+ children,
397
+ class: classes,
398
+ disabled,
399
+ value,
400
+ ...attrs
401
+ }) => (
402
+ <NavigationMenuItemRoot
403
+ {...rootAttrs(attrs)}
404
+ disabled={disabled}
405
+ value={value}
406
+ attr:class={classes}
407
+ attr:data-disabled={disabled ? 'true' : undefined}
408
+ attr:data-slot="navigation-menu-item"
409
+ >
410
+ {children}
411
+ </NavigationMenuItemRoot>
412
+ )
413
+
414
+ /** Unstyled button that opens an item content panel. */
415
+ const NavigationMenuTrigger: Stateless<NavigationMenuTriggerArgs> = ({
416
+ children,
417
+ class: classes,
418
+ disabled,
419
+ id: idArg,
420
+ ref,
421
+ textValue,
422
+ type = 'button',
423
+ 'set:onclick': onClick,
424
+ 'set:onfocus': onFocus,
425
+ 'set:onmouseenter': onMouseEnter,
426
+ 'set:onmouseleave': onMouseLeave,
427
+ ...attrs
428
+ }) => {
429
+ const item = ItemContext()
430
+ const disabledFlag = Boolean(disabled ?? item?.disabled)
431
+ const label = textValue ?? text(children)
432
+ const adoptedId = item?.adoptTriggerId(idArg)
433
+
434
+ return (
435
+ <button
436
+ {...attrs}
437
+ {...triggerAttrs({
438
+ controls: item?.contentId,
439
+ expanded: Boolean(item?.open),
440
+ id: adoptedId ?? idArg,
441
+ open: Boolean(item?.open),
442
+ ref,
443
+ setTrigger: item?.setTrigger,
444
+ triggerId: item?.triggerId,
445
+ })}
446
+ class={classes}
447
+ data-label={label}
448
+ data-navigation-menu-trigger="true"
449
+ data-slot="navigation-menu-trigger"
450
+ data-value={item?.value}
451
+ disabled={disabledFlag}
452
+ type={type}
453
+ set:onclick={(event: Event) => {
454
+ callHandler(onClick, event)
455
+ if (event.defaultPrevented || disabledFlag) return
456
+ // Clicking a closed trigger opens immediately; a hover-opened
457
+ // panel holds through its first click (the hover→click race);
458
+ // any other open panel closes.
459
+ item?.clickTrigger(event)
460
+ }}
461
+ set:onfocus={(event: FocusEvent) => {
462
+ callHandler(onFocus, event)
463
+ if (event.defaultPrevented || disabledFlag) return
464
+ item?.registerTriggerFocus(event)
465
+ }}
466
+ set:onmouseenter={(event: MouseEvent) => {
467
+ callHandler(onMouseEnter, event)
468
+ if (event.defaultPrevented || disabledFlag) return
469
+ item?.registerTriggerHover(true, event)
470
+ }}
471
+ set:onmouseleave={(event: MouseEvent) => {
472
+ callHandler(onMouseLeave, event)
473
+ item?.registerTriggerHover(false, event)
474
+ }}
475
+ >
476
+ {children}
477
+ </button>
478
+ )
479
+ }
480
+
481
+ /** Unstyled popover panel for a NavigationMenuItem. */
482
+ const NavigationMenuContent: Stateless<NavigationMenuContentArgs> = ({
483
+ children,
484
+ class: classes,
485
+ ref,
486
+ style,
487
+ 'set:onmouseenter': onMouseEnter,
488
+ 'set:onmouseleave': onMouseLeave,
489
+ ...attrs
490
+ }) => {
491
+ const item = ItemContext()
492
+
493
+ return (
494
+ <div
495
+ {...attrs}
496
+ {...contentAttrs({
497
+ id: item?.contentId,
498
+ open: Boolean(item?.open),
499
+ ref,
500
+ setContent: item?.setContent,
501
+ style: item?.contentStyle(style),
502
+ tabindex: '-1',
503
+ })}
504
+ aria-labelledby={item?.triggerId}
505
+ class={classes}
506
+ data-slot="navigation-menu-content"
507
+ set:onmouseenter={(event: MouseEvent) => {
508
+ callHandler(onMouseEnter, event)
509
+ item?.registerContentHover(true, event)
510
+ }}
511
+ set:onmouseleave={(event: MouseEvent) => {
512
+ callHandler(onMouseLeave, event)
513
+ item?.registerContentHover(false, event)
514
+ }}
515
+ >
516
+ {children}
517
+ </div>
518
+ )
519
+ }
520
+
521
+ /** Unstyled link for use inside or directly within a navigation menu item. */
522
+ const NavigationMenuLink: Stateless<NavigationMenuLinkArgs> = ({
523
+ active,
524
+ as = 'a',
525
+ children,
526
+ class: classes,
527
+ type = 'button',
528
+ 'set:onclick': onClick,
529
+ ...attrs
530
+ }) => {
531
+ const item = ItemContext()
532
+ const state = active ? 'true' : undefined
533
+ const current = active ? 'page' : undefined
534
+ const click = (event: Event) => {
535
+ callHandler(onClick, event)
536
+ if (!event.defaultPrevented) item?.close(event)
537
+ }
538
+
539
+ if (as === 'button') {
540
+ return (
541
+ <button
542
+ {...attrs}
543
+ class={classes}
544
+ data-active={state}
545
+ data-slot="navigation-menu-link"
546
+ type={type}
547
+ set:onclick={click}
548
+ >
549
+ {children}
550
+ </button>
551
+ )
552
+ }
553
+
554
+ return (
555
+ <a
556
+ {...attrs}
557
+ aria-current={current}
558
+ class={classes}
559
+ data-active={state}
560
+ data-slot="navigation-menu-link"
561
+ set:onclick={click}
562
+ >
563
+ {children}
564
+ </a>
565
+ )
566
+ }
567
+
568
+ // NavigationMenu uses per-item anchored panels. Its themed surface lives on
569
+ // NavigationMenuContent; there is no shared Viewport or Indicator contract.
570
+
571
+ export {
572
+ NavigationMenu,
573
+ NavigationMenuContent,
574
+ NavigationMenuItem,
575
+ NavigationMenuLink,
576
+ NavigationMenuList,
577
+ NavigationMenuTrigger,
578
+ }