@toptal/picasso-shared 16.0.0 → 16.0.1-alpha-pf-fix-alpha-release-4de9263c3.0
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/dist-package/src/utils/index.d.ts +2 -0
- package/dist-package/src/utils/index.d.ts.map +1 -1
- package/dist-package/src/utils/index.js +2 -0
- package/dist-package/src/utils/index.js.map +1 -1
- package/dist-package/src/utils/to-react-change-event.d.ts +19 -0
- package/dist-package/src/utils/to-react-change-event.d.ts.map +1 -0
- package/dist-package/src/utils/to-react-change-event.js +28 -0
- package/dist-package/src/utils/to-react-change-event.js.map +1 -0
- package/dist-package/src/utils/to-react-event.d.ts +33 -0
- package/dist-package/src/utils/to-react-event.d.ts.map +1 -0
- package/dist-package/src/utils/to-react-event.js +56 -0
- package/dist-package/src/utils/to-react-event.js.map +1 -0
- package/package.json +3 -3
- package/src/utils/index.ts +2 -0
- package/src/utils/test.ts +186 -0
- package/src/utils/to-react-change-event.ts +42 -0
- package/src/utils/to-react-event.ts +61 -0
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { default as isBrowser } from './is-browser';
|
|
2
2
|
export { default as getElementById } from './get-element-by-id';
|
|
3
3
|
export { default as isForwardRef } from './is-forward-ref';
|
|
4
|
+
export { default as toReactEvent } from './to-react-event';
|
|
5
|
+
export { default as toReactChangeEvent } from './to-react-change-event';
|
|
4
6
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,cAAc,CAAA;AACnD,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAC/D,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kBAAkB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,cAAc,CAAA;AACnD,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAC/D,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC1D,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC1D,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,yBAAyB,CAAA"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { default as isBrowser } from './is-browser';
|
|
2
2
|
export { default as getElementById } from './get-element-by-id';
|
|
3
3
|
export { default as isForwardRef } from './is-forward-ref';
|
|
4
|
+
export { default as toReactEvent } from './to-react-event';
|
|
5
|
+
export { default as toReactChangeEvent } from './to-react-change-event';
|
|
4
6
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,cAAc,CAAA;AACnD,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAC/D,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kBAAkB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,cAAc,CAAA;AACnD,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAC/D,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC1D,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAC1D,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,yBAAyB,CAAA"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
type FormInputElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
|
|
3
|
+
/**
|
|
4
|
+
* Boundary cast for form-component `onChange` adapters. Specialization of
|
|
5
|
+
* `toReactEvent` with a tighter generic constraint (form-input elements only)
|
|
6
|
+
* and a dev-only sanity check on the event target.
|
|
7
|
+
*
|
|
8
|
+
* Use in `onCheckedChange` / `onValueChange` adapters when bridging
|
|
9
|
+
* `@base-ui/react`'s native DOM `Event` to Picasso's `React.ChangeEvent<T>`
|
|
10
|
+
* public type.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* onCheckedChange={(checked, { event }) =>
|
|
14
|
+
* onChange?.(toReactChangeEvent<HTMLInputElement>(event), checked)
|
|
15
|
+
* }
|
|
16
|
+
*/
|
|
17
|
+
declare const toReactChangeEvent: <T extends FormInputElement = HTMLInputElement>(event: Event) => React.ChangeEvent<T>;
|
|
18
|
+
export default toReactChangeEvent;
|
|
19
|
+
//# sourceMappingURL=to-react-change-event.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"to-react-change-event.d.ts","sourceRoot":"","sources":["../../../src/utils/to-react-change-event.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B,KAAK,gBAAgB,GACjB,gBAAgB,GAChB,mBAAmB,GACnB,iBAAiB,CAAA;AAErB;;;;;;;;;;;;;GAaG;AACH,QAAA,MAAM,kBAAkB,GAAI,CAAC,SAAS,gBAAgB,4BAC7C,KAAK,KACX,KAAK,CAAC,WAAW,CAAC,CAAC,CAcrB,CAAA;AAED,eAAe,kBAAkB,CAAA"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import toReactEvent from './to-react-event';
|
|
2
|
+
/**
|
|
3
|
+
* Boundary cast for form-component `onChange` adapters. Specialization of
|
|
4
|
+
* `toReactEvent` with a tighter generic constraint (form-input elements only)
|
|
5
|
+
* and a dev-only sanity check on the event target.
|
|
6
|
+
*
|
|
7
|
+
* Use in `onCheckedChange` / `onValueChange` adapters when bridging
|
|
8
|
+
* `@base-ui/react`'s native DOM `Event` to Picasso's `React.ChangeEvent<T>`
|
|
9
|
+
* public type.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* onCheckedChange={(checked, { event }) =>
|
|
13
|
+
* onChange?.(toReactChangeEvent<HTMLInputElement>(event), checked)
|
|
14
|
+
* }
|
|
15
|
+
*/
|
|
16
|
+
const toReactChangeEvent = (event) => {
|
|
17
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
18
|
+
const target = event.target;
|
|
19
|
+
if (target === null || !('tagName' in target)) {
|
|
20
|
+
// eslint-disable-next-line no-console
|
|
21
|
+
console.warn('[picasso] toReactChangeEvent: event.target is not a DOM element. ' +
|
|
22
|
+
'Consumer onChange may receive an unexpected event shape.');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return toReactEvent(event);
|
|
26
|
+
};
|
|
27
|
+
export default toReactChangeEvent;
|
|
28
|
+
//# sourceMappingURL=to-react-change-event.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"to-react-change-event.js","sourceRoot":"","sources":["../../../src/utils/to-react-change-event.ts"],"names":[],"mappings":"AAEA,OAAO,YAAY,MAAM,kBAAkB,CAAA;AAO3C;;;;;;;;;;;;;GAaG;AACH,MAAM,kBAAkB,GAAG,CACzB,KAAY,EACU,EAAE;IACxB,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC;QAC1C,MAAM,MAAM,GAAG,KAAK,CAAC,MAAwB,CAAA;QAE7C,IAAI,MAAM,KAAK,IAAI,IAAI,CAAC,CAAC,SAAS,IAAI,MAAM,CAAC,EAAE,CAAC;YAC9C,sCAAsC;YACtC,OAAO,CAAC,IAAI,CACV,mEAAmE;gBACjE,0DAA0D,CAC7D,CAAA;QACH,CAAC;IACH,CAAC;IAED,OAAO,YAAY,CAAuB,KAAK,CAAC,CAAA;AAClD,CAAC,CAAA;AAED,eAAe,kBAAkB,CAAA"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Boundary cast at the `@base-ui/react` ↔ Picasso form-component interface.
|
|
4
|
+
*
|
|
5
|
+
* `@base-ui/react` v1 callbacks surface the native DOM `Event` via
|
|
6
|
+
* `eventDetails.event`. Picasso's public `onChange` / `onClick` / etc. types
|
|
7
|
+
* pre-date the migration and expect React-flavored `React.SyntheticEvent`
|
|
8
|
+
* variants (`React.ChangeEvent<T>`, `React.MouseEvent<T>`, …).
|
|
9
|
+
*
|
|
10
|
+
* React doesn't expose a public API to construct a real SyntheticEvent. This
|
|
11
|
+
* helper bridges the type boundary by returning a Proxy that:
|
|
12
|
+
* - Forwards all native-event property access unchanged (identity preserved
|
|
13
|
+
* for `target`, `currentTarget`, `bubbles`, `cancelable`, `defaultPrevented`,
|
|
14
|
+
* `type`, `timeStamp`, `isTrusted`, `preventDefault`, `stopPropagation`, etc.).
|
|
15
|
+
* - Synthesizes the four React-SyntheticEvent shim methods that don't exist
|
|
16
|
+
* on native events: `nativeEvent`, `persist`, `isDefaultPrevented`,
|
|
17
|
+
* `isPropagationStopped`.
|
|
18
|
+
* - Does NOT mutate the underlying native event (Proxy wraps; doesn't touch
|
|
19
|
+
* the original).
|
|
20
|
+
*
|
|
21
|
+
* Runtime: O(1) Proxy allocation + per-access dispatch. No copying.
|
|
22
|
+
*
|
|
23
|
+
* Use the specialized `toReactChangeEvent<T>` helper for form-input `onChange`
|
|
24
|
+
* adapters (tighter generic constraint catches misuse). Use this primitive for
|
|
25
|
+
* non-change-event cases (Slider value events, MouseEvent adapters, etc.).
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* const reactEvent = toReactEvent<React.MouseEvent<HTMLButtonElement>>(nativeEvent)
|
|
29
|
+
* onClick?.(reactEvent)
|
|
30
|
+
*/
|
|
31
|
+
declare const toReactEvent: <R extends React.SyntheticEvent>(event: Event) => R;
|
|
32
|
+
export default toReactEvent;
|
|
33
|
+
//# sourceMappingURL=to-react-event.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"to-react-event.d.ts","sourceRoot":"","sources":["../../../src/utils/to-react-event.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAI9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,QAAA,MAAM,YAAY,GAAI,CAAC,SAAS,KAAK,CAAC,cAAc,SAAS,KAAK,KAAG,CAyBjD,CAAA;AAEpB,eAAe,YAAY,CAAA"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const noop = () => { };
|
|
2
|
+
/**
|
|
3
|
+
* Boundary cast at the `@base-ui/react` ↔ Picasso form-component interface.
|
|
4
|
+
*
|
|
5
|
+
* `@base-ui/react` v1 callbacks surface the native DOM `Event` via
|
|
6
|
+
* `eventDetails.event`. Picasso's public `onChange` / `onClick` / etc. types
|
|
7
|
+
* pre-date the migration and expect React-flavored `React.SyntheticEvent`
|
|
8
|
+
* variants (`React.ChangeEvent<T>`, `React.MouseEvent<T>`, …).
|
|
9
|
+
*
|
|
10
|
+
* React doesn't expose a public API to construct a real SyntheticEvent. This
|
|
11
|
+
* helper bridges the type boundary by returning a Proxy that:
|
|
12
|
+
* - Forwards all native-event property access unchanged (identity preserved
|
|
13
|
+
* for `target`, `currentTarget`, `bubbles`, `cancelable`, `defaultPrevented`,
|
|
14
|
+
* `type`, `timeStamp`, `isTrusted`, `preventDefault`, `stopPropagation`, etc.).
|
|
15
|
+
* - Synthesizes the four React-SyntheticEvent shim methods that don't exist
|
|
16
|
+
* on native events: `nativeEvent`, `persist`, `isDefaultPrevented`,
|
|
17
|
+
* `isPropagationStopped`.
|
|
18
|
+
* - Does NOT mutate the underlying native event (Proxy wraps; doesn't touch
|
|
19
|
+
* the original).
|
|
20
|
+
*
|
|
21
|
+
* Runtime: O(1) Proxy allocation + per-access dispatch. No copying.
|
|
22
|
+
*
|
|
23
|
+
* Use the specialized `toReactChangeEvent<T>` helper for form-input `onChange`
|
|
24
|
+
* adapters (tighter generic constraint catches misuse). Use this primitive for
|
|
25
|
+
* non-change-event cases (Slider value events, MouseEvent adapters, etc.).
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* const reactEvent = toReactEvent<React.MouseEvent<HTMLButtonElement>>(nativeEvent)
|
|
29
|
+
* onClick?.(reactEvent)
|
|
30
|
+
*/
|
|
31
|
+
const toReactEvent = (event) => new Proxy(event, {
|
|
32
|
+
get(target, key) {
|
|
33
|
+
switch (key) {
|
|
34
|
+
case 'nativeEvent':
|
|
35
|
+
return target;
|
|
36
|
+
case 'persist':
|
|
37
|
+
// React 17+ removed event pooling; persist is a no-op in modern React.
|
|
38
|
+
return noop;
|
|
39
|
+
case 'isDefaultPrevented':
|
|
40
|
+
return () => target.defaultPrevented;
|
|
41
|
+
case 'isPropagationStopped':
|
|
42
|
+
// Native events don't track propagation-stop state after dispatch;
|
|
43
|
+
// returning false is safe for consumers checking "did I already stop?".
|
|
44
|
+
return () => false;
|
|
45
|
+
default: {
|
|
46
|
+
const value = Reflect.get(target, key, target);
|
|
47
|
+
// Native event methods (preventDefault, stopPropagation, …) must keep
|
|
48
|
+
// the native event as `this`; invoked through the Proxy they'd receive
|
|
49
|
+
// the Proxy as `this` and throw "Illegal invocation" in real browsers.
|
|
50
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
export default toReactEvent;
|
|
56
|
+
//# sourceMappingURL=to-react-event.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"to-react-event.js","sourceRoot":"","sources":["../../../src/utils/to-react-event.ts"],"names":[],"mappings":"AAEA,MAAM,IAAI,GAAG,GAAS,EAAE,GAAE,CAAC,CAAA;AAE3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,YAAY,GAAG,CAAiC,KAAY,EAAK,EAAE,CACvE,IAAI,KAAK,CAAC,KAAK,EAAE;IACf,GAAG,CAAC,MAAM,EAAE,GAAG;QACb,QAAQ,GAAG,EAAE,CAAC;YACZ,KAAK,aAAa;gBAChB,OAAO,MAAM,CAAA;YACf,KAAK,SAAS;gBACZ,uEAAuE;gBACvE,OAAO,IAAI,CAAA;YACb,KAAK,oBAAoB;gBACvB,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,gBAAgB,CAAA;YACtC,KAAK,sBAAsB;gBACzB,mEAAmE;gBACnE,wEAAwE;gBACxE,OAAO,GAAG,EAAE,CAAC,KAAK,CAAA;YACpB,OAAO,CAAC,CAAC,CAAC;gBACR,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,CAAA;gBAE9C,sEAAsE;gBACtE,uEAAuE;gBACvE,uEAAuE;gBACvE,OAAO,OAAO,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAA;YACjE,CAAC;QACH,CAAC;IACH,CAAC;CACF,CAAiB,CAAA;AAEpB,eAAe,YAAY,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toptal/picasso-shared",
|
|
3
|
-
"version": "16.0.0",
|
|
3
|
+
"version": "16.0.1-alpha-pf-fix-alpha-release-4de9263c3.0",
|
|
4
4
|
"description": "Shared types, utils for Picasso internal usage",
|
|
5
5
|
"author": "Toptal",
|
|
6
6
|
"homepage": "https://github.com/toptal/picasso/tree/master/packages/picasso-shared#readme",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"peerDependencies": {
|
|
24
24
|
"react": ">=16.12.0 < 19.0.0",
|
|
25
25
|
"typescript": "^5.5.0",
|
|
26
|
-
"@toptal/picasso-provider": "
|
|
26
|
+
"@toptal/picasso-provider": "6.0.1-alpha-pf-fix-alpha-release-4de9263c3.0",
|
|
27
27
|
"notistack": "3.0.1",
|
|
28
28
|
"react-dom": ">=16.12.0 < 19.0.0"
|
|
29
29
|
},
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/classnames": "^2.3.1",
|
|
37
37
|
"notistack": "3.0.1",
|
|
38
|
-
"@toptal/picasso-provider": "6.0.0"
|
|
38
|
+
"@toptal/picasso-provider": "6.0.1-alpha-pf-fix-alpha-release-4de9263c3.0"
|
|
39
39
|
},
|
|
40
40
|
"sideEffects": [
|
|
41
41
|
"**/styles.ts",
|
package/src/utils/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { default as isBrowser } from './is-browser'
|
|
2
2
|
export { default as getElementById } from './get-element-by-id'
|
|
3
3
|
export { default as isForwardRef } from './is-forward-ref'
|
|
4
|
+
export { default as toReactEvent } from './to-react-event'
|
|
5
|
+
export { default as toReactChangeEvent } from './to-react-change-event'
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { toReactEvent, toReactChangeEvent } from './'
|
|
2
|
+
|
|
3
|
+
describe('toReactEvent', () => {
|
|
4
|
+
const makeNativeChangeEvent = (target: HTMLInputElement): Event => {
|
|
5
|
+
const event = new Event('change', { bubbles: true, cancelable: true })
|
|
6
|
+
|
|
7
|
+
Object.defineProperty(event, 'target', { value: target, writable: false })
|
|
8
|
+
|
|
9
|
+
return event
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let input: HTMLInputElement
|
|
13
|
+
let nativeEvent: Event
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
input = document.createElement('input')
|
|
17
|
+
input.type = 'checkbox'
|
|
18
|
+
input.checked = true
|
|
19
|
+
nativeEvent = makeNativeChangeEvent(input)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('forwards native event properties unchanged', () => {
|
|
23
|
+
const result =
|
|
24
|
+
toReactEvent<React.ChangeEvent<HTMLInputElement>>(nativeEvent)
|
|
25
|
+
|
|
26
|
+
expect(result.type).toBe('change')
|
|
27
|
+
expect(result.target).toBe(input)
|
|
28
|
+
expect(result.bubbles).toBe(true)
|
|
29
|
+
expect(result.cancelable).toBe(true)
|
|
30
|
+
// Reading `target.checked` works because target IS the native input
|
|
31
|
+
expect(result.target.checked).toBe(true)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('synthesizes `nativeEvent` to return the original event', () => {
|
|
35
|
+
const result =
|
|
36
|
+
toReactEvent<React.ChangeEvent<HTMLInputElement>>(nativeEvent)
|
|
37
|
+
|
|
38
|
+
expect(result.nativeEvent).toBe(nativeEvent)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('synthesizes `persist` as a noop function', () => {
|
|
42
|
+
const result =
|
|
43
|
+
toReactEvent<React.ChangeEvent<HTMLInputElement>>(nativeEvent)
|
|
44
|
+
|
|
45
|
+
expect(typeof result.persist).toBe('function')
|
|
46
|
+
expect(() => result.persist()).not.toThrow()
|
|
47
|
+
expect(result.persist()).toBeUndefined()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('synthesizes `isDefaultPrevented` to reflect native state', () => {
|
|
51
|
+
const result =
|
|
52
|
+
toReactEvent<React.ChangeEvent<HTMLInputElement>>(nativeEvent)
|
|
53
|
+
|
|
54
|
+
expect(result.isDefaultPrevented()).toBe(false)
|
|
55
|
+
nativeEvent.preventDefault()
|
|
56
|
+
expect(result.isDefaultPrevented()).toBe(true)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('synthesizes `isPropagationStopped` to return false (native lacks the state)', () => {
|
|
60
|
+
const result =
|
|
61
|
+
toReactEvent<React.ChangeEvent<HTMLInputElement>>(nativeEvent)
|
|
62
|
+
|
|
63
|
+
expect(result.isPropagationStopped()).toBe(false)
|
|
64
|
+
nativeEvent.stopPropagation()
|
|
65
|
+
// Native events don't track propagation-stop state after dispatch.
|
|
66
|
+
// The shim returns `false` consistently; consumers checking
|
|
67
|
+
// "did I already stop?" get a safe default.
|
|
68
|
+
expect(result.isPropagationStopped()).toBe(false)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('does not mutate the underlying native event', () => {
|
|
72
|
+
const nativeKeys = Object.keys(nativeEvent)
|
|
73
|
+
|
|
74
|
+
toReactEvent<React.ChangeEvent<HTMLInputElement>>(nativeEvent)
|
|
75
|
+
|
|
76
|
+
expect(Object.keys(nativeEvent)).toEqual(nativeKeys)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('forwards method calls to the native event', () => {
|
|
80
|
+
const preventDefaultSpy = jest.spyOn(nativeEvent, 'preventDefault')
|
|
81
|
+
const stopPropagationSpy = jest.spyOn(nativeEvent, 'stopPropagation')
|
|
82
|
+
|
|
83
|
+
const result =
|
|
84
|
+
toReactEvent<React.ChangeEvent<HTMLInputElement>>(nativeEvent)
|
|
85
|
+
|
|
86
|
+
result.preventDefault()
|
|
87
|
+
result.stopPropagation()
|
|
88
|
+
|
|
89
|
+
expect(preventDefaultSpy).toHaveBeenCalled()
|
|
90
|
+
expect(stopPropagationSpy).toHaveBeenCalled()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('binds native methods to the native event (avoids "Illegal invocation")', () => {
|
|
94
|
+
let probeBoundToNativeEvent = false
|
|
95
|
+
|
|
96
|
+
// A method that records whether its `this` is the native event. Native DOM
|
|
97
|
+
// methods (stopPropagation, …) throw "Illegal invocation" in real browsers
|
|
98
|
+
// when invoked with the Proxy as `this` instead of the native event.
|
|
99
|
+
;(nativeEvent as unknown as { probe: () => void }).probe = function probe() {
|
|
100
|
+
probeBoundToNativeEvent = this === nativeEvent
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const result =
|
|
104
|
+
toReactEvent<React.ChangeEvent<HTMLInputElement>>(nativeEvent)
|
|
105
|
+
|
|
106
|
+
;(result as unknown as { probe: () => void }).probe()
|
|
107
|
+
|
|
108
|
+
expect(probeBoundToNativeEvent).toBe(true)
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe('toReactChangeEvent', () => {
|
|
113
|
+
const makeNativeChangeEvent = (target: EventTarget | null): Event => {
|
|
114
|
+
const event = new Event('change', { bubbles: true, cancelable: true })
|
|
115
|
+
|
|
116
|
+
Object.defineProperty(event, 'target', { value: target, writable: false })
|
|
117
|
+
|
|
118
|
+
return event
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let consoleWarnSpy: jest.SpyInstance
|
|
122
|
+
|
|
123
|
+
beforeEach(() => {
|
|
124
|
+
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation()
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
afterEach(() => {
|
|
128
|
+
consoleWarnSpy.mockRestore()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('returns a React.ChangeEvent shape with the input target', () => {
|
|
132
|
+
const input = document.createElement('input')
|
|
133
|
+
|
|
134
|
+
input.type = 'text'
|
|
135
|
+
input.value = 'hello'
|
|
136
|
+
const nativeEvent = makeNativeChangeEvent(input)
|
|
137
|
+
|
|
138
|
+
const result = toReactChangeEvent<HTMLInputElement>(nativeEvent)
|
|
139
|
+
|
|
140
|
+
expect(result.target).toBe(input)
|
|
141
|
+
expect(result.target.value).toBe('hello')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('defaults the generic to HTMLInputElement', () => {
|
|
145
|
+
const input = document.createElement('input')
|
|
146
|
+
const nativeEvent = makeNativeChangeEvent(input)
|
|
147
|
+
|
|
148
|
+
// No generic supplied — defaults to HTMLInputElement.
|
|
149
|
+
const result = toReactChangeEvent(nativeEvent)
|
|
150
|
+
|
|
151
|
+
// TypeScript should infer `result.target` as HTMLInputElement.
|
|
152
|
+
expect(result.target).toBe(input)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('delegates to toReactEvent (inherits Proxy shim methods)', () => {
|
|
156
|
+
const input = document.createElement('input')
|
|
157
|
+
const nativeEvent = makeNativeChangeEvent(input)
|
|
158
|
+
|
|
159
|
+
const result = toReactChangeEvent<HTMLInputElement>(nativeEvent)
|
|
160
|
+
|
|
161
|
+
expect(result.nativeEvent).toBe(nativeEvent)
|
|
162
|
+
expect(typeof result.persist).toBe('function')
|
|
163
|
+
expect(result.isDefaultPrevented()).toBe(false)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('emits a dev-warning when target is null', () => {
|
|
167
|
+
const nativeEvent = makeNativeChangeEvent(null)
|
|
168
|
+
|
|
169
|
+
toReactChangeEvent<HTMLInputElement>(nativeEvent)
|
|
170
|
+
|
|
171
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
172
|
+
expect.stringContaining(
|
|
173
|
+
'toReactChangeEvent: event.target is not a DOM element'
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('does not warn when target is a valid DOM element', () => {
|
|
179
|
+
const input = document.createElement('input')
|
|
180
|
+
const nativeEvent = makeNativeChangeEvent(input)
|
|
181
|
+
|
|
182
|
+
toReactChangeEvent<HTMLInputElement>(nativeEvent)
|
|
183
|
+
|
|
184
|
+
expect(consoleWarnSpy).not.toHaveBeenCalled()
|
|
185
|
+
})
|
|
186
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type React from 'react'
|
|
2
|
+
|
|
3
|
+
import toReactEvent from './to-react-event'
|
|
4
|
+
|
|
5
|
+
type FormInputElement =
|
|
6
|
+
| HTMLInputElement
|
|
7
|
+
| HTMLTextAreaElement
|
|
8
|
+
| HTMLSelectElement
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Boundary cast for form-component `onChange` adapters. Specialization of
|
|
12
|
+
* `toReactEvent` with a tighter generic constraint (form-input elements only)
|
|
13
|
+
* and a dev-only sanity check on the event target.
|
|
14
|
+
*
|
|
15
|
+
* Use in `onCheckedChange` / `onValueChange` adapters when bridging
|
|
16
|
+
* `@base-ui/react`'s native DOM `Event` to Picasso's `React.ChangeEvent<T>`
|
|
17
|
+
* public type.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* onCheckedChange={(checked, { event }) =>
|
|
21
|
+
* onChange?.(toReactChangeEvent<HTMLInputElement>(event), checked)
|
|
22
|
+
* }
|
|
23
|
+
*/
|
|
24
|
+
const toReactChangeEvent = <T extends FormInputElement = HTMLInputElement>(
|
|
25
|
+
event: Event
|
|
26
|
+
): React.ChangeEvent<T> => {
|
|
27
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
28
|
+
const target = event.target as Element | null
|
|
29
|
+
|
|
30
|
+
if (target === null || !('tagName' in target)) {
|
|
31
|
+
// eslint-disable-next-line no-console
|
|
32
|
+
console.warn(
|
|
33
|
+
'[picasso] toReactChangeEvent: event.target is not a DOM element. ' +
|
|
34
|
+
'Consumer onChange may receive an unexpected event shape.'
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return toReactEvent<React.ChangeEvent<T>>(event)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default toReactChangeEvent
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type React from 'react'
|
|
2
|
+
|
|
3
|
+
const noop = (): void => {}
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Boundary cast at the `@base-ui/react` ↔ Picasso form-component interface.
|
|
7
|
+
*
|
|
8
|
+
* `@base-ui/react` v1 callbacks surface the native DOM `Event` via
|
|
9
|
+
* `eventDetails.event`. Picasso's public `onChange` / `onClick` / etc. types
|
|
10
|
+
* pre-date the migration and expect React-flavored `React.SyntheticEvent`
|
|
11
|
+
* variants (`React.ChangeEvent<T>`, `React.MouseEvent<T>`, …).
|
|
12
|
+
*
|
|
13
|
+
* React doesn't expose a public API to construct a real SyntheticEvent. This
|
|
14
|
+
* helper bridges the type boundary by returning a Proxy that:
|
|
15
|
+
* - Forwards all native-event property access unchanged (identity preserved
|
|
16
|
+
* for `target`, `currentTarget`, `bubbles`, `cancelable`, `defaultPrevented`,
|
|
17
|
+
* `type`, `timeStamp`, `isTrusted`, `preventDefault`, `stopPropagation`, etc.).
|
|
18
|
+
* - Synthesizes the four React-SyntheticEvent shim methods that don't exist
|
|
19
|
+
* on native events: `nativeEvent`, `persist`, `isDefaultPrevented`,
|
|
20
|
+
* `isPropagationStopped`.
|
|
21
|
+
* - Does NOT mutate the underlying native event (Proxy wraps; doesn't touch
|
|
22
|
+
* the original).
|
|
23
|
+
*
|
|
24
|
+
* Runtime: O(1) Proxy allocation + per-access dispatch. No copying.
|
|
25
|
+
*
|
|
26
|
+
* Use the specialized `toReactChangeEvent<T>` helper for form-input `onChange`
|
|
27
|
+
* adapters (tighter generic constraint catches misuse). Use this primitive for
|
|
28
|
+
* non-change-event cases (Slider value events, MouseEvent adapters, etc.).
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* const reactEvent = toReactEvent<React.MouseEvent<HTMLButtonElement>>(nativeEvent)
|
|
32
|
+
* onClick?.(reactEvent)
|
|
33
|
+
*/
|
|
34
|
+
const toReactEvent = <R extends React.SyntheticEvent>(event: Event): R =>
|
|
35
|
+
new Proxy(event, {
|
|
36
|
+
get(target, key) {
|
|
37
|
+
switch (key) {
|
|
38
|
+
case 'nativeEvent':
|
|
39
|
+
return target
|
|
40
|
+
case 'persist':
|
|
41
|
+
// React 17+ removed event pooling; persist is a no-op in modern React.
|
|
42
|
+
return noop
|
|
43
|
+
case 'isDefaultPrevented':
|
|
44
|
+
return () => target.defaultPrevented
|
|
45
|
+
case 'isPropagationStopped':
|
|
46
|
+
// Native events don't track propagation-stop state after dispatch;
|
|
47
|
+
// returning false is safe for consumers checking "did I already stop?".
|
|
48
|
+
return () => false
|
|
49
|
+
default: {
|
|
50
|
+
const value = Reflect.get(target, key, target)
|
|
51
|
+
|
|
52
|
+
// Native event methods (preventDefault, stopPropagation, …) must keep
|
|
53
|
+
// the native event as `this`; invoked through the Proxy they'd receive
|
|
54
|
+
// the Proxy as `this` and throw "Illegal invocation" in real browsers.
|
|
55
|
+
return typeof value === 'function' ? value.bind(target) : value
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
}) as unknown as R
|
|
60
|
+
|
|
61
|
+
export default toReactEvent
|