@octanejs/base-ui 0.1.1 → 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/README.md +7 -1
- package/package.json +12 -4
- package/src/alert-dialog.ts +47 -0
- package/src/checkbox.ts +7 -16
- package/src/dialog.ts +859 -0
- package/src/field.ts +4 -30
- package/src/index.ts +3 -0
- package/src/number-field.ts +8 -45
- package/src/popover.ts +1205 -0
- package/src/radio.ts +3 -24
- package/src/slider.ts +4 -5
- package/src/switch.ts +4 -30
- package/src/utils/InternalBackdrop.ts +28 -0
- package/src/utils/adaptiveOriginMiddleware.ts +82 -0
- package/src/utils/closePart.ts +61 -0
- package/src/utils/constants.ts +9 -0
- package/src/utils/createChangeEventDetails.ts +7 -0
- package/src/utils/dom.ts +9 -0
- package/src/utils/empty.ts +7 -0
- package/src/utils/floating/FloatingFocusManager.ts +833 -0
- package/src/utils/floating/FloatingPortal.ts +268 -0
- package/src/utils/floating/FloatingRootStore.ts +127 -0
- package/src/utils/floating/FloatingTree.ts +70 -0
- package/src/utils/floating/FloatingTreeStore.ts +22 -0
- package/src/utils/floating/FocusGuard.ts +34 -0
- package/src/utils/floating/composite.ts +20 -0
- package/src/utils/floating/constants.ts +8 -0
- package/src/utils/floating/createAttribute.ts +4 -0
- package/src/utils/floating/createEventEmitter.ts +20 -0
- package/src/utils/floating/element.ts +54 -0
- package/src/utils/floating/enqueueFocus.ts +38 -0
- package/src/utils/floating/event.ts +53 -0
- package/src/utils/floating/getEmptyRootContext.ts +19 -0
- package/src/utils/floating/markOthers.ts +197 -0
- package/src/utils/floating/nodes.ts +31 -0
- package/src/utils/floating/tabbable.ts +260 -0
- package/src/utils/floating/types.ts +57 -0
- package/src/utils/floating/useClick.ts +174 -0
- package/src/utils/floating/useDismiss.ts +641 -0
- package/src/utils/floating/useFloating.ts +198 -0
- package/src/utils/floating/useFloatingRootContext.ts +81 -0
- package/src/utils/floating/useHoverFloatingInteraction.ts +12 -0
- package/src/utils/floating/useHoverReferenceInteraction.ts +17 -0
- package/src/utils/floating/useSyncedFloatingRootContext.ts +93 -0
- package/src/utils/getDisabledMountTransitionStyles.ts +12 -0
- package/src/utils/hideMiddleware.ts +22 -0
- package/src/utils/inertValue.ts +5 -0
- package/src/utils/mergeCleanups.ts +13 -0
- package/src/utils/platform.ts +46 -2
- package/src/utils/popupStateMapping.ts +48 -0
- package/src/utils/popups/index.ts +4 -0
- package/src/utils/popups/popupStoreUtils.ts +484 -0
- package/src/utils/popups/popupTriggerMap.ts +62 -0
- package/src/utils/popups/store.ts +147 -0
- package/src/utils/popups/useTriggerFocusGuards.ts +73 -0
- package/src/utils/store/ReactStore.ts +168 -0
- package/src/utils/store/Store.ts +84 -0
- package/src/utils/store/createSelector.ts +66 -0
- package/src/utils/store/useStore.ts +49 -0
- package/src/utils/useAnchorPositioning.ts +581 -0
- package/src/utils/useAnchoredPopupScrollLock.ts +50 -0
- package/src/utils/useAnimationFrame.ts +28 -5
- package/src/utils/useEnhancedClickHandler.ts +47 -0
- package/src/utils/useOnFirstRender.ts +11 -0
- package/src/utils/useOpenInteractionType.ts +32 -0
- package/src/utils/usePositioner.ts +48 -0
- package/src/utils/useScrollLock.ts +253 -0
package/src/radio.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
//
|
|
5
5
|
// A radio renders a `<span role="radio">` + a hidden `<input type="radio">` (a CompositeItem
|
|
6
6
|
// for roving focus when inside a <RadioGroup>). octane adaptations mirror Switch/Checkbox:
|
|
7
|
-
// native events; the
|
|
8
|
-
//
|
|
7
|
+
// native events; the hidden input takes the live `checked` prop (octane inputs are CONTROLLED
|
|
8
|
+
// exactly like React's) with native click dispatch.
|
|
9
9
|
import { createContext, createElement, useContext, useLayoutEffect, useMemo, useRef } from 'octane';
|
|
10
10
|
|
|
11
11
|
import { S, subSlot } from './internal';
|
|
@@ -138,9 +138,6 @@ function RadioRoot(props: RadioRootProps): any {
|
|
|
138
138
|
const radioRef = useRef<HTMLElement | null>(null, subSlot(slot, 'radioRef'));
|
|
139
139
|
const inputRef = useRef<HTMLInputElement | null>(null, subSlot(slot, 'inputRef'));
|
|
140
140
|
|
|
141
|
-
// octane: reflect the INITIAL checked as the `checked` ATTRIBUTE (see Switch).
|
|
142
|
-
const initialCheckedRef = useRef(checked, subSlot(slot, 'initialChecked'));
|
|
143
|
-
|
|
144
141
|
const handleControlRef = useStableCallback(
|
|
145
142
|
(element: HTMLElement | null) => {
|
|
146
143
|
if (!element) {
|
|
@@ -168,23 +165,6 @@ function RadioRoot(props: RadioRootProps): any {
|
|
|
168
165
|
subSlot(slot, 'e:filled'),
|
|
169
166
|
);
|
|
170
167
|
|
|
171
|
-
// octane: drive `input.checked` PROPERTY imperatively (matches React's controlled input).
|
|
172
|
-
useLayoutEffect(
|
|
173
|
-
() => {
|
|
174
|
-
const input = inputRef.current;
|
|
175
|
-
if (!input) {
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
const setNativeChecked = Object.getOwnPropertyDescriptor(
|
|
179
|
-
ownerWindow(input).HTMLInputElement.prototype,
|
|
180
|
-
'checked',
|
|
181
|
-
)?.set;
|
|
182
|
-
setNativeChecked?.call(input, checked);
|
|
183
|
-
},
|
|
184
|
-
[checked],
|
|
185
|
-
subSlot(slot, 'e:syncChecked'),
|
|
186
|
-
);
|
|
187
|
-
|
|
188
168
|
useLayoutEffect(
|
|
189
169
|
() => {
|
|
190
170
|
if (!inputRef.current) {
|
|
@@ -275,8 +255,7 @@ function RadioRoot(props: RadioRootProps): any {
|
|
|
275
255
|
'aria-hidden': true,
|
|
276
256
|
...(value !== undefined ? { value: serializeValue(value) } : {}),
|
|
277
257
|
disabled,
|
|
278
|
-
|
|
279
|
-
checked: initialCheckedRef.current || undefined,
|
|
258
|
+
checked,
|
|
280
259
|
required,
|
|
281
260
|
readOnly,
|
|
282
261
|
onChange(event: any) {
|
package/src/slider.ts
CHANGED
|
@@ -1441,11 +1441,10 @@ function SliderThumb(componentProps: any): any {
|
|
|
1441
1441
|
},
|
|
1442
1442
|
tabIndex: tabIndexProp ?? undefined,
|
|
1443
1443
|
type: 'range',
|
|
1444
|
-
// octane:
|
|
1445
|
-
//
|
|
1446
|
-
//
|
|
1447
|
-
//
|
|
1448
|
-
// attribute drives the property/position.
|
|
1444
|
+
// octane: `value` is live CONTROLLED (React parity) — the runtime drives the
|
|
1445
|
+
// `.value` property, mirrors it to the attribute, and reasserts after commits and
|
|
1446
|
+
// discrete events. Base UI prevents native range interaction (keydown →
|
|
1447
|
+
// preventDefault), so `thumbValue` stays the single source of truth, as on React.
|
|
1449
1448
|
value: thumbValue ?? '',
|
|
1450
1449
|
},
|
|
1451
1450
|
(props: any) => validation.getValidationProps(disabled, props),
|
package/src/switch.ts
CHANGED
|
@@ -4,10 +4,9 @@
|
|
|
4
4
|
//
|
|
5
5
|
// A switch renders a `<span role="switch">` plus a hidden `<input type="checkbox">`. octane
|
|
6
6
|
// adaptations: (1) events are NATIVE — the handlers read the event directly (Base UI's
|
|
7
|
-
// `event.nativeEvent`); (2)
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// setter in a layout effect — matching React's property-only controlled behavior. Field/Form
|
|
7
|
+
// `event.nativeEvent`); (2) the hidden input takes the live `checked` prop — octane inputs
|
|
8
|
+
// are CONTROLLED exactly like React's (property-driven, reasserted on every commit and after
|
|
9
|
+
// discrete events; only the INITIAL checked reflects to the attribute). Field/Form
|
|
11
10
|
// integration is inert when standalone (default contexts).
|
|
12
11
|
import { createContext, createElement, useContext, useLayoutEffect, useMemo, useRef } from 'octane';
|
|
13
12
|
|
|
@@ -155,11 +154,6 @@ function SwitchRoot(props: SwitchRootProps): any {
|
|
|
155
154
|
subSlot(slot, 'checked'),
|
|
156
155
|
);
|
|
157
156
|
|
|
158
|
-
// octane: React's controlled `<input checked>` reflects the INITIAL checked to the
|
|
159
|
-
// `checked` ATTRIBUTE (its default-state), then updates only the property. Capture that
|
|
160
|
-
// initial value so the attribute matches; the property is driven by the sync effect below.
|
|
161
|
-
const initialCheckedRef = useRef(checked, subSlot(slot, 'initialChecked'));
|
|
162
|
-
|
|
163
157
|
useRegisterFieldControl(
|
|
164
158
|
switchRef,
|
|
165
159
|
id,
|
|
@@ -180,24 +174,6 @@ function SwitchRoot(props: SwitchRootProps): any {
|
|
|
180
174
|
subSlot(slot, 'e:filled'),
|
|
181
175
|
);
|
|
182
176
|
|
|
183
|
-
// octane uncontrolled-input adaptation: drive the hidden input's `checked` PROPERTY (never
|
|
184
|
-
// the attribute) through the native setter, so the DOM matches React's controlled input.
|
|
185
|
-
useLayoutEffect(
|
|
186
|
-
() => {
|
|
187
|
-
const input = inputRef.current;
|
|
188
|
-
if (!input) {
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
const setNativeChecked = Object.getOwnPropertyDescriptor(
|
|
192
|
-
ownerWindow(input).HTMLInputElement.prototype,
|
|
193
|
-
'checked',
|
|
194
|
-
)?.set;
|
|
195
|
-
setNativeChecked?.call(input, checked);
|
|
196
|
-
},
|
|
197
|
-
[checked],
|
|
198
|
-
subSlot(slot, 'e:syncChecked'),
|
|
199
|
-
);
|
|
200
|
-
|
|
201
177
|
useValueChanged(
|
|
202
178
|
checked,
|
|
203
179
|
() => {
|
|
@@ -268,9 +244,7 @@ function SwitchRoot(props: SwitchRootProps): any {
|
|
|
268
244
|
|
|
269
245
|
const inputProps: Record<string, any> = mergeProps(
|
|
270
246
|
{
|
|
271
|
-
|
|
272
|
-
// default-state); the live value is driven via the property in the sync effect.
|
|
273
|
-
checked: initialCheckedRef.current || undefined,
|
|
247
|
+
checked,
|
|
274
248
|
disabled,
|
|
275
249
|
form,
|
|
276
250
|
id: hiddenInputId,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/utils/InternalBackdrop.tsx. The always-present backdrop a
|
|
2
|
+
// modal popup renders so Floating UI's outside-press detection sees an element that existed when the
|
|
3
|
+
// popup opened. octane: `createElement`; ref-as-prop.
|
|
4
|
+
import { createElement } from 'octane';
|
|
5
|
+
|
|
6
|
+
export function InternalBackdrop(props: any): any {
|
|
7
|
+
const { cutout, ref, ...otherProps } = props;
|
|
8
|
+
|
|
9
|
+
let clipPath: string | undefined;
|
|
10
|
+
if (cutout) {
|
|
11
|
+
const rect = cutout.getBoundingClientRect();
|
|
12
|
+
clipPath = `polygon(0% 0%,100% 0%,100% 100%,0% 100%,0% 0%,${rect.left}px ${rect.top}px,${rect.left}px ${rect.bottom}px,${rect.right}px ${rect.bottom}px,${rect.right}px ${rect.top}px,${rect.left}px ${rect.top}px)`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return createElement('div', {
|
|
16
|
+
ref,
|
|
17
|
+
role: 'presentation',
|
|
18
|
+
['data-base-ui-inert']: '',
|
|
19
|
+
...otherProps,
|
|
20
|
+
style: {
|
|
21
|
+
position: 'fixed',
|
|
22
|
+
inset: 0,
|
|
23
|
+
userSelect: 'none',
|
|
24
|
+
WebkitUserSelect: 'none',
|
|
25
|
+
clipPath,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Ported verbatim from .base-ui/packages/react/src/utils/adaptiveOriginMiddleware.ts (v1.6.0).
|
|
2
|
+
// Pure @floating-ui middleware (no React) — used only when a popup is anchored to a viewport
|
|
3
|
+
// (`hasViewport`). Rewrites the resolved x/y into `right`/`bottom` origins on the sides that need
|
|
4
|
+
// it so CSS transforms scale from the correct corner. `DEFAULT_SIDES` is the fallback used when the
|
|
5
|
+
// middleware doesn't run (no transition, or `adaptiveOrigin` disabled).
|
|
6
|
+
import { getSide } from '@floating-ui/utils';
|
|
7
|
+
|
|
8
|
+
import { ownerDocument, ownerWindow } from './owner';
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_SIDES = {
|
|
11
|
+
sideX: 'left',
|
|
12
|
+
sideY: 'top',
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
export const adaptiveOrigin: any = {
|
|
16
|
+
name: 'adaptiveOrigin',
|
|
17
|
+
async fn(state: any) {
|
|
18
|
+
const {
|
|
19
|
+
x: rawX,
|
|
20
|
+
y: rawY,
|
|
21
|
+
rects: { floating: floatRect },
|
|
22
|
+
elements: { floating },
|
|
23
|
+
platform,
|
|
24
|
+
strategy,
|
|
25
|
+
placement,
|
|
26
|
+
} = state;
|
|
27
|
+
|
|
28
|
+
const win = ownerWindow(floating);
|
|
29
|
+
const styles = win.getComputedStyle(floating);
|
|
30
|
+
const hasTransition = styles.transitionDuration !== '0s' && styles.transitionDuration !== '';
|
|
31
|
+
|
|
32
|
+
if (!hasTransition) {
|
|
33
|
+
return {
|
|
34
|
+
x: rawX,
|
|
35
|
+
y: rawY,
|
|
36
|
+
data: DEFAULT_SIDES,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const offsetParent = await platform.getOffsetParent?.(floating);
|
|
41
|
+
|
|
42
|
+
let offsetDimensions = { width: 0, height: 0 };
|
|
43
|
+
|
|
44
|
+
// For fixed strategy, prefer visualViewport if available
|
|
45
|
+
if (strategy === 'fixed' && win?.visualViewport) {
|
|
46
|
+
offsetDimensions = {
|
|
47
|
+
width: win.visualViewport.width,
|
|
48
|
+
height: win.visualViewport.height,
|
|
49
|
+
};
|
|
50
|
+
} else if (offsetParent === win) {
|
|
51
|
+
const doc = ownerDocument(floating);
|
|
52
|
+
offsetDimensions = {
|
|
53
|
+
width: doc.documentElement.clientWidth,
|
|
54
|
+
height: doc.documentElement.clientHeight,
|
|
55
|
+
};
|
|
56
|
+
} else if (await platform.isElement?.(offsetParent)) {
|
|
57
|
+
offsetDimensions = await platform.getDimensions(offsetParent);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const currentSide = getSide(placement);
|
|
61
|
+
let x = rawX;
|
|
62
|
+
let y = rawY;
|
|
63
|
+
|
|
64
|
+
if (currentSide === 'left') {
|
|
65
|
+
x = offsetDimensions.width - (rawX + floatRect.width);
|
|
66
|
+
}
|
|
67
|
+
if (currentSide === 'top') {
|
|
68
|
+
y = offsetDimensions.height - (rawY + floatRect.height);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const sideX = currentSide === 'left' ? 'right' : DEFAULT_SIDES.sideX;
|
|
72
|
+
const sideY = currentSide === 'top' ? 'bottom' : DEFAULT_SIDES.sideY;
|
|
73
|
+
return {
|
|
74
|
+
x,
|
|
75
|
+
y,
|
|
76
|
+
data: {
|
|
77
|
+
sideX,
|
|
78
|
+
sideY,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/utils/closePart.tsx (v1.6.0), octane-adapted
|
|
2
|
+
// (slot-threaded plain-`.ts`). Tracks how many "close" parts (e.g. `Popover.Close`) are mounted
|
|
3
|
+
// inside a popup so the focus manager can decide whether the popup is effectively modal.
|
|
4
|
+
import {
|
|
5
|
+
createContext,
|
|
6
|
+
useContext,
|
|
7
|
+
useState,
|
|
8
|
+
useMemo,
|
|
9
|
+
useLayoutEffect,
|
|
10
|
+
createElement,
|
|
11
|
+
} from 'octane';
|
|
12
|
+
|
|
13
|
+
import { S, subSlot } from '../internal';
|
|
14
|
+
import { useStableCallback } from './useStableCallback';
|
|
15
|
+
|
|
16
|
+
interface ClosePartContextValue {
|
|
17
|
+
register: () => () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ClosePartContext = createContext<ClosePartContextValue | undefined>(undefined);
|
|
21
|
+
|
|
22
|
+
export function useClosePartCount(): { context: ClosePartContextValue; hasClosePart: boolean } {
|
|
23
|
+
const slot = S('useClosePartCount');
|
|
24
|
+
const [closePartCount, setClosePartCount] = useState(0, subSlot(slot, 'count'));
|
|
25
|
+
|
|
26
|
+
const register = useStableCallback(
|
|
27
|
+
() => {
|
|
28
|
+
setClosePartCount((count: number) => count + 1);
|
|
29
|
+
return () => {
|
|
30
|
+
setClosePartCount((count: number) => Math.max(0, count - 1));
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
subSlot(slot, 'reg'),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const context = useMemo(() => ({ register }), [register], subSlot(slot, 'ctx'));
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
context,
|
|
40
|
+
hasClosePart: closePartCount > 0,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function ClosePartProvider(props: { value: ClosePartContextValue; children?: any }): any {
|
|
45
|
+
return createElement(ClosePartContext.Provider, {
|
|
46
|
+
value: props.value,
|
|
47
|
+
children: props.children,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function useClosePartRegistration(): void {
|
|
52
|
+
const slot = S('useClosePartRegistration');
|
|
53
|
+
const context = useContext(ClosePartContext);
|
|
54
|
+
useLayoutEffect(
|
|
55
|
+
() => {
|
|
56
|
+
return context?.register();
|
|
57
|
+
},
|
|
58
|
+
[context],
|
|
59
|
+
subSlot(slot, 'eff'),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Ported from .base-ui/packages/react/src/internals/constants.ts (v1.6.0) — the subset the
|
|
2
|
+
// overlay/positioner layer needs. `POPUP_COLLISION_AVOIDANCE` is the default collision config
|
|
3
|
+
// popups pass to `useAnchorPositioning`; `DISABLED_TRANSITIONS_STYLE` disables transitions while
|
|
4
|
+
// a popup is in the `starting` transition phase (so the mount frame doesn't animate).
|
|
5
|
+
export const POPUP_COLLISION_AVOIDANCE = {
|
|
6
|
+
fallbackAxisSide: 'end',
|
|
7
|
+
} as const;
|
|
8
|
+
|
|
9
|
+
export const DISABLED_TRANSITIONS_STYLE = { style: { transition: 'none' } } as const;
|
|
@@ -22,6 +22,13 @@ export const REASONS = {
|
|
|
22
22
|
scrub: 'scrub',
|
|
23
23
|
trackPress: 'track-press',
|
|
24
24
|
drag: 'drag',
|
|
25
|
+
triggerHover: 'trigger-hover',
|
|
26
|
+
triggerFocus: 'trigger-focus',
|
|
27
|
+
outsidePress: 'outside-press',
|
|
28
|
+
closePress: 'close-press',
|
|
29
|
+
focusOut: 'focus-out',
|
|
30
|
+
escapeKey: 'escape-key',
|
|
31
|
+
imperativeAction: 'imperative-action',
|
|
25
32
|
} as const;
|
|
26
33
|
|
|
27
34
|
export type BaseUIEventReason = (typeof REASONS)[keyof typeof REASONS];
|
package/src/utils/dom.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Small DOM type-guards (Base UI uses `@floating-ui/utils/dom`'s isElement/isHTMLElement). jsdom
|
|
2
|
+
// and normal DOM are single-realm here, so `instanceof` is sufficient.
|
|
3
|
+
export function isElement(value: unknown): value is Element {
|
|
4
|
+
return value != null && value instanceof Element;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function isHTMLElement(value: unknown): value is HTMLElement {
|
|
8
|
+
return value != null && value instanceof HTMLElement;
|
|
9
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Ported from @base-ui/utils/empty: shared frozen empties + NOOP, compared by identity across the
|
|
2
|
+
// codebase (a stable reference lets effects/memos bail out). NOOP re-exports the existing shared
|
|
3
|
+
// one so there is a single identity.
|
|
4
|
+
export { NOOP } from './noop';
|
|
5
|
+
|
|
6
|
+
export const EMPTY_OBJECT = {} as Record<string, never>;
|
|
7
|
+
export const EMPTY_ARRAY = [] as never[];
|