import { nanoid } from 'nanoid/non-secure'; import { CommonNavigationAction, Router, ParamListBase, BaseRouter, DefaultRouterOptions, NavigationState, Route, } from '@react-navigation/native'; import { SheetActions, BottomSheetActionType } from './actions'; export type BottomSheetRouterOptions = DefaultRouterOptions; export type BottomSheetNavigationState = NavigationState & { type: 'sheet'; }; export function BottomSheetRouter(routerOptions: BottomSheetRouterOptions) { const router: Router< BottomSheetNavigationState, CommonNavigationAction | BottomSheetActionType > = { type: 'sheet', getInitialState({ routeNames, routeParamList }) { const initialRouteName = routerOptions.initialRouteName !== undefined && routeNames.includes(routerOptions.initialRouteName) ? routerOptions.initialRouteName : routeNames[0]; return { stale: false, type: 'sheet', key: `sheet-${nanoid()}`, index: 0, routeNames, routes: [ { key: `${initialRouteName}-${nanoid()}`, name: initialRouteName, params: routeParamList[initialRouteName], }, ], }; }, getRehydratedState(partialState, { routeNames, routeParamList }) { let state = partialState; if (state.stale === false) { return state; } const routes = state.routes .filter((route) => routeNames.includes(route.name)) .map( (route) => ({ ...route, key: route.key || `${route.name}-${nanoid()}`, params: routeParamList[route.name] !== undefined ? { ...routeParamList[route.name], ...route.params, } : route.params, } as Route) ); if (routes.length === 0) { const initialRouteName = routerOptions.initialRouteName !== undefined ? routerOptions.initialRouteName : routeNames[0]; routes.push({ key: `${initialRouteName}-${nanoid()}`, name: initialRouteName, params: routeParamList[initialRouteName], }); } return { stale: false, type: 'sheet', key: `sheet-${nanoid()}`, index: routes.length - 1, routeNames, routes, }; }, getStateForRouteNamesChange(state, { routeNames, routeParamList }) { const routes = state.routes.filter((route) => routeNames.includes(route.name) ); if (routes.length === 0) { const initialRouteName = routerOptions.initialRouteName !== undefined && routeNames.includes(routerOptions.initialRouteName) ? routerOptions.initialRouteName : routeNames[0]; routes.push({ key: `${initialRouteName}-${nanoid()}`, name: initialRouteName, params: routeParamList[initialRouteName], }); } return { ...state, routeNames, routes, index: Math.min(state.index, routes.length - 1), }; }, getStateForRouteFocus(state, key) { const index = state.routes.findIndex((r) => r.key === key); if (index === -1 || index === state.index) { return state; } return { ...state, index, routes: state.routes.slice(0, index + 1), }; }, getStateForAction(state, action, options) { const { routeParamList } = options; switch (action.type) { case 'PUSH': { if ( action.payload.name !== undefined && !state.routeNames.includes(action.payload.name) ) { return null; } if (action.payload.key || action.payload.name) { // If the route already exists, navigate to that let index = -1; const getId = // `getId` and `key` can't be used together action.payload.key === undefined && action.payload.name !== undefined ? options.routeGetIdList[action.payload.name] : undefined; const id = getId?.({ params: action.payload.params }); if (id) { index = state.routes.findIndex( (route) => route.name === action.payload.name && id === getId?.({ params: route.params }) ); } else if ( (state.routes[state.index].name === action.payload.name && action.payload.key === undefined) || state.routes[state.index].key === action.payload.key ) { index = state.index; } else { for (let i = state.routes.length - 1; i >= 0; i--) { if ( (state.routes[i].name === action.payload.name && action.payload.key === undefined) || state.routes[i].key === action.payload.key ) { index = i; break; } } } if ( index === -1 && action.payload.key && action.payload.name === undefined ) { return null; } if (index === -1 && action.payload.name !== undefined) { const routes = [ ...state.routes, { key: action.payload.key ?? `${action.payload.name}-${nanoid()}`, name: action.payload.name, params: routeParamList[action.payload.name] !== undefined ? { ...routeParamList[action.payload.name], ...action.payload.params, } : action.payload.params, }, ]; return { ...state, routes, index: routes.length - 1, }; } const route = state.routes[index]; let params; params = action.payload.params ? { ...route.params, ...action.payload.params, } : route.params; return { ...state, index, routes: [ ...state.routes.slice(0, index), params !== route.params ? { ...route, params } : state.routes[index], ], }; } return null; } case 'REPLACE': { const index = action.target === state.key && action.source ? state.routes.findIndex((r) => r.key === action.source) : state.index; if (index === -1) { return null; } const { name, key, params } = action.payload; if (!state.routeNames.includes(name)) { return null; } return { ...state, routes: state.routes.map((route, i) => i === index ? { key: key !== undefined ? key : `${name}-${nanoid()}`, name, params: routeParamList[name] !== undefined ? { ...routeParamList[name], ...params, } : params, } : route ), }; } case 'NAVIGATE': { if ( action.payload.name !== undefined && !state.routeNames.includes(action.payload.name) ) { return null; } if (action.payload.key || action.payload.name) { // If the route already exists, navigate to that let index = -1; const getId = // `getId` and `key` can't be used together action.payload.key === undefined && action.payload.name !== undefined ? options.routeGetIdList[action.payload.name] : undefined; const id = getId?.({ params: action.payload.params }); if (id) { index = state.routes.findIndex( (route) => route.name === action.payload.name && id === getId?.({ params: route.params }) ); } else if ( (state.routes[state.index].name === action.payload.name && action.payload.key === undefined) || state.routes[state.index].key === action.payload.key ) { index = state.index; } else { for (let i = state.routes.length - 1; i >= 0; i--) { if ( (state.routes[i].name === action.payload.name && action.payload.key === undefined) || state.routes[i].key === action.payload.key ) { index = i; break; } } } if ( index === -1 && action.payload.key && action.payload.name === undefined ) { return null; } if (index === -1 && action.payload.name !== undefined) { const routes = [ ...state.routes, { key: action.payload.key ?? `${action.payload.name}-${nanoid()}`, name: action.payload.name, params: routeParamList[action.payload.name] !== undefined ? { ...routeParamList[action.payload.name], ...action.payload.params, } : action.payload.params, }, ]; return { ...state, routes, index: routes.length - 1, }; } const route = state.routes[index]; let params; if (action.payload.merge === false) { params = routeParamList[route.name] !== undefined ? { ...routeParamList[route.name], ...action.payload.params, } : action.payload.params; } else { params = action.payload.params ? { ...route.params, ...action.payload.params, } : route.params; } return { ...state, index, routes: [ ...state.routes.slice(0, index), params !== route.params ? { ...route, params } : state.routes[index], ], }; } return null; } case 'POP': { const index = action.target === state.key && action.source ? state.routes.findIndex((r) => r.key === action.source) : state.index; if (index > 0) { const routes = state.routes .slice(0, state.routes.length - 1) .concat(state.routes.slice(index + 1)); return { ...state, index: routes.length - 1, routes, }; } return null; } case 'POP_ALL': { const index = action.target === state.key && action.source ? state.routes.findIndex((r) => r.key === action.source) : state.index; if (index > 1) { const routes = state.routes.slice(0, 1); return { ...state, index: 0, routes, }; } return null; } case 'POP_TO_TOP': { const index = action.target === state.key && action.source ? state.routes.findIndex((r) => r.key === action.source) : state.index; if (index > 2) { const routes = state.routes.slice(0, 2); return { ...state, index: 0, routes, }; } return null; } case 'GO_BACK': { if (state.index > 0) { return router.getStateForAction( state, { type: 'POP', payload: {}, target: action.target, source: action.source, }, options ); } return null; } default: return BaseRouter.getStateForAction(state, action); } }, shouldActionChangeFocus(action: CommonNavigationAction) { return action.type === 'NAVIGATE'; }, actionCreators: SheetActions, }; return router; }