Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ More importantly, the client is designed to be incorporated into an existing Rea
- @react-native-picker/picker >= 2.9.0
- @react-navigation/bottom-tabs >= 6.5.7
- @react-navigation/native >= 6.1.6
- @react-navigation/native-stack >= 6.11.0
- @react-navigation/stack >= 6.3.16
- @types/react >= 18.3.12
- react >= 18.3.1
Expand Down
1 change: 1 addition & 0 deletions demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@react-native-picker/picker": "2.11.1",
"@react-navigation/bottom-tabs": "6.5.7",
"@react-navigation/native": "6.1.6",
"@react-navigation/native-stack": "6.11.0",
"@react-navigation/stack": "6.3.16",
"expo": "~53.0.17",
"expo-constants": "~17.1.7",
Expand Down
37 changes: 25 additions & 12 deletions demo/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2014,7 +2014,7 @@

"@react-navigation/bottom-tabs@6.5.7":
version "6.5.7"
resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-6.5.7.tgz#08470c96e0d11481422214bb98f0ff034038856c"
resolved "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-6.5.7.tgz#08470c96e0d11481422214bb98f0ff034038856c"
integrity sha512-9oZYyRu2z7+1pr2dX5V54rHFPmlj4ztwQxFe85zwpnGcPtGIsXj7VCIdlHnjRHJBBFCszvJGQpYY6/G2+DfD+A==
dependencies:
"@react-navigation/elements" "^1.3.17"
Expand All @@ -2023,7 +2023,7 @@

"@react-navigation/core@^6.4.8":
version "6.4.17"
resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-6.4.17.tgz#f277a196b578c8a456efcc563d1c9bd87eb4ab04"
resolved "https://registry.npmjs.org/@react-navigation/core/-/core-6.4.17.tgz#f277a196b578c8a456efcc563d1c9bd87eb4ab04"
integrity sha512-Nd76EpomzChWAosGqWOYE3ItayhDzIEzzZsT7PfGcRFDgW5miHV2t4MZcq9YIK4tzxZjVVpYbIynOOQQd1e0Cg==
dependencies:
"@react-navigation/routers" "^6.1.9"
Expand All @@ -2033,14 +2033,22 @@
react-is "^16.13.0"
use-latest-callback "^0.2.1"

"@react-navigation/elements@^1.3.17":
"@react-navigation/elements@^1.3.17", "@react-navigation/elements@^1.3.31":
version "1.3.31"
resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-1.3.31.tgz#28dd802a0787bb03fc0e5be296daf1804dbebbcf"
resolved "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.31.tgz#28dd802a0787bb03fc0e5be296daf1804dbebbcf"
integrity sha512-bUzP4Awlljx5RKEExw8WYtif8EuQni2glDaieYROKTnaxsu9kEIA515sXQgUDZU4Ob12VoL7+z70uO3qrlfXcQ==

"@react-navigation/native-stack@6.11.0":
version "6.11.0"
resolved "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-6.11.0.tgz#a33f92cbd55dfe28fb0ba67df99aaa95240eb87c"
integrity sha512-U5EcUB9Q2NQspCFwYGGNJm0h6wBCOv7T30QjndmvlawLkNt7S7KWbpWyxS9XBHSIKF57RgWjfxuJNTgTstpXxw==
dependencies:
"@react-navigation/elements" "^1.3.31"
warn-once "^0.1.0"

"@react-navigation/native@6.1.6":
version "6.1.6"
resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-6.1.6.tgz#84ff5cf85b91f660470fa9407c06c8ee393d5792"
resolved "https://registry.npmjs.org/@react-navigation/native/-/native-6.1.6.tgz#84ff5cf85b91f660470fa9407c06c8ee393d5792"
integrity sha512-14PmSy4JR8HHEk04QkxQ0ZLuqtiQfb4BV9kkMXD2/jI4TZ+yc43OnO6fQ2o9wm+Bq8pY3DxyerC2AjNUz+oH7Q==
dependencies:
"@react-navigation/core" "^6.4.8"
Expand All @@ -2050,14 +2058,14 @@

"@react-navigation/routers@^6.1.9":
version "6.1.9"
resolved "https://registry.yarnpkg.com/@react-navigation/routers/-/routers-6.1.9.tgz#73f5481a15a38e36592a0afa13c3c064b9f90bed"
resolved "https://registry.npmjs.org/@react-navigation/routers/-/routers-6.1.9.tgz#73f5481a15a38e36592a0afa13c3c064b9f90bed"
integrity sha512-lTM8gSFHSfkJvQkxacGM6VJtBt61ip2XO54aNfswD+KMw6eeZ4oehl7m0me3CR9hnDE4+60iAZR8sAhvCiI3NA==
dependencies:
nanoid "^3.1.23"

"@react-navigation/stack@6.3.16":
version "6.3.16"
resolved "https://registry.yarnpkg.com/@react-navigation/stack/-/stack-6.3.16.tgz#cf94e3c8c1587455515743e91d328beef722e0ab"
resolved "https://registry.npmjs.org/@react-navigation/stack/-/stack-6.3.16.tgz#cf94e3c8c1587455515743e91d328beef722e0ab"
integrity sha512-KTOn9cNuZ6p154Htbl2DiR95Wl+c7niLPRiGs7gjOkyVDGiaGQF9ODNQTYBDE1OxZGHe/EyYc6T2CbmiItLWDg==
dependencies:
"@react-navigation/elements" "^1.3.17"
Expand Down Expand Up @@ -7617,7 +7625,12 @@ mz@^2.7.0:
object-assign "^4.0.1"
thenify-all "^1.0.0"

nanoid@^3.1.23, nanoid@^3.3.11, nanoid@^3.3.7:
nanoid@^3.1.23:
version "3.3.18"
resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz#f66a2de1199ffde0fcf21c8a5f13106b1c081913"
integrity sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==

nanoid@^3.3.11, nanoid@^3.3.7:
version "3.3.11"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
Expand Down Expand Up @@ -8753,7 +8766,7 @@ react-freeze@^1.0.0:

react-is@^16.13.0, react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==

react-is@^18.0.0, react-is@^18.3.1:
Expand Down Expand Up @@ -10510,9 +10523,9 @@ urlpattern-polyfill@^10.0.0:
integrity sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==

use-latest-callback@^0.2.1:
version "0.2.4"
resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.2.4.tgz#35c0f028f85a3f4cf025b06011110e87cc18f57e"
integrity sha512-LS2s2n1usUUnDq4oVh1ca6JFX9uSqUncTfAm44WMg0v6TxL7POUTk1B044NH8TeLkFbNajIsgDHcgNpNzZucdg==
version "0.2.6"
resolved "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.6.tgz#e5ea752808c86219acc179ace0ae3c1203255e77"
integrity sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg==

util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
version "1.0.2"
Expand Down
2 changes: 2 additions & 0 deletions docs/reference_hyperview_component.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ function Screen({ url }) {
```

- `entrypointUrl` is the URL for the screen. `Hyperview` will make a `GET` request to fetch the URL. The response should be an HXML doc, that will be parsed and rendered on-screen.
- `enableNativeRoutes` enables native-stack navigation on iOS and Android. It defaults to `false` and is always disabled on web.
- `fetch` is the fetch method used to make requests in the screen. Since it is injected, the app can modify the method. This is commonly done to add headers for authentication, like an API header or cookie.
- `enableModalDismissGesture` enables swipe-down dismissal for native modal routes on iOS and uses the native `modal` presentation instead of `fullScreenModal`. It defaults to `false` and has no effect when native routes are disabled or on Android.

To see more examples of `Hyperview` features, check out the [demo app code](https://github.com/Instawork/hyperview/blob/2bd2cce6c381d9bb83983024fb7dc42045ad4261/demo/App.tsx#L33-L43).
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@react-native-picker/picker": "^2.9.0",
"@react-navigation/bottom-tabs": "^6.5.7",
"@react-navigation/native": "^6.1.6",
"@react-navigation/native-stack": "^6.11.0",
"@react-navigation/stack": "^6.3.16",
"@types/react": "~18.3.12",
"react": "^18.3.1",
Expand All @@ -65,6 +66,7 @@
"@react-native-picker/picker": "2.9.0",
"@react-navigation/bottom-tabs": "6.5.7",
"@react-navigation/native": "6.1.6",
"@react-navigation/native-stack": "6.11.0",
"@react-navigation/stack": "6.3.16",
"@testing-library/react-native": "13.2.0",
"@tsconfig/react-native": "3.0.2",
Expand Down
1 change: 1 addition & 0 deletions scripts/update-demo.sh
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ yarn add \
yarn add --exact \
@react-navigation/bottom-tabs@6.5.7 \
@react-navigation/native@6.1.6 \
@react-navigation/native-stack@6.11.0 \
@react-navigation/stack@6.3.16

# Make Hyperview symlinkable
Expand Down
55 changes: 31 additions & 24 deletions src/components/navigator-stack/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,61 +2,68 @@ import * as CustomStackRouter from 'hyperview/src/components/navigator-stack/rou
import * as NavigatorService from 'hyperview/src/services/navigator';
import * as React from 'react';
import type {
CompatibleStackViewProps,
NativeViewComponent,
NavigationBuilderWithDescribe,
NavigationEventMap,
NavigationOptions,
Props,
StackOptions,
StackViewComponent,
} from './types';
import {
StackActionHelpers,
StackNavigationState,
createNavigatorFactory,
useNavigationBuilder,
} from '@react-navigation/native';
import {
StackNavigationEventMap,
StackNavigationOptions,
StackView,
} from '@react-navigation/stack';
import { NativeStackView } from '@react-navigation/native-stack';
import type { ParamListBase } from '@react-navigation/routers';
import { StackView } from '@react-navigation/stack';
import { useHvDocContext } from 'hyperview/src/elements/hv-doc';
import { useHyperview } from 'hyperview/src/contexts/hyperview';

const CompatibleStackView = StackView as React.ComponentType<CompatibleStackViewProps>;
const CompatibleNativeStackView = NativeStackView as NativeViewComponent;
const CompatibleStackView = StackView as StackViewComponent;

const CustomStackNavigator = (props: Props) => {
const Navigator = (props: Props) => {
const { getSourceDoc } = useHvDocContext();
const { entrypointUrl } = useHyperview();
const { enableNativeRoutes, entrypointUrl } = useHyperview();
const { direction } = NavigatorService.useCompatibleLocale();

const builder = useNavigationBuilder<
StackNavigationState<ParamListBase>,
StackOptions,
StackActionHelpers<ParamListBase>,
StackNavigationOptions,
StackNavigationEventMap
NavigationOptions,
NavigationEventMap
>(CustomStackRouter.Router, {
children: props.children,
entrypointUrl,
getDoc: () => getSourceDoc(),
id: props.id,
initialRouteName: props.initialRouteName,
screenOptions: props.screenOptions,
screenOptions: props.screenOptions as NavigationOptions,
});
const { state, descriptors, navigation, NavigationContent } = builder;
const { describe } = (builder as unknown) as NavigationBuilderWithDescribe;

return (
<NavigationContent>
<CompatibleStackView
describe={NavigatorService.isReactNavigation7 ? describe : undefined}
descriptors={descriptors}
direction={NavigatorService.isReactNavigation7 ? direction : undefined}
navigation={navigation}
state={state}
/>
</NavigationContent>
const stackView = enableNativeRoutes ? (
<CompatibleNativeStackView
describe={NavigatorService.isReactNavigation7 ? describe : undefined}
descriptors={descriptors}
navigation={navigation}
state={state}
/>
) : (
<CompatibleStackView
describe={NavigatorService.isReactNavigation7 ? describe : undefined}
descriptors={descriptors}
direction={NavigatorService.isReactNavigation7 ? direction : undefined}
navigation={navigation}
state={state}
/>
);

return <NavigationContent>{stackView}</NavigationContent>;
};

export default createNavigatorFactory(CustomStackNavigator);
export default createNavigatorFactory(Navigator);
120 changes: 120 additions & 0 deletions src/components/navigator-stack/router.rn6.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import * as DomErrors from 'hyperview/src/services/dom/errors';
import * as NavigatorService from 'hyperview/src/services/navigator';
import { CommonActions } from '@react-navigation/native';
import { DOMParser } from '@instawork/xmldom';
import { Router } from './router';
import { createStackNavigationState } from 'hyperview/test/helpers/navigation';

// Deliberately unmocked, unlike router.test.tsx: that file stubs `useLocale`
// so `isReactNavigation7` reads true while the installed router is still
// React Navigation 6, which cannot exercise the version-gated paths below.

const parser = new DOMParser({
errorHandler: {
error: (error: string) => {
throw new DomErrors.XMLParserError(error);
},
fatalError: (error: string) => {
throw new DomErrors.XMLParserFatalError(error);
},
warning: (error: string) => {
throw new DomErrors.XMLParserWarning(error);
},
},
locator: {},
});

const routerOptions = {
routeGetIdList: {
card: undefined,
modal: undefined,
'tabs-route': undefined,
},
routeNames: ['tabs-route', 'card', 'modal'],
routeParamList: {
card: undefined,
modal: undefined,
'tabs-route': {
id: 'tabs-route',
},
},
};

const doc = parser.parseFromString(`
<doc xmlns="https://hyperview.org/hyperview">
<navigator id="root-navigator" type="stack">
<nav-route id="tabs-route">
<navigator id="tabs-navigator" type="tab">
<nav-route id="shifts-route" href="/shifts" />
<nav-route id="messages-route" href="/messages" />
</navigator>
</nav-route>
</navigator>
</doc>
`);

const router = Router({
entrypointUrl: 'https://example.com',
getDoc: () => doc,
id: 'root-navigator',
});

describe('stack router under React Navigation 6', () => {
it('detects the installed React Navigation 6 dependency', () => {
expect(NavigatorService.isReactNavigation7).toBe(false);
});

describe('with a route stacked above the target navigator', () => {
// The shape behind the storybook regression: a deep-linked card sits on
// top of the tabs, and a bare navigate to a tab route must not discard it.
const state = createStackNavigationState({
index: 1,
key: 'root-stack',
routeNames: routerOptions.routeNames,
routes: [
{
key: 'tabs-route-key',
name: 'tabs-route',
params: { id: 'tabs-route' },
},
{
key: 'card-key',
name: 'card',
},
],
stale: false,
type: 'stack',
});
const action = CommonActions.navigate('messages-route');

it('leaves the action for the mounted child without dropping the card', () => {
const result = router.getStateForAction(state, action, routerOptions);

expect(result).toBeNull();
});
});

describe('with the target navigator on top', () => {
const state = createStackNavigationState({
index: 0,
key: 'root-stack',
routeNames: routerOptions.routeNames,
routes: [
{
key: 'tabs-route-key',
name: 'tabs-route',
params: { id: 'tabs-route' },
},
],
stale: false,
type: 'stack',
});
const action = CommonActions.navigate('messages-route');

it('matches master by leaving the action for the mounted child', () => {
const result = router.getStateForAction(state, action, routerOptions);

expect(result).toBeNull();
});
});
});
Loading