| 1 | import type { |
| 2 | NavigationState, |
| 3 | ParamListBase, |
| 4 | StackNavigationState, |
| 5 | } from "@react-navigation/native"; |
| 6 | |
| 7 | type Route = StackNavigationState<ParamListBase>["routes"][number]; |
| 8 | |
| 9 | /** Project one router history into native columns and a modal presentation stack. */ |
| 10 | export function projectWorkspaceStack( |
| 11 | state: StackNavigationState<ParamListBase>, |
| 12 | isOverlay: (route: Route) => boolean, |
| 13 | ) { |
| 14 | const firstOverlay = state.routes.findIndex(isOverlay); |
| 15 | const workspaceRoutes = state.routes.slice(0, firstOverlay < 0 ? undefined : firstOverlay); |
| 16 | const primary = workspaceRoutes.find((route) => route.name === "Home"); |
| 17 | const detail = workspaceRoutes.filter((route) => route.name !== "Home"); |
| 18 | const overlays = firstOverlay < 0 ? [] : state.routes.slice(firstOverlay); |
| 19 | return { primary, detail, overlays }; |
| 20 | } |
| 21 | |
| 22 | /** Native callbacks can arrive after a JS pop or replace; those must not pop the next screen. */ |
| 23 | export function nativeWorkspacePopCount( |
| 24 | state: Pick<NavigationState, "index" | "routes">, |
| 25 | dismissedKey: string, |
| 26 | ): number { |
| 27 | const index = state.routes.findIndex((route) => route.key === dismissedKey); |
| 28 | return index <= 0 || index > state.index ? 0 : state.index - index + 1; |
| 29 | } |
| 30 | |
| 31 | /** POP counts backwards from its source, so dismiss a group from the active descendant. */ |
| 32 | export function nativeWorkspacePopAction( |
| 33 | state: Pick<NavigationState, "key" | "index" | "routes">, |
| 34 | dismissedKey: string, |
| 35 | ) { |
| 36 | const count = nativeWorkspacePopCount(state, dismissedKey); |
| 37 | return count |
| 38 | ? { |
| 39 | type: "POP" as const, |
| 40 | payload: { count }, |
| 41 | source: state.routes[state.index]!.key, |
| 42 | target: state.key, |
| 43 | } |
| 44 | : null; |
| 45 | } |
| 46 | |
| 47 | /** A native card pop must preserve pushes made before its completion event reaches JS. */ |
| 48 | export function nativeStackPopAction( |
| 49 | state: Pick<NavigationState, "key" | "index" | "routes">, |
| 50 | dismissedKey: string, |
| 51 | destinationKey?: string, |
| 52 | ) { |
| 53 | const sourceIndex = state.routes.findIndex((route) => route.key === dismissedKey); |
| 54 | const destinationIndex = |
| 55 | destinationKey === undefined |
| 56 | ? sourceIndex - 1 |
| 57 | : state.routes.findIndex((route) => route.key === destinationKey); |
| 58 | if ( |
| 59 | sourceIndex <= 0 || |
| 60 | sourceIndex > state.index || |
| 61 | destinationIndex < 0 || |
| 62 | destinationIndex >= sourceIndex |
| 63 | ) { |
| 64 | return null; |
| 65 | } |
| 66 | return { |
| 67 | type: "POP" as const, |
| 68 | payload: { count: sourceIndex - destinationIndex }, |
| 69 | source: dismissedKey, |
| 70 | target: state.key, |
| 71 | }; |
| 72 | } |
| 73 | |
| 74 | /** Group pushes with the modal that owns their native stack. */ |
| 75 | export function partitionStackPresentations<T>( |
| 76 | routes: readonly T[], |
| 77 | isModal: (route: T) => boolean, |
| 78 | ): T[][] { |
| 79 | const groups: T[][] = []; |
| 80 | for (const route of routes) { |
| 81 | if (groups.length === 0 || isModal(route)) groups.push([]); |
| 82 | groups.at(-1)!.push(route); |
| 83 | } |
| 84 | return groups; |
| 85 | } |
| 86 | |
| 87 | /** Keep outgoing screens in place until dismissal; Fabric moves enqueue native pop/push operations. */ |
| 88 | export function reconcileStackScreens<T extends { readonly key: string }>( |
| 89 | previous: readonly T[], |
| 90 | current: readonly T[], |
| 91 | completedNativeDismissals: ReadonlySet<string> = new Set(), |
| 92 | ): T[] { |
| 93 | const active = new Set(current.map((route) => route.key)); |
| 94 | const screens = [...current]; |
| 95 | previous.forEach((route, index) => { |
| 96 | if (!active.has(route.key) && !completedNativeDismissals.has(route.key)) { |
| 97 | screens.splice(Math.min(index, screens.length), 0, route); |
| 98 | } |
| 99 | }); |
| 100 | return screens; |
| 101 | } |