@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.
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/package.json +45 -0
- package/src/AccessibleIcon.ts +26 -0
- package/src/Accordion.ts +282 -0
- package/src/AlertDialog.ts +110 -0
- package/src/Arrow.ts +21 -0
- package/src/AspectRatio.ts +34 -0
- package/src/Avatar.ts +152 -0
- package/src/Checkbox.ts +325 -0
- package/src/Collapsible.ts +170 -0
- package/src/ContextMenu.ts +303 -0
- package/src/Dialog.ts +308 -0
- package/src/DismissableLayer.ts +413 -0
- package/src/DropdownMenu.ts +275 -0
- package/src/FocusScope.ts +266 -0
- package/src/Form.ts +621 -0
- package/src/HoverCard.ts +318 -0
- package/src/Label.ts +25 -0
- package/src/Menu.ts +1057 -0
- package/src/Menubar.ts +572 -0
- package/src/NavigationMenu.ts +1236 -0
- package/src/OneTimePasswordField.ts +977 -0
- package/src/PasswordToggleField.ts +495 -0
- package/src/Popover.ts +354 -0
- package/src/Popper.ts +401 -0
- package/src/Portal.ts +19 -0
- package/src/Presence.ts +225 -0
- package/src/Primitive.ts +53 -0
- package/src/Progress.ts +92 -0
- package/src/Radio.ts +262 -0
- package/src/RadioGroup.ts +212 -0
- package/src/RovingFocusGroup.ts +259 -0
- package/src/ScrollArea.ts +966 -0
- package/src/Select.ts +1901 -0
- package/src/Separator.ts +36 -0
- package/src/Slider.ts +745 -0
- package/src/Slot.ts +105 -0
- package/src/Switch.ts +278 -0
- package/src/Tabs.ts +177 -0
- package/src/Toast.ts +923 -0
- package/src/Toggle.ts +31 -0
- package/src/ToggleGroup.ts +157 -0
- package/src/Toolbar.ts +130 -0
- package/src/Tooltip.ts +669 -0
- package/src/VisuallyHidden.ts +29 -0
- package/src/collection.ts +97 -0
- package/src/compose-event-handlers.ts +15 -0
- package/src/compose-refs.ts +53 -0
- package/src/context.ts +109 -0
- package/src/direction.ts +19 -0
- package/src/focus-guards.ts +56 -0
- package/src/index.ts +54 -0
- package/src/internal.ts +42 -0
- package/src/scroll-lock.ts +57 -0
- package/src/use-callback-ref.ts +28 -0
- package/src/use-effect-event.ts +36 -0
- package/src/use-is-hydrated.ts +23 -0
- package/src/use-previous.ts +28 -0
- package/src/use-size.ts +57 -0
- package/src/useControllableState.ts +78 -0
- 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 };
|
package/src/Checkbox.ts
ADDED
|
@@ -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 };
|