@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,346 @@
1
+ // Ported from .base-ui/packages/react/src/internals/composite/root/useCompositeRoot.ts.
2
+ // The composite roving-focus engine: owns `highlightedIndex`, wires arrow/Home/End keyboard
3
+ // navigation over `elementsRef`, and sets the default tab stop from the item marked
4
+ // `data-composite-item-active` (or the first enabled item). octane adaptations: events are
5
+ // NATIVE (no `.nativeEvent`), so handlers read the event directly; grid navigation is via a
6
+ // caller-supplied `grid` navigator (unused by ToggleGroup).
7
+ //
8
+ // SLOT: plain-`.ts` hook; the trailing arg is the caller's slot.
9
+ import { useLayoutEffect, useRef, useState } from 'octane';
10
+
11
+ import { S, splitSlot, subSlot } from '../../internal';
12
+ import { useStableCallback } from '../useStableCallback';
13
+ import { useComposedRefs } from '../composeRefs';
14
+ import {
15
+ ACTIVE_COMPOSITE_ITEM,
16
+ ARROW_DOWN,
17
+ ARROW_KEYS,
18
+ ARROW_LEFT,
19
+ ARROW_RIGHT,
20
+ ARROW_UP,
21
+ COMPOSITE_KEYS,
22
+ END,
23
+ HOME,
24
+ HORIZONTAL_KEYS,
25
+ HORIZONTAL_KEYS_WITH_EXTRA_KEYS,
26
+ MODIFIER_KEYS,
27
+ VERTICAL_KEYS,
28
+ VERTICAL_KEYS_WITH_EXTRA_KEYS,
29
+ isElementDisabled,
30
+ isNativeInput,
31
+ scrollIntoViewIfNeeded,
32
+ type ModifierKey,
33
+ type TextDirection,
34
+ } from './keys';
35
+ import {
36
+ findNonDisabledListIndex,
37
+ getMaxListIndex,
38
+ getMinListIndex,
39
+ getTarget,
40
+ isIndexOutOfListBounds,
41
+ isListIndexDisabled,
42
+ } from './list-utils';
43
+ import type { CompositeMetadata } from './CompositeList';
44
+
45
+ export interface UseCompositeRootParameters {
46
+ orientation?: 'horizontal' | 'vertical' | 'both';
47
+ grid?: ((params: any) => number) | undefined;
48
+ loopFocus?: boolean;
49
+ onLoop?:
50
+ | ((
51
+ event: any,
52
+ prevIndex: number,
53
+ nextIndex: number,
54
+ elementsRef: { current: Array<HTMLElement | null> },
55
+ ) => number)
56
+ | undefined;
57
+ highlightedIndex?: number;
58
+ onHighlightedIndexChange?: (index: number) => void;
59
+ direction: TextDirection;
60
+ rootRef?: any;
61
+ enableHomeAndEndKeys?: boolean;
62
+ stopEventPropagation?: boolean;
63
+ disabledIndices?: number[];
64
+ modifierKeys?: ModifierKey[];
65
+ }
66
+
67
+ const EMPTY_ARRAY: never[] = [];
68
+
69
+ function isModifierKeySet(event: any, ignoredModifierKeys: ModifierKey[]): boolean {
70
+ for (const key of MODIFIER_KEYS.values()) {
71
+ if (ignoredModifierKeys.includes(key)) {
72
+ continue;
73
+ }
74
+ if (event.getModifierState(key)) {
75
+ return true;
76
+ }
77
+ }
78
+ return false;
79
+ }
80
+
81
+ export function useCompositeRoot(...args: any[]): {
82
+ props: Record<string, any>;
83
+ highlightedIndex: number;
84
+ onHighlightedIndexChange: (index: number, shouldScrollIntoView?: boolean) => void;
85
+ elementsRef: { current: Array<HTMLElement | null> };
86
+ disabledIndices: number[] | undefined;
87
+ onMapChange: (map: Map<Element, CompositeMetadata<any>>) => void;
88
+ relayKeyboardEvent: (event: any) => void;
89
+ } {
90
+ const [user, slotArg] = splitSlot(args);
91
+ const slot = slotArg ?? S('useCompositeRoot');
92
+ const params = user[0] as UseCompositeRootParameters;
93
+ const {
94
+ loopFocus = true,
95
+ orientation = 'both',
96
+ grid,
97
+ onLoop,
98
+ direction,
99
+ highlightedIndex: externalHighlightedIndex,
100
+ onHighlightedIndexChange: externalSetHighlightedIndex,
101
+ rootRef: externalRef,
102
+ enableHomeAndEndKeys = false,
103
+ stopEventPropagation = false,
104
+ disabledIndices,
105
+ modifierKeys = EMPTY_ARRAY,
106
+ } = params;
107
+
108
+ const [internalHighlightedIndex, internalSetHighlightedIndex] = useState(0, subSlot(slot, 'hi'));
109
+ const isGrid = grid != null;
110
+
111
+ const rootRef = useRef<HTMLElement | null>(null, subSlot(slot, 'root'));
112
+ const mergedRef = useComposedRefs(rootRef, externalRef, subSlot(slot, 'merged'));
113
+
114
+ const elementsRef = useRef<Array<HTMLElement | null>>([], subSlot(slot, 'els'));
115
+ const hasSetDefaultIndexRef = useRef(false, subSlot(slot, 'hasSet'));
116
+
117
+ const highlightedIndex = externalHighlightedIndex ?? internalHighlightedIndex;
118
+ const onHighlightedIndexChange = useStableCallback(
119
+ (index: number, shouldScrollIntoView = false) => {
120
+ (externalSetHighlightedIndex ?? internalSetHighlightedIndex)(index);
121
+ if (shouldScrollIntoView) {
122
+ const newActiveItem = elementsRef.current[index];
123
+ scrollIntoViewIfNeeded(rootRef.current, newActiveItem, direction, orientation);
124
+ }
125
+ },
126
+ subSlot(slot, 'ohic'),
127
+ );
128
+
129
+ const onMapChange = useStableCallback(
130
+ (map: Map<Element, CompositeMetadata<any>>) => {
131
+ if (map.size === 0 || hasSetDefaultIndexRef.current) {
132
+ return;
133
+ }
134
+ hasSetDefaultIndexRef.current = true;
135
+ const sortedElements = Array.from(map.keys()) as Array<HTMLElement | null>;
136
+ const activeItem =
137
+ sortedElements.find((compositeElement) =>
138
+ compositeElement?.hasAttribute(ACTIVE_COMPOSITE_ITEM),
139
+ ) ?? null;
140
+ const activeIndex = activeItem ? sortedElements.indexOf(activeItem) : -1;
141
+
142
+ if (activeIndex !== -1) {
143
+ onHighlightedIndexChange(activeIndex);
144
+ } else if (
145
+ isListIndexDisabled({ current: sortedElements }, highlightedIndex, disabledIndices)
146
+ ) {
147
+ const firstEnabledIndex = findNonDisabledListIndex(
148
+ { current: sortedElements },
149
+ { disabledIndices },
150
+ );
151
+ if (!isIndexOutOfListBounds({ current: sortedElements }, firstEnabledIndex)) {
152
+ onHighlightedIndexChange(firstEnabledIndex);
153
+ }
154
+ }
155
+
156
+ scrollIntoViewIfNeeded(rootRef.current, activeItem, direction, orientation);
157
+ },
158
+ subSlot(slot, 'omc'),
159
+ );
160
+
161
+ useLayoutEffect(
162
+ () => {
163
+ if (
164
+ disabledIndices == null ||
165
+ externalHighlightedIndex != null ||
166
+ !hasSetDefaultIndexRef.current
167
+ ) {
168
+ return;
169
+ }
170
+ const elements = elementsRef.current;
171
+ if (isListIndexDisabled(elementsRef, highlightedIndex, disabledIndices)) {
172
+ const firstEnabledIndex = findNonDisabledListIndex(elementsRef, { disabledIndices });
173
+ if (!isIndexOutOfListBounds({ current: elements }, firstEnabledIndex)) {
174
+ onHighlightedIndexChange(firstEnabledIndex);
175
+ }
176
+ }
177
+ },
178
+ [
179
+ disabledIndices,
180
+ externalHighlightedIndex,
181
+ highlightedIndex,
182
+ elementsRef,
183
+ onHighlightedIndexChange,
184
+ ],
185
+ subSlot(slot, 'e:validate'),
186
+ );
187
+
188
+ const onKeyDown = useStableCallback(
189
+ (event: any) => {
190
+ const RELEVANT_KEYS = enableHomeAndEndKeys ? COMPOSITE_KEYS : ARROW_KEYS;
191
+ if (!RELEVANT_KEYS.has(event.key)) {
192
+ return;
193
+ }
194
+ if (isModifierKeySet(event, modifierKeys)) {
195
+ return;
196
+ }
197
+
198
+ const element = rootRef.current;
199
+ if (!element) {
200
+ return;
201
+ }
202
+
203
+ const isRtl = direction === 'rtl';
204
+ const horizontalForwardKey = isRtl ? ARROW_LEFT : ARROW_RIGHT;
205
+ const forwardKey = {
206
+ horizontal: horizontalForwardKey,
207
+ vertical: ARROW_DOWN,
208
+ both: horizontalForwardKey,
209
+ }[orientation];
210
+ const horizontalBackwardKey = isRtl ? ARROW_RIGHT : ARROW_LEFT;
211
+ const backwardKey = {
212
+ horizontal: horizontalBackwardKey,
213
+ vertical: ARROW_UP,
214
+ both: horizontalBackwardKey,
215
+ }[orientation];
216
+
217
+ // octane: the handler already receives the native event.
218
+ const target = getTarget(event);
219
+ if (target != null && isNativeInput(target) && !isElementDisabled(target as HTMLElement)) {
220
+ const input = target as HTMLInputElement;
221
+ const selectionStart = input.selectionStart;
222
+ const selectionEnd = input.selectionEnd;
223
+ const textContent = input.value ?? '';
224
+ if (selectionStart == null || event.shiftKey || selectionStart !== selectionEnd) {
225
+ return;
226
+ }
227
+ if (event.key !== backwardKey && selectionStart < textContent.length) {
228
+ return;
229
+ }
230
+ if (event.key !== forwardKey && selectionStart > 0) {
231
+ return;
232
+ }
233
+ }
234
+
235
+ let nextIndex = highlightedIndex;
236
+ const minIndex = getMinListIndex(elementsRef, disabledIndices);
237
+ const maxIndex = getMaxListIndex(elementsRef, disabledIndices);
238
+
239
+ if (grid != null) {
240
+ nextIndex = grid({
241
+ disabledIndices,
242
+ elementsRef,
243
+ event,
244
+ highlightedIndex,
245
+ loopFocus,
246
+ maxIndex,
247
+ minIndex,
248
+ onLoop: (e: any, prev: number, next: number) =>
249
+ onLoop ? onLoop(e, prev, next, elementsRef) : next,
250
+ orientation,
251
+ rtl: isRtl,
252
+ });
253
+ }
254
+
255
+ const forwardKeys = {
256
+ horizontal: [horizontalForwardKey],
257
+ vertical: [ARROW_DOWN],
258
+ both: [horizontalForwardKey, ARROW_DOWN],
259
+ }[orientation];
260
+ const backwardKeys = {
261
+ horizontal: [horizontalBackwardKey],
262
+ vertical: [ARROW_UP],
263
+ both: [horizontalBackwardKey, ARROW_UP],
264
+ }[orientation];
265
+
266
+ const preventedKeys = isGrid
267
+ ? RELEVANT_KEYS
268
+ : {
269
+ horizontal: enableHomeAndEndKeys ? HORIZONTAL_KEYS_WITH_EXTRA_KEYS : HORIZONTAL_KEYS,
270
+ vertical: enableHomeAndEndKeys ? VERTICAL_KEYS_WITH_EXTRA_KEYS : VERTICAL_KEYS,
271
+ both: RELEVANT_KEYS,
272
+ }[orientation];
273
+
274
+ if (enableHomeAndEndKeys) {
275
+ if (event.key === HOME) {
276
+ nextIndex = minIndex;
277
+ } else if (event.key === END) {
278
+ nextIndex = maxIndex;
279
+ }
280
+ }
281
+
282
+ if (
283
+ nextIndex === highlightedIndex &&
284
+ (forwardKeys.includes(event.key) || backwardKeys.includes(event.key))
285
+ ) {
286
+ if (loopFocus && nextIndex === maxIndex && forwardKeys.includes(event.key)) {
287
+ nextIndex = minIndex;
288
+ if (onLoop) {
289
+ nextIndex = onLoop(event, highlightedIndex, nextIndex, elementsRef);
290
+ }
291
+ } else if (loopFocus && nextIndex === minIndex && backwardKeys.includes(event.key)) {
292
+ nextIndex = maxIndex;
293
+ if (onLoop) {
294
+ nextIndex = onLoop(event, highlightedIndex, nextIndex, elementsRef);
295
+ }
296
+ } else {
297
+ nextIndex = findNonDisabledListIndex(elementsRef, {
298
+ startingIndex: nextIndex,
299
+ decrement: backwardKeys.includes(event.key),
300
+ disabledIndices,
301
+ });
302
+ }
303
+ }
304
+
305
+ if (nextIndex !== highlightedIndex && !isIndexOutOfListBounds(elementsRef, nextIndex)) {
306
+ if (stopEventPropagation) {
307
+ event.stopPropagation();
308
+ }
309
+ if (preventedKeys.has(event.key)) {
310
+ event.preventDefault();
311
+ }
312
+ onHighlightedIndexChange(nextIndex, true);
313
+
314
+ // Wait for FocusManager `returnFocus` to execute.
315
+ queueMicrotask(() => {
316
+ elementsRef.current[nextIndex]?.focus();
317
+ });
318
+ }
319
+ },
320
+ subSlot(slot, 'okd'),
321
+ );
322
+
323
+ const props: Record<string, any> = {
324
+ ref: mergedRef,
325
+ onFocus(event: any) {
326
+ const element = rootRef.current;
327
+ const target = getTarget(event);
328
+ if (!element || target == null || !isNativeInput(target)) {
329
+ return;
330
+ }
331
+ const input = target as HTMLInputElement;
332
+ input.setSelectionRange(0, input.value.length ?? 0);
333
+ },
334
+ onKeyDown,
335
+ };
336
+
337
+ return {
338
+ props,
339
+ highlightedIndex,
340
+ onHighlightedIndexChange,
341
+ elementsRef,
342
+ disabledIndices,
343
+ onMapChange,
344
+ relayKeyboardEvent: onKeyDown,
345
+ };
346
+ }
@@ -0,0 +1,22 @@
1
+ // Vendored from @floating-ui/react's `contains` (octane's @octanejs/floating-ui has the same
2
+ // helper internally but doesn't export it). True when `child` is `parent` or inside it,
3
+ // crossing shadow boundaries.
4
+ export function contains(parent?: Element | null, child?: Element | null): boolean {
5
+ if (!parent || !child) {
6
+ return false;
7
+ }
8
+ if (parent.contains(child)) {
9
+ return true;
10
+ }
11
+ const rootNode = child.getRootNode?.();
12
+ if (rootNode && (rootNode as ShadowRoot).host) {
13
+ let next: Node | null = child;
14
+ while (next) {
15
+ if (next === parent) {
16
+ return true;
17
+ }
18
+ next = (next as any).parentNode || (next as ShadowRoot).host;
19
+ }
20
+ }
21
+ return false;
22
+ }
@@ -0,0 +1,81 @@
1
+ // Ported from .base-ui/packages/react/src/internals/createBaseUIEventDetails.ts +
2
+ // reason-parts.ts. `createChangeEventDetails(reason, event, trigger)` builds the details
3
+ // object Base UI passes to `onXChange` callbacks — a cancelable, propagation-controllable
4
+ // wrapper around the originating event. octane dispatches NATIVE events (no synthetic
5
+ // `.nativeEvent`), so callers pass the native event directly.
6
+ //
7
+ // REASONS: only the reason strings the current components use are listed; more are added
8
+ // as components land (they are just string constants).
9
+ export const REASONS = {
10
+ none: 'none',
11
+ triggerPress: 'trigger-press',
12
+ itemPress: 'item-press',
13
+ keyboard: 'keyboard',
14
+ pointer: 'pointer',
15
+ inputChange: 'input-change',
16
+ inputClear: 'input-clear',
17
+ inputBlur: 'input-blur',
18
+ inputPaste: 'input-paste',
19
+ incrementPress: 'increment-press',
20
+ decrementPress: 'decrement-press',
21
+ wheel: 'wheel',
22
+ scrub: 'scrub',
23
+ trackPress: 'track-press',
24
+ drag: 'drag',
25
+ } as const;
26
+
27
+ export type BaseUIEventReason = (typeof REASONS)[keyof typeof REASONS];
28
+
29
+ export interface BaseUIChangeEventDetails<Reason extends string = string> {
30
+ reason: Reason;
31
+ event: Event;
32
+ cancel: () => void;
33
+ allowPropagation: () => void;
34
+ readonly isCanceled: boolean;
35
+ readonly isPropagationAllowed: boolean;
36
+ trigger: Element | undefined;
37
+ [key: string]: any;
38
+ }
39
+
40
+ export function createChangeEventDetails<Reason extends string = string>(
41
+ reason: Reason,
42
+ event?: Event,
43
+ trigger?: HTMLElement,
44
+ customProperties?: Record<string, any>,
45
+ ): BaseUIChangeEventDetails<Reason> {
46
+ let canceled = false;
47
+ let allowPropagation = false;
48
+ const details: BaseUIChangeEventDetails<Reason> = {
49
+ reason,
50
+ event: event ?? new Event('base-ui'),
51
+ cancel() {
52
+ canceled = true;
53
+ },
54
+ allowPropagation() {
55
+ allowPropagation = true;
56
+ },
57
+ get isCanceled() {
58
+ return canceled;
59
+ },
60
+ get isPropagationAllowed() {
61
+ return allowPropagation;
62
+ },
63
+ trigger,
64
+ ...customProperties,
65
+ };
66
+ return details;
67
+ }
68
+
69
+ // Ported from createBaseUIEventDetails.ts (createGenericEventDetails): a non-cancelable details
70
+ // object for "committed"/generic callbacks (e.g. Slider's `onValueCommitted`).
71
+ export function createGenericEventDetails<Reason extends string = string>(
72
+ reason: Reason,
73
+ event?: Event,
74
+ customProperties?: Record<string, any>,
75
+ ): { reason: Reason; event: Event; [key: string]: any } {
76
+ return {
77
+ reason,
78
+ event: event ?? new Event('base-ui'),
79
+ ...customProperties,
80
+ };
81
+ }
@@ -0,0 +1,13 @@
1
+ // Ported from .base-ui/packages/react/src/field/item/FieldItemContext.ts. A <Field.Item>
2
+ // (a labelled row inside a group) can disable its control; standalone reads the default.
3
+ import { createContext, useContext } from 'octane';
4
+
5
+ export interface FieldItemContextValue {
6
+ disabled: boolean;
7
+ }
8
+
9
+ export const FieldItemContext = createContext<FieldItemContextValue>({ disabled: false });
10
+
11
+ export function useFieldItemContext(): FieldItemContextValue {
12
+ return useContext(FieldItemContext);
13
+ }
@@ -0,0 +1,98 @@
1
+ // Ported from .base-ui/packages/react/src/internals/field-root-context/FieldRootContext.ts.
2
+ // A <Field.Root> provides validity/touched/dirty/etc. state + a validation object; standalone
3
+ // controls read `DEFAULT_FIELD_ROOT_CONTEXT` (inert validation, noop setters). `useFieldRootContext`
4
+ // detects "no provider" via `setValidityData === NOOP`.
5
+ import { createContext, useContext } from 'octane';
6
+
7
+ import { NOOP } from '../noop';
8
+ import {
9
+ DEFAULT_FIELD_ROOT_STATE,
10
+ DEFAULT_FIELD_STATE_ATTRIBUTES,
11
+ DEFAULT_VALIDITY_STATE,
12
+ type FieldRootState,
13
+ type FieldValidityData,
14
+ type FormValidationMode,
15
+ } from './constants';
16
+
17
+ export interface FieldValidation {
18
+ getValidationProps: (disabled: boolean, props?: Record<string, any>) => Record<string, any>;
19
+ inputRef: { current: HTMLInputElement | null };
20
+ registerInput: (...args: any[]) => void;
21
+ commit: (value: unknown) => Promise<void>;
22
+ change: (value: unknown) => void;
23
+ }
24
+
25
+ export interface FieldRootContextValue {
26
+ invalid: boolean | undefined;
27
+ name: string | undefined;
28
+ validityData: FieldValidityData;
29
+ setValidityData: (next: any) => void;
30
+ disabled: boolean | undefined;
31
+ touched: boolean;
32
+ setTouched: (next: any) => void;
33
+ dirty: boolean;
34
+ setDirty: (next: any) => void;
35
+ filled: boolean;
36
+ setFilled: (next: any) => void;
37
+ focused: boolean;
38
+ setFocused: (next: any) => void;
39
+ validate: (
40
+ value: unknown,
41
+ formValues: Record<string, unknown>,
42
+ ) => string | string[] | null | Promise<string | string[] | null>;
43
+ validationMode: FormValidationMode;
44
+ validationDebounceTime: number;
45
+ shouldValidateOnChange: () => boolean;
46
+ state: FieldRootState;
47
+ markedDirtyRef: { current: boolean };
48
+ registerFieldControl: (source: symbol, registration: any) => void;
49
+ validation: FieldValidation;
50
+ }
51
+
52
+ export const DEFAULT_FIELD_ROOT_CONTEXT: FieldRootContextValue = {
53
+ invalid: undefined,
54
+ name: undefined,
55
+ validityData: {
56
+ state: DEFAULT_VALIDITY_STATE,
57
+ errors: [],
58
+ error: '',
59
+ value: '',
60
+ initialValue: null,
61
+ },
62
+ setValidityData: NOOP,
63
+ disabled: undefined,
64
+ touched: DEFAULT_FIELD_STATE_ATTRIBUTES.touched,
65
+ setTouched: NOOP,
66
+ dirty: DEFAULT_FIELD_STATE_ATTRIBUTES.dirty,
67
+ setDirty: NOOP,
68
+ filled: DEFAULT_FIELD_STATE_ATTRIBUTES.filled,
69
+ setFilled: NOOP,
70
+ focused: DEFAULT_FIELD_STATE_ATTRIBUTES.focused,
71
+ setFocused: NOOP,
72
+ validate: () => null,
73
+ validationMode: 'onSubmit',
74
+ validationDebounceTime: 0,
75
+ shouldValidateOnChange: () => false,
76
+ state: DEFAULT_FIELD_ROOT_STATE,
77
+ markedDirtyRef: { current: false },
78
+ registerFieldControl: NOOP,
79
+ validation: {
80
+ getValidationProps: (_disabled: boolean, props: Record<string, any> = {}) => props,
81
+ inputRef: { current: null },
82
+ registerInput: NOOP,
83
+ commit: async () => {},
84
+ change: NOOP,
85
+ },
86
+ };
87
+
88
+ export const FieldRootContext = createContext<FieldRootContextValue>(DEFAULT_FIELD_ROOT_CONTEXT);
89
+
90
+ export function useFieldRootContext(optional = true): FieldRootContextValue {
91
+ const context = useContext(FieldRootContext);
92
+ if (context.setValidityData === NOOP && !optional) {
93
+ throw new Error(
94
+ 'Base UI: FieldRootContext is missing. Field parts must be placed within <Field.Root>.',
95
+ );
96
+ }
97
+ return context;
98
+ }
@@ -0,0 +1,37 @@
1
+ // Ported from .base-ui/packages/react/src/internals/form-context/FormContext.ts. A <Form>
2
+ // provides error state + a field registry; standalone controls read the default (no errors,
3
+ // noop clearErrors).
4
+ import { createContext, useContext } from 'octane';
5
+
6
+ import { NOOP } from '../noop';
7
+ import type { FieldValidityData, FormValidationMode } from './constants';
8
+
9
+ export type Errors = Record<string, string | string[]>;
10
+
11
+ export interface FormFieldEntry {
12
+ name: string | undefined;
13
+ validate: () => void;
14
+ validityData: FieldValidityData;
15
+ controlRef: { current: HTMLElement | null };
16
+ getValue: () => unknown;
17
+ }
18
+
19
+ export interface FormContextValue {
20
+ errors: Errors;
21
+ clearErrors: (name: string | undefined) => void;
22
+ formRef: { current: { fields: Map<string, FormFieldEntry> } };
23
+ validationMode: FormValidationMode;
24
+ submitAttemptedRef: { current: boolean };
25
+ }
26
+
27
+ export const FormContext = createContext<FormContextValue>({
28
+ formRef: { current: { fields: new Map() } },
29
+ errors: {},
30
+ clearErrors: NOOP,
31
+ validationMode: 'onSubmit',
32
+ submitAttemptedRef: { current: false },
33
+ });
34
+
35
+ export function useFormContext(): FormContextValue {
36
+ return useContext(FormContext);
37
+ }
@@ -0,0 +1,32 @@
1
+ // Ported from .base-ui/packages/react/src/internals/labelable-provider/LabelableContext.ts.
2
+ // Associates a labelable element with an accessible name/description. Standalone controls read
3
+ // the default (no label association; `registerControlId === NOOP` marks "no provider").
4
+ import { createContext, useContext } from 'octane';
5
+
6
+ import { NOOP } from '../noop';
7
+
8
+ export interface LabelableContextValue {
9
+ controlId: string | null | undefined;
10
+ registerControlId: (source: symbol, id: string | null | undefined) => void;
11
+ labelId: string | undefined;
12
+ setLabelId: (
13
+ next: string | undefined | ((prev: string | undefined) => string | undefined),
14
+ ) => void;
15
+ messageIds: string[];
16
+ setMessageIds: (next: string[] | ((prev: string[]) => string[])) => void;
17
+ getDescriptionProps: (externalProps: Record<string, any>) => Record<string, any>;
18
+ }
19
+
20
+ export const LabelableContext = createContext<LabelableContextValue>({
21
+ controlId: undefined,
22
+ registerControlId: NOOP,
23
+ labelId: undefined,
24
+ setLabelId: NOOP,
25
+ messageIds: [],
26
+ setMessageIds: NOOP,
27
+ getDescriptionProps: (externalProps: Record<string, any>) => externalProps,
28
+ });
29
+
30
+ export function useLabelableContext(): LabelableContextValue {
31
+ return useContext(LabelableContext);
32
+ }