@octanejs/base-ui 0.1.21 → 0.1.23
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 +5 -5
- package/src/checkbox.ts +8 -1
- package/src/dialog.ts +5 -1
- package/src/meter.ts +10 -3
- package/src/number-field.ts +7 -1
- package/src/popover.ts +4 -1
- package/src/progress.ts +10 -3
- package/src/radio.ts +15 -2
- package/src/slider.ts +8 -2
- package/src/switch.ts +16 -2
- package/src/utils/FloatingPortalLite.ts +6 -1
- package/src/utils/floating/FloatingFocusManager.ts +6 -2
- package/src/utils/floating/FloatingPortal.ts +15 -3
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/base-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
|
-
"node": ">=22"
|
|
7
|
+
"node": ">=22.22.2"
|
|
8
8
|
},
|
|
9
9
|
"octane": {
|
|
10
10
|
"hookSlots": {
|
|
@@ -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.
|
|
44
|
+
"@octanejs/floating-ui": "0.1.24"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"octane": "0.1.
|
|
47
|
+
"octane": "0.1.25"
|
|
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.
|
|
56
|
+
"octane": "0.1.25"
|
|
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
|
-
|
|
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', {
|
|
106
|
+
createElement('span', {
|
|
107
|
+
key: 'sr-hint',
|
|
108
|
+
role: 'presentation',
|
|
109
|
+
style: visuallyHidden,
|
|
110
|
+
children: 'x',
|
|
111
|
+
}),
|
|
105
112
|
],
|
|
106
113
|
};
|
|
107
114
|
|
package/src/number-field.ts
CHANGED
|
@@ -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
|
-
|
|
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', {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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: [
|
|
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', {
|
|
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) {
|