@octanejs/radix 0.1.2 → 0.1.3
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/README.md +7 -1
- package/package.json +11 -4
- package/src/Checkbox.ts +15 -25
- package/src/OneTimePasswordField.ts +11 -30
- package/src/Radio.ts +13 -17
- package/src/Select.ts +20 -46
- package/src/Slider.ts +13 -15
- package/src/Switch.ts +11 -18
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @octanejs/radix
|
|
2
2
|
|
|
3
3
|
[Radix UI Primitives](https://www.radix-ui.com/primitives) for the
|
|
4
|
-
[octane](https://github.com/octanejs/octane)
|
|
4
|
+
[octane](https://github.com/octanejs/octane) UI framework — a port of
|
|
5
5
|
[`@radix-ui/react`](https://www.npmjs.com/org/radix-ui) (headless, accessible UI
|
|
6
6
|
primitives) on octane's hooks. It mirrors the unified `radix-ui` package's shape: the
|
|
7
7
|
low-level composition utilities are exported directly, and each component is a namespace
|
|
@@ -98,3 +98,9 @@ Remaining (documented in
|
|
|
98
98
|
coverage for the overlay/portal components and Phase-5 polish. The port surfaced —
|
|
99
99
|
and fixed, in octane itself — fourteen runtime/compiler parity bugs along the way;
|
|
100
100
|
each is pinned by an octane regression test and a changeset.
|
|
101
|
+
|
|
102
|
+
## Status
|
|
103
|
+
|
|
104
|
+
Current scope, known divergences, and verification status are tracked in the
|
|
105
|
+
generated [bindings status table](../../docs/bindings-status.md), sourced from
|
|
106
|
+
this package's [`status.json`](./status.json).
|
package/package.json
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/radix",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"octane": {
|
|
7
|
+
"hookSlots": {
|
|
8
|
+
"manual": [
|
|
9
|
+
"src"
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
},
|
|
6
13
|
"description": "Radix UI Primitives for the octane renderer — a port of @radix-ui/react (headless, accessible UI primitives) on octane's hooks, built on the same @octanejs/floating-ui substrate.",
|
|
7
14
|
"author": {
|
|
8
15
|
"name": "Dominic Gannaway",
|
|
@@ -28,11 +35,11 @@
|
|
|
28
35
|
},
|
|
29
36
|
"dependencies": {
|
|
30
37
|
"aria-hidden": "^1.2.6",
|
|
31
|
-
"@octanejs/floating-ui": "0.1.
|
|
32
|
-
"octane": "0.1.
|
|
38
|
+
"@octanejs/floating-ui": "0.1.3",
|
|
39
|
+
"octane": "0.1.4"
|
|
33
40
|
},
|
|
34
41
|
"devDependencies": {
|
|
35
|
-
"@tsrx/react": "^0.2.
|
|
42
|
+
"@tsrx/react": "^0.2.37",
|
|
36
43
|
"esbuild": "^0.28.1",
|
|
37
44
|
"radix-ui": "^1.4.3",
|
|
38
45
|
"react": "^19.2.0",
|
package/src/Checkbox.ts
CHANGED
|
@@ -2,15 +2,14 @@
|
|
|
2
2
|
// .radix-primitives/packages/react/checkbox/src/checkbox.tsx). A `role=checkbox` button
|
|
3
3
|
// (with `indeterminate` support) that — when inside a form — renders a hidden native
|
|
4
4
|
// checkbox "bubble input" so native form machinery (FormData, validation, reset,
|
|
5
|
-
// change listeners) reflects the state.
|
|
6
|
-
// imperatively via the native `checked` property setter + dispatched events.
|
|
5
|
+
// change listeners) reflects the state.
|
|
7
6
|
//
|
|
8
|
-
// octane adaptations (documented;
|
|
9
|
-
// no-controlled-inputs model — see docs/react-parity-migration-plan.md):
|
|
7
|
+
// octane adaptations (documented; see docs/react-parity-migration-plan.md):
|
|
10
8
|
// - React's synthetic `event.isPropagationStopped()` → native `event.cancelBubble`.
|
|
11
|
-
// -
|
|
12
|
-
//
|
|
13
|
-
// the
|
|
9
|
+
// - The source's uncontrolled bubble input (`defaultChecked` + prototype-descriptor
|
|
10
|
+
// `checked` writes) → a live CONTROLLED `checked` prop: octane's React-parity
|
|
11
|
+
// controlled form components write the DOM property at commit and reassert it after
|
|
12
|
+
// every event flush, so the bubble effect only dispatches events.
|
|
14
13
|
// - The bubble effect ALSO dispatches a native `change` event after the source's
|
|
15
14
|
// `click`: React forms observe checkbox clicks through synthetic `onChange`, which
|
|
16
15
|
// octane doesn't have — a native bubbling `change` gives octane `<form onChange>`
|
|
@@ -230,7 +229,6 @@ export function BubbleInput(props: any): any {
|
|
|
230
229
|
control,
|
|
231
230
|
hasConsumerStoppedPropagationRef,
|
|
232
231
|
checked,
|
|
233
|
-
defaultChecked,
|
|
234
232
|
required,
|
|
235
233
|
disabled,
|
|
236
234
|
name,
|
|
@@ -244,23 +242,18 @@ export function BubbleInput(props: any): any {
|
|
|
244
242
|
const prevChecked = usePrevious(checked, subSlot(slot, 'prev'));
|
|
245
243
|
const controlSize = useSize(control, subSlot(slot, 'size'));
|
|
246
244
|
|
|
247
|
-
// Bubble checked change to parents (e.g form change event)
|
|
245
|
+
// Bubble checked change to parents (e.g form change event). The controlled
|
|
246
|
+
// `checked` prop below keeps the input's DOM state in sync, so — unlike the
|
|
247
|
+
// source's uncontrolled input — this effect only dispatches the events
|
|
248
|
+
// (`indeterminate` stays imperative: it is property-only, as in the source).
|
|
248
249
|
useEffect(
|
|
249
250
|
() => {
|
|
250
251
|
const input = bubbleInput;
|
|
251
252
|
if (!input) return;
|
|
252
253
|
|
|
253
|
-
const inputProto = window.HTMLInputElement.prototype;
|
|
254
|
-
const descriptor = Object.getOwnPropertyDescriptor(
|
|
255
|
-
inputProto,
|
|
256
|
-
'checked',
|
|
257
|
-
) as PropertyDescriptor;
|
|
258
|
-
const setChecked = descriptor.set;
|
|
259
|
-
|
|
260
254
|
const bubbles = !hasConsumerStoppedPropagationRef.current;
|
|
261
|
-
if (prevChecked !== checked
|
|
255
|
+
if (prevChecked !== checked) {
|
|
262
256
|
input.indeterminate = isIndeterminate(checked);
|
|
263
|
-
setChecked.call(input, isIndeterminate(checked) ? false : checked);
|
|
264
257
|
input.dispatchEvent(new Event('click', { bubbles }));
|
|
265
258
|
// octane adaptation: also fire the native `change` React's synthetic
|
|
266
259
|
// onChange would have synthesised from the click (see file header).
|
|
@@ -271,16 +264,13 @@ export function BubbleInput(props: any): any {
|
|
|
271
264
|
subSlot(slot, 'e:bubble'),
|
|
272
265
|
);
|
|
273
266
|
|
|
274
|
-
const defaultCheckedRef = useRef(
|
|
275
|
-
isIndeterminate(checked) ? false : checked,
|
|
276
|
-
subSlot(slot, 'default'),
|
|
277
|
-
);
|
|
278
267
|
return createElement(Primitive.input, {
|
|
279
268
|
type: 'checkbox',
|
|
280
269
|
'aria-hidden': true,
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
|
|
270
|
+
// Live CONTROLLED checked (octane React-parity): the runtime writes the DOM
|
|
271
|
+
// property, mirrors only the INITIAL state to the attribute, and reasserts
|
|
272
|
+
// the property on every commit / after event flushes.
|
|
273
|
+
checked: isIndeterminate(checked) ? false : checked,
|
|
284
274
|
required,
|
|
285
275
|
disabled,
|
|
286
276
|
name,
|
|
@@ -16,12 +16,11 @@
|
|
|
16
16
|
// with the corresponding user prop via `composeEventHandlers` (React fires them in that
|
|
17
17
|
// order for a native `input` event). The user's `onChange` prop is therefore folded
|
|
18
18
|
// into the `input` binding and NOT bound to native `change` (native change = commit).
|
|
19
|
-
// - React's CONTROLLED `value={char}` cell
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// character that produced no state change).
|
|
19
|
+
// - React's CONTROLLED `value={char}` cell maps directly onto octane's controlled
|
|
20
|
+
// `value` prop (React-parity controlled form components): the runtime writes the DOM
|
|
21
|
+
// `.value` property at commit, reasserts it on every commit of the owning block, and
|
|
22
|
+
// restores it after each native event flush (React's controlled-value restoration —
|
|
23
|
+
// e.g. an invalid character that produced no state change snaps back automatically).
|
|
25
24
|
// - The source's `unstable_createCollection` (state-backed, `.at`/`.from`/`.size`/
|
|
26
25
|
// `.indexOf`) has no shared octane port (the shared collection.ts is the legacy API,
|
|
27
26
|
// which registers items in passive effects and never re-renders consumers). Since the
|
|
@@ -638,24 +637,10 @@ export function OneTimePasswordFieldInput(props: any): any {
|
|
|
638
637
|
setElement,
|
|
639
638
|
subSlot(slot, 'refs'),
|
|
640
639
|
);
|
|
640
|
+
// The cell's `value={char}` is live CONTROLLED (octane React-parity): the runtime
|
|
641
|
+
// keeps the DOM `.value` in sync and restores it after event flushes — no
|
|
642
|
+
// imperative sync or post-handler reassertion needed here.
|
|
641
643
|
const char = context.value[index] ?? '';
|
|
642
|
-
// Latest char for the post-handler controlled-value reassertion (octane adaptation).
|
|
643
|
-
const charRef = useRef(char, subSlot(slot, 'char'));
|
|
644
|
-
charRef.current = char;
|
|
645
|
-
|
|
646
|
-
// octane adaptation: React's controlled `value={char}` → imperative `.value` property
|
|
647
|
-
// sync when the cell's state char changes (the attribute alone can't update a dirty
|
|
648
|
-
// input).
|
|
649
|
-
useLayoutEffect(
|
|
650
|
-
() => {
|
|
651
|
-
const input = inputRef.current;
|
|
652
|
-
if (input && input.value !== char) {
|
|
653
|
-
input.value = char;
|
|
654
|
-
}
|
|
655
|
-
},
|
|
656
|
-
[char],
|
|
657
|
-
subSlot(slot, 'e:value'),
|
|
658
|
-
);
|
|
659
644
|
|
|
660
645
|
const keyboardActionTimeoutRef = useRef<number | null>(null, subSlot(slot, 'timeout'));
|
|
661
646
|
useEffect(
|
|
@@ -807,16 +792,12 @@ export function OneTimePasswordFieldInput(props: any): any {
|
|
|
807
792
|
}
|
|
808
793
|
}),
|
|
809
794
|
// octane adaptation: ONE native `input` binding runs the source's onInput
|
|
810
|
-
// logic then its onChange logic (React's order for a native input event)
|
|
811
|
-
//
|
|
812
|
-
//
|
|
795
|
+
// logic then its onChange logic (React's order for a native input event).
|
|
796
|
+
// React's controlled-input restoration is the runtime's: after the event
|
|
797
|
+
// flush, the controlled `value` snaps the DOM back to the rendered char.
|
|
813
798
|
onInput: (event: Event) => {
|
|
814
799
|
composeEventHandlers(onInputProp, handleInput)(event);
|
|
815
800
|
composeEventHandlers(onChangeProp, handleChange)(event);
|
|
816
|
-
const input = inputRef.current;
|
|
817
|
-
if (input && input.value !== charRef.current) {
|
|
818
|
-
input.value = charRef.current;
|
|
819
|
-
}
|
|
820
801
|
},
|
|
821
802
|
onKeyDown: composeEventHandlers(props?.onKeyDown, (event: KeyboardEvent) => {
|
|
822
803
|
switch (event.key) {
|
package/src/Radio.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// Ported from @radix-ui/react-radio-group's internal radio (source:
|
|
2
2
|
// .radix-primitives/packages/react/radio-group/src/radio.tsx). The single-radio
|
|
3
3
|
// building block RadioGroup composes: a `role=radio` button + Presence indicator +
|
|
4
|
-
// the hidden native-radio "bubble input" (same machinery as Checkbox
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// the hidden native-radio "bubble input" (same machinery as Checkbox — a live
|
|
5
|
+
// CONTROLLED `checked` prop keeps the DOM in sync, the bubble effect only dispatches
|
|
6
|
+
// events). Same octane adaptations as Checkbox.ts: `isPropagationStopped()` →
|
|
7
|
+
// `event.cancelBubble`; the source's uncontrolled input + descriptor `checked` writes
|
|
8
|
+
// → octane's controlled `checked`; a native `change` dispatched alongside the
|
|
9
|
+
// source's `click`.
|
|
8
10
|
import { createElement, useEffect, useRef, useState } from 'octane';
|
|
9
11
|
|
|
10
12
|
import { composeEventHandlers } from './compose-event-handlers';
|
|
@@ -199,22 +201,16 @@ export function RadioBubbleInput(props: any): any {
|
|
|
199
201
|
const prevChecked = usePrevious(checked, subSlot(slot, 'prev'));
|
|
200
202
|
const controlSize = useSize(control, subSlot(slot, 'size'));
|
|
201
203
|
|
|
202
|
-
// Bubble checked change to parents (e.g form change event)
|
|
204
|
+
// Bubble checked change to parents (e.g form change event). The controlled
|
|
205
|
+
// `checked` prop below keeps the input's DOM state in sync, so this effect
|
|
206
|
+
// only dispatches the events (see Checkbox.ts).
|
|
203
207
|
useEffect(
|
|
204
208
|
() => {
|
|
205
209
|
const input = bubbleInput;
|
|
206
210
|
if (!input) return;
|
|
207
211
|
|
|
208
|
-
const inputProto = window.HTMLInputElement.prototype;
|
|
209
|
-
const descriptor = Object.getOwnPropertyDescriptor(
|
|
210
|
-
inputProto,
|
|
211
|
-
'checked',
|
|
212
|
-
) as PropertyDescriptor;
|
|
213
|
-
const setChecked = descriptor.set;
|
|
214
|
-
|
|
215
212
|
const bubbles = !hasConsumerStoppedPropagationRef.current;
|
|
216
|
-
if (prevChecked !== checked
|
|
217
|
-
setChecked.call(input, checked);
|
|
213
|
+
if (prevChecked !== checked) {
|
|
218
214
|
input.dispatchEvent(new Event('click', { bubbles }));
|
|
219
215
|
// octane adaptation: also fire the native `change` (see Checkbox.ts header).
|
|
220
216
|
input.dispatchEvent(new Event('change', { bubbles }));
|
|
@@ -224,12 +220,12 @@ export function RadioBubbleInput(props: any): any {
|
|
|
224
220
|
subSlot(slot, 'e:bubble'),
|
|
225
221
|
);
|
|
226
222
|
|
|
227
|
-
const defaultCheckedRef = useRef(checked, subSlot(slot, 'default'));
|
|
228
223
|
return createElement(Primitive.input, {
|
|
229
224
|
type: 'radio',
|
|
230
225
|
'aria-hidden': true,
|
|
231
|
-
//
|
|
232
|
-
|
|
226
|
+
// Live CONTROLLED checked (octane React-parity — see Checkbox.ts). The runtime's
|
|
227
|
+
// event-restore also re-syncs radio-group cousins after a dispatched `click`.
|
|
228
|
+
checked,
|
|
233
229
|
required,
|
|
234
230
|
disabled,
|
|
235
231
|
name,
|
package/src/Select.ts
CHANGED
|
@@ -18,13 +18,13 @@
|
|
|
18
18
|
// - `SelectValue`'s `<React.Fragment key={placeholder|value}>` remount trick is dropped:
|
|
19
19
|
// octane's value-hole reconciliation replaces the placeholder/value content directly.
|
|
20
20
|
// - The bubble `<select>`: React's `defaultValue={selectValue}` + `key={nativeSelectKey}`
|
|
21
|
-
// (re-built so the default option associates)
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
21
|
+
// (re-built so the default option associates) → a live CONTROLLED `value` prop:
|
|
22
|
+
// octane's React-parity controlled form components re-project a select's value onto
|
|
23
|
+
// its options at every commit (so late-registering options associate without a
|
|
24
|
+
// rebuild) and restore it after event flushes. Value CHANGES additionally dispatch a
|
|
25
|
+
// bubbling native `change` event (as in the source) so octane `<form onChange>`
|
|
26
|
+
// observes them. React's synthetic `onChange` on the select is the native `change`
|
|
27
|
+
// event (a `<select>` is not a text input, so no `onInput` remap).
|
|
28
28
|
// - jsdom-environment guards (same class as use-size.ts's ResizeObserver guard; no
|
|
29
29
|
// behavior change in browsers): `hasPointerCapture`/`releasePointerCapture` and
|
|
30
30
|
// `scrollIntoView` are called only when they exist.
|
|
@@ -113,7 +113,6 @@ interface SelectContextValue {
|
|
|
113
113
|
form?: string;
|
|
114
114
|
// Native `<option>` element descriptors registered by mounted ItemTexts.
|
|
115
115
|
nativeOptions: Set<any>;
|
|
116
|
-
nativeSelectKey: string;
|
|
117
116
|
isFormControl: boolean;
|
|
118
117
|
}
|
|
119
118
|
|
|
@@ -181,14 +180,6 @@ export function Provider(props: any): any {
|
|
|
181
180
|
);
|
|
182
181
|
const contentId = useId(subSlot(slot, 'contentId'));
|
|
183
182
|
|
|
184
|
-
// The native `select` only associates the correct default value if the corresponding
|
|
185
|
-
// `option` is rendered as a child **at the same time** as itself. Because it might take
|
|
186
|
-
// a few renders for our items to gather the information to build the native `option`(s),
|
|
187
|
-
// this key tracks the option set (the bubble select re-syncs its value off it).
|
|
188
|
-
const nativeSelectKey = Array.from(nativeOptionsSet)
|
|
189
|
-
.map((option: any) => option?.props?.value)
|
|
190
|
-
.join(';');
|
|
191
|
-
|
|
192
183
|
const handleNativeOptionAdd = useCallback(
|
|
193
184
|
(option: any) => {
|
|
194
185
|
setNativeOptionsSet((prev: Set<any>) => new Set(prev).add(option));
|
|
@@ -229,7 +220,6 @@ export function Provider(props: any): any {
|
|
|
229
220
|
autoComplete,
|
|
230
221
|
form,
|
|
231
222
|
nativeOptions: nativeOptionsSet,
|
|
232
|
-
nativeSelectKey,
|
|
233
223
|
isFormControl,
|
|
234
224
|
};
|
|
235
225
|
|
|
@@ -1690,7 +1680,7 @@ export function BubbleInput(props: any): any {
|
|
|
1690
1680
|
const { __scopeSelect, ref: forwardedRef, ...selectProps } = props ?? {};
|
|
1691
1681
|
const context = useSelectContext(BUBBLE_INPUT_NAME, __scopeSelect);
|
|
1692
1682
|
const { value, onValueChange, required, disabled, name, autoComplete, form } = context;
|
|
1693
|
-
const { nativeOptions
|
|
1683
|
+
const { nativeOptions } = context;
|
|
1694
1684
|
const ref = useRef<HTMLSelectElement | null>(null, subSlot(slot, 'ref'));
|
|
1695
1685
|
const composedRefs = useComposedRefs(forwardedRef, ref, subSlot(slot, 'refs'));
|
|
1696
1686
|
const selectValue = value ?? '';
|
|
@@ -1704,49 +1694,32 @@ export function BubbleInput(props: any): any {
|
|
|
1704
1694
|
(option: any) => (option?.props?.value ?? '') === '',
|
|
1705
1695
|
);
|
|
1706
1696
|
|
|
1707
|
-
// Bubble value change to parents (e.g form change event)
|
|
1697
|
+
// Bubble value change to parents (e.g form change event). The controlled
|
|
1698
|
+
// `value` prop below keeps the select's DOM selection in sync (octane
|
|
1699
|
+
// re-projects it at every commit, covering mount + late option registration
|
|
1700
|
+
// — the source's `key={nativeSelectKey}` rebuild), so this effect only
|
|
1701
|
+
// dispatches the event.
|
|
1708
1702
|
useEffect(
|
|
1709
1703
|
() => {
|
|
1710
1704
|
const select = ref.current;
|
|
1711
1705
|
if (!select) return;
|
|
1712
1706
|
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
selectProto,
|
|
1716
|
-
'value',
|
|
1717
|
-
) as PropertyDescriptor;
|
|
1718
|
-
const setValue = descriptor.set;
|
|
1719
|
-
if (prevValue !== selectValue && setValue) {
|
|
1720
|
-
const event = new Event('change', { bubbles: true });
|
|
1721
|
-
setValue.call(select, selectValue);
|
|
1722
|
-
select.dispatchEvent(event);
|
|
1707
|
+
if (prevValue !== selectValue) {
|
|
1708
|
+
select.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1723
1709
|
}
|
|
1724
1710
|
},
|
|
1725
1711
|
[prevValue, selectValue],
|
|
1726
1712
|
subSlot(slot, 'e:bubble'),
|
|
1727
1713
|
);
|
|
1728
1714
|
|
|
1729
|
-
// octane adaptation: React associates the default value by re-building the keyed
|
|
1730
|
-
// `select` with `defaultValue` each time the option set changes; octane's native-input
|
|
1731
|
-
// model has no defaultValue, so silently sync the `value` property once the rendered
|
|
1732
|
-
// options can represent it (mount + whenever the option set changes).
|
|
1733
|
-
useEffect(
|
|
1734
|
-
() => {
|
|
1735
|
-
const select = ref.current;
|
|
1736
|
-
if (select && select.value !== selectValue) {
|
|
1737
|
-
select.value = selectValue;
|
|
1738
|
-
}
|
|
1739
|
-
},
|
|
1740
|
-
[nativeSelectKey, selectValue],
|
|
1741
|
-
subSlot(slot, 'e:default'),
|
|
1742
|
-
);
|
|
1743
|
-
|
|
1744
1715
|
/**
|
|
1745
1716
|
* We purposefully use a `select` here to support form autofill as much as
|
|
1746
1717
|
* possible.
|
|
1747
1718
|
*
|
|
1748
|
-
*
|
|
1749
|
-
*
|
|
1719
|
+
* The `value` is live CONTROLLED (octane React-parity): the runtime projects
|
|
1720
|
+
* it onto the options at every commit and restores it after event flushes;
|
|
1721
|
+
* value changes still bubble to any parent form `onChange` via the `change`
|
|
1722
|
+
* event dispatched above.
|
|
1750
1723
|
*
|
|
1751
1724
|
* We use visually hidden styles rather than `display: "none"` because
|
|
1752
1725
|
* Safari autofill won't work otherwise.
|
|
@@ -1759,6 +1732,7 @@ export function BubbleInput(props: any): any {
|
|
|
1759
1732
|
autoComplete,
|
|
1760
1733
|
disabled,
|
|
1761
1734
|
form,
|
|
1735
|
+
value: selectValue,
|
|
1762
1736
|
// React's synthetic onChange on a `<select>` is the native `change` event.
|
|
1763
1737
|
onChange: (event: Event) => onValueChange((event.target as HTMLSelectElement).value),
|
|
1764
1738
|
...selectProps,
|
package/src/Slider.ts
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
// Horizontal/Vertical orientation layers translate pointer positions and step keys;
|
|
5
5
|
// Impl owns pointer capture + slide events; Track/Range/Thumb render the parts (thumb
|
|
6
6
|
// positions are percentage-based with an in-bounds offset). Each Thumb renders a
|
|
7
|
-
// hidden native input "bubble input" inside forms (
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// hidden native input "bubble input" inside forms (a live CONTROLLED `value` — octane's
|
|
8
|
+
// React-parity controlled form components keep the DOM property in sync — plus a
|
|
9
|
+
// dispatched bubbling `input` event, which octane's native `<form onInput>` observes
|
|
10
|
+
// directly, so no extra adaptation is needed here).
|
|
10
11
|
import { createElement, useEffect, useMemo, useRef, useState } from 'octane';
|
|
11
12
|
|
|
12
13
|
import { createCollection } from './collection';
|
|
@@ -588,19 +589,16 @@ export function BubbleInput(props: any): any {
|
|
|
588
589
|
const composedRefs = useComposedRefs(ref, forwardedRef, subSlot(slot, 'refs'));
|
|
589
590
|
const prevValue = usePrevious(value, subSlot(slot, 'prev'));
|
|
590
591
|
|
|
591
|
-
// Bubble value change to parents (e.g form change event)
|
|
592
|
+
// Bubble value change to parents (e.g form change event). The controlled
|
|
593
|
+
// `value` prop below keeps the input's DOM value in sync (commit-time
|
|
594
|
+
// property write + reassert), so this effect only dispatches the event.
|
|
592
595
|
useEffect(
|
|
593
596
|
() => {
|
|
594
597
|
const input = ref.current;
|
|
595
598
|
if (!input) return;
|
|
596
599
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
const setValue = descriptor.set;
|
|
600
|
-
if (prevValue !== value && setValue) {
|
|
601
|
-
const event = new Event('input', { bubbles: true });
|
|
602
|
-
setValue.call(input, value);
|
|
603
|
-
input.dispatchEvent(event);
|
|
600
|
+
if (prevValue !== value) {
|
|
601
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
604
602
|
}
|
|
605
603
|
},
|
|
606
604
|
[prevValue, value],
|
|
@@ -610,10 +608,10 @@ export function BubbleInput(props: any): any {
|
|
|
610
608
|
// We purposefully do not use `type="hidden"` here otherwise forms that wrap it
|
|
611
609
|
// will not be able to access its value via the FormData API.
|
|
612
610
|
//
|
|
613
|
-
// octane: the source
|
|
614
|
-
// swallow the programmatic dispatch)
|
|
615
|
-
//
|
|
616
|
-
// the
|
|
611
|
+
// octane: the source uses `defaultValue` + a prototype-descriptor `value` write
|
|
612
|
+
// (React's controlled model would swallow the programmatic dispatch); octane's
|
|
613
|
+
// live CONTROLLED `value` has no such conflict — the runtime keeps the DOM
|
|
614
|
+
// property in sync and the effect above dispatches the bubbling `input` event.
|
|
617
615
|
return createElement(Primitive.input, {
|
|
618
616
|
style: { display: 'none' },
|
|
619
617
|
name,
|
package/src/Switch.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// Ported from @radix-ui/react-switch (source:
|
|
2
2
|
// .radix-primitives/packages/react/switch/src/switch.tsx). A `role=switch` button with
|
|
3
3
|
// a Thumb; inside a form it renders the hidden native-checkbox "bubble input" (same
|
|
4
|
-
// machinery as Checkbox —
|
|
5
|
-
// events). Same octane adaptations as Checkbox.ts:
|
|
6
|
-
// `event.cancelBubble`;
|
|
7
|
-
// `
|
|
4
|
+
// machinery as Checkbox — a live CONTROLLED `checked` prop keeps the DOM in sync, the
|
|
5
|
+
// bubble effect only dispatches events). Same octane adaptations as Checkbox.ts:
|
|
6
|
+
// `isPropagationStopped()` → `event.cancelBubble`; the source's uncontrolled input +
|
|
7
|
+
// descriptor `checked` writes → octane's controlled `checked`; a native `change`
|
|
8
|
+
// dispatched alongside the source's `click`.
|
|
8
9
|
import { createElement, useEffect, useRef, useState } from 'octane';
|
|
9
10
|
|
|
10
11
|
import { composeEventHandlers } from './compose-event-handlers';
|
|
@@ -193,7 +194,6 @@ export function BubbleInput(props: any): any {
|
|
|
193
194
|
control,
|
|
194
195
|
hasConsumerStoppedPropagationRef,
|
|
195
196
|
checked,
|
|
196
|
-
defaultChecked,
|
|
197
197
|
required,
|
|
198
198
|
disabled,
|
|
199
199
|
name,
|
|
@@ -207,22 +207,16 @@ export function BubbleInput(props: any): any {
|
|
|
207
207
|
const prevChecked = usePrevious(checked, subSlot(slot, 'prev'));
|
|
208
208
|
const controlSize = useSize(control, subSlot(slot, 'size'));
|
|
209
209
|
|
|
210
|
-
// Bubble checked change to parents (e.g form change event)
|
|
210
|
+
// Bubble checked change to parents (e.g form change event). The controlled
|
|
211
|
+
// `checked` prop below keeps the input's DOM state in sync, so this effect
|
|
212
|
+
// only dispatches the events (see Checkbox.ts).
|
|
211
213
|
useEffect(
|
|
212
214
|
() => {
|
|
213
215
|
const input = bubbleInput;
|
|
214
216
|
if (!input) return;
|
|
215
217
|
|
|
216
|
-
const inputProto = window.HTMLInputElement.prototype;
|
|
217
|
-
const descriptor = Object.getOwnPropertyDescriptor(
|
|
218
|
-
inputProto,
|
|
219
|
-
'checked',
|
|
220
|
-
) as PropertyDescriptor;
|
|
221
|
-
const setChecked = descriptor.set;
|
|
222
|
-
|
|
223
218
|
const bubbles = !hasConsumerStoppedPropagationRef.current;
|
|
224
|
-
if (prevChecked !== checked
|
|
225
|
-
setChecked.call(input, checked);
|
|
219
|
+
if (prevChecked !== checked) {
|
|
226
220
|
input.dispatchEvent(new Event('click', { bubbles }));
|
|
227
221
|
// octane adaptation: also fire the native `change` (see Checkbox.ts header).
|
|
228
222
|
input.dispatchEvent(new Event('change', { bubbles }));
|
|
@@ -232,12 +226,11 @@ export function BubbleInput(props: any): any {
|
|
|
232
226
|
subSlot(slot, 'e:bubble'),
|
|
233
227
|
);
|
|
234
228
|
|
|
235
|
-
const defaultCheckedRef = useRef(checked, subSlot(slot, 'default'));
|
|
236
229
|
return createElement(Primitive.input, {
|
|
237
230
|
type: 'checkbox',
|
|
238
231
|
'aria-hidden': true,
|
|
239
|
-
//
|
|
240
|
-
checked
|
|
232
|
+
// Live CONTROLLED checked (octane React-parity — see Checkbox.ts).
|
|
233
|
+
checked,
|
|
241
234
|
required,
|
|
242
235
|
disabled,
|
|
243
236
|
name,
|