@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.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +33 -0
  3. package/package.json +47 -0
  4. package/src/avatar.ts +308 -0
  5. package/src/checkbox-group.ts +183 -0
  6. package/src/checkbox.ts +578 -0
  7. package/src/field.ts +676 -0
  8. package/src/fieldset.ts +150 -0
  9. package/src/form.ts +183 -0
  10. package/src/index.ts +23 -0
  11. package/src/input.ts +9 -0
  12. package/src/internal.ts +42 -0
  13. package/src/merge-props.ts +2 -0
  14. package/src/meter.ts +204 -0
  15. package/src/number-field.ts +978 -0
  16. package/src/progress.ts +260 -0
  17. package/src/radio-group.ts +285 -0
  18. package/src/radio.ts +401 -0
  19. package/src/separator.ts +43 -0
  20. package/src/slider.ts +1617 -0
  21. package/src/switch.ts +374 -0
  22. package/src/toggle-group.ts +136 -0
  23. package/src/toggle.ts +145 -0
  24. package/src/use-render.ts +40 -0
  25. package/src/utils/CheckboxGroupContext.ts +30 -0
  26. package/src/utils/CompositeRootContext.ts +27 -0
  27. package/src/utils/DirectionContext.ts +15 -0
  28. package/src/utils/RadioGroupContext.ts +27 -0
  29. package/src/utils/ToggleGroupContext.ts +33 -0
  30. package/src/utils/addEventListener.ts +13 -0
  31. package/src/utils/areArraysEqual.ts +11 -0
  32. package/src/utils/clamp.ts +8 -0
  33. package/src/utils/composeRefs.ts +53 -0
  34. package/src/utils/composite/CompositeItem.ts +56 -0
  35. package/src/utils/composite/CompositeList.ts +190 -0
  36. package/src/utils/composite/CompositeListContext.ts +23 -0
  37. package/src/utils/composite/CompositeRoot.ts +132 -0
  38. package/src/utils/composite/keys.ts +179 -0
  39. package/src/utils/composite/list-utils.ts +78 -0
  40. package/src/utils/composite/useCompositeItem.ts +58 -0
  41. package/src/utils/composite/useCompositeListItem.ts +108 -0
  42. package/src/utils/composite/useCompositeRoot.ts +346 -0
  43. package/src/utils/contains.ts +22 -0
  44. package/src/utils/createChangeEventDetails.ts +81 -0
  45. package/src/utils/field/FieldItemContext.ts +13 -0
  46. package/src/utils/field/FieldRootContext.ts +98 -0
  47. package/src/utils/field/FormContext.ts +37 -0
  48. package/src/utils/field/LabelableContext.ts +32 -0
  49. package/src/utils/field/LabelableProvider.ts +108 -0
  50. package/src/utils/field/constants.ts +80 -0
  51. package/src/utils/field/getCombinedFieldValidityData.ts +16 -0
  52. package/src/utils/field/useAriaLabelledBy.ts +86 -0
  53. package/src/utils/field/useFieldControlRegistration.ts +179 -0
  54. package/src/utils/field/useFieldValidation.ts +311 -0
  55. package/src/utils/field/useLabel.ts +95 -0
  56. package/src/utils/field/useLabelableId.ts +99 -0
  57. package/src/utils/field/useRegisterFieldControl.ts +41 -0
  58. package/src/utils/formatNumber.ts +44 -0
  59. package/src/utils/getDefaultFormSubmitter.ts +19 -0
  60. package/src/utils/getElementRef.ts +13 -0
  61. package/src/utils/getStateAttributesProps.ts +34 -0
  62. package/src/utils/matchesFocusVisible.ts +16 -0
  63. package/src/utils/mergeObjects.ts +18 -0
  64. package/src/utils/mergeProps.ts +206 -0
  65. package/src/utils/noop.ts +4 -0
  66. package/src/utils/number/constants.ts +4 -0
  67. package/src/utils/number/parse.ts +227 -0
  68. package/src/utils/number/types.ts +25 -0
  69. package/src/utils/number/validate.ts +129 -0
  70. package/src/utils/owner.ts +9 -0
  71. package/src/utils/platform.ts +15 -0
  72. package/src/utils/resolveAriaLabelledBy.ts +11 -0
  73. package/src/utils/resolveClassName.ts +8 -0
  74. package/src/utils/resolveRef.ts +10 -0
  75. package/src/utils/resolveStyle.ts +10 -0
  76. package/src/utils/serializeValue.ts +15 -0
  77. package/src/utils/slider/asc.ts +4 -0
  78. package/src/utils/slider/getMidpoint.ts +14 -0
  79. package/src/utils/slider/getPushedThumbValues.ts +73 -0
  80. package/src/utils/slider/getSliderValue.ts +27 -0
  81. package/src/utils/slider/replaceArrayItemAtIndex.ts +8 -0
  82. package/src/utils/slider/resolveThumbCollision.ts +178 -0
  83. package/src/utils/slider/roundValueToStep.ts +22 -0
  84. package/src/utils/slider/validateMinimumDistance.ts +22 -0
  85. package/src/utils/slider/valueArrayToPercentages.ts +11 -0
  86. package/src/utils/stringifyLocale.ts +10 -0
  87. package/src/utils/useAnimationFrame.ts +59 -0
  88. package/src/utils/useAnimationsFinished.ts +100 -0
  89. package/src/utils/useBaseUiId.ts +19 -0
  90. package/src/utils/useButton.ts +217 -0
  91. package/src/utils/useCheckboxGroupParent.ts +113 -0
  92. package/src/utils/useControlled.ts +42 -0
  93. package/src/utils/useFocusableWhenDisabled.ts +84 -0
  94. package/src/utils/useForcedRerendering.ts +11 -0
  95. package/src/utils/useIsHydrating.ts +9 -0
  96. package/src/utils/useOpenChangeComplete.ts +46 -0
  97. package/src/utils/usePressAndHold.ts +35 -0
  98. package/src/utils/useRefWithInit.ts +23 -0
  99. package/src/utils/useRegisteredLabelId.ts +32 -0
  100. package/src/utils/useRenderElement.ts +165 -0
  101. package/src/utils/useStableCallback.ts +26 -0
  102. package/src/utils/useTimeout.ts +51 -0
  103. package/src/utils/useTransitionStatus.ts +115 -0
  104. package/src/utils/useValueAsRef.ts +25 -0
  105. package/src/utils/useValueChanged.ts +37 -0
  106. package/src/utils/valueToPercent.ts +4 -0
  107. package/src/utils/visuallyHidden.ts +24 -0
@@ -0,0 +1,30 @@
1
+ // Ported from .base-ui/packages/react/src/checkbox-group/CheckboxGroupContext.ts. Provided by
2
+ // a <CheckboxGroup>; `useCheckboxGroupContext()` returns undefined for a standalone checkbox.
3
+ // The full CheckboxGroup + parent-checkbox machinery lands with CheckboxGroup — the `parent`
4
+ // surface is typed loosely here.
5
+ import { createContext, useContext } from 'octane';
6
+
7
+ import type { FieldValidation } from './field/FieldRootContext';
8
+
9
+ export interface CheckboxGroupContextValue {
10
+ value: string[] | undefined;
11
+ defaultValue: string[] | undefined;
12
+ setValue: (value: string[], eventDetails: any) => void;
13
+ allValues: string[] | undefined;
14
+ parent: any;
15
+ disabled: boolean;
16
+ validation: FieldValidation;
17
+ registerControlRef: (element: HTMLButtonElement | null) => void;
18
+ }
19
+
20
+ export const CheckboxGroupContext = createContext<CheckboxGroupContextValue | undefined>(undefined);
21
+
22
+ export function useCheckboxGroupContext(optional = true): CheckboxGroupContextValue | undefined {
23
+ const context = useContext(CheckboxGroupContext);
24
+ if (context === undefined && !optional) {
25
+ throw new Error(
26
+ 'Base UI: CheckboxGroupContext is missing. CheckboxGroup parts must be placed within <CheckboxGroup>.',
27
+ );
28
+ }
29
+ return context;
30
+ }
@@ -0,0 +1,27 @@
1
+ // Ported from .base-ui/packages/react/src/internals/composite/root/CompositeRootContext.ts.
2
+ // The composite (roving-focus) root context. Only the pieces the current components read
3
+ // are typed; the full surface lands with the composite navigation system (ToggleGroup /
4
+ // Toolbar / menus). Standalone button-likes call `useCompositeRootContext(true)`, which
5
+ // returns undefined when there's no <Composite.Root> ancestor.
6
+ import { createContext, useContext } from 'octane';
7
+
8
+ export interface CompositeRootContextValue {
9
+ highlightedIndex: number;
10
+ onHighlightedIndexChange: (index: number, shouldScrollIntoView?: boolean) => void;
11
+ highlightItemOnHover: boolean;
12
+ relayKeyboardEvent: (event: KeyboardEvent) => void;
13
+ }
14
+
15
+ export const CompositeRootContext = createContext<CompositeRootContextValue | undefined>(undefined);
16
+
17
+ export function useCompositeRootContext(optional: true): CompositeRootContextValue | undefined;
18
+ export function useCompositeRootContext(optional?: false): CompositeRootContextValue;
19
+ export function useCompositeRootContext(optional = false): CompositeRootContextValue | undefined {
20
+ const context = useContext(CompositeRootContext);
21
+ if (context === undefined && !optional) {
22
+ throw new Error(
23
+ 'Base UI: CompositeRootContext is missing. Composite parts must be placed within <Composite.Root>.',
24
+ );
25
+ }
26
+ return context;
27
+ }
@@ -0,0 +1,15 @@
1
+ // Ported from .base-ui/packages/react/src/direction-context/DirectionContext.tsx. Reading
2
+ // direction ('ltr' | 'rtl'); `useDirection()` reads the nearest provider, defaulting to
3
+ // 'ltr'. (Base UI's `DirectionProvider` is a Phase-later public part; only the context +
4
+ // hook are needed by the composite system now.)
5
+ import { createContext, useContext } from 'octane';
6
+
7
+ import type { TextDirection } from './composite/keys';
8
+
9
+ export type { TextDirection };
10
+
11
+ export const DirectionContext = createContext<TextDirection | undefined>(undefined);
12
+
13
+ export function useDirection(): TextDirection {
14
+ return useContext(DirectionContext) ?? 'ltr';
15
+ }
@@ -0,0 +1,27 @@
1
+ // Ported from .base-ui/packages/react/src/radio-group/RadioGroupContext.ts. Provided by a
2
+ // <RadioGroup>; a standalone <Radio.Root> reads undefined and self-manages `checked`.
3
+ import { createContext, useContext } from 'octane';
4
+
5
+ import type { FieldValidation } from './field/FieldRootContext';
6
+
7
+ export interface RadioGroupContextValue<Value = any> {
8
+ disabled: boolean | undefined;
9
+ readOnly: boolean | undefined;
10
+ required: boolean | undefined;
11
+ form: string | undefined;
12
+ name: string | undefined;
13
+ checkedValue: Value | undefined;
14
+ setCheckedValue: (value: Value, eventDetails: any) => void;
15
+ touched: boolean;
16
+ setTouched: (next: boolean | ((prev: boolean) => boolean)) => void;
17
+ validation?: FieldValidation | undefined;
18
+ registerControlRef: (element: HTMLElement | null, disabled?: boolean) => void;
19
+ registerInputRef: (element: HTMLInputElement | null) => void;
20
+ [key: string]: any;
21
+ }
22
+
23
+ export const RadioGroupContext = createContext<RadioGroupContextValue | undefined>(undefined);
24
+
25
+ export function useRadioGroupContext(): RadioGroupContextValue | undefined {
26
+ return useContext(RadioGroupContext);
27
+ }
@@ -0,0 +1,33 @@
1
+ // Ported from .base-ui/packages/react/src/toggle-group/ToggleGroupContext.ts. The context
2
+ // a <ToggleGroup> provides to its child <Toggle>s. `useToggleGroupContext()` defaults to
3
+ // optional (returns undefined when a Toggle is standalone). ToggleGroup itself lands in a
4
+ // later Phase-1 step; this context is the shared contract.
5
+ import { createContext, useContext } from 'octane';
6
+
7
+ import type { BaseUIChangeEventDetails } from './createChangeEventDetails';
8
+
9
+ export interface ToggleGroupContextValue<Value = any> {
10
+ value: readonly Value[];
11
+ setGroupValue: (
12
+ newValue: Value,
13
+ nextPressed: boolean,
14
+ eventDetails: BaseUIChangeEventDetails,
15
+ ) => void;
16
+ disabled: boolean;
17
+ orientation: 'horizontal' | 'vertical';
18
+ isValueInitialized: boolean;
19
+ }
20
+
21
+ export const ToggleGroupContext = createContext<ToggleGroupContextValue | undefined>(undefined);
22
+
23
+ export function useToggleGroupContext<Value = any>(
24
+ optional = true,
25
+ ): ToggleGroupContextValue<Value> | undefined {
26
+ const context = useContext(ToggleGroupContext) as ToggleGroupContextValue<Value> | undefined;
27
+ if (context === undefined && !optional) {
28
+ throw new Error(
29
+ 'Base UI: ToggleGroupContext is missing. ToggleGroup parts must be placed within <ToggleGroup>.',
30
+ );
31
+ }
32
+ return context;
33
+ }
@@ -0,0 +1,13 @@
1
+ // Ported from .base-ui/packages/utils/src/addEventListener.ts. Adds a listener, returns a
2
+ // cleanup that removes it.
3
+ export function addEventListener(
4
+ target: { addEventListener: any; removeEventListener: any },
5
+ type: string,
6
+ listener: EventListenerOrEventListenerObject,
7
+ options?: boolean | AddEventListenerOptions,
8
+ ): () => void {
9
+ target.addEventListener(type, listener, options);
10
+ return () => {
11
+ target.removeEventListener(type, listener, options);
12
+ };
13
+ }
@@ -0,0 +1,11 @@
1
+ // Ported verbatim from .base-ui/packages/react/src/internals/areArraysEqual.ts.
2
+ export function areArraysEqual<Item>(
3
+ array1: ReadonlyArray<Item>,
4
+ array2: ReadonlyArray<Item>,
5
+ itemComparer: (a: Item, b: Item) => boolean = (a, b) => a === b,
6
+ ): boolean {
7
+ return (
8
+ array1.length === array2.length &&
9
+ array1.every((value, index) => itemComparer(value, array2[index]))
10
+ );
11
+ }
@@ -0,0 +1,8 @@
1
+ // Ported verbatim from .base-ui/packages/react/src/internals/clamp.ts.
2
+ export function clamp(
3
+ val: number,
4
+ min: number = Number.MIN_SAFE_INTEGER,
5
+ max: number = Number.MAX_SAFE_INTEGER,
6
+ ): number {
7
+ return Math.max(min, Math.min(val, max));
8
+ }
@@ -0,0 +1,53 @@
1
+ // Ported from @radix-ui/react-compose-refs. Merges multiple refs (objects or callbacks,
2
+ // incl. React-19 cleanup-returning callback refs) into one callback ref. octane is
3
+ // ref-as-prop, so these compose the `ref` values a Slot/Primitive threads onto a child.
4
+ import { useCallback } from 'octane';
5
+
6
+ import { S, splitSlot, subSlot } from '../internal';
7
+
8
+ type PossibleRef<T> = ((instance: T | null) => void | (() => void)) | { current: T | null } | null;
9
+
10
+ function setRef<T>(ref: PossibleRef<T>, value: T | null): void | (() => void) {
11
+ if (typeof ref === 'function') {
12
+ return ref(value);
13
+ } else if (ref !== null && ref !== undefined) {
14
+ ref.current = value;
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Compose multiple refs into a single callback ref. Honors cleanup functions returned by
20
+ * callback refs (React 19 semantics); falls back to calling refs with `null` on unmount.
21
+ */
22
+ export function composeRefs<T>(...refs: PossibleRef<T>[]): (node: T | null) => void | (() => void) {
23
+ return (node: T | null) => {
24
+ let hasCleanup = false;
25
+ const cleanups = refs.map((ref) => {
26
+ const cleanup = setRef(ref, node);
27
+ if (!hasCleanup && typeof cleanup === 'function') {
28
+ hasCleanup = true;
29
+ }
30
+ return cleanup;
31
+ });
32
+ if (hasCleanup) {
33
+ return () => {
34
+ for (let i = 0; i < cleanups.length; i++) {
35
+ const cleanup = cleanups[i];
36
+ if (typeof cleanup === 'function') {
37
+ cleanup();
38
+ } else {
39
+ setRef(refs[i], null);
40
+ }
41
+ }
42
+ };
43
+ }
44
+ };
45
+ }
46
+
47
+ /** Hook version — a stable composed callback ref over the given refs. */
48
+ export function useComposedRefs<T>(...args: any[]): (node: T | null) => void | (() => void) {
49
+ const [user, slotArg] = splitSlot(args);
50
+ const slot = slotArg ?? S('useComposedRefs');
51
+ const refs = user as PossibleRef<T>[];
52
+ return useCallback(composeRefs(...refs), refs, subSlot(slot, 'cb'));
53
+ }
@@ -0,0 +1,56 @@
1
+ // Ported from .base-ui/packages/react/src/internals/composite/item/CompositeItem.tsx.
2
+ // Renders a composite item, merging the roving-focus props (tabIndex/onFocus/onMouseMove)
3
+ // UNDER the caller's own props, and composing the caller's refs with the composite ref.
4
+ import { S, subSlot } from '../../internal';
5
+ import { useRenderElement } from '../useRenderElement';
6
+ import type { StateAttributesMapping } from '../getStateAttributesProps';
7
+ import { useCompositeItem } from './useCompositeItem';
8
+
9
+ const EMPTY_ARRAY: never[] = [];
10
+ const EMPTY_OBJECT: Record<string, never> = {};
11
+
12
+ export interface CompositeItemProps<Metadata, State extends Record<string, any>> {
13
+ render?: any;
14
+ className?: any;
15
+ style?: any;
16
+ children?: any;
17
+ metadata?: Metadata;
18
+ refs?: any[];
19
+ props?: Array<Record<string, any> | (() => Record<string, any>)>;
20
+ state?: State;
21
+ stateAttributesMapping?: StateAttributesMapping<State>;
22
+ tag?: string;
23
+ [key: string]: any;
24
+ }
25
+
26
+ export function CompositeItem<Metadata, State extends Record<string, any>>(
27
+ componentProps: CompositeItemProps<Metadata, State>,
28
+ ): any {
29
+ const slot = S('CompositeItem');
30
+ const {
31
+ render,
32
+ className,
33
+ style,
34
+ state = EMPTY_OBJECT as State,
35
+ props = EMPTY_ARRAY,
36
+ refs = EMPTY_ARRAY,
37
+ metadata,
38
+ stateAttributesMapping,
39
+ tag = 'div',
40
+ ...elementProps
41
+ } = componentProps;
42
+
43
+ const { compositeProps, compositeRef } = useCompositeItem({ metadata }, subSlot(slot, 'item'));
44
+
45
+ return useRenderElement(
46
+ tag,
47
+ { render, className, style },
48
+ {
49
+ state,
50
+ ref: [...refs, compositeRef],
51
+ props: [compositeProps, ...props, elementProps],
52
+ stateAttributesMapping,
53
+ },
54
+ subSlot(slot, 're'),
55
+ );
56
+ }
@@ -0,0 +1,190 @@
1
+ // Ported from .base-ui/packages/react/src/internals/composite/list/CompositeList.tsx.
2
+ // Registers composite items in a stable Map, sorts them by document position to assign each
3
+ // a DOM-order `index`, and notifies subscribers (`onMapChange` / `subscribeMapChange`) plus
4
+ // keeps `elementsRef` sized. A MutationObserver re-sorts when the DOM reorders. Rendered by
5
+ // CompositeRoot as `<CompositeListContext.Provider>{children}</Provider>`.
6
+ import { createElement, useLayoutEffect, useMemo, useRef, useState } from 'octane';
7
+
8
+ import { S, subSlot } from '../../internal';
9
+ import { useStableCallback } from '../useStableCallback';
10
+ import { useRefWithInit } from '../useRefWithInit';
11
+ import { CompositeListContext } from './CompositeListContext';
12
+
13
+ export type CompositeMetadata<CustomMetadata> = {
14
+ index?: number | null | undefined;
15
+ } & CustomMetadata;
16
+
17
+ export interface CompositeListProps<Metadata> {
18
+ children: any;
19
+ elementsRef: { current: Array<HTMLElement | null> };
20
+ labelsRef?: { current: Array<string | null> } | undefined;
21
+ onMapChange?: (newMap: Map<Element, CompositeMetadata<Metadata> | null>) => void;
22
+ }
23
+
24
+ function createMap<Metadata>(): Map<Element, CompositeMetadata<Metadata> | null> {
25
+ return new Map();
26
+ }
27
+
28
+ function createListeners(): Set<Function> {
29
+ return new Set();
30
+ }
31
+
32
+ function sortByDocumentPosition(a: Element, b: Element): number {
33
+ const position = a.compareDocumentPosition(b);
34
+ if (
35
+ position & Node.DOCUMENT_POSITION_FOLLOWING ||
36
+ position & Node.DOCUMENT_POSITION_CONTAINED_BY
37
+ ) {
38
+ return -1;
39
+ }
40
+ if (position & Node.DOCUMENT_POSITION_PRECEDING || position & Node.DOCUMENT_POSITION_CONTAINS) {
41
+ return 1;
42
+ }
43
+ return 0;
44
+ }
45
+
46
+ export function CompositeList<Metadata>(props: CompositeListProps<Metadata>): any {
47
+ const slot = S('CompositeList');
48
+ const { children, elementsRef, labelsRef, onMapChange: onMapChangeProp } = props;
49
+
50
+ const onMapChange = useStableCallback(onMapChangeProp, subSlot(slot, 'omc'));
51
+
52
+ const nextIndexRef = useRef(0, subSlot(slot, 'nextIdx'));
53
+ const listeners = useRefWithInit<Set<Function>>(
54
+ createListeners,
55
+ subSlot(slot, 'listeners'),
56
+ ).current;
57
+
58
+ const map = useRefWithInit<Map<Element, CompositeMetadata<Metadata> | null>>(
59
+ createMap,
60
+ subSlot(slot, 'map'),
61
+ ).current;
62
+ const [mapTick, setMapTick] = useState(0, subSlot(slot, 'tick'));
63
+ const lastTickRef = useRef(mapTick, subSlot(slot, 'lastTick'));
64
+
65
+ const register = useStableCallback(
66
+ (node: Element, metadata: Metadata) => {
67
+ map.set(node, (metadata ?? null) as CompositeMetadata<Metadata> | null);
68
+ lastTickRef.current += 1;
69
+ setMapTick(lastTickRef.current);
70
+ },
71
+ subSlot(slot, 'reg'),
72
+ );
73
+
74
+ const unregister = useStableCallback(
75
+ (node: Element) => {
76
+ map.delete(node);
77
+ lastTickRef.current += 1;
78
+ setMapTick(lastTickRef.current);
79
+ },
80
+ subSlot(slot, 'unreg'),
81
+ );
82
+
83
+ const sortedMap = useMemo(
84
+ () => {
85
+ const newMap = new Map<Element, CompositeMetadata<Metadata>>();
86
+ const sortedNodes = Array.from(map.keys())
87
+ .filter((node) => node.isConnected)
88
+ .sort(sortByDocumentPosition);
89
+ sortedNodes.forEach((node, index) => {
90
+ const metadata = map.get(node) ?? ({} as CompositeMetadata<Metadata>);
91
+ newMap.set(node, { ...metadata, index });
92
+ });
93
+ return newMap;
94
+ },
95
+ [map, mapTick],
96
+ subSlot(slot, 'sorted'),
97
+ );
98
+
99
+ useLayoutEffect(
100
+ () => {
101
+ if (typeof MutationObserver !== 'function' || sortedMap.size === 0) {
102
+ return undefined;
103
+ }
104
+ const mutationObserver = new MutationObserver((entries) => {
105
+ const diff = new Set<Node>();
106
+ const updateDiff = (node: Node) => (diff.has(node) ? diff.delete(node) : diff.add(node));
107
+ entries.forEach((entry) => {
108
+ entry.removedNodes.forEach(updateDiff);
109
+ entry.addedNodes.forEach(updateDiff);
110
+ });
111
+ if (diff.size === 0) {
112
+ lastTickRef.current += 1;
113
+ setMapTick(lastTickRef.current);
114
+ }
115
+ });
116
+ sortedMap.forEach((_, node) => {
117
+ if (node.parentElement) {
118
+ mutationObserver.observe(node.parentElement, { childList: true });
119
+ }
120
+ });
121
+ return () => {
122
+ mutationObserver.disconnect();
123
+ };
124
+ },
125
+ [sortedMap],
126
+ subSlot(slot, 'e:mo'),
127
+ );
128
+
129
+ useLayoutEffect(
130
+ () => {
131
+ const shouldUpdateLengths = lastTickRef.current === mapTick;
132
+ if (shouldUpdateLengths) {
133
+ if (elementsRef.current.length !== sortedMap.size) {
134
+ elementsRef.current.length = sortedMap.size;
135
+ }
136
+ if (labelsRef && labelsRef.current.length !== sortedMap.size) {
137
+ labelsRef.current.length = sortedMap.size;
138
+ }
139
+ nextIndexRef.current = sortedMap.size;
140
+ }
141
+ onMapChange(sortedMap);
142
+ },
143
+ [onMapChange, sortedMap, elementsRef, labelsRef, mapTick],
144
+ subSlot(slot, 'e:len'),
145
+ );
146
+
147
+ useLayoutEffect(
148
+ () => () => {
149
+ elementsRef.current = [];
150
+ },
151
+ [elementsRef],
152
+ subSlot(slot, 'e:cleanEl'),
153
+ );
154
+
155
+ useLayoutEffect(
156
+ () => () => {
157
+ if (labelsRef) {
158
+ labelsRef.current = [];
159
+ }
160
+ },
161
+ [labelsRef],
162
+ subSlot(slot, 'e:cleanLbl'),
163
+ );
164
+
165
+ const subscribeMapChange = useStableCallback(
166
+ (fn: (map: Map<Element, any>) => void) => {
167
+ listeners.add(fn);
168
+ return () => {
169
+ listeners.delete(fn);
170
+ };
171
+ },
172
+ subSlot(slot, 'sub'),
173
+ );
174
+
175
+ useLayoutEffect(
176
+ () => {
177
+ listeners.forEach((l) => (l as (m: any) => void)(sortedMap));
178
+ },
179
+ [listeners, sortedMap],
180
+ subSlot(slot, 'e:notify'),
181
+ );
182
+
183
+ const contextValue = useMemo(
184
+ () => ({ register, unregister, subscribeMapChange, elementsRef, labelsRef, nextIndexRef }),
185
+ [register, unregister, subscribeMapChange, elementsRef, labelsRef, nextIndexRef],
186
+ subSlot(slot, 'ctx'),
187
+ );
188
+
189
+ return createElement(CompositeListContext.Provider, { value: contextValue, children });
190
+ }
@@ -0,0 +1,23 @@
1
+ // Ported from .base-ui/packages/react/src/internals/composite/list/CompositeListContext.ts.
2
+ import { createContext, useContext } from 'octane';
3
+
4
+ export interface CompositeListContextValue<Metadata> {
5
+ register: (node: Element, metadata: Metadata) => void;
6
+ unregister: (node: Element) => void;
7
+ subscribeMapChange: (fn: (map: Map<Element, Metadata | null>) => void) => () => void;
8
+ elementsRef: { current: Array<HTMLElement | null> };
9
+ labelsRef?: { current: Array<string | null> } | undefined;
10
+ nextIndexRef: { current: number };
11
+ }
12
+
13
+ export const CompositeListContext = createContext<CompositeListContextValue<any>>({
14
+ register: () => {},
15
+ unregister: () => {},
16
+ subscribeMapChange: () => () => {},
17
+ elementsRef: { current: [] },
18
+ nextIndexRef: { current: 0 },
19
+ });
20
+
21
+ export function useCompositeListContext(): CompositeListContextValue<any> {
22
+ return useContext(CompositeListContext);
23
+ }
@@ -0,0 +1,132 @@
1
+ // Ported from .base-ui/packages/react/src/internals/composite/root/CompositeRoot.tsx.
2
+ // Wraps a composite in the roving-focus engine (useCompositeRoot) + the item registry
3
+ // (CompositeList), and provides CompositeRootContext to the items.
4
+ import { createElement, useMemo } from 'octane';
5
+
6
+ import { S, subSlot } from '../../internal';
7
+ import { useRenderElement } from '../useRenderElement';
8
+ import type { StateAttributesMapping } from '../getStateAttributesProps';
9
+ import { useDirection } from '../DirectionContext';
10
+ import { CompositeRootContext } from '../CompositeRootContext';
11
+ import { CompositeList, type CompositeMetadata } from './CompositeList';
12
+ import { useCompositeRoot } from './useCompositeRoot';
13
+ import type { ModifierKey } from './keys';
14
+
15
+ const EMPTY_ARRAY: never[] = [];
16
+ const EMPTY_OBJECT: Record<string, never> = {};
17
+
18
+ export interface CompositeRootProps<Metadata, State extends Record<string, any>> {
19
+ render?: any;
20
+ className?: any;
21
+ style?: any;
22
+ refs?: any[];
23
+ props?: Array<Record<string, any> | (() => Record<string, any>)>;
24
+ state?: State;
25
+ stateAttributesMapping?: StateAttributesMapping<State>;
26
+ highlightedIndex?: number;
27
+ onHighlightedIndexChange?: (index: number) => void;
28
+ orientation?: 'horizontal' | 'vertical' | 'both';
29
+ grid?: ((params: any) => number) | undefined;
30
+ loopFocus?: boolean;
31
+ onLoop?: any;
32
+ enableHomeAndEndKeys?: boolean;
33
+ onMapChange?: (newMap: Map<Element, CompositeMetadata<Metadata> | null>) => void;
34
+ stopEventPropagation?: boolean;
35
+ rootRef?: any;
36
+ disabledIndices?: number[];
37
+ modifierKeys?: ModifierKey[];
38
+ highlightItemOnHover?: boolean;
39
+ tag?: string;
40
+ [key: string]: any;
41
+ }
42
+
43
+ export function CompositeRoot<Metadata extends {}, State extends Record<string, any>>(
44
+ componentProps: CompositeRootProps<Metadata, State>,
45
+ ): any {
46
+ const slot = S('CompositeRoot');
47
+ const {
48
+ render,
49
+ className,
50
+ style,
51
+ refs = EMPTY_ARRAY,
52
+ props = EMPTY_ARRAY,
53
+ state = EMPTY_OBJECT as State,
54
+ stateAttributesMapping,
55
+ highlightedIndex: highlightedIndexProp,
56
+ onHighlightedIndexChange: onHighlightedIndexChangeProp,
57
+ orientation,
58
+ grid,
59
+ loopFocus,
60
+ onLoop,
61
+ enableHomeAndEndKeys,
62
+ onMapChange: onMapChangeProp,
63
+ stopEventPropagation = true,
64
+ rootRef,
65
+ disabledIndices,
66
+ modifierKeys,
67
+ highlightItemOnHover = false,
68
+ tag = 'div',
69
+ ...elementProps
70
+ } = componentProps;
71
+
72
+ const direction = useDirection();
73
+
74
+ const {
75
+ props: defaultProps,
76
+ highlightedIndex,
77
+ onHighlightedIndexChange,
78
+ elementsRef,
79
+ onMapChange: onMapChangeUnwrapped,
80
+ relayKeyboardEvent,
81
+ } = useCompositeRoot(
82
+ {
83
+ grid,
84
+ loopFocus,
85
+ onLoop,
86
+ orientation,
87
+ highlightedIndex: highlightedIndexProp,
88
+ onHighlightedIndexChange: onHighlightedIndexChangeProp,
89
+ rootRef,
90
+ stopEventPropagation,
91
+ enableHomeAndEndKeys,
92
+ direction,
93
+ disabledIndices,
94
+ modifierKeys,
95
+ },
96
+ subSlot(slot, 'root'),
97
+ );
98
+
99
+ const element = useRenderElement(
100
+ tag,
101
+ { render, className, style },
102
+ {
103
+ state,
104
+ ref: refs,
105
+ props: [defaultProps, ...props, elementProps],
106
+ stateAttributesMapping,
107
+ },
108
+ subSlot(slot, 're'),
109
+ );
110
+
111
+ const contextValue = useMemo(
112
+ () => ({
113
+ highlightedIndex,
114
+ onHighlightedIndexChange,
115
+ highlightItemOnHover,
116
+ relayKeyboardEvent,
117
+ }),
118
+ [highlightedIndex, onHighlightedIndexChange, highlightItemOnHover, relayKeyboardEvent],
119
+ subSlot(slot, 'ctx'),
120
+ );
121
+
122
+ const list = createElement(CompositeList, {
123
+ elementsRef,
124
+ onMapChange: (newMap: any) => {
125
+ onMapChangeProp?.(newMap);
126
+ onMapChangeUnwrapped(newMap);
127
+ },
128
+ children: element,
129
+ });
130
+
131
+ return createElement(CompositeRootContext.Provider, { value: contextValue, children: list });
132
+ }