Compare commits

...

65 Commits

Author SHA1 Message Date
osdnk
2980627cbf chore: publish
- @react-navigation/bottom-tabs@5.4.6
 - @react-navigation/compat@5.1.22
 - @react-navigation/core@5.8.0
 - @react-navigation/drawer@5.7.6
 - @react-navigation/material-bottom-tabs@5.2.6
 - @react-navigation/material-top-tabs@5.2.6
 - @react-navigation/native@5.4.1
 - @react-navigation/routers@5.4.5
 - @react-navigation/stack@5.3.8
2020-05-20 10:29:05 +02:00
Michał Osadnik
d024ec6d74 feat: add getCurrentOptions (#8277) 2020-05-19 20:53:08 +02:00
Satyajit Sahoo
4d1b85c751 chore: limit height of material top tabs example 2020-05-19 19:27:05 +02:00
Satyajit Sahoo
4a818fdfb3 chore: tweak master-detail example 2020-05-19 18:56:51 +02:00
Satyajit Sahoo
0194de1061 chore: upgrade bob 2020-05-19 14:25:20 +02:00
Michał Osadnik
7b25c8eb2e feat: add getCurrentRoute (#8254) 2020-05-18 01:55:09 +02:00
Satyajit Sahoo
9304a8a16c chore: publish
- @react-navigation/bottom-tabs@5.4.5
 - @react-navigation/compat@5.1.21
 - @react-navigation/core@5.7.0
 - @react-navigation/drawer@5.7.5
 - @react-navigation/material-bottom-tabs@5.2.5
 - @react-navigation/material-top-tabs@5.2.5
 - @react-navigation/native@5.4.0
 - @react-navigation/stack@5.3.7
2020-05-17 01:20:24 +02:00
Satyajit Sahoo
51b40879bd fix: center icons in material tab bar. fixes #8248 2020-05-17 01:06:29 +02:00
Satyajit Sahoo
51f4d11fdf fix: don't use Object.fromEntries 2020-05-17 00:56:19 +02:00
Satyajit Sahoo
c2aa4bb2eb test: fix integration tests 2020-05-16 21:18:42 +02:00
Satyajit Sahoo
199a892a6d chore: fix the links in the example 2020-05-16 01:58:05 +02:00
Satyajit Sahoo
60cb3c9ba7 feat: add a PathConfig type 2020-05-15 18:57:28 +02:00
Satyajit Sahoo
6ccceaea8b chore: tweak linking config in the example 2020-05-15 18:48:03 +02:00
Satyajit Sahoo
d14f38b80a fix: fix types for linking options 2020-05-15 18:37:58 +02:00
Satyajit Sahoo
c481748f00 chore: publish
- @react-navigation/stack@5.3.6
2020-05-15 17:39:47 +02:00
Satyajit Sahoo
d45dbe97dc fix: reduce header title margin. fixes #8267 2020-05-15 17:39:33 +02:00
Janic Duplessis
7623945f6e chore: fix repo url in readme (#8235) 2020-05-14 19:10:08 +02:00
Satyajit Sahoo
1dddaff45c chore: publish
- @react-navigation/bottom-tabs@5.4.4
 - @react-navigation/compat@5.1.20
 - @react-navigation/core@5.6.1
 - @react-navigation/drawer@5.7.4
 - @react-navigation/material-bottom-tabs@5.2.4
 - @react-navigation/material-top-tabs@5.2.4
 - @react-navigation/native@5.3.2
 - @react-navigation/stack@5.3.5
2020-05-14 13:22:54 +02:00
Satyajit Sahoo
21b397f0d6 fix: don't use flat since it's not supported in node 2020-05-14 13:22:14 +02:00
Satyajit Sahoo
2ff0531695 chore: publish
- @react-navigation/bottom-tabs@5.4.3
 - @react-navigation/compat@5.1.19
 - @react-navigation/core@5.6.0
 - @react-navigation/drawer@5.7.3
 - @react-navigation/material-bottom-tabs@5.2.3
 - @react-navigation/material-top-tabs@5.2.3
 - @react-navigation/native@5.3.1
 - @react-navigation/stack@5.3.4
2020-05-14 12:45:50 +02:00
Satyajit Sahoo
0149e85a95 fix: ignore state updates when we're not mounted
closes #8226
2020-05-14 12:43:44 +02:00
Satyajit Sahoo
3c47716826 fix: ignore extra slashes in the pattern 2020-05-14 04:40:44 +02:00
Satyajit Sahoo
acc9646426 feat: merge path patterns for nested screens (#8253)
Currently, when we define path patterns in the linking config, they ignore the parent screen's path when parsing, for example, say we have this config:

{
  Home: {
    path: 'home',
    screens: {
      Profile: 'u/:id',
    },
  },
}
If we parse the URL /home, we'll get this state:

{
  routes: [{ name: 'Home' }],
}
If we parse the URL /home/u/cal, we'll get this state:

{
  routes: [
    {
      name: 'Home',
      state: {
        routes: [
          {
            name: 'Home',
            state: {
              routes: [
                {
                  name: 'Profile',
                  params: { id: 'cal' },
                },
              ],
            },
          },
        ],
      },
    },
  ],
}
Note how we have 2 Home screens nested inside each other. This is not something people usually expect and it seems to trip up a lot of people. Something like following will be more intuitive:

{
  routes: [
    {
      name: 'Home',
      state: {
        routes: [
          {
            name: 'Profile',
            params: { id: 'cal' },
          },
        ],
      },
    },
  ],
}
Essentially, we're treating patterns as relative to their parent rather than matching the exact segments. This PR changes the behavior of parsing links to treat configs like so. I hope it'll be easier to understand for people.

There is also a new option, exact, which brings back the olde behavior of matching the exact pattern:

{
  Home: {
    path: 'home',
    screens: {
      Profile: {
        path: 'u/:id',
        exact: true,
      },
    },
  },
}
Which will be useful if home is not in the URL, e.g. /u/cal.

This change only affects situations where both parent and child screen configuration have a pattern defined. If the parent didn't have a pattern defined, nothing changes from previous behavior:

{
  Home: {
    screens: {
      Profile: {
        path: 'u/:id',
      },
    },
  },
}
2020-05-14 00:39:32 +02:00
Satyajit Sahoo
6dce0780ed chore: publish
- @react-navigation/stack@5.3.3
2020-05-11 17:31:35 +02:00
Hein Rutjes
dd7cff2016 fix: fix ios transitionspec settle time (#8028)
# Why

When using the stack navigator on iOS, it takes a (too) long time before the  `didFocus` and stack `onTransitionEnd` lifecycle events are triggered. The visual animation is typically completed well within 500 msec, but it takes around 1000 msec before the previously mentioned events are emitted. This causes problems with for instance `react-navigation-shared-element`, which relies on these events to fire in a timely manner(https://github.com/IjzerenHein/react-navigation-shared-element/issues/24)

# How

This PR updates the resting threshold so that the underlying spring settles faster. No visual differences or differences in smoothness were witnessed during testing.

## Before

Time to settle `didFocus`: **941**
Time to settle `stack.onTransitionEnd`: **924**

```
15:59:55.743 [ListViewStack]startTransition, closing: false, nestingDepth: 1
15:59:55.744 [ListViewStack]willFocus, scene: "DetailScreen", depth: 1, closing: false
15:59:55.745 Transition start: "ListScreen" -> "DetailScreen"
15:59:56.667 [ListViewStack]endTransition, closing: false, nestingDepth: 1
15:59:56.668 Transition end: "DetailScreen"
15:59:56.685 [ListViewStack]didFocus, scene: "DetailScreen", depth: 1
```

## After

Time to settle `didFocus`: **529**
Time to settle `stack.onTransitionEnd`: **512**

```
15:55:00.686 [ListViewStack]startTransition, closing: false, nestingDepth: 1
15:55:00.687 [ListViewStack]willFocus, scene: "DetailScreen", depth: 1, closing: false
15:55:00.687 Transition start: "ListScreen" -> "DetailScreen"
15:55:01.198 [ListViewStack]endTransition, closing: false, nestingDepth: 1
15:55:01.199 Transition end: "DetailScreen"
15:55:01.216 [ListViewStack]didFocus, scene: "DetailScreen", depth: 1
2020-05-11 17:03:12 +02:00
Satyajit Sahoo
740c6b6706 chore: publish
- @react-navigation/bottom-tabs@5.4.2
 - @react-navigation/compat@5.1.18
 - @react-navigation/drawer@5.7.2
 - @react-navigation/material-bottom-tabs@5.2.2
 - @react-navigation/material-top-tabs@5.2.2
 - @react-navigation/native@5.3.0
 - @react-navigation/stack@5.3.2
2020-05-10 08:34:40 +02:00
Satyajit Sahoo
039017bc2a feat: initialState should take priority over deep link 2020-05-10 08:28:48 +02:00
Satyajit Sahoo
b85a1c3055 chore: publish
- @react-navigation/bottom-tabs@5.4.1
 - @react-navigation/compat@5.1.17
 - @react-navigation/core@5.5.2
 - @react-navigation/drawer@5.7.1
 - @react-navigation/material-bottom-tabs@5.2.1
 - @react-navigation/material-top-tabs@5.2.1
 - @react-navigation/native@5.2.6
 - @react-navigation/routers@5.4.4
 - @react-navigation/stack@5.3.1
2020-05-08 19:16:47 +02:00
Satyajit Sahoo
18f8188dc8 chore: add source key to package.json 2020-05-08 19:14:29 +02:00
Satyajit Sahoo
47a1229837 fix: fix building typescript definitions. closes #8216 2020-05-08 19:09:13 +02:00
Satyajit Sahoo
00b11e303e chore: publish
- @react-navigation/bottom-tabs@5.4.0
 - @react-navigation/compat@5.1.16
 - @react-navigation/core@5.5.1
 - @react-navigation/drawer@5.7.0
 - @react-navigation/material-bottom-tabs@5.2.0
 - @react-navigation/material-top-tabs@5.2.0
 - @react-navigation/native@5.2.5
 - @react-navigation/routers@5.4.3
 - @react-navigation/stack@5.3.0
2020-05-08 16:34:03 +02:00
Satyajit Sahoo
f384706741 feat: use links in bottom navigation tabs 2020-05-08 16:11:24 +02:00
Satyajit Sahoo
d1a6f3e30e chore: upgrade depenendecies 2020-05-08 16:06:28 +02:00
Satyajit Sahoo
fd6636a8cd chore: update circleci config 2020-05-08 03:19:47 +02:00
Satyajit Sahoo
eb24fea8b9 chore: upgrade depenendecies 2020-05-07 21:08:55 +02:00
Linus Unnebäck
85ae378d8c fix: return a promise-like from getInitialState (#8210) 2020-05-07 20:56:55 +02:00
Satyajit Sahoo
bea14aa26f feat: add generic type aliases for screen props
closes #7971
2020-05-06 19:00:04 +02:00
Satyajit Sahoo
4d1e102f8c fix: include safe are insets in title's margins 2020-05-06 16:49:02 +02:00
Satyajit Sahoo
f07cd13561 fix: add proper margins to the header title 2020-05-06 16:14:40 +02:00
Satyajit Sahoo
f6d06768d3 fix: avoid cleaning up state when a new navigator is mounted. fixes #8195 2020-05-06 15:49:59 +02:00
Satyajit Sahoo
3381d680d7 chore: publish
- @react-navigation/bottom-tabs@5.3.4
 - @react-navigation/compat@5.1.15
 - @react-navigation/core@5.5.0
 - @react-navigation/drawer@5.6.4
 - @react-navigation/material-bottom-tabs@5.1.15
 - @react-navigation/material-top-tabs@5.1.15
 - @react-navigation/native@5.2.4
 - @react-navigation/stack@5.2.19
2020-05-05 20:07:13 +02:00
Wojciech Lewicki
fcd1cc64c1 feat: add support for optional params to linking (#8196) 2020-05-05 17:18:34 +02:00
Wojciech Lewicki
3999fc2836 feat: support params anywhere in path segement (#8184) 2020-05-04 15:07:27 +02:00
Satyajit Sahoo
9fd2635756 fix: return undefined for buildLink if linking is not enabled 2020-05-04 06:35:22 +02:00
Satyajit Sahoo
6bec620a3f chore: publish
- @react-navigation/bottom-tabs@5.3.3
 - @react-navigation/compat@5.1.14
 - @react-navigation/drawer@5.6.3
 - @react-navigation/material-bottom-tabs@5.1.14
 - @react-navigation/material-top-tabs@5.1.14
 - @react-navigation/native@5.2.3
 - @react-navigation/stack@5.2.18
2020-05-01 17:31:59 +02:00
Satyajit Sahoo
c7b8e2e966 fix: default linking enabled to true 2020-05-01 17:28:41 +02:00
Satyajit Sahoo
719e1a7b46 chore: publish
- @react-navigation/bottom-tabs@5.3.2
 - @react-navigation/compat@5.1.13
 - @react-navigation/drawer@5.6.2
 - @react-navigation/material-bottom-tabs@5.1.13
 - @react-navigation/material-top-tabs@5.1.13
 - @react-navigation/native@5.2.2
 - @react-navigation/stack@5.2.17
2020-05-01 16:51:12 +02:00
Satyajit Sahoo
10eca8b92e fix: don't throw when using 'useLinking'. fixes #8171 2020-05-01 16:49:06 +02:00
Satyajit Sahoo
b66e3436a7 chore: publish
- @react-navigation/bottom-tabs@5.3.1
 - @react-navigation/compat@5.1.12
 - @react-navigation/drawer@5.6.1
 - @react-navigation/material-bottom-tabs@5.1.12
 - @react-navigation/material-top-tabs@5.1.12
 - @react-navigation/native@5.2.1
 - @react-navigation/stack@5.2.16
2020-05-01 00:28:55 +02:00
Satyajit Sahoo
1c075ffb16 fix: render fallback only if linking is enabled. closes #8161 2020-05-01 00:27:42 +02:00
Satyajit Sahoo
1ee3038a4d chore: publish
- @react-navigation/bottom-tabs@5.3.0
 - @react-navigation/compat@5.1.11
 - @react-navigation/core@5.4.0
 - @react-navigation/drawer@5.6.0
 - @react-navigation/material-bottom-tabs@5.1.11
 - @react-navigation/material-top-tabs@5.1.11
 - @react-navigation/native@5.2.0
 - @react-navigation/routers@5.4.2
 - @react-navigation/stack@5.2.15
2020-04-30 23:01:46 +02:00
Evan Bacon
961b519fb1 chore: create _redirects for netlify deploy (#8160) 2020-04-30 23:01:21 +02:00
Satyajit Sahoo
0a19e94b23 fix: make sure the address bar hides when scrolling on web
This commit adds a check to detect if the screen content fills the available body, and if yes, then it adjusts the styles so that scrolling triggers a scroll on the body which hides the address bar in browser.

Tested on Safari in iOS and Chrome on Android.

This behaviour can be overriden by the user by specifying `cardStyle: { flex: 1 }`, which will keep both the header and the address bar always visible.
2020-04-30 21:53:17 +02:00
Evan Bacon
1e73fed6de chore: fix scrolling in web examples (#8020) 2020-04-30 13:17:55 +02:00
Satyajit Sahoo
3193a30da6 refactor: add missing methods to container navigation prop 2020-04-29 19:14:24 +02:00
Satyajit Sahoo
499c50cd43 refactor: make history type-checked 2020-04-29 19:13:14 +02:00
ainar
420f6926e1 fix: fix backBehavior with initialRoute (#8110) 2020-04-29 13:37:15 +02:00
Satyajit Sahoo
70be3f6d86 fix: fix closing drawer on web with tap on overlay 2020-04-29 13:05:30 +02:00
WoLewicki
bd35b4fc20 fix: parsing url 2020-04-29 12:52:30 +02:00
Satyajit Sahoo
c511bc0b2b refactor: stub gesture handler on web
Gesture handler doesn't work great on Web and causes issues such as disabling text selection even when not enabled. So we stub it out. It also reduces bundle size on web.
2020-04-29 12:49:46 +02:00
Satyajit Sahoo
b4834ce703 chore: replace AsyncStorage with localStorage on web 2020-04-29 02:16:11 +02:00
Satyajit Sahoo
ae5442ebe8 fix: return onPress instead of onClick for useLinkProps 2020-04-28 23:05:16 +02:00
Satyajit Sahoo
6dd52d35cf refactor: simplify resolving the thenable 2020-04-28 16:14:58 +02:00
Satyajit Sahoo
d6fa279d93 fix: add catch to thenable returned by getInitialState 2020-04-28 15:35:06 +02:00
Satyajit Sahoo
c3fa83efe0 fix: handle empty paths when parsing 2020-04-28 15:12:43 +02:00
91 changed files with 6119 additions and 1712 deletions

View File

@@ -1,89 +1,96 @@
version: 2 version: 2.1
defaults: &defaults executors:
docker: default:
- image: circleci/node:10 docker:
working_directory: ~/project - image: circleci/node:10
working_directory: ~/project
environment:
YARN_CACHE_FOLDER: "~/.cache/yarn"
commands:
attach_project:
steps:
- attach_workspace:
at: ~/project
jobs: jobs:
install-dependencies: install-dependencies:
<<: *defaults executor: default
steps: steps:
- checkout - checkout
- attach_workspace: - attach_project
at: ~/project
- restore_cache: - restore_cache:
keys: keys:
- v1-dependencies-{{ checksum "yarn.lock" }} - v2-dependencies-{{ checksum "yarn.lock" }}
- v1-dependencies- - v2-dependencies-
- run: - run:
name: Install project dependencies name: Install project dependencies
command: yarn install --frozen-lockfile command: yarn install --frozen-lockfile
- save_cache: - save_cache:
key: v1-dependencies-{{ checksum "yarn.lock" }} key: v2-dependencies-{{ checksum "yarn.lock" }}
paths: node_modules paths: ~/.cache/yarn
- persist_to_workspace: - persist_to_workspace:
root: . root: .
paths: . paths: .
lint-and-typecheck: lint-and-typecheck:
<<: *defaults executor: default
steps: steps:
- attach_workspace: - attach_project
at: ~/project - run:
- run: name: Lint files
name: Lint files command: yarn lint
command: yarn lint - run:
- run: name: Typecheck files
name: Typecheck files command: yarn typescript
command: yarn typescript
unit-tests: unit-tests:
<<: *defaults executor: default
steps: steps:
- attach_workspace: - attach_project
at: ~/project - run:
- run: name: Run unit tests
name: Run unit tests command: yarn test --coverage
command: yarn test --coverage - run:
- run: name: Upload test coverage
name: Upload test coverage command: cat ./coverage/lcov.info | ./node_modules/.bin/codecov
command: cat ./coverage/lcov.info | ./node_modules/.bin/codecov - store_artifacts:
- store_artifacts: path: coverage
path: coverage destination: coverage
destination: coverage
integration-tests: integration-tests:
<<: *defaults executor: default
steps: steps:
- attach_workspace: - attach_project
at: ~/project - run:
- run: name: Install Headless Chrome dependencies
name: Install Headless Chrome dependencies command: |
command: | sudo apt-get install -yq \
sudo apt-get install -yq \ gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \
gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \ libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \
libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \ libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 \
libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 \ libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates \
libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates \ fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget - run:
- run: name: Build example for web
name: Build example for web command: yarn example expo build:web --no-pwa
command: yarn example expo build:web --no-pwa - run:
- run: name: Run integration tests
name: Run integration tests command: yarn example test --maxWorkers=2
command: yarn example test --maxWorkers=2
build-packages: build-packages:
<<: *defaults executor: default
steps: steps:
- attach_workspace: - attach_project
at: ~/project - run:
- run: name: Build packages in the monorepo
name: Build packages in the monorepo command: yarn lerna run prepare
command: yarn lerna run prepare - run:
- run: name: Verify paths for types
name: Verify paths for types command: node scripts/check-types-path.js
command: node scripts/check-types-path.js
workflows: workflows:
version: 2
build-and-test: build-and-test:
jobs: jobs:
- install-dependencies - install-dependencies

View File

@@ -6,7 +6,7 @@ beforeEach(async () => {
it('loads the article page', async () => { it('loads the article page', async () => {
expect(await page.evaluate(() => location.pathname + location.search)).toBe( expect(await page.evaluate(() => location.pathname + location.search)).toBe(
'/link-component/Article?author=Gandalf' '/link-component/article/gandalf'
); );
expect( expect(
((await page.accessibility.snapshot()) as any)?.children?.find( ((await page.accessibility.snapshot()) as any)?.children?.find(
@@ -16,24 +16,24 @@ it('loads the article page', async () => {
}); });
it('goes to the album page and goes back', async () => { it('goes to the album page and goes back', async () => {
await page.click('[href="/link-component/Album"]'); await page.click('[href="/link-component/music"]');
expect(await page.evaluate(() => location.pathname + location.search)).toBe( expect(await page.evaluate(() => location.pathname + location.search)).toBe(
'/link-component/Album' '/link-component/music'
); );
expect( expect(
((await page.accessibility.snapshot()) as any)?.children?.find( ((await page.accessibility.snapshot()) as any)?.children?.find(
(it: any) => it.role === 'heading' (it: any) => it.role === 'heading'
)?.name )?.name
).toBe('Album'); ).toBe('Albums');
await page.click('[aria-label="Article by Gandalf, back"]'); await page.click('[aria-label="Article by Gandalf, back"]');
await page.waitForNavigation(); await page.waitForNavigation();
expect(await page.evaluate(() => location.pathname + location.search)).toBe( expect(await page.evaluate(() => location.pathname + location.search)).toBe(
'/link-component/Article?author=Gandalf' '/link-component/article/gandalf'
); );
expect( expect(

View File

@@ -12,33 +12,33 @@
"test": "jest" "test": "jest"
}, },
"dependencies": { "dependencies": {
"@expo/vector-icons": "^10.0.0", "@expo/vector-icons": "^10.2.0",
"@react-native-community/masked-view": "^0.1.7", "@react-native-community/masked-view": "^0.1.10",
"color": "^3.1.2", "color": "^3.1.2",
"expo": "^37.0.0", "expo": "^37.0.8",
"expo-asset": "~8.1.3", "expo-asset": "~8.1.3",
"expo-blur": "~8.1.0", "expo-blur": "~8.1.0",
"react": "~16.9.0", "react": "~16.9.0",
"react-dom": "~16.9.0", "react-dom": "~16.9.0",
"react-native": "~0.61.5", "react-native": "~0.61.5",
"react-native-gesture-handler": "^1.6.0", "react-native-gesture-handler": "^1.6.0",
"react-native-paper": "^3.7.0", "react-native-paper": "^3.10.1",
"react-native-reanimated": "^1.7.0", "react-native-reanimated": "^1.8.0",
"react-native-restart": "^0.0.14", "react-native-restart": "^0.0.15",
"react-native-safe-area-context": "^0.7.3", "react-native-safe-area-context": "^0.7.3",
"react-native-screens": "^2.3.0", "react-native-screens": "^2.7.0",
"react-native-tab-view": "2.14.0", "react-native-tab-view": "2.14.0",
"react-native-unimodules": "~0.8.1", "react-native-unimodules": "~0.9.1",
"react-native-web": "^0.11.7" "react-native-web": "^0.11.7"
}, },
"devDependencies": { "devDependencies": {
"@expo/webpack-config": "^0.11.19", "@expo/webpack-config": "^0.11.19",
"@types/jest-dev-server": "^4.2.0", "@types/jest-dev-server": "^4.2.0",
"@types/react": "^16.9.23", "@types/react": "^16.9.34",
"@types/react-native": "^0.60.22", "@types/react-native": "^0.62.7",
"babel-preset-expo": "^8.1.0", "babel-preset-expo": "^8.1.0",
"expo-cli": "^3.17.18", "expo-cli": "^3.20.1",
"jest": "^25.2.7", "jest": "^26.0.1",
"jest-dev-server": "^4.4.0", "jest-dev-server": "^4.4.0",
"playwright": "^0.14.0", "playwright": "^0.14.0",
"serve": "^11.3.0", "serve": "^11.3.0",

View File

@@ -0,0 +1,3 @@
import { AsyncStorage } from 'react-native';
export default AsyncStorage;

View File

@@ -0,0 +1,14 @@
export default {
getItem(key: string) {
return Promise.resolve(localStorage.getItem(key));
},
setItem(key: string, value: string) {
return Promise.resolve(localStorage.setItem(key, value));
},
removeItem(key: string) {
return Promise.resolve(localStorage.removeItem(key));
},
clear() {
return Promise.resolve(localStorage.clear());
},
};

View File

@@ -1,5 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { View, ScrollView, StyleSheet } from 'react-native'; import { View, ScrollView, StyleSheet, Platform } from 'react-native';
import { Button } from 'react-native-paper'; import { Button } from 'react-native-paper';
import { import {
createCompatNavigatorFactory, createCompatNavigatorFactory,
@@ -23,6 +23,8 @@ type NestedStackParams = {
Article: { author: string }; Article: { author: string };
}; };
const scrollEnabled = Platform.select({ web: true, default: false });
const AlbumsScreen: CompatScreenType<StackNavigationProp< const AlbumsScreen: CompatScreenType<StackNavigationProp<
CompatStackParams CompatStackParams
>> = ({ navigation }) => { >> = ({ navigation }) => {
@@ -44,7 +46,7 @@ const AlbumsScreen: CompatScreenType<StackNavigationProp<
Go back Go back
</Button> </Button>
</View> </View>
<Albums scrollEnabled={false} /> <Albums scrollEnabled={scrollEnabled} />
</ScrollView> </ScrollView>
); );
}; };
@@ -70,7 +72,7 @@ const FeedScreen: CompatScreenType<StackNavigationProp<NestedStackParams>> = ({
Go back Go back
</Button> </Button>
</View> </View>
<NewsFeed scrollEnabled={false} /> <NewsFeed scrollEnabled={scrollEnabled} />
</ScrollView> </ScrollView>
); );
}; };
@@ -100,7 +102,7 @@ const ArticleScreen: CompatScreenType<StackNavigationProp<
</View> </View>
<Article <Article
author={{ name: navigation.getParam('author') }} author={{ name: navigation.getParam('author') }}
scrollEnabled={false} scrollEnabled={scrollEnabled}
/> />
</ScrollView> </ScrollView>
); );

View File

@@ -1,5 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { View, StyleSheet, ScrollView } from 'react-native'; import { View, StyleSheet, ScrollView, Platform } from 'react-native';
import { Button } from 'react-native-paper'; import { Button } from 'react-native-paper';
import { import {
Link, Link,
@@ -17,18 +17,29 @@ import Albums from '../Shared/Albums';
type SimpleStackParams = { type SimpleStackParams = {
Article: { author: string }; Article: { author: string };
Album: undefined; Albums: undefined;
}; };
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>; type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
const scrollEnabled = Platform.select({ web: true, default: false });
const LinkButton = ({ const LinkButton = ({
to, to,
...rest ...rest
}: React.ComponentProps<typeof Button> & { to: string }) => { }: React.ComponentProps<typeof Button> & { to: string }) => {
const props = useLinkProps({ to }); const { onPress, ...props } = useLinkProps({ to });
return <Button {...props} {...rest} />; return (
<Button
{...props}
{...rest}
{...Platform.select({
web: { onClick: onPress } as any,
default: { onPress },
})}
/>
);
}; };
const ArticleScreen = ({ const ArticleScreen = ({
@@ -42,24 +53,24 @@ const ArticleScreen = ({
<ScrollView> <ScrollView>
<View style={styles.buttons}> <View style={styles.buttons}>
<Link <Link
to="/link-component/Album" to="/link-component/music"
style={[styles.button, { padding: 8 }]} style={[styles.button, { padding: 8 }]}
> >
Go to /link-component/Album Go to /link-component/music
</Link> </Link>
<Link <Link
to="/link-component/Album" to="/link-component/music"
action={StackActions.replace('Album')} action={StackActions.replace('Albums')}
style={[styles.button, { padding: 8 }]} style={[styles.button, { padding: 8 }]}
> >
Replace with /link-component/Album Replace with /link-component/music
</Link> </Link>
<LinkButton <LinkButton
to="/link-component/Album" to="/link-component/music"
mode="contained" mode="contained"
style={styles.button} style={styles.button}
> >
Go to /link-component/Album Go to /link-component/music
</LinkButton> </LinkButton>
<Button <Button
mode="outlined" mode="outlined"
@@ -69,7 +80,10 @@ const ArticleScreen = ({
Go back Go back
</Button> </Button>
</View> </View>
<Article author={{ name: route.params.author }} scrollEnabled={false} /> <Article
author={{ name: route.params.author }}
scrollEnabled={scrollEnabled}
/>
</ScrollView> </ScrollView>
); );
}; };
@@ -83,17 +97,17 @@ const AlbumsScreen = ({
<ScrollView> <ScrollView>
<View style={styles.buttons}> <View style={styles.buttons}>
<Link <Link
to="/link-component/Article?author=Babel" to="/link-component/article/babel"
style={[styles.button, { padding: 8 }]} style={[styles.button, { padding: 8 }]}
> >
Go to /link-component/Article Go to /link-component/article
</Link> </Link>
<LinkButton <LinkButton
to="/link-component/Article?author=Babel" to="/link-component/article/babel"
mode="contained" mode="contained"
style={styles.button} style={styles.button}
> >
Go to /link-component/Article Go to /link-component/article
</LinkButton> </LinkButton>
<Button <Button
mode="outlined" mode="outlined"
@@ -103,7 +117,7 @@ const AlbumsScreen = ({
Go back Go back
</Button> </Button>
</View> </View>
<Albums scrollEnabled={false} /> <Albums scrollEnabled={scrollEnabled} />
</ScrollView> </ScrollView>
); );
}; };
@@ -130,9 +144,9 @@ export default function SimpleStackScreen({ navigation, ...rest }: Props) {
initialParams={{ author: 'Gandalf' }} initialParams={{ author: 'Gandalf' }}
/> />
<SimpleStack.Screen <SimpleStack.Screen
name="Album" name="Albums"
component={AlbumsScreen} component={AlbumsScreen}
options={{ title: 'Album' }} options={{ title: 'Albums' }}
/> />
</SimpleStack.Navigator> </SimpleStack.Navigator>
); );

View File

@@ -1,12 +1,18 @@
import * as React from 'react'; import * as React from 'react';
import { Dimensions, ScaledSize } from 'react-native'; import { Dimensions, ScaledSize } from 'react-native';
import { Appbar } from 'react-native-paper'; import { Appbar } from 'react-native-paper';
import { ParamListBase } from '@react-navigation/native'; import {
useTheme,
useNavigation,
ParamListBase,
} from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack'; import { StackNavigationProp } from '@react-navigation/stack';
import { import {
createDrawerNavigator, createDrawerNavigator,
DrawerNavigationProp, DrawerNavigationProp,
DrawerContent, DrawerContent,
DrawerContentComponentProps,
DrawerContentOptions,
} from '@react-navigation/drawer'; } from '@react-navigation/drawer';
import Article from '../Shared/Article'; import Article from '../Shared/Article';
import Albums from '../Shared/Albums'; import Albums from '../Shared/Albums';
@@ -15,7 +21,7 @@ import NewsFeed from '../Shared/NewsFeed';
type DrawerParams = { type DrawerParams = {
Article: undefined; Article: undefined;
NewsFeed: undefined; NewsFeed: undefined;
Album: undefined; Albums: undefined;
}; };
type DrawerNavigation = DrawerNavigationProp<DrawerParams>; type DrawerNavigation = DrawerNavigationProp<DrawerParams>;
@@ -43,10 +49,11 @@ const Header = ({
onGoBack: () => void; onGoBack: () => void;
title: string; title: string;
}) => { }) => {
const { colors } = useTheme();
const isLargeScreen = useIsLargeScreen(); const isLargeScreen = useIsLargeScreen();
return ( return (
<Appbar.Header> <Appbar.Header style={{ backgroundColor: colors.card, elevation: 1 }}>
{isLargeScreen ? null : <Appbar.BackAction onPress={onGoBack} />} {isLargeScreen ? null : <Appbar.BackAction onPress={onGoBack} />}
<Appbar.Content title={title} /> <Appbar.Content title={title} />
</Appbar.Header> </Appbar.Header>
@@ -80,6 +87,23 @@ const AlbumsScreen = ({ navigation }: { navigation: DrawerNavigation }) => {
); );
}; };
const CustomDrawerContent = (
props: DrawerContentComponentProps<DrawerContentOptions>
) => {
const { colors } = useTheme();
const navigation = useNavigation();
return (
<>
<Appbar.Header style={{ backgroundColor: colors.card, elevation: 1 }}>
<Appbar.Action icon="close" onPress={() => navigation.goBack()} />
<Appbar.Content title="Pages" />
</Appbar.Header>
<DrawerContent {...props} />
</>
);
};
const Drawer = createDrawerNavigator<DrawerParams>(); const Drawer = createDrawerNavigator<DrawerParams>();
type Props = Partial<React.ComponentProps<typeof Drawer.Navigator>> & { type Props = Partial<React.ComponentProps<typeof Drawer.Navigator>> & {
@@ -100,15 +124,7 @@ export default function DrawerScreen({ navigation, ...rest }: Props) {
drawerType={isLargeScreen ? 'permanent' : 'back'} drawerType={isLargeScreen ? 'permanent' : 'back'}
drawerStyle={isLargeScreen ? null : { width: '100%' }} drawerStyle={isLargeScreen ? null : { width: '100%' }}
overlayColor="transparent" overlayColor="transparent"
drawerContent={(props) => ( drawerContent={(props) => <CustomDrawerContent {...props} />}
<>
<Appbar.Header>
<Appbar.Action icon="close" onPress={() => navigation.goBack()} />
<Appbar.Content title="Pages" />
</Appbar.Header>
<DrawerContent {...props} />
</>
)}
{...rest} {...rest}
> >
<Drawer.Screen name="Article" component={ArticleScreen} /> <Drawer.Screen name="Article" component={ArticleScreen} />
@@ -118,9 +134,9 @@ export default function DrawerScreen({ navigation, ...rest }: Props) {
options={{ title: 'Feed' }} options={{ title: 'Feed' }}
/> />
<Drawer.Screen <Drawer.Screen
name="Album" name="Albums"
component={AlbumsScreen} component={AlbumsScreen}
options={{ title: 'Album' }} options={{ title: 'Albums' }}
/> />
</Drawer.Navigator> </Drawer.Navigator>
); );

View File

@@ -1,4 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { ParamListBase } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs'; import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
import Albums from '../Shared/Albums'; import Albums from '../Shared/Albums';
import Contacts from '../Shared/Contacts'; import Contacts from '../Shared/Contacts';
@@ -12,7 +14,15 @@ type MaterialTopTabParams = {
const MaterialTopTabs = createMaterialTopTabNavigator<MaterialTopTabParams>(); const MaterialTopTabs = createMaterialTopTabNavigator<MaterialTopTabParams>();
export default function MaterialTopTabsScreen() { type Props = {
navigation: StackNavigationProp<ParamListBase>;
};
export default function MaterialTopTabsScreen({ navigation }: Props) {
navigation.setOptions({
cardStyle: { flex: 1 },
});
return ( return (
<MaterialTopTabs.Navigator> <MaterialTopTabs.Navigator>
<MaterialTopTabs.Screen <MaterialTopTabs.Screen

View File

@@ -1,5 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { View, StyleSheet, ScrollView } from 'react-native'; import { View, StyleSheet, ScrollView, Platform } from 'react-native';
import { Button } from 'react-native-paper'; import { Button } from 'react-native-paper';
import { RouteProp, ParamListBase } from '@react-navigation/native'; import { RouteProp, ParamListBase } from '@react-navigation/native';
import { import {
@@ -12,11 +12,13 @@ import Albums from '../Shared/Albums';
type ModalStackParams = { type ModalStackParams = {
Article: { author: string }; Article: { author: string };
Album: undefined; Albums: undefined;
}; };
type ModalStackNavigation = StackNavigationProp<ModalStackParams>; type ModalStackNavigation = StackNavigationProp<ModalStackParams>;
const scrollEnabled = Platform.select({ web: true, default: false });
const ArticleScreen = ({ const ArticleScreen = ({
navigation, navigation,
route, route,
@@ -29,7 +31,7 @@ const ArticleScreen = ({
<View style={styles.buttons}> <View style={styles.buttons}>
<Button <Button
mode="contained" mode="contained"
onPress={() => navigation.push('Album')} onPress={() => navigation.push('Albums')}
style={styles.button} style={styles.button}
> >
Push album Push album
@@ -42,7 +44,10 @@ const ArticleScreen = ({
Go back Go back
</Button> </Button>
</View> </View>
<Article author={{ name: route.params.author }} scrollEnabled={false} /> <Article
author={{ name: route.params.author }}
scrollEnabled={scrollEnabled}
/>
</ScrollView> </ScrollView>
); );
}; };
@@ -66,7 +71,7 @@ const AlbumsScreen = ({ navigation }: { navigation: ModalStackNavigation }) => {
Go back Go back
</Button> </Button>
</View> </View>
<Albums scrollEnabled={false} /> <Albums scrollEnabled={scrollEnabled} />
</ScrollView> </ScrollView>
); );
}; };
@@ -107,9 +112,9 @@ export default function SimpleStackScreen({ navigation, options }: Props) {
initialParams={{ author: 'Gandalf' }} initialParams={{ author: 'Gandalf' }}
/> />
<ModalPresentationStack.Screen <ModalPresentationStack.Screen
name="Album" name="Albums"
component={AlbumsScreen} component={AlbumsScreen}
options={{ title: 'Album' }} options={{ title: 'Albums' }}
/> />
</ModalPresentationStack.Navigator> </ModalPresentationStack.Navigator>
); );

View File

@@ -1,5 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { View, StyleSheet, ScrollView } from 'react-native'; import { View, Platform, StyleSheet, ScrollView } from 'react-native';
import { Button } from 'react-native-paper'; import { Button } from 'react-native-paper';
import { RouteProp, ParamListBase } from '@react-navigation/native'; import { RouteProp, ParamListBase } from '@react-navigation/native';
import { import {
@@ -13,11 +13,13 @@ import NewsFeed from '../Shared/NewsFeed';
type SimpleStackParams = { type SimpleStackParams = {
Article: { author: string }; Article: { author: string };
NewsFeed: undefined; NewsFeed: undefined;
Album: undefined; Albums: undefined;
}; };
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>; type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
const scrollEnabled = Platform.select({ web: true, default: false });
const ArticleScreen = ({ const ArticleScreen = ({
navigation, navigation,
route, route,
@@ -43,7 +45,10 @@ const ArticleScreen = ({
Pop screen Pop screen
</Button> </Button>
</View> </View>
<Article author={{ name: route.params.author }} scrollEnabled={false} /> <Article
author={{ name: route.params.author }}
scrollEnabled={scrollEnabled}
/>
</ScrollView> </ScrollView>
); );
}; };
@@ -58,7 +63,7 @@ const NewsFeedScreen = ({
<View style={styles.buttons}> <View style={styles.buttons}>
<Button <Button
mode="contained" mode="contained"
onPress={() => navigation.navigate('Album')} onPress={() => navigation.navigate('Albums')}
style={styles.button} style={styles.button}
> >
Navigate to album Navigate to album
@@ -71,7 +76,7 @@ const NewsFeedScreen = ({
Go back Go back
</Button> </Button>
</View> </View>
<NewsFeed scrollEnabled={false} /> <NewsFeed scrollEnabled={scrollEnabled} />
</ScrollView> </ScrollView>
); );
}; };
@@ -99,7 +104,7 @@ const AlbumsScreen = ({
Pop by 2 Pop by 2
</Button> </Button>
</View> </View>
<Albums scrollEnabled={false} /> <Albums scrollEnabled={scrollEnabled} />
</ScrollView> </ScrollView>
); );
}; };
@@ -131,9 +136,9 @@ export default function SimpleStackScreen({ navigation, ...rest }: Props) {
options={{ title: 'Feed' }} options={{ title: 'Feed' }}
/> />
<SimpleStack.Screen <SimpleStack.Screen
name="Album" name="Albums"
component={AlbumsScreen} component={AlbumsScreen}
options={{ title: 'Album' }} options={{ title: 'Albums' }}
/> />
</SimpleStack.Navigator> </SimpleStack.Navigator>
); );

View File

@@ -15,11 +15,13 @@ import Albums from '../Shared/Albums';
type SimpleStackParams = { type SimpleStackParams = {
Article: { author: string }; Article: { author: string };
Album: undefined; Albums: undefined;
}; };
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>; type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
const scrollEnabled = Platform.select({ web: true, default: false });
const ArticleScreen = ({ const ArticleScreen = ({
navigation, navigation,
route, route,
@@ -32,7 +34,7 @@ const ArticleScreen = ({
<View style={styles.buttons}> <View style={styles.buttons}>
<Button <Button
mode="contained" mode="contained"
onPress={() => navigation.push('Album')} onPress={() => navigation.push('Albums')}
style={styles.button} style={styles.button}
> >
Push album Push album
@@ -45,7 +47,10 @@ const ArticleScreen = ({
Go back Go back
</Button> </Button>
</View> </View>
<Article author={{ name: route.params.author }} scrollEnabled={false} /> <Article
author={{ name: route.params.author }}
scrollEnabled={scrollEnabled}
/>
</ScrollView> </ScrollView>
); );
}; };
@@ -75,7 +80,7 @@ const AlbumsScreen = ({
Go back Go back
</Button> </Button>
</View> </View>
<Albums scrollEnabled={false} /> <Albums scrollEnabled={scrollEnabled} />
</ScrollView> </ScrollView>
); );
}; };
@@ -126,10 +131,10 @@ export default function SimpleStackScreen({ navigation, ...rest }: Props) {
initialParams={{ author: 'Gandalf' }} initialParams={{ author: 'Gandalf' }}
/> />
<SimpleStack.Screen <SimpleStack.Screen
name="Album" name="Albums"
component={AlbumsScreen} component={AlbumsScreen}
options={{ options={{
title: 'Album', title: 'Albums',
headerBackTitle: 'Back', headerBackTitle: 'Back',
headerTransparent: true, headerTransparent: true,
headerBackground: () => ( headerBackground: () => (

View File

@@ -1,5 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { View, StyleSheet, ScrollView } from 'react-native'; import { View, StyleSheet, ScrollView, Platform } from 'react-native';
import { Button, Paragraph } from 'react-native-paper'; import { Button, Paragraph } from 'react-native-paper';
import { RouteProp, ParamListBase, useTheme } from '@react-navigation/native'; import { RouteProp, ParamListBase, useTheme } from '@react-navigation/native';
import { import {
@@ -15,6 +15,8 @@ type SimpleStackParams = {
type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>; type SimpleStackNavigation = StackNavigationProp<SimpleStackParams>;
const scrollEnabled = Platform.select({ web: true, default: false });
const ArticleScreen = ({ const ArticleScreen = ({
navigation, navigation,
route, route,
@@ -40,7 +42,10 @@ const ArticleScreen = ({
Go back Go back
</Button> </Button>
</View> </View>
<Article author={{ name: route.params.author }} scrollEnabled={false} /> <Article
author={{ name: route.params.author }}
scrollEnabled={scrollEnabled}
/>
</ScrollView> </ScrollView>
); );
}; };

View File

@@ -1,13 +1,13 @@
import * as React from 'react'; import * as React from 'react';
import { import {
ScrollView, ScrollView,
AsyncStorage,
YellowBox, YellowBox,
Platform, Platform,
StatusBar, StatusBar,
I18nManager, I18nManager,
Dimensions, Dimensions,
ScaledSize, ScaledSize,
Linking,
} from 'react-native'; } from 'react-native';
// eslint-disable-next-line import/no-unresolved // eslint-disable-next-line import/no-unresolved
import { enableScreens } from 'react-native-screens'; import { enableScreens } from 'react-native-screens';
@@ -26,7 +26,6 @@ import {
} from 'react-native-paper'; } from 'react-native-paper';
import { import {
InitialState, InitialState,
NavigationContainerRef,
NavigationContainer, NavigationContainer,
DefaultTheme, DefaultTheme,
DarkTheme, DarkTheme,
@@ -42,6 +41,7 @@ import {
HeaderStyleInterpolators, HeaderStyleInterpolators,
} from '@react-navigation/stack'; } from '@react-navigation/stack';
import AsyncStorage from './AsyncStorage';
import LinkingPrefixes from './LinkingPrefixes'; import LinkingPrefixes from './LinkingPrefixes';
import SettingsItem from './Shared/SettingsItem'; import SettingsItem from './Shared/SettingsItem';
import SimpleStack from './Screens/SimpleStack'; import SimpleStack from './Screens/SimpleStack';
@@ -129,8 +129,6 @@ const THEME_PERSISTENCE_KEY = 'THEME_TYPE';
Asset.loadAsync(StackAssets); Asset.loadAsync(StackAssets);
export default function App() { export default function App() {
const containerRef = React.useRef<NavigationContainerRef>(null);
const [theme, setTheme] = React.useState(DefaultTheme); const [theme, setTheme] = React.useState(DefaultTheme);
const [isReady, setIsReady] = React.useState(false); const [isReady, setIsReady] = React.useState(false);
@@ -141,18 +139,18 @@ export default function App() {
React.useEffect(() => { React.useEffect(() => {
const restoreState = async () => { const restoreState = async () => {
try { try {
let state; const initialUrl = await Linking.getInitialURL();
if (Platform.OS !== 'web' && state === undefined) { if (Platform.OS !== 'web' || initialUrl === null) {
const savedState = await AsyncStorage.getItem( const savedState = await AsyncStorage.getItem(
NAVIGATION_PERSISTENCE_KEY NAVIGATION_PERSISTENCE_KEY
); );
state = savedState ? JSON.parse(savedState) : undefined; const state = savedState ? JSON.parse(savedState) : undefined;
}
if (state !== undefined) { if (state !== undefined) {
setInitialState(state); setInitialState(state);
}
} }
} finally { } finally {
try { try {
@@ -208,7 +206,6 @@ export default function App() {
<StatusBar barStyle={theme.dark ? 'light-content' : 'dark-content'} /> <StatusBar barStyle={theme.dark ? 'light-content' : 'dark-content'} />
)} )}
<NavigationContainer <NavigationContainer
ref={containerRef}
initialState={initialState} initialState={initialState}
onStateChange={(state) => onStateChange={(state) =>
AsyncStorage.setItem( AsyncStorage.setItem(
@@ -242,6 +239,22 @@ export default function App() {
{ Home: '' } { Home: '' }
), ),
}, },
Article: {
path: 'article/:author?',
parse: {
author: (author) =>
author.charAt(0).toUpperCase() +
author.slice(1).replace(/-/g, ' '),
},
stringify: {
author: (author: string) =>
author.toLowerCase().replace(/\s/g, '-'),
},
},
Albums: 'music',
Chat: 'chat',
Contacts: 'people',
NewsFeed: 'feed',
}, },
}} }}
fallback={<Text>Loading</Text>} fallback={<Text>Loading</Text>}

1
example/web/_redirects Normal file
View File

@@ -0,0 +1 @@
/* /index.html 200

View File

@@ -24,19 +24,21 @@ module.exports = async function (env, argv) {
); );
Object.assign(config.resolve.alias, { Object.assign(config.resolve.alias, {
react: path.resolve(node_modules, 'react'), 'react': path.resolve(node_modules, 'react'),
'react-native': path.resolve(node_modules, 'react-native-web'), 'react-native': path.resolve(node_modules, 'react-native-web'),
'react-native-web': path.resolve(node_modules, 'react-native-web'), 'react-native-web': path.resolve(node_modules, 'react-native-web'),
'@expo/vector-icons': path.resolve(node_modules, '@expo/vector-icons'), '@expo/vector-icons': path.resolve(node_modules, '@expo/vector-icons'),
}); });
fs.readdirSync(packages).forEach((name) => { fs.readdirSync(packages)
config.resolve.alias[`@react-navigation/${name}`] = path.resolve( .filter((name) => !name.startsWith('.'))
packages, .forEach((name) => {
name, config.resolve.alias[`@react-navigation/${name}`] = path.resolve(
'src' packages,
); name,
}); require(`../packages/${name}/package.json`).source
);
});
return config; return config;
}; };

View File

@@ -13,7 +13,7 @@
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/satya164/react-navigation.git" "url": "git+https://github.com/react-navigation/react-navigation.git"
}, },
"author": "Satyajit Sahoo <satyajit.happy@gmail.com> (https://github.com/satya164/), Michał Osadnik <micosa97@gmail.com> (https://github.com/osdnk/)", "author": "Satyajit Sahoo <satyajit.happy@gmail.com> (https://github.com/satya164/), Michał Osadnik <micosa97@gmail.com> (https://github.com/osdnk/)",
"scripts": { "scripts": {
@@ -27,23 +27,23 @@
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-class-properties": "^7.8.3", "@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-optional-chaining": "^7.9.0", "@babel/plugin-proposal-optional-chaining": "^7.9.0",
"@babel/preset-env": "^7.9.0", "@babel/preset-env": "^7.9.6",
"@babel/preset-flow": "^7.9.0", "@babel/preset-flow": "^7.9.0",
"@babel/preset-react": "^7.9.4", "@babel/preset-react": "^7.9.4",
"@babel/preset-typescript": "^7.9.0", "@babel/preset-typescript": "^7.9.0",
"@babel/runtime": "^7.9.2", "@babel/runtime": "^7.9.6",
"@commitlint/config-conventional": "^8.3.4", "@commitlint/config-conventional": "^8.3.4",
"@types/jest": "^25.2.1", "@types/jest": "^25.2.1",
"babel-jest": "^25.2.6", "babel-jest": "^26.0.1",
"codecov": "^3.6.5", "codecov": "^3.6.5",
"commitlint": "^8.3.5", "commitlint": "^8.3.5",
"core-js": "^3.6.4", "core-js": "^3.6.5",
"eslint": "^6.8.0", "eslint": "^6.8.0",
"eslint-config-satya164": "^3.1.6", "eslint-config-satya164": "^3.1.7",
"husky": "^4.2.3", "husky": "^4.2.5",
"jest": "^25.2.7", "jest": "^26.0.1",
"lerna": "^3.20.2", "lerna": "^3.20.2",
"prettier": "^2.0.4", "prettier": "^2.0.5",
"typescript": "^3.8.3" "typescript": "^3.8.3"
}, },
"resolutions": { "resolutions": {

View File

@@ -3,6 +3,112 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.4.6](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.4.5...@react-navigation/bottom-tabs@5.4.6) (2020-05-20)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.4.5](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.4.4...@react-navigation/bottom-tabs@5.4.5) (2020-05-16)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.4.4](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.4.3...@react-navigation/bottom-tabs@5.4.4) (2020-05-14)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.4.3](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.4.2...@react-navigation/bottom-tabs@5.4.3) (2020-05-14)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.4.2](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.4.1...@react-navigation/bottom-tabs@5.4.2) (2020-05-10)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.4.1](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.4.0...@react-navigation/bottom-tabs@5.4.1) (2020-05-08)
### Bug Fixes
* fix building typescript definitions. closes [#8216](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/issues/8216) ([47a1229](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/47a12298378747edd2d22e54dc1c8677f98c49b4))
# [5.4.0](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.3.4...@react-navigation/bottom-tabs@5.4.0) (2020-05-08)
### Features
* add generic type aliases for screen props ([bea14aa](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/bea14aa26fd5cbfebc7973733c5cf1f44fd323aa)), closes [#7971](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/issues/7971)
## [5.3.4](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.3.3...@react-navigation/bottom-tabs@5.3.4) (2020-05-05)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.3.3](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.3.2...@react-navigation/bottom-tabs@5.3.3) (2020-05-01)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.3.2](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.3.1...@react-navigation/bottom-tabs@5.3.2) (2020-05-01)
**Note:** Version bump only for package @react-navigation/bottom-tabs
## [5.3.1](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.3.0...@react-navigation/bottom-tabs@5.3.1) (2020-04-30)
**Note:** Version bump only for package @react-navigation/bottom-tabs
# [5.3.0](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.8...@react-navigation/bottom-tabs@5.3.0) (2020-04-30)
### Features
* add `useLinkBuilder` hook to build links ([2792f43](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/2792f438fe45428fe193e3708fee7ad61966cbf4))
* add action prop to Link ([942d2be](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/commit/942d2be2c72720469475ce12ec8df23825994dbf))
## [5.2.8](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.7...@react-navigation/bottom-tabs@5.2.8) (2020-04-27) ## [5.2.8](https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs/compare/@react-navigation/bottom-tabs@5.2.7...@react-navigation/bottom-tabs@5.2.8) (2020-04-27)
**Note:** Version bump only for package @react-navigation/bottom-tabs **Note:** Version bump only for package @react-navigation/bottom-tabs

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/bottom-tabs", "name": "@react-navigation/bottom-tabs",
"description": "Bottom tab navigator following iOS design guidelines", "description": "Bottom tab navigator following iOS design guidelines",
"version": "5.2.8", "version": "5.4.6",
"keywords": [ "keywords": [
"react-native-component", "react-native-component",
"react-component", "react-component",
@@ -15,6 +15,7 @@
"repository": "https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs", "repository": "https://github.com/react-navigation/react-navigation/tree/master/packages/bottom-tabs",
"main": "lib/commonjs/index.js", "main": "lib/commonjs/index.js",
"react-native": "src/index.tsx", "react-native": "src/index.tsx",
"source": "src/index.tsx",
"module": "lib/module/index.js", "module": "lib/module/index.js",
"types": "lib/typescript/src/index.d.ts", "types": "lib/typescript/src/index.d.ts",
"files": [ "files": [
@@ -34,16 +35,16 @@
"react-native-iphone-x-helper": "^1.2.1" "react-native-iphone-x-helper": "^1.2.1"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.10.0", "@react-native-community/bob": "^0.14.3",
"@react-navigation/native": "^5.1.7", "@react-navigation/native": "^5.4.1",
"@types/color": "^3.0.1", "@types/color": "^3.0.1",
"@types/react": "^16.9.23", "@types/react": "^16.9.34",
"@types/react-native": "^0.61.22", "@types/react-native": "^0.62.7",
"del-cli": "^3.0.0", "del-cli": "^3.0.0",
"react": "~16.9.0", "react": "~16.9.0",
"react-native": "~0.61.5", "react-native": "~0.61.5",
"react-native-safe-area-context": "^0.7.3", "react-native-safe-area-context": "^0.7.3",
"react-native-screens": "^2.3.0", "react-native-screens": "^2.7.0",
"typescript": "^3.8.3" "typescript": "^3.8.3"
}, },
"peerDependencies": { "peerDependencies": {

View File

@@ -15,6 +15,7 @@ export { default as BottomTabBar } from './views/BottomTabBar';
export type { export type {
BottomTabNavigationOptions, BottomTabNavigationOptions,
BottomTabNavigationProp, BottomTabNavigationProp,
BottomTabScreenProps,
BottomTabBarProps, BottomTabBarProps,
BottomTabBarOptions, BottomTabBarOptions,
} from './types'; } from './types';

View File

@@ -13,6 +13,7 @@ import {
Descriptor, Descriptor,
TabNavigationState, TabNavigationState,
TabActionHelpers, TabActionHelpers,
RouteProp,
} from '@react-navigation/native'; } from '@react-navigation/native';
export type BottomTabNavigationEventMap = { export type BottomTabNavigationEventMap = {
@@ -45,6 +46,14 @@ export type BottomTabNavigationProp<
> & > &
TabActionHelpers<ParamList>; TabActionHelpers<ParamList>;
export type BottomTabScreenProps<
ParamList extends ParamListBase,
RouteName extends keyof ParamList = string
> = {
navigation: BottomTabNavigationProp<ParamList, RouteName>;
route: RouteProp<ParamList, RouteName>;
};
export type BottomTabNavigationOptions = { export type BottomTabNavigationOptions = {
/** /**
* Title text for the screen. * Title text for the screen.

View File

@@ -3,6 +3,105 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.1.22](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.21...@react-navigation/compat@5.1.22) (2020-05-20)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.21](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.20...@react-navigation/compat@5.1.21) (2020-05-16)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.20](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.19...@react-navigation/compat@5.1.20) (2020-05-14)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.19](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.18...@react-navigation/compat@5.1.19) (2020-05-14)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.18](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.17...@react-navigation/compat@5.1.18) (2020-05-10)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.17](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.16...@react-navigation/compat@5.1.17) (2020-05-08)
### Bug Fixes
* fix building typescript definitions. closes [#8216](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/issues/8216) ([47a1229](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/commit/47a12298378747edd2d22e54dc1c8677f98c49b4))
## [5.1.16](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.15...@react-navigation/compat@5.1.16) (2020-05-08)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.15](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.14...@react-navigation/compat@5.1.15) (2020-05-05)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.14](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.13...@react-navigation/compat@5.1.14) (2020-05-01)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.13](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.12...@react-navigation/compat@5.1.13) (2020-05-01)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.12](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.11...@react-navigation/compat@5.1.12) (2020-04-30)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.11](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.10...@react-navigation/compat@5.1.11) (2020-04-30)
**Note:** Version bump only for package @react-navigation/compat
## [5.1.10](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.9...@react-navigation/compat@5.1.10) (2020-04-27) ## [5.1.10](https://github.com/react-navigation/react-navigation/tree/master/packages/compat/compare/@react-navigation/compat@5.1.9...@react-navigation/compat@5.1.10) (2020-04-27)

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/compat", "name": "@react-navigation/compat",
"description": "Compatibility layer to write navigator definitions in static configuration format", "description": "Compatibility layer to write navigator definitions in static configuration format",
"version": "5.1.10", "version": "5.1.22",
"license": "MIT", "license": "MIT",
"repository": "https://github.com/react-navigation/react-navigation/tree/master/packages/compat", "repository": "https://github.com/react-navigation/react-navigation/tree/master/packages/compat",
"bugs": { "bugs": {
@@ -10,6 +10,7 @@
"homepage": "https://reactnavigation.org/docs/compatibility.html", "homepage": "https://reactnavigation.org/docs/compatibility.html",
"main": "lib/commonjs/index.js", "main": "lib/commonjs/index.js",
"react-native": "src/index.tsx", "react-native": "src/index.tsx",
"source": "src/index.tsx",
"module": "lib/module/index.js", "module": "lib/module/index.js",
"types": "lib/typescript/src/index.d.ts", "types": "lib/typescript/src/index.d.ts",
"files": [ "files": [
@@ -25,9 +26,9 @@
"clean": "del lib" "clean": "del lib"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.10.0", "@react-native-community/bob": "^0.14.3",
"@react-navigation/native": "^5.1.7", "@react-navigation/native": "^5.4.1",
"@types/react": "^16.9.23", "@types/react": "^16.9.34",
"react": "~16.9.0", "react": "~16.9.0",
"typescript": "^3.8.3" "typescript": "^3.8.3"
}, },

View File

@@ -3,6 +3,113 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [5.8.0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.7.0...@react-navigation/core@5.8.0) (2020-05-20)
### Features
* add getCurrentOptions ([#8277](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/8277)) ([d024ec6](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/d024ec6d74dffe481ce6fde732c729e20c1668f4))
* add getCurrentRoute ([#8254](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/8254)) ([7b25c8e](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/7b25c8eb2e6f96128fd86b92615346ce55bedeca))
# [5.7.0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.6.1...@react-navigation/core@5.7.0) (2020-05-16)
### Bug Fixes
* don't use Object.fromEntries ([51f4d11](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/51f4d11fdf4bd2bb06f8cd4094f051816590e62c))
### Features
* add a PathConfig type ([60cb3c9](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/60cb3c9ba76d7ef166c9fe8b55f23728975b5b6e))
## [5.6.1](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.6.0...@react-navigation/core@5.6.1) (2020-05-14)
### Bug Fixes
* don't use flat since it's not supported in node ([21b397f](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/21b397f0d6b96ec4875d3172f47533130bb08009))
# [5.6.0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.5.2...@react-navigation/core@5.6.0) (2020-05-14)
### Bug Fixes
* ignore extra slashes in the pattern ([3c47716](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/3c47716826d0dfa69dfa6112141c116723372ea1))
* ignore state updates when we're not mounted ([0149e85](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/0149e85a95b90c6a9d487fa753ddbf5d01c03e3d)), closes [#8226](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/8226)
### Features
* merge path patterns for nested screens ([#8253](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/8253)) ([acc9646](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/acc9646426fee53558d686dfbe5fd0e35361d8c0))
## [5.5.2](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.5.1...@react-navigation/core@5.5.2) (2020-05-08)
### Bug Fixes
* fix building typescript definitions. closes [#8216](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/8216) ([47a1229](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/47a12298378747edd2d22e54dc1c8677f98c49b4))
## [5.5.1](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.5.0...@react-navigation/core@5.5.1) (2020-05-08)
### Bug Fixes
* avoid cleaning up state when a new navigator is mounted. fixes [#8195](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/8195) ([f6d0676](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/f6d06768d3c36d1f5beaffcb660f3c259209f2e7))
# [5.5.0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.4.0...@react-navigation/core@5.5.0) (2020-05-05)
### Features
* add support for optional params to linking ([#8196](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/8196)) ([fcd1cc6](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/fcd1cc64c151e4941f3f544a54b5048d853821f6))
* support params anywhere in path segement ([#8184](https://github.com/react-navigation/react-navigation/tree/master/packages/core/issues/8184)) ([3999fc2](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/3999fc28365c3a06a17d963c7be7fb7e897f99e0))
# [5.4.0](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.3.5...@react-navigation/core@5.4.0) (2020-04-30)
### Bug Fixes
* handle empty paths when parsing ([c3fa83e](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/c3fa83efe0d73db76365f8be3d6a8ca1d1289b71))
* parsing url ([bd35b4f](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/bd35b4fc202c3868fb75c3675b62de67557089e1))
### Features
* add `useLinkBuilder` hook to build links ([2792f43](https://github.com/react-navigation/react-navigation/tree/master/packages/core/commit/2792f438fe45428fe193e3708fee7ad61966cbf4))
## [5.3.5](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.3.4...@react-navigation/core@5.3.5) (2020-04-27) ## [5.3.5](https://github.com/react-navigation/react-navigation/tree/master/packages/core/compare/@react-navigation/core@5.3.4...@react-navigation/core@5.3.5) (2020-04-27)

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/core", "name": "@react-navigation/core",
"description": "Core utilities for building navigators", "description": "Core utilities for building navigators",
"version": "5.3.5", "version": "5.8.0",
"keywords": [ "keywords": [
"react", "react",
"react-native", "react-native",
@@ -15,6 +15,7 @@
"homepage": "https://reactnavigation.org", "homepage": "https://reactnavigation.org",
"main": "lib/commonjs/index.js", "main": "lib/commonjs/index.js",
"react-native": "src/index.tsx", "react-native": "src/index.tsx",
"source": "src/index.tsx",
"module": "lib/module/index.js", "module": "lib/module/index.js",
"types": "lib/typescript/src/index.d.ts", "types": "lib/typescript/src/index.d.ts",
"files": [ "files": [
@@ -29,21 +30,21 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"@react-navigation/routers": "^5.4.1", "@react-navigation/routers": "^5.4.5",
"escape-string-regexp": "^2.0.0", "escape-string-regexp": "^4.0.0",
"nanoid": "^3.0.2", "nanoid": "^3.1.5",
"query-string": "^6.12.0", "query-string": "^6.12.1",
"react-is": "^16.13.0", "react-is": "^16.13.0",
"use-subscription": "^1.4.0" "use-subscription": "^1.4.0"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.10.0", "@react-native-community/bob": "^0.14.3",
"@types/react": "^16.9.23", "@types/react": "^16.9.34",
"@types/react-is": "^16.7.1", "@types/react-is": "^16.7.1",
"@types/use-subscription": "^1.0.0", "@types/use-subscription": "^1.0.0",
"del-cli": "^3.0.0", "del-cli": "^3.0.0",
"react": "~16.9.0", "react": "~16.9.0",
"react-native-testing-library": "^1.12.0", "react-native-testing-library": "^1.13.2",
"react-test-renderer": "~16.13.1", "react-test-renderer": "~16.13.1",
"typescript": "^3.8.3" "typescript": "^3.8.3"
}, },

View File

@@ -13,6 +13,7 @@ import { ScheduleUpdateContext } from './useScheduleUpdate';
import useFocusedListeners from './useFocusedListeners'; import useFocusedListeners from './useFocusedListeners';
import useDevTools from './useDevTools'; import useDevTools from './useDevTools';
import useStateGetters from './useStateGetters'; import useStateGetters from './useStateGetters';
import useOptionsGetters from './useOptionsGetters';
import useEventEmitter from './useEventEmitter'; import useEventEmitter from './useEventEmitter';
import useSyncState from './useSyncState'; import useSyncState from './useSyncState';
import isSerializable from './isSerializable'; import isSerializable from './isSerializable';
@@ -39,6 +40,10 @@ export const NavigationStateContext = React.createContext<{
setState: ( setState: (
state: NavigationState | PartialState<NavigationState> | undefined state: NavigationState | PartialState<NavigationState> | undefined
) => void; ) => void;
addOptionsGetter?: (
key: string,
getter: () => object | undefined | null
) => void;
}>({ }>({
isDefault: true, isDefault: true,
@@ -199,8 +204,21 @@ const BaseNavigationContainer = React.forwardRef(
return getStateForRoute('root'); return getStateForRoute('root');
}, [getStateForRoute]); }, [getStateForRoute]);
const getCurrentRoute = React.useCallback(() => {
let state = getRootState();
if (state === undefined) {
return undefined;
}
while (state.routes[state.index].state !== undefined) {
state = state.routes[state.index].state as NavigationState;
}
return state.routes[state.index];
}, [getRootState]);
const emitter = useEventEmitter(); const emitter = useEventEmitter();
const { addOptionsGetter, getCurrentOptions } = useOptionsGetters({});
React.useImperativeHandle(ref, () => ({ React.useImperativeHandle(ref, () => ({
...(Object.keys(CommonActions) as (keyof typeof CommonActions)[]).reduce< ...(Object.keys(CommonActions) as (keyof typeof CommonActions)[]).reduce<
any any
@@ -219,6 +237,10 @@ const BaseNavigationContainer = React.forwardRef(
dispatch, dispatch,
canGoBack, canGoBack,
getRootState, getRootState,
dangerouslyGetState: () => state,
dangerouslyGetParent: () => undefined,
getCurrentRoute,
getCurrentOptions,
})); }));
const builderContext = React.useMemo( const builderContext = React.useMemo(
@@ -242,8 +264,9 @@ const BaseNavigationContainer = React.forwardRef(
setState, setState,
getKey, getKey,
setKey, setKey,
addOptionsGetter,
}), }),
[getKey, getState, setKey, setState, state] [getKey, getState, setKey, setState, state, addOptionsGetter]
); );
React.useEffect(() => { React.useEffect(() => {

View File

@@ -11,6 +11,7 @@ import NavigationRouteContext from './NavigationRouteContext';
import StaticContainer from './StaticContainer'; import StaticContainer from './StaticContainer';
import EnsureSingleNavigator from './EnsureSingleNavigator'; import EnsureSingleNavigator from './EnsureSingleNavigator';
import { NavigationProp, RouteConfig, EventMapBase } from './types'; import { NavigationProp, RouteConfig, EventMapBase } from './types';
import useOptionsGetters from './useOptionsGetters';
type Props< type Props<
State extends NavigationState, State extends NavigationState,
@@ -24,6 +25,7 @@ type Props<
}; };
getState: () => State; getState: () => State;
setState: (state: State) => void; setState: (state: State) => void;
options: object;
}; };
/** /**
@@ -40,11 +42,24 @@ export default function SceneView<
navigation, navigation,
getState, getState,
setState, setState,
options,
}: Props<State, ScreenOptions, EventMap>) { }: Props<State, ScreenOptions, EventMap>) {
const navigatorKeyRef = React.useRef<string | undefined>(); const navigatorKeyRef = React.useRef<string | undefined>();
const getKey = React.useCallback(() => navigatorKeyRef.current, []); const getKey = React.useCallback(() => navigatorKeyRef.current, []);
const optionsRef = React.useRef<object | undefined>(options);
React.useEffect(() => {
optionsRef.current = options;
}, [options]);
const getOptions = React.useCallback(() => optionsRef.current, []);
const { addOptionsGetter } = useOptionsGetters({
key: route.key,
getOptions,
});
const setKey = React.useCallback((key: string) => { const setKey = React.useCallback((key: string) => {
navigatorKeyRef.current = key; navigatorKeyRef.current = key;
}, []); }, []);
@@ -77,8 +92,16 @@ export default function SceneView<
setState: setCurrentState, setState: setCurrentState,
getKey, getKey,
setKey, setKey,
addOptionsGetter,
}), }),
[getCurrentState, getKey, route.state, setCurrentState, setKey] [
getCurrentState,
getKey,
route.state,
setCurrentState,
setKey,
addOptionsGetter,
]
); );
return ( return (

View File

@@ -117,7 +117,8 @@ it("doesn't add query param for empty params", () => {
}); });
it('handles state with config with nested screens', () => { it('handles state with config with nested screens', () => {
const path = '/foe/bar/sweet/apple/baz/jane?answer=42&count=10&valid=true'; const path =
'/foo/foe/bar/sweet/apple/baz/jane?answer=42&count=10&valid=true';
const config = { const config = {
Foo: { Foo: {
path: 'foo', path: 'foo',
@@ -182,8 +183,77 @@ it('handles state with config with nested screens', () => {
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path); expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
}); });
it('handles state with config with nested screens and exact', () => {
const path = '/foe/bar/sweet/apple/baz/jane?answer=42&count=10&valid=true';
const config = {
Foo: {
path: 'foo',
screens: {
Foe: {
path: 'foe',
exact: true,
},
},
},
Bar: 'bar/:type/:fruit',
Baz: {
path: 'baz/:author',
parse: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toUpperCase()),
count: Number,
valid: Boolean,
},
stringify: {
author: (author: string) => author.toLowerCase(),
id: (id: number) => `x${id}`,
unknown: (_: unknown) => 'x',
},
},
};
const state = {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Foe',
state: {
routes: [
{
name: 'Bar',
params: { fruit: 'apple', type: 'sweet' },
state: {
routes: [
{
name: 'Baz',
params: {
author: 'Jane',
count: '10',
answer: '42',
valid: true,
},
},
],
},
},
],
},
},
],
},
},
],
};
expect(getPathFromState(state, config)).toBe(path);
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
});
it('handles state with config with nested screens and unused configs', () => { it('handles state with config with nested screens and unused configs', () => {
const path = '/foe/baz/jane?answer=42&count=10&valid=true'; const path = '/foo/foe/baz/jane?answer=42&count=10&valid=true';
const config = { const config = {
Foo: { Foo: {
path: 'foo', path: 'foo',
@@ -239,6 +309,66 @@ it('handles state with config with nested screens and unused configs', () => {
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path); expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
}); });
it('handles state with config with nested screens and unused configs with exact', () => {
const path = '/foe/baz/jane?answer=42&count=10&valid=true';
const config = {
Foo: {
path: 'foo',
screens: {
Foe: {
path: 'foe',
exact: true,
},
},
},
Baz: {
path: 'baz/:author',
parse: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toUpperCase()),
count: Number,
valid: Boolean,
},
stringify: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toLowerCase()),
unknown: (_: unknown) => 'x',
},
},
};
const state = {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Foe',
state: {
routes: [
{
name: 'Baz',
params: {
author: 'Jane',
count: 10,
answer: '42',
valid: true,
},
},
],
},
},
],
},
},
],
};
expect(getPathFromState(state, config)).toBe(path);
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
});
it('handles nested object with stringify in it', () => { it('handles nested object with stringify in it', () => {
const path = '/bar/sweet/apple/foo/bis/jane?answer=42&count=10&valid=true'; const path = '/bar/sweet/apple/foo/bis/jane?answer=42&count=10&valid=true';
const config = { const config = {
@@ -252,7 +382,6 @@ it('handles nested object with stringify in it', () => {
}, },
Bar: 'bar/:type/:fruit', Bar: 'bar/:type/:fruit',
Baz: { Baz: {
path: 'baz',
screens: { screens: {
Bos: 'bos', Bos: 'bos',
Bis: { Bis: {
@@ -312,8 +441,82 @@ it('handles nested object with stringify in it', () => {
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path); expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
}); });
it('handles nested object with stringify in it with exact', () => {
const path = '/bar/sweet/apple/foo/bis/jane?answer=42&count=10&valid=true';
const config = {
Foo: {
path: 'foo',
screens: {
Foe: {
path: 'foe',
},
},
},
Bar: 'bar/:type/:fruit',
Baz: {
path: 'baz',
screens: {
Bos: 'bos',
Bis: {
path: 'bis/:author',
exact: true,
stringify: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toLowerCase()),
},
parse: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toUpperCase()),
count: Number,
valid: Boolean,
},
},
},
},
};
const state = {
routes: [
{
name: 'Bar',
params: { fruit: 'apple', type: 'sweet' },
state: {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Baz',
state: {
routes: [
{
name: 'Bis',
params: {
author: 'Jane',
count: 10,
answer: '42',
valid: true,
},
},
],
},
},
],
},
},
],
},
},
],
};
expect(getPathFromState(state, config)).toBe(path);
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
});
it('handles nested object for second route depth', () => { it('handles nested object for second route depth', () => {
const path = '/baz'; const path = '/foo/bar/baz';
const config = { const config = {
Foo: { Foo: {
path: 'foo', path: 'foo',
@@ -351,7 +554,95 @@ it('handles nested object for second route depth', () => {
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path); expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
}); });
it('handles nested object for second route depth and and path and stringify in roots', () => { it('handles nested object for second route depth with exact', () => {
const path = '/baz';
const config = {
Foo: {
path: 'foo',
screens: {
Foe: 'foe',
Bar: {
path: 'bar',
screens: {
Baz: {
path: 'baz',
exact: true,
},
},
},
},
},
};
const state = {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Bar',
state: {
routes: [{ name: 'Baz' }],
},
},
],
},
},
],
};
expect(getPathFromState(state, config)).toBe(path);
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
});
it('handles nested object for second route depth and path and stringify in roots', () => {
const path = '/foo/dathomir/bar/42/baz';
const config = {
Foo: {
path: 'foo/:planet',
stringify: {
id: (id: number) => `planet=${id}`,
},
screens: {
Foe: 'foe',
Bar: {
path: 'bar/:id',
parse: {
id: Number,
},
screens: {
Baz: 'baz',
},
},
},
},
};
const state = {
routes: [
{
name: 'Foo',
params: { planet: 'dathomir' },
state: {
routes: [
{
name: 'Bar',
state: {
routes: [{ name: 'Baz', params: { id: 42 } }],
},
},
],
},
},
],
};
expect(getPathFromState(state, config)).toBe(path);
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
});
it('handles nested object for second route depth and path and stringify in roots with exact', () => {
const path = '/baz'; const path = '/baz';
const config = { const config = {
Foo: { Foo: {
@@ -370,7 +661,10 @@ it('handles nested object for second route depth and and path and stringify in r
id: Number, id: Number,
}, },
screens: { screens: {
Baz: 'baz', Baz: {
path: 'baz',
exact: true,
},
}, },
}, },
}, },
@@ -426,8 +720,51 @@ it('ignores empty string paths', () => {
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path); expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
}); });
it('keeps query params if path is empty', () => {
const path = '/?foo=42';
const config = {
Foo: {
screens: {
Foe: 'foe',
Bar: {
screens: {
Qux: {
path: '',
parse: { foo: Number },
},
Baz: 'baz',
},
},
},
},
};
const state = {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Bar',
state: {
routes: [{ name: 'Qux', params: { foo: 42 } }],
},
},
],
},
},
],
};
expect(getPathFromState(state, config)).toBe(path);
expect(getPathFromState(getStateFromPath(path, config), config)).toEqual(
path
);
});
it('cuts nested configs too', () => { it('cuts nested configs too', () => {
const path = '/baz'; const path = '/foo/baz';
const config = { const config = {
Foo: { Foo: {
path: 'foo', path: 'foo',
@@ -435,7 +772,48 @@ it('cuts nested configs too', () => {
Bar: '', Bar: '',
}, },
}, },
Baz: { path: 'baz' }, Baz: {
path: 'baz',
},
};
const state = {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Bar',
state: {
routes: [{ name: 'Baz' }],
},
},
],
},
},
],
};
expect(getPathFromState(state, config)).toBe(path);
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
});
it('cuts nested configs too with exact', () => {
const path = '/baz';
const config = {
Foo: {
path: 'foo',
screens: {
Bar: {
path: '',
exact: true,
},
},
},
Baz: {
path: 'baz',
},
}; };
const state = { const state = {
@@ -461,7 +839,7 @@ it('cuts nested configs too', () => {
}); });
it('handles empty path at the end', () => { it('handles empty path at the end', () => {
const path = '/bar'; const path = '/foo/bar';
const config = { const config = {
Foo: { Foo: {
path: 'foo', path: 'foo',
@@ -495,6 +873,8 @@ it('handles empty path at the end', () => {
}); });
it('returns "/" for empty path', () => { it('returns "/" for empty path', () => {
const path = '/';
const config = { const config = {
Foo: { Foo: {
path: '', path: '',
@@ -519,7 +899,8 @@ it('returns "/" for empty path', () => {
], ],
}; };
expect(getPathFromState(state, config)).toBe('/'); expect(getPathFromState(state, config)).toBe(path);
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
}); });
it('parses no path specified', () => { it('parses no path specified', () => {
@@ -595,7 +976,6 @@ it('strips undefined query params', () => {
}, },
Bar: 'bar/:type/:fruit', Bar: 'bar/:type/:fruit',
Baz: { Baz: {
path: 'baz',
screens: { screens: {
Bos: 'bos', Bos: 'bos',
Bis: { Bis: {
@@ -635,7 +1015,79 @@ it('strips undefined query params', () => {
params: { params: {
author: 'Jane', author: 'Jane',
count: 10, count: 10,
answer: undefined, valid: true,
},
},
],
},
},
],
},
},
],
},
},
],
};
expect(getPathFromState(state, config)).toBe(path);
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
});
it('strips undefined query params with exact', () => {
const path = '/bar/sweet/apple/foo/bis/jane?count=10&valid=true';
const config = {
Foo: {
path: 'foo',
screens: {
Foe: {
path: 'foe',
},
},
},
Bar: 'bar/:type/:fruit',
Baz: {
path: 'baz',
screens: {
Bos: 'bos',
Bis: {
path: 'bis/:author',
exact: true,
stringify: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toLowerCase()),
},
parse: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toUpperCase()),
count: Number,
valid: Boolean,
},
},
},
},
};
const state = {
routes: [
{
name: 'Bar',
params: { fruit: 'apple', type: 'sweet' },
state: {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Baz',
state: {
routes: [
{
name: 'Bis',
params: {
author: 'Jane',
count: 10,
valid: true, valid: true,
}, },
}, },
@@ -668,7 +1120,6 @@ it('handles stripping all query params', () => {
}, },
Bar: 'bar/:type/:fruit', Bar: 'bar/:type/:fruit',
Baz: { Baz: {
path: 'baz',
screens: { screens: {
Bos: 'bos', Bos: 'bos',
Bis: { Bis: {
@@ -707,9 +1158,6 @@ it('handles stripping all query params', () => {
name: 'Bis', name: 'Bis',
params: { params: {
author: 'Jane', author: 'Jane',
count: undefined,
answer: undefined,
valid: undefined,
}, },
}, },
], ],
@@ -727,3 +1175,93 @@ it('handles stripping all query params', () => {
expect(getPathFromState(state, config)).toBe(path); expect(getPathFromState(state, config)).toBe(path);
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path); expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
}); });
it('handles stripping all query params with exact', () => {
const path = '/bar/sweet/apple/foo/bis/jane';
const config = {
Foo: {
path: 'foo',
screens: {
Foe: {
path: 'foe',
},
},
},
Bar: 'bar/:type/:fruit',
Baz: {
path: 'baz',
screens: {
Bos: 'bos',
Bis: {
path: 'bis/:author',
exact: true,
stringify: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toLowerCase()),
},
parse: {
author: (author: string) =>
author.replace(/^\w/, (c) => c.toUpperCase()),
count: Number,
valid: Boolean,
},
},
},
},
};
const state = {
routes: [
{
name: 'Bar',
params: { fruit: 'apple', type: 'sweet' },
state: {
routes: [
{
name: 'Foo',
state: {
routes: [
{
name: 'Baz',
state: {
routes: [
{
name: 'Bis',
params: {
author: 'Jane',
},
},
],
},
},
],
},
},
],
},
},
],
};
expect(getPathFromState(state, config)).toBe(path);
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
});
it('replaces undefined query params', () => {
const path = '/bar/undefined/apple';
const config = {
Bar: 'bar/:type/:fruit',
};
const state = {
routes: [
{
name: 'Bar',
params: { fruit: 'apple' },
},
],
};
expect(getPathFromState(state, config)).toBe(path);
expect(getPathFromState(getStateFromPath(path, config), config)).toBe(path);
});

File diff suppressed because it is too large Load Diff

View File

@@ -379,6 +379,8 @@ it("doesn't update state if action wasn't handled", () => {
}); });
it('cleans up state when the navigator unmounts', () => { it('cleans up state when the navigator unmounts', () => {
jest.useFakeTimers();
const TestNavigator = (props: any) => { const TestNavigator = (props: any) => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props); const { state, descriptors } = useNavigationBuilder(MockRouter, props);
@@ -426,6 +428,8 @@ it('cleans up state when the navigator unmounts', () => {
<BaseNavigationContainer onStateChange={onStateChange} children={null} /> <BaseNavigationContainer onStateChange={onStateChange} children={null} />
); );
act(() => jest.runAllTimers());
expect(onStateChange).toBeCalledTimes(2); expect(onStateChange).toBeCalledTimes(2);
expect(onStateChange).lastCalledWith(undefined); expect(onStateChange).lastCalledWith(undefined);
}); });
@@ -1486,3 +1490,128 @@ it("doesn't throw if children is null", () => {
expect(() => render(element).update(element)).not.toThrowError(); expect(() => render(element).update(element)).not.toThrowError();
}); });
it('returns currently focused route with getCurrentRoute', () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return descriptors[state.routes[state.index].key].render();
};
const TestScreen = () => null;
const navigation = React.createRef<NavigationContainerRef>();
const container = (
<BaseNavigationContainer ref={navigation}>
<TestNavigator>
<Screen name="bar" options={{ a: 'b' }}>
{() => (
<TestNavigator initialRouteName="bar-a">
<Screen
name="bar-a"
component={TestScreen}
options={{ sample: 'data' }}
/>
</TestNavigator>
)}
</Screen>
<Screen name="xux" component={TestScreen} />
</TestNavigator>
</BaseNavigationContainer>
);
render(container).update(container);
expect(navigation.current?.getCurrentRoute()).toEqual({
key: 'bar-a',
name: 'bar-a',
});
});
it("returns currently focused route's options with getCurrentOptions", () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return descriptors[state.routes[state.index].key].render();
};
const TestScreen = () => null;
const navigation = React.createRef<NavigationContainerRef>();
const container = (
<BaseNavigationContainer ref={navigation}>
<TestNavigator>
<Screen name="bar" options={{ a: 'b' }}>
{() => (
<TestNavigator
initialRouteName="bar-a"
screenOptions={() => ({ sample2: 'data' })}
>
<Screen
name="bar-a"
component={TestScreen}
options={{ sample: 'data' }}
/>
</TestNavigator>
)}
</Screen>
<Screen name="xux" component={TestScreen} />
</TestNavigator>
</BaseNavigationContainer>
);
render(container).update(container);
expect(navigation.current?.getCurrentOptions()).toEqual({
sample: 'data',
sample2: 'data',
});
});
it('does not throw if while getting current options with no options defined', () => {
const TestNavigator = (props: any): any => {
const { state, descriptors } = useNavigationBuilder(MockRouter, props);
return descriptors[state.routes[state.index].key].render();
};
const TestScreen = () => null;
const navigation = React.createRef<NavigationContainerRef>();
const container = (
<BaseNavigationContainer ref={navigation}>
<TestNavigator>
<Screen name="bar" options={{ a: 'b' }}>
{() => (
<TestNavigator initialRouteName="bar-a">
<Screen
name="bar-b"
component={TestScreen}
options={{ wrongKey: true }}
/>
<Screen name="bar-a" component={TestScreen} />
</TestNavigator>
)}
</Screen>
</TestNavigator>
</BaseNavigationContainer>
);
render(container).update(container);
expect(navigation.current?.getCurrentOptions()).toEqual({});
});
it('does not throw if while getting current options with empty container', () => {
const navigation = React.createRef<NavigationContainerRef>();
// @ts-ignore
const container = <BaseNavigationContainer ref={navigation} />;
render(container).update(container);
expect(navigation.current?.getCurrentOptions()).toEqual(undefined);
});

View File

@@ -4,19 +4,18 @@ import {
PartialState, PartialState,
Route, Route,
} from '@react-navigation/routers'; } from '@react-navigation/routers';
import { PathConfig } from './types';
type State = NavigationState | Omit<PartialState<NavigationState>, 'stale'>; type State = NavigationState | Omit<PartialState<NavigationState>, 'stale'>;
type StringifyConfig = Record<string, (value: any) => string>; type StringifyConfig = Record<string, (value: any) => string>;
type Options = { type OptionsItem = PathConfig[string];
[routeName: string]:
| string type ConfigItem = {
| { pattern?: string;
path?: string; stringify?: StringifyConfig;
stringify?: StringifyConfig; screens?: Record<string, ConfigItem>;
screens?: Options;
};
}; };
/** /**
@@ -48,137 +47,192 @@ type Options = {
*/ */
export default function getPathFromState( export default function getPathFromState(
state?: State, state?: State,
options: Options = {} options: PathConfig = {}
): string { ): string {
if (state === undefined) { if (state === undefined) {
throw Error('NavigationState not passed'); throw Error('NavigationState not passed');
} }
let path = '/';
// Create a normalized configs array which will be easier to use
const configs = createNormalizedConfigs(options);
let path = '/';
let current: State | undefined = state; let current: State | undefined = state;
const allParams: Record<string, any> = {};
while (current) { while (current) {
let index = typeof current.index === 'number' ? current.index : 0; let index = typeof current.index === 'number' ? current.index : 0;
let route = current.routes[index] as Route<string> & { let route = current.routes[index] as Route<string> & {
state?: State; state?: State;
}; };
let currentOptions = options;
let pattern = route.name;
// we keep all the route names that appeared during going deeper in config in case the pattern is resolved to undefined
let nestedRouteNames = '';
while (route.name in currentOptions) { let pattern: string | undefined;
if (typeof currentOptions[route.name] === 'string') {
pattern = currentOptions[route.name] as string; let currentParams: Record<string, any> = { ...route.params };
break; let currentOptions = configs;
} else if (typeof currentOptions[route.name] === 'object') {
// if there is no `screens` property, we return pattern // Keep all the route names that appeared during going deeper in config in case the pattern is resolved to undefined
if ( let nestedRouteNames = [];
!(currentOptions[route.name] as {
screens: Options; let hasNext = true;
}).screens
) { while (route.name in currentOptions && hasNext) {
pattern = (currentOptions[route.name] as { path: string }).path; pattern = currentOptions[route.name].pattern;
nestedRouteNames = `${nestedRouteNames}/${route.name}`;
break; nestedRouteNames.push(route.name);
if (route.params) {
const stringify = currentOptions[route.name]?.stringify;
currentParams = fromEntries(
Object.entries(route.params).map(([key, value]) => [
key,
stringify?.[key] ? stringify[key](value) : String(value),
])
);
if (pattern) {
Object.assign(allParams, currentParams);
}
}
// If there is no `screens` property or no nested state, we return pattern
if (!currentOptions[route.name].screens || route.state === undefined) {
hasNext = false;
} else {
index =
typeof route.state.index === 'number'
? route.state.index
: route.state.routes.length - 1;
const nextRoute = route.state.routes[index];
const nestedConfig = currentOptions[route.name].screens;
// if there is config for next route name, we go deeper
if (nestedConfig && nextRoute.name in nestedConfig) {
route = nextRoute as Route<string> & { state?: State };
currentOptions = nestedConfig;
} else { } else {
// if it is the end of state, we return pattern // If not, there is no sense in going deeper in config
if (route.state === undefined) { hasNext = false;
pattern = (currentOptions[route.name] as { path: string }).path;
nestedRouteNames = `${nestedRouteNames}/${route.name}`;
break;
} else {
index =
typeof route.state.index === 'number' ? route.state.index : 0;
const nextRoute = route.state.routes[index];
const deeperConfig = (currentOptions[route.name] as {
screens: Options;
}).screens;
// if there is config for next route name, we go deeper
if (nextRoute.name in deeperConfig) {
nestedRouteNames = `${nestedRouteNames}/${route.name}`;
route = nextRoute as Route<string> & { state?: State };
currentOptions = deeperConfig;
} else {
// if not, there is no sense in going deeper in config
pattern = (currentOptions[route.name] as { path: string }).path;
nestedRouteNames = `${nestedRouteNames}/${route.name}`;
break;
}
}
} }
} }
} }
if (pattern === undefined) { if (pattern === undefined) {
// cut the first `/` pattern = nestedRouteNames.join('/');
pattern = nestedRouteNames.substring(1);
} }
// we don't add empty path strings to path if (currentOptions[route.name] !== undefined) {
if (pattern !== '') { path += pattern
const config = .split('/')
currentOptions[route.name] !== undefined .map((p) => {
? (currentOptions[route.name] as { stringify?: StringifyConfig }) const name = p.replace(/^:/, '').replace(/\?$/, '');
.stringify
: undefined;
const params = route.params // If the path has a pattern for a param, put the param in the path
? // Stringify all of the param values before we use them if (p.startsWith(':')) {
Object.entries(route.params).reduce<{ const value = allParams[name];
[key: string]: string;
}>((acc, [key, value]) => {
acc[key] = config?.[key] ? config[key](value) : String(value);
return acc;
}, {})
: undefined;
if (currentOptions[route.name] !== undefined) { // Remove the used value from the params object since we'll use the rest for query string
path += pattern if (currentParams) {
.split('/')
.map((p) => {
const name = p.replace(/^:/, '');
// If the path has a pattern for a param, put the param in the path
if (params && name in params && p.startsWith(':')) {
const value = params[name];
// Remove the used value from the params object since we'll use the rest for query string
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete params[name]; delete currentParams[name];
return encodeURIComponent(value);
} }
return encodeURIComponent(p); if (value === undefined && p.endsWith('?')) {
}) // Optional params without value assigned in route.params should be ignored
.join('/'); return '';
} else { }
path += encodeURIComponent(route.name);
return encodeURIComponent(value);
}
return encodeURIComponent(p);
})
.join('/');
} else {
path += encodeURIComponent(route.name);
}
if (route.state) {
path += '/';
} else if (currentParams) {
for (let param in currentParams) {
if (currentParams[param] === 'undefined') {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete currentParams[param];
}
} }
if (route.state) { const query = queryString.stringify(currentParams);
path += '/';
} else if (params) {
for (let param in params) {
if (params[param] === 'undefined') {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete params[param];
}
}
const query = queryString.stringify(params);
if (query) { if (query) {
path += `?${query}`; path += `?${query}`;
}
} }
} }
current = route.state; current = route.state;
} }
path = // Remove multiple as well as trailing slashes
path !== '/' && path.slice(path.length - 1) === '/' path = path.replace(/\/+/g, '/');
? path.slice(0, -1) path = path.length > 1 ? path.replace(/\/$/, '') : path;
: path;
return path; return path;
} }
// Object.fromEntries is not available in older iOS versions
const fromEntries = <K extends string, V>(entries: (readonly [K, V])[]) =>
entries.reduce((acc, [k, v]) => {
acc[k] = v;
return acc;
}, {} as Record<K, V>);
const joinPaths = (...paths: string[]): string =>
([] as string[])
.concat(...paths.map((p) => p.split('/')))
.filter(Boolean)
.join('/');
const createConfigItem = (
config: OptionsItem | string,
parentPattern?: string
): ConfigItem => {
if (typeof config === 'string') {
// If a string is specified as the value of the key(e.g. Foo: '/path'), use it as the pattern
const pattern = parentPattern ? joinPaths(parentPattern, config) : config;
return { pattern };
}
// If an object is specified as the value (e.g. Foo: { ... }),
// It can have `path` property and `screens` prop which has nested configs
const pattern =
config.exact !== true && parentPattern && config.path
? joinPaths(parentPattern, config.path)
: config.path;
const screens = config.screens
? createNormalizedConfigs(config.screens, pattern)
: undefined;
return {
// Normalize pattern to remove any leading, trailing slashes, duplicate slashes etc.
pattern: pattern?.split('/').filter(Boolean).join('/'),
stringify: config.stringify,
screens,
};
};
const createNormalizedConfigs = (
options: PathConfig,
pattern?: string
): Record<string, ConfigItem> =>
fromEntries(
Object.entries(options).map(([name, c]) => {
const result = createConfigItem(c, pattern);
return [name, result];
})
);

View File

@@ -5,25 +5,17 @@ import {
PartialState, PartialState,
InitialState, InitialState,
} from '@react-navigation/routers'; } from '@react-navigation/routers';
import { PathConfig } from './types';
type ParseConfig = Record<string, (value: string) => any>; type ParseConfig = Record<string, (value: string) => any>;
type Options = {
[routeName: string]:
| string
| {
path?: string;
parse?: ParseConfig;
screens?: Options;
initialRouteName?: string;
};
};
type RouteConfig = { type RouteConfig = {
match: RegExp; screen: string;
regex?: RegExp;
path: string;
pattern: string; pattern: string;
routeNames: string[]; routeNames: string[];
parse: ParseConfig | undefined; parse?: ParseConfig;
}; };
type InitialRouteConfig = { type InitialRouteConfig = {
@@ -56,34 +48,73 @@ type ResultState = PartialState<NavigationState> & {
*/ */
export default function getStateFromPath( export default function getStateFromPath(
path: string, path: string,
options: Options = {} options: PathConfig = {}
): ResultState | undefined { ): ResultState | undefined {
if (path === '') { let initialRoutes: InitialRouteConfig[] = [];
// Create a normalized configs array which will be easier to use
const configs = ([] as RouteConfig[])
.concat(
...Object.keys(options).map((key) =>
createNormalizedConfigs(key, options, [], initialRoutes)
)
)
.sort(
(a, b) =>
// Sort configs so the most exhaustive is always first to be chosen
b.pattern.split('/').length - a.pattern.split('/').length
);
let remaining = path
.replace(/\/+/g, '/') // Replace multiple slash (//) with single ones
.replace(/^\//, '') // Remove extra leading slash
.replace(/\?.*$/, ''); // Remove query params which we will handle later
// Make sure there is a trailing slash
remaining = remaining.endsWith('/') ? remaining : `${remaining}/`;
if (remaining === '/') {
// We need to add special handling of empty path so navigation to empty path also works
// When handling empty path, we should only look at the root level config
const match = configs.find(
(config) =>
config.path === '' &&
config.routeNames.every(
// Make sure that none of the parent configs have a non-empty path defined
(name) => !configs.find((c) => c.screen === name)?.path
)
);
if (match) {
return createNestedStateObject(
match.routeNames.map((name, i, self) => {
if (i === self.length - 1) {
return { name, params: parseQueryParams(path, match.parse) };
}
return { name };
}),
initialRoutes
);
}
return undefined; return undefined;
} }
let initialRoutes: InitialRouteConfig[] = [];
// Create a normalized configs array which will be easier to use
const configs = ([] as RouteConfig[]).concat(
...Object.keys(options).map((key) =>
createNormalizedConfigs(key, options, [], initialRoutes)
)
);
let result: PartialState<NavigationState> | undefined; let result: PartialState<NavigationState> | undefined;
let current: PartialState<NavigationState> | undefined; let current: PartialState<NavigationState> | undefined;
let remaining = path
.replace(/[/]+/, '/') // Replace multiple slash (//) with single ones
.replace(/^\//, '') // Remove extra leading slash
.replace(/\?.*/, ''); // Remove query params which we will handle later
while (remaining) { while (remaining) {
let routeNames: string[] | undefined; let routeNames: string[] | undefined;
let params: Record<string, any> | undefined; let allParams: Record<string, any> | undefined;
// Go through all configs, and see if the next path segment matches our regex // Go through all configs, and see if the next path segment matches our regex
for (const config of configs) { for (const config of configs) {
const match = remaining.match(config.match); if (!config.regex) {
continue;
}
const match = remaining.match(config.regex);
// If our regex matches, we need to extract params from the path // If our regex matches, we need to extract params from the path
if (match) { if (match) {
@@ -94,21 +125,16 @@ export default function getStateFromPath(
.filter((p) => p.startsWith(':')); .filter((p) => p.startsWith(':'));
if (paramPatterns.length) { if (paramPatterns.length) {
params = paramPatterns.reduce<Record<string, any>>((acc, p, i) => { allParams = paramPatterns.reduce<Record<string, any>>((acc, p, i) => {
const key = p.replace(/^:/, ''); const value = match![(i + 1) * 2].replace(/\//, ''); // The param segments appear every second item starting from 2 in the regex match result
const value = match[i + 1]; // The param segments start from index 1 in the regex match result
acc[key] = acc[p] = value;
config.parse && config.parse[key]
? config.parse[key](value)
: value;
return acc; return acc;
}, {}); }, {});
} }
// Remove the matched segment from the remaining path remaining = remaining.replace(match[1], '');
remaining = remaining.replace(match[0], '');
break; break;
} }
@@ -123,34 +149,46 @@ export default function getStateFromPath(
remaining = segments.join('/'); remaining = segments.join('/');
} }
let state: InitialState; const state = createNestedStateObject(
let routeName = routeNames.shift() as string; routeNames.map((name) => {
let initialRoute = findInitialRoute(routeName, initialRoutes); const config = configs.find((c) => c.screen === name);
state = createNestedState( let params: object | undefined;
initialRoute,
routeName,
routeNames.length === 0,
params
);
if (routeNames.length > 0) { if (allParams && config?.path) {
let nestedState = state; const pattern = config.path;
while ((routeName = routeNames.shift() as string)) { if (pattern) {
initialRoute = findInitialRoute(routeName, initialRoutes); const paramPatterns = pattern
nestedState.routes[nestedState.index || 0].state = createNestedState( .split('/')
initialRoute, .filter((p) => p.startsWith(':'));
routeName,
routeNames.length === 0, if (paramPatterns.length) {
params params = paramPatterns.reduce<Record<string, any>>((acc, p) => {
); const key = p.replace(/^:/, '').replace(/\?$/, '');
if (routeNames.length > 0) { const value = allParams![p];
nestedState = nestedState.routes[nestedState.index || 0]
.state as InitialState; if (value) {
acc[key] =
config.parse && config.parse[key]
? config.parse[key](value)
: value;
}
return acc;
}, {});
}
}
} }
}
} if (params && Object.keys(params).length) {
return { name, params };
}
return { name };
}),
initialRoutes
);
if (current) { if (current) {
// The state should be nested inside the deepest route we parsed before // The state should be nested inside the deepest route we parsed before
@@ -172,74 +210,78 @@ export default function getStateFromPath(
return undefined; return undefined;
} }
const query = path.split('?')[1]; const route = findFocusedRoute(current);
const params = parseQueryParams(
if (query) { path,
while (current?.routes[current.index || 0].state) { findParseConfigForRoute(route.name, configs)
// The query params apply to the deepest route );
current = current.routes[current.index || 0].state;
}
const route = (current as PartialState<NavigationState>).routes[
current?.index || 0
];
const params = queryString.parse(query);
const parseFunction = findParseConfigForRoute(route.name, configs);
if (parseFunction) {
Object.keys(params).forEach((name) => {
if (parseFunction[name] && typeof params[name] === 'string') {
params[name] = parseFunction[name](params[name] as string);
}
});
}
if (params) {
route.params = { ...route.params, ...params }; route.params = { ...route.params, ...params };
} }
return result; return result;
} }
function createNormalizedConfigs( const joinPaths = (...paths: string[]): string =>
key: string, ([] as string[])
routeConfig: Options, .concat(...paths.map((p) => p.split('/')))
.filter(Boolean)
.join('/');
const createNormalizedConfigs = (
screen: string,
routeConfig: PathConfig,
routeNames: string[] = [], routeNames: string[] = [],
initials: InitialRouteConfig[] initials: InitialRouteConfig[],
): RouteConfig[] { parentPattern?: string
): RouteConfig[] => {
const configs: RouteConfig[] = []; const configs: RouteConfig[] = [];
routeNames.push(key); routeNames.push(screen);
const value = routeConfig[key]; const config = routeConfig[screen];
if (typeof value === 'string') { if (typeof config === 'string') {
// If a string is specified as the value of the key(e.g. Foo: '/path'), use it as the pattern // If a string is specified as the value of the key(e.g. Foo: '/path'), use it as the pattern
if (value !== '') { const pattern = parentPattern ? joinPaths(parentPattern, config) : config;
configs.push(createConfigItem(routeNames, value));
} configs.push(createConfigItem(screen, routeNames, pattern, config));
} else if (typeof value === 'object') { } else if (typeof config === 'object') {
let pattern: string | undefined;
// if an object is specified as the value (e.g. Foo: { ... }), // if an object is specified as the value (e.g. Foo: { ... }),
// it can have `path` property and // it can have `path` property and
// it could have `screens` prop which has nested configs // it could have `screens` prop which has nested configs
if (value.path && value.path !== '') { if (typeof config.path === 'string') {
configs.push(createConfigItem(routeNames, value.path, value.parse)); pattern =
config.exact !== true && parentPattern
? joinPaths(parentPattern, config.path)
: config.path;
configs.push(
createConfigItem(screen, routeNames, pattern, config.path, config.parse)
);
} }
if (value.screens) {
if (config.screens) {
// property `initialRouteName` without `screens` has no purpose // property `initialRouteName` without `screens` has no purpose
if (value.initialRouteName) { if (config.initialRouteName) {
initials.push({ initials.push({
initialRouteName: value.initialRouteName, initialRouteName: config.initialRouteName,
connectedRoutes: Object.keys(value.screens), connectedRoutes: Object.keys(config.screens),
}); });
} }
Object.keys(value.screens).forEach((nestedConfig) => {
Object.keys(config.screens).forEach((nestedConfig) => {
const result = createNormalizedConfigs( const result = createNormalizedConfigs(
nestedConfig, nestedConfig,
value.screens as Options, config.screens as PathConfig,
routeNames, routeNames,
initials initials,
pattern
); );
configs.push(...result); configs.push(...result);
}); });
} }
@@ -248,43 +290,62 @@ function createNormalizedConfigs(
routeNames.pop(); routeNames.pop();
return configs; return configs;
} };
function createConfigItem( const createConfigItem = (
screen: string,
routeNames: string[], routeNames: string[],
pattern: string, pattern: string,
path: string,
parse?: ParseConfig parse?: ParseConfig
): RouteConfig { ): RouteConfig => {
const match = new RegExp( // Normalize pattern to remove any leading, trailing slashes, duplicate slashes etc.
'^' + escape(pattern).replace(/:[a-z0-9]+/gi, '([^/]+)') + '/?' pattern = pattern.split('/').filter(Boolean).join('/');
);
const regex = pattern
? new RegExp(
`^(${pattern
.split('/')
.map((it) => {
if (it.startsWith(':')) {
return `(([^/]+\\/)${it.endsWith('?') ? '?' : ''})`;
}
return `${escape(it)}\\/`;
})
.join('')})`
)
: undefined;
return { return {
match, screen,
regex,
pattern, pattern,
path,
// The routeNames array is mutated, so copy it to keep the current state // The routeNames array is mutated, so copy it to keep the current state
routeNames: [...routeNames], routeNames: [...routeNames],
parse, parse,
}; };
} };
function findParseConfigForRoute( const findParseConfigForRoute = (
routeName: string, routeName: string,
flatConfig: RouteConfig[] flatConfig: RouteConfig[]
): ParseConfig | undefined { ): ParseConfig | undefined => {
for (const config of flatConfig) { for (const config of flatConfig) {
if (routeName === config.routeNames[config.routeNames.length - 1]) { if (routeName === config.routeNames[config.routeNames.length - 1]) {
return config.parse; return config.parse;
} }
} }
return undefined;
}
// tries to find an initial route connected with the one passed return undefined;
function findInitialRoute( };
// Try to find an initial route connected with the one passed
const findInitialRoute = (
routeName: string, routeName: string,
initialRoutes: InitialRouteConfig[] initialRoutes: InitialRouteConfig[]
): string | undefined { ): string | undefined => {
for (const config of initialRoutes) { for (const config of initialRoutes) {
if (config.connectedRoutes.includes(routeName)) { if (config.connectedRoutes.includes(routeName)) {
return config.initialRouteName === routeName return config.initialRouteName === routeName
@@ -293,28 +354,25 @@ function findInitialRoute(
} }
} }
return undefined; return undefined;
} };
// returns nested state object with values depending on whether // returns state object with values depending on whether
// it is the end of state and if there is initialRoute for this level // it is the end of state and if there is initialRoute for this level
function createNestedState( const createStateObject = (
initialRoute: string | undefined, initialRoute: string | undefined,
routeName: string, routeName: string,
isEmpty: boolean, params: Record<string, any> | undefined,
params?: Record<string, any> | undefined isEmpty: boolean
): InitialState { ): InitialState => {
if (isEmpty) { if (isEmpty) {
if (initialRoute) { if (initialRoute) {
return { return {
index: 1, index: 1,
routes: [ routes: [{ name: initialRoute }, { name: routeName as string, params }],
{ name: initialRoute },
{ name: routeName as string, ...(params && { params }) },
],
}; };
} else { } else {
return { return {
routes: [{ name: routeName as string, ...(params && { params }) }], routes: [{ name: routeName as string, params }],
}; };
} }
} else { } else {
@@ -323,11 +381,87 @@ function createNestedState(
index: 1, index: 1,
routes: [ routes: [
{ name: initialRoute }, { name: initialRoute },
{ name: routeName as string, state: { routes: [] } }, { name: routeName as string, params, state: { routes: [] } },
], ],
}; };
} else { } else {
return { routes: [{ name: routeName as string, state: { routes: [] } }] }; return {
routes: [{ name: routeName as string, params, state: { routes: [] } }],
};
} }
} }
} };
const createNestedStateObject = (
routes: { name: string; params?: object }[],
initialRoutes: InitialRouteConfig[]
) => {
let state: InitialState;
let route = routes.shift() as { name: string; params?: object };
let initialRoute = findInitialRoute(route.name, initialRoutes);
state = createStateObject(
initialRoute,
route.name,
route.params,
routes.length === 0
);
if (routes.length > 0) {
let nestedState = state;
while ((route = routes.shift() as { name: string; params?: object })) {
initialRoute = findInitialRoute(route.name, initialRoutes);
const nestedStateIndex =
nestedState.index || nestedState.routes.length - 1;
nestedState.routes[nestedStateIndex].state = createStateObject(
initialRoute,
route.name,
route.params,
routes.length === 0
);
if (routes.length > 0) {
nestedState = nestedState.routes[nestedStateIndex]
.state as InitialState;
}
}
}
return state;
};
const findFocusedRoute = (state: InitialState) => {
let current: InitialState | undefined = state;
while (current?.routes[current.index || 0].state) {
// The query params apply to the deepest route
current = current.routes[current.index || 0].state;
}
const route = (current as PartialState<NavigationState>).routes[
current?.index || 0
];
return route;
};
const parseQueryParams = (
path: string,
parseConfig?: Record<string, (value: string) => any>
) => {
const query = path.split('?')[1];
const params = queryString.parse(query);
if (parseConfig) {
Object.keys(params).forEach((name) => {
if (parseConfig[name] && typeof params[name] === 'string') {
params[name] = parseConfig[name](params[name] as string);
}
});
}
return Object.keys(params).length ? params : undefined;
};

View File

@@ -422,6 +422,14 @@ export type NavigationContainerRef = NavigationHelpers<ParamListBase> &
* Get the rehydrated navigation state of the navigation tree. * Get the rehydrated navigation state of the navigation tree.
*/ */
getRootState(): NavigationState; getRootState(): NavigationState;
/**
* Get the currently focused navigation route.
*/
getCurrentRoute(): Route<string> | undefined;
/**
* Get the currently focused route's options.
*/
getCurrentOptions(): object | undefined;
}; };
export type TypedNavigator< export type TypedNavigator<
@@ -462,3 +470,16 @@ export type TypedNavigator<
_: RouteConfig<ParamList, RouteName, State, ScreenOptions, EventMap> _: RouteConfig<ParamList, RouteName, State, ScreenOptions, EventMap>
) => null; ) => null;
}; };
export type PathConfig = {
[routeName: string]:
| string
| {
path?: string;
exact?: boolean;
parse?: Record<string, (value: string) => any>;
stringify?: Record<string, (value: any) => string>;
screens?: PathConfig;
initialRouteName?: string;
};
};

View File

@@ -117,6 +117,28 @@ export default function useDescriptors<
const screen = screens[route.name]; const screen = screens[route.name];
const navigation = navigations[route.key]; const navigation = navigations[route.key];
const routeOptions = {
// The default `screenOptions` passed to the navigator
...(typeof screenOptions === 'object' || screenOptions == null
? screenOptions
: screenOptions({
// @ts-ignore
route,
navigation,
})),
// The `options` prop passed to `Screen` elements
...(typeof screen.options === 'object' || screen.options == null
? screen.options
: screen.options({
// @ts-ignore
route,
// @ts-ignore
navigation,
})),
// The options set via `navigation.setOptions`
...options[route.key],
};
acc[route.key] = { acc[route.key] = {
navigation, navigation,
render() { render() {
@@ -128,31 +150,12 @@ export default function useDescriptors<
screen={screen} screen={screen}
getState={getState} getState={getState}
setState={setState} setState={setState}
options={routeOptions}
/> />
</NavigationBuilderContext.Provider> </NavigationBuilderContext.Provider>
); );
}, },
options: { options: routeOptions,
// The default `screenOptions` passed to the navigator
...(typeof screenOptions === 'object' || screenOptions == null
? screenOptions
: screenOptions({
// @ts-ignore
route,
navigation,
})),
// The `options` prop passed to `Screen` elements
...(typeof screen.options === 'object' || screen.options == null
? screen.options
: screen.options({
// @ts-ignore
route,
// @ts-ignore
navigation,
})),
// The options set via `navigation.setOptions`
...options[route.key],
},
}; };
return acc; return acc;

View File

@@ -9,6 +9,7 @@ import useNavigation from './useNavigation';
*/ */
export default function useIsFocused(): boolean { export default function useIsFocused(): boolean {
const navigation = useNavigation(); const navigation = useNavigation();
// eslint-disable-next-line react-hooks/exhaustive-deps
const getCurrentValue = React.useCallback(navigation.isFocused, [navigation]); const getCurrentValue = React.useCallback(navigation.isFocused, [navigation]);
const subscribe = React.useCallback( const subscribe = React.useCallback(
(callback: (value: boolean) => void) => { (callback: (value: boolean) => void) => {

View File

@@ -362,9 +362,14 @@ export default function useNavigationBuilder<
return () => { return () => {
// We need to clean up state for this navigator on unmount // We need to clean up state for this navigator on unmount
if (getCurrentState() !== undefined && getKey() === navigatorKey) { // We do it in a timeout because we need to detect if another navigator mounted in the meantime
setState(undefined); // For example, if another navigator has started rendering, we should skip cleanup
} // Otherwise, our cleanup step will cleanup state for the other navigator and re-initialize it
setTimeout(() => {
if (getCurrentState() !== undefined && getKey() === navigatorKey) {
setState(undefined);
}
}, 0);
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);

View File

@@ -0,0 +1,70 @@
import * as React from 'react';
import { NavigationStateContext } from './BaseNavigationContainer';
import { NavigationState } from '@react-navigation/routers';
export default function useOptionsGetters({
key,
getOptions,
getState,
}: {
key?: string;
getOptions?: () => object | undefined;
getState?: () => NavigationState;
}) {
const optionsGettersFromChild = React.useRef<
Record<string, (() => object | undefined | null) | undefined>
>({});
const { addOptionsGetter: parentAddOptionsGetter } = React.useContext(
NavigationStateContext
);
const getOptionsFromListener = React.useCallback(() => {
for (let key in optionsGettersFromChild.current) {
if (optionsGettersFromChild.current.hasOwnProperty(key)) {
const result = optionsGettersFromChild.current[key]?.();
// null means unfocused route
if (result !== null) {
return result;
}
}
}
return null;
}, []);
const getCurrentOptions = React.useCallback(() => {
if (getState) {
const state = getState();
if (state.routes[state.index].key !== key) {
// null means unfocused route
return null;
}
}
const optionsFromListener = getOptionsFromListener();
if (optionsFromListener !== null) {
return optionsFromListener;
}
return getOptions?.() ?? undefined;
}, [getState, getOptionsFromListener, getOptions, key]);
React.useEffect(() => {
return parentAddOptionsGetter?.(key!, getCurrentOptions);
}, [getCurrentOptions, parentAddOptionsGetter, key]);
const addOptionsGetter = React.useCallback(
(key: string, getter: () => object | undefined | null) => {
optionsGettersFromChild.current[key] = getter;
return () => {
optionsGettersFromChild.current[key] = undefined;
};
},
[]
);
return {
addOptionsGetter,
getCurrentOptions,
};
}

View File

@@ -8,6 +8,15 @@ const UNINTIALIZED_STATE = {};
export default function useSyncState<T>(initialState?: (() => T) | T) { export default function useSyncState<T>(initialState?: (() => T) | T) {
const stateRef = React.useRef<T>(UNINTIALIZED_STATE as any); const stateRef = React.useRef<T>(UNINTIALIZED_STATE as any);
const isSchedulingRef = React.useRef(false); const isSchedulingRef = React.useRef(false);
const isMountedRef = React.useRef(true);
React.useEffect(() => {
isMountedRef.current = true;
return () => {
isMountedRef.current = false;
};
}, []);
if (stateRef.current === UNINTIALIZED_STATE) { if (stateRef.current === UNINTIALIZED_STATE) {
stateRef.current = stateRef.current =
@@ -20,7 +29,7 @@ export default function useSyncState<T>(initialState?: (() => T) | T) {
const getState = React.useCallback(() => stateRef.current, []); const getState = React.useCallback(() => stateRef.current, []);
const setState = React.useCallback((state: T) => { const setState = React.useCallback((state: T) => {
if (state === stateRef.current) { if (state === stateRef.current || !isMountedRef.current) {
return; return;
} }
@@ -42,6 +51,10 @@ export default function useSyncState<T>(initialState?: (() => T) | T) {
}, []); }, []);
const flushUpdates = React.useCallback(() => { const flushUpdates = React.useCallback(() => {
if (!isMountedRef.current) {
return;
}
// Make sure that the tracking state is up-to-date. // Make sure that the tracking state is up-to-date.
// We call it unconditionally, but React should skip the update if state is unchanged. // We call it unconditionally, but React should skip the update if state is unchanged.
setTrackingState(stateRef.current); setTrackingState(stateRef.current);

View File

@@ -3,6 +3,118 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.7.6](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.7.5...@react-navigation/drawer@5.7.6) (2020-05-20)
**Note:** Version bump only for package @react-navigation/drawer
## [5.7.5](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.7.4...@react-navigation/drawer@5.7.5) (2020-05-16)
**Note:** Version bump only for package @react-navigation/drawer
## [5.7.4](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.7.3...@react-navigation/drawer@5.7.4) (2020-05-14)
**Note:** Version bump only for package @react-navigation/drawer
## [5.7.3](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.7.2...@react-navigation/drawer@5.7.3) (2020-05-14)
**Note:** Version bump only for package @react-navigation/drawer
## [5.7.2](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.7.1...@react-navigation/drawer@5.7.2) (2020-05-10)
**Note:** Version bump only for package @react-navigation/drawer
## [5.7.1](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.7.0...@react-navigation/drawer@5.7.1) (2020-05-08)
### Bug Fixes
* fix building typescript definitions. closes [#8216](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/8216) ([47a1229](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/47a12298378747edd2d22e54dc1c8677f98c49b4))
# [5.7.0](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.6.4...@react-navigation/drawer@5.7.0) (2020-05-08)
### Features
* add generic type aliases for screen props ([bea14aa](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/bea14aa26fd5cbfebc7973733c5cf1f44fd323aa)), closes [#7971](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/issues/7971)
## [5.6.4](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.6.3...@react-navigation/drawer@5.6.4) (2020-05-05)
**Note:** Version bump only for package @react-navigation/drawer
## [5.6.3](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.6.2...@react-navigation/drawer@5.6.3) (2020-05-01)
**Note:** Version bump only for package @react-navigation/drawer
## [5.6.2](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.6.1...@react-navigation/drawer@5.6.2) (2020-05-01)
**Note:** Version bump only for package @react-navigation/drawer
## [5.6.1](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.6.0...@react-navigation/drawer@5.6.1) (2020-04-30)
**Note:** Version bump only for package @react-navigation/drawer
# [5.6.0](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.5.1...@react-navigation/drawer@5.6.0) (2020-04-30)
### Bug Fixes
* fix closing drawer on web with tap on overlay ([70be3f6](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/70be3f6d863c56211e2f90bdf743bd8526338248))
* make sure the address bar hides when scrolling on web ([0a19e94](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/0a19e94b23a4d2b5f22d1d9deb0544f586f475ee))
### Features
* add `useLinkBuilder` hook to build links ([2792f43](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/2792f438fe45428fe193e3708fee7ad61966cbf4))
* add action prop to Link ([942d2be](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/commit/942d2be2c72720469475ce12ec8df23825994dbf))
## [5.5.1](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.5.0...@react-navigation/drawer@5.5.1) (2020-04-27) ## [5.5.1](https://github.com/react-navigation/react-navigation/tree/master/packages/drawer/compare/@react-navigation/drawer@5.5.0...@react-navigation/drawer@5.5.1) (2020-04-27)
**Note:** Version bump only for package @react-navigation/drawer **Note:** Version bump only for package @react-navigation/drawer

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/drawer", "name": "@react-navigation/drawer",
"description": "Drawer navigator component with animated transitions and gesturess", "description": "Drawer navigator component with animated transitions and gesturess",
"version": "5.5.1", "version": "5.7.6",
"keywords": [ "keywords": [
"react-native-component", "react-native-component",
"react-component", "react-component",
@@ -20,6 +20,7 @@
"homepage": "https://reactnavigation.org/docs/drawer-navigator.html", "homepage": "https://reactnavigation.org/docs/drawer-navigator.html",
"main": "lib/commonjs/index.js", "main": "lib/commonjs/index.js",
"react-native": "src/index.tsx", "react-native": "src/index.tsx",
"source": "src/index.tsx",
"module": "lib/module/index.js", "module": "lib/module/index.js",
"types": "lib/typescript/src/index.d.ts", "types": "lib/typescript/src/index.d.ts",
"files": [ "files": [
@@ -39,17 +40,17 @@
"react-native-iphone-x-helper": "^1.2.1" "react-native-iphone-x-helper": "^1.2.1"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.10.0", "@react-native-community/bob": "^0.14.3",
"@react-navigation/native": "^5.1.7", "@react-navigation/native": "^5.4.1",
"@types/react": "^16.9.23", "@types/react": "^16.9.34",
"@types/react-native": "^0.61.22", "@types/react-native": "^0.62.7",
"del-cli": "^3.0.0", "del-cli": "^3.0.0",
"react": "~16.9.0", "react": "~16.9.0",
"react-native": "~0.61.5", "react-native": "~0.61.5",
"react-native-gesture-handler": "^1.6.0", "react-native-gesture-handler": "^1.6.0",
"react-native-reanimated": "^1.7.0", "react-native-reanimated": "^1.8.0",
"react-native-safe-area-context": "^0.7.3", "react-native-safe-area-context": "^0.7.3",
"react-native-screens": "^2.3.0", "react-native-screens": "^2.7.0",
"typescript": "^3.8.3" "typescript": "^3.8.3"
}, },
"peerDependencies": { "peerDependencies": {

View File

@@ -25,6 +25,7 @@ export { default as useIsDrawerOpen } from './utils/useIsDrawerOpen';
export type { export type {
DrawerNavigationOptions, DrawerNavigationOptions,
DrawerNavigationProp, DrawerNavigationProp,
DrawerScreenProps,
DrawerContentOptions, DrawerContentOptions,
DrawerContentComponentProps, DrawerContentComponentProps,
} from './types'; } from './types';

View File

@@ -8,8 +8,9 @@ import {
NavigationHelpers, NavigationHelpers,
DrawerNavigationState, DrawerNavigationState,
DrawerActionHelpers, DrawerActionHelpers,
RouteProp,
} from '@react-navigation/native'; } from '@react-navigation/native';
import { PanGestureHandler } from 'react-native-gesture-handler'; import type { PanGestureHandlerProperties } from 'react-native-gesture-handler';
export type Scene = { export type Scene = {
route: Route<string>; route: Route<string>;
@@ -32,6 +33,7 @@ export type DrawerNavigationConfig<T = DrawerContentOptions> = {
drawerType?: 'front' | 'back' | 'slide' | 'permanent'; drawerType?: 'front' | 'back' | 'slide' | 'permanent';
/** /**
* How far from the edge of the screen the swipe gesture should activate. * How far from the edge of the screen the swipe gesture should activate.
* Not supported on Web.
*/ */
edgeWidth?: number; edgeWidth?: number;
/** /**
@@ -58,8 +60,9 @@ export type DrawerNavigationConfig<T = DrawerContentOptions> = {
statusBarAnimation?: 'slide' | 'none' | 'fade'; statusBarAnimation?: 'slide' | 'none' | 'fade';
/** /**
* Props to pass to the underlying pan gesture handler. * Props to pass to the underlying pan gesture handler.
* Not supported on Web.
*/ */
gestureHandlerProps?: React.ComponentProps<typeof PanGestureHandler>; gestureHandlerProps?: PanGestureHandlerProperties;
/** /**
* Whether the screens should render the first time they are accessed. Defaults to `true`. * Whether the screens should render the first time they are accessed. Defaults to `true`.
* Set it to `false` if you want to render all screens on initial render. * Set it to `false` if you want to render all screens on initial render.
@@ -113,13 +116,15 @@ export type DrawerNavigationOptions = {
* Whether you can use gestures to open or close the drawer. * Whether you can use gestures to open or close the drawer.
* Setting this to `false` disables swipe gestures as well as tap on overlay to close. * Setting this to `false` disables swipe gestures as well as tap on overlay to close.
* See `swipeEnabled` to disable only the swipe gesture. * See `swipeEnabled` to disable only the swipe gesture.
* Defaults to `true` * Defaults to `true`.
* Not supported on Web.
*/ */
gestureEnabled?: boolean; gestureEnabled?: boolean;
/** /**
* Whether you can use swipe gestures to open or close the drawer. * Whether you can use swipe gestures to open or close the drawer.
* Defaults to `true` * Defaults to `true`.
* Not supported on Web.
*/ */
swipeEnabled?: boolean; swipeEnabled?: boolean;
@@ -204,6 +209,14 @@ export type DrawerNavigationProp<
> & > &
DrawerActionHelpers<ParamList>; DrawerActionHelpers<ParamList>;
export type DrawerScreenProps<
ParamList extends ParamListBase,
RouteName extends keyof ParamList = string
> = {
navigation: DrawerNavigationProp<ParamList, RouteName>;
route: RouteProp<ParamList, RouteName>;
};
export type DrawerDescriptor = Descriptor< export type DrawerDescriptor = Descriptor<
ParamListBase, ParamListBase,
string, string,

View File

@@ -10,14 +10,14 @@ import {
StyleProp, StyleProp,
View, View,
InteractionManager, InteractionManager,
TouchableWithoutFeedback,
} from 'react-native'; } from 'react-native';
import Animated from 'react-native-reanimated';
import { import {
PanGestureHandler, PanGestureHandler,
TapGestureHandler, TapGestureHandler,
State as GestureState, GestureState,
TapGestureHandlerStateChangeEvent, } from './GestureHandler';
} from 'react-native-gesture-handler';
import Animated from 'react-native-reanimated';
import Overlay from './Overlay'; import Overlay from './Overlay';
const { const {
@@ -79,7 +79,6 @@ type Props = {
open: boolean; open: boolean;
onOpen: () => void; onOpen: () => void;
onClose: () => void; onClose: () => void;
onGestureRef?: (ref: PanGestureHandler | null) => void;
gestureEnabled: boolean; gestureEnabled: boolean;
swipeEnabled: boolean; swipeEnabled: boolean;
drawerPosition: 'left' | 'right'; drawerPosition: 'left' | 'right';
@@ -511,28 +510,17 @@ export default class DrawerView extends React.Component<Props> {
}, },
]); ]);
private handleTapStateChange = private handleTapStateChange = event([
Platform.OS === 'web' {
? // FIXME: Drawer doesn't close on Web with the same code that we use for native nativeEvent: {
({ nativeEvent }: TapGestureHandlerStateChangeEvent) => { oldState: (s: Animated.Value<number>) =>
if ( cond(
nativeEvent.state === GestureState.END && eq(s, GestureState.ACTIVE),
nativeEvent.oldState === GestureState.ACTIVE set(this.manuallyTriggerSpring, TRUE)
) { ),
this.toggleDrawer(false); },
} },
} ]);
: event([
{
nativeEvent: {
oldState: (s: Animated.Value<number>) =>
cond(
eq(s, GestureState.ACTIVE),
set(this.manuallyTriggerSpring, TRUE)
),
},
},
]);
private handleContainerLayout = (e: LayoutChangeEvent) => private handleContainerLayout = (e: LayoutChangeEvent) =>
this.containerWidth.setValue(e.nativeEvent.layout.width); this.containerWidth.setValue(e.nativeEvent.layout.width);
@@ -579,7 +567,6 @@ export default class DrawerView extends React.Component<Props> {
sceneContainerStyle, sceneContainerStyle,
drawerStyle, drawerStyle,
overlayStyle, overlayStyle,
onGestureRef,
renderDrawerContent, renderDrawerContent,
renderSceneContent, renderSceneContent,
gestureHandlerProps, gestureHandlerProps,
@@ -624,7 +611,6 @@ export default class DrawerView extends React.Component<Props> {
return ( return (
<PanGestureHandler <PanGestureHandler
ref={onGestureRef}
activeOffsetX={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]} activeOffsetX={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]}
failOffsetY={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]} failOffsetY={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]}
onGestureEvent={this.handleGestureEvent} onGestureEvent={this.handleGestureEvent}
@@ -663,12 +649,20 @@ export default class DrawerView extends React.Component<Props> {
</View> </View>
{ {
// Disable overlay if sidebar is permanent // Disable overlay if sidebar is permanent
drawerType === 'permanent' ? null : ( drawerType === 'permanent' ? null : Platform.OS === 'web' ? (
<TouchableWithoutFeedback
onPress={
gestureEnabled ? () => this.toggleDrawer(false) : undefined
}
>
<Overlay progress={progress} style={overlayStyle as any} />
</TouchableWithoutFeedback>
) : (
<TapGestureHandler <TapGestureHandler
enabled={gestureEnabled} enabled={gestureEnabled}
onHandlerStateChange={this.handleTapStateChange} onHandlerStateChange={this.handleTapStateChange}
> >
<Overlay progress={progress} style={overlayStyle} /> <Overlay progress={progress} style={overlayStyle as any} />
</TapGestureHandler> </TapGestureHandler>
) )
} }
@@ -737,6 +731,11 @@ const styles = StyleSheet.create({
}, },
main: { main: {
flex: 1, flex: 1,
overflow: 'hidden', ...Platform.select({
// FIXME: We need to hide `overflowX` on Web so the translated content doesn't show offscreen.
// But adding `overflowX: 'hidden'` prevents content from collapsing the URL bar.
web: null,
default: { overflow: 'hidden' },
}),
}, },
}); });

View File

@@ -11,10 +11,6 @@ import {
} from 'react-native'; } from 'react-native';
// eslint-disable-next-line import/no-unresolved // eslint-disable-next-line import/no-unresolved
import { ScreenContainer } from 'react-native-screens'; import { ScreenContainer } from 'react-native-screens';
import {
PanGestureHandler,
GestureHandlerRootView,
} from 'react-native-gesture-handler';
import { import {
NavigationHelpersContext, NavigationHelpersContext,
DrawerNavigationState, DrawerNavigationState,
@@ -22,7 +18,7 @@ import {
useTheme, useTheme,
} from '@react-navigation/native'; } from '@react-navigation/native';
import DrawerGestureContext from '../utils/DrawerGestureContext'; import { GestureHandlerRootView } from './GestureHandler';
import SafeAreaProviderCompat from './SafeAreaProviderCompat'; import SafeAreaProviderCompat from './SafeAreaProviderCompat';
import ResourceSavingScene from './ResourceSavingScene'; import ResourceSavingScene from './ResourceSavingScene';
import DrawerContent from './DrawerContent'; import DrawerContent from './DrawerContent';
@@ -94,8 +90,6 @@ export default function DrawerView({
Dimensions.get('window') Dimensions.get('window')
); );
const drawerGestureRef = React.useRef<PanGestureHandler>(null);
const { colors } = useTheme(); const { colors } = useTheme();
const isDrawerOpen = state.history.some((it) => it.type === 'drawer'); const isDrawerOpen = state.history.some((it) => it.type === 'drawer');
@@ -205,55 +199,49 @@ export default function DrawerView({
<NavigationHelpersContext.Provider value={navigation}> <NavigationHelpersContext.Provider value={navigation}>
<GestureHandlerWrapper style={styles.content}> <GestureHandlerWrapper style={styles.content}>
<SafeAreaProviderCompat> <SafeAreaProviderCompat>
<DrawerGestureContext.Provider value={drawerGestureRef}> <DrawerOpenContext.Provider value={isDrawerOpen}>
<DrawerOpenContext.Provider value={isDrawerOpen}> <Drawer
<Drawer open={isDrawerOpen}
open={isDrawerOpen} gestureEnabled={gestureEnabled}
gestureEnabled={gestureEnabled} swipeEnabled={swipeEnabled}
swipeEnabled={swipeEnabled} onOpen={handleDrawerOpen}
onOpen={handleDrawerOpen} onClose={handleDrawerClose}
onClose={handleDrawerClose} gestureHandlerProps={gestureHandlerProps}
onGestureRef={(ref) => { drawerType={drawerType}
// @ts-ignore drawerPosition={drawerPosition}
drawerGestureRef.current = ref; sceneContainerStyle={[
}} { backgroundColor: colors.background },
gestureHandlerProps={gestureHandlerProps} sceneContainerStyle,
drawerType={drawerType} ]}
drawerPosition={drawerPosition} drawerStyle={[
sceneContainerStyle={[ {
{ backgroundColor: colors.background }, width: getDefaultDrawerWidth(dimensions),
sceneContainerStyle, backgroundColor: colors.card,
]} },
drawerStyle={[ drawerType === 'permanent' &&
{ (drawerPosition === 'left'
width: getDefaultDrawerWidth(dimensions), ? {
backgroundColor: colors.card, borderRightColor: colors.border,
}, borderRightWidth: StyleSheet.hairlineWidth,
drawerType === 'permanent' && }
(drawerPosition === 'left' : {
? { borderLeftColor: colors.border,
borderRightColor: colors.border, borderLeftWidth: StyleSheet.hairlineWidth,
borderRightWidth: StyleSheet.hairlineWidth, }),
} drawerStyle,
: { ]}
borderLeftColor: colors.border, overlayStyle={{ backgroundColor: overlayColor }}
borderLeftWidth: StyleSheet.hairlineWidth, swipeEdgeWidth={edgeWidth}
}), swipeDistanceThreshold={minSwipeDistance}
drawerStyle, hideStatusBar={hideStatusBar}
]} statusBarAnimation={statusBarAnimation}
overlayStyle={{ backgroundColor: overlayColor }} renderDrawerContent={renderNavigationView}
swipeEdgeWidth={edgeWidth} renderSceneContent={renderContent}
swipeDistanceThreshold={minSwipeDistance} keyboardDismissMode={keyboardDismissMode}
hideStatusBar={hideStatusBar} drawerPostion={drawerPosition}
statusBarAnimation={statusBarAnimation} dimensions={dimensions}
renderDrawerContent={renderNavigationView} />
renderSceneContent={renderContent} </DrawerOpenContext.Provider>
keyboardDismissMode={keyboardDismissMode}
drawerPostion={drawerPosition}
dimensions={dimensions}
/>
</DrawerOpenContext.Provider>
</DrawerGestureContext.Provider>
</SafeAreaProviderCompat> </SafeAreaProviderCompat>
</GestureHandlerWrapper> </GestureHandlerWrapper>
</NavigationHelpersContext.Provider> </NavigationHelpersContext.Provider>

View File

@@ -0,0 +1,23 @@
import * as React from 'react';
import {
PanGestureHandler as PanGestureHandlerNative,
PanGestureHandlerProperties,
} from 'react-native-gesture-handler';
import DrawerGestureContext from '../utils/DrawerGestureContext';
export function PanGestureHandler(props: PanGestureHandlerProperties) {
const gestureRef = React.useRef<PanGestureHandlerNative>(null);
return (
<DrawerGestureContext.Provider value={gestureRef}>
<PanGestureHandlerNative {...props} />
</DrawerGestureContext.Provider>
);
}
export {
GestureHandlerRootView,
TapGestureHandler,
State as GestureState,
PanGestureHandlerGestureEvent,
} from 'react-native-gesture-handler';

View File

@@ -0,0 +1,31 @@
import * as React from 'react';
import { View } from 'react-native';
import type {
PanGestureHandlerProperties,
TapGestureHandlerProperties,
} from 'react-native-gesture-handler';
const Dummy: any = ({ children }: { children: React.ReactNode }) => (
<>{children}</>
);
export const PanGestureHandler = Dummy as React.ComponentType<
PanGestureHandlerProperties
>;
export const TapGestureHandler = Dummy as React.ComponentType<
TapGestureHandlerProperties
>;
export const GestureHandlerRootView = View;
export const GestureState = {
UNDETERMINED: 0,
FAILED: 1,
BEGAN: 2,
CANCELLED: 3,
ACTIVE: 4,
END: 5,
};
export type { PanGestureHandlerGestureEvent } from 'react-native-gesture-handler';

View File

@@ -29,7 +29,7 @@ export default class ResourceSavingScene extends React.Component<Props> {
styles.container, styles.container,
Platform.OS === 'web' Platform.OS === 'web'
? { display: isVisible ? 'flex' : 'none' } ? { display: isVisible ? 'flex' : 'none' }
: null, : { overflow: 'hidden' },
style, style,
]} ]}
collapsable={false} collapsable={false}
@@ -52,7 +52,6 @@ export default class ResourceSavingScene extends React.Component<Props> {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
overflow: 'hidden',
}, },
attached: { attached: {
flex: 1, flex: 1,

View File

@@ -3,6 +3,112 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.2.6](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.2.5...@react-navigation/material-bottom-tabs@5.2.6) (2020-05-20)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.2.5](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.2.4...@react-navigation/material-bottom-tabs@5.2.5) (2020-05-16)
### Bug Fixes
* center icons in material tab bar. fixes [#8248](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/issues/8248) ([51b4087](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/commit/51b40879bdb9cea5462a2291955513a88eb87340))
## [5.2.4](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.2.3...@react-navigation/material-bottom-tabs@5.2.4) (2020-05-14)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.2.3](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.2.2...@react-navigation/material-bottom-tabs@5.2.3) (2020-05-14)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.2.2](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.2.1...@react-navigation/material-bottom-tabs@5.2.2) (2020-05-10)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.2.1](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.2.0...@react-navigation/material-bottom-tabs@5.2.1) (2020-05-08)
### Bug Fixes
* fix building typescript definitions. closes [#8216](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/issues/8216) ([47a1229](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/commit/47a12298378747edd2d22e54dc1c8677f98c49b4))
# [5.2.0](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.1.15...@react-navigation/material-bottom-tabs@5.2.0) (2020-05-08)
### Features
* add generic type aliases for screen props ([bea14aa](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/commit/bea14aa26fd5cbfebc7973733c5cf1f44fd323aa)), closes [#7971](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/issues/7971)
* use links in bottom navigation tabs ([f384706](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/commit/f384706741f7e2422c284b65da10425f7af680c0))
## [5.1.15](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.1.14...@react-navigation/material-bottom-tabs@5.1.15) (2020-05-05)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.1.14](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.1.13...@react-navigation/material-bottom-tabs@5.1.14) (2020-05-01)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.1.13](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.1.12...@react-navigation/material-bottom-tabs@5.1.13) (2020-05-01)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.1.12](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.1.11...@react-navigation/material-bottom-tabs@5.1.12) (2020-04-30)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.1.11](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.1.10...@react-navigation/material-bottom-tabs@5.1.11) (2020-04-30)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs
## [5.1.10](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.1.9...@react-navigation/material-bottom-tabs@5.1.10) (2020-04-27) ## [5.1.10](https://github.com/react-navigation/react-navigation/tree/master/packages/material-bottom-tabs/compare/@react-navigation/material-bottom-tabs@5.1.9...@react-navigation/material-bottom-tabs@5.1.10) (2020-04-27)
**Note:** Version bump only for package @react-navigation/material-bottom-tabs **Note:** Version bump only for package @react-navigation/material-bottom-tabs

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/material-bottom-tabs", "name": "@react-navigation/material-bottom-tabs",
"description": "Integration for bottom navigation component from react-native-paper", "description": "Integration for bottom navigation component from react-native-paper",
"version": "5.1.10", "version": "5.2.6",
"keywords": [ "keywords": [
"react-native-component", "react-native-component",
"react-component", "react-component",
@@ -20,6 +20,7 @@
"homepage": "https://reactnavigation.org/docs/material-bottom-tab-navigator.html", "homepage": "https://reactnavigation.org/docs/material-bottom-tab-navigator.html",
"main": "lib/commonjs/index.js", "main": "lib/commonjs/index.js",
"react-native": "src/index.tsx", "react-native": "src/index.tsx",
"source": "src/index.tsx",
"module": "lib/module/index.js", "module": "lib/module/index.js",
"types": "lib/typescript/src/index.d.ts", "types": "lib/typescript/src/index.d.ts",
"files": [ "files": [
@@ -35,15 +36,15 @@
"clean": "del lib" "clean": "del lib"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.10.0", "@react-native-community/bob": "^0.14.3",
"@react-navigation/native": "^5.1.7", "@react-navigation/native": "^5.4.1",
"@types/react": "^16.9.23", "@types/react": "^16.9.34",
"@types/react-native": "^0.61.22", "@types/react-native": "^0.62.7",
"@types/react-native-vector-icons": "^6.4.5", "@types/react-native-vector-icons": "^6.4.5",
"del-cli": "^3.0.0", "del-cli": "^3.0.0",
"react": "~16.9.0", "react": "~16.9.0",
"react-native": "~0.61.5", "react-native": "~0.61.5",
"react-native-paper": "^3.7.0", "react-native-paper": "^3.10.1",
"react-native-vector-icons": "^6.6.0", "react-native-vector-icons": "^6.6.0",
"typescript": "^3.8.3" "typescript": "^3.8.3"
}, },

View File

@@ -14,4 +14,5 @@ export { default as MaterialBottomTabView } from './views/MaterialBottomTabView'
export type { export type {
MaterialBottomTabNavigationOptions, MaterialBottomTabNavigationOptions,
MaterialBottomTabNavigationProp, MaterialBottomTabNavigationProp,
MaterialBottomTabScreenProps,
} from './types'; } from './types';

View File

@@ -6,6 +6,7 @@ import {
NavigationHelpers, NavigationHelpers,
TabNavigationState, TabNavigationState,
TabActionHelpers, TabActionHelpers,
RouteProp,
} from '@react-navigation/native'; } from '@react-navigation/native';
export type MaterialBottomTabNavigationEventMap = { export type MaterialBottomTabNavigationEventMap = {
@@ -32,6 +33,14 @@ export type MaterialBottomTabNavigationProp<
> & > &
TabActionHelpers<ParamList>; TabActionHelpers<ParamList>;
export type MaterialBottomTabScreenProps<
ParamList extends ParamListBase,
RouteName extends keyof ParamList = string
> = {
navigation: MaterialBottomTabNavigationProp<ParamList, RouteName>;
route: RouteProp<ParamList, RouteName>;
};
export type MaterialBottomTabNavigationOptions = { export type MaterialBottomTabNavigationOptions = {
/** /**
* Title text for the screen. * Title text for the screen.

View File

@@ -1,5 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { StyleSheet } from 'react-native'; import { StyleSheet, Platform } from 'react-native';
import { BottomNavigation, DefaultTheme, DarkTheme } from 'react-native-paper'; import { BottomNavigation, DefaultTheme, DarkTheme } from 'react-native-paper';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { import {
@@ -8,6 +8,8 @@ import {
TabNavigationState, TabNavigationState,
TabActions, TabActions,
useTheme, useTheme,
useLinkBuilder,
Link,
} from '@react-navigation/native'; } from '@react-navigation/native';
import { import {
@@ -24,13 +26,14 @@ type Props = MaterialBottomTabNavigationConfig & {
type Scene = { route: { key: string } }; type Scene = { route: { key: string } };
export default function MaterialBottomTabView({ function MaterialBottomTabViewInner({
state, state,
navigation, navigation,
descriptors, descriptors,
...rest ...rest
}: Props) { }: Props) {
const { dark, colors } = useTheme(); const { dark, colors } = useTheme();
const buildLink = useLinkBuilder();
const theme = React.useMemo(() => { const theme = React.useMemo(() => {
const t = dark ? DarkTheme : DefaultTheme; const t = dark ? DarkTheme : DefaultTheme;
@@ -46,67 +49,104 @@ export default function MaterialBottomTabView({
}, [colors, dark]); }, [colors, dark]);
return ( return (
<NavigationHelpersContext.Provider value={navigation}> <BottomNavigation
<BottomNavigation {...rest}
{...rest} theme={theme}
theme={theme} navigationState={state}
navigationState={state} onIndexChange={(index: number) =>
onIndexChange={(index: number) => navigation.dispatch({
navigation.dispatch({ ...TabActions.jumpTo(state.routes[index].name),
...TabActions.jumpTo(state.routes[index].name), target: state.key,
target: state.key, })
}) }
renderScene={({ route }) => descriptors[route.key].render()}
renderTouchable={
Platform.OS === 'web'
? ({
onPress,
route,
accessibilityRole: _0,
borderless: _1,
centered: _2,
rippleColor: _3,
style,
...rest
}) => {
return (
<Link
{...rest}
// @ts-ignore
to={buildLink(route.name, route.params)}
accessibilityRole="link"
onPress={(e: any) => {
if (
!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) && // ignore clicks with modifier keys
(e.button == null || e.button === 0) // ignore everything but left clicks
) {
e.preventDefault();
onPress?.(e);
}
}}
style={[styles.touchable, style]}
/>
);
}
: undefined
}
renderIcon={({ route, focused, color }) => {
const { options } = descriptors[route.key];
if (typeof options.tabBarIcon === 'string') {
return (
<MaterialCommunityIcons
name={options.tabBarIcon}
color={color}
size={24}
style={styles.icon}
/>
);
} }
renderScene={({ route }) => descriptors[route.key].render()}
renderIcon={({ route, focused, color }) => {
const { options } = descriptors[route.key];
if (typeof options.tabBarIcon === 'string') { if (typeof options.tabBarIcon === 'function') {
return ( return options.tabBarIcon({ focused, color });
<MaterialCommunityIcons
name={options.tabBarIcon}
color={color}
size={24}
style={styles.icon}
importantForAccessibility="no-hide-descendants"
accessibilityElementsHidden
/>
);
}
if (typeof options.tabBarIcon === 'function') {
return options.tabBarIcon({ focused, color });
}
return null;
}}
getLabelText={({ route }: Scene) => {
const { options } = descriptors[route.key];
return options.tabBarLabel !== undefined
? options.tabBarLabel
: options.title !== undefined
? options.title
: (route as Route<string>).name;
}}
getColor={({ route }) => descriptors[route.key].options.tabBarColor}
getBadge={({ route }) => descriptors[route.key].options.tabBarBadge}
getAccessibilityLabel={({ route }) =>
descriptors[route.key].options.tabBarAccessibilityLabel
} }
getTestID={({ route }) => descriptors[route.key].options.tabBarTestID}
onTabPress={({ route, preventDefault }) => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
if (event.defaultPrevented) { return null;
preventDefault(); }}
} getLabelText={({ route }: Scene) => {
}} const { options } = descriptors[route.key];
/>
return options.tabBarLabel !== undefined
? options.tabBarLabel
: options.title !== undefined
? options.title
: (route as Route<string>).name;
}}
getColor={({ route }) => descriptors[route.key].options.tabBarColor}
getBadge={({ route }) => descriptors[route.key].options.tabBarBadge}
getAccessibilityLabel={({ route }) =>
descriptors[route.key].options.tabBarAccessibilityLabel
}
getTestID={({ route }) => descriptors[route.key].options.tabBarTestID}
onTabPress={({ route, preventDefault }) => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
if (event.defaultPrevented) {
preventDefault();
}
}}
/>
);
}
export default function MaterialBottomTabView(props: Props) {
return (
<NavigationHelpersContext.Provider value={props.navigation}>
<MaterialBottomTabViewInner {...props} />
</NavigationHelpersContext.Provider> </NavigationHelpersContext.Provider>
); );
} }
@@ -115,4 +155,8 @@ const styles = StyleSheet.create({
icon: { icon: {
backgroundColor: 'transparent', backgroundColor: 'transparent',
}, },
touchable: {
display: 'flex',
justifyContent: 'center',
},
}); });

View File

@@ -3,6 +3,108 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.2.6](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.2.5...@react-navigation/material-top-tabs@5.2.6) (2020-05-20)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.2.5](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.2.4...@react-navigation/material-top-tabs@5.2.5) (2020-05-16)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.2.4](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.2.3...@react-navigation/material-top-tabs@5.2.4) (2020-05-14)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.2.3](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.2.2...@react-navigation/material-top-tabs@5.2.3) (2020-05-14)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.2.2](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.2.1...@react-navigation/material-top-tabs@5.2.2) (2020-05-10)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.2.1](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.2.0...@react-navigation/material-top-tabs@5.2.1) (2020-05-08)
### Bug Fixes
* fix building typescript definitions. closes [#8216](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/issues/8216) ([47a1229](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/commit/47a12298378747edd2d22e54dc1c8677f98c49b4))
# [5.2.0](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.1.15...@react-navigation/material-top-tabs@5.2.0) (2020-05-08)
### Features
* add generic type aliases for screen props ([bea14aa](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/commit/bea14aa26fd5cbfebc7973733c5cf1f44fd323aa)), closes [#7971](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/issues/7971)
## [5.1.15](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.1.14...@react-navigation/material-top-tabs@5.1.15) (2020-05-05)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.1.14](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.1.13...@react-navigation/material-top-tabs@5.1.14) (2020-05-01)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.1.13](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.1.12...@react-navigation/material-top-tabs@5.1.13) (2020-05-01)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.1.12](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.1.11...@react-navigation/material-top-tabs@5.1.12) (2020-04-30)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.1.11](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.1.10...@react-navigation/material-top-tabs@5.1.11) (2020-04-30)
**Note:** Version bump only for package @react-navigation/material-top-tabs
## [5.1.10](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.1.9...@react-navigation/material-top-tabs@5.1.10) (2020-04-27) ## [5.1.10](https://github.com/react-navigation/react-navigation/tree/master/packages/material-top-tabs/compare/@react-navigation/material-top-tabs@5.1.9...@react-navigation/material-top-tabs@5.1.10) (2020-04-27)
**Note:** Version bump only for package @react-navigation/material-top-tabs **Note:** Version bump only for package @react-navigation/material-top-tabs

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/material-top-tabs", "name": "@react-navigation/material-top-tabs",
"description": "Integration for the animated tab view component from react-native-tab-view", "description": "Integration for the animated tab view component from react-native-tab-view",
"version": "5.1.10", "version": "5.2.6",
"keywords": [ "keywords": [
"react-native-component", "react-native-component",
"react-component", "react-component",
@@ -20,6 +20,7 @@
"homepage": "https://reactnavigation.org/docs/material-top-tab-navigator.html", "homepage": "https://reactnavigation.org/docs/material-top-tab-navigator.html",
"main": "lib/commonjs/index.js", "main": "lib/commonjs/index.js",
"react-native": "src/index.tsx", "react-native": "src/index.tsx",
"source": "src/index.tsx",
"module": "lib/module/index.js", "module": "lib/module/index.js",
"types": "lib/typescript/src/index.d.ts", "types": "lib/typescript/src/index.d.ts",
"files": [ "files": [
@@ -38,15 +39,15 @@
"color": "^3.1.2" "color": "^3.1.2"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.10.0", "@react-native-community/bob": "^0.14.3",
"@react-navigation/native": "^5.1.7", "@react-navigation/native": "^5.4.1",
"@types/react": "^16.9.23", "@types/react": "^16.9.34",
"@types/react-native": "^0.61.22", "@types/react-native": "^0.62.7",
"del-cli": "^3.0.0", "del-cli": "^3.0.0",
"react": "~16.9.0", "react": "~16.9.0",
"react-native": "~0.61.5", "react-native": "~0.61.5",
"react-native-gesture-handler": "^1.6.0", "react-native-gesture-handler": "^1.6.0",
"react-native-reanimated": "^1.7.0", "react-native-reanimated": "^1.8.0",
"react-native-tab-view": "^2.14.0", "react-native-tab-view": "^2.14.0",
"typescript": "^3.8.3" "typescript": "^3.8.3"
}, },

View File

@@ -15,6 +15,7 @@ export { default as MaterialTopTabBar } from './views/MaterialTopTabBar';
export type { export type {
MaterialTopTabNavigationOptions, MaterialTopTabNavigationOptions,
MaterialTopTabNavigationProp, MaterialTopTabNavigationProp,
MaterialTopTabScreenProps,
MaterialTopTabBarProps, MaterialTopTabBarProps,
MaterialTopTabBarOptions, MaterialTopTabBarOptions,
} from './types'; } from './types';

View File

@@ -8,6 +8,7 @@ import {
NavigationProp, NavigationProp,
TabNavigationState, TabNavigationState,
TabActionHelpers, TabActionHelpers,
RouteProp,
} from '@react-navigation/native'; } from '@react-navigation/native';
export type MaterialTopTabNavigationEventMap = { export type MaterialTopTabNavigationEventMap = {
@@ -46,6 +47,14 @@ export type MaterialTopTabNavigationProp<
> & > &
TabActionHelpers<ParamList>; TabActionHelpers<ParamList>;
export type MaterialTopTabScreenProps<
ParamList extends ParamListBase,
RouteName extends keyof ParamList = string
> = {
navigation: MaterialTopTabNavigationProp<ParamList, RouteName>;
route: RouteProp<ParamList, RouteName>;
};
export type MaterialTopTabNavigationOptions = { export type MaterialTopTabNavigationOptions = {
/** /**
* Title text for the screen. * Title text for the screen.

View File

@@ -3,6 +3,143 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.4.1](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.4.0...@react-navigation/native@5.4.1) (2020-05-20)
**Note:** Version bump only for package @react-navigation/native
# [5.4.0](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.3.2...@react-navigation/native@5.4.0) (2020-05-16)
### Bug Fixes
* fix types for linking options ([d14f38b](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/d14f38b80ad569d5828c1919cea426c659173924))
### Features
* add a PathConfig type ([60cb3c9](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/60cb3c9ba76d7ef166c9fe8b55f23728975b5b6e))
## [5.3.2](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.3.1...@react-navigation/native@5.3.2) (2020-05-14)
**Note:** Version bump only for package @react-navigation/native
## [5.3.1](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.3.0...@react-navigation/native@5.3.1) (2020-05-14)
**Note:** Version bump only for package @react-navigation/native
# [5.3.0](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.2.6...@react-navigation/native@5.3.0) (2020-05-10)
### Features
* initialState should take priority over deep link ([039017b](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/039017bc2af69120d2d10e8f2c8a62919c37eb65))
## [5.2.6](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.2.5...@react-navigation/native@5.2.6) (2020-05-08)
### Bug Fixes
* fix building typescript definitions. closes [#8216](https://github.com/react-navigation/react-navigation/tree/master/packages/native/issues/8216) ([47a1229](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/47a12298378747edd2d22e54dc1c8677f98c49b4))
## [5.2.5](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.2.4...@react-navigation/native@5.2.5) (2020-05-08)
### Bug Fixes
* return a promise-like from getInitialState ([#8210](https://github.com/react-navigation/react-navigation/tree/master/packages/native/issues/8210)) ([85ae378](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/85ae378d8cb1073895b281e13ebccee881d4c062))
## [5.2.4](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.2.3...@react-navigation/native@5.2.4) (2020-05-05)
### Bug Fixes
* return undefined for buildLink if linking is not enabled ([9fd2635](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/9fd2635756362c8da79656b4d9b101bebaaf7003))
## [5.2.3](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.2.2...@react-navigation/native@5.2.3) (2020-05-01)
### Bug Fixes
* default linking enabled to true ([c7b8e2e](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/c7b8e2e9666733143eef156b27f3e4995c36b856))
## [5.2.2](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.2.1...@react-navigation/native@5.2.2) (2020-05-01)
### Bug Fixes
* don't throw when using 'useLinking'. fixes [#8171](https://github.com/react-navigation/react-navigation/tree/master/packages/native/issues/8171) ([10eca8b](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/10eca8b92edbce6dbef8abaf189e4b59a29b3748))
## [5.2.1](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.2.0...@react-navigation/native@5.2.1) (2020-04-30)
### Bug Fixes
* render fallback only if linking is enabled. closes [#8161](https://github.com/react-navigation/react-navigation/tree/master/packages/native/issues/8161) ([1c075ff](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/1c075ffb169d233ed0515efea264a5a69b4de52e))
# [5.2.0](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.1.7...@react-navigation/native@5.2.0) (2020-04-30)
### Bug Fixes
* add catch to thenable returned by getInitialState ([d6fa279](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/d6fa279d9371c7a6403d10d209a2a64147891c63))
* return onPress instead of onClick for useLinkProps ([ae5442e](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/ae5442ebe812b91fa1f12164f27d1aeed918ab0e))
### Features
* add `useLinkBuilder` hook to build links ([2792f43](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/2792f438fe45428fe193e3708fee7ad61966cbf4))
* add a useLinkProps hook ([f2291d1](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/f2291d110faa2aa8e10c9133c1c0c28d54af7917))
* add action prop to Link ([942d2be](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/942d2be2c72720469475ce12ec8df23825994dbf))
* add Link component as useLinkTo hook for navigating to links ([2573b5b](https://github.com/react-navigation/react-navigation/tree/master/packages/native/commit/2573b5beaac1240434e52f3f57bb29da2f541c88))
## [5.1.7](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.1.6...@react-navigation/native@5.1.7) (2020-04-27) ## [5.1.7](https://github.com/react-navigation/react-navigation/tree/master/packages/native/compare/@react-navigation/native@5.1.6...@react-navigation/native@5.1.7) (2020-04-27)
**Note:** Version bump only for package @react-navigation/native **Note:** Version bump only for package @react-navigation/native

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/native", "name": "@react-navigation/native",
"description": "React Native integration for React Navigation", "description": "React Native integration for React Navigation",
"version": "5.1.7", "version": "5.4.1",
"keywords": [ "keywords": [
"react-native", "react-native",
"react-navigation", "react-navigation",
@@ -16,6 +16,7 @@
"homepage": "https://reactnavigation.org", "homepage": "https://reactnavigation.org",
"main": "lib/commonjs/index.js", "main": "lib/commonjs/index.js",
"react-native": "src/index.tsx", "react-native": "src/index.tsx",
"source": "src/index.tsx",
"module": "lib/module/index.js", "module": "lib/module/index.js",
"types": "lib/typescript/src/index.d.ts", "types": "lib/typescript/src/index.d.ts",
"files": [ "files": [
@@ -31,16 +32,16 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"@react-navigation/core": "^5.3.5" "@react-navigation/core": "^5.8.0"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.10.0", "@react-native-community/bob": "^0.14.3",
"@types/react": "^16.9.23", "@types/react": "^16.9.34",
"@types/react-native": "^0.61.22", "@types/react-native": "^0.62.7",
"del-cli": "^3.0.0", "del-cli": "^3.0.0",
"react": "~16.9.0", "react": "~16.9.0",
"react-native": "~0.61.5", "react-native": "~0.61.5",
"react-native-testing-library": "^1.12.0", "react-native-testing-library": "^1.13.2",
"typescript": "^3.8.3" "typescript": "^3.8.3"
}, },
"peerDependencies": { "peerDependencies": {

View File

@@ -1,5 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { Text, TextProps, GestureResponderEvent } from 'react-native'; import { Text, TextProps, GestureResponderEvent, Platform } from 'react-native';
import { NavigationAction } from '@react-navigation/core'; import { NavigationAction } from '@react-navigation/core';
import useLinkProps from './useLinkProps'; import useLinkProps from './useLinkProps';
@@ -28,16 +28,15 @@ export default function Link({ to, action, ...rest }: Props) {
rest.onPress?.(e); rest.onPress?.(e);
} }
if (props.onClick) { props.onPress(e);
props.onClick(e);
} else {
props.onPress(e);
}
}; };
return React.createElement(Text, { return React.createElement(Text, {
...props, ...props,
...rest, ...rest,
...(props.onClick ? { onClick: onPress } : { onPress }), ...Platform.select({
web: { onClick: onPress } as any,
default: { onPress },
}),
}); });
} }

View File

@@ -22,7 +22,7 @@ type Props = NavigationContainerProps & {
* Container component which holds the navigation state designed for React Native apps. * Container component which holds the navigation state designed for React Native apps.
* This should be rendered at the root wrapping the whole app. * This should be rendered at the root wrapping the whole app.
* *
* @param props.initialState Initial state object for the navigation tree. When deep link handling is enabled, this will be ignored if there's an incoming link. * @param props.initialState Initial state object for the navigation tree. When deep link handling is enabled, this will override deep links when specified. Make sure that you don't specify an `initialState` when there's a deep link (`Linking.getInitialURL()`).
* @param props.onStateChange Callback which is called with the latest navigation state when it changes. * @param props.onStateChange Callback which is called with the latest navigation state when it changes.
* @param props.theme Theme object for the navigators. * @param props.theme Theme object for the navigators.
* @param props.linking Options for deep linking. Deep link handling is enabled when this prop is provided, unless `linking.enabled` is `false`. * @param props.linking Options for deep linking. Deep link handling is enabled when this prop is provided, unless `linking.enabled` is `false`.
@@ -46,15 +46,13 @@ const NavigationContainer = React.forwardRef(function NavigationContainer(
...linking, ...linking,
}); });
const [isReady, initialState = rest.initialState] = useThenable( const [isReady, initialState] = useThenable(getInitialState);
getInitialState
);
React.useImperativeHandle(ref, () => refContainer.current); React.useImperativeHandle(ref, () => refContainer.current);
const linkingContext = React.useMemo(() => ({ options: linking }), [linking]); const linkingContext = React.useMemo(() => ({ options: linking }), [linking]);
if (!isReady) { if (rest.initialState == null && isLinkingEnabled && !isReady) {
// This is temporary until we have Suspense for data-fetching // This is temporary until we have Suspense for data-fetching
// Then the fallback will be handled by a parent `Suspense` component // Then the fallback will be handled by a parent `Suspense` component
return fallback as React.ReactElement; return fallback as React.ReactElement;
@@ -65,7 +63,9 @@ const NavigationContainer = React.forwardRef(function NavigationContainer(
<ThemeProvider value={theme}> <ThemeProvider value={theme}>
<BaseNavigationContainer <BaseNavigationContainer
{...rest} {...rest}
initialState={initialState} initialState={
rest.initialState == null ? initialState : rest.initialState
}
ref={refContainer} ref={refContainer}
/> />
</ThemeProvider> </ThemeProvider>

View File

@@ -17,7 +17,7 @@ it('throws if multiple instances of useLinking are used', () => {
let element; let element;
expect(() => (element = render(<Sample />))).toThrowError( expect(() => (element = render(<Sample />))).toThrowError(
"Looks like you are using 'useLinking' in multiple components." 'Looks like you have configured linking in multiple places.'
); );
// @ts-ignore // @ts-ignore
@@ -41,9 +41,7 @@ it('throws if multiple instances of useLinking are used', () => {
<B /> <B />
</> </>
)) ))
).toThrowError( ).toThrowError('Looks like you have configured linking in multiple places.');
"Looks like you are using 'useLinking' in multiple components."
);
// @ts-ignore // @ts-ignore
element?.unmount(); element?.unmount();
@@ -57,5 +55,19 @@ it('throws if multiple instances of useLinking are used', () => {
render(wrapper2).unmount(); render(wrapper2).unmount();
expect(() => render(wrapper2)).not.toThrow(); expect(() => (element = render(wrapper2))).not.toThrow();
// @ts-ignore
element?.unmount();
function Sample3() {
useLinking(ref, options);
useLinking(ref, { ...options, enabled: false });
return null;
}
expect(() => (element = render(<Sample3 />))).not.toThrowError();
// @ts-ignore
element?.unmount();
}); });

View File

@@ -1,6 +1,7 @@
import { import {
getStateFromPath as getStateFromPathDefault, getStateFromPath as getStateFromPathDefault,
getPathFromState as getPathFromStateDefault, getPathFromState as getPathFromStateDefault,
PathConfig,
} from '@react-navigation/core'; } from '@react-navigation/core';
export type Theme = { export type Theme = {
@@ -39,7 +40,7 @@ export type LinkingOptions = {
* } * }
* ``` * ```
*/ */
config?: Parameters<typeof getStateFromPathDefault>[1]; config?: PathConfig;
/** /**
* Custom function to parse the URL to a valid navigation state (advanced). * Custom function to parse the URL to a valid navigation state (advanced).
* Only applicable on Web. * Only applicable on Web.

View File

@@ -52,14 +52,18 @@ export default function useLinkBuilder() {
(name: string, params?: object) => { (name: string, params?: object) => {
const { options } = linking; const { options } = linking;
// If we couldn't find a navigation object in context, we're at root if (options?.enabled === false) {
// So we'll construct a basic state object to use return undefined;
}
const state = navigation const state = navigation
? getRootStateForNavigate(navigation, { ? getRootStateForNavigate(navigation, {
index: 0, index: 0,
routes: [{ name, params }], routes: [{ name, params }],
}) })
: { : // If we couldn't find a navigation object in context, we're at root
// So we'll construct a basic state object to use
{
index: 0, index: 0,
routes: [{ name, params }], routes: [{ name, params }],
}; };

View File

@@ -49,6 +49,14 @@ export default function useLinkProps({ to, action }: Props) {
throw new Error("Couldn't find a navigation object."); throw new Error("Couldn't find a navigation object.");
} }
} else { } else {
if (typeof to !== 'string') {
throw new Error(
`To 'to' option is invalid (found '${String(
to
)}'. It must be a valid string for navigation.`
);
}
linkTo(to); linkTo(to);
} }
} }
@@ -57,9 +65,6 @@ export default function useLinkProps({ to, action }: Props) {
return { return {
href: to, href: to,
accessibilityRole: 'link' as const, accessibilityRole: 'link' as const,
...Platform.select({ onPress,
web: { onClick: onPress } as any,
default: { onPress },
}),
}; };
} }

View File

@@ -12,22 +12,28 @@ let isUsingLinking = false;
export default function useLinking( export default function useLinking(
ref: React.RefObject<NavigationContainerRef>, ref: React.RefObject<NavigationContainerRef>,
{ {
enabled, enabled = true,
prefixes, prefixes,
config, config,
getStateFromPath = getStateFromPathDefault, getStateFromPath = getStateFromPathDefault,
}: LinkingOptions }: LinkingOptions
) { ) {
React.useEffect(() => { React.useEffect(() => {
if (isUsingLinking) { if (enabled !== false && isUsingLinking) {
throw new Error( throw new Error(
"Looks like you are using 'useLinking' in multiple components. This is likely an error since deep links should only be handled in one place to avoid conflicts." + [
(Platform.OS === 'android' 'Looks like you have configured linking in multiple places. This is likely an error since deep links should only be handled in one place to avoid conflicts. Make sure that:',
? "\n\nIf you're not using it in multiple components, ensure that you have set 'android:launchMode=singleTask' in the '<activity />' section of the 'AndroidManifest.xml' file to avoid launching multiple activities which run multiple instances of the root component." "- You are not using both 'linking' prop and 'useLinking'",
: '') "- You don't have 'useLinking' in multiple components",
Platform.OS === 'android'
? "- You have set 'android:launchMode=singleTask' in the '<activity />' section of the 'AndroidManifest.xml' file to avoid launching multiple instances"
: '',
]
.join('\n')
.trim()
); );
} else { } else {
isUsingLinking = true; isUsingLinking = enabled !== false;
} }
return () => { return () => {

View File

@@ -10,6 +10,15 @@ import { LinkingOptions } from './types';
type ResultState = ReturnType<typeof getStateFromPathDefault>; type ResultState = ReturnType<typeof getStateFromPathDefault>;
type HistoryState = { index: number };
declare const history: {
state?: HistoryState;
go(delta: number): void;
pushState(state: HistoryState, title: string, url: string): void;
replaceState(state: HistoryState, title: string, url: string): void;
};
const getStateLength = (state: NavigationState) => { const getStateLength = (state: NavigationState) => {
let length = 0; let length = 0;
@@ -34,19 +43,25 @@ let isUsingLinking = false;
export default function useLinking( export default function useLinking(
ref: React.RefObject<NavigationContainerRef>, ref: React.RefObject<NavigationContainerRef>,
{ {
enabled, enabled = true,
config, config,
getStateFromPath = getStateFromPathDefault, getStateFromPath = getStateFromPathDefault,
getPathFromState = getPathFromStateDefault, getPathFromState = getPathFromStateDefault,
}: LinkingOptions }: LinkingOptions
) { ) {
React.useEffect(() => { React.useEffect(() => {
if (isUsingLinking) { if (enabled !== false && isUsingLinking) {
throw new Error( throw new Error(
"Looks like you are using 'useLinking' in multiple components. This is likely an error since URL integration should only be handled in one place to avoid conflicts. Also ensure that you set your android activity launchMode to single task in your AndroiManifest.xml file." [
'Looks like you have configured linking in multiple places. This is likely an error since URL integration should only be handled in one place to avoid conflicts. Make sure that:',
"- You are not using both 'linking' prop and 'useLinking'",
"- You don't have 'useLinking' in multiple components",
]
.join('\n')
.trim()
); );
} else { } else {
isUsingLinking = true; isUsingLinking = enabled !== false;
} }
return () => { return () => {
@@ -81,10 +96,16 @@ export default function useLinking(
} }
// Make it a thenable to keep consistent with the native impl // Make it a thenable to keep consistent with the native impl
return { const thenable = {
then: (callback: (state: ResultState | undefined) => void) => then(onfulfilled?: (state: ResultState | undefined) => void) {
callback(value), return Promise.resolve(onfulfilled ? onfulfilled(value) : value);
},
catch() {
return thenable;
},
}; };
return thenable as PromiseLike<ResultState | undefined>;
}, []); }, []);
const previousStateLengthRef = React.useRef<number | undefined>(undefined); const previousStateLengthRef = React.useRef<number | undefined>(undefined);

View File

@@ -1,10 +1,6 @@
import * as React from 'react'; import * as React from 'react';
export default function useThenable<T>( export default function useThenable<T>(create: () => PromiseLike<T>) {
create: () => {
then(success: (result: T) => void, error?: (error: any) => void): void;
}
) {
const [promise] = React.useState(create); const [promise] = React.useState(create);
// Check if our thenable is synchronous // Check if our thenable is synchronous
@@ -24,20 +20,20 @@ export default function useThenable<T>(
React.useEffect(() => { React.useEffect(() => {
let cancelled = false; let cancelled = false;
if (!resolved) { const resolve = async () => {
promise.then( let result;
(result) => {
if (!cancelled) { try {
setState([true, result]); result = await promise;
} } finally {
}, if (!cancelled) {
(error) => { setState([true, result]);
if (!cancelled) {
console.error(error);
setState([true, undefined]);
}
} }
); }
};
if (!resolved) {
resolve();
} }
return () => { return () => {

View File

@@ -3,6 +3,44 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.4.5](https://github.com/react-navigation/react-navigation/tree/master/packages/routers/compare/@react-navigation/routers@5.4.4...@react-navigation/routers@5.4.5) (2020-05-20)
**Note:** Version bump only for package @react-navigation/routers
## [5.4.4](https://github.com/react-navigation/react-navigation/tree/master/packages/routers/compare/@react-navigation/routers@5.4.3...@react-navigation/routers@5.4.4) (2020-05-08)
### Bug Fixes
* fix building typescript definitions. closes [#8216](https://github.com/react-navigation/react-navigation/tree/master/packages/routers/issues/8216) ([47a1229](https://github.com/react-navigation/react-navigation/tree/master/packages/routers/commit/47a12298378747edd2d22e54dc1c8677f98c49b4))
## [5.4.3](https://github.com/react-navigation/react-navigation/tree/master/packages/routers/compare/@react-navigation/routers@5.4.2...@react-navigation/routers@5.4.3) (2020-05-08)
**Note:** Version bump only for package @react-navigation/routers
## [5.4.2](https://github.com/react-navigation/react-navigation/tree/master/packages/routers/compare/@react-navigation/routers@5.4.1...@react-navigation/routers@5.4.2) (2020-04-30)
### Bug Fixes
* fix backBehavior with initialRoute ([#8110](https://github.com/react-navigation/react-navigation/tree/master/packages/routers/issues/8110)) ([420f692](https://github.com/react-navigation/react-navigation/tree/master/packages/routers/commit/420f6926e111d32c2388c44ff0bee2b8ea238a57))
## [5.4.1](https://github.com/react-navigation/react-navigation/tree/master/packages/routers/compare/@react-navigation/routers@5.4.0...@react-navigation/routers@5.4.1) (2020-04-27) ## [5.4.1](https://github.com/react-navigation/react-navigation/tree/master/packages/routers/compare/@react-navigation/routers@5.4.0...@react-navigation/routers@5.4.1) (2020-04-27)

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/routers", "name": "@react-navigation/routers",
"description": "Routers to help build custom navigators", "description": "Routers to help build custom navigators",
"version": "5.4.1", "version": "5.4.5",
"keywords": [ "keywords": [
"react", "react",
"react-native", "react-native",
@@ -15,6 +15,7 @@
"homepage": "https://reactnavigation.org/docs/custom-routers.html", "homepage": "https://reactnavigation.org/docs/custom-routers.html",
"main": "lib/commonjs/index.js", "main": "lib/commonjs/index.js",
"react-native": "src/index.tsx", "react-native": "src/index.tsx",
"source": "src/index.tsx",
"module": "lib/module/index.js", "module": "lib/module/index.js",
"types": "lib/typescript/src/index.d.ts", "types": "lib/typescript/src/index.d.ts",
"files": [ "files": [
@@ -30,10 +31,10 @@
"clean": "del lib" "clean": "del lib"
}, },
"dependencies": { "dependencies": {
"nanoid": "^3.0.2" "nanoid": "^3.1.5"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.10.0", "@react-native-community/bob": "^0.14.3",
"del-cli": "^3.0.0", "del-cli": "^3.0.0",
"typescript": "^3.8.3" "typescript": "^3.8.3"
}, },

View File

@@ -59,21 +59,31 @@ export const TabActions = {
const getRouteHistory = ( const getRouteHistory = (
routes: Route<string>[], routes: Route<string>[],
index: number, index: number,
backBehavior: BackBehavior backBehavior: BackBehavior,
initialRouteName: string | undefined
) => { ) => {
const history = [{ type: TYPE_ROUTE, key: routes[index].key }]; const history = [{ type: TYPE_ROUTE, key: routes[index].key }];
let initialRouteIndex;
switch (backBehavior) { switch (backBehavior) {
case 'initialRoute':
if (index !== 0) {
history.unshift({ type: TYPE_ROUTE, key: routes[0].key });
}
break;
case 'order': case 'order':
for (let i = index; i > 0; i--) { for (let i = index; i > 0; i--) {
history.unshift({ type: TYPE_ROUTE, key: routes[i - 1].key }); history.unshift({ type: TYPE_ROUTE, key: routes[i - 1].key });
} }
break; break;
case 'initialRoute':
initialRouteIndex = routes.findIndex(
(route) => route.name === initialRouteName
);
initialRouteIndex = initialRouteIndex === -1 ? 0 : initialRouteIndex;
if (initialRouteIndex !== index) {
history.unshift({
type: TYPE_ROUTE,
key: routes[initialRouteIndex].key,
});
}
break;
case 'history': case 'history':
// The history will fill up on navigation // The history will fill up on navigation
break; break;
@@ -85,7 +95,8 @@ const getRouteHistory = (
const changeIndex = ( const changeIndex = (
state: TabNavigationState, state: TabNavigationState,
index: number, index: number,
backBehavior: BackBehavior backBehavior: BackBehavior,
initialRouteName: string | undefined
) => { ) => {
let history; let history;
@@ -96,7 +107,12 @@ const changeIndex = (
.filter((it) => (it.type === 'route' ? it.key !== currentKey : false)) .filter((it) => (it.type === 'route' ? it.key !== currentKey : false))
.concat({ type: TYPE_ROUTE, key: currentKey }); .concat({ type: TYPE_ROUTE, key: currentKey });
} else { } else {
history = getRouteHistory(state.routes, index, backBehavior); history = getRouteHistory(
state.routes,
index,
backBehavior,
initialRouteName
);
} }
return { return {
@@ -130,7 +146,12 @@ export default function TabRouter({
params: routeParamList[name], params: routeParamList[name],
})); }));
const history = getRouteHistory(routes, index, backBehavior); const history = getRouteHistory(
routes,
index,
backBehavior,
initialRouteName
);
return { return {
stale: false, stale: false,
@@ -189,7 +210,12 @@ export default function TabRouter({
); );
if (!history?.length) { if (!history?.length) {
history = getRouteHistory(routes, index, backBehavior); history = getRouteHistory(
routes,
index,
backBehavior,
initialRouteName
);
} }
return { return {
@@ -223,7 +249,12 @@ export default function TabRouter({
); );
if (!history.length) { if (!history.length) {
history = getRouteHistory(routes, index, backBehavior); history = getRouteHistory(
routes,
index,
backBehavior,
initialRouteName
);
} }
return { return {
@@ -242,7 +273,7 @@ export default function TabRouter({
return state; return state;
} }
return changeIndex(state, index, backBehavior); return changeIndex(state, index, backBehavior, initialRouteName);
}, },
getStateForAction(state, action) { getStateForAction(state, action) {
@@ -284,7 +315,8 @@ export default function TabRouter({
: state.routes, : state.routes,
}, },
index, index,
backBehavior backBehavior,
initialRouteName
); );
} }

View File

@@ -882,6 +882,78 @@ it('handles back action with backBehavior: initialRoute', () => {
).toEqual(null); ).toEqual(null);
}); });
it('handles back action with backBehavior: initialRoute and initialRouteName', () => {
const router = TabRouter({
backBehavior: 'initialRoute',
initialRouteName: 'baz',
});
const options = {
routeNames: ['bar', 'baz', 'qux'],
routeParamList: {},
};
let state = router.getInitialState(options);
expect(
router.getStateForAction(state, CommonActions.goBack(), options)
).toEqual(null);
state = router.getStateForAction(
state,
TabActions.jumpTo('qux'),
options
) as TabNavigationState;
expect(
router.getStateForAction(state, CommonActions.goBack(), options)
).toEqual({
stale: false,
type: 'tab',
key: 'tab-test',
index: 1,
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-test', name: 'bar' },
{ key: 'baz-test', name: 'baz' },
{ key: 'qux-test', name: 'qux' },
],
history: [{ type: 'route', key: 'baz-test' }],
});
state = router.getStateForAction(
state,
TabActions.jumpTo('bar'),
options
) as TabNavigationState;
expect(
router.getStateForAction(state, CommonActions.goBack(), options)
).toEqual({
stale: false,
type: 'tab',
key: 'tab-test',
index: 1,
routeNames: ['bar', 'baz', 'qux'],
routes: [
{ key: 'bar-test', name: 'bar' },
{ key: 'baz-test', name: 'baz' },
{ key: 'qux-test', name: 'qux' },
],
history: [{ type: 'route', key: 'baz-test' }],
});
state = router.getStateForAction(
state,
TabActions.jumpTo('baz'),
options
) as TabNavigationState;
expect(
router.getStateForAction(state, CommonActions.goBack(), options)
).toEqual(null);
});
it('handles back action with backBehavior: none', () => { it('handles back action with backBehavior: none', () => {
const router = TabRouter({ backBehavior: 'none' }); const router = TabRouter({ backBehavior: 'none' });
const options = { const options = {

View File

@@ -3,6 +3,139 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [5.3.8](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.3.7...@react-navigation/stack@5.3.8) (2020-05-20)
**Note:** Version bump only for package @react-navigation/stack
## [5.3.7](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.3.6...@react-navigation/stack@5.3.7) (2020-05-16)
**Note:** Version bump only for package @react-navigation/stack
## [5.3.6](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.3.5...@react-navigation/stack@5.3.6) (2020-05-15)
### Bug Fixes
* reduce header title margin. fixes [#8267](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/issues/8267) ([d45dbe9](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/commit/d45dbe97dc6625c6a8e80b5e658ab5a95045e5e8))
## [5.3.5](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.3.4...@react-navigation/stack@5.3.5) (2020-05-14)
**Note:** Version bump only for package @react-navigation/stack
## [5.3.4](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.3.3...@react-navigation/stack@5.3.4) (2020-05-14)
**Note:** Version bump only for package @react-navigation/stack
## [5.3.3](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.3.2...@react-navigation/stack@5.3.3) (2020-05-11)
### Bug Fixes
* fix ios transitionspec settle time ([#8028](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/issues/8028)) ([dd7cff2](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/commit/dd7cff201608365a80f1d50a006df3e0d18e94a1))
## [5.3.2](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.3.1...@react-navigation/stack@5.3.2) (2020-05-10)
**Note:** Version bump only for package @react-navigation/stack
## [5.3.1](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.3.0...@react-navigation/stack@5.3.1) (2020-05-08)
### Bug Fixes
* fix building typescript definitions. closes [#8216](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/issues/8216) ([47a1229](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/commit/47a12298378747edd2d22e54dc1c8677f98c49b4))
# [5.3.0](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.2.19...@react-navigation/stack@5.3.0) (2020-05-08)
### Bug Fixes
* add proper margins to the header title ([f07cd13](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/commit/f07cd135619d635e8841aa0df0b6e687636e7408))
* include safe are insets in title's margins ([4d1e102](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/commit/4d1e102f8c3ffab116d0195fbab3086f6345a077))
### Features
* add generic type aliases for screen props ([bea14aa](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/commit/bea14aa26fd5cbfebc7973733c5cf1f44fd323aa)), closes [#7971](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/issues/7971)
## [5.2.19](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.2.18...@react-navigation/stack@5.2.19) (2020-05-05)
**Note:** Version bump only for package @react-navigation/stack
## [5.2.18](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.2.17...@react-navigation/stack@5.2.18) (2020-05-01)
**Note:** Version bump only for package @react-navigation/stack
## [5.2.17](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.2.16...@react-navigation/stack@5.2.17) (2020-05-01)
**Note:** Version bump only for package @react-navigation/stack
## [5.2.16](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.2.15...@react-navigation/stack@5.2.16) (2020-04-30)
**Note:** Version bump only for package @react-navigation/stack
## [5.2.15](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.2.14...@react-navigation/stack@5.2.15) (2020-04-30)
### Bug Fixes
* make sure the address bar hides when scrolling on web ([0a19e94](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/commit/0a19e94b23a4d2b5f22d1d9deb0544f586f475ee))
## [5.2.14](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.2.13...@react-navigation/stack@5.2.14) (2020-04-27) ## [5.2.14](https://github.com/react-navigation/react-navigation/tree/master/packages/stack/compare/@react-navigation/stack@5.2.13...@react-navigation/stack@5.2.14) (2020-04-27)

View File

@@ -1,7 +1,7 @@
{ {
"name": "@react-navigation/stack", "name": "@react-navigation/stack",
"description": "Stack navigator component for iOS and Android with animated transitions and gestures", "description": "Stack navigator component for iOS and Android with animated transitions and gestures",
"version": "5.2.14", "version": "5.3.8",
"keywords": [ "keywords": [
"react-native-component", "react-native-component",
"react-component", "react-component",
@@ -19,6 +19,7 @@
"homepage": "https://reactnavigation.org/docs/stack-navigator.html", "homepage": "https://reactnavigation.org/docs/stack-navigator.html",
"main": "lib/commonjs/index.js", "main": "lib/commonjs/index.js",
"react-native": "src/index.tsx", "react-native": "src/index.tsx",
"source": "src/index.tsx",
"module": "lib/module/index.js", "module": "lib/module/index.js",
"types": "lib/typescript/src/index.d.ts", "types": "lib/typescript/src/index.d.ts",
"files": [ "files": [
@@ -38,18 +39,18 @@
"react-native-iphone-x-helper": "^1.2.1" "react-native-iphone-x-helper": "^1.2.1"
}, },
"devDependencies": { "devDependencies": {
"@react-native-community/bob": "^0.10.0", "@react-native-community/bob": "^0.14.3",
"@react-native-community/masked-view": "^0.1.7", "@react-native-community/masked-view": "^0.1.10",
"@react-navigation/native": "^5.1.7", "@react-navigation/native": "^5.4.1",
"@types/color": "^3.0.1", "@types/color": "^3.0.1",
"@types/react": "^16.9.23", "@types/react": "^16.9.34",
"@types/react-native": "^0.61.22", "@types/react-native": "^0.62.7",
"del-cli": "^3.0.0", "del-cli": "^3.0.0",
"react": "~16.9.0", "react": "~16.9.0",
"react-native": "~0.61.5", "react-native": "~0.61.5",
"react-native-gesture-handler": "^1.6.0", "react-native-gesture-handler": "^1.6.0",
"react-native-safe-area-context": "^0.7.3", "react-native-safe-area-context": "^0.7.3",
"react-native-screens": "^2.3.0", "react-native-screens": "^2.7.0",
"typescript": "^3.8.3" "typescript": "^3.8.3"
}, },
"peerDependencies": { "peerDependencies": {

View File

@@ -11,8 +11,8 @@ export const TransitionIOSSpec: TransitionSpec = {
damping: 500, damping: 500,
mass: 3, mass: 3,
overshootClamping: true, overshootClamping: true,
restDisplacementThreshold: 0.01, restDisplacementThreshold: 10,
restSpeedThreshold: 0.01, restSpeedThreshold: 10,
}, },
}; };

View File

@@ -51,6 +51,7 @@ export { default as useGestureHandlerRef } from './utils/useGestureHandlerRef';
export type { export type {
StackNavigationOptions, StackNavigationOptions,
StackNavigationProp, StackNavigationProp,
StackScreenProps,
StackHeaderProps, StackHeaderProps,
StackHeaderLeftButtonProps, StackHeaderLeftButtonProps,
StackHeaderTitleProps, StackHeaderTitleProps,

View File

@@ -15,6 +15,7 @@ import {
NavigationHelpers, NavigationHelpers,
StackNavigationState, StackNavigationState,
StackActionHelpers, StackActionHelpers,
RouteProp,
} from '@react-navigation/native'; } from '@react-navigation/native';
export type StackNavigationEventMap = { export type StackNavigationEventMap = {
@@ -45,6 +46,14 @@ export type StackNavigationProp<
> & > &
StackActionHelpers<ParamList>; StackActionHelpers<ParamList>;
export type StackScreenProps<
ParamList extends ParamListBase,
RouteName extends keyof ParamList = string
> = {
navigation: StackNavigationProp<ParamList, RouteName>;
route: RouteProp<ParamList, RouteName>;
};
export type Layout = { width: number; height: number }; export type Layout = { width: number; height: number };
export type GestureDirection = export type GestureDirection =
@@ -276,7 +285,8 @@ export type StackNavigationOptions = StackHeaderOptions &
cardStyle?: StyleProp<ViewStyle>; cardStyle?: StyleProp<ViewStyle>;
/** /**
* Whether transition animation should be enabled the screen. * Whether transition animation should be enabled the screen.
* If you set it to `false`, the screen won't animate when pushing or popping. Defaults to `true`. * If you set it to `false`, the screen won't animate when pushing or popping.
* Defaults to `true` on Android and iOS, `false` on Web.
*/ */
animationEnabled?: boolean; animationEnabled?: boolean;
/** /**
@@ -286,10 +296,12 @@ export type StackNavigationOptions = StackHeaderOptions &
animationTypeForReplace?: 'push' | 'pop'; animationTypeForReplace?: 'push' | 'pop';
/** /**
* Whether you can use gestures to dismiss this screen. Defaults to `true` on iOS, `false` on Android. * Whether you can use gestures to dismiss this screen. Defaults to `true` on iOS, `false` on Android.
* Not supported on Web.
*/ */
gestureEnabled?: boolean; gestureEnabled?: boolean;
/** /**
* Object to override the distance of touch start from the edge of the screen to recognize gestures. * Object to override the distance of touch start from the edge of the screen to recognize gestures.
* Not supported on Web.
*/ */
gestureResponseDistance?: { gestureResponseDistance?: {
/** /**
@@ -302,8 +314,8 @@ export type StackNavigationOptions = StackHeaderOptions &
horizontal?: number; horizontal?: number;
}; };
/** /**
* Number which determines the relevance of velocity for the gesture. * Number which determines the relevance of velocity for the gesture. Defaults to 0.3.
* Defaults to 0.3. * Not supported on Web.
*/ */
gestureVelocityImpact?: number; gestureVelocityImpact?: number;
/** /**

View File

@@ -1,6 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { PanGestureHandler } from 'react-native-gesture-handler';
export default React.createContext<React.Ref<PanGestureHandler> | undefined>( export default React.createContext<React.Ref<
undefined import('react-native-gesture-handler').PanGestureHandler
); > | null>(null);

View File

@@ -0,0 +1,22 @@
import * as React from 'react';
import {
PanGestureHandler as PanGestureHandlerNative,
PanGestureHandlerProperties,
} from 'react-native-gesture-handler';
import GestureHandlerRefContext from '../utils/GestureHandlerRefContext';
export function PanGestureHandler(props: PanGestureHandlerProperties) {
const gestureRef = React.useRef<PanGestureHandlerNative>(null);
return (
<GestureHandlerRefContext.Provider value={gestureRef}>
<PanGestureHandlerNative {...props} />
</GestureHandlerRefContext.Provider>
);
}
export {
GestureHandlerRootView,
State as GestureState,
PanGestureHandlerGestureEvent,
} from 'react-native-gesture-handler';

View File

@@ -0,0 +1,24 @@
import * as React from 'react';
import { View } from 'react-native';
import type { PanGestureHandlerProperties } from 'react-native-gesture-handler';
const Dummy: any = ({ children }: { children: React.ReactNode }) => (
<>{children}</>
);
export const PanGestureHandler = Dummy as React.ComponentType<
PanGestureHandlerProperties
>;
export const GestureHandlerRootView = View;
export const GestureState = {
UNDETERMINED: 0,
FAILED: 1,
BEGAN: 2,
CANCELLED: 3,
ACTIVE: 4,
END: 5,
};
export type { PanGestureHandlerGestureEvent } from 'react-native-gesture-handler';

View File

@@ -41,6 +41,7 @@ export default React.memo(function Header(props: StackHeaderProps) {
: previous.route.name; : previous.route.name;
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
const goBack = React.useCallback( const goBack = React.useCallback(
debounce(() => { debounce(() => {
if (navigation.isFocused() && navigation.canGoBack()) { if (navigation.isFocused() && navigation.canGoBack()) {

View File

@@ -10,7 +10,7 @@ import {
} from 'react-native'; } from 'react-native';
import { useTheme } from '@react-navigation/native'; import { useTheme } from '@react-navigation/native';
import MaskedView from '../MaskedView'; import MaskedView from '../MaskedView';
import TouchableItem from '../TouchableItem'; import { TouchableItem } from '../TouchableItem';
import { StackHeaderLeftButtonProps } from '../../types'; import { StackHeaderLeftButtonProps } from '../../types';
type Props = StackHeaderLeftButtonProps; type Props = StackHeaderLeftButtonProps;

View File

@@ -307,6 +307,8 @@ export default class HeaderSegment extends React.Component<Props, State> {
}) })
: null; : null;
const rightButton = right ? right({ tintColor: headerTintColor }) : null;
return ( return (
<React.Fragment> <React.Fragment>
<Animated.View <Animated.View
@@ -345,8 +347,19 @@ export default class HeaderSegment extends React.Component<Props, State> {
pointerEvents="box-none" pointerEvents="box-none"
style={[ style={[
headerTitleAlign === 'left' headerTitleAlign === 'left'
? { position: 'absolute', left: leftButton ? 72 : 16 } ? {
: { marginHorizontal: 18 }, position: 'absolute',
left: (leftButton ? 72 : 16) + insets.left,
right: (rightButton ? 72 : 16) + insets.right,
}
: {
marginHorizontal:
(leftButton ? 32 : 16) +
(leftButton && headerBackTitleVisible !== false
? 40
: 0) +
Math.max(insets.left, insets.right),
},
titleStyle, titleStyle,
titleContainerStyle, titleContainerStyle,
]} ]}
@@ -359,7 +372,7 @@ export default class HeaderSegment extends React.Component<Props, State> {
style: customTitleStyle, style: customTitleStyle,
})} })}
</Animated.View> </Animated.View>
{right ? ( {rightButton ? (
<Animated.View <Animated.View
pointerEvents="box-none" pointerEvents="box-none"
style={[ style={[
@@ -369,7 +382,7 @@ export default class HeaderSegment extends React.Component<Props, State> {
rightContainerStyle, rightContainerStyle,
]} ]}
> >
{right({ tintColor: headerTintColor })} {rightButton}
</Animated.View> </Animated.View>
) : null} ) : null}
</View> </View>

View File

@@ -2,7 +2,7 @@ import * as React from 'react';
import { Animated, StyleSheet, Platform } from 'react-native'; import { Animated, StyleSheet, Platform } from 'react-native';
import { useTheme } from '@react-navigation/native'; import { useTheme } from '@react-navigation/native';
type Props = React.ComponentProps<typeof Animated.Text> & { type Props = Omit<React.ComponentProps<typeof Animated.Text>, 'key'> & {
tintColor?: string; tintColor?: string;
children?: string; children?: string;
}; };

View File

@@ -9,14 +9,15 @@ import {
Platform, Platform,
InteractionManager, InteractionManager,
} from 'react-native'; } from 'react-native';
import {
PanGestureHandler,
State as GestureState,
PanGestureHandlerGestureEvent,
} from 'react-native-gesture-handler';
import { EdgeInsets } from 'react-native-safe-area-context'; import { EdgeInsets } from 'react-native-safe-area-context';
import Color from 'color'; import Color from 'color';
import StackGestureRefContext from '../../utils/GestureHandlerRefContext';
import CardSheet from './CardSheet';
import {
PanGestureHandler,
GestureState,
PanGestureHandlerGestureEvent,
} from '../GestureHandler';
import CardAnimationContext from '../../utils/CardAnimationContext'; import CardAnimationContext from '../../utils/CardAnimationContext';
import getDistanceForDirection from '../../utils/getDistanceForDirection'; import getDistanceForDirection from '../../utils/getDistanceForDirection';
import getInvertedMultiplier from '../../utils/getInvertedMultiplier'; import getInvertedMultiplier from '../../utils/getInvertedMultiplier';
@@ -36,6 +37,7 @@ type Props = ViewProps & {
gesture: Animated.Value; gesture: Animated.Value;
layout: Layout; layout: Layout;
insets: EdgeInsets; insets: EdgeInsets;
pageOverflowEnabled: boolean;
gestureDirection: GestureDirection; gestureDirection: GestureDirection;
onOpen: () => void; onOpen: () => void;
onClose: () => void; onClose: () => void;
@@ -412,8 +414,6 @@ export default class Card extends React.Component<Props> {
} }
} }
private gestureRef = React.createRef<PanGestureHandler>();
private contentRef = React.createRef<View>(); private contentRef = React.createRef<View>();
render() { render() {
@@ -430,6 +430,7 @@ export default class Card extends React.Component<Props> {
shadowEnabled, shadowEnabled,
gestureEnabled, gestureEnabled,
gestureDirection, gestureDirection,
pageOverflowEnabled,
children, children,
containerStyle: customContainerStyle, containerStyle: customContainerStyle,
contentStyle, contentStyle,
@@ -499,7 +500,6 @@ export default class Card extends React.Component<Props> {
pointerEvents="box-none" pointerEvents="box-none"
> >
<PanGestureHandler <PanGestureHandler
ref={this.gestureRef}
enabled={layout.width !== 0 && gestureEnabled} enabled={layout.width !== 0 && gestureEnabled}
onGestureEvent={handleGestureEvent} onGestureEvent={handleGestureEvent}
onHandlerStateChange={this.handleGestureStateChange} onHandlerStateChange={this.handleGestureStateChange}
@@ -523,14 +523,14 @@ export default class Card extends React.Component<Props> {
pointerEvents="none" pointerEvents="none"
/> />
) : null} ) : null}
<View <CardSheet
ref={this.contentRef} ref={this.contentRef}
style={[styles.content, contentStyle]} enabled={pageOverflowEnabled}
layout={layout}
style={contentStyle}
> >
<StackGestureRefContext.Provider value={this.gestureRef}> {children}
{children} </CardSheet>
</StackGestureRefContext.Provider>
</View>
</Animated.View> </Animated.View>
</PanGestureHandler> </PanGestureHandler>
</Animated.View> </Animated.View>
@@ -544,10 +544,6 @@ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
}, },
content: {
flex: 1,
overflow: 'hidden',
},
overlay: { overlay: {
flex: 1, flex: 1,
backgroundColor: '#000', backgroundColor: '#000',

View File

@@ -4,7 +4,13 @@ import { Route, useTheme } from '@react-navigation/native';
import { Props as HeaderContainerProps } from '../Header/HeaderContainer'; import { Props as HeaderContainerProps } from '../Header/HeaderContainer';
import Card from './Card'; import Card from './Card';
import HeaderHeightContext from '../../utils/HeaderHeightContext'; import HeaderHeightContext from '../../utils/HeaderHeightContext';
import { Scene, Layout, StackHeaderMode, TransitionPreset } from '../../types'; import {
Scene,
Layout,
StackHeaderMode,
StackCardMode,
TransitionPreset,
} from '../../types';
type Props = TransitionPreset & { type Props = TransitionPreset & {
index: number; index: number;
@@ -45,6 +51,7 @@ type Props = TransitionPreset & {
horizontal?: number; horizontal?: number;
}; };
gestureVelocityImpact?: number; gestureVelocityImpact?: number;
mode: StackCardMode;
headerMode: StackHeaderMode; headerMode: StackHeaderMode;
headerShown?: boolean; headerShown?: boolean;
headerTransparent?: boolean; headerTransparent?: boolean;
@@ -73,6 +80,7 @@ function CardContainer({
gestureVelocityImpact, gestureVelocityImpact,
getPreviousRoute, getPreviousRoute,
getFocusedRoute, getFocusedRoute,
mode,
headerMode, headerMode,
headerShown, headerShown,
headerStyleInterpolator, headerStyleInterpolator,
@@ -178,6 +186,7 @@ function CardContainer({
accessibilityElementsHidden={!focused} accessibilityElementsHidden={!focused}
importantForAccessibility={focused ? 'auto' : 'no-hide-descendants'} importantForAccessibility={focused ? 'auto' : 'no-hide-descendants'}
pointerEvents={active ? 'box-none' : pointerEvents} pointerEvents={active ? 'box-none' : pointerEvents}
pageOverflowEnabled={headerMode === 'screen' && mode === 'card'}
containerStyle={ containerStyle={
headerMode === 'float' && !headerTransparent && headerShown !== false headerMode === 'float' && !headerTransparent && headerShown !== false
? { marginTop: headerHeight } ? { marginTop: headerHeight }

View File

@@ -0,0 +1,49 @@
import * as React from 'react';
import { View, ViewProps, StyleSheet } from 'react-native';
type Props = ViewProps & {
enabled: boolean;
layout: { width: number; height: number };
children: React.ReactNode;
};
// This component will render a page which overflows the screen
// if the container fills the body by comparing the size
// This lets the document.body handle scrolling of the content
// It's necessary for mobile browsers to be able to hide address bar on scroll
export default React.forwardRef<View, Props>(function CardSheet(
{ enabled, layout, style, ...rest },
ref
) {
const [fill, setFill] = React.useState(false);
React.useEffect(() => {
if (typeof document === 'undefined' || !document.body) {
// Only run when DOM is available
return;
}
const width = document.body.clientWidth;
const height = document.body.clientHeight;
setFill(width === layout.width && height === layout.height);
}, [layout.height, layout.width]);
return (
<View
{...rest}
ref={ref}
style={[enabled && fill ? styles.page : styles.card, style]}
/>
);
});
const styles = StyleSheet.create({
page: {
minHeight: '100%',
},
card: {
flex: 1,
overflow: 'hidden',
},
});

View File

@@ -519,6 +519,7 @@ export default class CardStack extends React.Component<Props, State> {
onHeaderHeightChange={this.handleHeaderLayout} onHeaderHeightChange={this.handleHeaderLayout}
getPreviousRoute={getPreviousRoute} getPreviousRoute={getPreviousRoute}
getFocusedRoute={this.getFocusedRoute} getFocusedRoute={this.getFocusedRoute}
mode={mode}
headerMode={headerMode} headerMode={headerMode}
headerShown={headerShown} headerShown={headerShown}
headerTransparent={headerTransparent} headerTransparent={headerTransparent}
@@ -564,7 +565,6 @@ export default class CardStack extends React.Component<Props, State> {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
overflow: 'hidden',
}, },
floating: { floating: {
position: 'absolute', position: 'absolute',

View File

@@ -1,7 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { View, Platform, StyleSheet } from 'react-native'; import { View, Platform, StyleSheet } from 'react-native';
import { SafeAreaConsumer, EdgeInsets } from 'react-native-safe-area-context'; import { SafeAreaConsumer, EdgeInsets } from 'react-native-safe-area-context';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { import {
NavigationHelpersContext, NavigationHelpersContext,
StackActions, StackActions,
@@ -9,6 +8,7 @@ import {
Route, Route,
} from '@react-navigation/native'; } from '@react-navigation/native';
import { GestureHandlerRootView } from '../GestureHandler';
import CardStack from './CardStack'; import CardStack from './CardStack';
import KeyboardManager from '../KeyboardManager'; import KeyboardManager from '../KeyboardManager';
import HeaderContainer, { import HeaderContainer, {
@@ -420,7 +420,7 @@ export default class StackView extends React.Component<Props, State> {
} = this.state; } = this.state;
const headerMode = const headerMode =
mode !== 'modal' && Platform.OS === 'ios' ? 'float' : 'screen'; mode === 'card' && Platform.OS === 'ios' ? 'float' : 'screen';
return ( return (
<NavigationHelpersContext.Provider value={navigation}> <NavigationHelpersContext.Provider value={navigation}>

View File

@@ -0,0 +1,81 @@
/**
* TouchableItem renders a touchable that looks native on both iOS and Android.
*
* It provides an abstraction on top of TouchableNativeFeedback and
* TouchableOpacity.
*
* On iOS you can pass the props of TouchableOpacity, on Android pass the props
* of TouchableNativeFeedback.
*/
import * as React from 'react';
import {
Platform,
TouchableNativeFeedback,
TouchableOpacity,
View,
ViewProps,
} from 'react-native';
import BorderlessButton from './BorderlessButton';
export type Props = ViewProps & {
pressColor: string;
disabled?: boolean;
borderless?: boolean;
delayPressIn?: number;
onPress?: () => void;
children: React.ReactNode;
};
const ANDROID_VERSION_LOLLIPOP = 21;
export class TouchableItem extends React.Component<Props> {
static defaultProps = {
borderless: false,
pressColor: 'rgba(0, 0, 0, .32)',
};
render() {
/*
* TouchableNativeFeedback.Ripple causes a crash on old Android versions,
* therefore only enable it on Android Lollipop and above.
*
* All touchables on Android should have the ripple effect according to
* platform design guidelines.
* We need to pass the background prop to specify a borderless ripple effect.
*/
if (
Platform.OS === 'android' &&
Platform.Version >= ANDROID_VERSION_LOLLIPOP
) {
const { style, pressColor, borderless, children, ...rest } = this.props;
return (
<TouchableNativeFeedback
{...rest}
useForeground={TouchableNativeFeedback.canUseNativeForeground()}
background={TouchableNativeFeedback.Ripple(pressColor, borderless)}
>
<View style={style}>{React.Children.only(children)}</View>
</TouchableNativeFeedback>
);
} else if (Platform.OS === 'ios') {
return (
<BorderlessButton
hitSlop={{ top: 10, bottom: 10, right: 10, left: 10 }}
disallowInterruption
enabled={!this.props.disabled}
{...this.props}
>
{this.props.children}
</BorderlessButton>
);
} else {
return (
<TouchableOpacity {...this.props}>
{this.props.children}
</TouchableOpacity>
);
}
}
}

View File

@@ -1,80 +1,3 @@
/** import { TouchableOpacity } from 'react-native';
* TouchableItem renders a touchable that looks native on both iOS and Android.
*
* It provides an abstraction on top of TouchableNativeFeedback and
* TouchableOpacity.
*
* On iOS you can pass the props of TouchableOpacity, on Android pass the props
* of TouchableNativeFeedback.
*/
import * as React from 'react';
import {
Platform,
TouchableNativeFeedback,
TouchableOpacity,
View,
ViewProps,
} from 'react-native';
import BorderlessButton from './BorderlessButton'; export const TouchableItem = (TouchableOpacity as any) as typeof import('./TouchableItem.native').TouchableItem;
type Props = ViewProps & {
pressColor: string;
disabled?: boolean;
borderless?: boolean;
delayPressIn?: number;
onPress?: () => void;
};
const ANDROID_VERSION_LOLLIPOP = 21;
export default class TouchableItem extends React.Component<Props> {
static defaultProps = {
borderless: false,
pressColor: 'rgba(0, 0, 0, .32)',
};
render() {
/*
* TouchableNativeFeedback.Ripple causes a crash on old Android versions,
* therefore only enable it on Android Lollipop and above.
*
* All touchables on Android should have the ripple effect according to
* platform design guidelines.
* We need to pass the background prop to specify a borderless ripple effect.
*/
if (
Platform.OS === 'android' &&
Platform.Version >= ANDROID_VERSION_LOLLIPOP
) {
const { style, pressColor, borderless, children, ...rest } = this.props;
return (
<TouchableNativeFeedback
{...rest}
useForeground={TouchableNativeFeedback.canUseNativeForeground()}
background={TouchableNativeFeedback.Ripple(pressColor, borderless)}
>
<View style={style}>{React.Children.only(children)}</View>
</TouchableNativeFeedback>
);
} else if (Platform.OS === 'ios') {
return (
<BorderlessButton
hitSlop={{ top: 10, bottom: 10, right: 10, left: 10 }}
disallowInterruption
enabled={!this.props.disabled}
{...this.props}
>
{this.props.children}
</BorderlessButton>
);
} else {
return (
<TouchableOpacity {...this.props}>
{this.props.children}
</TouchableOpacity>
);
}
}
}

2456
yarn.lock

File diff suppressed because it is too large Load Diff