@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,198 @@
1
+ // Ported from .base-ui/packages/react/src/floating-ui-react/hooks/useFloating.ts (v1.6.0),
2
+ // octane-adapted (slot-threaded). Base UI wraps `@floating-ui/react-dom`'s `useFloating` (imported
3
+ // as `usePosition`) with its Store-based `FloatingRootStore`; octane swaps the react-dom wrapper for
4
+ // `@octanejs/floating-ui`'s `usePositionFloating` (the octane port of exactly that hook), keeping
5
+ // all of Base UI's store logic. Everything else — reference/floating element wiring, position
6
+ // reference bridging, the `FloatingContext` shape — is faithful.
7
+ import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'octane';
8
+ import { usePositionFloating } from '@octanejs/floating-ui';
9
+
10
+ import { S, subSlot } from '../../internal';
11
+ import { isElement } from '../dom';
12
+ import { useFloatingTree } from './FloatingTree';
13
+ import { useFloatingRootContext } from './useFloatingRootContext';
14
+ import type { FloatingRootStore } from './FloatingRootStore';
15
+
16
+ export function useFloating(options: any = {}, slot?: symbol | undefined): any {
17
+ const localSlot = slot ?? S('useFloating');
18
+ const { nodeId, externalTree } = options;
19
+
20
+ const internalStore = useFloatingRootContext(options, subSlot(localSlot, 'internal'));
21
+ const store: FloatingRootStore = options.rootContext || internalStore;
22
+
23
+ const referenceElement = store.useState('referenceElement', subSlot(localSlot, 'ref'));
24
+ const floatingElement = store.useState('floatingElement', subSlot(localSlot, 'flo'));
25
+ const domReferenceElement = store.useState('domReferenceElement', subSlot(localSlot, 'dref'));
26
+ const open = store.useState('open', subSlot(localSlot, 'open'));
27
+ const floatingId = store.useState('floatingId', subSlot(localSlot, 'fid'));
28
+
29
+ const [positionReference, setPositionReferenceRaw] = useState<any>(
30
+ null,
31
+ subSlot(localSlot, 'posref'),
32
+ );
33
+ const [localDomReference, setLocalDomReference] = useState<any>(
34
+ undefined,
35
+ subSlot(localSlot, 'ldref'),
36
+ );
37
+ const [localFloatingElement, setLocalFloatingElement] = useState<any>(
38
+ undefined,
39
+ subSlot(localSlot, 'lflo'),
40
+ );
41
+
42
+ const domReferenceRef = useRef<any>(null, subSlot(localSlot, 'drefref'));
43
+
44
+ const tree = useFloatingTree(externalTree);
45
+
46
+ const storeElements = useMemo(
47
+ () => ({
48
+ reference: referenceElement,
49
+ floating: floatingElement,
50
+ domReference: domReferenceElement,
51
+ }),
52
+ [referenceElement, floatingElement, domReferenceElement],
53
+ subSlot(localSlot, 'm:se'),
54
+ );
55
+
56
+ const position = usePositionFloating([
57
+ {
58
+ ...options,
59
+ elements: {
60
+ ...storeElements,
61
+ ...(positionReference && { reference: positionReference }),
62
+ },
63
+ },
64
+ subSlot(localSlot, 'pos'),
65
+ ]);
66
+
67
+ const localDomReferenceElement = isElement(localDomReference)
68
+ ? (localDomReference as Element)
69
+ : null;
70
+
71
+ const syncedFloatingElement =
72
+ localFloatingElement === undefined ? store.state.floatingElement : localFloatingElement;
73
+
74
+ store.useSyncedValue('referenceElement', localDomReference ?? null, subSlot(localSlot, 's:ref'));
75
+ store.useSyncedValue(
76
+ 'domReferenceElement',
77
+ localDomReference === undefined ? domReferenceElement : localDomReferenceElement,
78
+ subSlot(localSlot, 's:dref'),
79
+ );
80
+ store.useSyncedValue('floatingElement', syncedFloatingElement, subSlot(localSlot, 's:flo'));
81
+
82
+ const setPositionReference = useCallback(
83
+ (node: any) => {
84
+ const computedPositionReference = isElement(node)
85
+ ? {
86
+ getBoundingClientRect: () => node.getBoundingClientRect(),
87
+ getClientRects: () => node.getClientRects(),
88
+ contextElement: node,
89
+ }
90
+ : node;
91
+ setPositionReferenceRaw(computedPositionReference);
92
+ position.refs.setReference(computedPositionReference);
93
+ },
94
+ [position.refs],
95
+ subSlot(localSlot, 'spr'),
96
+ );
97
+
98
+ const setReference = useCallback(
99
+ (node: any) => {
100
+ if (isElement(node) || node === null) {
101
+ domReferenceRef.current = node;
102
+ setLocalDomReference(node);
103
+ }
104
+
105
+ if (
106
+ isElement(position.refs.reference.current) ||
107
+ position.refs.reference.current === null ||
108
+ (node !== null && !isElement(node))
109
+ ) {
110
+ position.refs.setReference(node);
111
+ }
112
+ },
113
+ [position.refs],
114
+ subSlot(localSlot, 'sr'),
115
+ );
116
+
117
+ const setFloating = useCallback(
118
+ (node: any) => {
119
+ setLocalFloatingElement(node);
120
+ position.refs.setFloating(node);
121
+ },
122
+ [position.refs],
123
+ subSlot(localSlot, 'sf'),
124
+ );
125
+
126
+ const refs = useMemo(
127
+ () => ({
128
+ ...position.refs,
129
+ setReference,
130
+ setFloating,
131
+ setPositionReference,
132
+ domReference: domReferenceRef,
133
+ }),
134
+ [position.refs, setReference, setFloating, setPositionReference],
135
+ subSlot(localSlot, 'm:refs'),
136
+ );
137
+
138
+ const elements = useMemo(
139
+ () => ({
140
+ ...position.elements,
141
+ domReference: domReferenceElement,
142
+ }),
143
+ [position.elements, domReferenceElement],
144
+ subSlot(localSlot, 'm:el'),
145
+ );
146
+
147
+ const context = useMemo(
148
+ () => ({
149
+ ...position,
150
+ dataRef: store.context.dataRef,
151
+ open,
152
+ onOpenChange: store.setOpen,
153
+ events: store.context.events,
154
+ floatingId,
155
+ refs,
156
+ elements,
157
+ nodeId,
158
+ rootStore: store,
159
+ }),
160
+ [position, refs, elements, nodeId, store, open, floatingId],
161
+ subSlot(localSlot, 'm:ctx'),
162
+ );
163
+
164
+ useLayoutEffect(
165
+ () => {
166
+ if (domReferenceElement) {
167
+ domReferenceRef.current = domReferenceElement;
168
+ }
169
+ },
170
+ [domReferenceElement],
171
+ subSlot(localSlot, 'e:dref'),
172
+ );
173
+
174
+ useLayoutEffect(
175
+ () => {
176
+ store.context.dataRef.current.floatingContext = context;
177
+
178
+ const node = tree?.nodesRef.current.find((n: any) => n.id === nodeId);
179
+ if (node) {
180
+ node.context = context;
181
+ }
182
+ },
183
+ undefined,
184
+ subSlot(localSlot, 'e:ctx'),
185
+ );
186
+
187
+ return useMemo(
188
+ () => ({
189
+ ...position,
190
+ context,
191
+ refs,
192
+ elements,
193
+ rootStore: store,
194
+ }),
195
+ [position, refs, elements, context, store],
196
+ subSlot(localSlot, 'm:ret'),
197
+ );
198
+ }
@@ -0,0 +1,81 @@
1
+ // Ported from .base-ui/packages/react/src/floating-ui-react/hooks/useFloatingRootContext.ts
2
+ // (v1.6.0), octane-adapted (slot-threaded). Creates the internal `FloatingRootStore` used when a
3
+ // consumer of `useFloating` doesn't pass its own `rootContext`. `floatingId` is a raw octane
4
+ // `useId` (no prefix), matching Base UI's `@base-ui/utils/useId`, so portal/popup ids stay
5
+ // byte-identical across the differential.
6
+ import { useId, useLayoutEffect } from 'octane';
7
+
8
+ import { S, subSlot } from '../../internal';
9
+ import { isElement } from '../dom';
10
+ import { useRefWithInit } from '../useRefWithInit';
11
+ import { PopupTriggerMap } from '../popups';
12
+ import { useFloatingParentNodeId } from './FloatingTree';
13
+ import { FloatingRootStore, type FloatingRootState } from './FloatingRootStore';
14
+
15
+ export interface UseFloatingRootContextOptions {
16
+ open?: boolean | undefined;
17
+ onOpenChange?(open: boolean, eventDetails: any): void;
18
+ elements?:
19
+ | {
20
+ reference?: any;
21
+ floating?: HTMLElement | null | undefined;
22
+ }
23
+ | undefined;
24
+ }
25
+
26
+ export function useFloatingRootContext(
27
+ options: UseFloatingRootContextOptions,
28
+ slot: symbol | undefined,
29
+ ): FloatingRootStore {
30
+ const localSlot = slot ?? S('useFloatingRootContext');
31
+ const { open = false, onOpenChange, elements = {} } = options;
32
+
33
+ const floatingId = useId(subSlot(localSlot, 'id'));
34
+ const nested = useFloatingParentNodeId() != null;
35
+
36
+ const store = useRefWithInit<FloatingRootStore>(
37
+ () =>
38
+ new FloatingRootStore({
39
+ open,
40
+ transitionStatus: undefined,
41
+ onOpenChange,
42
+ referenceElement: elements.reference ?? null,
43
+ floatingElement: elements.floating ?? null,
44
+ triggerElements: new PopupTriggerMap(),
45
+ floatingId,
46
+ syncOnly: false,
47
+ nested,
48
+ }),
49
+ subSlot(localSlot, 'store'),
50
+ ).current;
51
+
52
+ useLayoutEffect(
53
+ () => {
54
+ const valuesToSync: Partial<FloatingRootState> = {
55
+ open,
56
+ floatingId,
57
+ };
58
+
59
+ // Only sync elements that are defined to avoid overwriting existing ones
60
+ if (elements.reference !== undefined) {
61
+ valuesToSync.referenceElement = elements.reference;
62
+ valuesToSync.domReferenceElement = isElement(elements.reference)
63
+ ? elements.reference
64
+ : null;
65
+ }
66
+
67
+ if (elements.floating !== undefined) {
68
+ valuesToSync.floatingElement = elements.floating;
69
+ }
70
+
71
+ store.update(valuesToSync);
72
+ },
73
+ [open, floatingId, elements.reference, elements.floating, store],
74
+ subSlot(localSlot, 'eff'),
75
+ );
76
+
77
+ store.context.onOpenChange = onOpenChange;
78
+ store.context.nested = nested;
79
+
80
+ return store;
81
+ }
@@ -0,0 +1,12 @@
1
+ // STUB — the popup side of the open-on-hover interaction (Base UI's useHoverFloatingInteraction:
2
+ // keep-open-while-hovering-popup + closeDelay + safePolygon, part of the ~1400-line hover system).
3
+ // It is only active when a trigger sets `openOnHover` (off by default), so returning nothing here
4
+ // leaves click-to-open Popover/Tooltip fully functional. TODO: port the real popup-hover keep-open
5
+ // behavior alongside `useHoverReferenceInteraction` when hover-open tests land.
6
+ export function useHoverFloatingInteraction(
7
+ _context: any,
8
+ _props: any,
9
+ _slot?: symbol | undefined,
10
+ ): void {
11
+ // no-op while hover-open is out of scope
12
+ }
@@ -0,0 +1,17 @@
1
+ // STUB — the full open-on-hover interaction (Base UI's useHoverReferenceInteraction + useHover +
2
+ // safePolygon, ~1400 lines) is deferred. It is only active when a trigger sets `openOnHover` (off by
3
+ // default), so returning `{}` here leaves click-to-open Popover/Tooltip fully functional. `safePolygon`
4
+ // is a placeholder passed to `handleClose` (ignored while the hook is inert). TODO: port the real
5
+ // hover (useHover pointer tracking + safePolygon "safe area" close intent) when hover-open tests land.
6
+
7
+ export function safePolygon(): (...args: any[]) => any {
8
+ return () => undefined;
9
+ }
10
+
11
+ export function useHoverReferenceInteraction(
12
+ _context: any,
13
+ _props: any,
14
+ _slot?: symbol | undefined,
15
+ ): Record<string, any> {
16
+ return {};
17
+ }
@@ -0,0 +1,93 @@
1
+ // Ported from .base-ui/packages/react/src/floating-ui-react/hooks/useSyncedFloatingRootContext.ts
2
+ // (v1.6.0), octane-adapted (slot-threaded). Keeps a FloatingRootStore in sync with a popup store:
3
+ // reuses a provided root context, else creates one once and updates it each render from the popup
4
+ // store's open/reference/floating state.
5
+ import { useRef, useLayoutEffect } from 'octane';
6
+
7
+ import { subSlot } from '../../internal';
8
+ import { isElement } from '../dom';
9
+ import type { ReactStore } from '../store/ReactStore';
10
+ import type { PopupStoreContext, PopupStoreSelectors, PopupStoreState } from '../popups/store';
11
+ import { FloatingRootStore, type FloatingRootState } from './FloatingRootStore';
12
+
13
+ export interface UseSyncedFloatingRootContextOptions<State extends PopupStoreState<unknown>> {
14
+ popupStore: ReactStore<State, PopupStoreContext<any>, PopupStoreSelectors>;
15
+ treatPopupAsFloatingElement?: boolean | undefined;
16
+ floatingRootContext?: FloatingRootStore | undefined;
17
+ floatingId: string | undefined;
18
+ nested: boolean;
19
+ onOpenChange(open: boolean, eventDetails: any): void;
20
+ }
21
+
22
+ export function useSyncedFloatingRootContext<State extends PopupStoreState<unknown>>(
23
+ options: UseSyncedFloatingRootContextOptions<State>,
24
+ slot: symbol | undefined,
25
+ ): FloatingRootStore {
26
+ const {
27
+ popupStore,
28
+ treatPopupAsFloatingElement = false,
29
+ floatingRootContext: floatingRootContextProp,
30
+ floatingId,
31
+ nested,
32
+ onOpenChange,
33
+ } = options;
34
+
35
+ const open = popupStore.useState('open', subSlot(slot, 'open'));
36
+ const referenceElement = popupStore.useState('activeTriggerElement', subSlot(slot, 'ref'));
37
+ const floatingElement = popupStore.useState(
38
+ treatPopupAsFloatingElement ? 'popupElement' : 'positionerElement',
39
+ subSlot(slot, 'floating'),
40
+ );
41
+ const triggerElements = popupStore.context.triggerElements;
42
+
43
+ const handleOpenChange = onOpenChange as (open: boolean, eventDetails: any) => void;
44
+
45
+ const internalStoreRef = useRef<FloatingRootStore | null>(null, subSlot(slot, 'store'));
46
+ if (floatingRootContextProp === undefined && internalStoreRef.current === null) {
47
+ internalStoreRef.current = new FloatingRootStore({
48
+ open,
49
+ transitionStatus: undefined,
50
+ referenceElement,
51
+ floatingElement,
52
+ triggerElements,
53
+ onOpenChange: handleOpenChange,
54
+ floatingId,
55
+ syncOnly: true,
56
+ nested,
57
+ });
58
+ }
59
+
60
+ const store = floatingRootContextProp ?? internalStoreRef.current!;
61
+
62
+ popupStore.useSyncedValue(
63
+ 'floatingId',
64
+ floatingId as State['floatingId'],
65
+ subSlot(slot, 'syncId'),
66
+ );
67
+
68
+ useLayoutEffect(
69
+ () => {
70
+ const valuesToSync: Partial<FloatingRootState> = {
71
+ open,
72
+ floatingId,
73
+ referenceElement,
74
+ floatingElement,
75
+ };
76
+ if (isElement(referenceElement)) {
77
+ valuesToSync.domReferenceElement = referenceElement;
78
+ }
79
+ if (store.state.positionReference === store.state.referenceElement) {
80
+ valuesToSync.positionReference = referenceElement;
81
+ }
82
+ store.update(valuesToSync);
83
+ },
84
+ [open, floatingId, referenceElement, floatingElement, store],
85
+ subSlot(slot, 'e:sync'),
86
+ );
87
+
88
+ // Keep non-reactive context values fresh for interactions that call `store.setOpen`.
89
+ store.context.onOpenChange = handleOpenChange;
90
+ store.context.nested = nested;
91
+
92
+ return store;
93
+ }
@@ -0,0 +1,12 @@
1
+ // Ported from .base-ui/packages/react/src/utils/getDisabledMountTransitionStyles.ts (v1.6.0).
2
+ // Returns a `{ style: { transition: 'none' } }` prop entry while a popup is in the `starting`
3
+ // transition phase so the initial mount frame doesn't animate; an empty object otherwise.
4
+ import { EMPTY_OBJECT } from './empty';
5
+ import { DISABLED_TRANSITIONS_STYLE } from './constants';
6
+ import type { TransitionStatus } from './useTransitionStatus';
7
+
8
+ export function getDisabledMountTransitionStyles(transitionStatus: TransitionStatus): {
9
+ style?: Record<string, any> | undefined;
10
+ } {
11
+ return transitionStatus === 'starting' ? DISABLED_TRANSITIONS_STYLE : EMPTY_OBJECT;
12
+ }
@@ -0,0 +1,22 @@
1
+ // Ported from .base-ui/packages/react/src/utils/hideMiddleware.ts (v1.6.0). Base UI imports the
2
+ // native `hide` from `@floating-ui/react-dom`; octane's `@octanejs/floating-ui` re-exports the same
3
+ // `@floating-ui/dom` `hide`, so we wrap that. Adds an extra "anchor hidden" heuristic: a reference
4
+ // rect that is fully zeroed (width/height/x/y all 0) is treated as hidden even if native `hide`
5
+ // doesn't flag it.
6
+ import { hide as nativeHide } from '@octanejs/floating-ui';
7
+
8
+ const nativeHideFn = (nativeHide() as any).fn;
9
+
10
+ export const hide: any = {
11
+ name: 'hide',
12
+ async fn(state: any) {
13
+ const { width, height, x, y } = state.rects.reference;
14
+ const anchorHidden = width === 0 && height === 0 && x === 0 && y === 0;
15
+ const nativeHideResult = await nativeHideFn(state);
16
+ return {
17
+ data: {
18
+ referenceHidden: nativeHideResult.data?.referenceHidden || anchorHidden,
19
+ },
20
+ };
21
+ },
22
+ };
@@ -0,0 +1,5 @@
1
+ // Ported from @base-ui/utils/inertValue. octane targets React-19 semantics, where `inert` is a
2
+ // real boolean attribute, so the value passes through unchanged.
3
+ export function inertValue(value?: boolean): boolean | undefined {
4
+ return value;
5
+ }
@@ -0,0 +1,13 @@
1
+ // Ported verbatim from @base-ui/utils/mergeCleanups. Combines cleanup fns (dropping falsy) into one.
2
+ type Cleanup = false | null | undefined | (() => void);
3
+
4
+ export function mergeCleanups(...cleanups: Cleanup[]): () => void {
5
+ return () => {
6
+ for (let i = 0; i < cleanups.length; i += 1) {
7
+ const cleanup = cleanups[i];
8
+ if (cleanup) {
9
+ cleanup();
10
+ }
11
+ }
12
+ };
13
+ }
@@ -1,15 +1,59 @@
1
- // Minimal port of @base-ui/utils/platform — only the `os.ios` check NumberField needs.
2
- // (jsdom is not iOS, so the iOS input-mode branch is inert in tests.)
1
+ // Minimal port of @base-ui/utils/platform — the checks the ported components need: `os.ios`
2
+ // (NumberField input mode), `engine.webkit` (useDismiss IME timing), `screenReader.voiceOver`
3
+ // (FocusGuard). All derive from the UA string; under jsdom they resolve to `false`, so the
4
+ // UA-specific branches stay inert in tests.
5
+ function ua(): string {
6
+ return typeof navigator === 'undefined' ? '' : (navigator.userAgent ?? '');
7
+ }
8
+
3
9
  function isIOS(): boolean {
4
10
  if (typeof navigator === 'undefined') {
5
11
  return false;
6
12
  }
7
13
  return /iP(ad|hone|od)/.test(navigator.platform ?? '');
8
14
  }
15
+
16
+ function isWebKit(): boolean {
17
+ const s = ua();
18
+ // WebKit but not Chromium/Blink (which also report "AppleWebKit").
19
+ return /AppleWebKit/.test(s) && !/Chrome|Chromium|Edg\//.test(s);
20
+ }
21
+
22
+ function isVoiceOver(): boolean {
23
+ // VoiceOver isn't directly detectable; Base UI infers it from Apple platforms. jsdom → false.
24
+ return /Mac OS X|iPhone|iPad/.test(ua());
25
+ }
26
+
27
+ function isAndroid(): boolean {
28
+ return /Android/.test(ua());
29
+ }
30
+
31
+ function isJsdom(): boolean {
32
+ return /jsdom/i.test(ua());
33
+ }
34
+
9
35
  export const platform = {
10
36
  os: {
11
37
  get ios() {
12
38
  return isIOS();
13
39
  },
40
+ get android() {
41
+ return isAndroid();
42
+ },
43
+ },
44
+ engine: {
45
+ get webkit() {
46
+ return isWebKit();
47
+ },
48
+ },
49
+ env: {
50
+ get jsdom() {
51
+ return isJsdom();
52
+ },
53
+ },
54
+ screenReader: {
55
+ get voiceOver() {
56
+ return isVoiceOver();
57
+ },
14
58
  },
15
59
  };
@@ -0,0 +1,48 @@
1
+ // Ported from .base-ui/packages/react/src/utils/popupStateMapping.ts (v1.6.0). Maps popup/trigger
2
+ // `open` (+ transition/anchor) state → the shared `data-*` attributes. Pure.
3
+ import type { StateAttributesMapping } from './getStateAttributesProps';
4
+
5
+ export const CommonPopupDataAttributes = {
6
+ open: 'data-open',
7
+ closed: 'data-closed',
8
+ startingStyle: 'data-starting-style',
9
+ endingStyle: 'data-ending-style',
10
+ anchorHidden: 'data-anchor-hidden',
11
+ side: 'data-side',
12
+ align: 'data-align',
13
+ } as const;
14
+
15
+ export const CommonTriggerDataAttributes = {
16
+ popupOpen: 'data-popup-open',
17
+ pressed: 'data-pressed',
18
+ } as const;
19
+
20
+ const TRIGGER_HOOK = { [CommonTriggerDataAttributes.popupOpen]: '' };
21
+ const PRESSABLE_TRIGGER_HOOK = {
22
+ [CommonTriggerDataAttributes.popupOpen]: '',
23
+ [CommonTriggerDataAttributes.pressed]: '',
24
+ };
25
+ const POPUP_OPEN_HOOK = { [CommonPopupDataAttributes.open]: '' };
26
+ const POPUP_CLOSED_HOOK = { [CommonPopupDataAttributes.closed]: '' };
27
+ const ANCHOR_HIDDEN_HOOK = { [CommonPopupDataAttributes.anchorHidden]: '' };
28
+
29
+ export const triggerOpenStateMapping: StateAttributesMapping<{ open: boolean }> = {
30
+ open(value: boolean) {
31
+ return value ? TRIGGER_HOOK : null;
32
+ },
33
+ };
34
+
35
+ export const pressableTriggerOpenStateMapping: StateAttributesMapping<{ open: boolean }> = {
36
+ open(value: boolean) {
37
+ return value ? PRESSABLE_TRIGGER_HOOK : null;
38
+ },
39
+ };
40
+
41
+ export const popupStateMapping: StateAttributesMapping<{ open: boolean; anchorHidden: boolean }> = {
42
+ open(value: boolean) {
43
+ return value ? POPUP_OPEN_HOOK : POPUP_CLOSED_HOOK;
44
+ },
45
+ anchorHidden(value: boolean) {
46
+ return value ? ANCHOR_HIDDEN_HOOK : null;
47
+ },
48
+ };
@@ -0,0 +1,4 @@
1
+ // Mirrors .base-ui/packages/react/src/utils/popups/index.ts — the popup store engine barrel.
2
+ export * from './store';
3
+ export * from './popupStoreUtils';
4
+ export * from './popupTriggerMap';