@djangocfg/ui-core 2.1.412 → 2.1.413
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/package.json +4 -4
- package/src/components/data/avatar-group/index.tsx +224 -0
- package/src/components/data/badge-overflow/index.tsx +259 -0
- package/src/components/data/circular-progress/index.tsx +358 -0
- package/src/components/data/relative-time-card/index.tsx +191 -0
- package/src/components/data/stat/index.tsx +140 -0
- package/src/components/data/status/index.tsx +80 -0
- package/src/components/effects/GlowBackground.tsx +9 -1
- package/src/components/effects/swap/index.tsx +289 -0
- package/src/components/feedback/banner/index.tsx +693 -0
- package/src/components/forms/checkbox-group/index.tsx +243 -0
- package/src/components/forms/editable/index.tsx +420 -0
- package/src/components/forms/input-otp/index.tsx +12 -3
- package/src/components/forms/mask-input/index.tsx +466 -0
- package/src/components/forms/otp/index.tsx +12 -8
- package/src/components/forms/segmented-input/index.tsx +319 -0
- package/src/components/forms/tags-input/index.tsx +896 -0
- package/src/components/forms/time-picker/index.tsx +285 -0
- package/src/components/index.ts +51 -0
- package/src/components/layout/key-value/index.tsx +884 -0
- package/src/components/layout/stack/index.tsx +349 -0
- package/src/components/navigation/context-menu/index.tsx +9 -6
- package/src/components/navigation/stepper/index.tsx +1307 -0
- package/src/components/select/multi-select-pro-async.tsx +11 -2
- package/src/components/select/multi-select-pro.tsx +11 -2
- package/src/components/specialized/presence/index.tsx +181 -0
- package/src/components/specialized/primitive/index.tsx +83 -0
- package/src/components/specialized/visually-hidden/index.tsx +19 -0
- package/src/components/specialized/visually-hidden-input/index.tsx +99 -0
- package/src/hooks/dom/index.ts +4 -0
- package/src/hooks/dom/useFormReset.ts +49 -0
- package/src/hooks/dom/useLayoutEffect.ts +16 -0
- package/src/hooks/dom/useSize.ts +57 -0
- package/src/hooks/state/index.ts +4 -0
- package/src/hooks/state/useCallbackRef.ts +25 -0
- package/src/hooks/state/usePrevious.ts +20 -0
- package/src/hooks/state/useStateMachine.ts +29 -0
- package/src/lib/compose-event-handlers.ts +22 -0
- package/src/lib/compose-refs.ts +65 -0
- package/src/lib/create-context.tsx +62 -0
- package/src/lib/get-element-ref.ts +33 -0
- package/src/lib/index.ts +5 -0
- package/src/lib/styles.ts +103 -0
- package/src/styles/README.md +43 -0
- package/src/styles/palette/utils.ts +15 -5
- package/src/styles/utilities/animations.css +135 -0
- package/src/styles/utilities/display.css +62 -0
- package/src/styles/utilities/glass.css +57 -0
- package/src/styles/utilities/marquee.css +69 -0
- package/src/styles/utilities/step.css +25 -0
- package/src/styles/utilities.css +6 -259
|
@@ -0,0 +1,1307 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Check } from "lucide-react"
|
|
4
|
+
import * as React from "react"
|
|
5
|
+
import { Slot } from "@radix-ui/react-slot"
|
|
6
|
+
|
|
7
|
+
import { cn } from "../../../lib/utils"
|
|
8
|
+
|
|
9
|
+
const ROOT_NAME = "Stepper"
|
|
10
|
+
const LIST_NAME = "StepperList"
|
|
11
|
+
const ITEM_NAME = "StepperItem"
|
|
12
|
+
const TRIGGER_NAME = "StepperTrigger"
|
|
13
|
+
const INDICATOR_NAME = "StepperIndicator"
|
|
14
|
+
const SEPARATOR_NAME = "StepperSeparator"
|
|
15
|
+
const TITLE_NAME = "StepperTitle"
|
|
16
|
+
const DESCRIPTION_NAME = "StepperDescription"
|
|
17
|
+
const CONTENT_NAME = "StepperContent"
|
|
18
|
+
const PREV_NAME = "StepperPrev"
|
|
19
|
+
const NEXT_NAME = "StepperNext"
|
|
20
|
+
|
|
21
|
+
const ENTRY_FOCUS = "stepperFocusGroup.onEntryFocus"
|
|
22
|
+
const EVENT_OPTIONS = { bubbles: false, cancelable: true }
|
|
23
|
+
const ARROW_KEYS = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"]
|
|
24
|
+
|
|
25
|
+
type Direction = "ltr" | "rtl"
|
|
26
|
+
type Orientation = "horizontal" | "vertical"
|
|
27
|
+
type NavigationDirection = "next" | "prev"
|
|
28
|
+
type ActivationMode = "automatic" | "manual"
|
|
29
|
+
type DataState = "inactive" | "active" | "completed"
|
|
30
|
+
|
|
31
|
+
interface DivProps extends React.ComponentProps<"div"> {
|
|
32
|
+
asChild?: boolean
|
|
33
|
+
}
|
|
34
|
+
interface ButtonProps extends React.ComponentProps<"button"> {
|
|
35
|
+
asChild?: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type ListElement = HTMLDivElement
|
|
39
|
+
type TriggerElement = HTMLButtonElement
|
|
40
|
+
|
|
41
|
+
function getId(
|
|
42
|
+
id: string,
|
|
43
|
+
variant: "trigger" | "content" | "title" | "description",
|
|
44
|
+
value: string,
|
|
45
|
+
) {
|
|
46
|
+
return `${id}-${variant}-${value}`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type FocusIntent = "first" | "last" | "prev" | "next"
|
|
50
|
+
|
|
51
|
+
const MAP_KEY_TO_FOCUS_INTENT: Record<string, FocusIntent> = {
|
|
52
|
+
ArrowLeft: "prev",
|
|
53
|
+
ArrowUp: "prev",
|
|
54
|
+
ArrowRight: "next",
|
|
55
|
+
ArrowDown: "next",
|
|
56
|
+
PageUp: "first",
|
|
57
|
+
Home: "first",
|
|
58
|
+
PageDown: "last",
|
|
59
|
+
End: "last",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getDirectionAwareKey(key: string, dir?: Direction) {
|
|
63
|
+
if (dir !== "rtl") return key
|
|
64
|
+
return key === "ArrowLeft"
|
|
65
|
+
? "ArrowRight"
|
|
66
|
+
: key === "ArrowRight"
|
|
67
|
+
? "ArrowLeft"
|
|
68
|
+
: key
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getFocusIntent(
|
|
72
|
+
event: React.KeyboardEvent<TriggerElement>,
|
|
73
|
+
dir?: Direction,
|
|
74
|
+
orientation?: Orientation,
|
|
75
|
+
) {
|
|
76
|
+
const key = getDirectionAwareKey(event.key, dir)
|
|
77
|
+
if (orientation === "horizontal" && ["ArrowUp", "ArrowDown"].includes(key))
|
|
78
|
+
return undefined
|
|
79
|
+
if (orientation === "vertical" && ["ArrowLeft", "ArrowRight"].includes(key))
|
|
80
|
+
return undefined
|
|
81
|
+
return MAP_KEY_TO_FOCUS_INTENT[key]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function focusFirst(
|
|
85
|
+
candidates: React.RefObject<TriggerElement | null>[],
|
|
86
|
+
preventScroll = false,
|
|
87
|
+
) {
|
|
88
|
+
const PREVIOUSLY_FOCUSED_ELEMENT = document.activeElement
|
|
89
|
+
for (const candidateRef of candidates) {
|
|
90
|
+
const candidate = candidateRef.current
|
|
91
|
+
if (!candidate) continue
|
|
92
|
+
if (candidate === PREVIOUSLY_FOCUSED_ELEMENT) return
|
|
93
|
+
candidate.focus({ preventScroll })
|
|
94
|
+
if (document.activeElement !== PREVIOUSLY_FOCUSED_ELEMENT) return
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function wrapArray<T>(array: T[], startIndex: number) {
|
|
99
|
+
return array.map<T>(
|
|
100
|
+
(_, index) => array[(startIndex + index) % array.length] as T,
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getDataState(
|
|
105
|
+
value: string | undefined,
|
|
106
|
+
itemValue: string,
|
|
107
|
+
stepState: StepState | undefined,
|
|
108
|
+
steps: Map<string, StepState>,
|
|
109
|
+
variant: "item" | "separator" = "item",
|
|
110
|
+
): DataState {
|
|
111
|
+
const stepKeys = Array.from(steps.keys())
|
|
112
|
+
const currentIndex = stepKeys.indexOf(itemValue)
|
|
113
|
+
|
|
114
|
+
if (stepState?.completed) return "completed"
|
|
115
|
+
|
|
116
|
+
if (value === itemValue) {
|
|
117
|
+
return variant === "separator" ? "inactive" : "active"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (value) {
|
|
121
|
+
const activeIndex = stepKeys.indexOf(value)
|
|
122
|
+
|
|
123
|
+
if (activeIndex > currentIndex) return "completed"
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return "inactive"
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
interface StepState {
|
|
130
|
+
value: string
|
|
131
|
+
completed: boolean
|
|
132
|
+
disabled: boolean
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface StoreState {
|
|
136
|
+
steps: Map<string, StepState>
|
|
137
|
+
value: string
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface Store {
|
|
141
|
+
subscribe: (callback: () => void) => () => void
|
|
142
|
+
getState: () => StoreState
|
|
143
|
+
setState: <K extends keyof StoreState>(key: K, value: StoreState[K]) => void
|
|
144
|
+
setStateWithValidation: (
|
|
145
|
+
value: string,
|
|
146
|
+
direction: NavigationDirection,
|
|
147
|
+
) => Promise<boolean>
|
|
148
|
+
hasValidation: () => boolean
|
|
149
|
+
notify: () => void
|
|
150
|
+
addStep: (value: string, completed: boolean, disabled: boolean) => void
|
|
151
|
+
removeStep: (value: string) => void
|
|
152
|
+
setStep: (value: string, completed: boolean, disabled: boolean) => void
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const StoreContext = React.createContext<Store | null>(null)
|
|
156
|
+
|
|
157
|
+
function useStoreContext(consumerName: string) {
|
|
158
|
+
const context = React.useContext(StoreContext)
|
|
159
|
+
if (!context) {
|
|
160
|
+
throw new Error(`\`${consumerName}\` must be used within \`${ROOT_NAME}\``)
|
|
161
|
+
}
|
|
162
|
+
return context
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function useStore<T>(selector: (state: StoreState) => T): T {
|
|
166
|
+
const store = useStoreContext("useStore")
|
|
167
|
+
|
|
168
|
+
const getSnapshot = React.useCallback(
|
|
169
|
+
() => selector(store.getState()),
|
|
170
|
+
[store, selector],
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
interface ItemData {
|
|
177
|
+
id: string
|
|
178
|
+
ref: React.RefObject<TriggerElement | null>
|
|
179
|
+
value: string
|
|
180
|
+
active: boolean
|
|
181
|
+
disabled: boolean
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
interface StepperContextValue {
|
|
185
|
+
rootId: string
|
|
186
|
+
dir: Direction
|
|
187
|
+
orientation: Orientation
|
|
188
|
+
activationMode: ActivationMode
|
|
189
|
+
disabled: boolean
|
|
190
|
+
nonInteractive: boolean
|
|
191
|
+
loop: boolean
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const StepperContext = React.createContext<StepperContextValue | null>(null)
|
|
195
|
+
|
|
196
|
+
function useStepperContext(consumerName: string) {
|
|
197
|
+
const context = React.useContext(StepperContext)
|
|
198
|
+
if (!context) {
|
|
199
|
+
throw new Error(`\`${consumerName}\` must be used within \`${ROOT_NAME}\``)
|
|
200
|
+
}
|
|
201
|
+
return context
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
interface StepperProps extends DivProps {
|
|
205
|
+
value?: string
|
|
206
|
+
defaultValue?: string
|
|
207
|
+
onValueChange?: (value: string) => void
|
|
208
|
+
onValueComplete?: (value: string, completed: boolean) => void
|
|
209
|
+
onValueAdd?: (value: string) => void
|
|
210
|
+
onValueRemove?: (value: string) => void
|
|
211
|
+
onValidate?: (
|
|
212
|
+
value: string,
|
|
213
|
+
direction: NavigationDirection,
|
|
214
|
+
) => boolean | Promise<boolean>
|
|
215
|
+
activationMode?: ActivationMode
|
|
216
|
+
dir?: Direction
|
|
217
|
+
orientation?: Orientation
|
|
218
|
+
disabled?: boolean
|
|
219
|
+
loop?: boolean
|
|
220
|
+
nonInteractive?: boolean
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function Stepper(props: StepperProps) {
|
|
224
|
+
const {
|
|
225
|
+
value,
|
|
226
|
+
defaultValue,
|
|
227
|
+
onValueChange,
|
|
228
|
+
onValueComplete,
|
|
229
|
+
onValueAdd,
|
|
230
|
+
onValueRemove,
|
|
231
|
+
onValidate,
|
|
232
|
+
dir: dirProp,
|
|
233
|
+
orientation = "horizontal",
|
|
234
|
+
activationMode = "automatic",
|
|
235
|
+
asChild,
|
|
236
|
+
disabled = false,
|
|
237
|
+
nonInteractive = false,
|
|
238
|
+
loop = false,
|
|
239
|
+
className,
|
|
240
|
+
id,
|
|
241
|
+
...rootProps
|
|
242
|
+
} = props
|
|
243
|
+
|
|
244
|
+
const listenersRef = React.useRef<Set<() => void>>(new Set())
|
|
245
|
+
const stateRef = React.useRef<StoreState>({
|
|
246
|
+
steps: new Map(),
|
|
247
|
+
value: value ?? defaultValue ?? "",
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
const propsRef = React.useRef({
|
|
251
|
+
onValueChange,
|
|
252
|
+
onValueComplete,
|
|
253
|
+
onValueAdd,
|
|
254
|
+
onValueRemove,
|
|
255
|
+
onValidate,
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
React.useEffect(() => {
|
|
259
|
+
propsRef.current = {
|
|
260
|
+
onValueChange,
|
|
261
|
+
onValueComplete,
|
|
262
|
+
onValueAdd,
|
|
263
|
+
onValueRemove,
|
|
264
|
+
onValidate,
|
|
265
|
+
}
|
|
266
|
+
}, [onValueChange, onValueComplete, onValueAdd, onValueRemove, onValidate])
|
|
267
|
+
|
|
268
|
+
const store = React.useMemo<Store>(() => {
|
|
269
|
+
return {
|
|
270
|
+
subscribe: (cb) => {
|
|
271
|
+
listenersRef.current.add(cb)
|
|
272
|
+
return () => listenersRef.current.delete(cb)
|
|
273
|
+
},
|
|
274
|
+
getState: () => stateRef.current,
|
|
275
|
+
setState: (key, value) => {
|
|
276
|
+
if (Object.is(stateRef.current[key], value)) return
|
|
277
|
+
|
|
278
|
+
if (key === "value" && typeof value === "string") {
|
|
279
|
+
stateRef.current.value = value
|
|
280
|
+
propsRef.current.onValueChange?.(value)
|
|
281
|
+
} else {
|
|
282
|
+
stateRef.current[key] = value
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
store.notify()
|
|
286
|
+
},
|
|
287
|
+
setStateWithValidation: async (value, direction) => {
|
|
288
|
+
if (!propsRef.current.onValidate) {
|
|
289
|
+
store.setState("value", value)
|
|
290
|
+
return true
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const isValid = await propsRef.current.onValidate(value, direction)
|
|
295
|
+
if (isValid) {
|
|
296
|
+
store.setState("value", value)
|
|
297
|
+
}
|
|
298
|
+
return isValid
|
|
299
|
+
} catch {
|
|
300
|
+
return false
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
hasValidation: () => !!propsRef.current.onValidate,
|
|
304
|
+
addStep: (value, completed, disabled) => {
|
|
305
|
+
const newStep: StepState = { value, completed, disabled }
|
|
306
|
+
stateRef.current.steps.set(value, newStep)
|
|
307
|
+
propsRef.current.onValueAdd?.(value)
|
|
308
|
+
store.notify()
|
|
309
|
+
},
|
|
310
|
+
removeStep: (value) => {
|
|
311
|
+
stateRef.current.steps.delete(value)
|
|
312
|
+
propsRef.current.onValueRemove?.(value)
|
|
313
|
+
store.notify()
|
|
314
|
+
},
|
|
315
|
+
setStep: (value, completed, disabled) => {
|
|
316
|
+
const step = stateRef.current.steps.get(value)
|
|
317
|
+
if (step) {
|
|
318
|
+
const updatedStep: StepState = { ...step, completed, disabled }
|
|
319
|
+
stateRef.current.steps.set(value, updatedStep)
|
|
320
|
+
|
|
321
|
+
if (completed !== step.completed) {
|
|
322
|
+
propsRef.current.onValueComplete?.(value, completed)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
store.notify()
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
notify: () => {
|
|
329
|
+
for (const cb of listenersRef.current) {
|
|
330
|
+
cb()
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
}
|
|
334
|
+
}, [])
|
|
335
|
+
|
|
336
|
+
React.useEffect(() => {
|
|
337
|
+
if (value !== undefined) {
|
|
338
|
+
store.setState("value", value)
|
|
339
|
+
}
|
|
340
|
+
}, [value, store])
|
|
341
|
+
|
|
342
|
+
const dir: Direction = dirProp ?? "ltr"
|
|
343
|
+
|
|
344
|
+
const instanceId = React.useId()
|
|
345
|
+
const rootId = id ?? instanceId
|
|
346
|
+
|
|
347
|
+
const contextValue = React.useMemo<StepperContextValue>(
|
|
348
|
+
() => ({
|
|
349
|
+
rootId,
|
|
350
|
+
dir,
|
|
351
|
+
orientation,
|
|
352
|
+
activationMode,
|
|
353
|
+
disabled,
|
|
354
|
+
nonInteractive,
|
|
355
|
+
loop,
|
|
356
|
+
}),
|
|
357
|
+
[rootId, dir, orientation, activationMode, disabled, nonInteractive, loop],
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
const RootPrimitive = asChild ? Slot : "div"
|
|
361
|
+
|
|
362
|
+
return (
|
|
363
|
+
<StoreContext.Provider value={store}>
|
|
364
|
+
<StepperContext.Provider value={contextValue}>
|
|
365
|
+
<RootPrimitive
|
|
366
|
+
id={rootId}
|
|
367
|
+
data-disabled={disabled ? "" : undefined}
|
|
368
|
+
data-orientation={orientation}
|
|
369
|
+
data-slot="stepper"
|
|
370
|
+
dir={dir}
|
|
371
|
+
{...rootProps}
|
|
372
|
+
className={cn(
|
|
373
|
+
"flex gap-6",
|
|
374
|
+
orientation === "horizontal" ? "w-full flex-col" : "flex-row",
|
|
375
|
+
className,
|
|
376
|
+
)}
|
|
377
|
+
/>
|
|
378
|
+
</StepperContext.Provider>
|
|
379
|
+
</StoreContext.Provider>
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
interface FocusContextValue {
|
|
384
|
+
tabStopId: string | null
|
|
385
|
+
onItemFocus: (tabStopId: string) => void
|
|
386
|
+
onItemShiftTab: () => void
|
|
387
|
+
onFocusableItemAdd: () => void
|
|
388
|
+
onFocusableItemRemove: () => void
|
|
389
|
+
onItemRegister: (item: ItemData) => void
|
|
390
|
+
onItemUnregister: (id: string) => void
|
|
391
|
+
getItems: () => ItemData[]
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const FocusContext = React.createContext<FocusContextValue | null>(null)
|
|
395
|
+
|
|
396
|
+
function useFocusContext(consumerName: string) {
|
|
397
|
+
const context = React.useContext(FocusContext)
|
|
398
|
+
if (!context) {
|
|
399
|
+
throw new Error(
|
|
400
|
+
`\`${consumerName}\` must be used within \`FocusProvider\``,
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
return context
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function StepperList(props: DivProps) {
|
|
407
|
+
const {
|
|
408
|
+
asChild,
|
|
409
|
+
onBlur: onBlurProp,
|
|
410
|
+
onFocus: onFocusProp,
|
|
411
|
+
onMouseDown: onMouseDownProp,
|
|
412
|
+
className,
|
|
413
|
+
children,
|
|
414
|
+
ref,
|
|
415
|
+
...listProps
|
|
416
|
+
} = props
|
|
417
|
+
|
|
418
|
+
const context = useStepperContext(LIST_NAME)
|
|
419
|
+
const orientation = context.orientation
|
|
420
|
+
const currentValue = useStore((state) => state.value)
|
|
421
|
+
|
|
422
|
+
const propsRef = React.useRef({
|
|
423
|
+
onBlur: onBlurProp,
|
|
424
|
+
onFocus: onFocusProp,
|
|
425
|
+
onMouseDown: onMouseDownProp,
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
React.useEffect(() => {
|
|
429
|
+
propsRef.current = { onBlur: onBlurProp, onFocus: onFocusProp, onMouseDown: onMouseDownProp }
|
|
430
|
+
}, [onBlurProp, onFocusProp, onMouseDownProp])
|
|
431
|
+
|
|
432
|
+
const [tabStopId, setTabStopId] = React.useState<string | null>(null)
|
|
433
|
+
const [isTabbingBackOut, setIsTabbingBackOut] = React.useState(false)
|
|
434
|
+
const [focusableItemCount, setFocusableItemCount] = React.useState(0)
|
|
435
|
+
const isClickFocusRef = React.useRef(false)
|
|
436
|
+
const itemsRef = React.useRef<Map<string, ItemData>>(new Map())
|
|
437
|
+
const listRef = React.useRef<ListElement>(null)
|
|
438
|
+
|
|
439
|
+
const composedRef = React.useCallback(
|
|
440
|
+
(node: ListElement | null) => {
|
|
441
|
+
listRef.current = node
|
|
442
|
+
if (typeof ref === "function") {
|
|
443
|
+
ref(node)
|
|
444
|
+
} else if (ref) {
|
|
445
|
+
ref.current = node
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
[ref],
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
const onItemFocus = React.useCallback((tabStopId: string) => {
|
|
452
|
+
setTabStopId(tabStopId)
|
|
453
|
+
}, [])
|
|
454
|
+
|
|
455
|
+
const onItemShiftTab = React.useCallback(() => {
|
|
456
|
+
setIsTabbingBackOut(true)
|
|
457
|
+
}, [])
|
|
458
|
+
|
|
459
|
+
const onFocusableItemAdd = React.useCallback(() => {
|
|
460
|
+
setFocusableItemCount((prevCount) => prevCount + 1)
|
|
461
|
+
}, [])
|
|
462
|
+
|
|
463
|
+
const onFocusableItemRemove = React.useCallback(() => {
|
|
464
|
+
setFocusableItemCount((prevCount) => prevCount - 1)
|
|
465
|
+
}, [])
|
|
466
|
+
|
|
467
|
+
const onItemRegister = React.useCallback((item: ItemData) => {
|
|
468
|
+
itemsRef.current.set(item.id, item)
|
|
469
|
+
}, [])
|
|
470
|
+
|
|
471
|
+
const onItemUnregister = React.useCallback((id: string) => {
|
|
472
|
+
itemsRef.current.delete(id)
|
|
473
|
+
}, [])
|
|
474
|
+
|
|
475
|
+
const getItems = React.useCallback(() => {
|
|
476
|
+
return Array.from(itemsRef.current.values())
|
|
477
|
+
.filter((item) => item.ref.current)
|
|
478
|
+
.sort((a, b) => {
|
|
479
|
+
const elementA = a.ref.current
|
|
480
|
+
const elementB = b.ref.current
|
|
481
|
+
if (!elementA || !elementB) return 0
|
|
482
|
+
const position = elementA.compareDocumentPosition(elementB)
|
|
483
|
+
if (position & Node.DOCUMENT_POSITION_FOLLOWING) {
|
|
484
|
+
return -1
|
|
485
|
+
}
|
|
486
|
+
if (position & Node.DOCUMENT_POSITION_PRECEDING) {
|
|
487
|
+
return 1
|
|
488
|
+
}
|
|
489
|
+
return 0
|
|
490
|
+
})
|
|
491
|
+
}, [])
|
|
492
|
+
|
|
493
|
+
const onBlur = React.useCallback(
|
|
494
|
+
(event: React.FocusEvent<ListElement>) => {
|
|
495
|
+
propsRef.current.onBlur?.(event)
|
|
496
|
+
if (event.defaultPrevented) return
|
|
497
|
+
|
|
498
|
+
setIsTabbingBackOut(false)
|
|
499
|
+
},
|
|
500
|
+
[],
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
const onFocus = React.useCallback(
|
|
504
|
+
(event: React.FocusEvent<ListElement>) => {
|
|
505
|
+
propsRef.current.onFocus?.(event)
|
|
506
|
+
if (event.defaultPrevented) return
|
|
507
|
+
|
|
508
|
+
const isKeyboardFocus = !isClickFocusRef.current
|
|
509
|
+
if (
|
|
510
|
+
event.target === event.currentTarget &&
|
|
511
|
+
isKeyboardFocus &&
|
|
512
|
+
!isTabbingBackOut
|
|
513
|
+
) {
|
|
514
|
+
const entryFocusEvent = new CustomEvent(ENTRY_FOCUS, EVENT_OPTIONS)
|
|
515
|
+
event.currentTarget.dispatchEvent(entryFocusEvent)
|
|
516
|
+
|
|
517
|
+
if (!entryFocusEvent.defaultPrevented) {
|
|
518
|
+
const items = Array.from(itemsRef.current.values()).filter(
|
|
519
|
+
(item) => !item.disabled,
|
|
520
|
+
)
|
|
521
|
+
const selectedItem = currentValue
|
|
522
|
+
? items.find((item) => item.value === currentValue)
|
|
523
|
+
: undefined
|
|
524
|
+
const activeItem = items.find((item) => item.active)
|
|
525
|
+
const currentItem = items.find((item) => item.id === tabStopId)
|
|
526
|
+
|
|
527
|
+
const candidateItems = [
|
|
528
|
+
selectedItem,
|
|
529
|
+
activeItem,
|
|
530
|
+
currentItem,
|
|
531
|
+
...items,
|
|
532
|
+
].filter(Boolean) as ItemData[]
|
|
533
|
+
const candidateRefs = candidateItems.map((item) => item.ref)
|
|
534
|
+
focusFirst(candidateRefs, false)
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
isClickFocusRef.current = false
|
|
538
|
+
},
|
|
539
|
+
[isTabbingBackOut, currentValue, tabStopId],
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
const onMouseDown = React.useCallback(
|
|
543
|
+
(event: React.MouseEvent<ListElement>) => {
|
|
544
|
+
propsRef.current.onMouseDown?.(event)
|
|
545
|
+
|
|
546
|
+
if (event.defaultPrevented) return
|
|
547
|
+
|
|
548
|
+
isClickFocusRef.current = true
|
|
549
|
+
},
|
|
550
|
+
[],
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
const focusContextValue = React.useMemo<FocusContextValue>(
|
|
554
|
+
() => ({
|
|
555
|
+
tabStopId,
|
|
556
|
+
onItemFocus,
|
|
557
|
+
onItemShiftTab,
|
|
558
|
+
onFocusableItemAdd,
|
|
559
|
+
onFocusableItemRemove,
|
|
560
|
+
onItemRegister,
|
|
561
|
+
onItemUnregister,
|
|
562
|
+
getItems,
|
|
563
|
+
}),
|
|
564
|
+
[
|
|
565
|
+
tabStopId,
|
|
566
|
+
onItemFocus,
|
|
567
|
+
onItemShiftTab,
|
|
568
|
+
onFocusableItemAdd,
|
|
569
|
+
onFocusableItemRemove,
|
|
570
|
+
onItemRegister,
|
|
571
|
+
onItemUnregister,
|
|
572
|
+
getItems,
|
|
573
|
+
],
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
const ListPrimitive = asChild ? Slot : "div"
|
|
577
|
+
|
|
578
|
+
return (
|
|
579
|
+
<FocusContext.Provider value={focusContextValue}>
|
|
580
|
+
<ListPrimitive
|
|
581
|
+
role="tablist"
|
|
582
|
+
aria-orientation={orientation}
|
|
583
|
+
data-orientation={orientation}
|
|
584
|
+
data-slot="stepper-list"
|
|
585
|
+
dir={context.dir}
|
|
586
|
+
tabIndex={isTabbingBackOut || focusableItemCount === 0 ? -1 : 0}
|
|
587
|
+
{...listProps}
|
|
588
|
+
ref={composedRef}
|
|
589
|
+
className={cn(
|
|
590
|
+
"flex outline-none",
|
|
591
|
+
orientation === "horizontal"
|
|
592
|
+
? "flex-row items-center"
|
|
593
|
+
: "flex-col items-start",
|
|
594
|
+
className,
|
|
595
|
+
)}
|
|
596
|
+
onBlur={onBlur}
|
|
597
|
+
onFocus={onFocus}
|
|
598
|
+
onMouseDown={onMouseDown}
|
|
599
|
+
>
|
|
600
|
+
{children}
|
|
601
|
+
</ListPrimitive>
|
|
602
|
+
</FocusContext.Provider>
|
|
603
|
+
)
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
interface StepperItemContextValue {
|
|
607
|
+
value: string
|
|
608
|
+
stepState: StepState | undefined
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const StepperItemContext = React.createContext<StepperItemContextValue | null>(
|
|
612
|
+
null,
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
function useStepperItemContext(consumerName: string) {
|
|
616
|
+
const context = React.useContext(StepperItemContext)
|
|
617
|
+
if (!context) {
|
|
618
|
+
throw new Error(`\`${consumerName}\` must be used within \`${ITEM_NAME}\``)
|
|
619
|
+
}
|
|
620
|
+
return context
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
interface StepperItemProps extends DivProps {
|
|
624
|
+
value: string
|
|
625
|
+
completed?: boolean
|
|
626
|
+
disabled?: boolean
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function StepperItem(props: StepperItemProps) {
|
|
630
|
+
const {
|
|
631
|
+
value: itemValue,
|
|
632
|
+
completed = false,
|
|
633
|
+
disabled = false,
|
|
634
|
+
asChild,
|
|
635
|
+
className,
|
|
636
|
+
children,
|
|
637
|
+
ref,
|
|
638
|
+
...itemProps
|
|
639
|
+
} = props
|
|
640
|
+
|
|
641
|
+
const context = useStepperContext(ITEM_NAME)
|
|
642
|
+
const store = useStoreContext(ITEM_NAME)
|
|
643
|
+
const orientation = context.orientation
|
|
644
|
+
const value = useStore((state) => state.value)
|
|
645
|
+
|
|
646
|
+
React.useEffect(() => {
|
|
647
|
+
store.addStep(itemValue, completed, disabled)
|
|
648
|
+
|
|
649
|
+
return () => {
|
|
650
|
+
store.removeStep(itemValue)
|
|
651
|
+
}
|
|
652
|
+
}, [itemValue, completed, disabled, store])
|
|
653
|
+
|
|
654
|
+
React.useEffect(() => {
|
|
655
|
+
store.setStep(itemValue, completed, disabled)
|
|
656
|
+
}, [itemValue, completed, disabled, store])
|
|
657
|
+
|
|
658
|
+
const stepState = useStore((state) => state.steps.get(itemValue))
|
|
659
|
+
const steps = useStore((state) => state.steps)
|
|
660
|
+
const dataState = getDataState(value, itemValue, stepState, steps)
|
|
661
|
+
|
|
662
|
+
const itemContextValue = React.useMemo<StepperItemContextValue>(
|
|
663
|
+
() => ({
|
|
664
|
+
value: itemValue,
|
|
665
|
+
stepState,
|
|
666
|
+
}),
|
|
667
|
+
[itemValue, stepState],
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
const ItemPrimitive = asChild ? Slot : "div"
|
|
671
|
+
|
|
672
|
+
return (
|
|
673
|
+
<StepperItemContext.Provider value={itemContextValue}>
|
|
674
|
+
<ItemPrimitive
|
|
675
|
+
data-disabled={stepState?.disabled ? "" : undefined}
|
|
676
|
+
data-orientation={orientation}
|
|
677
|
+
data-state={dataState}
|
|
678
|
+
data-slot="stepper-item"
|
|
679
|
+
dir={context.dir}
|
|
680
|
+
{...itemProps}
|
|
681
|
+
ref={ref}
|
|
682
|
+
className={cn(
|
|
683
|
+
"relative flex not-last:flex-1 items-center",
|
|
684
|
+
orientation === "horizontal" ? "flex-row" : "flex-col",
|
|
685
|
+
className,
|
|
686
|
+
)}
|
|
687
|
+
>
|
|
688
|
+
{children}
|
|
689
|
+
</ItemPrimitive>
|
|
690
|
+
</StepperItemContext.Provider>
|
|
691
|
+
)
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function StepperTrigger(props: ButtonProps) {
|
|
695
|
+
const {
|
|
696
|
+
asChild,
|
|
697
|
+
onClick: onClickProp,
|
|
698
|
+
onFocus: onFocusProp,
|
|
699
|
+
onKeyDown: onKeyDownProp,
|
|
700
|
+
onMouseDown: onMouseDownProp,
|
|
701
|
+
disabled,
|
|
702
|
+
className,
|
|
703
|
+
ref,
|
|
704
|
+
...triggerProps
|
|
705
|
+
} = props
|
|
706
|
+
|
|
707
|
+
const context = useStepperContext(TRIGGER_NAME)
|
|
708
|
+
const itemContext = useStepperItemContext(TRIGGER_NAME)
|
|
709
|
+
const itemValue = itemContext.value
|
|
710
|
+
|
|
711
|
+
const store = useStoreContext(TRIGGER_NAME)
|
|
712
|
+
const focusContext = useFocusContext(TRIGGER_NAME)
|
|
713
|
+
const value = useStore((state) => state.value)
|
|
714
|
+
const steps = useStore((state) => state.steps)
|
|
715
|
+
const stepState = useStore((state) => state.steps.get(itemValue))
|
|
716
|
+
|
|
717
|
+
const propsRef = React.useRef({
|
|
718
|
+
onClick: onClickProp,
|
|
719
|
+
onFocus: onFocusProp,
|
|
720
|
+
onKeyDown: onKeyDownProp,
|
|
721
|
+
onMouseDown: onMouseDownProp,
|
|
722
|
+
})
|
|
723
|
+
|
|
724
|
+
React.useEffect(() => {
|
|
725
|
+
propsRef.current = {
|
|
726
|
+
onClick: onClickProp,
|
|
727
|
+
onFocus: onFocusProp,
|
|
728
|
+
onKeyDown: onKeyDownProp,
|
|
729
|
+
onMouseDown: onMouseDownProp,
|
|
730
|
+
}
|
|
731
|
+
}, [onClickProp, onFocusProp, onKeyDownProp, onMouseDownProp])
|
|
732
|
+
|
|
733
|
+
const activationMode = context.activationMode
|
|
734
|
+
const orientation = context.orientation
|
|
735
|
+
const loop = context.loop
|
|
736
|
+
|
|
737
|
+
const stepIndex = Array.from(steps.keys()).indexOf(itemValue)
|
|
738
|
+
|
|
739
|
+
const stepPosition = stepIndex + 1
|
|
740
|
+
const stepCount = steps.size
|
|
741
|
+
|
|
742
|
+
const triggerId = getId(context.rootId, "trigger", itemValue)
|
|
743
|
+
const contentId = getId(context.rootId, "content", itemValue)
|
|
744
|
+
const titleId = getId(context.rootId, "title", itemValue)
|
|
745
|
+
const descriptionId = getId(context.rootId, "description", itemValue)
|
|
746
|
+
|
|
747
|
+
const isDisabled = disabled || stepState?.disabled || context.disabled
|
|
748
|
+
const isActive = value === itemValue
|
|
749
|
+
const isTabStop = focusContext.tabStopId === triggerId
|
|
750
|
+
const dataState = getDataState(value, itemValue, stepState, steps)
|
|
751
|
+
|
|
752
|
+
const triggerRef = React.useRef<TriggerElement>(null)
|
|
753
|
+
|
|
754
|
+
const composedRef = React.useCallback(
|
|
755
|
+
(node: TriggerElement | null) => {
|
|
756
|
+
triggerRef.current = node
|
|
757
|
+
if (typeof ref === "function") {
|
|
758
|
+
ref(node)
|
|
759
|
+
} else if (ref) {
|
|
760
|
+
ref.current = node
|
|
761
|
+
}
|
|
762
|
+
},
|
|
763
|
+
[ref],
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
const isArrowKeyPressedRef = React.useRef(false)
|
|
767
|
+
const isMouseClickRef = React.useRef(false)
|
|
768
|
+
|
|
769
|
+
React.useEffect(() => {
|
|
770
|
+
function onKeyDown(event: KeyboardEvent) {
|
|
771
|
+
if (ARROW_KEYS.includes(event.key)) {
|
|
772
|
+
isArrowKeyPressedRef.current = true
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
function onKeyUp() {
|
|
776
|
+
isArrowKeyPressedRef.current = false
|
|
777
|
+
}
|
|
778
|
+
document.addEventListener("keydown", onKeyDown)
|
|
779
|
+
document.addEventListener("keyup", onKeyUp)
|
|
780
|
+
return () => {
|
|
781
|
+
document.removeEventListener("keydown", onKeyDown)
|
|
782
|
+
document.removeEventListener("keyup", onKeyUp)
|
|
783
|
+
}
|
|
784
|
+
}, [])
|
|
785
|
+
|
|
786
|
+
React.useEffect(() => {
|
|
787
|
+
focusContext.onItemRegister({
|
|
788
|
+
id: triggerId,
|
|
789
|
+
ref: triggerRef,
|
|
790
|
+
value: itemValue,
|
|
791
|
+
active: isTabStop,
|
|
792
|
+
disabled: !!isDisabled,
|
|
793
|
+
})
|
|
794
|
+
|
|
795
|
+
if (!isDisabled) {
|
|
796
|
+
focusContext.onFocusableItemAdd()
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
return () => {
|
|
800
|
+
focusContext.onItemUnregister(triggerId)
|
|
801
|
+
if (!isDisabled) {
|
|
802
|
+
focusContext.onFocusableItemRemove()
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}, [focusContext, triggerId, itemValue, isTabStop, isDisabled])
|
|
806
|
+
|
|
807
|
+
const onClick = React.useCallback(
|
|
808
|
+
async (event: React.MouseEvent<TriggerElement>) => {
|
|
809
|
+
propsRef.current.onClick?.(event)
|
|
810
|
+
if (event.defaultPrevented) return
|
|
811
|
+
|
|
812
|
+
if (!isDisabled && !context.nonInteractive) {
|
|
813
|
+
const currentStepIndex = Array.from(steps.keys()).indexOf(value ?? "")
|
|
814
|
+
const targetStepIndex = Array.from(steps.keys()).indexOf(itemValue)
|
|
815
|
+
const direction = targetStepIndex > currentStepIndex ? "next" : "prev"
|
|
816
|
+
|
|
817
|
+
await store.setStateWithValidation(itemValue, direction)
|
|
818
|
+
}
|
|
819
|
+
},
|
|
820
|
+
[isDisabled, context.nonInteractive, store, itemValue, value, steps],
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
const onFocus = React.useCallback(
|
|
824
|
+
async (event: React.FocusEvent<TriggerElement>) => {
|
|
825
|
+
propsRef.current.onFocus?.(event)
|
|
826
|
+
if (event.defaultPrevented) return
|
|
827
|
+
|
|
828
|
+
focusContext.onItemFocus(triggerId)
|
|
829
|
+
|
|
830
|
+
const isKeyboardFocus = !isMouseClickRef.current
|
|
831
|
+
|
|
832
|
+
if (
|
|
833
|
+
!isActive &&
|
|
834
|
+
!isDisabled &&
|
|
835
|
+
activationMode !== "manual" &&
|
|
836
|
+
!context.nonInteractive &&
|
|
837
|
+
isKeyboardFocus
|
|
838
|
+
) {
|
|
839
|
+
const currentStepIndex = Array.from(steps.keys()).indexOf(value || "")
|
|
840
|
+
const targetStepIndex = Array.from(steps.keys()).indexOf(itemValue)
|
|
841
|
+
const direction = targetStepIndex > currentStepIndex ? "next" : "prev"
|
|
842
|
+
|
|
843
|
+
await store.setStateWithValidation(itemValue, direction)
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
isMouseClickRef.current = false
|
|
847
|
+
},
|
|
848
|
+
[
|
|
849
|
+
focusContext,
|
|
850
|
+
triggerId,
|
|
851
|
+
activationMode,
|
|
852
|
+
isActive,
|
|
853
|
+
isDisabled,
|
|
854
|
+
context.nonInteractive,
|
|
855
|
+
store,
|
|
856
|
+
itemValue,
|
|
857
|
+
value,
|
|
858
|
+
steps,
|
|
859
|
+
],
|
|
860
|
+
)
|
|
861
|
+
|
|
862
|
+
const onKeyDown = React.useCallback(
|
|
863
|
+
async (event: React.KeyboardEvent<TriggerElement>) => {
|
|
864
|
+
propsRef.current.onKeyDown?.(event)
|
|
865
|
+
if (event.defaultPrevented) return
|
|
866
|
+
|
|
867
|
+
if (event.key === "Enter" && context.nonInteractive) {
|
|
868
|
+
event.preventDefault()
|
|
869
|
+
return
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
if (
|
|
873
|
+
(event.key === "Enter" || event.key === " ") &&
|
|
874
|
+
activationMode === "manual" &&
|
|
875
|
+
!context.nonInteractive
|
|
876
|
+
) {
|
|
877
|
+
event.preventDefault()
|
|
878
|
+
if (!isDisabled && triggerRef.current) {
|
|
879
|
+
triggerRef.current.click()
|
|
880
|
+
}
|
|
881
|
+
return
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (event.key === "Tab" && event.shiftKey) {
|
|
885
|
+
focusContext.onItemShiftTab()
|
|
886
|
+
return
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (event.target !== event.currentTarget) return
|
|
890
|
+
|
|
891
|
+
const focusIntent = getFocusIntent(event, context.dir, orientation)
|
|
892
|
+
|
|
893
|
+
if (focusIntent !== undefined) {
|
|
894
|
+
if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey)
|
|
895
|
+
return
|
|
896
|
+
event.preventDefault()
|
|
897
|
+
|
|
898
|
+
const items = focusContext.getItems().filter((item) => !item.disabled)
|
|
899
|
+
let candidateRefs = items.map((item) => item.ref)
|
|
900
|
+
|
|
901
|
+
if (focusIntent === "last") {
|
|
902
|
+
candidateRefs.reverse()
|
|
903
|
+
} else if (focusIntent === "prev" || focusIntent === "next") {
|
|
904
|
+
if (focusIntent === "prev") candidateRefs.reverse()
|
|
905
|
+
const currentIndex = candidateRefs.findIndex(
|
|
906
|
+
(ref) => ref.current === event.currentTarget,
|
|
907
|
+
)
|
|
908
|
+
candidateRefs = loop
|
|
909
|
+
? wrapArray(candidateRefs, currentIndex + 1)
|
|
910
|
+
: candidateRefs.slice(currentIndex + 1)
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
if (store.hasValidation() && candidateRefs.length > 0) {
|
|
914
|
+
const nextRef = candidateRefs[0]
|
|
915
|
+
const nextElement = nextRef?.current
|
|
916
|
+
const nextItem = items.find(
|
|
917
|
+
(item) => item.ref.current === nextElement,
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
if (nextItem && nextItem.value !== itemValue) {
|
|
921
|
+
const currentStepIndex = Array.from(steps.keys()).indexOf(
|
|
922
|
+
value || "",
|
|
923
|
+
)
|
|
924
|
+
const targetStepIndex = Array.from(steps.keys()).indexOf(
|
|
925
|
+
nextItem.value,
|
|
926
|
+
)
|
|
927
|
+
const direction: NavigationDirection =
|
|
928
|
+
targetStepIndex > currentStepIndex ? "next" : "prev"
|
|
929
|
+
|
|
930
|
+
if (direction === "next") {
|
|
931
|
+
const isValid = await store.setStateWithValidation(
|
|
932
|
+
nextItem.value,
|
|
933
|
+
direction,
|
|
934
|
+
)
|
|
935
|
+
if (!isValid) return
|
|
936
|
+
} else {
|
|
937
|
+
store.setState("value", nextItem.value)
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
queueMicrotask(() => nextElement?.focus())
|
|
941
|
+
return
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
queueMicrotask(() => focusFirst(candidateRefs))
|
|
946
|
+
}
|
|
947
|
+
},
|
|
948
|
+
[
|
|
949
|
+
focusContext,
|
|
950
|
+
context.nonInteractive,
|
|
951
|
+
context.dir,
|
|
952
|
+
activationMode,
|
|
953
|
+
orientation,
|
|
954
|
+
loop,
|
|
955
|
+
isDisabled,
|
|
956
|
+
store,
|
|
957
|
+
itemValue,
|
|
958
|
+
value,
|
|
959
|
+
steps,
|
|
960
|
+
],
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
const onMouseDown = React.useCallback(
|
|
964
|
+
(event: React.MouseEvent<TriggerElement>) => {
|
|
965
|
+
propsRef.current.onMouseDown?.(event)
|
|
966
|
+
if (event.defaultPrevented) return
|
|
967
|
+
|
|
968
|
+
isMouseClickRef.current = true
|
|
969
|
+
|
|
970
|
+
if (isDisabled) {
|
|
971
|
+
event.preventDefault()
|
|
972
|
+
} else {
|
|
973
|
+
focusContext.onItemFocus(triggerId)
|
|
974
|
+
}
|
|
975
|
+
},
|
|
976
|
+
[focusContext, triggerId, isDisabled],
|
|
977
|
+
)
|
|
978
|
+
|
|
979
|
+
const TriggerPrimitive = asChild ? Slot : "button"
|
|
980
|
+
|
|
981
|
+
return (
|
|
982
|
+
<TriggerPrimitive
|
|
983
|
+
id={triggerId}
|
|
984
|
+
role="tab"
|
|
985
|
+
type="button"
|
|
986
|
+
aria-controls={contentId}
|
|
987
|
+
aria-current={isActive ? "step" : undefined}
|
|
988
|
+
aria-describedby={`${titleId} ${descriptionId}`}
|
|
989
|
+
aria-posinset={stepPosition}
|
|
990
|
+
aria-selected={isActive}
|
|
991
|
+
aria-setsize={stepCount}
|
|
992
|
+
data-disabled={isDisabled ? "" : undefined}
|
|
993
|
+
data-state={dataState}
|
|
994
|
+
data-slot="stepper-trigger"
|
|
995
|
+
disabled={isDisabled}
|
|
996
|
+
tabIndex={isTabStop ? 0 : -1}
|
|
997
|
+
{...triggerProps}
|
|
998
|
+
ref={composedRef}
|
|
999
|
+
className={cn(
|
|
1000
|
+
"inline-flex items-center justify-center gap-3 rounded-md text-left outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
1001
|
+
"not-has-data-[slot=description]:rounded-full not-has-data-[slot=title]:rounded-full",
|
|
1002
|
+
className,
|
|
1003
|
+
)}
|
|
1004
|
+
onClick={onClick}
|
|
1005
|
+
onFocus={onFocus}
|
|
1006
|
+
onKeyDown={onKeyDown}
|
|
1007
|
+
onMouseDown={onMouseDown}
|
|
1008
|
+
/>
|
|
1009
|
+
)
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
interface StepperIndicatorProps extends Omit<DivProps, "children"> {
|
|
1013
|
+
children?: React.ReactNode | ((dataState: DataState) => React.ReactNode)
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function StepperIndicator(props: StepperIndicatorProps) {
|
|
1017
|
+
const { className, children, asChild, ref, ...indicatorProps } = props
|
|
1018
|
+
|
|
1019
|
+
const context = useStepperContext(INDICATOR_NAME)
|
|
1020
|
+
const itemContext = useStepperItemContext(INDICATOR_NAME)
|
|
1021
|
+
|
|
1022
|
+
const value = useStore((state) => state.value)
|
|
1023
|
+
const itemValue = itemContext.value
|
|
1024
|
+
const stepState = useStore((state) => state.steps.get(itemValue))
|
|
1025
|
+
const steps = useStore((state) => state.steps)
|
|
1026
|
+
|
|
1027
|
+
const stepPosition = Array.from(steps.keys()).indexOf(itemValue) + 1
|
|
1028
|
+
|
|
1029
|
+
const dataState = getDataState(value, itemValue, stepState, steps)
|
|
1030
|
+
|
|
1031
|
+
const IndicatorPrimitive = asChild ? Slot : "div"
|
|
1032
|
+
|
|
1033
|
+
return (
|
|
1034
|
+
<IndicatorPrimitive
|
|
1035
|
+
data-state={dataState}
|
|
1036
|
+
data-slot="stepper-indicator"
|
|
1037
|
+
dir={context.dir}
|
|
1038
|
+
{...indicatorProps}
|
|
1039
|
+
ref={ref}
|
|
1040
|
+
className={cn(
|
|
1041
|
+
"flex size-7 shrink-0 items-center justify-center rounded-full border-2 border-muted bg-background font-medium text-muted-foreground text-sm transition-colors data-[state=active]:border-primary data-[state=completed]:border-primary data-[state=active]:bg-primary data-[state=completed]:bg-primary data-[state=active]:text-primary-foreground data-[state=completed]:text-primary-foreground",
|
|
1042
|
+
className,
|
|
1043
|
+
)}
|
|
1044
|
+
>
|
|
1045
|
+
{typeof children === "function" ? (
|
|
1046
|
+
children(dataState)
|
|
1047
|
+
) : children ? (
|
|
1048
|
+
children
|
|
1049
|
+
) : dataState === "completed" ? (
|
|
1050
|
+
<Check className="size-4" />
|
|
1051
|
+
) : (
|
|
1052
|
+
stepPosition
|
|
1053
|
+
)}
|
|
1054
|
+
</IndicatorPrimitive>
|
|
1055
|
+
)
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
interface StepperSeparatorProps extends DivProps {
|
|
1059
|
+
forceMount?: boolean
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function StepperSeparator(props: StepperSeparatorProps) {
|
|
1063
|
+
const {
|
|
1064
|
+
className,
|
|
1065
|
+
asChild,
|
|
1066
|
+
forceMount = false,
|
|
1067
|
+
ref,
|
|
1068
|
+
...separatorProps
|
|
1069
|
+
} = props
|
|
1070
|
+
|
|
1071
|
+
const context = useStepperContext(SEPARATOR_NAME)
|
|
1072
|
+
const itemContext = useStepperItemContext(SEPARATOR_NAME)
|
|
1073
|
+
const value = useStore((state) => state.value)
|
|
1074
|
+
const steps = useStore((state) => state.steps)
|
|
1075
|
+
|
|
1076
|
+
const orientation = context.orientation
|
|
1077
|
+
|
|
1078
|
+
const stepIndex = Array.from(steps.keys()).indexOf(itemContext.value)
|
|
1079
|
+
|
|
1080
|
+
const isLastStep = stepIndex === steps.size - 1
|
|
1081
|
+
|
|
1082
|
+
if (isLastStep && !forceMount) return null
|
|
1083
|
+
|
|
1084
|
+
const dataState = getDataState(
|
|
1085
|
+
value,
|
|
1086
|
+
itemContext.value,
|
|
1087
|
+
itemContext.stepState,
|
|
1088
|
+
steps,
|
|
1089
|
+
"separator",
|
|
1090
|
+
)
|
|
1091
|
+
|
|
1092
|
+
const SeparatorPrimitive = asChild ? Slot : "div"
|
|
1093
|
+
|
|
1094
|
+
return (
|
|
1095
|
+
<SeparatorPrimitive
|
|
1096
|
+
role="separator"
|
|
1097
|
+
aria-hidden="true"
|
|
1098
|
+
aria-orientation={orientation}
|
|
1099
|
+
data-orientation={orientation}
|
|
1100
|
+
data-state={dataState}
|
|
1101
|
+
data-slot="stepper-separator"
|
|
1102
|
+
dir={context.dir}
|
|
1103
|
+
{...separatorProps}
|
|
1104
|
+
ref={ref}
|
|
1105
|
+
className={cn(
|
|
1106
|
+
"bg-border transition-colors data-[state=active]:bg-primary data-[state=completed]:bg-primary",
|
|
1107
|
+
orientation === "horizontal" ? "h-px flex-1" : "h-10 w-px",
|
|
1108
|
+
className,
|
|
1109
|
+
)}
|
|
1110
|
+
/>
|
|
1111
|
+
)
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
interface StepperTitleProps extends React.ComponentProps<"span"> {
|
|
1115
|
+
asChild?: boolean
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function StepperTitle(props: StepperTitleProps) {
|
|
1119
|
+
const { className, asChild, ref, ...titleProps } = props
|
|
1120
|
+
|
|
1121
|
+
const context = useStepperContext(TITLE_NAME)
|
|
1122
|
+
const itemContext = useStepperItemContext(TITLE_NAME)
|
|
1123
|
+
|
|
1124
|
+
const titleId = getId(context.rootId, "title", itemContext.value)
|
|
1125
|
+
|
|
1126
|
+
const TitlePrimitive = asChild ? Slot : "span"
|
|
1127
|
+
|
|
1128
|
+
return (
|
|
1129
|
+
<TitlePrimitive
|
|
1130
|
+
id={titleId}
|
|
1131
|
+
data-slot="stepper-title"
|
|
1132
|
+
dir={context.dir}
|
|
1133
|
+
{...titleProps}
|
|
1134
|
+
ref={ref}
|
|
1135
|
+
className={cn("font-medium text-sm", className)}
|
|
1136
|
+
/>
|
|
1137
|
+
)
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
interface StepperDescriptionProps extends React.ComponentProps<"span"> {
|
|
1141
|
+
asChild?: boolean
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function StepperDescription(props: StepperDescriptionProps) {
|
|
1145
|
+
const { className, asChild, ref, ...descriptionProps } = props
|
|
1146
|
+
|
|
1147
|
+
const context = useStepperContext(DESCRIPTION_NAME)
|
|
1148
|
+
const itemContext = useStepperItemContext(DESCRIPTION_NAME)
|
|
1149
|
+
|
|
1150
|
+
const descriptionId = getId(context.rootId, "description", itemContext.value)
|
|
1151
|
+
|
|
1152
|
+
const DescriptionPrimitive = asChild ? Slot : "span"
|
|
1153
|
+
|
|
1154
|
+
return (
|
|
1155
|
+
<DescriptionPrimitive
|
|
1156
|
+
id={descriptionId}
|
|
1157
|
+
data-slot="stepper-description"
|
|
1158
|
+
dir={context.dir}
|
|
1159
|
+
{...descriptionProps}
|
|
1160
|
+
ref={ref}
|
|
1161
|
+
className={cn("text-muted-foreground text-xs", className)}
|
|
1162
|
+
/>
|
|
1163
|
+
)
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
interface StepperContentProps extends DivProps {
|
|
1167
|
+
value: string
|
|
1168
|
+
forceMount?: boolean
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function StepperContent(props: StepperContentProps) {
|
|
1172
|
+
const {
|
|
1173
|
+
value: valueProp,
|
|
1174
|
+
asChild,
|
|
1175
|
+
forceMount = false,
|
|
1176
|
+
ref,
|
|
1177
|
+
className,
|
|
1178
|
+
...contentProps
|
|
1179
|
+
} = props
|
|
1180
|
+
|
|
1181
|
+
const context = useStepperContext(CONTENT_NAME)
|
|
1182
|
+
const value = useStore((state) => state.value)
|
|
1183
|
+
|
|
1184
|
+
const contentId = getId(context.rootId, "content", valueProp)
|
|
1185
|
+
const triggerId = getId(context.rootId, "trigger", valueProp)
|
|
1186
|
+
|
|
1187
|
+
if (valueProp !== value && !forceMount) return null
|
|
1188
|
+
|
|
1189
|
+
const ContentPrimitive = asChild ? Slot : "div"
|
|
1190
|
+
|
|
1191
|
+
return (
|
|
1192
|
+
<ContentPrimitive
|
|
1193
|
+
id={contentId}
|
|
1194
|
+
role="tabpanel"
|
|
1195
|
+
aria-labelledby={triggerId}
|
|
1196
|
+
data-slot="stepper-content"
|
|
1197
|
+
dir={context.dir}
|
|
1198
|
+
{...contentProps}
|
|
1199
|
+
ref={ref}
|
|
1200
|
+
className={cn("flex-1 outline-none", className)}
|
|
1201
|
+
/>
|
|
1202
|
+
)
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function StepperPrev(props: ButtonProps) {
|
|
1206
|
+
const { asChild, onClick: onClickProp, disabled, ...prevProps } = props
|
|
1207
|
+
|
|
1208
|
+
const store = useStoreContext(PREV_NAME)
|
|
1209
|
+
const value = useStore((state) => state.value)
|
|
1210
|
+
const steps = useStore((state) => state.steps)
|
|
1211
|
+
|
|
1212
|
+
const propsRef = React.useRef({ onClick: onClickProp })
|
|
1213
|
+
React.useEffect(() => {
|
|
1214
|
+
propsRef.current = { onClick: onClickProp }
|
|
1215
|
+
}, [onClickProp])
|
|
1216
|
+
|
|
1217
|
+
const stepKeys = Array.from(steps.keys())
|
|
1218
|
+
const currentIndex = value ? stepKeys.indexOf(value) : -1
|
|
1219
|
+
const isDisabled = disabled || currentIndex <= 0
|
|
1220
|
+
|
|
1221
|
+
const onClick = React.useCallback(
|
|
1222
|
+
async (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
1223
|
+
propsRef.current.onClick?.(event)
|
|
1224
|
+
if (event.defaultPrevented || isDisabled) return
|
|
1225
|
+
|
|
1226
|
+
const prevIndex = Math.max(currentIndex - 1, 0)
|
|
1227
|
+
const prevStepValue = stepKeys[prevIndex]
|
|
1228
|
+
|
|
1229
|
+
if (prevStepValue) {
|
|
1230
|
+
store.setState("value", prevStepValue)
|
|
1231
|
+
}
|
|
1232
|
+
},
|
|
1233
|
+
[isDisabled, currentIndex, stepKeys, store],
|
|
1234
|
+
)
|
|
1235
|
+
|
|
1236
|
+
const PrevPrimitive = asChild ? Slot : "button"
|
|
1237
|
+
|
|
1238
|
+
return (
|
|
1239
|
+
<PrevPrimitive
|
|
1240
|
+
type="button"
|
|
1241
|
+
data-slot="stepper-prev"
|
|
1242
|
+
disabled={isDisabled}
|
|
1243
|
+
{...prevProps}
|
|
1244
|
+
onClick={onClick}
|
|
1245
|
+
/>
|
|
1246
|
+
)
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function StepperNext(props: ButtonProps) {
|
|
1250
|
+
const { asChild, onClick: onClickProp, disabled, ...nextProps } = props
|
|
1251
|
+
|
|
1252
|
+
const store = useStoreContext(NEXT_NAME)
|
|
1253
|
+
const value = useStore((state) => state.value)
|
|
1254
|
+
const steps = useStore((state) => state.steps)
|
|
1255
|
+
|
|
1256
|
+
const propsRef = React.useRef({ onClick: onClickProp })
|
|
1257
|
+
React.useEffect(() => {
|
|
1258
|
+
propsRef.current = { onClick: onClickProp }
|
|
1259
|
+
}, [onClickProp])
|
|
1260
|
+
|
|
1261
|
+
const stepKeys = Array.from(steps.keys())
|
|
1262
|
+
const currentIndex = value ? stepKeys.indexOf(value) : -1
|
|
1263
|
+
const isDisabled = disabled || currentIndex >= stepKeys.length - 1
|
|
1264
|
+
|
|
1265
|
+
const onClick = React.useCallback(
|
|
1266
|
+
async (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
1267
|
+
propsRef.current.onClick?.(event)
|
|
1268
|
+
if (event.defaultPrevented || isDisabled) return
|
|
1269
|
+
|
|
1270
|
+
const nextIndex = Math.min(currentIndex + 1, stepKeys.length - 1)
|
|
1271
|
+
const nextStepValue = stepKeys[nextIndex]
|
|
1272
|
+
|
|
1273
|
+
if (nextStepValue) {
|
|
1274
|
+
await store.setStateWithValidation(nextStepValue, "next")
|
|
1275
|
+
}
|
|
1276
|
+
},
|
|
1277
|
+
[isDisabled, currentIndex, stepKeys, store],
|
|
1278
|
+
)
|
|
1279
|
+
|
|
1280
|
+
const NextPrimitive = asChild ? Slot : "button"
|
|
1281
|
+
|
|
1282
|
+
return (
|
|
1283
|
+
<NextPrimitive
|
|
1284
|
+
type="button"
|
|
1285
|
+
data-slot="stepper-next"
|
|
1286
|
+
disabled={isDisabled}
|
|
1287
|
+
{...nextProps}
|
|
1288
|
+
onClick={onClick}
|
|
1289
|
+
/>
|
|
1290
|
+
)
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
export {
|
|
1294
|
+
Stepper,
|
|
1295
|
+
StepperContent,
|
|
1296
|
+
StepperDescription,
|
|
1297
|
+
StepperIndicator,
|
|
1298
|
+
StepperItem,
|
|
1299
|
+
StepperList,
|
|
1300
|
+
StepperNext,
|
|
1301
|
+
StepperPrev,
|
|
1302
|
+
StepperSeparator,
|
|
1303
|
+
StepperTitle,
|
|
1304
|
+
StepperTrigger,
|
|
1305
|
+
useStore as useStepper,
|
|
1306
|
+
}
|
|
1307
|
+
export type { StepperProps }
|