diff --git a/packages/webpack-plugin/lib/runtime/components/react/context.ts b/packages/webpack-plugin/lib/runtime/components/react/context.ts index 8b37fac858..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,4 +113,6 @@ export const PortalContext = createContext(null as any) export const StickyContext = createContext({ registerStickyHeader: noop, unregisterStickyHeader: noop }) +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..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 @@ -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 36a5216739..9594ce33c9 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 { FixedStackContext } from './context' export interface _ViewProps extends ViewProps { style?: ExtendedViewStyle @@ -138,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) @@ -750,7 +766,7 @@ const _View = forwardRef, _ViewProps>((viewProps, r parentWidth, parentHeight }) - + const fixedStackContext = useContext(FixedStackContext) const { textStyle, backgroundStyle, innerStyle = {} } = splitStyle(normalStyle) const textPassThrough = useTextPassThroughValue(textStyle, textProps) @@ -772,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) @@ -830,7 +847,8 @@ const _View = forwardRef, _ViewProps>((viewProps, r } if (hasPositionFixed) { - finalComponent = createElement(Portal, null, finalComponent) + finalComponent = createElement(FixedStackContext.Provider, { value: { stackPath: fixedStackPath as number[] } }, finalComponent) + finalComponent = createElement(Portal, { stackPath: fixedStackPath }, finalComponent) } return finalComponent })