From 23a63cf4b1934030c236926ab12a9cf26ee357d5 Mon Sep 17 00:00:00 2001 From: Soon Wang Date: Fri, 3 Jul 2026 18:55:46 +0800 Subject: [PATCH 01/10] fix(rn): add range prop to Picker component and implement tests --- .../components/react/mpx-picker/index.tsx | 1 + .../runtime/react-native/mpx-picker.spec.ts | 118 ++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 packages/webpack-plugin/test/runtime/react-native/mpx-picker.spec.ts diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx index 820ec34213..0f4933818e 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx @@ -230,6 +230,7 @@ const Picker = forwardRef, PickerProps>( const specificProps = extendObject({}, innerProps, { mode, + range, children, bindchange: onChange, bindcolumnchange: onColumnChange, diff --git a/packages/webpack-plugin/test/runtime/react-native/mpx-picker.spec.ts b/packages/webpack-plugin/test/runtime/react-native/mpx-picker.spec.ts new file mode 100644 index 0000000000..bb27a4a8af --- /dev/null +++ b/packages/webpack-plugin/test/runtime/react-native/mpx-picker.spec.ts @@ -0,0 +1,118 @@ +/// + +const mockOpen = jest.fn() +const mockShow = jest.fn() +const mockHide = jest.fn() +const mockRemove = jest.fn() + +jest.mock('react-native', () => ({ + StyleSheet: { + hairlineWidth: 1 / 3, + create: (style: any) => style + }, + Text: 'Text', + TouchableWithoutFeedback: 'TouchableWithoutFeedback', + View: 'View' +}), { 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('react', () => { + const actual = jest.requireActual('react') + return Object.assign({}, actual, { + forwardRef: (render: any) => render, + createElement: jest.fn((type: any, props: any, ...children: any[]) => ({ type, props: props || {}, children })), + useCallback: (fn: any) => fn, + useContext: jest.fn(() => null), + useEffect: (effect: any) => effect(), + useImperativeHandle: () => undefined, + useMemo: (factory: any) => factory(), + useRef: (init: any) => ({ current: init }), + useState: (init: any) => [typeof init === 'function' ? init() : init, () => undefined] + }) +}) + +jest.mock('../../../lib/runtime/components/react/mpx-popup', () => ({ + createPopupManager: () => ({ + open: mockOpen, + show: mockShow, + hide: mockHide, + remove: mockRemove + }) +})) + +jest.mock('../../../lib/runtime/components/react/mpx-picker/selector', () => ({ + __esModule: true, + default: 'PickerSelector' +})) + +jest.mock('../../../lib/runtime/components/react/mpx-picker/multiSelector', () => ({ + __esModule: true, + default: 'PickerMultiSelector' +})) + +jest.mock('../../../lib/runtime/components/react/mpx-picker/time', () => ({ + __esModule: true, + default: 'PickerTime' +})) + +jest.mock('../../../lib/runtime/components/react/mpx-picker/date', () => ({ + __esModule: true, + default: 'PickerDate' +})) + +jest.mock('../../../lib/runtime/components/react/mpx-picker/region', () => ({ + __esModule: true, + default: 'PickerRegion' +})) + +// eslint-disable-next-line import/first +import Picker from '../../../lib/runtime/components/react/mpx-picker' +// eslint-disable-next-line import/first +import { PickerMode } from '../../../lib/runtime/components/react/mpx-picker/type' + +const findElementByType = (element: any, type: any): any => { + if (!element) return null + if (Array.isArray(element)) { + let matched = null + element.some((item) => { + matched = findElementByType(item, type) + return !!matched + }) + return matched + } + if (element.type === type) return element + return findElementByType(element.props?.children || element.children, type) +} + +describe('MpxPicker RN runtime', () => { + beforeEach(() => { + mockOpen.mockClear() + mockShow.mockClear() + mockHide.mockClear() + mockRemove.mockClear() + }) + + test('passes range to selector popup content after filtering root props', () => { + const range = ['Beijing', 'Shanghai', 'Guangzhou'] + const result = (Picker as any)({ + mode: PickerMode.SELECTOR, + value: 1, + range, + children: 'Select' + }, null) + + const popupContent = mockOpen.mock.calls[0][0] + const selector = findElementByType(popupContent, 'PickerSelector') + const triggerView = findElementByType(result, 'View') + + expect(selector.props.range).toBe(range) + expect(triggerView.props.range).toBeUndefined() + }) +}) From 8f579fd85619bdf5d33bd4f3cf6a370be4b180ab Mon Sep 17 00:00:00 2001 From: Soon Wang Date: Sun, 5 Jul 2026 12:20:28 +0800 Subject: [PATCH 02/10] fix(rn): refine Picker component's type definitions and improve props handling --- .../components/react/mpx-picker/index.tsx | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx index 0f4933818e..7de0f056d9 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx @@ -78,13 +78,13 @@ const styles = StyleSheet.create({ } }) -const pickerModalMap: Record> = { +const pickerModalMap = { [PickerMode.SELECTOR]: PickerSelector, [PickerMode.MULTI_SELECTOR]: PickerMultiSelector, [PickerMode.TIME]: PickerTime, [PickerMode.DATE]: PickerDate, [PickerMode.REGION]: PickerRegion -} +} satisfies Record> const getDefaultValue = (mode: PickerMode) => { switch (mode) { @@ -228,15 +228,6 @@ const Picker = forwardRef, PickerProps>( hide() } - const specificProps = extendObject({}, innerProps, { - mode, - range, - children, - bindchange: onChange, - bindcolumnchange: onColumnChange, - getRange: () => range - }) - const renderPickerContent = () => { if (disabled) { return null @@ -245,8 +236,16 @@ const Picker = forwardRef, PickerProps>( if (!(_mode in pickerModalMap)) { return warn(`[Mpx runtime warn]: Unsupported mode: ${mode}`) } + const specificProps = extendObject({}, innerProps, { + mode: _mode, + range, + children, + bindchange: onChange, + bindcolumnchange: onColumnChange, + getRange: () => range + }) as PickerProps const _value: any = value - const PickerModal = pickerModalMap[_mode] + const PickerModal = pickerModalMap[_mode] as React.ComponentType const renderPickerModal = ( <> {headerText && ( From 50e9588f933464666e372e42b271e7e1d83f49cc Mon Sep 17 00:00:00 2001 From: Soon Wang Date: Sun, 5 Jul 2026 12:31:42 +0800 Subject: [PATCH 03/10] fix(rn): set default range prop to empty array in Picker component and add test for default behavior --- .../runtime/components/react/mpx-picker/index.tsx | 2 +- .../components/react/mpx-picker/multiSelector.tsx | 2 +- .../components/react/mpx-picker/selector.tsx | 2 +- .../test/runtime/react-native/mpx-picker.spec.ts | 15 +++++++++++++++ 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx index 7de0f056d9..5a347039b0 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx @@ -115,7 +115,7 @@ const Picker = forwardRef, PickerProps>( const { mode, value, - range = null, + range = [], children, disabled, bindcancel, diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/multiSelector.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/multiSelector.tsx index 8661d45a95..7503625edf 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/multiSelector.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/multiSelector.tsx @@ -23,7 +23,7 @@ const styles = StyleSheet.create({ }) const formatRangeFun = (range: RangeItem[], rangeKey = '') => - rangeKey ? range.map((item: Obj) => item[rangeKey]) : range + rangeKey ? range.map((item) => (item as Obj)[rangeKey]) : range const formatValueFn = (value: number | number[]) => { return Array.isArray(value) ? value : [value] diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/selector.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/selector.tsx index 6d66dba341..d6e91f1289 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/selector.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/selector.tsx @@ -24,7 +24,7 @@ const styles = StyleSheet.create({ }) const formatRangeFun = (range: RangeItem[], rangeKey = '') => - rangeKey ? range.map((item: Obj) => item[rangeKey]) : range + rangeKey ? range.map((item) => (item as Obj)[rangeKey]) : range const formatValueFn = (value: number | number[] = 0) => { const _value = Array.isArray(value) ? value[0] : value diff --git a/packages/webpack-plugin/test/runtime/react-native/mpx-picker.spec.ts b/packages/webpack-plugin/test/runtime/react-native/mpx-picker.spec.ts index bb27a4a8af..ed6dd3a4a2 100644 --- a/packages/webpack-plugin/test/runtime/react-native/mpx-picker.spec.ts +++ b/packages/webpack-plugin/test/runtime/react-native/mpx-picker.spec.ts @@ -115,4 +115,19 @@ describe('MpxPicker RN runtime', () => { expect(selector.props.range).toBe(range) expect(triggerView.props.range).toBeUndefined() }) + + test('defaults omitted selector range to empty array', () => { + const result = (Picker as any)({ + mode: PickerMode.SELECTOR, + value: 0, + children: 'Select' + }, null) + + const popupContent = mockOpen.mock.calls[0][0] + const selector = findElementByType(popupContent, 'PickerSelector') + const triggerView = findElementByType(result, 'View') + + expect(selector.props.range).toEqual([]) + expect(triggerView.props.range).toBeUndefined() + }) }) From 1fbd62f1e7bad7994d50f6371a37121cb1c5bc2c Mon Sep 17 00:00:00 2001 From: Soon Wang Date: Mon, 6 Jul 2026 19:13:08 +0800 Subject: [PATCH 04/10] fix(rn): enhance Picker component to support text style propagation and update tests --- .../references/rn-template-reference.md | 2 + docs-vitepress/guide/rn/component.md | 2 + .../components/react/mpx-picker/index.tsx | 46 +++++++++++++++---- .../components/react/mpx-picker/type.ts | 4 ++ .../runtime/react-native/mpx-picker.spec.ts | 25 ++++++++++ 5 files changed, 71 insertions(+), 8 deletions(-) diff --git a/.agents/skills/mpx2rn/references/rn-template-reference.md b/.agents/skills/mpx2rn/references/rn-template-reference.md index 55e6cd3708..c11c296da4 100644 --- a/.agents/skills/mpx2rn/references/rn-template-reference.md +++ b/.agents/skills/mpx2rn/references/rn-template-reference.md @@ -1111,6 +1111,8 @@ movable-view 的可移动区域。 从底部弹起的滚动选择器。 +`picker` 节点上的文本样式(如 `color`、`font-size` 等)会透传给触发区域的子节点,用于对齐小程序中父节点文本样式影响子内容的表现。 + #### 属性 | 属性名 | 类型 | 默认值 | 说明 | diff --git a/docs-vitepress/guide/rn/component.md b/docs-vitepress/guide/rn/component.md index 537e495088..dd64fc09f7 100644 --- a/docs-vitepress/guide/rn/component.md +++ b/docs-vitepress/guide/rn/component.md @@ -503,6 +503,8 @@ movable-view的可移动区域。 从底部弹起的滚动选择器。 +`picker` 节点上的文本样式(如 `color`、`font-size` 等)会透传给触发区域的子节点,用于对齐小程序中父节点文本样式影响子内容的表现。 + 属性 | 属性名 | 类型 | 默认值 | 说明 | diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx index 5a347039b0..bc4f0128b4 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx @@ -9,7 +9,7 @@ import PickerRegion from './region' import { FormContext, FormFieldValue, RouteContext } from '../context' import useNodesRef, { HandlerRef } from '../useNodesRef' import useInnerProps, { getCustomEvent } from '../getInnerListeners' -import { extendObject, useLayout } from '../utils' +import { extendObject, splitProps, splitStyle, useLayout, useTextPassThrough, useTransformStyle, wrapChildren } from '../utils' import { createPopupManager } from '../mpx-popup' import { EventType, LanguageCode, PickerMode, PickerProps } from './type' @@ -111,7 +111,9 @@ const buttonTextMap: Record = } const Picker = forwardRef, PickerProps>( - (props: PickerProps, ref): React.JSX.Element => { + (pickerProps: PickerProps, ref): React.JSX.Element => { + const { textProps, innerProps: splitInnerProps } = splitProps(pickerProps) + const props = (splitInnerProps || {}) as PickerProps const { mode, value, @@ -120,7 +122,12 @@ const Picker = forwardRef, PickerProps>( disabled, bindcancel, bindchange, - 'header-text': headerText = '' + 'header-text': headerText = '', + style, + 'enable-var': enableVar, + 'enable-text-pass-through': enableTextPassThrough, + 'parent-width': parentWidth, + 'parent-height': parentHeight } = props const { pageId } = useContext(RouteContext) || {} @@ -130,11 +137,25 @@ const Picker = forwardRef, PickerProps>( const nodeRef = useRef(null) const pickerRef = useRef(null) const { open, show, hide, remove } = useRef(createPopupManager()).current + const { + normalStyle, + hasVarDec, + varContextRef, + hasSelfPercent, + setWidth, + setHeight + } = useTransformStyle(style, { enableVar, parentWidth, parentHeight }) + const { textStyle, innerStyle = {} } = splitStyle(normalStyle) + const textPassThrough = useTextPassThrough(textStyle, textProps, { enableTextPassThrough }) - useNodesRef(props, ref, nodeRef) - const { layoutRef, layoutProps } = useLayout({ + useNodesRef(props, ref, nodeRef, { + style: normalStyle + }) + const { layoutRef, layoutProps, layoutStyle } = useLayout({ props, - hasSelfPercent: false, + hasSelfPercent, + setWidth, + setHeight, nodeRef }) @@ -143,7 +164,8 @@ const Picker = forwardRef, PickerProps>( {}, props, { - ref: nodeRef + ref: nodeRef, + style: extendObject({}, innerStyle, layoutStyle) }, layoutProps ), @@ -284,7 +306,15 @@ const Picker = forwardRef, PickerProps>( return createElement( TouchableWithoutFeedback, { onPress: show }, - createElement(View, innerProps, children) + createElement( + View, + innerProps, + wrapChildren(children, { + hasVarDec, + varContext: varContextRef.current, + textPassThrough + }) + ) ) } ) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/type.ts b/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/type.ts index dea29c665c..063a8d3272 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/type.ts +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/type.ts @@ -28,6 +28,10 @@ export interface BasePickerProps { /** 作为表单组件时的名称 */ name?: string style?: Record + 'enable-var'?: boolean + 'enable-text-pass-through'?: boolean + 'parent-width'?: number + 'parent-height'?: number children?: React.ReactNode range?: RangeItem[] ref?: any diff --git a/packages/webpack-plugin/test/runtime/react-native/mpx-picker.spec.ts b/packages/webpack-plugin/test/runtime/react-native/mpx-picker.spec.ts index ed6dd3a4a2..5bf30cbc1b 100644 --- a/packages/webpack-plugin/test/runtime/react-native/mpx-picker.spec.ts +++ b/packages/webpack-plugin/test/runtime/react-native/mpx-picker.spec.ts @@ -76,6 +76,8 @@ jest.mock('../../../lib/runtime/components/react/mpx-picker/region', () => ({ import Picker from '../../../lib/runtime/components/react/mpx-picker' // eslint-disable-next-line import/first import { PickerMode } from '../../../lib/runtime/components/react/mpx-picker/type' +// eslint-disable-next-line import/first +import { TextPassThroughContext } from '../../../lib/runtime/components/react/context' const findElementByType = (element: any, type: any): any => { if (!element) return null @@ -130,4 +132,27 @@ describe('MpxPicker RN runtime', () => { expect(selector.props.range).toEqual([]) expect(triggerView.props.range).toBeUndefined() }) + + test('passes text style through to trigger children', () => { + const result = (Picker as any)({ + mode: PickerMode.SELECTOR, + value: 0, + range: ['Beijing'], + style: { + width: 100, + color: 'red', + fontSize: 20 + }, + children: 'Select' + }, null) + + const triggerView = findElementByType(result, 'View') + const textProvider = findElementByType(triggerView, TextPassThroughContext.Provider) + + expect(triggerView.props.style).toEqual({ width: 100 }) + expect(textProvider.props.value.textStyle).toEqual({ + color: 'red', + fontSize: 20 + }) + }) }) From 77599732a3b0cbd2c9292dc3851cebaed20aa1bf Mon Sep 17 00:00:00 2001 From: Soon Wang Date: Mon, 6 Jul 2026 19:39:31 +0800 Subject: [PATCH 05/10] fix(rn): add update functionality to Picker component and enhance tests for range updates --- .../components/react/mpx-picker/index.tsx | 32 ++++++++++++++++--- .../runtime/react-native/mpx-picker.spec.ts | 27 ++++++++++++++++ 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx index bc4f0128b4..8b77d177c7 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx @@ -136,7 +136,7 @@ const Picker = forwardRef, PickerProps>( pickerValue.current = Array.isArray(value) ? value.slice() : value const nodeRef = useRef(null) const pickerRef = useRef(null) - const { open, show, hide, remove } = useRef(createPopupManager()).current + const { open, show, hide, remove, update } = useRef(createPopupManager()).current const { normalStyle, hasVarDec, @@ -250,7 +250,7 @@ const Picker = forwardRef, PickerProps>( hide() } - const renderPickerContent = () => { + const getPickerContent = () => { if (disabled) { return null } @@ -268,7 +268,7 @@ const Picker = forwardRef, PickerProps>( }) as PickerProps const _value: any = value const PickerModal = pickerModalMap[_mode] as React.ComponentType - const renderPickerModal = ( + return ( <> {headerText && ( @@ -292,8 +292,16 @@ const Picker = forwardRef, PickerProps>( ) + } + + const renderPickerContent = () => { + const renderPickerModal = getPickerContent() + if (!renderPickerModal) { + return false + } const contentHeight = headerText ? 350 : 310 open(renderPickerModal, pageId, { contentHeight }) + return true } useEffect(() => { @@ -303,9 +311,25 @@ const Picker = forwardRef, PickerProps>( } }, []) + const updatePickerContent = () => { + const renderPickerModal = getPickerContent() + if (!renderPickerModal) { + return false + } + update(renderPickerModal) + return true + } + + const showPicker = () => { + if (renderPickerContent()) { + updatePickerContent() + show() + } + } + return createElement( TouchableWithoutFeedback, - { onPress: show }, + { onPress: showPicker }, createElement( View, innerProps, diff --git a/packages/webpack-plugin/test/runtime/react-native/mpx-picker.spec.ts b/packages/webpack-plugin/test/runtime/react-native/mpx-picker.spec.ts index 5bf30cbc1b..f5f5dfb5c4 100644 --- a/packages/webpack-plugin/test/runtime/react-native/mpx-picker.spec.ts +++ b/packages/webpack-plugin/test/runtime/react-native/mpx-picker.spec.ts @@ -4,6 +4,7 @@ const mockOpen = jest.fn() const mockShow = jest.fn() const mockHide = jest.fn() const mockRemove = jest.fn() +const mockUpdate = jest.fn() jest.mock('react-native', () => ({ StyleSheet: { @@ -43,6 +44,7 @@ jest.mock('../../../lib/runtime/components/react/mpx-popup', () => ({ open: mockOpen, show: mockShow, hide: mockHide, + update: mockUpdate, remove: mockRemove }) })) @@ -99,6 +101,7 @@ describe('MpxPicker RN runtime', () => { mockShow.mockClear() mockHide.mockClear() mockRemove.mockClear() + mockUpdate.mockClear() }) test('passes range to selector popup content after filtering root props', () => { @@ -133,6 +136,30 @@ describe('MpxPicker RN runtime', () => { expect(triggerView.props.range).toBeUndefined() }) + test('updates selector popup content before next open when range changes', () => { + const range = ['Beijing', 'Shanghai'] + ;(Picker as any)({ + mode: PickerMode.SELECTOR, + value: 0, + range: [], + children: 'Select' + }, null) + const result = (Picker as any)({ + mode: PickerMode.SELECTOR, + value: 0, + range, + children: 'Select' + }, null) + + expect(mockUpdate).not.toHaveBeenCalled() + result.props.onPress() + + const popupContent = mockUpdate.mock.calls[mockUpdate.mock.calls.length - 1][0] + const selector = findElementByType(popupContent, 'PickerSelector') + + expect(selector.props.range).toBe(range) + }) + test('passes text style through to trigger children', () => { const result = (Picker as any)({ mode: PickerMode.SELECTOR, From 787cb34fb67c3e0cefd4081d9b3a36b527b31709 Mon Sep 17 00:00:00 2001 From: Soon Wang Date: Tue, 7 Jul 2026 15:36:34 +0800 Subject: [PATCH 06/10] fix(rn): enhance Picker component to handle background styles and update tests --- .../components/react/mpx-picker/index.tsx | 48 +++++++++++-------- .../runtime/react-native/mpx-picker.spec.ts | 38 +++++++++++++-- 2 files changed, 64 insertions(+), 22 deletions(-) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx index 8b77d177c7..34f99d64ab 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx @@ -6,6 +6,7 @@ import PickerMultiSelector from './multiSelector' import PickerTime from './time' import PickerDate from './date' import PickerRegion from './region' +import Portal from '../mpx-portal' import { FormContext, FormFieldValue, RouteContext } from '../context' import useNodesRef, { HandlerRef } from '../useNodesRef' import useInnerProps, { getCustomEvent } from '../getInnerListeners' @@ -142,12 +143,17 @@ const Picker = forwardRef, PickerProps>( hasVarDec, varContextRef, hasSelfPercent, + hasPositionFixed, setWidth, setHeight } = useTransformStyle(style, { enableVar, parentWidth, parentHeight }) - const { textStyle, innerStyle = {} } = splitStyle(normalStyle) + const { textStyle, backgroundStyle, innerStyle = {} } = splitStyle(normalStyle) const textPassThrough = useTextPassThrough(textStyle, textProps, { enableTextPassThrough }) + if (backgroundStyle) { + warn('Picker does not support background image-related styles!') + } + useNodesRef(props, ref, nodeRef, { style: normalStyle }) @@ -258,14 +264,19 @@ const Picker = forwardRef, PickerProps>( if (!(_mode in pickerModalMap)) { return warn(`[Mpx runtime warn]: Unsupported mode: ${mode}`) } - const specificProps = extendObject({}, innerProps, { + const modalProps = props as any + const specificProps = { mode: _mode, range, - children, + 'range-key': modalProps['range-key'], + start: modalProps.start, + end: modalProps.end, + fields: modalProps.fields, + level: modalProps.level, + 'custom-item': modalProps['custom-item'], bindchange: onChange, - bindcolumnchange: onColumnChange, - getRange: () => range - }) as PickerProps + bindcolumnchange: onColumnChange + } as PickerProps const _value: any = value const PickerModal = pickerModalMap[_mode] as React.ComponentType return ( @@ -294,7 +305,7 @@ const Picker = forwardRef, PickerProps>( ) } - const renderPickerContent = () => { + const openPickerContent = () => { const renderPickerModal = getPickerContent() if (!renderPickerModal) { return false @@ -305,29 +316,22 @@ const Picker = forwardRef, PickerProps>( } useEffect(() => { - renderPickerContent() + openPickerContent() return () => { remove() } }, []) - const updatePickerContent = () => { + const showPicker = () => { const renderPickerModal = getPickerContent() if (!renderPickerModal) { - return false + return } update(renderPickerModal) - return true + show() } - const showPicker = () => { - if (renderPickerContent()) { - updatePickerContent() - show() - } - } - - return createElement( + const finalComponent = createElement( TouchableWithoutFeedback, { onPress: showPicker }, createElement( @@ -340,6 +344,12 @@ const Picker = forwardRef, PickerProps>( }) ) ) + + if (hasPositionFixed) { + return createElement(Portal, null, finalComponent) + } + + return finalComponent } ) diff --git a/packages/webpack-plugin/test/runtime/react-native/mpx-picker.spec.ts b/packages/webpack-plugin/test/runtime/react-native/mpx-picker.spec.ts index f5f5dfb5c4..9ce03ab410 100644 --- a/packages/webpack-plugin/test/runtime/react-native/mpx-picker.spec.ts +++ b/packages/webpack-plugin/test/runtime/react-native/mpx-picker.spec.ts @@ -5,6 +5,7 @@ const mockShow = jest.fn() const mockHide = jest.fn() const mockRemove = jest.fn() const mockUpdate = jest.fn() +const mockWarn = jest.fn() jest.mock('react-native', () => ({ StyleSheet: { @@ -102,14 +103,20 @@ describe('MpxPicker RN runtime', () => { mockHide.mockClear() mockRemove.mockClear() mockUpdate.mockClear() + mockWarn.mockClear() + const mpxConfig = (global as any).__mpx.config + mpxConfig.ignoreWarning = true + delete mpxConfig.warnHandler }) - test('passes range to selector popup content after filtering root props', () => { - const range = ['Beijing', 'Shanghai', 'Guangzhou'] + test('passes selector props to popup content after filtering root props', () => { + const range = [{ name: 'Beijing' }, { name: 'Shanghai' }, { name: 'Guangzhou' }] const result = (Picker as any)({ mode: PickerMode.SELECTOR, value: 1, range, + 'range-key': 'name', + style: { width: 100 }, children: 'Select' }, null) @@ -118,6 +125,9 @@ describe('MpxPicker RN runtime', () => { const triggerView = findElementByType(result, 'View') expect(selector.props.range).toBe(range) + expect(selector.props['range-key']).toBe('name') + expect(selector.props.style).toBeUndefined() + expect(selector.props.children).toBeUndefined() expect(triggerView.props.range).toBeUndefined() }) @@ -136,7 +146,7 @@ describe('MpxPicker RN runtime', () => { expect(triggerView.props.range).toBeUndefined() }) - test('updates selector popup content before next open when range changes', () => { + test('updates selector popup content before showing when range changes', () => { const range = ['Beijing', 'Shanghai'] ;(Picker as any)({ mode: PickerMode.SELECTOR, @@ -152,11 +162,14 @@ describe('MpxPicker RN runtime', () => { }, null) expect(mockUpdate).not.toHaveBeenCalled() + const openCallCount = mockOpen.mock.calls.length result.props.onPress() const popupContent = mockUpdate.mock.calls[mockUpdate.mock.calls.length - 1][0] const selector = findElementByType(popupContent, 'PickerSelector') + expect(mockOpen.mock.calls.length).toBe(openCallCount) + expect(mockShow).toHaveBeenCalledTimes(1) expect(selector.props.range).toBe(range) }) @@ -182,4 +195,23 @@ describe('MpxPicker RN runtime', () => { fontSize: 20 }) }) + + test('warns when background image-related styles are used', () => { + const mpxConfig = (global as any).__mpx.config + mpxConfig.ignoreWarning = false + mpxConfig.warnHandler = mockWarn + + const result = (Picker as any)({ + mode: PickerMode.SELECTOR, + value: 0, + range: ['Beijing'], + style: { + backgroundImage: 'url(test.png)' + }, + children: 'Select' + }, null) + + expect(result).toBeTruthy() + expect(mockWarn).toHaveBeenCalledWith('Picker does not support background image-related styles!') + }) }) From 2997b6741a44ba07617863a2f2007e0f8df742f7 Mon Sep 17 00:00:00 2001 From: Soon Wang Date: Tue, 7 Jul 2026 15:44:28 +0800 Subject: [PATCH 07/10] fix(rn): update mockEmitter for improved event handling in Picker tests --- .../test/runtime/react-native/mpx-picker.spec.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/webpack-plugin/test/runtime/react-native/mpx-picker.spec.ts b/packages/webpack-plugin/test/runtime/react-native/mpx-picker.spec.ts index 9ce03ab410..a01bd9cbfc 100644 --- a/packages/webpack-plugin/test/runtime/react-native/mpx-picker.spec.ts +++ b/packages/webpack-plugin/test/runtime/react-native/mpx-picker.spec.ts @@ -6,6 +6,11 @@ const mockHide = jest.fn() const mockRemove = jest.fn() const mockUpdate = jest.fn() const mockWarn = jest.fn() +const mockEmitter = { + addListener: jest.fn(), + emit: jest.fn(), + removeListener: jest.fn() +} jest.mock('react-native', () => ({ StyleSheet: { @@ -14,7 +19,9 @@ jest.mock('react-native', () => ({ }, Text: 'Text', TouchableWithoutFeedback: 'TouchableWithoutFeedback', - View: 'View' + View: 'View', + DeviceEventEmitter: mockEmitter, + NativeEventEmitter: jest.fn(() => mockEmitter) }), { virtual: false }) jest.mock('react-native-gesture-handler', () => ({ @@ -212,6 +219,6 @@ describe('MpxPicker RN runtime', () => { }, null) expect(result).toBeTruthy() - expect(mockWarn).toHaveBeenCalledWith('Picker does not support background image-related styles!') + expect(mockWarn.mock.calls[0][0]).toBe('Picker does not support background image-related styles!') }) }) From afb8e8545a3d4e33e92b07cc9acf2bf228b679f6 Mon Sep 17 00:00:00 2001 From: Soon Wang Date: Wed, 8 Jul 2026 17:07:15 +0800 Subject: [PATCH 08/10] fix(rn): simplify return statement in openPickerContent function --- .../lib/runtime/components/react/mpx-picker/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx index 34f99d64ab..3c438af4ff 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx @@ -308,11 +308,10 @@ const Picker = forwardRef, PickerProps>( const openPickerContent = () => { const renderPickerModal = getPickerContent() if (!renderPickerModal) { - return false + return } const contentHeight = headerText ? 350 : 310 open(renderPickerModal, pageId, { contentHeight }) - return true } useEffect(() => { From 3d89baad61328e9e60191cd9decbec17269b8b32 Mon Sep 17 00:00:00 2001 From: Soon Wang Date: Wed, 8 Jul 2026 19:06:32 +0800 Subject: [PATCH 09/10] fix(rn): update content height handling in Picker and Popup components --- .../template/wx/component-config/picker.js | 6 ----- .../components/react/mpx-picker/index.tsx | 7 +++--- .../components/react/mpx-popup/index.tsx | 10 +++++--- .../components/react/mpx-popup/popupBase.tsx | 6 ++--- .../test/platform/wx/template/picker.spec.js | 22 ++++++++++++++++++ .../runtime/react-native/mpx-picker.spec.ts | 23 +++++++++++++++++++ 6 files changed, 59 insertions(+), 15 deletions(-) create mode 100644 packages/webpack-plugin/test/platform/wx/template/picker.spec.js diff --git a/packages/webpack-plugin/lib/platform/template/wx/component-config/picker.js b/packages/webpack-plugin/lib/platform/template/wx/component-config/picker.js index ad2f1e349c..4902ccbf42 100644 --- a/packages/webpack-plugin/lib/platform/template/wx/component-config/picker.js +++ b/packages/webpack-plugin/lib/platform/template/wx/component-config/picker.js @@ -8,9 +8,6 @@ module.exports = function ({ print }) { const ttPropLog = print({ platform: 'bytedance', tag: TAG_NAME, isError: false }) const baiduPropLog = print({ platform: 'baidu', tag: TAG_NAME, isError: false }) const qaPropLog = print({ platform: 'qa', tag: TAG_NAME, isError: false }) - const iosPropLog = print({ platform: 'ios', tag: TAG_NAME, isError: false }) - const androidPropLog = print({ platform: 'android', tag: TAG_NAME, isError: false }) - const harmonyPropLog = print({ platform: 'harmony', tag: TAG_NAME, isError: false }) const ksPropLog = print({ platform: 'ks', tag: TAG_NAME, isError: false }) return { @@ -48,9 +45,6 @@ module.exports = function ({ print }) { ali: aliPropLog, jd: jdPropLog, qa: qaPropLog, - ios: iosPropLog, - android: androidPropLog, - harmony: harmonyPropLog, ks: ksPropLog } ], diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx index 3c438af4ff..64738361bf 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-picker/index.tsx @@ -256,6 +256,8 @@ const Picker = forwardRef, PickerProps>( hide() } + const getContentHeight = () => headerText ? 350 : 310 + const getPickerContent = () => { if (disabled) { return null @@ -310,8 +312,7 @@ const Picker = forwardRef, PickerProps>( if (!renderPickerModal) { return } - const contentHeight = headerText ? 350 : 310 - open(renderPickerModal, pageId, { contentHeight }) + open(renderPickerModal, pageId, { contentHeight: getContentHeight() }) } useEffect(() => { @@ -326,7 +327,7 @@ const Picker = forwardRef, PickerProps>( if (!renderPickerModal) { return } - update(renderPickerModal) + update(renderPickerModal, { contentHeight: getContentHeight() }) show() } diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-popup/index.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-popup/index.tsx index 1b05de197e..2162babde2 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-popup/index.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-popup/index.tsx @@ -11,6 +11,10 @@ export interface IUsePopupOptions { type?: PopupType } +type PopupOptions = { + contentHeight?: number +} + /** * 根据 type 返回对应的弹窗壳子组件 */ @@ -44,7 +48,7 @@ const createPopupManager = (options: IUsePopupOptions = {}) => { const open = ( childComponent: React.ReactNode, pageId: number | undefined, - options?: { contentHeight?: number } + options?: PopupOptions ) => { if (!isOpen && pageId != null) { isOpen = true @@ -57,9 +61,9 @@ const createPopupManager = (options: IUsePopupOptions = {}) => { } } - const update = (updatedChild: ReactElement | null) => { + const update = (updatedChild: ReactElement | null, options?: PopupOptions) => { if (popupKey !== null && child !== null && updatedChild !== null) { - child = cloneElement(child, { children: updatedChild }) + child = cloneElement(child, Object.assign({ children: updatedChild }, options)) Portal.update(popupKey, child) } } diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-popup/popupBase.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-popup/popupBase.tsx index 441c2426dc..58cdeaaffc 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-popup/popupBase.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-popup/popupBase.tsx @@ -59,8 +59,9 @@ const PopupBase = (props: PopupBaseProps = {}) => { contentHeight = MOVEOUT_HEIGHT, visible = false } = props + const moveoutHeight = contentHeight + bottom const fade = useSharedValue(MASK_OFF) - const slide = useSharedValue(contentHeight) + const slide = useSharedValue(moveoutHeight) const animatedStylesMask = useAnimatedStyle(() => ({ opacity: fade.value @@ -87,7 +88,7 @@ const PopupBase = (props: PopupBaseProps = {}) => { duration: 300 }) slide.value = withTiming( - contentHeight, + moveoutHeight, { easing: Easing.inOut(Easing.poly(3)), duration: 300 @@ -112,7 +113,6 @@ const PopupBase = (props: PopupBaseProps = {}) => { onTouchEnd={hide} style={[ styles.mask, - animatedStylesMask, { pointerEvents: visible ? 'auto' : 'none' } ]} > diff --git a/packages/webpack-plugin/test/platform/wx/template/picker.spec.js b/packages/webpack-plugin/test/platform/wx/template/picker.spec.js new file mode 100644 index 0000000000..93c0705dbd --- /dev/null +++ b/packages/webpack-plugin/test/platform/wx/template/picker.spec.js @@ -0,0 +1,22 @@ +const { compileTemplate, warnFn, errorFn } = require('../../util') + +describe('picker template transform', function () { + afterEach(() => { + warnFn.mockClear() + errorFn.mockClear() + }) + + it('should support header-text in react native modes', function () { + const input = '' + const modes = ['ios', 'android', 'harmony'] + + modes.forEach((mode) => { + compileTemplate(input, { srcMode: 'wx', mode }) + }) + + expect(warnFn.mock.calls.map(args => args[0])).not.toEqual(expect.arrayContaining([ + expect.stringContaining(' does not support [header-text] property') + ])) + expect(errorFn).not.toHaveBeenCalled() + }) +}) diff --git a/packages/webpack-plugin/test/runtime/react-native/mpx-picker.spec.ts b/packages/webpack-plugin/test/runtime/react-native/mpx-picker.spec.ts index a01bd9cbfc..c2e26bd265 100644 --- a/packages/webpack-plugin/test/runtime/react-native/mpx-picker.spec.ts +++ b/packages/webpack-plugin/test/runtime/react-native/mpx-picker.spec.ts @@ -180,6 +180,29 @@ describe('MpxPicker RN runtime', () => { expect(selector.props.range).toBe(range) }) + test('updates popup content height before showing when header text changes', () => { + (Picker as any)({ + mode: PickerMode.SELECTOR, + value: 0, + range: ['Beijing'], + children: 'Select' + }, null) + const result = (Picker as any)({ + mode: PickerMode.SELECTOR, + value: 0, + range: ['Beijing'], + 'header-text': 'City', + children: 'Select' + }, null) + + expect(mockOpen.mock.calls[0][2]).toEqual({ contentHeight: 310 }) + result.props.onPress() + + const updateOptions = mockUpdate.mock.calls[mockUpdate.mock.calls.length - 1][1] + + expect(updateOptions).toEqual({ contentHeight: 350 }) + }) + test('passes text style through to trigger children', () => { const result = (Picker as any)({ mode: PickerMode.SELECTOR, From 2c2bf6048685d3cd12f370169fae999ef533c8fd Mon Sep 17 00:00:00 2001 From: Soon Wang Date: Wed, 8 Jul 2026 19:32:55 +0800 Subject: [PATCH 10/10] fix(rn): add animated styles to PopupBase mask for improved visibility handling --- .../lib/runtime/components/react/mpx-popup/popupBase.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-popup/popupBase.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-popup/popupBase.tsx index 58cdeaaffc..efc65d7e39 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-popup/popupBase.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-popup/popupBase.tsx @@ -113,6 +113,7 @@ const PopupBase = (props: PopupBaseProps = {}) => { onTouchEnd={hide} style={[ styles.mask, + animatedStylesMask, { pointerEvents: visible ? 'auto' : 'none' } ]} >