Fix back swipe opening the drawer after a cold-start deep link (#11065)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-07-07 16:51:06 +03:00
committed by GitHub
parent 5e24a0b48c
commit 102905b962
3 changed files with 130 additions and 9 deletions
+107
View File
@@ -0,0 +1,107 @@
import {
buildStateObject,
getCurrentRoute,
isStateAtTabRoot,
} from '#/lib/routes/helpers'
import {type State} from '#/lib/routes/types'
describe('getCurrentRoute', () => {
it('returns Home when there is no state', () => {
expect(getCurrentRoute(undefined).name).toBe('Home')
})
it('descends into nested state using the index', () => {
const state = {
index: 0,
routes: [
{
name: 'HomeTab',
state: {
index: 1,
routes: [{name: 'Home'}, {name: 'PostThread'}],
},
},
],
} as unknown as State
expect(getCurrentRoute(state).name).toBe('PostThread')
})
/*
* Right after a cold start from a deep link, nested navigator states are
* still partial and have no `index`. React Navigation focuses the last
* route when rehydrating such a state, so getCurrentRoute must do the
* same (previously it stopped at the tab route, which re-enabled the
* drawer swipe gesture on top of the deep-linked screen).
*/
it('descends into partial nested state without an index', () => {
const state = {
routes: [
{
name: 'HomeTab',
state: {
routes: [{name: 'Home'}, {name: 'PostThread'}],
},
},
],
} as unknown as State
expect(getCurrentRoute(state).name).toBe('PostThread')
})
})
describe('isStateAtTabRoot', () => {
it('returns true for the initial deep link state of a tab root', () => {
const state = buildStateObject('HomeTab', 'Home', {}) as unknown as State
expect(isStateAtTabRoot(state)).toBe(true)
})
it('returns false for the initial deep link state of a nested screen', () => {
const state = buildStateObject(
'HomeTab',
'PostThread',
{name: 'alice.test', rkey: '123'},
[{name: 'Home', params: {}}],
) as unknown as State
expect(isStateAtTabRoot(state)).toBe(false)
})
})
describe('buildStateObject', () => {
it('focuses the deep-linked route in the nested state', () => {
const state = buildStateObject(
'HomeTab',
'PostThread',
{name: 'alice.test', rkey: '123'},
[{name: 'Home', params: {}}],
)
expect(state).toEqual({
index: 0,
routes: [
{
name: 'HomeTab',
state: {
index: 1,
routes: [
{name: 'Home', params: {}},
{name: 'PostThread', params: {name: 'alice.test', rkey: '123'}},
],
},
},
],
})
})
it('builds a single-route state for the Flat navigator', () => {
const state = buildStateObject('Flat', 'PostThread', {
name: 'alice.test',
rkey: '123',
})
expect(state).toEqual({
index: 0,
routes: [{name: 'PostThread', params: {name: 'alice.test', rkey: '123'}}],
})
})
})
+2 -5
View File
@@ -30,7 +30,7 @@ import {
} from '#/lib/hooks/useNotificationHandler'
import {useWebScrollRestoration} from '#/lib/hooks/useWebScrollRestoration'
import {useCallOnce} from '#/lib/once'
import {buildStateObject} from '#/lib/routes/helpers'
import {buildStateObject, getCurrentRoute} from '#/lib/routes/helpers'
import {
type AllNavigatorParams,
type BottomTabNavigatorParams,
@@ -783,10 +783,7 @@ const LINKING = {
getPathFromState(state: State) {
// find the current node in the navigation tree
let node = state.routes[state.index || 0]
while (node.state?.routes && typeof node.state?.index === 'number') {
node = node.state?.routes[node.state?.index]
}
const node = getCurrentRoute(state)
// build the path
const route = router.matchName(node.name)
+21 -4
View File
@@ -13,12 +13,20 @@ export function getRootNavigation<T extends {}>(
export function getCurrentRoute(state?: State) {
if (!state) {
return {name: 'Home'}
return {name: 'Home', params: undefined}
}
/*
* Nested navigator states may still be partial ("stale") right after a cold
* start from a deep link, in which case `index` is not set yet. React
* Navigation focuses the last route when it rehydrates such a state, so
* mirror that here instead of stopping the descent early and misreporting
* the tab root as the current route (which e.g. re-enabled the drawer
* swipe gesture on top of a deep-linked screen).
*/
let node = state.routes[state.index || 0]
while (node.state?.routes && typeof node.state?.index === 'number') {
node = node.state?.routes[node.state?.index]
while (node.state?.routes) {
node = node.state.routes[node.state.index ?? node.state.routes.length - 1]
}
return node
}
@@ -83,15 +91,24 @@ export function buildStateObject(
) {
if (stack === 'Flat') {
return {
index: 0,
routes: [{name: route, params}],
}
}
/*
* Set `index` explicitly so consumers of this state (e.g. getCurrentRoute)
* can tell which route is focused before React Navigation has rehydrated
* the nested navigator state.
*/
const routes = [...state, {name: route, params}]
return {
index: 0,
routes: [
{
name: stack,
state: {
routes: [...state, {name: route, params}],
index: routes.length - 1,
routes,
},
},
],