From 35626f7fbec775794e3d6233a4ec9cb6464f2b7c Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Thu, 18 Jun 2026 12:08:13 +0800 Subject: [PATCH 1/5] fix(webpack-plugin): avoid duplicate change in RN label controls --- .../runtime/components/react/getInnerListeners.ts | 14 ++++++++++++++ .../lib/runtime/components/react/mpx-checkbox.tsx | 3 ++- .../lib/runtime/components/react/mpx-label.tsx | 6 ++++-- .../lib/runtime/components/react/mpx-radio.tsx | 3 ++- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/packages/webpack-plugin/lib/runtime/components/react/getInnerListeners.ts b/packages/webpack-plugin/lib/runtime/components/react/getInnerListeners.ts index 894b6290ca..1e520ee766 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/getInnerListeners.ts +++ b/packages/webpack-plugin/lib/runtime/components/react/getInnerListeners.ts @@ -19,6 +19,20 @@ const globalEventState: GlobalEventState = { identifier: null } +const labelControlHandledEvents = new WeakSet() + +export const markLabelControlHandled = (evt: any) => { + const { nativeEvent } = evt + if (nativeEvent && typeof nativeEvent === 'object') { + labelControlHandledEvents.add(nativeEvent) + } +} + +export const isLabelControlHandled = (evt: any) => { + const { nativeEvent } = evt + return !!(nativeEvent && typeof nativeEvent === 'object' && labelControlHandledEvents.has(nativeEvent)) +} + const getTouchEvent = ( type: string, event: ExtendedNativeTouchEvent, diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-checkbox.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-checkbox.tsx index 3a9c646202..b375a408e8 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-checkbox.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-checkbox.tsx @@ -23,7 +23,7 @@ import { NativeSyntheticEvent } from 'react-native' import { warn } from '@mpxjs/utils' -import useInnerProps, { getCustomEvent } from './getInnerListeners' +import useInnerProps, { getCustomEvent, markLabelControlHandled } from './getInnerListeners' import useNodesRef, { HandlerRef } from './useNodesRef' import Icon from './mpx-icon' import { splitProps, splitStyle, useLayout, useTransformStyle, wrapChildren, extendObject, useTextPassThroughValue } from './utils' @@ -125,6 +125,7 @@ const Checkbox = forwardRef, CheckboxProps>( const onTap = (evt: NativeSyntheticEvent) => { bindtap && bindtap(getCustomEvent('tap', evt, { layoutRef }, props)) + markLabelControlHandled(evt) onChange(evt) } diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-label.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-label.tsx index 0633dbf26e..ed457a9b12 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-label.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-label.tsx @@ -4,7 +4,7 @@ import { JSX, useRef, forwardRef, ReactNode, useCallback, createElement } from 'react' import { View, ViewStyle, NativeSyntheticEvent } from 'react-native' import { noop, warn } from '@mpxjs/utils' -import useInnerProps, { getCustomEvent } from './getInnerListeners' +import useInnerProps, { getCustomEvent, isLabelControlHandled } from './getInnerListeners' import useNodesRef, { HandlerRef } from './useNodesRef' import { splitProps, splitStyle, useLayout, useTransformStyle, wrapChildren, extendObject, useTextPassThroughValue } from './utils' import { LabelContext, LabelContextValue } from './context' @@ -74,7 +74,9 @@ const Label = forwardRef, LabelProps>( const onTap = useCallback((evt: NativeSyntheticEvent) => { const { bindtap } = propsRef.current bindtap && bindtap(getCustomEvent('tap', evt, { layoutRef }, { props: propsRef.current })) - contextRef.current.triggerChange(evt) + if (!isLabelControlHandled(evt)) { + contextRef.current.triggerChange(evt) + } }, []) const innerProps = useInnerProps( diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-radio.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-radio.tsx index 8928325270..c089d34cab 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-radio.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-radio.tsx @@ -8,7 +8,7 @@ import { JSX, useRef, useState, forwardRef, useEffect, ReactNode, useContext, Di import { View, StyleSheet, ViewStyle, NativeSyntheticEvent } from 'react-native' import { warn } from '@mpxjs/utils' import { LabelContext, RadioGroupContext } from './context' -import useInnerProps, { getCustomEvent } from './getInnerListeners' +import useInnerProps, { getCustomEvent, markLabelControlHandled } from './getInnerListeners' import useNodesRef, { HandlerRef } from './useNodesRef' import { splitProps, splitStyle, useLayout, useTransformStyle, wrapChildren, extendObject, useTextPassThroughValue } from './utils' import Icon from './mpx-icon' @@ -114,6 +114,7 @@ const Radio = forwardRef, RadioProps>( const onTap = (evt: NativeSyntheticEvent) => { bindtap && bindtap(getCustomEvent('tap', evt, { layoutRef }, props)) + markLabelControlHandled(evt) onChange(evt) } From 38bcdc07b72217f5f81a0e58e5c9209ccc1c07db Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Mon, 20 Jul 2026 11:16:21 +0800 Subject: [PATCH 2/5] fix(rn): mark handled label control events --- .../components/react/getInnerListeners.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/webpack-plugin/lib/runtime/components/react/getInnerListeners.ts b/packages/webpack-plugin/lib/runtime/components/react/getInnerListeners.ts index 6f7166edb3..548bf07a67 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/getInnerListeners.ts +++ b/packages/webpack-plugin/lib/runtime/components/react/getInnerListeners.ts @@ -22,18 +22,16 @@ const globalEventState: GlobalEventState = { identifier: null } -const labelControlHandledEvents = new WeakSet() +type LabelControlEvent = NativeSyntheticEvent & { + _labelControlHandled?: boolean +} -export const markLabelControlHandled = (evt: NativeSyntheticEvent) => { - const { nativeEvent } = evt - if (nativeEvent) { - labelControlHandledEvents.add(nativeEvent) - } +export const markLabelControlHandled = (evt: LabelControlEvent) => { + evt._labelControlHandled = true } -export const isLabelControlHandled = (evt: NativeSyntheticEvent) => { - const { nativeEvent } = evt - return !!(nativeEvent && labelControlHandledEvents.has(nativeEvent)) +export const isLabelControlHandled = (evt: LabelControlEvent) => { + return !!evt._labelControlHandled } const baseRemovePropsMap: Record = { From 2a7d7832fcb6189c1f848137dcb990fb6ee1a0bb Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Thu, 23 Jul 2026 18:21:49 +0800 Subject: [PATCH 3/5] fix(webpack-plugin): avoid duplicate label control changes --- .../components/react/getInnerListeners.ts | 8 +-- .../react-native/get-inner-listeners.spec.ts | 50 +++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 packages/webpack-plugin/test/runtime/react-native/get-inner-listeners.spec.ts diff --git a/packages/webpack-plugin/lib/runtime/components/react/getInnerListeners.ts b/packages/webpack-plugin/lib/runtime/components/react/getInnerListeners.ts index 548bf07a67..a459c11de0 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/getInnerListeners.ts +++ b/packages/webpack-plugin/lib/runtime/components/react/getInnerListeners.ts @@ -22,16 +22,16 @@ const globalEventState: GlobalEventState = { identifier: null } -type LabelControlEvent = NativeSyntheticEvent & { +type LabelControlEvent = NativeSyntheticEvent export const markLabelControlHandled = (evt: LabelControlEvent) => { - evt._labelControlHandled = true + evt.nativeEvent._labelControlHandled = true } export const isLabelControlHandled = (evt: LabelControlEvent) => { - return !!evt._labelControlHandled + return !!evt.nativeEvent._labelControlHandled } const baseRemovePropsMap: Record = { diff --git a/packages/webpack-plugin/test/runtime/react-native/get-inner-listeners.spec.ts b/packages/webpack-plugin/test/runtime/react-native/get-inner-listeners.spec.ts new file mode 100644 index 0000000000..95a320d3d3 --- /dev/null +++ b/packages/webpack-plugin/test/runtime/react-native/get-inner-listeners.spec.ts @@ -0,0 +1,50 @@ +/// + +jest.mock('react-native', () => ({ + StyleSheet: { hairlineWidth: 1 / 3 }, + Image: class Image {} +}), { virtual: false }) + +jest.mock('react-native-gesture-handler', () => ({ + Gesture: { Tap: () => ({}), Pan: () => ({}), LongPress: () => ({}) } +}), { virtual: false }) + +jest.mock('react-native-safe-area-context', () => ({ + initialWindowMetrics: { insets: { top: 0, right: 0, bottom: 0, left: 0 } } +}), { virtual: false }) + +jest.mock('@mpxjs/utils', () => ({ + collectDataset: jest.fn() +})) + +jest.mock('../../../lib/runtime/components/react/utils', () => ({ + extendObject: Object.assign, + useNavigation: () => ({}) +})) + +// eslint-disable-next-line import/first +import { + isLabelControlHandled, + markLabelControlHandled +} from '../../../lib/runtime/components/react/getInnerListeners' + +describe('label control event handling', () => { + test('shares the handled state between shallow event copies', () => { + const nativeEvent = {} + const checkboxEvent = { nativeEvent } as any + const labelEvent = Object.assign({}, checkboxEvent) + + markLabelControlHandled(checkboxEvent) + + expect(isLabelControlHandled(labelEvent)).toBe(true) + }) + + test('does not share the handled state between different native events', () => { + const checkboxEvent = { nativeEvent: {} } as any + const nextLabelEvent = { nativeEvent: {} } as any + + markLabelControlHandled(checkboxEvent) + + expect(isLabelControlHandled(nextLabelEvent)).toBe(false) + }) +}) From 2a44929e9d82301366481357f123e026f0beb8c3 Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Fri, 24 Jul 2026 14:09:21 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E5=88=A0=E9=99=A4=E5=8D=95=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../react-native/get-inner-listeners.spec.ts | 50 ------------------- 1 file changed, 50 deletions(-) delete mode 100644 packages/webpack-plugin/test/runtime/react-native/get-inner-listeners.spec.ts diff --git a/packages/webpack-plugin/test/runtime/react-native/get-inner-listeners.spec.ts b/packages/webpack-plugin/test/runtime/react-native/get-inner-listeners.spec.ts deleted file mode 100644 index 95a320d3d3..0000000000 --- a/packages/webpack-plugin/test/runtime/react-native/get-inner-listeners.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -/// - -jest.mock('react-native', () => ({ - StyleSheet: { hairlineWidth: 1 / 3 }, - Image: class Image {} -}), { virtual: false }) - -jest.mock('react-native-gesture-handler', () => ({ - Gesture: { Tap: () => ({}), Pan: () => ({}), LongPress: () => ({}) } -}), { virtual: false }) - -jest.mock('react-native-safe-area-context', () => ({ - initialWindowMetrics: { insets: { top: 0, right: 0, bottom: 0, left: 0 } } -}), { virtual: false }) - -jest.mock('@mpxjs/utils', () => ({ - collectDataset: jest.fn() -})) - -jest.mock('../../../lib/runtime/components/react/utils', () => ({ - extendObject: Object.assign, - useNavigation: () => ({}) -})) - -// eslint-disable-next-line import/first -import { - isLabelControlHandled, - markLabelControlHandled -} from '../../../lib/runtime/components/react/getInnerListeners' - -describe('label control event handling', () => { - test('shares the handled state between shallow event copies', () => { - const nativeEvent = {} - const checkboxEvent = { nativeEvent } as any - const labelEvent = Object.assign({}, checkboxEvent) - - markLabelControlHandled(checkboxEvent) - - expect(isLabelControlHandled(labelEvent)).toBe(true) - }) - - test('does not share the handled state between different native events', () => { - const checkboxEvent = { nativeEvent: {} } as any - const nextLabelEvent = { nativeEvent: {} } as any - - markLabelControlHandled(checkboxEvent) - - expect(isLabelControlHandled(nextLabelEvent)).toBe(false) - }) -}) From 1d5407e810851ac1922699f8a4e25b21dfb1526f Mon Sep 17 00:00:00 2001 From: yandadaFreedom Date: Thu, 10 Sep 2026 15:17:45 +0800 Subject: [PATCH 5/5] fix(rn): mark label controls before tap handlers --- .../lib/runtime/components/react/mpx-checkbox.tsx | 2 +- .../webpack-plugin/lib/runtime/components/react/mpx-radio.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-checkbox.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-checkbox.tsx index 1a9203aeff..833588d78b 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-checkbox.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-checkbox.tsx @@ -120,8 +120,8 @@ const Checkbox = forwardRef, CheckboxProps>( } const onTap = (evt: NativeSyntheticEvent) => { - bindtap && bindtap(getCustomEvent('tap', evt, { layoutRef }, props)) markLabelControlHandled(evt) + bindtap && bindtap(getCustomEvent('tap', evt, { layoutRef }, props)) onChange(evt) } diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-radio.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-radio.tsx index 2d4c3625ba..a86728b883 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-radio.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-radio.tsx @@ -109,8 +109,8 @@ const Radio = forwardRef, RadioProps>( } const onTap = (evt: NativeSyntheticEvent) => { - bindtap && bindtap(getCustomEvent('tap', evt, { layoutRef }, props)) markLabelControlHandled(evt) + bindtap && bindtap(getCustomEvent('tap', evt, { layoutRef }, props)) onChange(evt) }