@octanejs/radix 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +100 -0
  3. package/package.json +45 -0
  4. package/src/AccessibleIcon.ts +26 -0
  5. package/src/Accordion.ts +282 -0
  6. package/src/AlertDialog.ts +110 -0
  7. package/src/Arrow.ts +21 -0
  8. package/src/AspectRatio.ts +34 -0
  9. package/src/Avatar.ts +152 -0
  10. package/src/Checkbox.ts +325 -0
  11. package/src/Collapsible.ts +170 -0
  12. package/src/ContextMenu.ts +303 -0
  13. package/src/Dialog.ts +308 -0
  14. package/src/DismissableLayer.ts +413 -0
  15. package/src/DropdownMenu.ts +275 -0
  16. package/src/FocusScope.ts +266 -0
  17. package/src/Form.ts +621 -0
  18. package/src/HoverCard.ts +318 -0
  19. package/src/Label.ts +25 -0
  20. package/src/Menu.ts +1057 -0
  21. package/src/Menubar.ts +572 -0
  22. package/src/NavigationMenu.ts +1236 -0
  23. package/src/OneTimePasswordField.ts +977 -0
  24. package/src/PasswordToggleField.ts +495 -0
  25. package/src/Popover.ts +354 -0
  26. package/src/Popper.ts +401 -0
  27. package/src/Portal.ts +19 -0
  28. package/src/Presence.ts +225 -0
  29. package/src/Primitive.ts +53 -0
  30. package/src/Progress.ts +92 -0
  31. package/src/Radio.ts +262 -0
  32. package/src/RadioGroup.ts +212 -0
  33. package/src/RovingFocusGroup.ts +259 -0
  34. package/src/ScrollArea.ts +966 -0
  35. package/src/Select.ts +1901 -0
  36. package/src/Separator.ts +36 -0
  37. package/src/Slider.ts +745 -0
  38. package/src/Slot.ts +105 -0
  39. package/src/Switch.ts +278 -0
  40. package/src/Tabs.ts +177 -0
  41. package/src/Toast.ts +923 -0
  42. package/src/Toggle.ts +31 -0
  43. package/src/ToggleGroup.ts +157 -0
  44. package/src/Toolbar.ts +130 -0
  45. package/src/Tooltip.ts +669 -0
  46. package/src/VisuallyHidden.ts +29 -0
  47. package/src/collection.ts +97 -0
  48. package/src/compose-event-handlers.ts +15 -0
  49. package/src/compose-refs.ts +53 -0
  50. package/src/context.ts +109 -0
  51. package/src/direction.ts +19 -0
  52. package/src/focus-guards.ts +56 -0
  53. package/src/index.ts +54 -0
  54. package/src/internal.ts +42 -0
  55. package/src/scroll-lock.ts +57 -0
  56. package/src/use-callback-ref.ts +28 -0
  57. package/src/use-effect-event.ts +36 -0
  58. package/src/use-is-hydrated.ts +23 -0
  59. package/src/use-previous.ts +28 -0
  60. package/src/use-size.ts +57 -0
  61. package/src/useControllableState.ts +78 -0
  62. package/src/useId.ts +15 -0
package/src/Avatar.ts ADDED
@@ -0,0 +1,152 @@
1
+ // Ported from @radix-ui/react-avatar (source:
2
+ // .radix-primitives/packages/react/avatar/src/avatar.tsx). Image renders only once its
3
+ // src has LOADED (probed off-DOM via `new Image()`); Fallback renders (optionally after
4
+ // `delayMs`) whenever the image hasn't loaded. The dev-only multiple-Image warning
5
+ // machinery is not ported (repo policy: skip React's dev-warning surface).
6
+ import {
7
+ createElement,
8
+ useEffect,
9
+ useEffectEvent,
10
+ useLayoutEffect,
11
+ useRef,
12
+ useState,
13
+ } from 'octane';
14
+
15
+ import { createContextScope } from './context';
16
+ import { S, subSlot } from './internal';
17
+ import { Primitive } from './Primitive';
18
+
19
+ type ImageLoadingStatus = 'idle' | 'loading' | 'loaded' | 'error';
20
+
21
+ const [createAvatarContext, createAvatarScope] = createContextScope('Avatar');
22
+ export { createAvatarScope };
23
+ const [AvatarProvider, useAvatarContext] = createAvatarContext<{
24
+ imageLoadingStatus: ImageLoadingStatus;
25
+ setImageLoadingStatus: (s: ImageLoadingStatus) => void;
26
+ }>('Avatar');
27
+
28
+ export function Root(props: any): any {
29
+ const slot = S('Avatar.Root');
30
+ const { __scopeAvatar, ...avatarProps } = props ?? {};
31
+ const [imageLoadingStatus, setImageLoadingStatus] = useState<ImageLoadingStatus>(
32
+ 'idle',
33
+ subSlot(slot, 'status'),
34
+ );
35
+ return createElement(AvatarProvider, {
36
+ scope: __scopeAvatar,
37
+ imageLoadingStatus,
38
+ setImageLoadingStatus,
39
+ children: createElement(Primitive.span, avatarProps),
40
+ });
41
+ }
42
+
43
+ export function Image(props: any): any {
44
+ const slot = S('Avatar.Image');
45
+ const { __scopeAvatar, src, onLoadingStatusChange, ...imageProps } = props ?? {};
46
+ const context = useAvatarContext('AvatarImage', __scopeAvatar);
47
+ const imageLoadingStatus = useImageLoadingStatus(
48
+ src,
49
+ {
50
+ referrerPolicy: imageProps.referrerPolicy,
51
+ crossOrigin: imageProps.crossOrigin,
52
+ loadingStatus: context.imageLoadingStatus,
53
+ setLoadingStatus: context.setImageLoadingStatus,
54
+ },
55
+ slot,
56
+ );
57
+ const handleLoadingStatusChange = useEffectEvent(
58
+ (status: ImageLoadingStatus) => {
59
+ onLoadingStatusChange?.(status);
60
+ },
61
+ subSlot(slot, 'cb'),
62
+ );
63
+ const loadingStatusRef = useRef<ImageLoadingStatus>(imageLoadingStatus, subSlot(slot, 'prev'));
64
+ useLayoutEffect(
65
+ () => {
66
+ const previousLoadingStatus = loadingStatusRef.current;
67
+ loadingStatusRef.current = imageLoadingStatus;
68
+ if (imageLoadingStatus !== previousLoadingStatus) {
69
+ handleLoadingStatusChange(imageLoadingStatus);
70
+ }
71
+ },
72
+ [imageLoadingStatus],
73
+ subSlot(slot, 'e:change'),
74
+ );
75
+ return imageLoadingStatus === 'loaded'
76
+ ? createElement(Primitive.img, { ...imageProps, src })
77
+ : null;
78
+ }
79
+
80
+ export function Fallback(props: any): any {
81
+ const slot = S('Avatar.Fallback');
82
+ const { __scopeAvatar, delayMs, ...fallbackProps } = props ?? {};
83
+ const context = useAvatarContext('AvatarFallback', __scopeAvatar);
84
+ const [canRender, setCanRender] = useState(delayMs === undefined, subSlot(slot, 'can'));
85
+ useEffect(
86
+ () => {
87
+ if (delayMs !== undefined) {
88
+ const timerId = window.setTimeout(() => setCanRender(true), delayMs);
89
+ return () => window.clearTimeout(timerId);
90
+ }
91
+ },
92
+ [delayMs],
93
+ subSlot(slot, 'e:delay'),
94
+ );
95
+ return canRender && context.imageLoadingStatus !== 'loaded'
96
+ ? createElement(Primitive.span, fallbackProps)
97
+ : null;
98
+ }
99
+
100
+ function useImageLoadingStatus(
101
+ src: string | undefined,
102
+ {
103
+ loadingStatus,
104
+ setLoadingStatus,
105
+ referrerPolicy,
106
+ crossOrigin,
107
+ }: {
108
+ referrerPolicy?: string;
109
+ crossOrigin?: string;
110
+ loadingStatus: ImageLoadingStatus;
111
+ setLoadingStatus: (s: ImageLoadingStatus) => void;
112
+ },
113
+ slot: symbol,
114
+ ): ImageLoadingStatus {
115
+ useLayoutEffect(
116
+ () => {
117
+ if (!src) {
118
+ setLoadingStatus('error');
119
+ return;
120
+ }
121
+ const image = new window.Image();
122
+ const handleLoad = (event: Event): void => {
123
+ const img = event.currentTarget as HTMLImageElement;
124
+ setLoadingStatus(getImageLoadingStatus(img));
125
+ };
126
+ const handleError = (): void => setLoadingStatus('error');
127
+ image.addEventListener('load', handleLoad);
128
+ image.addEventListener('error', handleError);
129
+ if (referrerPolicy) {
130
+ (image as any).referrerPolicy = referrerPolicy;
131
+ }
132
+ image.crossOrigin = crossOrigin ?? null;
133
+ image.src = src;
134
+
135
+ setLoadingStatus(getImageLoadingStatus(image));
136
+ return () => {
137
+ image.removeEventListener('load', handleLoad);
138
+ image.removeEventListener('error', handleError);
139
+ setLoadingStatus('idle');
140
+ };
141
+ },
142
+ [src, crossOrigin, referrerPolicy],
143
+ subSlot(slot, 'e:load'),
144
+ );
145
+ return loadingStatus;
146
+ }
147
+
148
+ function getImageLoadingStatus(image: HTMLImageElement): ImageLoadingStatus {
149
+ return image.complete ? (image.naturalWidth > 0 ? 'loaded' : 'error') : 'loading';
150
+ }
151
+
152
+ export { Root as Avatar, Image as AvatarImage, Fallback as AvatarFallback };
@@ -0,0 +1,325 @@
1
+ // Ported from @radix-ui/react-checkbox (source:
2
+ // .radix-primitives/packages/react/checkbox/src/checkbox.tsx). A `role=checkbox` button
3
+ // (with `indeterminate` support) that — when inside a form — renders a hidden native
4
+ // checkbox "bubble input" so native form machinery (FormData, validation, reset,
5
+ // change listeners) reflects the state. The bubble input is uncontrolled; state syncs
6
+ // imperatively via the native `checked` property setter + dispatched events.
7
+ //
8
+ // octane adaptations (documented; both stem from octane's native-event /
9
+ // no-controlled-inputs model — see docs/react-parity-migration-plan.md):
10
+ // - React's synthetic `event.isPropagationStopped()` → native `event.cancelBubble`.
11
+ // - `defaultChecked` prop → the native `checked` ATTRIBUTE (HTML's default-state
12
+ // semantics; live state is driven through the property setter, which never touches
13
+ // the attribute).
14
+ // - The bubble effect ALSO dispatches a native `change` event after the source's
15
+ // `click`: React forms observe checkbox clicks through synthetic `onChange`, which
16
+ // octane doesn't have — a native bubbling `change` gives octane `<form onChange>`
17
+ // the same functional outcome.
18
+ import { createElement, useEffect, useRef, useState } from 'octane';
19
+
20
+ import { composeEventHandlers } from './compose-event-handlers';
21
+ import { useComposedRefs } from './compose-refs';
22
+ import { createContextScope } from './context';
23
+ import { S, subSlot } from './internal';
24
+ import { Presence } from './Presence';
25
+ import { Primitive } from './Primitive';
26
+ import { usePrevious } from './use-previous';
27
+ import { useSize } from './use-size';
28
+ import { useControllableState } from './useControllableState';
29
+
30
+ const CHECKBOX_NAME = 'Checkbox';
31
+
32
+ const [createCheckboxContext, createCheckboxScope] = createContextScope(CHECKBOX_NAME);
33
+ export { createCheckboxScope };
34
+
35
+ export type CheckedState = boolean | 'indeterminate';
36
+
37
+ interface CheckboxContextValue {
38
+ checked: CheckedState;
39
+ setChecked: (checked: CheckedState | ((prev: CheckedState) => CheckedState)) => void;
40
+ disabled: boolean | undefined;
41
+ control: HTMLElement | null;
42
+ setControl: (control: HTMLElement | null) => void;
43
+ name: string | undefined;
44
+ form: string | undefined;
45
+ value: string | number;
46
+ hasConsumerStoppedPropagationRef: { current: boolean };
47
+ required: boolean | undefined;
48
+ defaultChecked: boolean | undefined;
49
+ isFormControl: boolean;
50
+ bubbleInput: HTMLInputElement | null;
51
+ setBubbleInput: (input: HTMLInputElement | null) => void;
52
+ }
53
+
54
+ const [CheckboxProviderImpl, useCheckboxContext] =
55
+ createCheckboxContext<CheckboxContextValue>(CHECKBOX_NAME);
56
+
57
+ export function Provider(props: any): any {
58
+ const slot = S('Checkbox.Provider');
59
+ const {
60
+ __scopeCheckbox,
61
+ checked: checkedProp,
62
+ children,
63
+ defaultChecked,
64
+ disabled,
65
+ form,
66
+ name,
67
+ onCheckedChange,
68
+ required,
69
+ value = 'on',
70
+ internal_do_not_use_render,
71
+ } = props ?? {};
72
+
73
+ const [checked, setChecked] = useControllableState<CheckedState>(
74
+ { prop: checkedProp, defaultProp: defaultChecked ?? false, onChange: onCheckedChange },
75
+ subSlot(slot, 'checked'),
76
+ );
77
+ const [control, setControl] = useState<HTMLElement | null>(null, subSlot(slot, 'control'));
78
+ const [bubbleInput, setBubbleInput] = useState<HTMLInputElement | null>(
79
+ null,
80
+ subSlot(slot, 'input'),
81
+ );
82
+ const hasConsumerStoppedPropagationRef = useRef(false, subSlot(slot, 'stopped'));
83
+ const isFormControl = control
84
+ ? !!form || !!control.closest('form')
85
+ : // We set this to true by default so that events bubble to forms without JS (SSR)
86
+ true;
87
+
88
+ const context: CheckboxContextValue = {
89
+ checked,
90
+ disabled,
91
+ setChecked,
92
+ control,
93
+ setControl,
94
+ name,
95
+ form,
96
+ value,
97
+ hasConsumerStoppedPropagationRef,
98
+ required,
99
+ defaultChecked: isIndeterminate(defaultChecked) ? false : defaultChecked,
100
+ isFormControl,
101
+ bubbleInput,
102
+ setBubbleInput,
103
+ };
104
+
105
+ return createElement(CheckboxProviderImpl, {
106
+ scope: __scopeCheckbox,
107
+ ...context,
108
+ children: isFunction(internal_do_not_use_render)
109
+ ? internal_do_not_use_render(context)
110
+ : children,
111
+ });
112
+ }
113
+
114
+ export function Trigger(props: any): any {
115
+ const slot = S('Checkbox.Trigger');
116
+ const { __scopeCheckbox, onKeyDown, onClick, ref: forwardedRef, ...checkboxProps } = props ?? {};
117
+ const {
118
+ control,
119
+ value,
120
+ disabled,
121
+ checked,
122
+ required,
123
+ setControl,
124
+ setChecked,
125
+ hasConsumerStoppedPropagationRef,
126
+ isFormControl,
127
+ bubbleInput,
128
+ } = useCheckboxContext('CheckboxTrigger', __scopeCheckbox);
129
+ const composedRefs = useComposedRefs(forwardedRef, setControl, subSlot(slot, 'refs'));
130
+
131
+ const initialCheckedStateRef = useRef(checked, subSlot(slot, 'initial'));
132
+ useEffect(
133
+ () => {
134
+ const form = (control as HTMLButtonElement | null)?.form;
135
+ if (form) {
136
+ const reset = (): void => setChecked(initialCheckedStateRef.current);
137
+ form.addEventListener('reset', reset);
138
+ return () => form.removeEventListener('reset', reset);
139
+ }
140
+ },
141
+ [control, setChecked],
142
+ subSlot(slot, 'e:reset'),
143
+ );
144
+
145
+ return createElement(Primitive.button, {
146
+ type: 'button',
147
+ role: 'checkbox',
148
+ 'aria-checked': isIndeterminate(checked) ? 'mixed' : checked,
149
+ 'aria-required': required,
150
+ 'data-state': getState(checked),
151
+ 'data-disabled': disabled ? '' : undefined,
152
+ disabled,
153
+ value,
154
+ ...checkboxProps,
155
+ ref: composedRefs,
156
+ onKeyDown: composeEventHandlers(onKeyDown, (event: KeyboardEvent) => {
157
+ // According to WAI ARIA, Checkboxes don't activate on enter keypress
158
+ if (event.key === 'Enter') event.preventDefault();
159
+ }),
160
+ onClick: composeEventHandlers(onClick, (event: MouseEvent) => {
161
+ setChecked((prevChecked) => (isIndeterminate(prevChecked) ? true : !prevChecked));
162
+ if (bubbleInput && isFormControl) {
163
+ hasConsumerStoppedPropagationRef.current = event.cancelBubble;
164
+ // if checkbox has a bubble input and is a form control, stop
165
+ // propagation from the button so that we only propagate one click
166
+ // event (from the input). We propagate changes from an input so
167
+ // that native form validation works and form events reflect
168
+ // checkbox updates.
169
+ if (!hasConsumerStoppedPropagationRef.current) event.stopPropagation();
170
+ }
171
+ }),
172
+ });
173
+ }
174
+
175
+ export function Root(props: any): any {
176
+ const {
177
+ __scopeCheckbox,
178
+ name,
179
+ checked,
180
+ defaultChecked,
181
+ required,
182
+ disabled,
183
+ value,
184
+ onCheckedChange,
185
+ form,
186
+ ref: forwardedRef,
187
+ ...checkboxProps
188
+ } = props ?? {};
189
+
190
+ return createElement(Provider, {
191
+ __scopeCheckbox,
192
+ checked,
193
+ defaultChecked,
194
+ disabled,
195
+ required,
196
+ onCheckedChange,
197
+ name,
198
+ form,
199
+ value,
200
+ internal_do_not_use_render: ({ isFormControl }: CheckboxContextValue) => [
201
+ createElement(Trigger, {
202
+ key: 'trigger',
203
+ ...checkboxProps,
204
+ ref: forwardedRef,
205
+ __scopeCheckbox,
206
+ }),
207
+ isFormControl ? createElement(BubbleInput, { key: 'bubble', __scopeCheckbox }) : null,
208
+ ],
209
+ });
210
+ }
211
+
212
+ export function Indicator(props: any): any {
213
+ const { __scopeCheckbox, forceMount, ...indicatorProps } = props ?? {};
214
+ const context = useCheckboxContext('CheckboxIndicator', __scopeCheckbox);
215
+ return createElement(Presence, {
216
+ present: forceMount || isIndeterminate(context.checked) || context.checked === true,
217
+ children: createElement(Primitive.span, {
218
+ 'data-state': getState(context.checked),
219
+ 'data-disabled': context.disabled ? '' : undefined,
220
+ ...indicatorProps,
221
+ style: { pointerEvents: 'none', ...props?.style },
222
+ }),
223
+ });
224
+ }
225
+
226
+ export function BubbleInput(props: any): any {
227
+ const slot = S('Checkbox.BubbleInput');
228
+ const { __scopeCheckbox, ref: forwardedRef, ...inputProps } = props ?? {};
229
+ const {
230
+ control,
231
+ hasConsumerStoppedPropagationRef,
232
+ checked,
233
+ defaultChecked,
234
+ required,
235
+ disabled,
236
+ name,
237
+ value,
238
+ form,
239
+ bubbleInput,
240
+ setBubbleInput,
241
+ } = useCheckboxContext('CheckboxBubbleInput', __scopeCheckbox);
242
+
243
+ const composedRefs = useComposedRefs(forwardedRef, setBubbleInput, subSlot(slot, 'refs'));
244
+ const prevChecked = usePrevious(checked, subSlot(slot, 'prev'));
245
+ const controlSize = useSize(control, subSlot(slot, 'size'));
246
+
247
+ // Bubble checked change to parents (e.g form change event)
248
+ useEffect(
249
+ () => {
250
+ const input = bubbleInput;
251
+ if (!input) return;
252
+
253
+ const inputProto = window.HTMLInputElement.prototype;
254
+ const descriptor = Object.getOwnPropertyDescriptor(
255
+ inputProto,
256
+ 'checked',
257
+ ) as PropertyDescriptor;
258
+ const setChecked = descriptor.set;
259
+
260
+ const bubbles = !hasConsumerStoppedPropagationRef.current;
261
+ if (prevChecked !== checked && setChecked) {
262
+ input.indeterminate = isIndeterminate(checked);
263
+ setChecked.call(input, isIndeterminate(checked) ? false : checked);
264
+ input.dispatchEvent(new Event('click', { bubbles }));
265
+ // octane adaptation: also fire the native `change` React's synthetic
266
+ // onChange would have synthesised from the click (see file header).
267
+ input.dispatchEvent(new Event('change', { bubbles }));
268
+ }
269
+ },
270
+ [bubbleInput, prevChecked, checked, hasConsumerStoppedPropagationRef],
271
+ subSlot(slot, 'e:bubble'),
272
+ );
273
+
274
+ const defaultCheckedRef = useRef(
275
+ isIndeterminate(checked) ? false : checked,
276
+ subSlot(slot, 'default'),
277
+ );
278
+ return createElement(Primitive.input, {
279
+ type: 'checkbox',
280
+ 'aria-hidden': true,
281
+ // octane: the native `checked` ATTRIBUTE is HTML's default-checked state (there is
282
+ // no `defaultChecked` prop outside React's controlled model).
283
+ checked: (defaultChecked ?? defaultCheckedRef.current) || undefined,
284
+ required,
285
+ disabled,
286
+ name,
287
+ value,
288
+ form,
289
+ ...inputProps,
290
+ tabIndex: -1,
291
+ ref: composedRefs,
292
+ style: {
293
+ ...props?.style,
294
+ ...controlSize,
295
+ position: 'absolute',
296
+ pointerEvents: 'none',
297
+ opacity: 0,
298
+ margin: 0,
299
+ // We transform because the input is absolutely positioned but we have
300
+ // rendered it **after** the button. This pulls it back to sit on top
301
+ // of the button.
302
+ transform: 'translateX(-100%)',
303
+ },
304
+ });
305
+ }
306
+
307
+ function isFunction(value: unknown): value is (...args: any[]) => any {
308
+ return typeof value === 'function';
309
+ }
310
+
311
+ function isIndeterminate(checked?: CheckedState | boolean): checked is 'indeterminate' {
312
+ return checked === 'indeterminate';
313
+ }
314
+
315
+ function getState(checked: CheckedState): string {
316
+ return isIndeterminate(checked) ? 'indeterminate' : checked ? 'checked' : 'unchecked';
317
+ }
318
+
319
+ export {
320
+ Root as Checkbox,
321
+ Provider as CheckboxProvider,
322
+ Trigger as CheckboxTrigger,
323
+ Indicator as CheckboxIndicator,
324
+ BubbleInput as CheckboxBubbleInput,
325
+ };
@@ -0,0 +1,170 @@
1
+ // Ported from @radix-ui/react-collapsible. An open/closed disclosure: Root owns the
2
+ // controllable `open` state + shares it via context; Trigger toggles it + carries ARIA;
3
+ // Content stays mounted through its exit animation via `Presence` and exposes the
4
+ // `--radix-collapsible-content-height/-width` CSS vars for animating. `.ts` components via
5
+ // createElement; ref-as-prop.
6
+ import { createElement, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'octane';
7
+
8
+ import { composeEventHandlers } from './compose-event-handlers';
9
+ import { useComposedRefs } from './compose-refs';
10
+ import { createContextScope } from './context';
11
+ import { S, subSlot } from './internal';
12
+ import { Presence } from './Presence';
13
+ import { Primitive } from './Primitive';
14
+ import { useControllableState } from './useControllableState';
15
+ import { useId } from './useId';
16
+
17
+ interface CollapsibleContextValue {
18
+ contentId: string;
19
+ disabled?: boolean;
20
+ open: boolean;
21
+ onOpenToggle: () => void;
22
+ }
23
+
24
+ // Scoped context — `createCollapsibleScope` lets a composing primitive (Accordion) thread
25
+ // its own Collapsible scope so the two don't collide.
26
+ export const [createCollapsibleContext, createCollapsibleScope] = createContextScope('Collapsible');
27
+ const [CollapsibleProvider, useCollapsibleContext] =
28
+ createCollapsibleContext<CollapsibleContextValue>('Collapsible');
29
+
30
+ function getState(open?: boolean): 'open' | 'closed' {
31
+ return open ? 'open' : 'closed';
32
+ }
33
+
34
+ export function Root(props: any): any {
35
+ const slot = S('Collapsible.Root');
36
+ const {
37
+ __scopeCollapsible,
38
+ open: openProp,
39
+ defaultOpen,
40
+ disabled,
41
+ onOpenChange,
42
+ ...rest
43
+ } = props ?? {};
44
+
45
+ const [open, setOpen] = useControllableState<boolean>(
46
+ { prop: openProp, defaultProp: defaultOpen ?? false, onChange: onOpenChange },
47
+ subSlot(slot, 'open'),
48
+ );
49
+ const contentId = useId(subSlot(slot, 'id'));
50
+ const onOpenToggle = useCallback(
51
+ () => setOpen((prev) => !prev),
52
+ [setOpen],
53
+ subSlot(slot, 'toggle'),
54
+ );
55
+
56
+ return createElement(CollapsibleProvider, {
57
+ scope: __scopeCollapsible,
58
+ contentId,
59
+ disabled,
60
+ open,
61
+ onOpenToggle,
62
+ children: createElement(Primitive.div, {
63
+ 'data-state': getState(open),
64
+ 'data-disabled': disabled ? '' : undefined,
65
+ ...rest,
66
+ }),
67
+ });
68
+ }
69
+
70
+ export function Trigger(props: any): any {
71
+ const { __scopeCollapsible, ...rest } = props ?? {};
72
+ const context = useCollapsibleContext('CollapsibleTrigger', __scopeCollapsible);
73
+ return createElement(Primitive.button, {
74
+ type: 'button',
75
+ // Radix parity: only reference the content while it's expanded.
76
+ 'aria-controls': context.open ? context.contentId : undefined,
77
+ 'aria-expanded': context.open || false,
78
+ 'data-state': getState(context.open),
79
+ 'data-disabled': context.disabled ? '' : undefined,
80
+ disabled: context.disabled,
81
+ ...rest,
82
+ onClick: composeEventHandlers(rest.onClick, context.onOpenToggle),
83
+ });
84
+ }
85
+
86
+ export function Content(props: any): any {
87
+ const { __scopeCollapsible, forceMount, ...contentProps } = props ?? {};
88
+ const context = useCollapsibleContext('CollapsibleContent', __scopeCollapsible);
89
+ return createElement(Presence, {
90
+ present: forceMount || context.open,
91
+ children: ({ present }: { present: boolean }) =>
92
+ createElement(ContentImpl, { ...contentProps, __scopeCollapsible, present }),
93
+ });
94
+ }
95
+
96
+ function ContentImpl(props: any): any {
97
+ const slot = S('Collapsible.ContentImpl');
98
+ const {
99
+ __scopeCollapsible,
100
+ present,
101
+ children,
102
+ ref: forwardedRef,
103
+ style,
104
+ ...contentProps
105
+ } = props;
106
+ const context = useCollapsibleContext('CollapsibleContent', __scopeCollapsible);
107
+
108
+ const [isPresent, setIsPresent] = useState<boolean>(present, subSlot(slot, 'present'));
109
+ const rRef = useRef<HTMLElement | null>(null, subSlot(slot, 'ref'));
110
+ const composedRefs = useComposedRefs(forwardedRef, rRef, subSlot(slot, 'refs'));
111
+ const heightRef = useRef(0, subSlot(slot, 'h'));
112
+ const widthRef = useRef(0, subSlot(slot, 'w'));
113
+ const isOpen = context.open || isPresent;
114
+ const isMountAnimationPreventedRef = useRef(isOpen, subSlot(slot, 'prevent'));
115
+ const originalStylesRef = useRef<any>(undefined, subSlot(slot, 'orig'));
116
+
117
+ useEffect(
118
+ () => {
119
+ const raf = requestAnimationFrame(() => (isMountAnimationPreventedRef.current = false));
120
+ return () => cancelAnimationFrame(raf);
121
+ },
122
+ [],
123
+ subSlot(slot, 'e:mount'),
124
+ );
125
+
126
+ useLayoutEffect(
127
+ () => {
128
+ const node = rRef.current;
129
+ if (node) {
130
+ originalStylesRef.current = originalStylesRef.current || {
131
+ transitionDuration: node.style.transitionDuration,
132
+ animationName: node.style.animationName,
133
+ };
134
+ // Block the animation while measuring the natural size.
135
+ node.style.transitionDuration = '0s';
136
+ node.style.animationName = 'none';
137
+ const rect = node.getBoundingClientRect();
138
+ heightRef.current = rect.height;
139
+ widthRef.current = rect.width;
140
+ if (!isMountAnimationPreventedRef.current) {
141
+ node.style.transitionDuration = originalStylesRef.current.transitionDuration;
142
+ node.style.animationName = originalStylesRef.current.animationName;
143
+ }
144
+ setIsPresent(present);
145
+ }
146
+ },
147
+ [context.open, present],
148
+ subSlot(slot, 'e:measure'),
149
+ );
150
+
151
+ const height = heightRef.current;
152
+ const width = widthRef.current;
153
+ return createElement(Primitive.div, {
154
+ 'data-state': getState(context.open),
155
+ 'data-disabled': context.disabled ? '' : undefined,
156
+ id: context.contentId,
157
+ hidden: !isOpen,
158
+ ...contentProps,
159
+ ref: composedRefs,
160
+ style: {
161
+ '--radix-collapsible-content-height': height ? `${height}px` : undefined,
162
+ '--radix-collapsible-content-width': width ? `${width}px` : undefined,
163
+ ...style,
164
+ },
165
+ children: isOpen ? children : null,
166
+ });
167
+ }
168
+
169
+ export { Root as Collapsible };
170
+ export { useCollapsibleContext };