@octanejs/base-ui 0.1.1 → 0.1.3

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,73 @@
1
+ // Ported from .base-ui/packages/react/src/utils/popups/useTriggerFocusGuards.ts (v1.6.0), octane-
2
+ // adapted (native events; `ReactDOM.flushSync` → octane `flushSync`; the ref threads a slot). Focus
3
+ // guards placed around an OPEN popup's trigger (Popover/Menu): tabbing out of the trigger closes the
4
+ // popup and moves focus to the right neighbour.
5
+ import { useRef, flushSync } from 'octane';
6
+
7
+ import { subSlot } from '../../internal';
8
+ import { contains } from '../floating/element';
9
+ import {
10
+ getNextTabbable,
11
+ getTabbableAfterElement,
12
+ getTabbableBeforeElement,
13
+ isOutsideEvent,
14
+ type FocusableElement,
15
+ } from '../floating/tabbable';
16
+ import { createChangeEventDetails, REASONS } from '../createChangeEventDetails';
17
+
18
+ interface TriggerFocusGuardStore {
19
+ setOpen(open: boolean, eventDetails: any): void;
20
+ select(key: 'positionerElement'): HTMLElement | null;
21
+ context: {
22
+ readonly beforeContentFocusGuardRef: { current: HTMLElement | null };
23
+ readonly triggerFocusTargetRef: { current: HTMLElement | null };
24
+ };
25
+ }
26
+
27
+ export function useTriggerFocusGuards(
28
+ store: TriggerFocusGuardStore,
29
+ triggerElementRef: { current: HTMLElement | null },
30
+ slot: symbol | undefined,
31
+ ) {
32
+ const preFocusGuardRef = useRef<HTMLElement | null>(null, subSlot(slot, 'pre'));
33
+
34
+ function handlePreFocusGuardFocus(event: any) {
35
+ flushSync(() => {
36
+ store.setOpen(
37
+ false,
38
+ createChangeEventDetails(REASONS.focusOut, event, event.currentTarget as HTMLElement),
39
+ );
40
+ });
41
+ const previousTabbable: FocusableElement | null = getTabbableBeforeElement(
42
+ preFocusGuardRef.current,
43
+ );
44
+ previousTabbable?.focus();
45
+ }
46
+
47
+ function handleFocusTargetFocus(event: any) {
48
+ const positionerElement = store.select('positionerElement');
49
+ if (positionerElement && isOutsideEvent(event, positionerElement)) {
50
+ store.context.beforeContentFocusGuardRef.current?.focus();
51
+ } else {
52
+ flushSync(() => {
53
+ store.setOpen(
54
+ false,
55
+ createChangeEventDetails(REASONS.focusOut, event, event.currentTarget as HTMLElement),
56
+ );
57
+ });
58
+ let nextTabbable = getTabbableAfterElement(
59
+ store.context.triggerFocusTargetRef.current || triggerElementRef.current,
60
+ );
61
+ while (nextTabbable !== null && contains(positionerElement, nextTabbable)) {
62
+ const prevTabbable = nextTabbable;
63
+ nextTabbable = getNextTabbable(nextTabbable);
64
+ if (nextTabbable === prevTabbable) {
65
+ break;
66
+ }
67
+ }
68
+ nextTabbable?.focus();
69
+ }
70
+ }
71
+
72
+ return { preFocusGuardRef, handlePreFocusGuardFocus, handleFocusTargetFocus };
73
+ }
@@ -0,0 +1,168 @@
1
+ // Ported from .base-ui/packages/utils/src/store/ReactStore.ts (v1.6.0), octane-adapted. A Store
2
+ // with controlled-state keys, non-reactive `context`, and named `selectors`. octane adaptations:
3
+ // every hook-bearing method threads an explicit slot (octane hooks are slot-keyed); the fiber
4
+ // "fastHooks" path is dropped for the plain `useStore`; React dev-warning/`useDebugValue` blocks
5
+ // are dropped (functional outcomes only). `useIsoLayoutEffect` → octane `useLayoutEffect`.
6
+ import { useLayoutEffect, useRef } from 'octane';
7
+ import { subSlot } from '../../internal';
8
+ import { useStableCallback } from '../useStableCallback';
9
+ import { NOOP } from '../noop';
10
+ import { Store } from './Store';
11
+ import { useStore } from './useStore';
12
+
13
+ type SelectorFunction<State> = (state: State, a1?: any, a2?: any, a3?: any) => any;
14
+
15
+ export class ReactStore<
16
+ State extends object,
17
+ Context = Record<string, never>,
18
+ Selectors extends Record<string, SelectorFunction<State>> = Record<string, never>,
19
+ > extends Store<State> {
20
+ readonly context: Context;
21
+
22
+ private selectors: Selectors | undefined;
23
+
24
+ constructor(state: State, context: Context = {} as Context, selectors?: Selectors) {
25
+ super(state);
26
+ this.context = context;
27
+ this.selectors = selectors;
28
+ }
29
+
30
+ /** Sync a single external value into the store (in a layout effect). */
31
+ useSyncedValue<Key extends keyof State>(
32
+ key: Key,
33
+ value: State[Key],
34
+ slot: symbol | undefined,
35
+ ): void {
36
+ const store = this;
37
+ useLayoutEffect(
38
+ () => {
39
+ if (store.state[key] !== value) {
40
+ store.set(key, value);
41
+ }
42
+ },
43
+ [store, key, value],
44
+ slot,
45
+ );
46
+ }
47
+
48
+ /** Sync a single external value into the store; reset to `undefined` on unmount. */
49
+ useSyncedValueWithCleanup<Key extends keyof State>(
50
+ key: Key,
51
+ value: State[Key],
52
+ slot: symbol | undefined,
53
+ ): void {
54
+ const store = this;
55
+ useLayoutEffect(
56
+ () => {
57
+ if (store.state[key] !== value) {
58
+ store.set(key, value);
59
+ }
60
+ return () => {
61
+ store.set(key, undefined as State[Key]);
62
+ };
63
+ },
64
+ [store, key, value],
65
+ slot,
66
+ );
67
+ }
68
+
69
+ /** Sync multiple external values into the store. */
70
+ useSyncedValues(statePart: Partial<State>, slot: symbol | undefined): void {
71
+ const store = this;
72
+ const dependencies = Object.values(statePart);
73
+ useLayoutEffect(
74
+ () => {
75
+ store.update(statePart);
76
+ },
77
+ [store, ...dependencies],
78
+ slot,
79
+ );
80
+ }
81
+
82
+ /** Register a controllable prop: when `controlled` is defined, the store key tracks it. */
83
+ useControlledProp<Key extends keyof State>(
84
+ key: Key,
85
+ controlled: State[Key] | undefined,
86
+ slot: symbol | undefined,
87
+ ): void {
88
+ const store = this;
89
+ const isControlled = controlled !== undefined;
90
+ useLayoutEffect(
91
+ () => {
92
+ if (isControlled && !Object.is(store.state[key], controlled)) {
93
+ store.setState({ ...store.state, [key]: controlled });
94
+ }
95
+ },
96
+ [store, key, controlled, isControlled],
97
+ slot,
98
+ );
99
+ }
100
+
101
+ /** Non-hook: read a selector's current value synchronously. */
102
+ select<Key extends keyof Selectors>(
103
+ key: Key,
104
+ a1?: unknown,
105
+ a2?: unknown,
106
+ a3?: unknown,
107
+ ): ReturnType<Selectors[Key]> {
108
+ const selector = this.selectors![key];
109
+ return selector(this.state, a1, a2, a3);
110
+ }
111
+
112
+ /** Subscribe to a selector's value (re-renders on change). */
113
+ useState<Key extends keyof Selectors>(
114
+ key: Key,
115
+ slot: symbol | undefined,
116
+ a1?: unknown,
117
+ a2?: unknown,
118
+ a3?: unknown,
119
+ ): ReturnType<Selectors[Key]> {
120
+ return useStore(this, this.selectors![key] as any, slot, a1, a2, a3);
121
+ }
122
+
123
+ /** Wrap a callback with `useStableCallback` and assign it to `context[key]`. */
124
+ useContextCallback<Key extends keyof Context>(
125
+ key: Key,
126
+ fn: ((...args: any[]) => any) | undefined,
127
+ slot: symbol | undefined,
128
+ ): void {
129
+ const stableFunction = useStableCallback(fn ?? (NOOP as (...args: any[]) => any), slot);
130
+ (this.context as Record<any, any>)[key] = stableFunction;
131
+ }
132
+
133
+ /** A stable ref-callback setter for a state key (commonly passed as a `ref`). */
134
+ useStateSetter<Key extends keyof State>(
135
+ key: Key,
136
+ slot: symbol | undefined,
137
+ ): (value: State[Key]) => void {
138
+ const store = this;
139
+ const ref = useRef<((v: State[Key]) => void) | undefined>(undefined, slot);
140
+ if (ref.current === undefined) {
141
+ ref.current = (value: State[Key]) => {
142
+ store.set(key, value);
143
+ };
144
+ }
145
+ return ref.current;
146
+ }
147
+
148
+ /** Non-hook: observe a selector; call `listener` when its value changes (fires once now). */
149
+ observe(
150
+ selector: keyof Selectors | ((state: State) => any),
151
+ listener: (newValue: any, oldValue: any, store: this) => void,
152
+ ): () => void {
153
+ const selectFn: (state: State) => any =
154
+ typeof selector === 'function' ? selector : (this.selectors![selector] as any);
155
+
156
+ let prevValue = selectFn(this.state);
157
+ listener(prevValue, prevValue, this);
158
+
159
+ return this.subscribe((nextState) => {
160
+ const nextValue = selectFn(nextState);
161
+ if (!Object.is(prevValue, nextValue)) {
162
+ const oldValue = prevValue;
163
+ prevValue = nextValue;
164
+ listener(nextValue, oldValue, this);
165
+ }
166
+ });
167
+ }
168
+ }
@@ -0,0 +1,84 @@
1
+ // Ported from .base-ui/packages/utils/src/store/Store.ts (v1.6.0). The observer-pattern data
2
+ // store used by every Base UI popup (Dialog/Popover/Tooltip/Menu/…). Pure state management —
3
+ // the ONLY octane adaptation is the `.use()` hook method, which forwards an explicit slot to the
4
+ // slot-based `useStore` (octane hooks are keyed by compiler/threaded slot, not call order).
5
+ import { useStore } from './useStore';
6
+
7
+ type Listener<T> = (state: T) => void;
8
+
9
+ export class Store<State> {
10
+ state: State;
11
+
12
+ private listeners: Set<Listener<State>>;
13
+
14
+ // Internal state to handle recursive `setState()` calls.
15
+ private updateTick: number;
16
+
17
+ constructor(state: State) {
18
+ this.state = state;
19
+ this.listeners = new Set();
20
+ this.updateTick = 0;
21
+ }
22
+
23
+ subscribe = (fn: Listener<State>) => {
24
+ this.listeners.add(fn);
25
+ return () => {
26
+ this.listeners.delete(fn);
27
+ };
28
+ };
29
+
30
+ getSnapshot = () => {
31
+ return this.state;
32
+ };
33
+
34
+ setState(newState: State) {
35
+ if (this.state === newState) {
36
+ return;
37
+ }
38
+
39
+ this.state = newState;
40
+ this.updateTick += 1;
41
+
42
+ const currentTick = this.updateTick;
43
+ for (const listener of this.listeners) {
44
+ if (currentTick !== this.updateTick) {
45
+ // A recursive `setState` already notified all listeners.
46
+ return;
47
+ }
48
+ listener(newState);
49
+ }
50
+ }
51
+
52
+ update(changes: Partial<State>) {
53
+ for (const key in changes) {
54
+ if (!Object.is(this.state[key], changes[key])) {
55
+ this.setState({ ...this.state, ...changes });
56
+ return;
57
+ }
58
+ }
59
+ }
60
+
61
+ set<T>(key: keyof State, value: T) {
62
+ if (!Object.is(this.state[key], value)) {
63
+ this.setState({ ...this.state, [key]: value });
64
+ }
65
+ }
66
+
67
+ notifyAll() {
68
+ const newState = { ...this.state };
69
+ this.setState(newState);
70
+ }
71
+
72
+ // octane: slot is threaded explicitly (was `use(selector, ...args)` in React).
73
+ use<F extends (...args: any) => any>(
74
+ selector: F,
75
+ slot: symbol | undefined,
76
+ a1?: unknown,
77
+ a2?: unknown,
78
+ a3?: unknown,
79
+ ): ReturnType<F> {
80
+ return useStore(this, selector as any, slot, a1, a2, a3) as ReturnType<F>;
81
+ }
82
+ }
83
+
84
+ export type ReadonlyStore<State> = Pick<Store<State>, 'getSnapshot' | 'subscribe' | 'state'>;
@@ -0,0 +1,66 @@
1
+ // Ported from .base-ui/packages/utils/src/store/createSelector.ts (v1.6.0). Combines up to six
2
+ // selector functions into one. Pure — the elaborate reselect-based generic types are simplified
3
+ // (the runtime is unchanged); the memoized variant (`createSelectorMemoized`, reselect-backed) is
4
+ // only ported if/when a component needs it.
5
+ export type CreateSelectorFunction = (...items: any[]) => (...args: any[]) => any;
6
+
7
+ /* eslint-disable id-denylist */
8
+ export const createSelector = ((
9
+ a: Function,
10
+ b?: Function,
11
+ c?: Function,
12
+ d?: Function,
13
+ e?: Function,
14
+ f?: Function,
15
+ ...other: any[]
16
+ ) => {
17
+ if (other.length > 0) {
18
+ throw new Error('Unsupported number of selectors');
19
+ }
20
+
21
+ let selector: any;
22
+
23
+ if (a && b && c && d && e && f) {
24
+ selector = (state: any, a1: any, a2: any, a3: any) => {
25
+ const va = a(state, a1, a2, a3);
26
+ const vb = b(state, a1, a2, a3);
27
+ const vc = c(state, a1, a2, a3);
28
+ const vd = d(state, a1, a2, a3);
29
+ const ve = e(state, a1, a2, a3);
30
+ return f(va, vb, vc, vd, ve, a1, a2, a3);
31
+ };
32
+ } else if (a && b && c && d && e) {
33
+ selector = (state: any, a1: any, a2: any, a3: any) => {
34
+ const va = a(state, a1, a2, a3);
35
+ const vb = b(state, a1, a2, a3);
36
+ const vc = c(state, a1, a2, a3);
37
+ const vd = d(state, a1, a2, a3);
38
+ return e(va, vb, vc, vd, a1, a2, a3);
39
+ };
40
+ } else if (a && b && c && d) {
41
+ selector = (state: any, a1: any, a2: any, a3: any) => {
42
+ const va = a(state, a1, a2, a3);
43
+ const vb = b(state, a1, a2, a3);
44
+ const vc = c(state, a1, a2, a3);
45
+ return d(va, vb, vc, a1, a2, a3);
46
+ };
47
+ } else if (a && b && c) {
48
+ selector = (state: any, a1: any, a2: any, a3: any) => {
49
+ const va = a(state, a1, a2, a3);
50
+ const vb = b(state, a1, a2, a3);
51
+ return c(va, vb, a1, a2, a3);
52
+ };
53
+ } else if (a && b) {
54
+ selector = (state: any, a1: any, a2: any, a3: any) => {
55
+ const va = a(state, a1, a2, a3);
56
+ return b(va, a1, a2, a3);
57
+ };
58
+ } else if (a) {
59
+ selector = a;
60
+ } else {
61
+ throw new Error('Missing arguments');
62
+ }
63
+
64
+ return selector;
65
+ }) as unknown as CreateSelectorFunction;
66
+ /* eslint-enable id-denylist */
@@ -0,0 +1,49 @@
1
+ // Ported from .base-ui/packages/utils/src/store/useStore.ts, octane-adapted. React's version has
2
+ // a fiber-instance "fastHooks" optimization + a legacy `useSyncExternalStoreWithSelector` shim for
3
+ // concurrent rendering. octane renders synchronously (a render always commits), so — exactly like
4
+ // `@octanejs/zustand/traditional` — we build directly on octane's real `useSyncExternalStore`: a
5
+ // ref caches the last selection and `getSnapshot` returns that SAME reference while the selection
6
+ // is Object.is-equal, so useSyncExternalStore's own equality check bails out the re-render.
7
+ //
8
+ // SLOT: plain-`.ts` hook; the caller threads a slot (the ref + the store subscription each derive
9
+ // a distinct sub-slot).
10
+ import { useSyncExternalStore, useRef } from 'octane';
11
+ import { subSlot } from '../../internal';
12
+ import type { ReadonlyStore } from './Store';
13
+
14
+ interface SelectionCell<U> {
15
+ hasValue: boolean;
16
+ value: U | undefined;
17
+ }
18
+
19
+ export function useStore<State, Value>(
20
+ store: ReadonlyStore<State>,
21
+ selector: (state: State, a1?: unknown, a2?: unknown, a3?: unknown) => Value,
22
+ slot: symbol | undefined,
23
+ a1?: unknown,
24
+ a2?: unknown,
25
+ a3?: unknown,
26
+ ): Value {
27
+ const cache = useRef<SelectionCell<Value>>(
28
+ { hasValue: false, value: undefined },
29
+ subSlot(slot, 'sel'),
30
+ );
31
+
32
+ const select = (state: State): Value => {
33
+ const next = selector(state, a1, a2, a3);
34
+ const c = cache.current;
35
+ if (c.hasValue && Object.is(c.value as Value, next)) {
36
+ return c.value as Value;
37
+ }
38
+ c.hasValue = true;
39
+ c.value = next;
40
+ return next;
41
+ };
42
+
43
+ return useSyncExternalStore(
44
+ store.subscribe,
45
+ () => select(store.getSnapshot()),
46
+ () => select(store.getSnapshot()),
47
+ subSlot(slot, 'uses'),
48
+ );
49
+ }