@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.
- package/LICENSE +21 -0
- package/README.md +33 -0
- package/package.json +47 -0
- package/src/avatar.ts +308 -0
- package/src/checkbox-group.ts +183 -0
- package/src/checkbox.ts +578 -0
- package/src/field.ts +676 -0
- package/src/fieldset.ts +150 -0
- package/src/form.ts +183 -0
- package/src/index.ts +23 -0
- package/src/input.ts +9 -0
- package/src/internal.ts +42 -0
- package/src/merge-props.ts +2 -0
- package/src/meter.ts +204 -0
- package/src/number-field.ts +978 -0
- package/src/progress.ts +260 -0
- package/src/radio-group.ts +285 -0
- package/src/radio.ts +401 -0
- package/src/separator.ts +43 -0
- package/src/slider.ts +1617 -0
- package/src/switch.ts +374 -0
- package/src/toggle-group.ts +136 -0
- package/src/toggle.ts +145 -0
- package/src/use-render.ts +40 -0
- package/src/utils/CheckboxGroupContext.ts +30 -0
- package/src/utils/CompositeRootContext.ts +27 -0
- package/src/utils/DirectionContext.ts +15 -0
- package/src/utils/RadioGroupContext.ts +27 -0
- package/src/utils/ToggleGroupContext.ts +33 -0
- package/src/utils/addEventListener.ts +13 -0
- package/src/utils/areArraysEqual.ts +11 -0
- package/src/utils/clamp.ts +8 -0
- package/src/utils/composeRefs.ts +53 -0
- package/src/utils/composite/CompositeItem.ts +56 -0
- package/src/utils/composite/CompositeList.ts +190 -0
- package/src/utils/composite/CompositeListContext.ts +23 -0
- package/src/utils/composite/CompositeRoot.ts +132 -0
- package/src/utils/composite/keys.ts +179 -0
- package/src/utils/composite/list-utils.ts +78 -0
- package/src/utils/composite/useCompositeItem.ts +58 -0
- package/src/utils/composite/useCompositeListItem.ts +108 -0
- package/src/utils/composite/useCompositeRoot.ts +346 -0
- package/src/utils/contains.ts +22 -0
- package/src/utils/createChangeEventDetails.ts +81 -0
- package/src/utils/field/FieldItemContext.ts +13 -0
- package/src/utils/field/FieldRootContext.ts +98 -0
- package/src/utils/field/FormContext.ts +37 -0
- package/src/utils/field/LabelableContext.ts +32 -0
- package/src/utils/field/LabelableProvider.ts +108 -0
- package/src/utils/field/constants.ts +80 -0
- package/src/utils/field/getCombinedFieldValidityData.ts +16 -0
- package/src/utils/field/useAriaLabelledBy.ts +86 -0
- package/src/utils/field/useFieldControlRegistration.ts +179 -0
- package/src/utils/field/useFieldValidation.ts +311 -0
- package/src/utils/field/useLabel.ts +95 -0
- package/src/utils/field/useLabelableId.ts +99 -0
- package/src/utils/field/useRegisterFieldControl.ts +41 -0
- package/src/utils/formatNumber.ts +44 -0
- package/src/utils/getDefaultFormSubmitter.ts +19 -0
- package/src/utils/getElementRef.ts +13 -0
- package/src/utils/getStateAttributesProps.ts +34 -0
- package/src/utils/matchesFocusVisible.ts +16 -0
- package/src/utils/mergeObjects.ts +18 -0
- package/src/utils/mergeProps.ts +206 -0
- package/src/utils/noop.ts +4 -0
- package/src/utils/number/constants.ts +4 -0
- package/src/utils/number/parse.ts +227 -0
- package/src/utils/number/types.ts +25 -0
- package/src/utils/number/validate.ts +129 -0
- package/src/utils/owner.ts +9 -0
- package/src/utils/platform.ts +15 -0
- package/src/utils/resolveAriaLabelledBy.ts +11 -0
- package/src/utils/resolveClassName.ts +8 -0
- package/src/utils/resolveRef.ts +10 -0
- package/src/utils/resolveStyle.ts +10 -0
- package/src/utils/serializeValue.ts +15 -0
- package/src/utils/slider/asc.ts +4 -0
- package/src/utils/slider/getMidpoint.ts +14 -0
- package/src/utils/slider/getPushedThumbValues.ts +73 -0
- package/src/utils/slider/getSliderValue.ts +27 -0
- package/src/utils/slider/replaceArrayItemAtIndex.ts +8 -0
- package/src/utils/slider/resolveThumbCollision.ts +178 -0
- package/src/utils/slider/roundValueToStep.ts +22 -0
- package/src/utils/slider/validateMinimumDistance.ts +22 -0
- package/src/utils/slider/valueArrayToPercentages.ts +11 -0
- package/src/utils/stringifyLocale.ts +10 -0
- package/src/utils/useAnimationFrame.ts +59 -0
- package/src/utils/useAnimationsFinished.ts +100 -0
- package/src/utils/useBaseUiId.ts +19 -0
- package/src/utils/useButton.ts +217 -0
- package/src/utils/useCheckboxGroupParent.ts +113 -0
- package/src/utils/useControlled.ts +42 -0
- package/src/utils/useFocusableWhenDisabled.ts +84 -0
- package/src/utils/useForcedRerendering.ts +11 -0
- package/src/utils/useIsHydrating.ts +9 -0
- package/src/utils/useOpenChangeComplete.ts +46 -0
- package/src/utils/usePressAndHold.ts +35 -0
- package/src/utils/useRefWithInit.ts +23 -0
- package/src/utils/useRegisteredLabelId.ts +32 -0
- package/src/utils/useRenderElement.ts +165 -0
- package/src/utils/useStableCallback.ts +26 -0
- package/src/utils/useTimeout.ts +51 -0
- package/src/utils/useTransitionStatus.ts +115 -0
- package/src/utils/useValueAsRef.ts +25 -0
- package/src/utils/useValueChanged.ts +37 -0
- package/src/utils/valueToPercent.ts +4 -0
- 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 };
|