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
@@ -0,0 +1,249 @@
1
+ import type { IntrinsicElements, Stateful, Stateless, WithChildren } from 'ajo'
2
+ import { listen, roving, selection, statefulRootAttrs as rootAttrs } from 'ajo-cloves'
3
+ import { context } from 'ajo/context'
4
+ import type { FixedArgs, OmitArg } from './utils'
5
+ import { flag, strings } from './utils'
6
+ import { Toggle, type ToggleArgs } from './toggle'
7
+
8
+ /** Selection cardinality of a toggle group. */
9
+ export type ToggleGroupType = 'multiple' | 'single'
10
+ /** Layout and keyboard-navigation axis of a toggle group. */
11
+ export type ToggleGroupOrientation = 'horizontal' | 'vertical'
12
+
13
+ type ToggleGroupSharedArgs = WithChildren<OmitArg<IntrinsicElements['div'], 'defaultValue' | 'type' | 'value'> & {
14
+ /** Disable every item in the group. */
15
+ disabled?: boolean
16
+ /** Allow arrow-key focus to wrap at the ends. */
17
+ loop?: boolean
18
+ /** Layout orientation. */
19
+ orientation?: ToggleGroupOrientation
20
+ /** Shared item size marker. */
21
+ size?: string
22
+ /** Spacing marker shared with items. */
23
+ spacing?: number
24
+ /** Shared item variant marker. */
25
+ variant?: string
26
+ }>
27
+
28
+ /** Props for a toggle group that selects at most one value. */
29
+ export type ToggleGroupSingleArgs = ToggleGroupSharedArgs & {
30
+ type?: 'single'
31
+ /** Controlled selected value. */
32
+ value?: string
33
+ /** Initial selected value for uncontrolled usage. */
34
+ defaultValue?: string
35
+ /** Called whenever the selected value changes. */
36
+ onValueChange?: (value: string, event: Event) => void
37
+ }
38
+
39
+ /** Props for a toggle group that selects multiple values. */
40
+ export type ToggleGroupMultipleArgs = ToggleGroupSharedArgs & {
41
+ type: 'multiple'
42
+ /** Controlled selected values. */
43
+ value?: string[]
44
+ /** Initial selected values for uncontrolled usage. */
45
+ defaultValue?: string[]
46
+ /** Called whenever the selected values change. */
47
+ onValueChange?: (value: string[], event: Event) => void
48
+ }
49
+
50
+ /** Props accepted by either single- or multiple-selection toggle groups. */
51
+ export type ToggleGroupArgs = ToggleGroupSingleArgs | ToggleGroupMultipleArgs
52
+
53
+ /** Props for a toggle button whose pressed state belongs to its group. */
54
+ export type ToggleGroupItemArgs = OmitArg<ToggleArgs, 'defaultPressed' | 'onPressedChange' | 'pressed'> & {
55
+ /** Item value used by the parent toggle group. */
56
+ value: string
57
+ /** Item size marker. */
58
+ size?: string
59
+ /** Item variant marker. */
60
+ variant?: string
61
+ } & FixedArgs<'defaultPressed' | 'onPressedChange' | 'pressed'>
62
+
63
+ /** State and styling markers exposed to descendant toggle-group items. */
64
+ export type ToggleGroupContextValue = {
65
+ disabled?: boolean
66
+ orientation: ToggleGroupOrientation
67
+ pressed: (value: string) => boolean
68
+ size?: string
69
+ spacing: number
70
+ toggle: (value: string, pressed: boolean, event: Event) => void
71
+ type: ToggleGroupType
72
+ variant?: string
73
+ }
74
+
75
+ type ToggleGroupRootArgs = WithChildren<{
76
+ defaultValue?: string | string[]
77
+ disabled?: boolean
78
+ loop?: boolean
79
+ onValueChange?: ((value: string, event: Event) => void) | ((value: string[], event: Event) => void)
80
+ orientation: ToggleGroupOrientation
81
+ size?: string
82
+ spacing: number
83
+ type: ToggleGroupType
84
+ value?: string | string[]
85
+ variant?: string
86
+ }>
87
+
88
+ /** Composition context exposing ToggleGroup state and styling markers to descendant items. */
89
+ export const ToggleGroupContext = context<ToggleGroupContextValue | null>(null)
90
+
91
+ const selected = (type: ToggleGroupType, value: unknown) =>
92
+ type === 'multiple'
93
+ ? strings(value)
94
+ : value == null || value === '' ? [] : [String(value)]
95
+
96
+ const isButton = (value: EventTarget | null): value is HTMLButtonElement =>
97
+ value instanceof HTMLButtonElement && value.dataset.slot === 'toggle-group-item'
98
+
99
+ const focusableItems = (root: HTMLElement) =>
100
+ Array.from(root.querySelectorAll<HTMLButtonElement>('button[data-slot="toggle-group-item"]'))
101
+ .filter(button => !button.disabled)
102
+
103
+ const ToggleGroupRoot: Stateful<ToggleGroupRootArgs> = function* ({ defaultValue, type: initialType }) {
104
+ let disabled = false
105
+ let loop = true
106
+ let onValueChange: ToggleGroupRootArgs['onValueChange']
107
+ let orientation: ToggleGroupOrientation = 'horizontal'
108
+ let type: ToggleGroupType = initialType
109
+ const sel = selection(this, {
110
+ multiple: () => type === 'multiple',
111
+ fallback: selected(initialType, defaultValue),
112
+ onChange: (next, event) => {
113
+ if (type === 'multiple') {
114
+ (onValueChange as ((value: string[], event: Event) => void) | undefined)?.(next, event as Event)
115
+ } else {
116
+ (onValueChange as ((value: string, event: Event) => void) | undefined)?.(next[0] ?? '', event as Event)
117
+ }
118
+ },
119
+ })
120
+
121
+ const pressed = (value: string) =>
122
+ sel.has(value)
123
+
124
+ const change = (value: string, _nextPressed: boolean, event: Event) => {
125
+ if (disabled) return
126
+ sel.toggle(value, event)
127
+ }
128
+
129
+ const nav = roving(this, {
130
+ items: () => focusableItems(this),
131
+ orientation: () => orientation,
132
+ loop: () => loop,
133
+ onMove: target => target.focus(),
134
+ })
135
+
136
+ listen(this, 'keydown', (event: KeyboardEvent) => {
137
+ if (!isButton(event.target)) return
138
+ // Inside a Toolbar the group's items join the toolbar's roving instead
139
+ // (Base UI model): one tab stop, arrows traverse into and out of the
140
+ // group without double-roving. The toolbar must be within the group's
141
+ // own layer: a group inside popover/dialog content that is a DOM
142
+ // descendant of a toolbar keeps its own roving (the toolbar's control
143
+ // row excludes it).
144
+ if (this.closest('[role="toolbar"], [popover], dialog')?.matches('[role="toolbar"]')) return
145
+ nav.handle(event)
146
+ })
147
+
148
+ for (const args of this) {
149
+ type = args.type
150
+ orientation = args.orientation
151
+ disabled = Boolean(args.disabled)
152
+ loop = args.loop !== false
153
+ onValueChange = args.onValueChange
154
+ sel.sync(args.value != null ? selected(type, args.value) : undefined)
155
+
156
+ ToggleGroupContext({
157
+ disabled,
158
+ orientation,
159
+ pressed,
160
+ size: args.size,
161
+ spacing: args.spacing,
162
+ toggle: change,
163
+ type,
164
+ variant: args.variant,
165
+ })
166
+
167
+ yield <>{args.children}</>
168
+ }
169
+ }
170
+
171
+
172
+ /** Unstyled toggle group with selection state and roving keyboard focus. */
173
+ const ToggleGroup: Stateless<ToggleGroupArgs> = ({
174
+ children,
175
+ defaultValue,
176
+ disabled,
177
+ loop = true,
178
+ onValueChange,
179
+ orientation = 'horizontal',
180
+ role = 'group',
181
+ size,
182
+ spacing = 2,
183
+ type = 'single',
184
+ value,
185
+ variant,
186
+ ...attrs
187
+ }) => {
188
+ const disabledFlag = Boolean(disabled)
189
+
190
+ return (
191
+ <ToggleGroupRoot
192
+ {...rootAttrs(attrs)}
193
+ defaultValue={defaultValue}
194
+ disabled={disabledFlag}
195
+ loop={loop}
196
+ onValueChange={onValueChange}
197
+ orientation={orientation}
198
+ size={size}
199
+ spacing={spacing}
200
+ type={type}
201
+ value={value}
202
+ variant={variant}
203
+ attr:aria-disabled={flag(disabledFlag)}
204
+ attr:aria-orientation={orientation}
205
+ attr:data-disabled={flag(disabledFlag)}
206
+ attr:data-orientation={orientation}
207
+ attr:data-size={size}
208
+ attr:data-slot="toggle-group"
209
+ attr:data-spacing={spacing}
210
+ attr:data-variant={variant}
211
+ attr:role={role}
212
+ >
213
+ {children}
214
+ </ToggleGroupRoot>
215
+ )
216
+ }
217
+
218
+ /** Unstyled toggle group item wired to the nearest group. */
219
+ const ToggleGroupItem: Stateless<ToggleGroupItemArgs> = ({
220
+ disabled,
221
+ size,
222
+ value,
223
+ variant,
224
+ ...attrs
225
+ }) => {
226
+ const group = ToggleGroupContext()
227
+ const itemValue = String(value)
228
+ const itemSize = size ?? group?.size
229
+ const itemVariant = variant ?? group?.variant
230
+ const disabledFlag = Boolean(disabled ?? group?.disabled)
231
+ const pressed = group?.pressed(itemValue) ?? false
232
+
233
+ return (
234
+ <Toggle
235
+ {...attrs}
236
+ data-orientation={group?.orientation ?? 'horizontal'}
237
+ data-size={itemSize}
238
+ data-slot="toggle-group-item"
239
+ data-spacing={group?.spacing ?? 2}
240
+ data-variant={itemVariant}
241
+ disabled={disabledFlag}
242
+ pressed={pressed}
243
+ value={itemValue}
244
+ onPressedChange={(next, event) => group?.toggle(itemValue, next, event)}
245
+ />
246
+ )
247
+ }
248
+
249
+ export { ToggleGroup, ToggleGroupItem }
package/src/toggle.tsx ADDED
@@ -0,0 +1,91 @@
1
+ import type { IntrinsicElements, Stateful, Stateless, WithChildren } from 'ajo'
2
+ import type { OmitArg } from './utils'
3
+ import { callHandler, controlled, dom, statefulRootAttrs as rootAttrs } from 'ajo-cloves'
4
+
5
+ /** Props for a controlled or uncontrolled two-state toggle button. */
6
+ export type ToggleArgs = OmitArg<IntrinsicElements['button'], 'children'> & WithChildren<{
7
+ /** Controlled pressed state. */
8
+ pressed?: boolean
9
+ /** Initial pressed state for uncontrolled usage. */
10
+ defaultPressed?: boolean
11
+ /** Called when the pressed state changes. */
12
+ onPressedChange?: (pressed: boolean, event: Event) => void
13
+ }>
14
+
15
+ type ToggleRootArgs = WithChildren<{
16
+ defaultPressed?: boolean
17
+ disabled?: boolean
18
+ onClick?: unknown
19
+ onPressedChange?: ToggleArgs['onPressedChange']
20
+ pressed?: boolean
21
+ }>
22
+
23
+ const pressedAttribute = (pressed: boolean) => pressed ? 'true' : 'false'
24
+ const stateAttribute = (pressed: boolean) => pressed ? 'on' : 'off'
25
+
26
+ const ToggleRoot: Stateful<ToggleRootArgs, 'button'> = function* ({ defaultPressed, pressed }) {
27
+ let disabled = false
28
+ let onClick: unknown
29
+ let onPressedChange: ToggleArgs['onPressedChange']
30
+ const state = controlled<boolean>(this, {
31
+ fallback: Boolean(pressed ?? defaultPressed),
32
+ onChange: (next, event) => onPressedChange?.(next, event!),
33
+ })
34
+
35
+ for (const args of this) {
36
+ disabled = Boolean(args.disabled)
37
+ onClick = args.onClick
38
+ onPressedChange = args.onPressedChange
39
+ state.sync(args.pressed != null ? Boolean(args.pressed) : undefined)
40
+
41
+ if (dom(this)) {
42
+ this.dataset.state = stateAttribute(state.value)
43
+ this.setAttribute('aria-pressed', pressedAttribute(state.value))
44
+ this.onclick = event => {
45
+ callHandler(onClick, event)
46
+ if (event.defaultPrevented || disabled) return
47
+
48
+ state.set(!state.value, event)
49
+ }
50
+ }
51
+
52
+ yield <>{args.children}</>
53
+ }
54
+ }
55
+
56
+ ToggleRoot.is = 'button'
57
+
58
+ /** Unstyled two-state button using aria-pressed. */
59
+ const Toggle: Stateless<ToggleArgs> = ({
60
+ children,
61
+ defaultPressed,
62
+ disabled,
63
+ onPressedChange,
64
+ pressed,
65
+ type = 'button',
66
+ 'set:onclick': onClick,
67
+ ...attrs
68
+ }) => {
69
+ const state = stateAttribute(Boolean(pressed ?? defaultPressed))
70
+ const disabledFlag = Boolean(disabled)
71
+
72
+ return (
73
+ <ToggleRoot
74
+ {...rootAttrs(attrs)}
75
+ defaultPressed={defaultPressed}
76
+ disabled={disabledFlag}
77
+ onClick={onClick}
78
+ onPressedChange={onPressedChange}
79
+ pressed={pressed}
80
+ attr:aria-pressed={pressedAttribute(state === 'on')}
81
+ attr:data-state={state}
82
+ attr:data-slot={attrs['data-slot'] ?? 'toggle'}
83
+ attr:disabled={disabledFlag || undefined}
84
+ attr:type={type}
85
+ >
86
+ {children}
87
+ </ToggleRoot>
88
+ )
89
+ }
90
+
91
+ export { Toggle }
@@ -0,0 +1,212 @@
1
+ import type { IntrinsicElements, Stateful, Stateless, WithChildren } from 'ajo'
2
+ import type { OmitArg } from './utils'
3
+ import { dom, listen, roving, statefulRootAttrs as rootAttrs } from 'ajo-cloves'
4
+ import { context } from 'ajo/context'
5
+ import { DirectionContext } from './direction'
6
+
7
+ /** Layout and keyboard-navigation axis of a toolbar. */
8
+ export type ToolbarOrientation = 'horizontal' | 'vertical'
9
+
10
+ /** Props for a toolbar with roving focus among descendant controls. */
11
+ export type ToolbarArgs = WithChildren<OmitArg<IntrinsicElements['div'], 'dir'> & {
12
+ /** Text direction for horizontal arrow-key navigation. Defaults to the nearest DirectionProvider. */
13
+ dir?: 'ltr' | 'rtl'
14
+ /** Allow arrow-key focus to wrap at the ends. */
15
+ loop?: boolean
16
+ /** Toolbar orientation. */
17
+ orientation?: ToolbarOrientation
18
+ }>
19
+
20
+ /** Props for a separator whose orientation follows the parent toolbar. */
21
+ export type ToolbarSeparatorArgs = IntrinsicElements['div']
22
+
23
+ type ToolbarRootArgs = WithChildren<{
24
+ dir: 'ltr' | 'rtl'
25
+ loop: boolean
26
+ orientation: ToolbarOrientation
27
+ }>
28
+
29
+ type ToolbarContextValue = {
30
+ orientation: ToolbarOrientation
31
+ }
32
+
33
+ const ToolbarContext = context<ToolbarContextValue | null>(null)
34
+
35
+ const CONTROL_SELECTOR = 'button, [href], input, select, textarea, summary, [tabindex]'
36
+
37
+ // Focusable toolbar controls in DOM order (the house trigger-row filter:
38
+ // enabled and visible; summary joins for details-based facets), scoped to
39
+ // the toolbar's own layer: controls inside floating popover content opened
40
+ // from the toolbar (menus, selects) or inside a composed <dialog> never join
41
+ // the row. Visibility uses checkVisibility because closed-details content is
42
+ // content-visibility-hidden in Chromium (offsetParent stays non-null there);
43
+ // the offsetParent fallback covers engines without it.
44
+ const controls = (root: HTMLElement) => {
45
+ const layer = root.closest('[popover], dialog')
46
+ return Array.from(root.querySelectorAll<HTMLElement>(CONTROL_SELECTOR)).filter(control =>
47
+ !(control as HTMLButtonElement).disabled &&
48
+ (control.checkVisibility?.() ?? control.offsetParent !== null) &&
49
+ control.closest('[popover], dialog') === layer,
50
+ )
51
+ }
52
+
53
+ const NON_TEXT_INPUTS = new Set(['button', 'checkbox', 'color', 'file', 'hidden', 'image', 'radio', 'range', 'reset', 'submit'])
54
+
55
+ // Text-entry controls own their caret keys (APG): roving must not steal them.
56
+ const textEntry = (element: HTMLElement) =>
57
+ element instanceof HTMLTextAreaElement ||
58
+ element.isContentEditable ||
59
+ (element instanceof HTMLInputElement && !NON_TEXT_INPUTS.has(element.type))
60
+
61
+ // A text control releases an arrow key only when caret handling no longer
62
+ // needs it: a collapsed caret sitting at the matching edge. Home/End and
63
+ // vertical arrows always stay with the caret. The escape edge follows the
64
+ // control's own resolved direction (a dir="auto" or forced-ltr input inside
65
+ // an rtl toolbar keeps its productive caret keys); the toolbar dir only
66
+ // steers roving movement.
67
+ const caretEscapes = (target: HTMLElement, event: KeyboardEvent) => {
68
+ if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return false
69
+ if (!(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)) return false
70
+ const caret = target.selectionStart
71
+ if (caret == null || caret !== target.selectionEnd) return false
72
+ const rtl = getComputedStyle(target).direction === 'rtl'
73
+ return (event.key === 'ArrowLeft') !== rtl ? caret === 0 : caret === target.value.length
74
+ }
75
+
76
+ const ToolbarRoot: Stateful<ToolbarRootArgs> = function* () {
77
+ let active: HTMLElement | null = null
78
+ let dir: 'ltr' | 'rtl' = 'ltr'
79
+ let loop = true
80
+ let orientation: ToolbarOrientation = 'horizontal'
81
+ let queued = false
82
+
83
+ // Single tab stop over the live control row (roving tabindex): the active
84
+ // control keeps tabindex 0, the rest get -1; focusing any control moves the
85
+ // stop, and the stop is repaired to the first control in DOM order when the
86
+ // active one unmounts, hides, or disables.
87
+ // Toolbar keeps this separate from bar(): it navigates arbitrary controls,
88
+ // not an open-value trigger row with follow policy.
89
+ const apply = () => {
90
+ if (!dom(this)) return
91
+ const row = controls(this)
92
+ if (!active || !row.includes(active)) active = row[0] ?? null
93
+ for (const control of row) {
94
+ const value = control === active ? '0' : '-1'
95
+ if (control.getAttribute('tabindex') !== value) control.setAttribute('tabindex', value)
96
+ }
97
+ }
98
+
99
+ const sync = () => {
100
+ if (queued) return
101
+ queued = true
102
+ queueMicrotask(() => {
103
+ queued = false
104
+ apply()
105
+ })
106
+ }
107
+
108
+ const nav = roving(this, {
109
+ items: () => controls(this),
110
+ orientation: () => orientation,
111
+ dir: () => dir,
112
+ loop: () => loop,
113
+ onMove: target => target.focus(),
114
+ })
115
+
116
+ listen(this, 'focusin', (event: FocusEvent) => {
117
+ const target = event.target as HTMLElement | null
118
+ if (!target || !controls(this).includes(target)) return
119
+ active = target
120
+ apply()
121
+ })
122
+
123
+ listen(this, 'keydown', (event: KeyboardEvent) => {
124
+ const target = event.target as HTMLElement | null
125
+ // A composed widget that already consumed the key (open menu, slider,
126
+ // a toggle group outside a toolbar) wins over toolbar roving, and so
127
+ // does an active IME composition (keyCode 229 covers engines that
128
+ // fire the arrow before isComposing settles): an arrow at a caret
129
+ // edge mid-composition must not move focus and abort the composition.
130
+ if (!target || event.defaultPrevented || event.isComposing || event.keyCode === 229) return
131
+ if (!controls(this).includes(target)) return
132
+ if (textEntry(target) && !caretEscapes(target, event)) return
133
+ nav.handle(event)
134
+ })
135
+
136
+ if (dom(this)) {
137
+ // Controls can mount/unmount or flip disabled/hidden from a nested
138
+ // component's own render without this component re-rendering; watch the
139
+ // subtree and repair the tab stop. The equality guard in apply() keeps
140
+ // its own tabindex writes from re-firing the observer.
141
+ const observer = new MutationObserver(sync)
142
+ observer.observe(this, {
143
+ attributeFilter: ['disabled', 'hidden', 'tabindex'],
144
+ attributes: true,
145
+ childList: true,
146
+ subtree: true,
147
+ })
148
+ this.signal.addEventListener('abort', () => observer.disconnect())
149
+ }
150
+
151
+ for (const args of this) {
152
+ dir = args.dir
153
+ loop = args.loop
154
+ orientation = args.orientation
155
+
156
+ ToolbarContext({ orientation })
157
+
158
+ sync()
159
+ yield <>{args.children}</>
160
+ }
161
+ }
162
+
163
+
164
+ /** Unstyled toolbar: arbitrary controls behind a single tab stop with dir-aware arrow-key roving (APG toolbar pattern). Button, ToggleGroup, Select, and Input compose inside; nested ToggleGroup items join the toolbar roving. */
165
+ const Toolbar: Stateless<ToolbarArgs> = ({
166
+ children,
167
+ 'data-slot': slot = 'toolbar',
168
+ dir,
169
+ loop = true,
170
+ orientation = 'horizontal',
171
+ role = 'toolbar',
172
+ ...attrs
173
+ }) => {
174
+ const resolvedDir = dir ?? DirectionContext()
175
+
176
+ return (
177
+ <ToolbarRoot
178
+ {...rootAttrs(attrs)}
179
+ dir={resolvedDir}
180
+ loop={loop}
181
+ orientation={orientation}
182
+ attr:aria-orientation={orientation}
183
+ attr:data-orientation={orientation}
184
+ attr:data-slot={slot}
185
+ attr:dir={resolvedDir}
186
+ attr:role={role}
187
+ >
188
+ {children}
189
+ </ToolbarRoot>
190
+ )
191
+ }
192
+
193
+ /** Unstyled separator between toolbar groups, oriented across the toolbar axis. */
194
+ const ToolbarSeparator: Stateless<ToolbarSeparatorArgs> = ({
195
+ role = 'separator',
196
+ ...attrs
197
+ }) => {
198
+ const toolbar = ToolbarContext()
199
+ const orientation = (toolbar?.orientation ?? 'horizontal') === 'horizontal' ? 'vertical' : 'horizontal'
200
+
201
+ return (
202
+ <div
203
+ {...attrs}
204
+ aria-orientation={orientation}
205
+ data-orientation={orientation}
206
+ data-slot="toolbar-separator"
207
+ role={role}
208
+ />
209
+ )
210
+ }
211
+
212
+ export { Toolbar, ToolbarSeparator }