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/dialog.tsx
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import type { IntrinsicElements, Stateful, Stateless, WithChildren } from 'ajo'
|
|
2
|
+
import { callHandler, callRef, controlled, id, statefulRootAttrs as rootAttrs } from 'ajo-cloves'
|
|
3
|
+
import { context } from 'ajo/context'
|
|
4
|
+
import { triggerAttrs } from './utils'
|
|
5
|
+
import type { FixedArgs, OmitArg } from './utils'
|
|
6
|
+
|
|
7
|
+
/** Arguments for the Dialog state provider and its wrapper host. */
|
|
8
|
+
export type DialogArgs = WithChildren<OmitArg<IntrinsicElements['div'], 'onchange'> & {
|
|
9
|
+
/** Controlled open state. */
|
|
10
|
+
open?: boolean
|
|
11
|
+
/** Initial open state for uncontrolled usage. */
|
|
12
|
+
defaultOpen?: boolean
|
|
13
|
+
/** Use native modal behavior. Set false for modeless dialogs. */
|
|
14
|
+
modal?: boolean
|
|
15
|
+
/** Called whenever the dialog opens or closes. */
|
|
16
|
+
onOpenChange?: (open: boolean, event?: Event) => void
|
|
17
|
+
/** Additional CSS classes for the root wrapper. */
|
|
18
|
+
class?: string
|
|
19
|
+
}> & FixedArgs<'onchange'>
|
|
20
|
+
|
|
21
|
+
/** Arguments for the button that opens its nearest Dialog. */
|
|
22
|
+
export type DialogTriggerArgs = WithChildren<IntrinsicElements['button'] & {
|
|
23
|
+
/** Additional CSS classes. */
|
|
24
|
+
class?: string
|
|
25
|
+
}>
|
|
26
|
+
|
|
27
|
+
/** Arguments for a button that closes its nearest Dialog. */
|
|
28
|
+
export type DialogCloseArgs = WithChildren<IntrinsicElements['button'] & {
|
|
29
|
+
/** Additional CSS classes. */
|
|
30
|
+
class?: string
|
|
31
|
+
}>
|
|
32
|
+
|
|
33
|
+
/** Arguments for the native dialog panel and its dismissal hooks. */
|
|
34
|
+
export type DialogContentArgs = WithChildren<OmitArg<IntrinsicElements['dialog'], 'open'> & {
|
|
35
|
+
/** Called when Escape requests dialog close. Prevent default to keep it open. */
|
|
36
|
+
onEscapeKeyDown?: (event: KeyboardEvent) => void
|
|
37
|
+
/** Called when the native backdrop is clicked. Prevent default to keep it open. */
|
|
38
|
+
onPointerDownOutside?: (event: MouseEvent) => void
|
|
39
|
+
/** Additional CSS classes for the dialog panel. */
|
|
40
|
+
class?: string
|
|
41
|
+
}> & FixedArgs<'open'>
|
|
42
|
+
|
|
43
|
+
/** Shared arguments for structural sections inside a dialog panel. */
|
|
44
|
+
export type DialogSectionArgs = WithChildren<IntrinsicElements['div'] & {
|
|
45
|
+
/** Additional CSS classes. */
|
|
46
|
+
class?: string
|
|
47
|
+
}>
|
|
48
|
+
|
|
49
|
+
/** Arguments for the title and description area of a dialog. */
|
|
50
|
+
export type DialogHeaderArgs = DialogSectionArgs
|
|
51
|
+
|
|
52
|
+
/** Arguments for the action area at the end of a dialog. */
|
|
53
|
+
export type DialogFooterArgs = DialogSectionArgs
|
|
54
|
+
|
|
55
|
+
/** Arguments for the heading that labels DialogContent. */
|
|
56
|
+
export type DialogTitleArgs = WithChildren<IntrinsicElements['h2'] & {
|
|
57
|
+
/** Additional CSS classes. */
|
|
58
|
+
class?: string
|
|
59
|
+
}>
|
|
60
|
+
|
|
61
|
+
/** Arguments for the text that describes DialogContent. */
|
|
62
|
+
export type DialogDescriptionArgs = WithChildren<IntrinsicElements['p'] & {
|
|
63
|
+
/** Additional CSS classes. */
|
|
64
|
+
class?: string
|
|
65
|
+
}>
|
|
66
|
+
|
|
67
|
+
type DialogContextValue = {
|
|
68
|
+
close: (event?: Event) => void
|
|
69
|
+
contentId: string
|
|
70
|
+
descriptionId: string
|
|
71
|
+
modal: boolean
|
|
72
|
+
open: boolean
|
|
73
|
+
setContent: (element: HTMLDialogElement | null) => void
|
|
74
|
+
setOpen: (open: boolean, event?: Event) => void
|
|
75
|
+
setTrigger: (element: HTMLButtonElement | null) => void
|
|
76
|
+
titleId: string
|
|
77
|
+
triggerId: string
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const DialogContext = context<DialogContextValue | null>(null)
|
|
81
|
+
|
|
82
|
+
const dialog = () => {
|
|
83
|
+
const value = DialogContext()
|
|
84
|
+
if (!value) throw new Error('Dialog component must be used within Dialog.')
|
|
85
|
+
return value
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const outside = (element: HTMLDialogElement, event: MouseEvent) => {
|
|
89
|
+
const rect = element.getBoundingClientRect()
|
|
90
|
+
return event.clientX < rect.left ||
|
|
91
|
+
event.clientX > rect.right ||
|
|
92
|
+
event.clientY < rect.top ||
|
|
93
|
+
event.clientY > rect.bottom
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const DialogRoot: Stateful<DialogArgs> = function* ({ defaultOpen, open }) {
|
|
97
|
+
const dialogId = id('dialog')
|
|
98
|
+
const watched = new WeakSet<HTMLDialogElement>()
|
|
99
|
+
let content: HTMLDialogElement | null = null
|
|
100
|
+
let modal = true
|
|
101
|
+
let onOpenChange: DialogArgs['onOpenChange']
|
|
102
|
+
let syncing = false
|
|
103
|
+
let trigger: HTMLButtonElement | null = null
|
|
104
|
+
const state = controlled<boolean>(this, {
|
|
105
|
+
fallback: Boolean(open ?? defaultOpen),
|
|
106
|
+
onChange: (next, event) => onOpenChange?.(next, event),
|
|
107
|
+
})
|
|
108
|
+
let current = state.value
|
|
109
|
+
|
|
110
|
+
const focusTrigger = () => queueMicrotask(() => trigger?.focus())
|
|
111
|
+
|
|
112
|
+
const sync = () => queueMicrotask(() => {
|
|
113
|
+
if (!content) return
|
|
114
|
+
syncing = true
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
if (current && !content.open) {
|
|
118
|
+
if (modal) content.showModal()
|
|
119
|
+
else content.show()
|
|
120
|
+
} else if (!current && content.open) {
|
|
121
|
+
content.close()
|
|
122
|
+
}
|
|
123
|
+
} finally {
|
|
124
|
+
queueMicrotask(() => syncing = false)
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
const setOpen = (next: boolean, event?: Event) => {
|
|
129
|
+
if (next === current) return
|
|
130
|
+
state.set(next, event)
|
|
131
|
+
current = state.value
|
|
132
|
+
sync()
|
|
133
|
+
if (!next) focusTrigger()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const setContent = (element: HTMLDialogElement | null) => {
|
|
137
|
+
content = element
|
|
138
|
+
if (!element || watched.has(element)) return
|
|
139
|
+
|
|
140
|
+
watched.add(element)
|
|
141
|
+
element.addEventListener('close', event => {
|
|
142
|
+
if (syncing || current === false) return
|
|
143
|
+
state.accept(false, event)
|
|
144
|
+
current = state.value
|
|
145
|
+
focusTrigger()
|
|
146
|
+
}, { signal: this.signal })
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const args of this) {
|
|
150
|
+
modal = args.modal !== false
|
|
151
|
+
onOpenChange = args.onOpenChange
|
|
152
|
+
current = state.sync(args.open == null ? undefined : Boolean(args.open))
|
|
153
|
+
|
|
154
|
+
DialogContext({
|
|
155
|
+
close: event => setOpen(false, event),
|
|
156
|
+
contentId: `${dialogId}-content`,
|
|
157
|
+
descriptionId: `${dialogId}-description`,
|
|
158
|
+
modal,
|
|
159
|
+
open: current,
|
|
160
|
+
setContent,
|
|
161
|
+
setOpen,
|
|
162
|
+
setTrigger: element => trigger = element,
|
|
163
|
+
titleId: `${dialogId}-title`,
|
|
164
|
+
triggerId: `${dialogId}-trigger`,
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
sync()
|
|
168
|
+
yield <>{args.children}</>
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
/** Unstyled root provider for a native dialog. */
|
|
174
|
+
const Dialog: Stateless<DialogArgs> = ({
|
|
175
|
+
children,
|
|
176
|
+
class: classes,
|
|
177
|
+
'data-slot': slot = 'dialog',
|
|
178
|
+
defaultOpen,
|
|
179
|
+
modal,
|
|
180
|
+
onOpenChange,
|
|
181
|
+
open,
|
|
182
|
+
...attrs
|
|
183
|
+
}) => (
|
|
184
|
+
<DialogRoot
|
|
185
|
+
{...rootAttrs(attrs)}
|
|
186
|
+
defaultOpen={defaultOpen}
|
|
187
|
+
modal={modal}
|
|
188
|
+
onOpenChange={onOpenChange}
|
|
189
|
+
open={open}
|
|
190
|
+
attr:class={classes}
|
|
191
|
+
attr:data-slot={slot}
|
|
192
|
+
>
|
|
193
|
+
{children}
|
|
194
|
+
</DialogRoot>
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
/** Unstyled button that opens the nearest Dialog. */
|
|
198
|
+
const DialogTrigger: Stateless<DialogTriggerArgs> = ({
|
|
199
|
+
children,
|
|
200
|
+
'data-slot': slot = 'dialog-trigger',
|
|
201
|
+
disabled,
|
|
202
|
+
id,
|
|
203
|
+
ref,
|
|
204
|
+
type = 'button',
|
|
205
|
+
'set:onclick': onClick,
|
|
206
|
+
...attrs
|
|
207
|
+
}) => {
|
|
208
|
+
const ctx = dialog()
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<button
|
|
212
|
+
{...attrs}
|
|
213
|
+
{...triggerAttrs({
|
|
214
|
+
controls: ctx.contentId,
|
|
215
|
+
expanded: ctx.open,
|
|
216
|
+
id,
|
|
217
|
+
open: ctx.open,
|
|
218
|
+
ref,
|
|
219
|
+
setTrigger: ctx.setTrigger,
|
|
220
|
+
triggerId: ctx.triggerId,
|
|
221
|
+
})}
|
|
222
|
+
data-slot={slot}
|
|
223
|
+
disabled={disabled}
|
|
224
|
+
type={type}
|
|
225
|
+
set:onclick={(event: Event) => {
|
|
226
|
+
callHandler(onClick, event)
|
|
227
|
+
if (event.defaultPrevented || disabled) return
|
|
228
|
+
ctx.setOpen(true, event)
|
|
229
|
+
}}
|
|
230
|
+
>
|
|
231
|
+
{children}
|
|
232
|
+
</button>
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Unstyled button that closes the nearest Dialog. */
|
|
237
|
+
const DialogClose: Stateless<DialogCloseArgs> = ({
|
|
238
|
+
children,
|
|
239
|
+
'data-slot': slot = 'dialog-close',
|
|
240
|
+
disabled,
|
|
241
|
+
type = 'button',
|
|
242
|
+
'set:onclick': onClick,
|
|
243
|
+
...attrs
|
|
244
|
+
}) => {
|
|
245
|
+
const ctx = dialog()
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<button
|
|
249
|
+
{...attrs}
|
|
250
|
+
data-slot={slot}
|
|
251
|
+
disabled={disabled}
|
|
252
|
+
type={type}
|
|
253
|
+
set:onclick={(event: Event) => {
|
|
254
|
+
callHandler(onClick, event)
|
|
255
|
+
if (event.defaultPrevented || disabled) return
|
|
256
|
+
ctx.close(event)
|
|
257
|
+
}}
|
|
258
|
+
>
|
|
259
|
+
{children}
|
|
260
|
+
</button>
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Unstyled native dialog panel. */
|
|
265
|
+
const DialogContent: Stateless<DialogContentArgs> = ({
|
|
266
|
+
children,
|
|
267
|
+
class: classes,
|
|
268
|
+
'data-slot': slot = 'dialog-content',
|
|
269
|
+
onEscapeKeyDown,
|
|
270
|
+
onPointerDownOutside,
|
|
271
|
+
ref,
|
|
272
|
+
'aria-describedby': describedBy,
|
|
273
|
+
'aria-labelledby': labelledBy,
|
|
274
|
+
'set:oncancel': onCancel,
|
|
275
|
+
'set:onclick': onClick,
|
|
276
|
+
'set:onkeydown': onKeydown,
|
|
277
|
+
...attrs
|
|
278
|
+
}) => {
|
|
279
|
+
const ctx = dialog()
|
|
280
|
+
const reference = (element: HTMLDialogElement | null) => {
|
|
281
|
+
ctx.setContent(element)
|
|
282
|
+
callRef(ref, element)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return (
|
|
286
|
+
<dialog
|
|
287
|
+
{...attrs}
|
|
288
|
+
aria-describedby={describedBy ?? ctx.descriptionId}
|
|
289
|
+
aria-labelledby={labelledBy ?? ctx.titleId}
|
|
290
|
+
aria-modal={ctx.modal ? 'true' : undefined}
|
|
291
|
+
class={classes}
|
|
292
|
+
data-slot={slot}
|
|
293
|
+
data-state={ctx.open ? 'open' : 'closed'}
|
|
294
|
+
id={ctx.contentId}
|
|
295
|
+
ref={reference}
|
|
296
|
+
set:oncancel={(event: Event) => {
|
|
297
|
+
callHandler(onCancel, event)
|
|
298
|
+
if (event.defaultPrevented) return
|
|
299
|
+
if (event.cancelable) event.preventDefault()
|
|
300
|
+
ctx.close(event)
|
|
301
|
+
}}
|
|
302
|
+
set:onclick={(event: MouseEvent) => {
|
|
303
|
+
callHandler(onClick, event)
|
|
304
|
+
if (event.defaultPrevented) return
|
|
305
|
+
const target = event.currentTarget as HTMLDialogElement
|
|
306
|
+
if (event.target === target && outside(target, event)) {
|
|
307
|
+
onPointerDownOutside?.(event)
|
|
308
|
+
if (!event.defaultPrevented) ctx.close(event)
|
|
309
|
+
}
|
|
310
|
+
}}
|
|
311
|
+
set:onkeydown={(event: KeyboardEvent) => {
|
|
312
|
+
if (event.key === 'Escape') onEscapeKeyDown?.(event)
|
|
313
|
+
callHandler(onKeydown, event)
|
|
314
|
+
}}
|
|
315
|
+
>
|
|
316
|
+
{children}
|
|
317
|
+
{/* Portal outlet for top-layer UI that must stay interactive while
|
|
318
|
+
this dialog is modal (everything outside the dialog subtree is
|
|
319
|
+
inert). The Toaster re-homes its viewport here through a second
|
|
320
|
+
render root; `skip` keeps that root's DOM out of this tree's
|
|
321
|
+
reconciliation, the `key` keeps sibling churn in the dialog's
|
|
322
|
+
children from ever claiming this element (unkeyed matching
|
|
323
|
+
pairs by tag name alone), and display:contents keeps the
|
|
324
|
+
outlet out of the panel's layout. */}
|
|
325
|
+
<div data-slot="dialog-portal" key="dialog-portal" skip style="display:contents" />
|
|
326
|
+
</dialog>
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Unstyled header area for dialog title and description. */
|
|
331
|
+
const DialogHeader: Stateless<DialogHeaderArgs> = ({
|
|
332
|
+
children,
|
|
333
|
+
'data-slot': slot = 'dialog-header',
|
|
334
|
+
...attrs
|
|
335
|
+
}) => (
|
|
336
|
+
<div {...attrs} data-slot={slot}>
|
|
337
|
+
{children}
|
|
338
|
+
</div>
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
/** Unstyled footer area for dialog actions. */
|
|
342
|
+
const DialogFooter: Stateless<DialogFooterArgs> = ({
|
|
343
|
+
children,
|
|
344
|
+
'data-slot': slot = 'dialog-footer',
|
|
345
|
+
...attrs
|
|
346
|
+
}) => (
|
|
347
|
+
<div {...attrs} data-slot={slot}>
|
|
348
|
+
{children}
|
|
349
|
+
</div>
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
/** Accessible title for DialogContent. */
|
|
353
|
+
const DialogTitle: Stateless<DialogTitleArgs> = ({
|
|
354
|
+
children,
|
|
355
|
+
'data-slot': slot = 'dialog-title',
|
|
356
|
+
id,
|
|
357
|
+
...attrs
|
|
358
|
+
}) => {
|
|
359
|
+
const ctx = DialogContext()
|
|
360
|
+
|
|
361
|
+
return (
|
|
362
|
+
<h2 {...attrs} data-slot={slot} id={id ?? ctx?.titleId}>
|
|
363
|
+
{children}
|
|
364
|
+
</h2>
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Accessible description for DialogContent. */
|
|
369
|
+
const DialogDescription: Stateless<DialogDescriptionArgs> = ({
|
|
370
|
+
children,
|
|
371
|
+
'data-slot': slot = 'dialog-description',
|
|
372
|
+
id,
|
|
373
|
+
...attrs
|
|
374
|
+
}) => {
|
|
375
|
+
const ctx = DialogContext()
|
|
376
|
+
|
|
377
|
+
return (
|
|
378
|
+
<p {...attrs} data-slot={slot} id={id ?? ctx?.descriptionId}>
|
|
379
|
+
{children}
|
|
380
|
+
</p>
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export {
|
|
385
|
+
Dialog,
|
|
386
|
+
DialogClose,
|
|
387
|
+
DialogContent,
|
|
388
|
+
DialogDescription,
|
|
389
|
+
DialogFooter,
|
|
390
|
+
DialogHeader,
|
|
391
|
+
DialogTitle,
|
|
392
|
+
DialogTrigger,
|
|
393
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { IntrinsicElements, Stateful, Stateless, WithChildren } from 'ajo'
|
|
2
|
+
import type { OmitArg } from './utils'
|
|
3
|
+
import { statefulRootAttrs as rootAttrs } from 'ajo-cloves'
|
|
4
|
+
import { context } from 'ajo/context'
|
|
5
|
+
|
|
6
|
+
/** Logical text direction inherited by direction-aware components. */
|
|
7
|
+
export type Direction = 'ltr' | 'rtl'
|
|
8
|
+
|
|
9
|
+
/** Arguments for a provider that owns direction context and the host `dir`. */
|
|
10
|
+
export type DirectionProviderArgs = WithChildren<OmitArg<IntrinsicElements['div'], 'dir'> & {
|
|
11
|
+
/** Text direction. */
|
|
12
|
+
dir?: Direction
|
|
13
|
+
}>
|
|
14
|
+
|
|
15
|
+
/** Direction inherited by direction-aware components. */
|
|
16
|
+
export const DirectionContext = context<Direction>('ltr')
|
|
17
|
+
|
|
18
|
+
type DirectionRootArgs = WithChildren<{
|
|
19
|
+
dir: Direction
|
|
20
|
+
}>
|
|
21
|
+
|
|
22
|
+
const DirectionRoot: Stateful<DirectionRootArgs> = function* () {
|
|
23
|
+
for (const { children, dir } of this) {
|
|
24
|
+
DirectionContext(dir)
|
|
25
|
+
yield <>{children}</>
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Unstyled provider that sets direction context and an inherited HTML `dir` attribute. */
|
|
30
|
+
const DirectionProvider: Stateless<DirectionProviderArgs> = ({
|
|
31
|
+
children,
|
|
32
|
+
dir = 'ltr',
|
|
33
|
+
...attrs
|
|
34
|
+
}) => (
|
|
35
|
+
<DirectionRoot
|
|
36
|
+
{...rootAttrs(attrs)}
|
|
37
|
+
dir={dir}
|
|
38
|
+
attr:data-slot="direction-provider"
|
|
39
|
+
attr:dir={dir}
|
|
40
|
+
>
|
|
41
|
+
{children}
|
|
42
|
+
</DirectionRoot>
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
export { DirectionProvider }
|
package/src/drawer.tsx
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type { Stateful, Stateless } from 'ajo'
|
|
2
|
+
import { callRef, move, statefulRootAttrs as rootAttrs } from 'ajo-cloves'
|
|
3
|
+
import { context } from 'ajo/context'
|
|
4
|
+
import { clx, withSlot } from './utils'
|
|
5
|
+
import {
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogClose,
|
|
8
|
+
DialogContent,
|
|
9
|
+
DialogDescription,
|
|
10
|
+
DialogFooter,
|
|
11
|
+
DialogHeader,
|
|
12
|
+
DialogTitle,
|
|
13
|
+
DialogTrigger,
|
|
14
|
+
type DialogArgs,
|
|
15
|
+
type DialogCloseArgs,
|
|
16
|
+
type DialogContentArgs,
|
|
17
|
+
type DialogDescriptionArgs,
|
|
18
|
+
type DialogFooterArgs,
|
|
19
|
+
type DialogHeaderArgs,
|
|
20
|
+
type DialogTitleArgs,
|
|
21
|
+
type DialogTriggerArgs,
|
|
22
|
+
} from './dialog'
|
|
23
|
+
|
|
24
|
+
/** Viewport edge from which a Drawer panel enters. */
|
|
25
|
+
export type DrawerSide = 'top' | 'right' | 'bottom' | 'left'
|
|
26
|
+
|
|
27
|
+
/** Arguments for the Drawer state provider and entry edge. */
|
|
28
|
+
export type DrawerArgs = DialogArgs & {
|
|
29
|
+
/** Edge where the drawer enters from. */
|
|
30
|
+
side?: DrawerSide
|
|
31
|
+
}
|
|
32
|
+
/** Arguments for a button that opens its nearest Drawer. */
|
|
33
|
+
export type DrawerTriggerArgs = DialogTriggerArgs
|
|
34
|
+
/** Arguments for a button that closes its nearest Drawer. */
|
|
35
|
+
export type DrawerCloseArgs = DialogCloseArgs
|
|
36
|
+
/** Arguments for the Drawer panel, drag handle, and default close control. */
|
|
37
|
+
export type DrawerContentArgs = DialogContentArgs & {
|
|
38
|
+
closeClass?: string
|
|
39
|
+
/** Icon class for the default close button. */
|
|
40
|
+
closeIconClass?: string
|
|
41
|
+
/** Accessible label for the default close button. */
|
|
42
|
+
closeLabel?: string
|
|
43
|
+
handle?: boolean
|
|
44
|
+
handleClass?: string
|
|
45
|
+
/** Accessible label for the drag handle. */
|
|
46
|
+
handleLabel?: string
|
|
47
|
+
showCloseButton?: boolean
|
|
48
|
+
sideClass?: Partial<Record<DrawerSide, string>>
|
|
49
|
+
}
|
|
50
|
+
/** Arguments for the title and description area of a Drawer. */
|
|
51
|
+
export type DrawerHeaderArgs = DialogHeaderArgs
|
|
52
|
+
/** Arguments for the action area at the end of a Drawer. */
|
|
53
|
+
export type DrawerFooterArgs = DialogFooterArgs
|
|
54
|
+
/** Arguments for the heading that labels DrawerContent. */
|
|
55
|
+
export type DrawerTitleArgs = DialogTitleArgs
|
|
56
|
+
/** Arguments for the text that describes DrawerContent. */
|
|
57
|
+
export type DrawerDescriptionArgs = DialogDescriptionArgs
|
|
58
|
+
|
|
59
|
+
type DrawerContextValue = {
|
|
60
|
+
drag: ReturnType<typeof move>
|
|
61
|
+
side: DrawerSide
|
|
62
|
+
setPanel: (element: HTMLDialogElement | null) => void
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const DrawerContext = context<DrawerContextValue>({
|
|
66
|
+
drag: {
|
|
67
|
+
start: () => false,
|
|
68
|
+
get active() {
|
|
69
|
+
return false
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
side: 'right',
|
|
73
|
+
setPanel: () => {},
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const DrawerRoot: Stateful<DrawerArgs> = function* () {
|
|
77
|
+
let side: DrawerSide = 'right'
|
|
78
|
+
let offset = 0
|
|
79
|
+
let panel: HTMLDialogElement | null = null
|
|
80
|
+
|
|
81
|
+
const reset = () => {
|
|
82
|
+
if (!panel) return
|
|
83
|
+
panel.style.transform = ''
|
|
84
|
+
panel.style.transition = ''
|
|
85
|
+
panel.style.willChange = ''
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const movePanel = (next: number) => {
|
|
89
|
+
if (!panel) return
|
|
90
|
+
panel.style.transition = 'none'
|
|
91
|
+
panel.style.transform = transform(next, side)
|
|
92
|
+
panel.style.willChange = 'transform'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const drag = move(this, {
|
|
96
|
+
onStart: () => {
|
|
97
|
+
offset = 0
|
|
98
|
+
},
|
|
99
|
+
onMove: data => {
|
|
100
|
+
const delta = side === 'left' || side === 'right' ? data.dx : data.dy
|
|
101
|
+
offset = Math.max(0, side === 'left' || side === 'top' ? -delta : delta)
|
|
102
|
+
movePanel(offset)
|
|
103
|
+
},
|
|
104
|
+
onEnd: data => {
|
|
105
|
+
reset()
|
|
106
|
+
if (!data.canceled && offset > 72 && panel?.open) panel.close()
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
for (const args of this) {
|
|
111
|
+
side = args.side ?? 'right'
|
|
112
|
+
DrawerContext({
|
|
113
|
+
drag,
|
|
114
|
+
side,
|
|
115
|
+
setPanel: element => panel = element,
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
yield (
|
|
119
|
+
<Dialog
|
|
120
|
+
data-slot="drawer-dialog"
|
|
121
|
+
defaultOpen={args.defaultOpen}
|
|
122
|
+
modal={args.modal}
|
|
123
|
+
onOpenChange={args.onOpenChange}
|
|
124
|
+
open={args.open}
|
|
125
|
+
>
|
|
126
|
+
{args.children}
|
|
127
|
+
</Dialog>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
/** Unstyled edge panel provider built on native Dialog behavior. */
|
|
134
|
+
const Drawer: Stateless<DrawerArgs> = ({
|
|
135
|
+
children,
|
|
136
|
+
class: classes,
|
|
137
|
+
defaultOpen,
|
|
138
|
+
modal,
|
|
139
|
+
onOpenChange,
|
|
140
|
+
open,
|
|
141
|
+
side,
|
|
142
|
+
...attrs
|
|
143
|
+
}) => (
|
|
144
|
+
<DrawerRoot
|
|
145
|
+
{...rootAttrs(attrs)}
|
|
146
|
+
defaultOpen={defaultOpen}
|
|
147
|
+
modal={modal}
|
|
148
|
+
onOpenChange={onOpenChange}
|
|
149
|
+
open={open}
|
|
150
|
+
side={side}
|
|
151
|
+
attr:class={classes}
|
|
152
|
+
attr:data-slot="drawer"
|
|
153
|
+
>
|
|
154
|
+
{children}
|
|
155
|
+
</DrawerRoot>
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
/** Unstyled button that opens the nearest Drawer. */
|
|
159
|
+
const DrawerTrigger: Stateless<DrawerTriggerArgs> = withSlot<DrawerTriggerArgs>(DialogTrigger, 'drawer-trigger')
|
|
160
|
+
|
|
161
|
+
/** Unstyled button that closes the nearest Drawer. */
|
|
162
|
+
const DrawerClose: Stateless<DrawerCloseArgs> = withSlot<DrawerCloseArgs>(DialogClose, 'drawer-close')
|
|
163
|
+
|
|
164
|
+
const transform = (offset: number, side: DrawerSide) => {
|
|
165
|
+
if (side === 'left') return `translateX(${-offset}px)`
|
|
166
|
+
if (side === 'right') return `translateX(${offset}px)`
|
|
167
|
+
if (side === 'top') return `translateY(${-offset}px)`
|
|
168
|
+
return `translateY(${offset}px)`
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Unstyled native Drawer panel with optional drag and close controls. */
|
|
172
|
+
const DrawerContent: Stateless<DrawerContentArgs> = ({
|
|
173
|
+
children,
|
|
174
|
+
class: classes,
|
|
175
|
+
closeClass,
|
|
176
|
+
closeIconClass,
|
|
177
|
+
closeLabel = 'Close',
|
|
178
|
+
handle = false,
|
|
179
|
+
handleClass,
|
|
180
|
+
handleLabel = 'Drag to close',
|
|
181
|
+
ref,
|
|
182
|
+
showCloseButton = true,
|
|
183
|
+
sideClass,
|
|
184
|
+
...attrs
|
|
185
|
+
}) => {
|
|
186
|
+
const drawer = DrawerContext()
|
|
187
|
+
const { side } = drawer
|
|
188
|
+
let panel: HTMLDialogElement | null = null
|
|
189
|
+
|
|
190
|
+
const reference = (element: HTMLDialogElement | null) => {
|
|
191
|
+
panel = element
|
|
192
|
+
drawer.setPanel(element)
|
|
193
|
+
callRef(ref, element)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<DialogContent
|
|
198
|
+
{...attrs}
|
|
199
|
+
class={clx(sideClass?.[side], classes)}
|
|
200
|
+
data-side={side}
|
|
201
|
+
data-slot="drawer-content"
|
|
202
|
+
ref={reference}
|
|
203
|
+
>
|
|
204
|
+
{handle ? (
|
|
205
|
+
<div
|
|
206
|
+
aria-label={handleLabel}
|
|
207
|
+
class={handleClass}
|
|
208
|
+
data-slot="drawer-handle"
|
|
209
|
+
role="button"
|
|
210
|
+
tabIndex={0}
|
|
211
|
+
set:onkeydown={(event: KeyboardEvent) => {
|
|
212
|
+
if ((event.key === 'Enter' || event.key === ' ') && panel?.open) {
|
|
213
|
+
event.preventDefault()
|
|
214
|
+
panel.close()
|
|
215
|
+
}
|
|
216
|
+
}}
|
|
217
|
+
set:onpointerdown={(event: PointerEvent) => drawer.drag.start(event)}
|
|
218
|
+
/>
|
|
219
|
+
) : null}
|
|
220
|
+
{children}
|
|
221
|
+
{showCloseButton ? (
|
|
222
|
+
<DrawerClose aria-label={closeLabel} class={closeClass}>
|
|
223
|
+
<span aria-hidden="true" class={closeIconClass} data-slot="drawer-close-icon" />
|
|
224
|
+
</DrawerClose>
|
|
225
|
+
) : null}
|
|
226
|
+
</DialogContent>
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Unstyled header area for Drawer title and description. */
|
|
231
|
+
const DrawerHeader: Stateless<DrawerHeaderArgs> = withSlot<DrawerHeaderArgs>(DialogHeader, 'drawer-header')
|
|
232
|
+
|
|
233
|
+
/** Unstyled footer area for Drawer actions. */
|
|
234
|
+
const DrawerFooter: Stateless<DrawerFooterArgs> = withSlot<DrawerFooterArgs>(DialogFooter, 'drawer-footer')
|
|
235
|
+
|
|
236
|
+
/** Accessible heading for DrawerContent. */
|
|
237
|
+
const DrawerTitle: Stateless<DrawerTitleArgs> = withSlot<DrawerTitleArgs>(DialogTitle, 'drawer-title')
|
|
238
|
+
|
|
239
|
+
/** Accessible description for DrawerContent. */
|
|
240
|
+
const DrawerDescription: Stateless<DrawerDescriptionArgs> = withSlot<DrawerDescriptionArgs>(DialogDescription, 'drawer-description')
|
|
241
|
+
|
|
242
|
+
export {
|
|
243
|
+
Drawer,
|
|
244
|
+
DrawerClose,
|
|
245
|
+
DrawerContent,
|
|
246
|
+
DrawerDescription,
|
|
247
|
+
DrawerFooter,
|
|
248
|
+
DrawerHeader,
|
|
249
|
+
DrawerTitle,
|
|
250
|
+
DrawerTrigger,
|
|
251
|
+
}
|