@octanejs/base-ui 0.1.1

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 (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +33 -0
  3. package/package.json +47 -0
  4. package/src/avatar.ts +308 -0
  5. package/src/checkbox-group.ts +183 -0
  6. package/src/checkbox.ts +578 -0
  7. package/src/field.ts +676 -0
  8. package/src/fieldset.ts +150 -0
  9. package/src/form.ts +183 -0
  10. package/src/index.ts +23 -0
  11. package/src/input.ts +9 -0
  12. package/src/internal.ts +42 -0
  13. package/src/merge-props.ts +2 -0
  14. package/src/meter.ts +204 -0
  15. package/src/number-field.ts +978 -0
  16. package/src/progress.ts +260 -0
  17. package/src/radio-group.ts +285 -0
  18. package/src/radio.ts +401 -0
  19. package/src/separator.ts +43 -0
  20. package/src/slider.ts +1617 -0
  21. package/src/switch.ts +374 -0
  22. package/src/toggle-group.ts +136 -0
  23. package/src/toggle.ts +145 -0
  24. package/src/use-render.ts +40 -0
  25. package/src/utils/CheckboxGroupContext.ts +30 -0
  26. package/src/utils/CompositeRootContext.ts +27 -0
  27. package/src/utils/DirectionContext.ts +15 -0
  28. package/src/utils/RadioGroupContext.ts +27 -0
  29. package/src/utils/ToggleGroupContext.ts +33 -0
  30. package/src/utils/addEventListener.ts +13 -0
  31. package/src/utils/areArraysEqual.ts +11 -0
  32. package/src/utils/clamp.ts +8 -0
  33. package/src/utils/composeRefs.ts +53 -0
  34. package/src/utils/composite/CompositeItem.ts +56 -0
  35. package/src/utils/composite/CompositeList.ts +190 -0
  36. package/src/utils/composite/CompositeListContext.ts +23 -0
  37. package/src/utils/composite/CompositeRoot.ts +132 -0
  38. package/src/utils/composite/keys.ts +179 -0
  39. package/src/utils/composite/list-utils.ts +78 -0
  40. package/src/utils/composite/useCompositeItem.ts +58 -0
  41. package/src/utils/composite/useCompositeListItem.ts +108 -0
  42. package/src/utils/composite/useCompositeRoot.ts +346 -0
  43. package/src/utils/contains.ts +22 -0
  44. package/src/utils/createChangeEventDetails.ts +81 -0
  45. package/src/utils/field/FieldItemContext.ts +13 -0
  46. package/src/utils/field/FieldRootContext.ts +98 -0
  47. package/src/utils/field/FormContext.ts +37 -0
  48. package/src/utils/field/LabelableContext.ts +32 -0
  49. package/src/utils/field/LabelableProvider.ts +108 -0
  50. package/src/utils/field/constants.ts +80 -0
  51. package/src/utils/field/getCombinedFieldValidityData.ts +16 -0
  52. package/src/utils/field/useAriaLabelledBy.ts +86 -0
  53. package/src/utils/field/useFieldControlRegistration.ts +179 -0
  54. package/src/utils/field/useFieldValidation.ts +311 -0
  55. package/src/utils/field/useLabel.ts +95 -0
  56. package/src/utils/field/useLabelableId.ts +99 -0
  57. package/src/utils/field/useRegisterFieldControl.ts +41 -0
  58. package/src/utils/formatNumber.ts +44 -0
  59. package/src/utils/getDefaultFormSubmitter.ts +19 -0
  60. package/src/utils/getElementRef.ts +13 -0
  61. package/src/utils/getStateAttributesProps.ts +34 -0
  62. package/src/utils/matchesFocusVisible.ts +16 -0
  63. package/src/utils/mergeObjects.ts +18 -0
  64. package/src/utils/mergeProps.ts +206 -0
  65. package/src/utils/noop.ts +4 -0
  66. package/src/utils/number/constants.ts +4 -0
  67. package/src/utils/number/parse.ts +227 -0
  68. package/src/utils/number/types.ts +25 -0
  69. package/src/utils/number/validate.ts +129 -0
  70. package/src/utils/owner.ts +9 -0
  71. package/src/utils/platform.ts +15 -0
  72. package/src/utils/resolveAriaLabelledBy.ts +11 -0
  73. package/src/utils/resolveClassName.ts +8 -0
  74. package/src/utils/resolveRef.ts +10 -0
  75. package/src/utils/resolveStyle.ts +10 -0
  76. package/src/utils/serializeValue.ts +15 -0
  77. package/src/utils/slider/asc.ts +4 -0
  78. package/src/utils/slider/getMidpoint.ts +14 -0
  79. package/src/utils/slider/getPushedThumbValues.ts +73 -0
  80. package/src/utils/slider/getSliderValue.ts +27 -0
  81. package/src/utils/slider/replaceArrayItemAtIndex.ts +8 -0
  82. package/src/utils/slider/resolveThumbCollision.ts +178 -0
  83. package/src/utils/slider/roundValueToStep.ts +22 -0
  84. package/src/utils/slider/validateMinimumDistance.ts +22 -0
  85. package/src/utils/slider/valueArrayToPercentages.ts +11 -0
  86. package/src/utils/stringifyLocale.ts +10 -0
  87. package/src/utils/useAnimationFrame.ts +59 -0
  88. package/src/utils/useAnimationsFinished.ts +100 -0
  89. package/src/utils/useBaseUiId.ts +19 -0
  90. package/src/utils/useButton.ts +217 -0
  91. package/src/utils/useCheckboxGroupParent.ts +113 -0
  92. package/src/utils/useControlled.ts +42 -0
  93. package/src/utils/useFocusableWhenDisabled.ts +84 -0
  94. package/src/utils/useForcedRerendering.ts +11 -0
  95. package/src/utils/useIsHydrating.ts +9 -0
  96. package/src/utils/useOpenChangeComplete.ts +46 -0
  97. package/src/utils/usePressAndHold.ts +35 -0
  98. package/src/utils/useRefWithInit.ts +23 -0
  99. package/src/utils/useRegisteredLabelId.ts +32 -0
  100. package/src/utils/useRenderElement.ts +165 -0
  101. package/src/utils/useStableCallback.ts +26 -0
  102. package/src/utils/useTimeout.ts +51 -0
  103. package/src/utils/useTransitionStatus.ts +115 -0
  104. package/src/utils/useValueAsRef.ts +25 -0
  105. package/src/utils/useValueChanged.ts +37 -0
  106. package/src/utils/valueToPercent.ts +4 -0
  107. package/src/utils/visuallyHidden.ts +24 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dominic Gannaway
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # @octanejs/base-ui
2
+
3
+ [Base UI](https://base-ui.com) (`@base-ui/react`) ported to the
4
+ [octane](https://github.com/octanejs/octane) renderer — headless, accessible, unstyled
5
+ UI primitives.
6
+
7
+ Alpha, in progress. Ported at full fidelity from the pinned `mui/base-ui` source
8
+ (`v1.6.0`), proven by differential parity tests against the real
9
+ `@base-ui/react`. See `docs/base-ui-migration-plan.md` for the phased plan
10
+ and progress.
11
+
12
+ ## API
13
+
14
+ Mirrors Base UI's deep-subpath imports:
15
+
16
+ ```ts
17
+ import { Separator } from '@octanejs/base-ui/separator';
18
+ import { useRender } from '@octanejs/base-ui/use-render';
19
+ import { mergeProps } from '@octanejs/base-ui/merge-props';
20
+ ```
21
+
22
+ Every component takes Base UI's universal composition props — `render` (a JSX element or
23
+ `(props, state) => element`), `className` (string or `(state) => string`), and `style`
24
+ (object or `(state) => object`) — routed through `useRenderElement`.
25
+
26
+ ## Intentional divergences from Base UI (React)
27
+
28
+ - **Native events, not synthetic.** Handlers receive native DOM events (octane delegates
29
+ natively). `event.preventBaseUIHandler()` still works — the shim is attached to the
30
+ native event.
31
+ - **ref-as-prop.** No `forwardRef`; `ref` is a normal prop (React-19 shape).
32
+ - **`className` composition** follows octane's `normalizeClass` at the apply site; the
33
+ `render`-prop merge concatenates strings exactly like Base UI.
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@octanejs/base-ui",
3
+ "version": "0.1.1",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "description": "Base UI (@base-ui/react) for the octane renderer — headless, accessible, unstyled UI primitives ported from base-ui.com at full fidelity.",
7
+ "author": {
8
+ "name": "Dominic Gannaway",
9
+ "email": "dg@domgan.com"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/octanejs/octane.git",
17
+ "directory": "packages/base-ui"
18
+ },
19
+ "main": "src/index.ts",
20
+ "module": "src/index.ts",
21
+ "types": "src/index.ts",
22
+ "files": [
23
+ "src",
24
+ "README.md"
25
+ ],
26
+ "exports": {
27
+ ".": "./src/index.ts",
28
+ "./*": "./src/*.ts"
29
+ },
30
+ "dependencies": {
31
+ "aria-hidden": "^1.2.6",
32
+ "tabbable": "^6.5.0",
33
+ "@octanejs/floating-ui": "0.1.2",
34
+ "octane": "0.1.3"
35
+ },
36
+ "devDependencies": {
37
+ "@base-ui/react": "^1.6.0",
38
+ "@tsrx/react": "^0.2.33",
39
+ "esbuild": "^0.28.1",
40
+ "react": "^19.2.0",
41
+ "react-dom": "^19.2.0",
42
+ "vitest": "^4.1.9"
43
+ },
44
+ "scripts": {
45
+ "test": "vitest run"
46
+ }
47
+ }
package/src/avatar.ts ADDED
@@ -0,0 +1,308 @@
1
+ // Ported from .base-ui/packages/react/src/avatar/ (v1.6.0): root/AvatarRoot,
2
+ // root/AvatarRootContext, root/stateAttributesMapping, image/AvatarImage,
3
+ // image/useImageLoadingStatus, fallback/AvatarFallback — plus its `index.parts` (the
4
+ // `Avatar` namespace).
5
+ //
6
+ // Displays a profile image with a graceful fallback. `Avatar.Root` (`<span>`) tracks the
7
+ // image load status via context; `Avatar.Image` (`<img>`) loads off-DOM and only mounts once
8
+ // loaded (through the transition system); `Avatar.Fallback` (`<span>`) shows until then.
9
+ // Base UI uses a PLAIN React context — ported as a plain octane createContext.
10
+ import {
11
+ createContext,
12
+ createElement,
13
+ useContext,
14
+ useEffect,
15
+ useLayoutEffect,
16
+ useMemo,
17
+ useRef,
18
+ useState,
19
+ } from 'octane';
20
+
21
+ import { S, splitSlot, subSlot } from './internal';
22
+ import { useRenderElement, type RenderProp } from './utils/useRenderElement';
23
+ import type { StateAttributesMapping } from './utils/getStateAttributesProps';
24
+ import { useStableCallback } from './utils/useStableCallback';
25
+ import {
26
+ useTransitionStatus,
27
+ transitionStatusMapping,
28
+ type TransitionStatus,
29
+ } from './utils/useTransitionStatus';
30
+ import { useOpenChangeComplete } from './utils/useOpenChangeComplete';
31
+ import { useTimeout } from './utils/useTimeout';
32
+
33
+ export type ImageLoadingStatus = 'idle' | 'loading' | 'loaded' | 'error';
34
+
35
+ // The avatar's `imageLoadingStatus` state is NOT surfaced as a data-* attribute.
36
+ const avatarStateAttributesMapping: StateAttributesMapping<{
37
+ imageLoadingStatus: ImageLoadingStatus;
38
+ }> = {
39
+ imageLoadingStatus: () => null,
40
+ };
41
+
42
+ const imageStateAttributesMapping = {
43
+ ...avatarStateAttributesMapping,
44
+ ...transitionStatusMapping,
45
+ };
46
+
47
+ // --- Context -----------------------------------------------------------------
48
+
49
+ export interface AvatarRootContextValue {
50
+ imageLoadingStatus: ImageLoadingStatus;
51
+ setImageLoadingStatus: (status: ImageLoadingStatus) => void;
52
+ }
53
+
54
+ const AvatarRootContext = createContext<AvatarRootContextValue | undefined>(undefined);
55
+
56
+ function useAvatarRootContext(): AvatarRootContextValue {
57
+ const context = useContext(AvatarRootContext);
58
+ if (context === undefined) {
59
+ throw new Error(
60
+ 'Base UI: AvatarRootContext is missing. Avatar parts must be placed within <Avatar.Root>.',
61
+ );
62
+ }
63
+ return context;
64
+ }
65
+
66
+ // --- Image load status hook --------------------------------------------------
67
+
68
+ function useImageLoadingStatus(...args: any[]): ImageLoadingStatus {
69
+ const [user, slotArg] = splitSlot(args);
70
+ const slot = slotArg ?? S('useImageLoadingStatus');
71
+ const src = user[0] as string | undefined;
72
+ const options = (user[1] as any) ?? {};
73
+ const { referrerPolicy, crossOrigin, sizes, srcSet } = options;
74
+
75
+ const [loadingStatus, setLoadingStatus] = useState<ImageLoadingStatus>(
76
+ 'idle',
77
+ subSlot(slot, 'ls'),
78
+ );
79
+
80
+ useLayoutEffect(
81
+ () => {
82
+ if (!src && !srcSet) {
83
+ setLoadingStatus('error');
84
+ return undefined;
85
+ }
86
+
87
+ let isMounted = true;
88
+ const image = new window.Image();
89
+
90
+ const updateStatus = (status: ImageLoadingStatus) => () => {
91
+ if (!isMounted) {
92
+ return;
93
+ }
94
+ setLoadingStatus(status);
95
+ };
96
+
97
+ setLoadingStatus('loading');
98
+ image.onload = updateStatus('loaded');
99
+ image.onerror = updateStatus('error');
100
+ if (referrerPolicy) {
101
+ image.referrerPolicy = referrerPolicy;
102
+ }
103
+ image.crossOrigin = crossOrigin ?? null;
104
+ if (sizes) {
105
+ image.sizes = sizes;
106
+ }
107
+ if (srcSet) {
108
+ image.srcset = srcSet;
109
+ }
110
+ if (src) {
111
+ image.src = src;
112
+ }
113
+
114
+ // Fast path for cached/decoded images.
115
+ if (image.complete) {
116
+ setLoadingStatus(image.naturalWidth > 0 ? 'loaded' : 'error');
117
+ }
118
+
119
+ return () => {
120
+ isMounted = false;
121
+ };
122
+ },
123
+ [src, srcSet, sizes, crossOrigin, referrerPolicy],
124
+ subSlot(slot, 'e:load'),
125
+ );
126
+
127
+ return loadingStatus;
128
+ }
129
+
130
+ // --- Root --------------------------------------------------------------------
131
+
132
+ export interface AvatarRootState {
133
+ imageLoadingStatus: ImageLoadingStatus;
134
+ }
135
+
136
+ export interface AvatarRootProps {
137
+ render?: RenderProp<AvatarRootState>;
138
+ className?: string | ((state: AvatarRootState) => string | undefined);
139
+ style?: Record<string, any> | ((state: AvatarRootState) => Record<string, any> | undefined);
140
+ ref?: any;
141
+ [key: string]: any;
142
+ }
143
+
144
+ function AvatarRoot(props: AvatarRootProps): any {
145
+ const slot = S('AvatarRoot');
146
+ const { className, render, style, ref, ...elementProps } = props;
147
+
148
+ const [imageLoadingStatus, setImageLoadingStatus] = useState<ImageLoadingStatus>(
149
+ 'idle',
150
+ subSlot(slot, 'ls'),
151
+ );
152
+
153
+ const state: AvatarRootState = { imageLoadingStatus };
154
+
155
+ const contextValue: AvatarRootContextValue = useMemo(
156
+ () => ({ imageLoadingStatus, setImageLoadingStatus }),
157
+ [imageLoadingStatus, setImageLoadingStatus],
158
+ subSlot(slot, 'ctx'),
159
+ );
160
+
161
+ const element = useRenderElement(
162
+ 'span',
163
+ { render, className, style },
164
+ { state, ref, props: elementProps, stateAttributesMapping: avatarStateAttributesMapping },
165
+ subSlot(slot, 're'),
166
+ );
167
+
168
+ return createElement(AvatarRootContext.Provider, { value: contextValue, children: element });
169
+ }
170
+
171
+ // --- Image -------------------------------------------------------------------
172
+
173
+ export interface AvatarImageState extends AvatarRootState {
174
+ transitionStatus: TransitionStatus;
175
+ }
176
+
177
+ function AvatarImage(props: any): any {
178
+ const slot = S('AvatarImage');
179
+ const {
180
+ className,
181
+ render,
182
+ onLoadingStatusChange: onLoadingStatusChangeProp,
183
+ style,
184
+ ref,
185
+ ...elementProps
186
+ } = props;
187
+
188
+ const { setImageLoadingStatus } = useAvatarRootContext();
189
+ const imageLoadingStatus = useImageLoadingStatus(
190
+ elementProps.src,
191
+ elementProps,
192
+ subSlot(slot, 'ils'),
193
+ );
194
+
195
+ const isVisible = imageLoadingStatus === 'loaded';
196
+ const { mounted, transitionStatus, setMounted } = useTransitionStatus(
197
+ isVisible,
198
+ subSlot(slot, 'ts'),
199
+ );
200
+
201
+ const imageRef = useRef<HTMLImageElement | null>(null, subSlot(slot, 'imgRef'));
202
+
203
+ const handleLoadingStatusChange = useStableCallback(
204
+ (status: ImageLoadingStatus) => {
205
+ onLoadingStatusChangeProp?.(status);
206
+ setImageLoadingStatus(status);
207
+ },
208
+ subSlot(slot, 'hlsc'),
209
+ );
210
+
211
+ useLayoutEffect(
212
+ () => {
213
+ if (imageLoadingStatus !== 'idle') {
214
+ handleLoadingStatusChange(imageLoadingStatus);
215
+ }
216
+ },
217
+ [imageLoadingStatus, handleLoadingStatusChange],
218
+ subSlot(slot, 'e:status'),
219
+ );
220
+
221
+ useLayoutEffect(
222
+ () => () => setImageLoadingStatus('idle'),
223
+ [setImageLoadingStatus],
224
+ subSlot(slot, 'e:reset'),
225
+ );
226
+
227
+ useOpenChangeComplete(
228
+ {
229
+ open: isVisible,
230
+ ref: imageRef,
231
+ onComplete() {
232
+ if (!isVisible) {
233
+ setMounted(false);
234
+ }
235
+ },
236
+ },
237
+ subSlot(slot, 'occ'),
238
+ );
239
+
240
+ const state: AvatarImageState = { imageLoadingStatus, transitionStatus };
241
+
242
+ const element = useRenderElement(
243
+ 'img',
244
+ { render, className, style },
245
+ {
246
+ state,
247
+ ref: [ref, imageRef],
248
+ props: elementProps,
249
+ stateAttributesMapping: imageStateAttributesMapping,
250
+ enabled: mounted,
251
+ },
252
+ subSlot(slot, 're'),
253
+ );
254
+
255
+ if (!mounted) {
256
+ return null;
257
+ }
258
+
259
+ return element;
260
+ }
261
+
262
+ // --- Fallback ----------------------------------------------------------------
263
+
264
+ function AvatarFallback(props: any): any {
265
+ const slot = S('AvatarFallback');
266
+ const { className, render, delay, style, ref, ...elementProps } = props;
267
+
268
+ const { imageLoadingStatus } = useAvatarRootContext();
269
+ const [delayPassed, setDelayPassed] = useState(delay === undefined, subSlot(slot, 'dp'));
270
+ const timeout = useTimeout(subSlot(slot, 'to'));
271
+
272
+ useEffect(
273
+ () => {
274
+ if (delay !== undefined) {
275
+ timeout.start(delay, () => setDelayPassed(true));
276
+ } else {
277
+ // Once shown without a delay, keep it visible.
278
+ setDelayPassed(true);
279
+ }
280
+ return timeout.clear;
281
+ },
282
+ [timeout, delay],
283
+ subSlot(slot, 'e:delay'),
284
+ );
285
+
286
+ const state: AvatarRootState = { imageLoadingStatus };
287
+
288
+ return useRenderElement(
289
+ 'span',
290
+ { render, className, style },
291
+ {
292
+ state,
293
+ ref,
294
+ props: elementProps,
295
+ stateAttributesMapping: avatarStateAttributesMapping,
296
+ enabled: imageLoadingStatus !== 'loaded' && (delay === undefined || delayPassed),
297
+ },
298
+ subSlot(slot, 're'),
299
+ );
300
+ }
301
+
302
+ // --- Namespace (mirrors `export * as Avatar`) --------------------------------
303
+
304
+ export const Avatar = {
305
+ Root: AvatarRoot,
306
+ Image: AvatarImage,
307
+ Fallback: AvatarFallback,
308
+ };
@@ -0,0 +1,183 @@
1
+ // Ported from .base-ui/packages/react/src/checkbox-group/CheckboxGroup.tsx (v1.6.0). Provides a
2
+ // shared value array to a set of <Checkbox.Root>s (with an optional select-all parent checkbox
3
+ // via `allValues` + `useCheckboxGroupParent`). Renders a `<div role="group">`. octane:
4
+ // forwardRef → ref-as-prop.
5
+ import { createElement, useCallback, useMemo, useRef } from 'octane';
6
+
7
+ import { S, subSlot } from './internal';
8
+ import { useRenderElement, type RenderProp } from './utils/useRenderElement';
9
+ import { useBaseUiId } from './utils/useBaseUiId';
10
+ import { useControlled } from './utils/useControlled';
11
+ import { useStableCallback } from './utils/useStableCallback';
12
+ import { useValueChanged } from './utils/useValueChanged';
13
+ import { areArraysEqual } from './utils/areArraysEqual';
14
+ import { useCheckboxGroupParent } from './utils/useCheckboxGroupParent';
15
+ import { CheckboxGroupContext, type CheckboxGroupContextValue } from './utils/CheckboxGroupContext';
16
+ import { fieldValidityMapping, type FieldRootState } from './utils/field/constants';
17
+ import { useFieldRootContext } from './utils/field/FieldRootContext';
18
+ import { useFormContext } from './utils/field/FormContext';
19
+ import { useLabelableContext } from './utils/field/LabelableContext';
20
+ import { useRegisterFieldControl } from './utils/field/useRegisterFieldControl';
21
+ import { PARENT_CHECKBOX } from './checkbox';
22
+
23
+ const EMPTY_ARRAY: string[] = [];
24
+
25
+ export interface CheckboxGroupState extends FieldRootState {
26
+ disabled: boolean;
27
+ }
28
+
29
+ export interface CheckboxGroupProps {
30
+ value?: string[];
31
+ defaultValue?: string[];
32
+ onValueChange?: (value: string[], eventDetails: any) => void;
33
+ allValues?: string[];
34
+ disabled?: boolean;
35
+ id?: string;
36
+ render?: RenderProp<CheckboxGroupState>;
37
+ className?: string | ((state: CheckboxGroupState) => string | undefined);
38
+ style?: Record<string, any> | ((state: CheckboxGroupState) => Record<string, any> | undefined);
39
+ ref?: any;
40
+ [key: string]: any;
41
+ }
42
+
43
+ function CheckboxGroup(props: CheckboxGroupProps): any {
44
+ const slot = S('CheckboxGroup');
45
+ const {
46
+ allValues,
47
+ className,
48
+ defaultValue: defaultValueProp,
49
+ disabled: disabledProp = false,
50
+ id: idProp,
51
+ onValueChange,
52
+ render,
53
+ value: externalValue,
54
+ style,
55
+ ref,
56
+ ...elementProps
57
+ } = props;
58
+
59
+ const {
60
+ disabled: fieldDisabled,
61
+ name: fieldName,
62
+ state: fieldState,
63
+ validation,
64
+ setFilled,
65
+ setDirty,
66
+ validityData,
67
+ } = useFieldRootContext();
68
+ const { labelId, getDescriptionProps } = useLabelableContext();
69
+ const { clearErrors } = useFormContext();
70
+
71
+ const disabled = fieldDisabled || disabledProp;
72
+
73
+ const defaultValue = useMemo<string[] | undefined>(
74
+ () => {
75
+ if (externalValue === undefined) {
76
+ return defaultValueProp ?? [];
77
+ }
78
+ return undefined;
79
+ },
80
+ [externalValue, defaultValueProp],
81
+ subSlot(slot, 'defaultValue'),
82
+ );
83
+
84
+ const [value, setValueUnwrapped] = useControlled<string[]>(
85
+ { controlled: externalValue, default: defaultValue, name: 'CheckboxGroup', state: 'value' },
86
+ subSlot(slot, 'value'),
87
+ );
88
+
89
+ const setValue = useStableCallback(
90
+ (v: string[], eventDetails: any) => {
91
+ onValueChange?.(v, eventDetails);
92
+ if (eventDetails.isCanceled) {
93
+ return;
94
+ }
95
+ setValueUnwrapped(v);
96
+ },
97
+ subSlot(slot, 'setValue'),
98
+ );
99
+
100
+ const parent = useCheckboxGroupParent(
101
+ { allValues, value, onValueChange: setValue },
102
+ subSlot(slot, 'parent'),
103
+ );
104
+
105
+ const id = useBaseUiId(idProp, subSlot(slot, 'id'));
106
+
107
+ const controlRef = useRef<HTMLButtonElement | null>(null, subSlot(slot, 'controlRef'));
108
+
109
+ const registerControlRef = useCallback(
110
+ (element: HTMLButtonElement | null) => {
111
+ if (controlRef.current == null && element != null && !element.hasAttribute(PARENT_CHECKBOX)) {
112
+ controlRef.current = element;
113
+ }
114
+ },
115
+ [],
116
+ subSlot(slot, 'registerControl'),
117
+ );
118
+
119
+ useRegisterFieldControl(
120
+ controlRef,
121
+ id,
122
+ value,
123
+ undefined,
124
+ !!fieldName && !disabled,
125
+ fieldName,
126
+ subSlot(slot, 'register'),
127
+ );
128
+
129
+ const resolvedValue = value ?? EMPTY_ARRAY;
130
+
131
+ useValueChanged(
132
+ resolvedValue,
133
+ () => {
134
+ if (fieldName) {
135
+ clearErrors(fieldName);
136
+ }
137
+ const initialValue = Array.isArray(validityData.initialValue)
138
+ ? (validityData.initialValue as readonly string[])
139
+ : EMPTY_ARRAY;
140
+ setFilled(resolvedValue.length > 0);
141
+ setDirty(!areArraysEqual(resolvedValue, initialValue));
142
+ validation.change(resolvedValue);
143
+ },
144
+ subSlot(slot, 'valueChanged'),
145
+ );
146
+
147
+ const state: CheckboxGroupState = { ...fieldState, disabled };
148
+
149
+ const contextValue: CheckboxGroupContextValue = useMemo(
150
+ () => ({
151
+ allValues,
152
+ value,
153
+ defaultValue,
154
+ setValue,
155
+ parent,
156
+ disabled,
157
+ validation,
158
+ registerControlRef,
159
+ }),
160
+ [allValues, value, defaultValue, setValue, parent, disabled, validation, registerControlRef],
161
+ subSlot(slot, 'ctx'),
162
+ );
163
+
164
+ const element = useRenderElement(
165
+ 'div',
166
+ { render, className, style },
167
+ {
168
+ state,
169
+ ref,
170
+ props: [
171
+ { id: idProp, role: 'group', 'aria-labelledby': labelId },
172
+ elementProps,
173
+ getDescriptionProps,
174
+ ],
175
+ stateAttributesMapping: fieldValidityMapping,
176
+ },
177
+ subSlot(slot, 're'),
178
+ );
179
+
180
+ return createElement(CheckboxGroupContext.Provider, { value: contextValue, children: element });
181
+ }
182
+
183
+ export { CheckboxGroup };