@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.
Files changed (67) hide show
  1. package/README.md +7 -1
  2. package/package.json +12 -4
  3. package/src/alert-dialog.ts +47 -0
  4. package/src/checkbox.ts +7 -16
  5. package/src/dialog.ts +859 -0
  6. package/src/field.ts +4 -30
  7. package/src/index.ts +3 -0
  8. package/src/number-field.ts +8 -45
  9. package/src/popover.ts +1205 -0
  10. package/src/radio.ts +3 -24
  11. package/src/slider.ts +4 -5
  12. package/src/switch.ts +4 -30
  13. package/src/utils/InternalBackdrop.ts +28 -0
  14. package/src/utils/adaptiveOriginMiddleware.ts +82 -0
  15. package/src/utils/closePart.ts +61 -0
  16. package/src/utils/constants.ts +9 -0
  17. package/src/utils/createChangeEventDetails.ts +7 -0
  18. package/src/utils/dom.ts +9 -0
  19. package/src/utils/empty.ts +7 -0
  20. package/src/utils/floating/FloatingFocusManager.ts +833 -0
  21. package/src/utils/floating/FloatingPortal.ts +268 -0
  22. package/src/utils/floating/FloatingRootStore.ts +127 -0
  23. package/src/utils/floating/FloatingTree.ts +70 -0
  24. package/src/utils/floating/FloatingTreeStore.ts +22 -0
  25. package/src/utils/floating/FocusGuard.ts +34 -0
  26. package/src/utils/floating/composite.ts +20 -0
  27. package/src/utils/floating/constants.ts +8 -0
  28. package/src/utils/floating/createAttribute.ts +4 -0
  29. package/src/utils/floating/createEventEmitter.ts +20 -0
  30. package/src/utils/floating/element.ts +54 -0
  31. package/src/utils/floating/enqueueFocus.ts +38 -0
  32. package/src/utils/floating/event.ts +53 -0
  33. package/src/utils/floating/getEmptyRootContext.ts +19 -0
  34. package/src/utils/floating/markOthers.ts +197 -0
  35. package/src/utils/floating/nodes.ts +31 -0
  36. package/src/utils/floating/tabbable.ts +260 -0
  37. package/src/utils/floating/types.ts +57 -0
  38. package/src/utils/floating/useClick.ts +174 -0
  39. package/src/utils/floating/useDismiss.ts +641 -0
  40. package/src/utils/floating/useFloating.ts +198 -0
  41. package/src/utils/floating/useFloatingRootContext.ts +81 -0
  42. package/src/utils/floating/useHoverFloatingInteraction.ts +12 -0
  43. package/src/utils/floating/useHoverReferenceInteraction.ts +17 -0
  44. package/src/utils/floating/useSyncedFloatingRootContext.ts +93 -0
  45. package/src/utils/getDisabledMountTransitionStyles.ts +12 -0
  46. package/src/utils/hideMiddleware.ts +22 -0
  47. package/src/utils/inertValue.ts +5 -0
  48. package/src/utils/mergeCleanups.ts +13 -0
  49. package/src/utils/platform.ts +46 -2
  50. package/src/utils/popupStateMapping.ts +48 -0
  51. package/src/utils/popups/index.ts +4 -0
  52. package/src/utils/popups/popupStoreUtils.ts +484 -0
  53. package/src/utils/popups/popupTriggerMap.ts +62 -0
  54. package/src/utils/popups/store.ts +147 -0
  55. package/src/utils/popups/useTriggerFocusGuards.ts +73 -0
  56. package/src/utils/store/ReactStore.ts +168 -0
  57. package/src/utils/store/Store.ts +84 -0
  58. package/src/utils/store/createSelector.ts +66 -0
  59. package/src/utils/store/useStore.ts +49 -0
  60. package/src/utils/useAnchorPositioning.ts +581 -0
  61. package/src/utils/useAnchoredPopupScrollLock.ts +50 -0
  62. package/src/utils/useAnimationFrame.ts +28 -5
  63. package/src/utils/useEnhancedClickHandler.ts +47 -0
  64. package/src/utils/useOnFirstRender.ts +11 -0
  65. package/src/utils/useOpenInteractionType.ts +32 -0
  66. package/src/utils/usePositioner.ts +48 -0
  67. package/src/utils/useScrollLock.ts +253 -0
@@ -0,0 +1,47 @@
1
+ // Ported from @base-ui/utils/useEnhancedClickHandler (v1.6.0), octane-adapted (native events; the
2
+ // refs/callbacks thread slots). Detects the pointer type behind a click (Safari/Firefox use
3
+ // MouseEvent, not PointerEvent) + keyboard clicks (`event.detail === 0`).
4
+ import { useRef, useCallback } from 'octane';
5
+
6
+ import { subSlot } from '../internal';
7
+
8
+ export type InteractionType = 'mouse' | 'touch' | 'pen' | 'keyboard' | '';
9
+
10
+ export function useEnhancedClickHandler(
11
+ handler: (event: any, interactionType: InteractionType) => void,
12
+ slot: symbol | undefined,
13
+ ): { onClick: (event: any) => void; onPointerDown: (event: any) => void } {
14
+ const lastClickInteractionTypeRef = useRef<InteractionType>('', subSlot(slot, 'last'));
15
+
16
+ const handlePointerDown = useCallback(
17
+ (event: any) => {
18
+ if (event.defaultPrevented) {
19
+ return;
20
+ }
21
+ lastClickInteractionTypeRef.current = event.pointerType as InteractionType;
22
+ handler(event, event.pointerType as InteractionType);
23
+ },
24
+ [handler],
25
+ subSlot(slot, 'pd'),
26
+ );
27
+
28
+ const handleClick = useCallback(
29
+ (event: any) => {
30
+ // event.detail is the click count; 0 means keyboard-triggered.
31
+ if (event.detail === 0) {
32
+ handler(event, 'keyboard');
33
+ return;
34
+ }
35
+ if ('pointerType' in event) {
36
+ handler(event, event.pointerType);
37
+ } else {
38
+ handler(event, lastClickInteractionTypeRef.current);
39
+ }
40
+ lastClickInteractionTypeRef.current = '';
41
+ },
42
+ [handler],
43
+ subSlot(slot, 'click'),
44
+ );
45
+
46
+ return { onClick: handleClick, onPointerDown: handlePointerDown };
47
+ }
@@ -0,0 +1,11 @@
1
+ // Ported from @base-ui/utils/useOnFirstRender: runs `fn` exactly once, during the first render.
2
+ // octane: the ref threads an explicit slot.
3
+ import { useRef } from 'octane';
4
+
5
+ export function useOnFirstRender(fn: () => void, slot: symbol | undefined): void {
6
+ const ref = useRef(true, slot);
7
+ if (ref.current) {
8
+ ref.current = false;
9
+ fn();
10
+ }
11
+ }
@@ -0,0 +1,32 @@
1
+ // Ported from .base-ui/packages/react/src/utils/useOpenInteractionType.ts (v1.6.0), octane-adapted
2
+ // (slot-threaded; native events). Records the interaction type (mouse/touch/pen/keyboard) that
3
+ // opened a popup, so trigger-owned focus can behave correctly.
4
+ import { useMemo } from 'octane';
5
+
6
+ import { subSlot } from '../internal';
7
+ import { useStableCallback } from './useStableCallback';
8
+ import { useEnhancedClickHandler, type InteractionType } from './useEnhancedClickHandler';
9
+ import { platform } from './platform';
10
+
11
+ export function useOpenMethodTriggerProps(
12
+ open: boolean | (() => boolean),
13
+ setOpenMethod: (interactionType: InteractionType | null) => void,
14
+ slot: symbol | undefined,
15
+ ): { onClick: (event: any) => void; onPointerDown: (event: any) => void } {
16
+ const handleTriggerClick = useStableCallback(
17
+ (_: any, interactionType: InteractionType) => {
18
+ const isOpen = typeof open === 'function' ? open() : open;
19
+ if (!isOpen) {
20
+ setOpenMethod(interactionType || (platform.os.ios ? 'touch' : ''));
21
+ }
22
+ },
23
+ subSlot(slot, 'h'),
24
+ );
25
+
26
+ const { onClick, onPointerDown } = useEnhancedClickHandler(
27
+ handleTriggerClick,
28
+ subSlot(slot, 'ech'),
29
+ );
30
+
31
+ return useMemo(() => ({ onClick, onPointerDown }), [onClick, onPointerDown], subSlot(slot, 'm'));
32
+ }
@@ -0,0 +1,48 @@
1
+ // Ported from .base-ui/packages/react/src/utils/usePositioner.tsx (v1.6.0), octane-adapted
2
+ // (slot-threaded). Renders the shared outer Positioner `<div>` used by popup components: applies the
3
+ // `role="presentation"`, hidden state, positioning styles, disabled-mount-transition styles, popup
4
+ // state attributes, and optional `inert` pointer-events guard.
5
+ import { S, subSlot } from '../internal';
6
+ import { popupStateMapping } from './popupStateMapping';
7
+ import { useRenderElement } from './useRenderElement';
8
+ import { getDisabledMountTransitionStyles } from './getDisabledMountTransitionStyles';
9
+ import type { TransitionStatus } from './useTransitionStatus';
10
+
11
+ interface UsePositionerOptions {
12
+ styles: Record<string, any>;
13
+ transitionStatus: TransitionStatus;
14
+ props?: Record<string, any> | undefined;
15
+ refs?: any;
16
+ hidden?: boolean | undefined;
17
+ inert?: boolean | undefined;
18
+ }
19
+
20
+ export function usePositioner<State extends Record<string, any>>(
21
+ componentProps: any,
22
+ state: State,
23
+ { styles, transitionStatus, props, refs, hidden, inert = false }: UsePositionerOptions,
24
+ slotArg?: symbol | undefined,
25
+ ): any {
26
+ const slot = slotArg ?? S('usePositioner');
27
+ const style: Record<string, any> = { ...styles };
28
+
29
+ if (inert) {
30
+ style.pointerEvents = 'none';
31
+ }
32
+
33
+ return useRenderElement(
34
+ 'div',
35
+ componentProps,
36
+ {
37
+ state,
38
+ ref: refs,
39
+ props: [
40
+ { role: 'presentation', hidden, style },
41
+ getDisabledMountTransitionStyles(transitionStatus),
42
+ props,
43
+ ],
44
+ stateAttributesMapping: popupStateMapping,
45
+ },
46
+ subSlot(slot, 're'),
47
+ );
48
+ }
@@ -0,0 +1,253 @@
1
+ // Ported from .base-ui/packages/utils/src/useScrollLock.ts (v1.6.0). Locks document scroll while a
2
+ // modal popup is open (a ref-counted `ScrollLocker` singleton; overlay-scrollbar vs inset-scrollbar
3
+ // strategies; scrollbar-gutter compensation). Pure DOM apart from the hook — octane adaptation:
4
+ // `useIsoLayoutEffect` → `useLayoutEffect` with an explicit slot.
5
+ import { isOverflowElement } from '@floating-ui/utils/dom';
6
+ import { useLayoutEffect } from 'octane';
7
+
8
+ import { addEventListener } from './addEventListener';
9
+ import { platform } from './platform';
10
+ import { ownerDocument, ownerWindow } from './owner';
11
+ import { Timeout } from './useTimeout';
12
+ import { AnimationFrame } from './useAnimationFrame';
13
+ import { NOOP } from './empty';
14
+
15
+ let originalHtmlStyles: Partial<CSSStyleDeclaration> = {};
16
+ let originalBodyStyles: Partial<CSSStyleDeclaration> = {};
17
+ let originalHtmlScrollBehavior = '';
18
+
19
+ function hasInsetScrollbars(referenceElement: Element | null) {
20
+ if (typeof document === 'undefined') {
21
+ return false;
22
+ }
23
+ const doc = ownerDocument(referenceElement);
24
+ const win = ownerWindow(doc);
25
+ return win.innerWidth - doc.documentElement.clientWidth > 0;
26
+ }
27
+
28
+ function supportsStableScrollbarGutter(referenceElement: Element | null) {
29
+ const supported =
30
+ typeof CSS !== 'undefined' && CSS.supports && CSS.supports('scrollbar-gutter', 'stable');
31
+ if (!supported || typeof document === 'undefined') {
32
+ return false;
33
+ }
34
+ const doc = ownerDocument(referenceElement);
35
+ const html = doc.documentElement;
36
+ const body = doc.body;
37
+ const scrollContainer = isOverflowElement(html) ? html : body;
38
+ const originalScrollContainerOverflowY = scrollContainer.style.overflowY;
39
+ const originalHtmlStyleGutter = html.style.scrollbarGutter;
40
+ html.style.scrollbarGutter = 'stable';
41
+ scrollContainer.style.overflowY = 'scroll';
42
+ const before = scrollContainer.offsetWidth;
43
+ scrollContainer.style.overflowY = 'hidden';
44
+ const after = scrollContainer.offsetWidth;
45
+ scrollContainer.style.overflowY = originalScrollContainerOverflowY;
46
+ html.style.scrollbarGutter = originalHtmlStyleGutter;
47
+ return before === after;
48
+ }
49
+
50
+ function preventScrollOverlayScrollbars(referenceElement: Element | null) {
51
+ const doc = ownerDocument(referenceElement);
52
+ const html = doc.documentElement;
53
+ const body = doc.body;
54
+ const elementToLock = isOverflowElement(html) ? html : body;
55
+ const originalElementToLockStyles = {
56
+ overflowY: elementToLock.style.overflowY,
57
+ overflowX: elementToLock.style.overflowX,
58
+ };
59
+ Object.assign(elementToLock.style, { overflowY: 'hidden', overflowX: 'hidden' });
60
+ return () => {
61
+ Object.assign(elementToLock.style, originalElementToLockStyles);
62
+ };
63
+ }
64
+
65
+ function preventScrollInsetScrollbars(referenceElement: Element | null) {
66
+ const doc = ownerDocument(referenceElement);
67
+ const html = doc.documentElement;
68
+ const body = doc.body;
69
+ const win = ownerWindow(html);
70
+
71
+ let scrollTop = 0;
72
+ let scrollLeft = 0;
73
+ let updateGutterOnly = false;
74
+ const resizeFrame = AnimationFrame.create();
75
+
76
+ if (platform.engine.webkit && (win.visualViewport?.scale ?? 1) !== 1) {
77
+ return () => {};
78
+ }
79
+
80
+ function lockScroll() {
81
+ const htmlStyles = win.getComputedStyle(html);
82
+ const bodyStyles = win.getComputedStyle(body);
83
+ const htmlScrollbarGutterValue = htmlStyles.scrollbarGutter || '';
84
+ const hasBothEdges = htmlScrollbarGutterValue.includes('both-edges');
85
+ const scrollbarGutterValue = hasBothEdges ? 'stable both-edges' : 'stable';
86
+
87
+ scrollTop = html.scrollTop;
88
+ scrollLeft = html.scrollLeft;
89
+
90
+ originalHtmlStyles = {
91
+ scrollbarGutter: html.style.scrollbarGutter,
92
+ overflowY: html.style.overflowY,
93
+ overflowX: html.style.overflowX,
94
+ };
95
+ originalHtmlScrollBehavior = html.style.scrollBehavior;
96
+
97
+ originalBodyStyles = {
98
+ position: body.style.position,
99
+ height: body.style.height,
100
+ width: body.style.width,
101
+ boxSizing: body.style.boxSizing,
102
+ overflowY: body.style.overflowY,
103
+ overflowX: body.style.overflowX,
104
+ scrollBehavior: body.style.scrollBehavior,
105
+ };
106
+
107
+ const isScrollableY = html.scrollHeight > html.clientHeight;
108
+ const isScrollableX = html.scrollWidth > html.clientWidth;
109
+ const hasConstantOverflowY =
110
+ htmlStyles.overflowY === 'scroll' || bodyStyles.overflowY === 'scroll';
111
+ const hasConstantOverflowX =
112
+ htmlStyles.overflowX === 'scroll' || bodyStyles.overflowX === 'scroll';
113
+
114
+ const scrollbarWidth = Math.max(0, win.innerWidth - body.clientWidth);
115
+ const scrollbarHeight = Math.max(0, win.innerHeight - body.clientHeight);
116
+
117
+ const marginY = parseFloat(bodyStyles.marginTop) + parseFloat(bodyStyles.marginBottom);
118
+ const marginX = parseFloat(bodyStyles.marginLeft) + parseFloat(bodyStyles.marginRight);
119
+ const elementToLock = isOverflowElement(html) ? html : body;
120
+
121
+ updateGutterOnly = supportsStableScrollbarGutter(referenceElement);
122
+
123
+ if (updateGutterOnly) {
124
+ html.style.scrollbarGutter = scrollbarGutterValue;
125
+ elementToLock.style.overflowY = 'hidden';
126
+ elementToLock.style.overflowX = 'hidden';
127
+ return;
128
+ }
129
+
130
+ Object.assign(html.style, {
131
+ scrollbarGutter: scrollbarGutterValue,
132
+ overflowY: 'hidden',
133
+ overflowX: 'hidden',
134
+ });
135
+
136
+ if (isScrollableY || hasConstantOverflowY) {
137
+ html.style.overflowY = 'scroll';
138
+ }
139
+ if (isScrollableX || hasConstantOverflowX) {
140
+ html.style.overflowX = 'scroll';
141
+ }
142
+
143
+ Object.assign(body.style, {
144
+ position: 'relative',
145
+ height:
146
+ marginY || scrollbarHeight ? `calc(100dvh - ${marginY + scrollbarHeight}px)` : '100dvh',
147
+ width: marginX || scrollbarWidth ? `calc(100vw - ${marginX + scrollbarWidth}px)` : '100vw',
148
+ boxSizing: 'border-box',
149
+ overflow: 'hidden',
150
+ scrollBehavior: 'unset',
151
+ });
152
+
153
+ body.scrollTop = scrollTop;
154
+ body.scrollLeft = scrollLeft;
155
+ html.setAttribute('data-base-ui-scroll-locked', '');
156
+ html.style.scrollBehavior = 'unset';
157
+ }
158
+
159
+ function cleanup() {
160
+ Object.assign(html.style, originalHtmlStyles);
161
+ Object.assign(body.style, originalBodyStyles);
162
+ if (!updateGutterOnly) {
163
+ html.scrollTop = scrollTop;
164
+ html.scrollLeft = scrollLeft;
165
+ html.removeAttribute('data-base-ui-scroll-locked');
166
+ html.style.scrollBehavior = originalHtmlScrollBehavior;
167
+ }
168
+ }
169
+
170
+ function handleResize() {
171
+ cleanup();
172
+ resizeFrame.request(lockScroll);
173
+ }
174
+
175
+ lockScroll();
176
+ const unsubscribeResize = addEventListener(win, 'resize', handleResize);
177
+
178
+ return () => {
179
+ resizeFrame.cancel();
180
+ cleanup();
181
+ if (typeof win.removeEventListener === 'function') {
182
+ unsubscribeResize();
183
+ }
184
+ };
185
+ }
186
+
187
+ class ScrollLocker {
188
+ lockCount = 0;
189
+ restore = null as (() => void) | null;
190
+ timeoutLock = Timeout.create();
191
+ timeoutUnlock = Timeout.create();
192
+
193
+ acquire(referenceElement: Element | null) {
194
+ this.lockCount += 1;
195
+ if (this.lockCount === 1 && this.restore === null) {
196
+ this.timeoutLock.start(0, () => this.lock(referenceElement));
197
+ }
198
+ return this.release;
199
+ }
200
+
201
+ release = () => {
202
+ this.lockCount -= 1;
203
+ if (this.lockCount === 0 && this.restore) {
204
+ this.timeoutUnlock.start(0, this.unlock);
205
+ }
206
+ };
207
+
208
+ private unlock = () => {
209
+ if (this.lockCount === 0 && this.restore) {
210
+ this.restore?.();
211
+ this.restore = null;
212
+ }
213
+ };
214
+
215
+ private lock(referenceElement: Element | null) {
216
+ if (this.lockCount === 0 || this.restore !== null) {
217
+ return;
218
+ }
219
+ const doc = ownerDocument(referenceElement);
220
+ const html = doc.documentElement;
221
+ const htmlOverflowY = ownerWindow(html).getComputedStyle(html).overflowY;
222
+ if (htmlOverflowY === 'hidden' || htmlOverflowY === 'clip') {
223
+ this.restore = NOOP;
224
+ return;
225
+ }
226
+ const hasOverlayScrollbars = platform.os.ios || !hasInsetScrollbars(referenceElement);
227
+ this.restore = hasOverlayScrollbars
228
+ ? preventScrollOverlayScrollbars(referenceElement)
229
+ : preventScrollInsetScrollbars(referenceElement);
230
+ }
231
+ }
232
+
233
+ const SCROLL_LOCKER = new ScrollLocker();
234
+
235
+ /**
236
+ * Locks the scroll of the document when enabled. octane: threads an explicit slot for the effect.
237
+ */
238
+ export function useScrollLock(
239
+ enabled: boolean,
240
+ referenceElement: Element | null,
241
+ slot: symbol | undefined,
242
+ ): void {
243
+ useLayoutEffect(
244
+ () => {
245
+ if (!enabled) {
246
+ return undefined;
247
+ }
248
+ return SCROLL_LOCKER.acquire(referenceElement);
249
+ },
250
+ [enabled, referenceElement],
251
+ slot,
252
+ );
253
+ }