@octanejs/base-ui 0.1.21 → 0.1.22

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octanejs/base-ui",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "engines": {
@@ -41,10 +41,10 @@
41
41
  "@floating-ui/utils": "^0.2.12",
42
42
  "aria-hidden": "^1.2.6",
43
43
  "tabbable": "^6.5.0",
44
- "@octanejs/floating-ui": "0.1.22"
44
+ "@octanejs/floating-ui": "0.1.23"
45
45
  },
46
46
  "peerDependencies": {
47
- "octane": "0.1.23"
47
+ "octane": "0.1.24"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@base-ui/react": "^1.6.0",
@@ -53,7 +53,7 @@
53
53
  "react": "^19.2.7",
54
54
  "react-dom": "^19.2.7",
55
55
  "vitest": "^4.1.10",
56
- "octane": "0.1.23"
56
+ "octane": "0.1.24"
57
57
  },
58
58
  "scripts": {
59
59
  "test": "vitest run"
package/src/checkbox.ts CHANGED
@@ -12,6 +12,7 @@
12
12
  import {
13
13
  createContext,
14
14
  createElement,
15
+ Fragment,
15
16
  useContext,
16
17
  useEffect,
17
18
  useLayoutEffect,
@@ -499,7 +500,13 @@ function CheckboxRoot(props: CheckboxRootProps): any {
499
500
 
500
501
  return createElement(CheckboxRootContext.Provider, {
501
502
  value: state,
502
- children: [element, hiddenValueInput, createElement('input', inputProps)],
503
+ // Octane reconciles an ARRAY child as a keyed list, so each slot carries a stable key;
504
+ // values that cannot be keyed in place go through a keyed Fragment.
505
+ children: [
506
+ createElement(Fragment, { key: 'element', children: element }),
507
+ createElement(Fragment, { key: 'hidden-value', children: hiddenValueInput }),
508
+ createElement('input', { key: 'input', ...inputProps }),
509
+ ],
503
510
  });
504
511
  }
505
512
 
package/src/dialog.ts CHANGED
@@ -634,14 +634,18 @@ function DialogPortal(props: any): any {
634
634
  value: keepMounted,
635
635
  children: createElement(FloatingPortal, {
636
636
  container,
637
+ // Octane reconciles an ARRAY child as a keyed list, so every entry needs a key —
638
+ // including the consumer's, which is why it goes through a keyed Fragment rather than
639
+ // being keyed in place. Same shape as the Popup path below.
637
640
  children: [
638
641
  mounted && modal === true
639
642
  ? createElement(InternalBackdrop, {
643
+ key: 'internal-backdrop',
640
644
  ref: store.context.internalBackdropRef,
641
645
  inert: inertValue(!open),
642
646
  })
643
647
  : null,
644
- children,
648
+ createElement(Fragment, { key: 'children', children }),
645
649
  ],
646
650
  }),
647
651
  });
package/src/meter.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  // A meter visualizes a value within a known range. `Meter.Root` (`role="meter"`) owns
6
6
  // the range math and screen-reader aria; `Track`/`Indicator` draw the bar; `Value`/`Label`
7
7
  // render text. Base UI uses a PLAIN React context — ported as a plain octane createContext.
8
- import { createContext, createElement, useContext, useMemo, useState } from 'octane';
8
+ import { createContext, createElement, Fragment, useContext, useMemo, useState } from 'octane';
9
9
 
10
10
  import { S, subSlot } from './internal';
11
11
  import { useRenderElement, type RenderProp } from './utils/useRenderElement';
@@ -98,10 +98,17 @@ function MeterRoot(props: MeterRootProps): any {
98
98
  'aria-valuenow': clampedValue,
99
99
  'aria-valuetext': ariaValuetext,
100
100
  role: 'meter',
101
+ // Octane reconciles an ARRAY child as a keyed list, so each slot carries a stable key;
102
+ // values that cannot be keyed in place go through a keyed Fragment.
101
103
  children: [
102
- children,
104
+ createElement(Fragment, { key: 'children', children }),
103
105
  // force NVDA to read the label (mui/base-ui#4184)
104
- createElement('span', { role: 'presentation', style: visuallyHidden, children: 'x' }),
106
+ createElement('span', {
107
+ key: 'sr-hint',
108
+ role: 'presentation',
109
+ style: visuallyHidden,
110
+ children: 'x',
111
+ }),
105
112
  ],
106
113
  };
107
114
 
@@ -11,6 +11,7 @@
11
11
  import {
12
12
  createContext,
13
13
  createElement,
14
+ Fragment,
14
15
  useContext,
15
16
  useEffect,
16
17
  useLayoutEffect,
@@ -482,7 +483,12 @@ function NumberFieldRoot(props: any): any {
482
483
 
483
484
  return createElement(NumberFieldRootContext.Provider, {
484
485
  value: contextValue,
485
- children: [element, hiddenInput],
486
+ // Octane reconciles an ARRAY child as a keyed list, so each slot carries a stable key;
487
+ // values that cannot be keyed in place go through a keyed Fragment.
488
+ children: [
489
+ createElement(Fragment, { key: 'element', children: element }),
490
+ createElement(Fragment, { key: 'hidden-input', children: hiddenInput }),
491
+ ],
486
492
  });
487
493
  }
488
494
 
package/src/popover.ts CHANGED
@@ -879,15 +879,18 @@ function PopoverPositioner(componentProps: any): any {
879
879
 
880
880
  return createElement(PopoverPositionerContext.Provider, {
881
881
  value: positioning as unknown as PopoverPositionerContextValue,
882
+ // Octane reconciles an ARRAY child as a keyed list, so both slots carry a key. Only the
883
+ // modal path renders the backdrop, which is why a non-modal popover never warned.
882
884
  children: [
883
885
  mounted && modal === true && openReason !== REASONS.triggerHover
884
886
  ? createElement(InternalBackdrop, {
887
+ key: 'internal-backdrop',
885
888
  ref: store.context.internalBackdropRef,
886
889
  inert: inertValue(!open),
887
890
  cutout: triggerElement,
888
891
  })
889
892
  : null,
890
- createElement(FloatingNode, { id: nodeId, children: element }),
893
+ createElement(FloatingNode, { key: 'floating-node', id: nodeId, children: element }),
891
894
  ],
892
895
  });
893
896
  }
package/src/progress.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  // the `status` state ('indeterminate' | 'progressing' | 'complete') is exposed on every
8
8
  // part as `data-progressing`/`data-complete`/`data-indeterminate`. Base UI uses a PLAIN
9
9
  // React context — ported as a plain octane createContext.
10
- import { createContext, createElement, useContext, useMemo, useState } from 'octane';
10
+ import { createContext, createElement, Fragment, useContext, useMemo, useState } from 'octane';
11
11
 
12
12
  import { S, subSlot } from './internal';
13
13
  import { useRenderElement, type RenderProp } from './utils/useRenderElement';
@@ -129,9 +129,16 @@ function ProgressRoot(props: ProgressRootProps): any {
129
129
  'aria-valuenow': value ?? undefined,
130
130
  'aria-valuetext': getAriaValueText(formattedValue, value),
131
131
  role: 'progressbar',
132
+ // Array children reconcile as a keyed list in octane, so both slots carry a key. The
133
+ // consumer's go through a keyed Fragment because they cannot be keyed in place.
132
134
  children: [
133
- children,
134
- createElement('span', { role: 'presentation', style: visuallyHidden, children: 'x' }),
135
+ createElement(Fragment, { key: 'children', children }),
136
+ createElement('span', {
137
+ key: 'sr-hint',
138
+ role: 'presentation',
139
+ style: visuallyHidden,
140
+ children: 'x',
141
+ }),
135
142
  ],
136
143
  };
137
144
 
package/src/radio.ts CHANGED
@@ -6,7 +6,15 @@
6
6
  // for roving focus when inside a <RadioGroup>). octane adaptations mirror Switch/Checkbox:
7
7
  // native events; the hidden input takes the live `checked` prop (octane inputs are CONTROLLED
8
8
  // exactly like React's) with native click dispatch.
9
- import { createContext, createElement, useContext, useLayoutEffect, useMemo, useRef } from 'octane';
9
+ import {
10
+ createContext,
11
+ createElement,
12
+ Fragment,
13
+ useContext,
14
+ useLayoutEffect,
15
+ useMemo,
16
+ useRef,
17
+ } from 'octane';
10
18
 
11
19
  import { S, subSlot } from './internal';
12
20
  import { useRenderElement, type RenderProp } from './utils/useRenderElement';
@@ -318,7 +326,12 @@ function RadioRoot(props: RadioRootProps): any {
318
326
 
319
327
  return createElement(RadioRootContext.Provider, {
320
328
  value: state,
321
- children: [rendered, createElement('input', inputProps)],
329
+ // Octane reconciles an ARRAY child as a keyed list, so each slot carries a stable key;
330
+ // values that cannot be keyed in place go through a keyed Fragment.
331
+ children: [
332
+ createElement(Fragment, { key: 'rendered', children: rendered }),
333
+ createElement('input', { key: 'input', ...inputProps }),
334
+ ],
322
335
  });
323
336
  }
324
337
 
package/src/slider.ts CHANGED
@@ -17,6 +17,7 @@
17
17
  import {
18
18
  createContext,
19
19
  createElement,
20
+ Fragment,
20
21
  useContext,
21
22
  useEffect,
22
23
  useLayoutEffect,
@@ -1460,7 +1461,7 @@ function SliderThumb(componentProps: any): any {
1460
1461
  subSlot(slot, 'inputRefs'),
1461
1462
  );
1462
1463
 
1463
- const inputElement = createElement('input', { ...inputProps, ref: mergedInputRef });
1464
+ const inputElement = createElement('input', { key: 'input', ...inputProps, ref: mergedInputRef });
1464
1465
 
1465
1466
  return useRenderElement(
1466
1467
  'div',
@@ -1471,7 +1472,12 @@ function SliderThumb(componentProps: any): any {
1471
1472
  props: [
1472
1473
  {
1473
1474
  ['data-index']: index,
1474
- children: [childrenProp, inputElement],
1475
+ // Keyed for the same reason as dialog and progress: octane reconciles an array
1476
+ // child as a keyed list, and the consumer's children cannot be keyed in place.
1477
+ children: [
1478
+ createElement(Fragment, { key: 'children', children: childrenProp }),
1479
+ inputElement,
1480
+ ],
1475
1481
  id,
1476
1482
  onBlur: onBlurProp,
1477
1483
  onFocus: onFocusProp,
package/src/switch.ts CHANGED
@@ -8,7 +8,15 @@
8
8
  // are CONTROLLED exactly like React's (property-driven, reasserted on every commit and after
9
9
  // discrete events; only the INITIAL checked reflects to the attribute). Field/Form
10
10
  // integration is inert when standalone (default contexts).
11
- import { createContext, createElement, useContext, useLayoutEffect, useMemo, useRef } from 'octane';
11
+ import {
12
+ createContext,
13
+ createElement,
14
+ Fragment,
15
+ useContext,
16
+ useLayoutEffect,
17
+ useMemo,
18
+ useRef,
19
+ } from 'octane';
12
20
 
13
21
  import { S, subSlot } from './internal';
14
22
  import { useRenderElement, type RenderProp } from './utils/useRenderElement';
@@ -322,7 +330,13 @@ function SwitchRoot(props: SwitchRootProps): any {
322
330
 
323
331
  return createElement(SwitchRootContext.Provider, {
324
332
  value: state,
325
- children: [element, hiddenValueInput, createElement('input', inputProps)],
333
+ // Octane reconciles an ARRAY child as a keyed list, so each slot carries a stable key;
334
+ // values that cannot be keyed in place go through a keyed Fragment.
335
+ children: [
336
+ createElement(Fragment, { key: 'element', children: element }),
337
+ createElement(Fragment, { key: 'hidden-value', children: hiddenValueInput }),
338
+ createElement('input', { key: 'input', ...inputProps }),
339
+ ],
326
340
  });
327
341
  }
328
342
 
@@ -24,6 +24,11 @@ export function FloatingPortalLite(componentProps: any): any {
24
24
  }
25
25
 
26
26
  return createElement(Fragment, {
27
- children: [portalSubtree, portalNode ? createPortal(children, portalNode) : null],
27
+ children: [
28
+ createElement(Fragment, { key: 'subtree', children: portalSubtree }),
29
+ portalNode
30
+ ? createElement(Fragment, { key: 'portal', children: createPortal(children, portalNode) })
31
+ : null,
32
+ ],
28
33
  });
29
34
  }
@@ -4,7 +4,7 @@
4
4
  // every hook threads an explicit slot; returns `[beforeGuard?, children, afterGuard?]`. Traps focus
5
5
  // inside the popup, hides the rest of the page from assistive tech (`markOthers`), restores focus on
6
6
  // close. Emits `data-base-ui-*` attributes for parity with Base UI.
7
- import { createElement, useState, useRef, useEffect, useLayoutEffect } from 'octane';
7
+ import { createElement, Fragment, useState, useRef, useEffect, useLayoutEffect } from 'octane';
8
8
  import { getNodeName, isHTMLElement } from '@floating-ui/utils/dom';
9
9
 
10
10
  import { S, subSlot } from '../../internal';
@@ -786,9 +786,12 @@ export function FloatingFocusManager(props: {
786
786
  const shouldRenderGuards =
787
787
  !disabled && (modal ? !isUntrappedTypeableCombobox : true) && (isInsidePortal || modal);
788
788
 
789
+ // Octane reconciles an ARRAY child as a keyed list, so each slot carries a stable key; the
790
+ // consumer's children go through a keyed Fragment, matching popover's trigger guards.
789
791
  return [
790
792
  shouldRenderGuards
791
793
  ? createElement(FocusGuard, {
794
+ key: 'guard-inside-before',
792
795
  'data-type': 'inside',
793
796
  ref: mergedBeforeGuardRef,
794
797
  onFocus(event: any) {
@@ -807,9 +810,10 @@ export function FloatingFocusManager(props: {
807
810
  },
808
811
  })
809
812
  : null,
810
- children,
813
+ createElement(Fragment, { key: 'children', children }),
811
814
  shouldRenderGuards
812
815
  ? createElement(FocusGuard, {
816
+ key: 'guard-inside-after',
813
817
  'data-type': 'inside',
814
818
  ref: mergedAfterGuardRef,
815
819
  onFocus(event: any) {
@@ -5,6 +5,7 @@
5
5
  import {
6
6
  createContext,
7
7
  createElement,
8
+ Fragment,
8
9
  createPortal,
9
10
  useContext,
10
11
  useState,
@@ -219,13 +220,17 @@ export function FloatingPortal(componentProps: any): any {
219
220
  subSlot(slot, 'ctx'),
220
221
  );
221
222
 
223
+ // Octane reconciles an ARRAY child as a keyed list, so every entry below carries a stable key.
224
+ // The slots are positional and fixed, so literal keys are correct here.
222
225
  return [
223
- portalSubtree,
226
+ createElement(Fragment, { key: 'portal-subtree', children: portalSubtree }),
224
227
  createElement(PortalContext.Provider, {
228
+ key: 'portal-context',
225
229
  value: portalContextValue,
226
230
  children: [
227
231
  shouldRenderGuards && portalNode
228
232
  ? createElement(FocusGuard, {
233
+ key: 'guard-before',
229
234
  'data-type': 'outside',
230
235
  ref: beforeOutsideRef,
231
236
  onFocus(event: any) {
@@ -239,11 +244,18 @@ export function FloatingPortal(componentProps: any): any {
239
244
  })
240
245
  : null,
241
246
  shouldRenderGuards && portalNode
242
- ? createElement('span', { 'aria-owns': portalNode.id, style: ownerVisuallyHidden })
247
+ ? createElement('span', {
248
+ key: 'aria-owns',
249
+ 'aria-owns': portalNode.id,
250
+ style: ownerVisuallyHidden,
251
+ })
252
+ : null,
253
+ portalNode
254
+ ? createElement(Fragment, { key: 'portal', children: createPortal(children, portalNode) })
243
255
  : null,
244
- portalNode ? createPortal(children, portalNode) : null,
245
256
  shouldRenderGuards && portalNode
246
257
  ? createElement(FocusGuard, {
258
+ key: 'guard-after',
247
259
  'data-type': 'outside',
248
260
  ref: afterOutsideRef,
249
261
  onFocus(event: any) {