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/slider.tsx
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import type { IntrinsicElements, Stateful, Stateless } from 'ajo'
|
|
2
|
+
import { callHandler, clamp, controlled, listen, move } from 'ajo-cloves'
|
|
3
|
+
import { FieldContext } from './field'
|
|
4
|
+
import type { FixedArgs, OmitArg } from './utils'
|
|
5
|
+
import { clx, flag, stlx, toNumber } from './utils'
|
|
6
|
+
|
|
7
|
+
/** Axis along which slider values increase. */
|
|
8
|
+
export type SliderOrientation = 'horizontal' | 'vertical'
|
|
9
|
+
|
|
10
|
+
/** Props for a single- or multi-thumb range slider. */
|
|
11
|
+
export type SliderArgs = OmitArg<IntrinsicElements['input'], 'children' | 'defaultValue' | 'type' | 'value'> & {
|
|
12
|
+
/** Controlled slider values. Use one value for a single thumb, multiple values for ranges. */
|
|
13
|
+
value?: number[]
|
|
14
|
+
/** Initial values for uncontrolled usage. */
|
|
15
|
+
defaultValue?: number[]
|
|
16
|
+
/** Lowest allowed value. */
|
|
17
|
+
min?: number
|
|
18
|
+
/** Highest allowed value. */
|
|
19
|
+
max?: number
|
|
20
|
+
/** Value granularity. */
|
|
21
|
+
step?: number | 'any'
|
|
22
|
+
/** Minimum distance, in steps, allowed between adjacent thumbs. */
|
|
23
|
+
minStepsBetweenThumbs?: number
|
|
24
|
+
/** Layout orientation. */
|
|
25
|
+
orientation?: SliderOrientation
|
|
26
|
+
/** Reverse the value direction. */
|
|
27
|
+
inverted?: boolean
|
|
28
|
+
/** Called whenever the value changes. */
|
|
29
|
+
onValueChange?: (value: number[], event: Event) => void
|
|
30
|
+
/** Called when an input change is committed. */
|
|
31
|
+
onValueCommit?: (value: number[], event: Event) => void
|
|
32
|
+
/** Classes for the native range inputs. */
|
|
33
|
+
inputClass?: string
|
|
34
|
+
/** Classes for the range fill element. */
|
|
35
|
+
rangeClass?: string
|
|
36
|
+
/** Classes for the thumb elements. */
|
|
37
|
+
thumbClass?: string
|
|
38
|
+
/** Classes for the track element. */
|
|
39
|
+
trackClass?: string
|
|
40
|
+
/** Extra input classes applied only in vertical orientation. */
|
|
41
|
+
verticalInputClass?: string
|
|
42
|
+
} & FixedArgs<'children' | 'type'>
|
|
43
|
+
|
|
44
|
+
type SliderRootArgs = OmitArg<SliderArgs, 'class' | 'inputClass' | 'rangeClass' | 'thumbClass' | 'trackClass' | 'verticalInputClass'> & {
|
|
45
|
+
inputAttrs: Record<string, unknown>
|
|
46
|
+
inputClass?: string
|
|
47
|
+
inputOnChange?: unknown
|
|
48
|
+
inputOnInput?: unknown
|
|
49
|
+
rangeClass?: string
|
|
50
|
+
thumbClass?: string
|
|
51
|
+
trackClass?: string
|
|
52
|
+
verticalInputClass?: string
|
|
53
|
+
} & FixedArgs<'class'>
|
|
54
|
+
|
|
55
|
+
type ValueCallback = (value: number[], event: Event) => void
|
|
56
|
+
|
|
57
|
+
type Runtime = {
|
|
58
|
+
disabled: boolean
|
|
59
|
+
inputs: Array<HTMLInputElement | null>
|
|
60
|
+
inverted: boolean
|
|
61
|
+
max: number
|
|
62
|
+
min: number
|
|
63
|
+
minStepsBetweenThumbs: number
|
|
64
|
+
onValueChange?: ValueCallback
|
|
65
|
+
onValueCommit?: ValueCallback
|
|
66
|
+
orientation: SliderOrientation
|
|
67
|
+
step?: number | 'any'
|
|
68
|
+
values: number[]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const sliderStep = (value: unknown): number | 'any' | undefined =>
|
|
72
|
+
value === 'any' ? 'any' : toNumber(value ?? 1, 1)
|
|
73
|
+
|
|
74
|
+
const sliderOrientation = (value: unknown): SliderOrientation =>
|
|
75
|
+
value === 'vertical' ? 'vertical' : 'horizontal'
|
|
76
|
+
|
|
77
|
+
const valueCallback = (value: unknown): ValueCallback | undefined =>
|
|
78
|
+
typeof value === 'function' ? value as ValueCallback : undefined
|
|
79
|
+
|
|
80
|
+
const percent = (value: number, min: number, max: number, inverted: boolean) => {
|
|
81
|
+
if (max <= min) return 0
|
|
82
|
+
const raw = clamp(((value - min) / (max - min)) * 100, 0, 100)
|
|
83
|
+
return inverted ? 100 - raw : raw
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const precision = (value: number) => {
|
|
87
|
+
const match = String(value).match(/\.(\d+)/)
|
|
88
|
+
return match ? match[1].length : 0
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const snap = (value: number, min: number, step: number | 'any' | undefined) => {
|
|
92
|
+
if (step === 'any' || step == null) return value
|
|
93
|
+
const amount = Number(step)
|
|
94
|
+
if (!Number.isFinite(amount) || amount <= 0) return value
|
|
95
|
+
const places = Math.max(precision(min), precision(amount))
|
|
96
|
+
const next = min + Math.round((value - min) / amount) * amount
|
|
97
|
+
return Number(next.toFixed(places + 2))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const valuesFrom = (
|
|
101
|
+
value: unknown,
|
|
102
|
+
min: number,
|
|
103
|
+
max: number,
|
|
104
|
+
step: number | 'any' | undefined,
|
|
105
|
+
minStepsBetweenThumbs = 0,
|
|
106
|
+
) => {
|
|
107
|
+
const raw = Array.isArray(value) && value.length
|
|
108
|
+
? value.map(item => toNumber(item, min))
|
|
109
|
+
: [min]
|
|
110
|
+
const gapStep = step === 'any' ? 0 : toNumber(step ?? 1, 1)
|
|
111
|
+
const gap = Math.max(0, minStepsBetweenThumbs) * Math.max(0, gapStep)
|
|
112
|
+
const next = raw
|
|
113
|
+
.map(item => snap(clamp(item, min, max), min, step))
|
|
114
|
+
.sort((a, b) => a - b)
|
|
115
|
+
|
|
116
|
+
for (let index = 0; index < next.length; index++) {
|
|
117
|
+
const lower = index > 0 ? next[index - 1] + gap : min
|
|
118
|
+
next[index] = clamp(next[index], lower, max)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (let index = next.length - 1; index >= 0; index--) {
|
|
122
|
+
const upper = index < next.length - 1 ? next[index + 1] - gap : max
|
|
123
|
+
next[index] = clamp(next[index], min, upper)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return next
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const sameValues = (first: number[], second: number[]) =>
|
|
130
|
+
first.length === second.length && first.every((value, index) => value === second[index])
|
|
131
|
+
|
|
132
|
+
const replaceValue = (runtime: Runtime, index: number, value: number) => {
|
|
133
|
+
const next = [...runtime.values]
|
|
134
|
+
const gapStep = runtime.step === 'any' ? 0 : toNumber(runtime.step ?? 1, 1)
|
|
135
|
+
const gap = Math.max(0, runtime.minStepsBetweenThumbs) * Math.max(0, gapStep)
|
|
136
|
+
const lower = index > 0 ? next[index - 1] + gap : runtime.min
|
|
137
|
+
const upper = index < next.length - 1 ? next[index + 1] - gap : runtime.max
|
|
138
|
+
next[index] = snap(clamp(value, lower, upper), runtime.min, runtime.step)
|
|
139
|
+
return next
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const closestThumb = (runtime: Runtime, value: number) => {
|
|
143
|
+
let nearest = 0
|
|
144
|
+
let distance = Number.POSITIVE_INFINITY
|
|
145
|
+
|
|
146
|
+
for (let index = 0; index < runtime.values.length; index++) {
|
|
147
|
+
const next = Math.abs(runtime.values[index] - value)
|
|
148
|
+
if (next < distance) {
|
|
149
|
+
nearest = index
|
|
150
|
+
distance = next
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return nearest
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const pointerValue = (element: HTMLElement, runtime: Runtime, event: PointerEvent) => {
|
|
158
|
+
const rect = element.getBoundingClientRect()
|
|
159
|
+
const distance = runtime.orientation === 'vertical'
|
|
160
|
+
? rect.bottom - event.clientY
|
|
161
|
+
: event.clientX - rect.left
|
|
162
|
+
const size = runtime.orientation === 'vertical' ? rect.height : rect.width
|
|
163
|
+
const raw = size > 0 ? clamp(distance / size, 0, 1) : 0
|
|
164
|
+
const ratio = runtime.inverted ? 1 - raw : raw
|
|
165
|
+
return runtime.min + ratio * (runtime.max - runtime.min)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const rangeStyle = (orientation: SliderOrientation, start: number, end: number) =>
|
|
169
|
+
orientation === 'vertical'
|
|
170
|
+
? stlx({ bottom: `${start}%`, height: `${Math.max(0, end - start)}%` })
|
|
171
|
+
: stlx({ left: `${start}%`, width: `${Math.max(0, end - start)}%` })
|
|
172
|
+
|
|
173
|
+
const thumbStyle = (orientation: SliderOrientation, position: number) =>
|
|
174
|
+
orientation === 'vertical'
|
|
175
|
+
? stlx({ bottom: `${position}%`, left: '50%', transform: 'translate(-50%, 50%)' })
|
|
176
|
+
: stlx({ left: `${position}%`, top: '50%', transform: 'translate(-50%, -50%)' })
|
|
177
|
+
|
|
178
|
+
const inputId = (id: unknown, index: number, total: number) => {
|
|
179
|
+
if (id == null) return undefined
|
|
180
|
+
const value = String(id)
|
|
181
|
+
return total === 1 ? value : `${value}-${index + 1}`
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const inputLabel = (label: unknown, index: number, total: number) => {
|
|
185
|
+
if (label == null) return total === 1 ? 'Slider' : `Slider thumb ${index + 1}`
|
|
186
|
+
const value = String(label)
|
|
187
|
+
return total === 1 ? value : `${value} ${index + 1}`
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const SliderRoot: Stateful<SliderRootArgs, 'span'> = function* ({ defaultValue, max = 100, min = 0, step = 1, value }) {
|
|
191
|
+
const initialMin = toNumber(min, 0)
|
|
192
|
+
const initialMax = toNumber(max, 100)
|
|
193
|
+
const initialStep = sliderStep(step)
|
|
194
|
+
const initial = valuesFrom(value ?? defaultValue, initialMin, initialMax, initialStep, 0)
|
|
195
|
+
let runtime: Runtime = {
|
|
196
|
+
disabled: false,
|
|
197
|
+
inputs: [],
|
|
198
|
+
inverted: false,
|
|
199
|
+
max: initialMax,
|
|
200
|
+
min: initialMin,
|
|
201
|
+
minStepsBetweenThumbs: 0,
|
|
202
|
+
orientation: 'horizontal',
|
|
203
|
+
step: initialStep,
|
|
204
|
+
values: initial,
|
|
205
|
+
}
|
|
206
|
+
const state = controlled<number[]>(this, {
|
|
207
|
+
fallback: initial,
|
|
208
|
+
onChange: (next, event) => runtime.onValueChange?.(next, event as Event),
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
const commit = (event: Event) => runtime.onValueCommit?.(runtime.values, event)
|
|
212
|
+
|
|
213
|
+
const update = (index: number, raw: number, event: Event) => {
|
|
214
|
+
const next = replaceValue(runtime, index, raw)
|
|
215
|
+
if (sameValues(next, runtime.values)) return
|
|
216
|
+
|
|
217
|
+
runtime.values = next
|
|
218
|
+
state.set(next, event)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let dragIndex = 0
|
|
222
|
+
const drag = move(this, {
|
|
223
|
+
onMove: (_data, event) => update(dragIndex, pointerValue(this, runtime, event), event),
|
|
224
|
+
onEnd: (_data, event) => commit(event as Event),
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
listen(this, 'pointerdown', (event: PointerEvent) => {
|
|
228
|
+
if (runtime.disabled || event.button !== 0) return
|
|
229
|
+
|
|
230
|
+
const raw = pointerValue(this, runtime, event)
|
|
231
|
+
const index = closestThumb(runtime, raw)
|
|
232
|
+
dragIndex = index
|
|
233
|
+
runtime.inputs[index]?.focus()
|
|
234
|
+
update(index, raw, event)
|
|
235
|
+
event.preventDefault()
|
|
236
|
+
drag.start(event)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
for (const {
|
|
240
|
+
disabled,
|
|
241
|
+
inputAttrs,
|
|
242
|
+
inputClass,
|
|
243
|
+
inputOnChange,
|
|
244
|
+
inputOnInput,
|
|
245
|
+
inverted,
|
|
246
|
+
max = 100,
|
|
247
|
+
min = 0,
|
|
248
|
+
minStepsBetweenThumbs = 0,
|
|
249
|
+
onValueChange,
|
|
250
|
+
onValueCommit,
|
|
251
|
+
orientation = 'horizontal',
|
|
252
|
+
rangeClass,
|
|
253
|
+
step = 1,
|
|
254
|
+
thumbClass,
|
|
255
|
+
trackClass,
|
|
256
|
+
value,
|
|
257
|
+
verticalInputClass,
|
|
258
|
+
} of this) {
|
|
259
|
+
const minValue = toNumber(min, 0)
|
|
260
|
+
const maxValue = toNumber(max, 100)
|
|
261
|
+
const stepValue = sliderStep(step)
|
|
262
|
+
const gapValue = toNumber(minStepsBetweenThumbs, 0)
|
|
263
|
+
const orientationValue = sliderOrientation(orientation)
|
|
264
|
+
const raw = state.sync(value != null ? value as number[] : undefined)
|
|
265
|
+
const values = valuesFrom(raw, minValue, maxValue, stepValue, gapValue)
|
|
266
|
+
if (!state.controlled && !sameValues(values, raw)) state.init(values)
|
|
267
|
+
|
|
268
|
+
runtime = {
|
|
269
|
+
disabled: Boolean(disabled),
|
|
270
|
+
inputs: runtime.inputs,
|
|
271
|
+
inverted: Boolean(inverted),
|
|
272
|
+
max: maxValue,
|
|
273
|
+
min: minValue,
|
|
274
|
+
minStepsBetweenThumbs: gapValue,
|
|
275
|
+
onValueChange: valueCallback(onValueChange),
|
|
276
|
+
onValueCommit: valueCallback(onValueCommit),
|
|
277
|
+
orientation: orientationValue,
|
|
278
|
+
step: stepValue,
|
|
279
|
+
values,
|
|
280
|
+
}
|
|
281
|
+
runtime.inputs.length = values.length
|
|
282
|
+
|
|
283
|
+
const positions = values.map(item => percent(item, minValue, maxValue, runtime.inverted))
|
|
284
|
+
const start = values.length > 1 ? Math.min(...positions) : 0
|
|
285
|
+
const end = values.length > 1 ? Math.max(...positions) : positions[0] ?? 0
|
|
286
|
+
const disabledFlag = disabled ? true : undefined
|
|
287
|
+
|
|
288
|
+
yield (
|
|
289
|
+
<>
|
|
290
|
+
<span
|
|
291
|
+
class={trackClass}
|
|
292
|
+
data-orientation={orientationValue}
|
|
293
|
+
data-slot="slider-track"
|
|
294
|
+
>
|
|
295
|
+
<span
|
|
296
|
+
class={rangeClass}
|
|
297
|
+
data-orientation={orientationValue}
|
|
298
|
+
data-slot="slider-range"
|
|
299
|
+
style={rangeStyle(orientationValue, start, end)}
|
|
300
|
+
/>
|
|
301
|
+
</span>
|
|
302
|
+
{values.map((item, index) => {
|
|
303
|
+
const handleInput = (event: Event) => {
|
|
304
|
+
callHandler(inputOnInput, event)
|
|
305
|
+
update(index, (event.currentTarget as HTMLInputElement).valueAsNumber, event)
|
|
306
|
+
}
|
|
307
|
+
const handleChange = (event: Event) => {
|
|
308
|
+
callHandler(inputOnChange, event)
|
|
309
|
+
commit(event)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<input
|
|
314
|
+
key={`slider-input-${index}`}
|
|
315
|
+
{...inputAttrs}
|
|
316
|
+
aria-label={inputLabel(inputAttrs['aria-label'], index, values.length)}
|
|
317
|
+
aria-orientation={orientationValue}
|
|
318
|
+
class={clx(inputClass, orientationValue === 'vertical' && verticalInputClass)}
|
|
319
|
+
data-orientation={orientationValue}
|
|
320
|
+
data-slot="slider-input"
|
|
321
|
+
disabled={disabledFlag}
|
|
322
|
+
id={inputId(inputAttrs.id, index, values.length)}
|
|
323
|
+
max={maxValue}
|
|
324
|
+
min={minValue}
|
|
325
|
+
ref={element => runtime.inputs[index] = element}
|
|
326
|
+
set:onchange={handleChange}
|
|
327
|
+
set:oninput={handleInput}
|
|
328
|
+
set:value={String(item)}
|
|
329
|
+
step={stepValue}
|
|
330
|
+
type="range"
|
|
331
|
+
value={item}
|
|
332
|
+
/>
|
|
333
|
+
)
|
|
334
|
+
})}
|
|
335
|
+
{positions.map((item, index) => (
|
|
336
|
+
<span
|
|
337
|
+
aria-hidden="true"
|
|
338
|
+
class={thumbClass}
|
|
339
|
+
data-index={index}
|
|
340
|
+
data-orientation={orientationValue}
|
|
341
|
+
data-slot="slider-thumb"
|
|
342
|
+
key={`slider-thumb-${index}`}
|
|
343
|
+
style={thumbStyle(orientationValue, item)}
|
|
344
|
+
/>
|
|
345
|
+
))}
|
|
346
|
+
</>
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
SliderRoot.is = 'span'
|
|
352
|
+
|
|
353
|
+
/** Unstyled range slider with native range inputs and pointer behavior. */
|
|
354
|
+
const Slider: Stateless<SliderArgs> = ({
|
|
355
|
+
class: classes,
|
|
356
|
+
defaultValue,
|
|
357
|
+
disabled,
|
|
358
|
+
inputClass,
|
|
359
|
+
inverted,
|
|
360
|
+
max = 100,
|
|
361
|
+
min = 0,
|
|
362
|
+
minStepsBetweenThumbs,
|
|
363
|
+
onValueChange,
|
|
364
|
+
onValueCommit,
|
|
365
|
+
orientation = 'horizontal',
|
|
366
|
+
rangeClass,
|
|
367
|
+
'set:onchange': inputOnChange,
|
|
368
|
+
'set:oninput': inputOnInput,
|
|
369
|
+
step = 1,
|
|
370
|
+
thumbClass,
|
|
371
|
+
trackClass,
|
|
372
|
+
type: _type,
|
|
373
|
+
value,
|
|
374
|
+
verticalInputClass,
|
|
375
|
+
...inputAttrs
|
|
376
|
+
}) => {
|
|
377
|
+
const disabledFlag = disabled ? true : undefined
|
|
378
|
+
const preview = valuesFrom(
|
|
379
|
+
value ?? defaultValue,
|
|
380
|
+
toNumber(min, 0),
|
|
381
|
+
toNumber(max, 100),
|
|
382
|
+
sliderStep(step),
|
|
383
|
+
toNumber(minStepsBetweenThumbs, 0),
|
|
384
|
+
)
|
|
385
|
+
const field = FieldContext()
|
|
386
|
+
const groupAttrs = field && preview.length > 1 ? field.groupAttrs : undefined
|
|
387
|
+
const controlAttrs = field && preview.length === 1 ? field.controlAttrs : undefined
|
|
388
|
+
const effectiveInputAttrs = controlAttrs ? { ...controlAttrs, ...inputAttrs } : inputAttrs
|
|
389
|
+
|
|
390
|
+
return (
|
|
391
|
+
<SliderRoot
|
|
392
|
+
defaultValue={defaultValue}
|
|
393
|
+
disabled={disabledFlag}
|
|
394
|
+
inputAttrs={effectiveInputAttrs}
|
|
395
|
+
inputClass={inputClass}
|
|
396
|
+
inputOnChange={inputOnChange}
|
|
397
|
+
inputOnInput={inputOnInput}
|
|
398
|
+
inverted={inverted}
|
|
399
|
+
max={max}
|
|
400
|
+
min={min}
|
|
401
|
+
minStepsBetweenThumbs={minStepsBetweenThumbs}
|
|
402
|
+
onValueChange={onValueChange}
|
|
403
|
+
onValueCommit={onValueCommit}
|
|
404
|
+
orientation={orientation}
|
|
405
|
+
rangeClass={rangeClass}
|
|
406
|
+
step={step}
|
|
407
|
+
thumbClass={thumbClass}
|
|
408
|
+
trackClass={trackClass}
|
|
409
|
+
value={value}
|
|
410
|
+
verticalInputClass={verticalInputClass}
|
|
411
|
+
attr:aria-describedby={groupAttrs?.['aria-describedby']}
|
|
412
|
+
attr:aria-labelledby={groupAttrs?.['aria-labelledby']}
|
|
413
|
+
attr:aria-disabled={flag(disabled)}
|
|
414
|
+
attr:class={classes}
|
|
415
|
+
attr:data-disabled={flag(disabled)}
|
|
416
|
+
attr:data-inverted={flag(inverted)}
|
|
417
|
+
attr:data-orientation={orientation}
|
|
418
|
+
attr:data-slot="slider"
|
|
419
|
+
attr:role={groupAttrs ? 'group' : undefined}
|
|
420
|
+
/>
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export { Slider }
|
package/src/switch.tsx
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { IntrinsicElements, Stateful, Stateless } from 'ajo'
|
|
2
|
+
import { callHandler, controlled } from 'ajo-cloves'
|
|
3
|
+
|
|
4
|
+
/** Props for a checkbox-backed binary switch and its visual thumb. */
|
|
5
|
+
export type SwitchArgs = IntrinsicElements['input'] & {
|
|
6
|
+
/** Controlled checked state. */
|
|
7
|
+
checked?: boolean
|
|
8
|
+
/** Initial checked state for uncontrolled usage. */
|
|
9
|
+
defaultChecked?: boolean
|
|
10
|
+
/** Called when the native checkbox state changes. */
|
|
11
|
+
onCheckedChange?: (checked: boolean, event: Event) => void
|
|
12
|
+
/** Classes for the hidden input element. */
|
|
13
|
+
inputClass?: string
|
|
14
|
+
/** Size marker mirrored as `data-size`. */
|
|
15
|
+
size?: string
|
|
16
|
+
/** Classes for the thumb element. */
|
|
17
|
+
thumbClass?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type SwitchRootArgs = {
|
|
21
|
+
checked?: boolean
|
|
22
|
+
defaultChecked?: boolean
|
|
23
|
+
inputAttrs: Record<string, unknown>
|
|
24
|
+
inputClass?: string
|
|
25
|
+
onCheckedChange?: SwitchArgs['onCheckedChange']
|
|
26
|
+
role: unknown
|
|
27
|
+
setChecked?: unknown
|
|
28
|
+
setOnChange?: unknown
|
|
29
|
+
size?: string
|
|
30
|
+
thumbClass?: string
|
|
31
|
+
value: unknown
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const SwitchRoot: Stateful<SwitchRootArgs, 'span'> = function* ({ checked, defaultChecked, setChecked }) {
|
|
35
|
+
let onCheckedChange: SwitchArgs['onCheckedChange']
|
|
36
|
+
const state = controlled<boolean>(this, {
|
|
37
|
+
fallback: Boolean(checked ?? defaultChecked ?? setChecked),
|
|
38
|
+
onChange: (next, event) => onCheckedChange?.(next, event as Event),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
for (const args of this) {
|
|
42
|
+
const initialChecked = args.checked ?? args.defaultChecked
|
|
43
|
+
onCheckedChange = args.onCheckedChange
|
|
44
|
+
state.sync(args.checked != null ? Boolean(args.checked) : undefined)
|
|
45
|
+
|
|
46
|
+
yield (
|
|
47
|
+
<>
|
|
48
|
+
<input
|
|
49
|
+
{...args.inputAttrs}
|
|
50
|
+
checked={initialChecked}
|
|
51
|
+
class={args.inputClass}
|
|
52
|
+
data-slot="switch-input"
|
|
53
|
+
role={args.role as string | undefined}
|
|
54
|
+
set:checked={state.value}
|
|
55
|
+
set:onchange={(event: Event) => {
|
|
56
|
+
callHandler(args.setOnChange, event)
|
|
57
|
+
state.set((event.currentTarget as HTMLInputElement).checked, event)
|
|
58
|
+
}}
|
|
59
|
+
type="checkbox"
|
|
60
|
+
value={args.value}
|
|
61
|
+
/>
|
|
62
|
+
<span aria-hidden="true" class={args.thumbClass} data-slot="switch-thumb" />
|
|
63
|
+
</>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
SwitchRoot.is = 'span'
|
|
69
|
+
|
|
70
|
+
/** Unstyled native switch control with form behavior. */
|
|
71
|
+
const Switch: Stateless<SwitchArgs> = ({
|
|
72
|
+
checked,
|
|
73
|
+
class: classes,
|
|
74
|
+
defaultChecked,
|
|
75
|
+
inputClass,
|
|
76
|
+
onCheckedChange,
|
|
77
|
+
role = 'switch',
|
|
78
|
+
size,
|
|
79
|
+
thumbClass,
|
|
80
|
+
'set:checked': setChecked,
|
|
81
|
+
'set:onchange': setOnChange,
|
|
82
|
+
type: _type,
|
|
83
|
+
value = 'on',
|
|
84
|
+
...attrs
|
|
85
|
+
}) => (
|
|
86
|
+
<SwitchRoot
|
|
87
|
+
checked={checked}
|
|
88
|
+
defaultChecked={defaultChecked}
|
|
89
|
+
inputAttrs={attrs}
|
|
90
|
+
inputClass={inputClass}
|
|
91
|
+
onCheckedChange={onCheckedChange}
|
|
92
|
+
role={role}
|
|
93
|
+
setChecked={setChecked}
|
|
94
|
+
setOnChange={setOnChange}
|
|
95
|
+
size={size}
|
|
96
|
+
thumbClass={thumbClass}
|
|
97
|
+
value={value}
|
|
98
|
+
attr:class={classes}
|
|
99
|
+
attr:data-size={size}
|
|
100
|
+
attr:data-slot="switch"
|
|
101
|
+
/>
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
export { Switch }
|