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.
- package/LICENSE +15 -0
- package/README.md +126 -0
- package/dist/accordion.js +130 -0
- package/dist/avatar.js +54 -0
- package/dist/calendar.js +2 -0
- package/dist/carousel.js +239 -0
- package/dist/chart.js +790 -0
- package/dist/checkbox-group.js +70 -0
- package/dist/checkbox.js +77 -0
- package/dist/chunks/bar-CVafh6C1.js +99 -0
- package/dist/chunks/calendar-q8jZ8Cxr.js +1923 -0
- package/dist/chunks/collection-DtRB63U4.js +111 -0
- package/dist/chunks/data-table-DpkWv4y4.js +1039 -0
- package/dist/chunks/menu-B45IyHHC.js +704 -0
- package/dist/chunks/native-BJdhd9XJ.js +20 -0
- package/dist/chunks/popup-C8Bb3l_g.js +459 -0
- package/dist/chunks/popup-surface-BDCgtVU0.js +18 -0
- package/dist/chunks/position-D6_i_SRn.js +434 -0
- package/dist/chunks/virtual-list-B7hjGkjk.js +413 -0
- package/dist/collapsible.js +106 -0
- package/dist/command.js +263 -0
- package/dist/context-menu.js +112 -0
- package/dist/data-table.js +5 -0
- package/dist/dialog.js +207 -0
- package/dist/direction.js +22 -0
- package/dist/drawer.js +139 -0
- package/dist/field.js +26 -0
- package/dist/index.js +38 -0
- package/dist/input-date.js +994 -0
- package/dist/input-group.js +52 -0
- package/dist/input-otp.js +179 -0
- package/dist/menu.js +2 -0
- package/dist/menubar.js +236 -0
- package/dist/message-scroller.js +446 -0
- package/dist/navigation-menu.js +330 -0
- package/dist/popover.js +307 -0
- package/dist/progress.js +39 -0
- package/dist/radio-group.js +107 -0
- package/dist/resizable.js +172 -0
- package/dist/select.js +961 -0
- package/dist/sidebar.js +343 -0
- package/dist/slider.js +259 -0
- package/dist/switch.js +53 -0
- package/dist/tabs.js +182 -0
- package/dist/toast.js +492 -0
- package/dist/toggle-group.js +111 -0
- package/dist/toggle.js +52 -0
- package/dist/toolbar.js +127 -0
- package/dist/tooltip.js +196 -0
- package/dist/utils.js +104 -0
- package/dist/virtual-list.js +2 -0
- package/package.json +250 -0
- package/src/accordion.tsx +261 -0
- package/src/availability.ts +261 -0
- package/src/avatar.tsx +99 -0
- package/src/bar.ts +156 -0
- package/src/calendar.tsx +1441 -0
- package/src/carousel.tsx +424 -0
- package/src/chart.tsx +1194 -0
- package/src/checkbox-group.tsx +132 -0
- package/src/checkbox.tsx +130 -0
- package/src/collapsible.tsx +188 -0
- package/src/collection.ts +154 -0
- package/src/command.tsx +511 -0
- package/src/context-menu.tsx +233 -0
- package/src/data-table-contract.ts +143 -0
- package/src/data-table-model.ts +760 -0
- package/src/data-table.tsx +475 -0
- package/src/dialog.tsx +393 -0
- package/src/direction.tsx +45 -0
- package/src/drawer.tsx +251 -0
- package/src/field.tsx +61 -0
- package/src/index.ts +37 -0
- package/src/input-date.tsx +1539 -0
- package/src/input-group.tsx +142 -0
- package/src/input-otp.tsx +324 -0
- package/src/menu-cluster.ts +124 -0
- package/src/menu.tsx +1095 -0
- package/src/menubar.tsx +459 -0
- package/src/message-scroller.tsx +732 -0
- package/src/native.ts +26 -0
- package/src/navigation-menu.tsx +578 -0
- package/src/popover.tsx +519 -0
- package/src/popup-surface.tsx +31 -0
- package/src/popup.ts +569 -0
- package/src/position.ts +523 -0
- package/src/progress.tsx +70 -0
- package/src/radio-group.tsx +186 -0
- package/src/resizable.tsx +310 -0
- package/src/segments.ts +922 -0
- package/src/select.tsx +1501 -0
- package/src/sidebar.tsx +683 -0
- package/src/slider.tsx +424 -0
- package/src/switch.tsx +104 -0
- package/src/tabs.tsx +314 -0
- package/src/toast.tsx +923 -0
- package/src/toggle-group.tsx +249 -0
- package/src/toggle.tsx +91 -0
- package/src/toolbar.tsx +212 -0
- package/src/tooltip.tsx +359 -0
- package/src/utils.ts +204 -0
- package/src/virtual-list.tsx +205 -0
- package/src/virtual.ts +385 -0
package/src/tabs.tsx
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import type { IntrinsicElements, Stateful, Stateless, WithChildren } from 'ajo'
|
|
2
|
+
import { callHandler, callRef, controlled, dom, id, indicator, listen, overflow, roving, statefulRootAttrs as rootAttrs } from 'ajo-cloves'
|
|
3
|
+
import { context } from 'ajo/context'
|
|
4
|
+
import { DirectionContext } from './direction'
|
|
5
|
+
import type { FixedArgs, OmitArg } from './utils'
|
|
6
|
+
|
|
7
|
+
/** Layout and keyboard-navigation axis of a tab list. */
|
|
8
|
+
export type TabsOrientation = 'horizontal' | 'vertical'
|
|
9
|
+
/** Whether moving focus or explicit activation selects a tab. */
|
|
10
|
+
export type TabsActivationMode = 'automatic' | 'manual'
|
|
11
|
+
|
|
12
|
+
/** Props for the tabs root and its controlled selection. */
|
|
13
|
+
export type TabsArgs = WithChildren<OmitArg<IntrinsicElements['div'], 'defaultValue' | 'dir' | 'onchange'> & {
|
|
14
|
+
/** Controlled selected tab value. */
|
|
15
|
+
value?: string
|
|
16
|
+
/** Initial selected tab value for uncontrolled usage. */
|
|
17
|
+
defaultValue?: string
|
|
18
|
+
/** Called whenever the selected tab changes. */
|
|
19
|
+
onValueChange?: (value: string, event?: Event) => void
|
|
20
|
+
/** Tab activation mode. Automatic follows focus; manual waits for Enter/Space or click. */
|
|
21
|
+
activationMode?: TabsActivationMode
|
|
22
|
+
/** Text direction for horizontal arrow-key navigation. Defaults to the nearest DirectionProvider. */
|
|
23
|
+
dir?: 'ltr' | 'rtl'
|
|
24
|
+
/** Allow arrow-key focus to wrap at the ends. */
|
|
25
|
+
loop?: boolean
|
|
26
|
+
/** Tablist orientation. */
|
|
27
|
+
orientation?: TabsOrientation
|
|
28
|
+
}> & FixedArgs<'onchange'>
|
|
29
|
+
|
|
30
|
+
/** Props for the container that owns tab triggers. */
|
|
31
|
+
export type TabsListArgs = WithChildren<IntrinsicElements['div']>
|
|
32
|
+
|
|
33
|
+
/** Props for a button that activates one tab value. */
|
|
34
|
+
export type TabsTriggerArgs = WithChildren<IntrinsicElements['button'] & {
|
|
35
|
+
/** Tab value controlled by this trigger. */
|
|
36
|
+
value: string
|
|
37
|
+
}>
|
|
38
|
+
|
|
39
|
+
/** Props for the panel associated with one tab value. */
|
|
40
|
+
export type TabsContentArgs = WithChildren<IntrinsicElements['div'] & {
|
|
41
|
+
/** Tab value that owns this panel. */
|
|
42
|
+
value: string
|
|
43
|
+
/** Keep the panel mounted while inactive. */
|
|
44
|
+
forceMount?: boolean
|
|
45
|
+
}>
|
|
46
|
+
|
|
47
|
+
type TabsRootArgs = WithChildren<{
|
|
48
|
+
activationMode: TabsActivationMode
|
|
49
|
+
defaultValue?: string
|
|
50
|
+
dir: 'ltr' | 'rtl'
|
|
51
|
+
loop: boolean
|
|
52
|
+
onValueChange?: (value: string, event?: Event) => void
|
|
53
|
+
orientation: TabsOrientation
|
|
54
|
+
value?: string
|
|
55
|
+
}>
|
|
56
|
+
|
|
57
|
+
type TabsContextValue = {
|
|
58
|
+
activationMode: TabsActivationMode
|
|
59
|
+
contentId: (value: string) => string
|
|
60
|
+
dir: 'ltr' | 'rtl'
|
|
61
|
+
loop: boolean
|
|
62
|
+
orientation: TabsOrientation
|
|
63
|
+
selected: (value: string) => boolean
|
|
64
|
+
setList: (element: HTMLElement | null) => void
|
|
65
|
+
setValue: (value: string, event?: Event) => void
|
|
66
|
+
triggerId: (value: string) => string
|
|
67
|
+
value: string
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const TabsContext = context<TabsContextValue | null>(null)
|
|
71
|
+
|
|
72
|
+
const state = (active: boolean) => active ? 'active' : 'inactive'
|
|
73
|
+
|
|
74
|
+
const tabs = (root: HTMLElement) =>
|
|
75
|
+
Array.from(root.querySelectorAll<HTMLButtonElement>('button[data-slot="tabs-trigger"]'))
|
|
76
|
+
.filter(button => !button.disabled && button.offsetParent !== null)
|
|
77
|
+
|
|
78
|
+
const TabsRoot: Stateful<TabsRootArgs> = function* ({ defaultValue, value }) {
|
|
79
|
+
const rootId = id('tabs')
|
|
80
|
+
let activationMode: TabsActivationMode = 'automatic'
|
|
81
|
+
let dir: 'ltr' | 'rtl' = 'ltr'
|
|
82
|
+
let loop = true
|
|
83
|
+
let onValueChange: TabsRootArgs['onValueChange']
|
|
84
|
+
let orientation: TabsOrientation = 'horizontal'
|
|
85
|
+
const state = controlled<string>(this, {
|
|
86
|
+
fallback: String(value ?? defaultValue ?? ''),
|
|
87
|
+
onChange: (next, event) => onValueChange?.(next, event),
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const triggerId = (value: string) => `${rootId}-trigger-${encodeURIComponent(value)}`
|
|
91
|
+
const contentId = (value: string) => `${rootId}-content-${encodeURIComponent(value)}`
|
|
92
|
+
const selected = (value: string) => state.value === value
|
|
93
|
+
|
|
94
|
+
const setValue = (next: string, event?: Event) => {
|
|
95
|
+
if (!next || next === state.value) return
|
|
96
|
+
state.set(next, event)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const ensureValue = () => {
|
|
100
|
+
if (state.value || state.controlled || !dom(this)) return
|
|
101
|
+
const first = tabs(this)[0]
|
|
102
|
+
if (first?.value) state.init(first.value)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const nav = roving(this, {
|
|
106
|
+
items: () => tabs(this),
|
|
107
|
+
orientation: () => orientation,
|
|
108
|
+
dir: () => dir,
|
|
109
|
+
loop: () => loop,
|
|
110
|
+
onMove: (target, event) => {
|
|
111
|
+
target.focus()
|
|
112
|
+
target.scrollIntoView({ block: 'nearest', inline: 'nearest' })
|
|
113
|
+
if (activationMode === 'automatic') setValue(String((target as HTMLButtonElement).value), event)
|
|
114
|
+
},
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// Overflow stamps on the tablist so themes can scroll it and fade the
|
|
118
|
+
// overflowing edges (data-overflow-x/-y pair with the mask preflight).
|
|
119
|
+
let list: HTMLElement | null = null
|
|
120
|
+
const edges = overflow(this, { target: () => list })
|
|
121
|
+
// Indicator variables on the tablist let themes glide an active marker
|
|
122
|
+
// between triggers instead of restyling each one.
|
|
123
|
+
const mark = indicator(this, {
|
|
124
|
+
target: () => list,
|
|
125
|
+
of: container => container.querySelector<HTMLElement>('button[data-slot="tabs-trigger"][data-state="active"]'),
|
|
126
|
+
})
|
|
127
|
+
const setList = (element: HTMLElement | null) => {
|
|
128
|
+
list = element
|
|
129
|
+
edges.sync()
|
|
130
|
+
mark.sync()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
listen(this, 'keydown', (event: KeyboardEvent) => {
|
|
134
|
+
const target = event.target as HTMLElement | null
|
|
135
|
+
if (!target?.matches('button[data-slot="tabs-trigger"]')) return
|
|
136
|
+
|
|
137
|
+
if (nav.handle(event)) return
|
|
138
|
+
|
|
139
|
+
if (activationMode === 'manual' && (event.key === 'Enter' || event.key === ' ')) {
|
|
140
|
+
event.preventDefault()
|
|
141
|
+
setValue(String((target as HTMLButtonElement).value), event)
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
for (const args of this) {
|
|
146
|
+
activationMode = args.activationMode
|
|
147
|
+
dir = args.dir
|
|
148
|
+
loop = args.loop
|
|
149
|
+
onValueChange = args.onValueChange
|
|
150
|
+
orientation = args.orientation
|
|
151
|
+
state.sync(args.value != null ? String(args.value) : undefined)
|
|
152
|
+
|
|
153
|
+
TabsContext({
|
|
154
|
+
activationMode,
|
|
155
|
+
contentId,
|
|
156
|
+
dir,
|
|
157
|
+
loop,
|
|
158
|
+
orientation,
|
|
159
|
+
selected,
|
|
160
|
+
setList,
|
|
161
|
+
setValue,
|
|
162
|
+
triggerId,
|
|
163
|
+
value: state.value,
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
edges.sync()
|
|
167
|
+
mark.sync()
|
|
168
|
+
queueMicrotask(ensureValue)
|
|
169
|
+
yield <>{args.children}</>
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
/** Unstyled root provider for tab state. */
|
|
175
|
+
const Tabs: Stateless<TabsArgs> = ({
|
|
176
|
+
activationMode = 'automatic',
|
|
177
|
+
children,
|
|
178
|
+
defaultValue,
|
|
179
|
+
dir,
|
|
180
|
+
loop = true,
|
|
181
|
+
onValueChange,
|
|
182
|
+
orientation = 'horizontal',
|
|
183
|
+
value,
|
|
184
|
+
...attrs
|
|
185
|
+
}) => {
|
|
186
|
+
const resolvedDir = dir ?? DirectionContext()
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<TabsRoot
|
|
190
|
+
{...rootAttrs(attrs)}
|
|
191
|
+
activationMode={activationMode}
|
|
192
|
+
defaultValue={defaultValue}
|
|
193
|
+
dir={resolvedDir}
|
|
194
|
+
loop={loop}
|
|
195
|
+
onValueChange={onValueChange}
|
|
196
|
+
orientation={orientation}
|
|
197
|
+
value={value}
|
|
198
|
+
attr:data-orientation={orientation}
|
|
199
|
+
attr:data-slot="tabs"
|
|
200
|
+
attr:dir={resolvedDir}
|
|
201
|
+
>
|
|
202
|
+
{children}
|
|
203
|
+
</TabsRoot>
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Unstyled container for tab triggers. */
|
|
208
|
+
const TabsList: Stateless<TabsListArgs> = ({
|
|
209
|
+
children,
|
|
210
|
+
ref,
|
|
211
|
+
role = 'tablist',
|
|
212
|
+
...attrs
|
|
213
|
+
}) => {
|
|
214
|
+
const context = TabsContext()
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<div
|
|
218
|
+
{...attrs}
|
|
219
|
+
aria-orientation={context?.orientation}
|
|
220
|
+
data-orientation={context?.orientation}
|
|
221
|
+
data-slot="tabs-list"
|
|
222
|
+
ref={(element: HTMLDivElement | null) => {
|
|
223
|
+
context?.setList(element)
|
|
224
|
+
callRef(ref, element)
|
|
225
|
+
}}
|
|
226
|
+
role={role}
|
|
227
|
+
>
|
|
228
|
+
{children}
|
|
229
|
+
</div>
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Unstyled button that activates a tab panel. */
|
|
234
|
+
const TabsTrigger: Stateless<TabsTriggerArgs> = ({
|
|
235
|
+
children,
|
|
236
|
+
disabled,
|
|
237
|
+
id,
|
|
238
|
+
'set:onclick': onClick,
|
|
239
|
+
'set:onfocus': onFocus,
|
|
240
|
+
type = 'button',
|
|
241
|
+
value,
|
|
242
|
+
...attrs
|
|
243
|
+
}) => {
|
|
244
|
+
const context = TabsContext()
|
|
245
|
+
const tabValue = String(value)
|
|
246
|
+
const active = Boolean(context?.selected(tabValue))
|
|
247
|
+
const disabledFlag = Boolean(disabled)
|
|
248
|
+
const triggerId = id ?? context?.triggerId(tabValue)
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<button
|
|
252
|
+
{...attrs}
|
|
253
|
+
aria-controls={context?.contentId(tabValue)}
|
|
254
|
+
aria-selected={active ? 'true' : 'false'}
|
|
255
|
+
data-slot="tabs-trigger"
|
|
256
|
+
data-state={state(active)}
|
|
257
|
+
data-value={tabValue}
|
|
258
|
+
disabled={disabledFlag}
|
|
259
|
+
id={triggerId}
|
|
260
|
+
role="tab"
|
|
261
|
+
set:onclick={(event: Event) => {
|
|
262
|
+
callHandler(onClick, event)
|
|
263
|
+
if (event.defaultPrevented || disabledFlag) return
|
|
264
|
+
context?.setValue(tabValue, event)
|
|
265
|
+
// A partially visible tab in an overflowing list scrolls fully
|
|
266
|
+
// into view on activation; scroll-padding keeps it clear of
|
|
267
|
+
// the theme's edge fade. No-op while the list fits.
|
|
268
|
+
;(event.currentTarget as HTMLElement).scrollIntoView({ block: 'nearest', inline: 'nearest' })
|
|
269
|
+
}}
|
|
270
|
+
set:onfocus={(event: FocusEvent) => {
|
|
271
|
+
callHandler(onFocus, event)
|
|
272
|
+
if (event.defaultPrevented || disabledFlag || active || context?.activationMode !== 'automatic') return
|
|
273
|
+
context?.setValue(tabValue, event)
|
|
274
|
+
}}
|
|
275
|
+
tabindex={active ? 0 : -1}
|
|
276
|
+
type={type}
|
|
277
|
+
value={tabValue}
|
|
278
|
+
>
|
|
279
|
+
{children}
|
|
280
|
+
</button>
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Unstyled panel displayed by its matching tab trigger. */
|
|
285
|
+
const TabsContent: Stateless<TabsContentArgs> = ({
|
|
286
|
+
children,
|
|
287
|
+
forceMount,
|
|
288
|
+
value,
|
|
289
|
+
...attrs
|
|
290
|
+
}) => {
|
|
291
|
+
const context = TabsContext()
|
|
292
|
+
const tabValue = String(value)
|
|
293
|
+
const active = Boolean(context?.selected(tabValue))
|
|
294
|
+
|
|
295
|
+
if (!active && !forceMount) return null
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
<div
|
|
299
|
+
{...attrs}
|
|
300
|
+
aria-labelledby={context?.triggerId(tabValue)}
|
|
301
|
+
data-slot="tabs-content"
|
|
302
|
+
data-state={state(active)}
|
|
303
|
+
data-value={tabValue}
|
|
304
|
+
hidden={active ? undefined : true}
|
|
305
|
+
id={context?.contentId(tabValue)}
|
|
306
|
+
role="tabpanel"
|
|
307
|
+
tabindex={0}
|
|
308
|
+
>
|
|
309
|
+
{children}
|
|
310
|
+
</div>
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export { Tabs, TabsContent, TabsList, TabsTrigger }
|