From 89d1df86cca10eb7bb8f8a09005926cc701be9e7 Mon Sep 17 00:00:00 2001 From: dos1in Date: Thu, 4 Jun 2026 11:32:05 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix(drn):=20=E4=BF=AE=E5=A4=8D=E5=8F=8C?= =?UTF-8?q?=E5=B1=82=20fixed=20=E5=B8=83=E5=B1=80=E5=AF=BC=E8=87=B4?= =?UTF-8?q?=E5=AD=90=E8=8A=82=E7=82=B9=E6=97=A0=E6=B3=95=E7=82=B9=E5=87=BB?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/runtime/components/react/context.ts | 2 ++ .../lib/runtime/components/react/mpx-view.tsx | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/webpack-plugin/lib/runtime/components/react/context.ts b/packages/webpack-plugin/lib/runtime/components/react/context.ts index 8b37fac858..5ba1ca4625 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/context.ts +++ b/packages/webpack-plugin/lib/runtime/components/react/context.ts @@ -105,4 +105,6 @@ export const PortalContext = createContext(null as any) export const StickyContext = createContext({ registerStickyHeader: noop, unregisterStickyHeader: noop }) +export const FixedContext = createContext(false) + export const ProviderContext = createContext(null) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-view.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-view.tsx index 36a5216739..fef7c676e7 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-view.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-view.tsx @@ -5,7 +5,7 @@ * ✔ hover-stay-time */ import { View, TextStyle, NativeSyntheticEvent, ViewProps, ImageStyle, StyleSheet, Image, LayoutChangeEvent } from 'react-native' -import { useRef, useState, useEffect, forwardRef, ReactNode, JSX, createElement } from 'react' +import { useRef, useState, useEffect, useContext, forwardRef, ReactNode, JSX, createElement } from 'react' import useInnerProps from './getInnerListeners' import Animated from 'react-native-reanimated' import useAnimationHooks, { AnimationType } from './animationHooks/index' @@ -17,6 +17,7 @@ import { error, isFunction } from '@mpxjs/utils' import LinearGradient from 'react-native-linear-gradient' import { GestureDetector, PanGesture } from 'react-native-gesture-handler' import Portal from './mpx-portal' +import { FixedContext } from './context' export interface _ViewProps extends ViewProps { style?: ExtendedViewStyle @@ -750,6 +751,7 @@ const _View = forwardRef, _ViewProps>((viewProps, r parentWidth, parentHeight }) + const inFixedContext = useContext(FixedContext) const { textStyle, backgroundStyle, innerStyle = {} } = splitStyle(normalStyle) const textPassThrough = useTextPassThroughValue(textStyle, textProps) @@ -830,6 +832,10 @@ const _View = forwardRef, _ViewProps>((viewProps, r } if (hasPositionFixed) { + finalComponent = createElement(FixedContext.Provider, { value: true }, finalComponent) + } + + if (hasPositionFixed && !inFixedContext) { finalComponent = createElement(Portal, null, finalComponent) } return finalComponent From a5fe02353997b587ad7dbfaccfc37d58fb2c4de6 Mon Sep 17 00:00:00 2001 From: dos1in Date: Thu, 4 Jun 2026 12:20:29 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(rn):=20=E7=BB=99=20portal=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=B1=82=E7=BA=A7=E4=BF=A1=E6=81=AF=E6=9D=A5=E8=A7=A3?= =?UTF-8?q?=E5=86=B3=E4=BA=8B=E4=BB=B6=E6=B6=88=E8=B4=B9=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/runtime/components/react/context.ts | 14 ++++-- .../components/react/mpx-portal/index.tsx | 11 +++-- .../react/mpx-portal/portal-host.tsx | 33 +++++++------ .../react/mpx-portal/portal-manager.tsx | 49 ++++++++++++++++--- .../lib/runtime/components/react/mpx-view.tsx | 28 ++++++++--- 5 files changed, 96 insertions(+), 39 deletions(-) diff --git a/packages/webpack-plugin/lib/runtime/components/react/context.ts b/packages/webpack-plugin/lib/runtime/components/react/context.ts index 5ba1ca4625..96d44c5bc2 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/context.ts +++ b/packages/webpack-plugin/lib/runtime/components/react/context.ts @@ -45,11 +45,15 @@ export interface IntersectionObserver { } export interface PortalContextValue { - mount: (children: React.ReactNode, key?: number | null, id?: number | null) => number | undefined - update: (key: number, children: React.ReactNode) => void + mount: (children: React.ReactNode, key?: number | null, id?: number | null, meta?: PortalMeta) => number | undefined + update: (key: number, children: React.ReactNode, meta?: PortalMeta) => void unmount: (key: number) => void } +export interface PortalMeta { + stackPath?: number[] +} + export interface ScrollViewContextValue { gestureRef: React.RefObject | null scrollOffset: Animated.Value | null @@ -75,6 +79,10 @@ export interface TextPassThroughContextValue { pendingTextProps?: Record } +export interface FixedStackContextValue { + stackPath: number[] +} + export const MovableAreaContext = createContext({ width: 0, height: 0 }) export const FormContext = createContext(null) @@ -105,6 +113,6 @@ export const PortalContext = createContext(null as any) export const StickyContext = createContext({ registerStickyHeader: noop, unregisterStickyHeader: noop }) -export const FixedContext = createContext(false) +export const FixedStackContext = createContext(null) export const ProviderContext = createContext(null) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-portal/index.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-portal/index.tsx index a161764634..ebd654ced5 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-portal/index.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-portal/index.tsx @@ -4,9 +4,10 @@ import PortalHost, { portal } from './portal-host' export type PortalProps = { children?: ReactNode + stackPath?: number[] } -const Portal = ({ children }: PortalProps): null => { +const Portal = ({ children, stackPath }: PortalProps): null => { const manager = useContext(PortalContext) const keyRef = useRef(null) const { pageId } = useContext(RouteContext) || {} @@ -25,15 +26,15 @@ const Portal = ({ children }: PortalProps): null => { } useEffect(() => { - manager.update(keyRef.current, children) - }, [children]) + manager.update(keyRef.current, children, { stackPath }) + }, [children, stackPath]) useEffect(() => { if (!manager) { throw new Error( 'Looks like you forgot to wrap your root component with `PortalHost` component from `@mpxjs/webpack-plugin/lib/runtime/components/react/dist/mpx-portal/index`.\n\n' - ) + ) } - keyRef.current = manager.mount(children, null, pageId) + keyRef.current = manager.mount(children, null, pageId, { stackPath }) return () => { manager.unmount(keyRef.current) } diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-portal/portal-host.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-portal/portal-host.tsx index 7d2268c17d..1724da9864 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-portal/portal-host.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-portal/portal-host.tsx @@ -7,6 +7,7 @@ import { } from 'react-native' import PortalManager from './portal-manager' import { PortalContext, RouteContext } from '../context' +import type { PortalMeta } from '../context' type PortalHostProps = { children: ReactNode, @@ -14,14 +15,14 @@ type PortalHostProps = { } interface PortalManagerContextValue { - mount: (key: number, children: React.ReactNode) => void - update: (key: number, children: React.ReactNode) => void + mount: (key: number, children: React.ReactNode, meta?: PortalMeta) => void + update: (key: number, children: React.ReactNode, meta?: PortalMeta) => void unmount: (key: number) => void } export type Operation = - | { type: 'mount'; key: number; children: ReactNode } - | { type: 'update'; key: number; children: ReactNode } + | { type: 'mount'; key: number; children: ReactNode; meta?: PortalMeta } + | { type: 'update'; key: number; children: ReactNode; meta?: PortalMeta } | { type: 'unmount'; key: number } // events @@ -39,9 +40,9 @@ const styles = StyleSheet.create({ class PortalGuard { private nextKey = 10000 - add = (e: ReactNode, id: number|null) => { + add = (e: ReactNode, id: number|null, meta?: PortalMeta) => { const key = this.nextKey++ - TopViewEventEmitter.emit(addType, e, key, id) + TopViewEventEmitter.emit(addType, e, key, id, meta) return key } @@ -49,8 +50,8 @@ class PortalGuard { TopViewEventEmitter.emit(removeType, key) } - update = (key: number, e: ReactNode) => { - TopViewEventEmitter.emit(updateType, key, e) + update = (key: number, e: ReactNode, meta?: PortalMeta) => { + TopViewEventEmitter.emit(updateType, key, e, meta) } } /** @@ -61,15 +62,15 @@ export const portal = new PortalGuard() const PortalHost = ({ children } :PortalHostProps): JSX.Element => { const _nextKey = useRef(0) const manager = useRef(null) - const queue = useRef>([]) + const queue = useRef>([]) const { pageId } = useContext(RouteContext) || {} - const mount = (children: ReactNode, _key?: number, id?: number|null) => { + const mount = (children: ReactNode, _key?: number, id?: number|null, meta?: PortalMeta) => { if (id !== pageId) return const key = _key || _nextKey.current++ if (manager.current) { - manager.current.mount(key, children) + manager.current.mount(key, children, meta) } else { - queue.current.push({ type: 'mount', key, children }) + queue.current.push({ type: 'mount', key, children, meta }) } return key } @@ -82,11 +83,11 @@ const PortalHost = ({ children } :PortalHostProps): JSX.Element => { } } - const update = (key: number, children?: ReactNode) => { + const update = (key: number, children?: ReactNode, meta?: PortalMeta) => { if (manager.current) { - manager.current.update(key, children) + manager.current.update(key, children, meta) } else { - const operation = { type: 'mount', key, children } + const operation = { type: 'mount', key, children, meta } const index = queue.current.findIndex((q) => q.type === 'mount' && q.key === key) if (index > -1) { queue.current[index] = operation @@ -108,7 +109,7 @@ const PortalHost = ({ children } :PortalHostProps): JSX.Element => { if (!operation) return switch (operation.type) { case 'mount': - manager.current.mount(operation.key, operation.children) + manager.current.mount(operation.key, operation.children, operation.meta) break case 'unmount': manager.current.unmount(operation.key) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-portal/portal-manager.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-portal/portal-manager.tsx index 249b640194..b98485320d 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-portal/portal-manager.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-portal/portal-manager.tsx @@ -1,31 +1,64 @@ -import { useState, useCallback, forwardRef, ForwardedRef, useImperativeHandle, ReactNode, ReactElement, Fragment } from 'react' +import { useState, useCallback, useRef, forwardRef, ForwardedRef, useImperativeHandle, ReactNode, ReactElement, Fragment } from 'react' +import { View, StyleSheet } from 'react-native' +import type { PortalMeta } from '../context' export type State = { portals: Array<{ key: number children: ReactNode + stackPath?: number[] + order: number }> } type PortalManagerProps = { } +type PortalItem = State['portals'][number] + +const styles = StyleSheet.create({ + portal: { + ...StyleSheet.absoluteFillObject + } +}) + +const compareStackPath = (left: number[], right: number[]) => { + const maxLength = Math.max(left.length, right.length) + for (let i = 0; i < maxLength; i++) { + const leftValue = i < left.length ? left[i] : 0 + const rightValue = i < right.length ? right[i] : 0 + if (leftValue !== rightValue) { + return leftValue - rightValue + } + } + return 0 +} + +const comparePortalItems = (left: PortalItem, right: PortalItem) => { + if (left.stackPath && right.stackPath) { + return compareStackPath(left.stackPath, right.stackPath) || left.order - right.order + } + return left.order - right.order +} + const _PortalManager = forwardRef((props: PortalManagerProps, ref:ForwardedRef): ReactElement => { const [state, setState] = useState({ portals: [] }) + const orderRef = useRef(0) - const mount = useCallback((key: number, children: ReactNode) => { + const mount = useCallback((key: number, children: ReactNode, meta?: PortalMeta) => { + const order = orderRef.current++ setState((prevState) => ({ - portals: [...prevState.portals, { key, children }] + portals: [...prevState.portals, { key, children, stackPath: meta?.stackPath, order }] })) }, []) - const update = useCallback((key: number, children: ReactNode) => { + const update = useCallback((key: number, children: ReactNode, meta?: PortalMeta) => { setState((prevState) => ({ portals: prevState.portals.map((item) => { if (item.key === key) { - return Object.assign({}, item, { children }) + return Object.assign({}, item, { children }, meta ? { stackPath: meta.stackPath } : {}) } return item }) @@ -47,9 +80,11 @@ const _PortalManager = forwardRef((props: PortalManagerProps, ref:ForwardedRef - {state.portals.map(({ key, children }) => ( + {[...state.portals].sort(comparePortalItems).map(({ key, children, stackPath }, index) => ( - {children} + {stackPath + ? {children} + : children} ))} diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-view.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-view.tsx index fef7c676e7..9594ce33c9 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-view.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-view.tsx @@ -17,7 +17,7 @@ import { error, isFunction } from '@mpxjs/utils' import LinearGradient from 'react-native-linear-gradient' import { GestureDetector, PanGesture } from 'react-native-gesture-handler' import Portal from './mpx-portal' -import { FixedContext } from './context' +import { FixedStackContext } from './context' export interface _ViewProps extends ViewProps { style?: ExtendedViewStyle @@ -139,6 +139,21 @@ const normalizeStyle = (style: ExtendedViewStyle = {}) => { return style } +const getStyleZIndex = (style?: ExtendedViewStyle) => { + const zIndex = style?.zIndex as unknown + if (typeof zIndex === 'number') return zIndex + if (typeof zIndex === 'string' && zIndex.trim()) { + const parsed = Number(zIndex) + return Number.isFinite(parsed) ? parsed : 0 + } + return 0 +} + +const getFixedStackPath = (style: ExtendedViewStyle, fixedStackContext: { stackPath: number[] } | null) => { + const localZIndex = getStyleZIndex(style) + return fixedStackContext ? [...fixedStackContext.stackPath, localZIndex] : [localZIndex] +} + const isPercent = (val: string | number | undefined): val is string => typeof val === 'string' && PERCENT_REGEX.test(val) const isBackgroundSizeKeyword = (val: string | number): boolean => typeof val === 'string' && /^cover|contain$/.test(val) @@ -751,8 +766,7 @@ const _View = forwardRef, _ViewProps>((viewProps, r parentWidth, parentHeight }) - const inFixedContext = useContext(FixedContext) - + const fixedStackContext = useContext(FixedStackContext) const { textStyle, backgroundStyle, innerStyle = {} } = splitStyle(normalStyle) const textPassThrough = useTextPassThroughValue(textStyle, textProps) @@ -774,6 +788,7 @@ const _View = forwardRef, _ViewProps>((viewProps, r } = useLayout({ props, hasSelfPercent, setWidth, setHeight, nodeRef }) const viewStyle = extendObject({}, innerStyle, layoutStyle) + const fixedStackPath = hasPositionFixed ? getFixedStackPath(viewStyle, fixedStackContext) : undefined const transitionend = isFunction(catchtransitionend) ? catchtransitionend : isFunction(bindtransitionend) @@ -832,11 +847,8 @@ const _View = forwardRef, _ViewProps>((viewProps, r } if (hasPositionFixed) { - finalComponent = createElement(FixedContext.Provider, { value: true }, finalComponent) - } - - if (hasPositionFixed && !inFixedContext) { - finalComponent = createElement(Portal, null, finalComponent) + finalComponent = createElement(FixedStackContext.Provider, { value: { stackPath: fixedStackPath as number[] } }, finalComponent) + finalComponent = createElement(Portal, { stackPath: fixedStackPath }, finalComponent) } return finalComponent }) From 556ce58a6c875d286d2c473f7552e17323b3fce6 Mon Sep 17 00:00:00 2001 From: dos1in Date: Thu, 4 Jun 2026 12:30:16 +0800 Subject: [PATCH 3/3] fix: lint fix --- .../lib/runtime/components/react/mpx-portal/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webpack-plugin/lib/runtime/components/react/mpx-portal/index.tsx b/packages/webpack-plugin/lib/runtime/components/react/mpx-portal/index.tsx index ebd654ced5..e8ed5d345a 100644 --- a/packages/webpack-plugin/lib/runtime/components/react/mpx-portal/index.tsx +++ b/packages/webpack-plugin/lib/runtime/components/react/mpx-portal/index.tsx @@ -32,7 +32,7 @@ const Portal = ({ children, stackPath }: PortalProps): null => { if (!manager) { throw new Error( 'Looks like you forgot to wrap your root component with `PortalHost` component from `@mpxjs/webpack-plugin/lib/runtime/components/react/dist/mpx-portal/index`.\n\n' - ) + ) } keyRef.current = manager.mount(children, null, pageId, { stackPath }) return () => {