@react-navigation/native 6.0.8 → 6.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,216 @@
1
+ import type { NavigationState } from '@react-navigation/core';
2
+ import { nanoid } from 'nanoid/non-secure';
3
+
4
+ type HistoryRecord = {
5
+ // Unique identifier for this record to match it with window.history.state
6
+ id: string;
7
+ // Navigation state object for the history entry
8
+ state: NavigationState;
9
+ // Path of the history entry
10
+ path: string;
11
+ };
12
+
13
+ export default function createMemoryHistory() {
14
+ let index = 0;
15
+ let items: HistoryRecord[] = [];
16
+
17
+ // Pending callbacks for `history.go(n)`
18
+ // We might modify the callback stored if it was interrupted, so we have a ref to identify it
19
+ const pending: { ref: unknown; cb: (interrupted?: boolean) => void }[] = [];
20
+
21
+ const interrupt = () => {
22
+ // If another history operation was performed we need to interrupt existing ones
23
+ // This makes sure that calls such as `history.replace` after `history.go` don't happen
24
+ // Since otherwise it won't be correct if something else has changed
25
+ pending.forEach((it) => {
26
+ const cb = it.cb;
27
+ it.cb = () => cb(true);
28
+ });
29
+ };
30
+
31
+ const history = {
32
+ get index(): number {
33
+ // We store an id in the state instead of an index
34
+ // Index could get out of sync with in-memory values if page reloads
35
+ const id = window.history.state?.id;
36
+
37
+ if (id) {
38
+ const index = items.findIndex((item) => item.id === id);
39
+
40
+ return index > -1 ? index : 0;
41
+ }
42
+
43
+ return 0;
44
+ },
45
+
46
+ get(index: number) {
47
+ return items[index];
48
+ },
49
+
50
+ backIndex({ path }: { path: string }) {
51
+ // We need to find the index from the element before current to get closest path to go back to
52
+ for (let i = index - 1; i >= 0; i--) {
53
+ const item = items[i];
54
+
55
+ if (item.path === path) {
56
+ return i;
57
+ }
58
+ }
59
+
60
+ return -1;
61
+ },
62
+
63
+ push({ path, state }: { path: string; state: NavigationState }) {
64
+ interrupt();
65
+
66
+ const id = nanoid();
67
+
68
+ // When a new entry is pushed, all the existing entries after index will be inaccessible
69
+ // So we remove any existing entries after the current index to clean them up
70
+ items = items.slice(0, index + 1);
71
+
72
+ items.push({ path, state, id });
73
+ index = items.length - 1;
74
+
75
+ // We pass empty string for title because it's ignored in all browsers except safari
76
+ // We don't store state object in history.state because:
77
+ // - browsers have limits on how big it can be, and we don't control the size
78
+ // - while not recommended, there could be non-serializable data in state
79
+ window.history.pushState({ id }, '', path);
80
+ },
81
+
82
+ replace({ path, state }: { path: string; state: NavigationState }) {
83
+ interrupt();
84
+
85
+ const id = window.history.state?.id ?? nanoid();
86
+
87
+ if (!items.length || items.findIndex((item) => item.id === id) < 0) {
88
+ // There are two scenarios for creating an array with only one history record:
89
+ // - When loaded id not found in the items array, this function by default will replace
90
+ // the first item. We need to keep only the new updated object, otherwise it will break
91
+ // the page when navigating forward in history.
92
+ // - This is the first time any state modifications are done
93
+ // So we need to push the entry as there's nothing to replace
94
+ items = [{ path, state, id }];
95
+ index = 0;
96
+ } else {
97
+ items[index] = { path, state, id };
98
+ }
99
+
100
+ window.history.replaceState({ id }, '', path);
101
+ },
102
+
103
+ // `history.go(n)` is asynchronous, there are couple of things to keep in mind:
104
+ // - it won't do anything if we can't go `n` steps, the `popstate` event won't fire.
105
+ // - each `history.go(n)` call will trigger a separate `popstate` event with correct location.
106
+ // - the `popstate` event fires before the next frame after calling `history.go(n)`.
107
+ // This method differs from `history.go(n)` in the sense that it'll go back as many steps it can.
108
+ go(n: number) {
109
+ interrupt();
110
+
111
+ // To guard against unexpected navigation out of the app we will assume that browser history is only as deep as the length of our memory
112
+ // history. If we don't have an item to navigate to then update our index and navigate as far as we can without taking the user out of the app.
113
+ const nextIndex = index + n;
114
+ const lastItemIndex = items.length - 1;
115
+ if (n < 0 && !items[nextIndex]) {
116
+ // Attempted to navigate beyond the first index. Negating the current index will align the browser history with the first item.
117
+ n = -index;
118
+ index = 0;
119
+ } else if (n > 0 && nextIndex > lastItemIndex) {
120
+ // Attempted to navigate past the last index. Calculate how many indices away from the last index and go there.
121
+ n = lastItemIndex - index;
122
+ index = lastItemIndex;
123
+ } else {
124
+ index = nextIndex;
125
+ }
126
+
127
+ if (n === 0) {
128
+ return;
129
+ }
130
+
131
+ // When we call `history.go`, `popstate` will fire when there's history to go back to
132
+ // So we need to somehow handle following cases:
133
+ // - There's history to go back, `history.go` is called, and `popstate` fires
134
+ // - `history.go` is called multiple times, we need to resolve on respective `popstate`
135
+ // - No history to go back, but `history.go` was called, browser has no API to detect it
136
+ return new Promise<void>((resolve, reject) => {
137
+ const done = (interrupted?: boolean) => {
138
+ clearTimeout(timer);
139
+
140
+ if (interrupted) {
141
+ reject(new Error('History was changed during navigation.'));
142
+ return;
143
+ }
144
+
145
+ // There seems to be a bug in Chrome regarding updating the title
146
+ // If we set a title just before calling `history.go`, the title gets lost
147
+ // However the value of `document.title` is still what we set it to
148
+ // It's just not displayed in the tab bar
149
+ // To update the tab bar, we need to reset the title to something else first (e.g. '')
150
+ // And set the title to what it was before so it gets applied
151
+ // It won't work without setting it to empty string coz otherwise title isn't changing
152
+ // Which means that the browser won't do anything after setting the title
153
+ const { title } = window.document;
154
+
155
+ window.document.title = '';
156
+ window.document.title = title;
157
+
158
+ resolve();
159
+ };
160
+
161
+ pending.push({ ref: done, cb: done });
162
+
163
+ // If navigation didn't happen within 100ms, assume that it won't happen
164
+ // This may not be accurate, but hopefully it won't take so much time
165
+ // In Chrome, navigation seems to happen instantly in next microtask
166
+ // But on Firefox, it seems to take much longer, around 50ms from our testing
167
+ // We're using a hacky timeout since there doesn't seem to be way to know for sure
168
+ const timer = setTimeout(() => {
169
+ const index = pending.findIndex((it) => it.ref === done);
170
+
171
+ if (index > -1) {
172
+ pending[index].cb();
173
+ pending.splice(index, 1);
174
+ }
175
+ }, 100);
176
+
177
+ const onPopState = () => {
178
+ const id = window.history.state?.id;
179
+ const currentIndex = items.findIndex((item) => item.id === id);
180
+
181
+ // Fix createMemoryHistory.index variable's value
182
+ // as it may go out of sync when navigating in the browser.
183
+ index = Math.max(currentIndex, 0);
184
+
185
+ const last = pending.pop();
186
+
187
+ window.removeEventListener('popstate', onPopState);
188
+ last?.cb();
189
+ };
190
+
191
+ window.addEventListener('popstate', onPopState);
192
+ window.history.go(n);
193
+ });
194
+ },
195
+
196
+ // The `popstate` event is triggered when history changes, except `pushState` and `replaceState`
197
+ // If we call `history.go(n)` ourselves, we don't want it to trigger the listener
198
+ // Here we normalize it so that only external changes (e.g. user pressing back/forward) trigger the listener
199
+ listen(listener: () => void) {
200
+ const onPopState = () => {
201
+ if (pending.length) {
202
+ // This was triggered by `history.go(n)`, we shouldn't call the listener
203
+ return;
204
+ }
205
+
206
+ listener();
207
+ };
208
+
209
+ window.addEventListener('popstate', onPopState);
210
+
211
+ return () => window.removeEventListener('popstate', onPopState);
212
+ },
213
+ };
214
+
215
+ return history;
216
+ }
package/src/index.tsx CHANGED
@@ -1,4 +1,5 @@
1
1
  export { default as Link } from './Link';
2
+ export { default as LinkingContext } from './LinkingContext';
2
3
  export { default as NavigationContainer } from './NavigationContainer';
3
4
  export { default as ServerContainer } from './ServerContainer';
4
5
  export { default as DarkTheme } from './theming/DarkTheme';
@@ -8,217 +8,14 @@ import {
8
8
  ParamListBase,
9
9
  } from '@react-navigation/core';
10
10
  import isEqual from 'fast-deep-equal';
11
- import { nanoid } from 'nanoid/non-secure';
12
11
  import * as React from 'react';
13
12
 
13
+ import createMemoryHistory from './createMemoryHistory';
14
14
  import ServerContext from './ServerContext';
15
15
  import type { LinkingOptions } from './types';
16
16
 
17
17
  type ResultState = ReturnType<typeof getStateFromPathDefault>;
18
18
 
19
- type HistoryRecord = {
20
- // Unique identifier for this record to match it with window.history.state
21
- id: string;
22
- // Navigation state object for the history entry
23
- state: NavigationState;
24
- // Path of the history entry
25
- path: string;
26
- };
27
-
28
- const createMemoryHistory = () => {
29
- let index = 0;
30
- let items: HistoryRecord[] = [];
31
-
32
- // Pending callbacks for `history.go(n)`
33
- // We might modify the callback stored if it was interrupted, so we have a ref to identify it
34
- const pending: { ref: unknown; cb: (interrupted?: boolean) => void }[] = [];
35
-
36
- const interrupt = () => {
37
- // If another history operation was performed we need to interrupt existing ones
38
- // This makes sure that calls such as `history.replace` after `history.go` don't happen
39
- // Since otherwise it won't be correct if something else has changed
40
- pending.forEach((it) => {
41
- const cb = it.cb;
42
- it.cb = () => cb(true);
43
- });
44
- };
45
-
46
- const history = {
47
- get index(): number {
48
- // We store an id in the state instead of an index
49
- // Index could get out of sync with in-memory values if page reloads
50
- const id = window.history.state?.id;
51
-
52
- if (id) {
53
- const index = items.findIndex((item) => item.id === id);
54
-
55
- return index > -1 ? index : 0;
56
- }
57
-
58
- return 0;
59
- },
60
-
61
- get(index: number) {
62
- return items[index];
63
- },
64
-
65
- backIndex({ path }: { path: string }) {
66
- // We need to find the index from the element before current to get closest path to go back to
67
- for (let i = index - 1; i >= 0; i--) {
68
- const item = items[i];
69
-
70
- if (item.path === path) {
71
- return i;
72
- }
73
- }
74
-
75
- return -1;
76
- },
77
-
78
- push({ path, state }: { path: string; state: NavigationState }) {
79
- interrupt();
80
-
81
- const id = nanoid();
82
-
83
- // When a new entry is pushed, all the existing entries after index will be inaccessible
84
- // So we remove any existing entries after the current index to clean them up
85
- items = items.slice(0, index + 1);
86
-
87
- items.push({ path, state, id });
88
- index = items.length - 1;
89
-
90
- // We pass empty string for title because it's ignored in all browsers except safari
91
- // We don't store state object in history.state because:
92
- // - browsers have limits on how big it can be, and we don't control the size
93
- // - while not recommended, there could be non-serializable data in state
94
- window.history.pushState({ id }, '', path);
95
- },
96
-
97
- replace({ path, state }: { path: string; state: NavigationState }) {
98
- interrupt();
99
-
100
- const id = window.history.state?.id ?? nanoid();
101
-
102
- if (!items.length || items.findIndex((item) => item.id === id) < 0) {
103
- // There are two scenarios for creating an array with only one history record:
104
- // - When loaded id not found in the items array, this function by default will replace
105
- // the first item. We need to keep only the new updated object, otherwise it will break
106
- // the page when navigating forward in history.
107
- // - This is the first time any state modifications are done
108
- // So we need to push the entry as there's nothing to replace
109
- items = [{ path, state, id }];
110
- index = 0;
111
- } else {
112
- items[index] = { path, state, id };
113
- }
114
-
115
- window.history.replaceState({ id }, '', path);
116
- },
117
-
118
- // `history.go(n)` is asynchronous, there are couple of things to keep in mind:
119
- // - it won't do anything if we can't go `n` steps, the `popstate` event won't fire.
120
- // - each `history.go(n)` call will trigger a separate `popstate` event with correct location.
121
- // - the `popstate` event fires before the next frame after calling `history.go(n)`.
122
- // This method differs from `history.go(n)` in the sense that it'll go back as many steps it can.
123
- go(n: number) {
124
- interrupt();
125
-
126
- if (n === 0) {
127
- return;
128
- }
129
-
130
- // We shouldn't go back more than the 0 index (otherwise we'll exit the page)
131
- // Or forward more than the available index (or the app will crash)
132
- index =
133
- n < 0 ? Math.max(index - n, 0) : Math.min(index + n, items.length - 1);
134
-
135
- // When we call `history.go`, `popstate` will fire when there's history to go back to
136
- // So we need to somehow handle following cases:
137
- // - There's history to go back, `history.go` is called, and `popstate` fires
138
- // - `history.go` is called multiple times, we need to resolve on respective `popstate`
139
- // - No history to go back, but `history.go` was called, browser has no API to detect it
140
- return new Promise<void>((resolve, reject) => {
141
- const done = (interrupted?: boolean) => {
142
- clearTimeout(timer);
143
-
144
- if (interrupted) {
145
- reject(new Error('History was changed during navigation.'));
146
- return;
147
- }
148
-
149
- // There seems to be a bug in Chrome regarding updating the title
150
- // If we set a title just before calling `history.go`, the title gets lost
151
- // However the value of `document.title` is still what we set it to
152
- // It's just not displayed in the tab bar
153
- // To update the tab bar, we need to reset the title to something else first (e.g. '')
154
- // And set the title to what it was before so it gets applied
155
- // It won't work without setting it to empty string coz otherwise title isn't changing
156
- // Which means that the browser won't do anything after setting the title
157
- const { title } = window.document;
158
-
159
- window.document.title = '';
160
- window.document.title = title;
161
-
162
- resolve();
163
- };
164
-
165
- pending.push({ ref: done, cb: done });
166
-
167
- // If navigation didn't happen within 100ms, assume that it won't happen
168
- // This may not be accurate, but hopefully it won't take so much time
169
- // In Chrome, navigation seems to happen instantly in next microtask
170
- // But on Firefox, it seems to take much longer, around 50ms from our testing
171
- // We're using a hacky timeout since there doesn't seem to be way to know for sure
172
- const timer = setTimeout(() => {
173
- const index = pending.findIndex((it) => it.ref === done);
174
-
175
- if (index > -1) {
176
- pending[index].cb();
177
- pending.splice(index, 1);
178
- }
179
- }, 100);
180
-
181
- const onPopState = () => {
182
- const id = window.history.state?.id;
183
- const currentIndex = items.findIndex((item) => item.id === id);
184
-
185
- // Fix createMemoryHistory.index variable's value
186
- // as it may go out of sync when navigating in the browser.
187
- index = Math.max(currentIndex, 0);
188
-
189
- const last = pending.pop();
190
-
191
- window.removeEventListener('popstate', onPopState);
192
- last?.cb();
193
- };
194
-
195
- window.addEventListener('popstate', onPopState);
196
- window.history.go(n);
197
- });
198
- },
199
-
200
- // The `popstate` event is triggered when history changes, except `pushState` and `replaceState`
201
- // If we call `history.go(n)` ourselves, we don't want it to trigger the listener
202
- // Here we normalize it so that only external changes (e.g. user pressing back/forward) trigger the listener
203
- listen(listener: () => void) {
204
- const onPopState = () => {
205
- if (pending.length) {
206
- // This was triggered by `history.go(n)`, we shouldn't call the listener
207
- return;
208
- }
209
-
210
- listener();
211
- };
212
-
213
- window.addEventListener('popstate', onPopState);
214
-
215
- return () => window.removeEventListener('popstate', onPopState);
216
- },
217
- };
218
-
219
- return history;
220
- };
221
-
222
19
  /**
223
20
  * Find the matching navigation state that changed between 2 navigation states
224
21
  * e.g.: a -> b -> c -> d and a -> b -> c -> e -> f, if history in b changed, b is the matching state