mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-12 22:51:18 +08:00
feat: add focus and blur events
This commit is contained in:
committed by
Satyajit Sahoo
parent
f130d3c292
commit
fb8d3024bf
30
README.md
30
README.md
@@ -133,6 +133,36 @@ function Selection({ navigation }) {
|
||||
|
||||
This allows options to be changed based on props, state or context, and doesn't have the disadvantages of static configuration.
|
||||
|
||||
## Navigation events
|
||||
|
||||
Screens can add listeners on the `navigation` prop like in React Navigation. By default, `focus` and `blur` events are fired when focused screen changes:
|
||||
|
||||
```js
|
||||
function Profile({ navigation }) {
|
||||
React.useEffect(() =>
|
||||
navigation.addListener('focus', () => {
|
||||
// do something
|
||||
})
|
||||
);
|
||||
|
||||
return <ProfileContent />;
|
||||
}
|
||||
```
|
||||
|
||||
Navigators can also emit custom events using the `emit` method in the `navigation` object passed:
|
||||
|
||||
```js
|
||||
navigation.emit({
|
||||
type: 'transitionStart',
|
||||
data: { blurring: false },
|
||||
target: route.key,
|
||||
});
|
||||
```
|
||||
|
||||
The `target` property determines the screen that will receive the event. If the `target` property is omitted, the event is dispatched to all screens in the navigator.
|
||||
|
||||
Screens cannot emit events as there is no `emit` method on a screen's `navigation` prop.
|
||||
|
||||
## Type-checking
|
||||
|
||||
The library exports few helper types. Each navigator also need to export a custom type for the `navigation` prop which should contain the actions they provide, .e.g. `push` for stack, `jumpTo` for tab etc.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { NavigationHelpers, ParamListBase } from './types';
|
||||
import { NavigationProp, ParamListBase } from './types';
|
||||
|
||||
const NavigationContext = React.createContext<
|
||||
NavigationHelpers<ParamListBase> | undefined
|
||||
NavigationProp<ParamListBase> | undefined
|
||||
>(undefined);
|
||||
|
||||
export default NavigationContext;
|
||||
|
||||
@@ -76,13 +76,25 @@ export default function MockRouter(options: DefaultRouterOptions) {
|
||||
case 'NOOP':
|
||||
return state;
|
||||
|
||||
case 'NAVIGATE': {
|
||||
const index = state.routes.findIndex(
|
||||
route => route.name === action.payload.name
|
||||
);
|
||||
|
||||
if (index === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { ...state, index };
|
||||
}
|
||||
|
||||
default:
|
||||
return BaseRouter.getStateForAction(state, action);
|
||||
}
|
||||
},
|
||||
|
||||
shouldActionPropagateToChildren() {
|
||||
return false;
|
||||
shouldActionPropagateToChildren(action) {
|
||||
return action.type === 'NAVIGATE';
|
||||
},
|
||||
|
||||
shouldActionChangeFocus() {
|
||||
|
||||
268
src/__tests__/useEventEmitter.test.tsx
Normal file
268
src/__tests__/useEventEmitter.test.tsx
Normal file
@@ -0,0 +1,268 @@
|
||||
import * as React from 'react';
|
||||
import { render, act } from 'react-native-testing-library';
|
||||
import useNavigationBuilder from '../useNavigationBuilder';
|
||||
import NavigationContainer from '../NavigationContainer';
|
||||
import Screen from '../Screen';
|
||||
import MockRouter from './__fixtures__/MockRouter';
|
||||
|
||||
it('fires focus and blur events in root navigator', () => {
|
||||
const TestNavigator = React.forwardRef((props: any, ref: any): any => {
|
||||
const { state, navigation, descriptors } = useNavigationBuilder(
|
||||
MockRouter,
|
||||
props
|
||||
);
|
||||
|
||||
React.useImperativeHandle(ref, () => navigation, [navigation]);
|
||||
|
||||
return state.routes.map(route => descriptors[route.key].render());
|
||||
});
|
||||
|
||||
const firstFocusCallback = jest.fn();
|
||||
const firstBlurCallback = jest.fn();
|
||||
|
||||
const secondFocusCallback = jest.fn();
|
||||
const secondBlurCallback = jest.fn();
|
||||
|
||||
const thirdFocusCallback = jest.fn();
|
||||
const thirdBlurCallback = jest.fn();
|
||||
|
||||
const fourthFocusCallback = jest.fn();
|
||||
const fourthBlurCallback = jest.fn();
|
||||
|
||||
const createComponent = (focusCallback: any, blurCallback: any) => ({
|
||||
navigation,
|
||||
}: any) => {
|
||||
React.useEffect(() => navigation.addListener('focus', focusCallback), [
|
||||
navigation,
|
||||
]);
|
||||
|
||||
React.useEffect(() => navigation.addListener('blur', blurCallback), [
|
||||
navigation,
|
||||
]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const navigation = React.createRef<any>();
|
||||
|
||||
const element = (
|
||||
<NavigationContainer>
|
||||
<TestNavigator ref={navigation}>
|
||||
<Screen
|
||||
name="first"
|
||||
component={createComponent(firstFocusCallback, firstBlurCallback)}
|
||||
/>
|
||||
<Screen
|
||||
name="second"
|
||||
component={createComponent(secondFocusCallback, secondBlurCallback)}
|
||||
/>
|
||||
<Screen
|
||||
name="third"
|
||||
component={createComponent(thirdFocusCallback, thirdBlurCallback)}
|
||||
/>
|
||||
<Screen
|
||||
name="fourth"
|
||||
component={createComponent(fourthFocusCallback, fourthBlurCallback)}
|
||||
/>
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
render(element);
|
||||
|
||||
expect(firstFocusCallback).toBeCalledTimes(1);
|
||||
expect(firstBlurCallback).toBeCalledTimes(0);
|
||||
expect(secondFocusCallback).toBeCalledTimes(0);
|
||||
expect(secondBlurCallback).toBeCalledTimes(0);
|
||||
expect(thirdFocusCallback).toBeCalledTimes(0);
|
||||
expect(thirdBlurCallback).toBeCalledTimes(0);
|
||||
expect(fourthFocusCallback).toBeCalledTimes(0);
|
||||
expect(fourthBlurCallback).toBeCalledTimes(0);
|
||||
|
||||
act(() => navigation.current.navigate('second'));
|
||||
|
||||
expect(firstBlurCallback).toBeCalledTimes(1);
|
||||
expect(secondFocusCallback).toBeCalledTimes(1);
|
||||
|
||||
act(() => navigation.current.navigate('fourth'));
|
||||
|
||||
expect(firstFocusCallback).toBeCalledTimes(1);
|
||||
expect(firstBlurCallback).toBeCalledTimes(1);
|
||||
expect(secondFocusCallback).toBeCalledTimes(1);
|
||||
expect(secondBlurCallback).toBeCalledTimes(1);
|
||||
expect(thirdFocusCallback).toBeCalledTimes(0);
|
||||
expect(thirdBlurCallback).toBeCalledTimes(0);
|
||||
expect(fourthFocusCallback).toBeCalledTimes(1);
|
||||
expect(fourthBlurCallback).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
it('fires focus and blur events in nested navigator', () => {
|
||||
const TestNavigator = React.forwardRef((props: any, ref: any): any => {
|
||||
const { state, navigation, descriptors } = useNavigationBuilder(
|
||||
MockRouter,
|
||||
props
|
||||
);
|
||||
|
||||
React.useImperativeHandle(ref, () => navigation, [navigation]);
|
||||
|
||||
return state.routes.map(route => descriptors[route.key].render());
|
||||
});
|
||||
|
||||
const firstFocusCallback = jest.fn();
|
||||
const firstBlurCallback = jest.fn();
|
||||
|
||||
const secondFocusCallback = jest.fn();
|
||||
const secondBlurCallback = jest.fn();
|
||||
|
||||
const thirdFocusCallback = jest.fn();
|
||||
const thirdBlurCallback = jest.fn();
|
||||
|
||||
const fourthFocusCallback = jest.fn();
|
||||
const fourthBlurCallback = jest.fn();
|
||||
|
||||
const createComponent = (focusCallback: any, blurCallback: any) => ({
|
||||
navigation,
|
||||
}: any) => {
|
||||
React.useEffect(() => navigation.addListener('focus', focusCallback), [
|
||||
navigation,
|
||||
]);
|
||||
|
||||
React.useEffect(() => navigation.addListener('blur', blurCallback), [
|
||||
navigation,
|
||||
]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const parent = React.createRef<any>();
|
||||
const child = React.createRef<any>();
|
||||
|
||||
const element = (
|
||||
<NavigationContainer>
|
||||
<TestNavigator ref={parent}>
|
||||
<Screen
|
||||
name="first"
|
||||
component={createComponent(firstFocusCallback, firstBlurCallback)}
|
||||
/>
|
||||
<Screen
|
||||
name="second"
|
||||
component={createComponent(secondFocusCallback, secondBlurCallback)}
|
||||
/>
|
||||
<Screen name="nested">
|
||||
{() => (
|
||||
<TestNavigator ref={child}>
|
||||
<Screen
|
||||
name="third"
|
||||
component={createComponent(
|
||||
thirdFocusCallback,
|
||||
thirdBlurCallback
|
||||
)}
|
||||
/>
|
||||
<Screen
|
||||
name="fourth"
|
||||
component={createComponent(
|
||||
fourthFocusCallback,
|
||||
fourthBlurCallback
|
||||
)}
|
||||
/>
|
||||
</TestNavigator>
|
||||
)}
|
||||
</Screen>
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
render(element);
|
||||
|
||||
expect(thirdFocusCallback).toBeCalledTimes(0);
|
||||
expect(firstFocusCallback).toBeCalledTimes(1);
|
||||
|
||||
act(() => child.current.navigate('fourth'));
|
||||
|
||||
expect(firstFocusCallback).toBeCalledTimes(1);
|
||||
expect(fourthFocusCallback).toBeCalledTimes(0);
|
||||
expect(thirdFocusCallback).toBeCalledTimes(0);
|
||||
|
||||
act(() => parent.current.navigate('second'));
|
||||
|
||||
expect(thirdFocusCallback).toBeCalledTimes(0);
|
||||
expect(secondFocusCallback).toBeCalledTimes(1);
|
||||
|
||||
act(() => parent.current.navigate('nested'));
|
||||
|
||||
expect(firstBlurCallback).toBeCalledTimes(1);
|
||||
expect(thirdFocusCallback).toBeCalledTimes(0);
|
||||
expect(fourthFocusCallback).toBeCalledTimes(1);
|
||||
|
||||
act(() => parent.current.navigate('third'));
|
||||
|
||||
expect(fourthBlurCallback).toBeCalledTimes(1);
|
||||
expect(thirdFocusCallback).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('fires custom events', () => {
|
||||
const eventName = 'someSuperCoolEvent';
|
||||
|
||||
const TestNavigator = React.forwardRef((props: any, ref: any): any => {
|
||||
const { state, navigation, descriptors } = useNavigationBuilder(
|
||||
MockRouter,
|
||||
props
|
||||
);
|
||||
|
||||
React.useImperativeHandle(ref, () => ({ navigation, state }), [
|
||||
navigation,
|
||||
state,
|
||||
]);
|
||||
|
||||
return state.routes.map(route => descriptors[route.key].render());
|
||||
});
|
||||
|
||||
const firstCallback = jest.fn();
|
||||
const secondCallback = jest.fn();
|
||||
const thirdCallback = jest.fn();
|
||||
|
||||
const createComponent = (callback: any) => ({ navigation }: any) => {
|
||||
React.useEffect(() => navigation.addListener(eventName, callback), [
|
||||
navigation,
|
||||
]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const ref = React.createRef<any>();
|
||||
|
||||
const element = (
|
||||
<NavigationContainer>
|
||||
<TestNavigator ref={ref}>
|
||||
<Screen name="first" component={createComponent(firstCallback)} />
|
||||
<Screen name="second" component={createComponent(secondCallback)} />
|
||||
<Screen name="third" component={createComponent(thirdCallback)} />
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
render(element);
|
||||
|
||||
expect(firstCallback).toBeCalledTimes(0);
|
||||
expect(secondCallback).toBeCalledTimes(0);
|
||||
expect(thirdCallback).toBeCalledTimes(0);
|
||||
|
||||
act(() =>
|
||||
ref.current.navigation.emit({
|
||||
type: eventName,
|
||||
target: ref.current.state.routes[ref.current.state.routes.length - 1].key,
|
||||
data: 42,
|
||||
})
|
||||
);
|
||||
|
||||
expect(firstCallback).toBeCalledTimes(0);
|
||||
expect(secondCallback).toBeCalledTimes(0);
|
||||
expect(thirdCallback).toBeCalledTimes(1);
|
||||
expect(thirdCallback.mock.calls[0][0]).toBe(42);
|
||||
|
||||
act(() => ref.current.navigation.emit({ type: eventName }));
|
||||
|
||||
expect(firstCallback).toBeCalledTimes(1);
|
||||
expect(secondCallback).toBeCalledTimes(1);
|
||||
expect(thirdCallback).toBeCalledTimes(2);
|
||||
});
|
||||
@@ -162,7 +162,35 @@ export type Router<
|
||||
|
||||
export type ParamListBase = { [key: string]: object | undefined };
|
||||
|
||||
class PrivateValueStore<A, B> {
|
||||
export type EventMapBase = {
|
||||
focus: undefined;
|
||||
blur: undefined;
|
||||
};
|
||||
|
||||
export type EventListenerCallback<Data> = Data extends undefined
|
||||
? () => void
|
||||
: (data: Data) => void;
|
||||
|
||||
export type EventConsumer<EventMap extends { [key: string]: any }> = {
|
||||
addListener<EventName extends Extract<keyof EventMap, string>>(
|
||||
type: EventName,
|
||||
callback: EventListenerCallback<EventMap[EventName]>
|
||||
): () => void;
|
||||
removeListener<EventName extends Extract<keyof EventMap, string>>(
|
||||
type: EventName,
|
||||
callback: EventListenerCallback<EventMap[EventName]>
|
||||
): void;
|
||||
};
|
||||
|
||||
export type EventEmitter<EventMap extends { [key: string]: any }> = {
|
||||
emit<EventName extends Extract<keyof EventMap, string>>(options: {
|
||||
type: EventName;
|
||||
data?: EventMap[EventName];
|
||||
target?: string;
|
||||
}): void;
|
||||
};
|
||||
|
||||
class PrivateValueStore<A, B, C> {
|
||||
/**
|
||||
* TypeScript requires a type to be actually used to be able to infer it.
|
||||
* This is a hacky way of storing type in a property without surfacing it in intellisense.
|
||||
@@ -171,6 +199,8 @@ class PrivateValueStore<A, B> {
|
||||
private __private_value_type_a?: A;
|
||||
// @ts-ignore
|
||||
private __private_value_type_b?: B;
|
||||
// @ts-ignore
|
||||
private __private_value_type_c?: C;
|
||||
}
|
||||
|
||||
type NavigationHelpersCommon<
|
||||
@@ -221,29 +251,38 @@ type NavigationHelpersCommon<
|
||||
* Go back to the previous route in history.
|
||||
*/
|
||||
goBack(): void;
|
||||
} & PrivateValueStore<ParamList, keyof ParamList>;
|
||||
|
||||
/**
|
||||
* Check if the screen is focused. The method returns `true` if focused, `false` otherwise.
|
||||
* Note that this method doesn't re-render screen when the focus changes. So don't use it in `render`.
|
||||
* To get notified of focus changes, use `addListener('focus', cb)` and `addListener('blur', cb)`.
|
||||
*/
|
||||
isFocused(): boolean;
|
||||
} & PrivateValueStore<ParamList, keyof ParamList, {}>;
|
||||
|
||||
export type NavigationHelpers<
|
||||
ParamList extends ParamListBase
|
||||
> = NavigationHelpersCommon<ParamList> & {
|
||||
/**
|
||||
* Update the param object for the route.
|
||||
* The new params will be shallow merged with the old one.
|
||||
*
|
||||
* @param params Params object for the current route.
|
||||
* @param key Key of the route for updating params.
|
||||
*/
|
||||
setParams<RouteName extends keyof ParamList>(
|
||||
params: ParamList[RouteName],
|
||||
key: string
|
||||
): void;
|
||||
};
|
||||
> = NavigationHelpersCommon<ParamList> &
|
||||
EventEmitter<{ [key: string]: any }> & {
|
||||
/**
|
||||
* Update the param object for the route.
|
||||
* The new params will be shallow merged with the old one.
|
||||
*
|
||||
* @param params Params object for the current route.
|
||||
* @param key Key of the route for updating params.
|
||||
*/
|
||||
setParams<RouteName extends keyof ParamList>(
|
||||
params: ParamList[RouteName],
|
||||
key: string
|
||||
): void;
|
||||
};
|
||||
|
||||
export type NavigationProp<
|
||||
ParamList extends ParamListBase,
|
||||
RouteName extends keyof ParamList = string,
|
||||
State extends NavigationState = NavigationState,
|
||||
ScreenOptions extends object = {}
|
||||
ScreenOptions extends object = {},
|
||||
EventMap extends { [key: string]: any } = {}
|
||||
> = NavigationHelpersCommon<ParamList, State> & {
|
||||
/**
|
||||
* Update the param object for the route.
|
||||
@@ -262,7 +301,8 @@ export type NavigationProp<
|
||||
* @param options Options object for the route.
|
||||
*/
|
||||
setOptions(options: Partial<ScreenOptions>): void;
|
||||
} & PrivateValueStore<ParamList, RouteName>;
|
||||
} & EventConsumer<EventMap & EventMapBase> &
|
||||
PrivateValueStore<ParamList, RouteName, EventMap>;
|
||||
|
||||
export type RouteProp<
|
||||
ParamList extends ParamListBase,
|
||||
@@ -280,7 +320,7 @@ export type RouteProp<
|
||||
export type CompositeNavigationProp<
|
||||
A extends NavigationProp<ParamListBase>,
|
||||
B extends NavigationHelpersCommon<ParamListBase>
|
||||
> = Omit<A & B, keyof NavigationProp<any, any, any>> &
|
||||
> = Omit<A & B, keyof NavigationProp<any>> &
|
||||
NavigationProp<
|
||||
/**
|
||||
* Param list from both navigation objects needs to be combined
|
||||
@@ -299,9 +339,15 @@ export type CompositeNavigationProp<
|
||||
A extends NavigationProp<any, any, infer S> ? S : NavigationState,
|
||||
/**
|
||||
* Screen options from both navigation objects needs to be combined
|
||||
* This allows typechecking `setOptions`
|
||||
*/
|
||||
(A extends NavigationProp<any, any, any, infer O> ? O : {}) &
|
||||
(B extends NavigationProp<any, any, any, infer P> ? P : {})
|
||||
(B extends NavigationProp<any, any, any, infer P> ? P : {}),
|
||||
/**
|
||||
* Event consumer config should refer to the config specified in the first type
|
||||
* This allows typechecking `addListener`/`removeListener`
|
||||
*/
|
||||
A extends NavigationProp<any, any, any, any, infer E> ? E : {}
|
||||
>;
|
||||
|
||||
export type Descriptor<ScreenOptions extends object> = {
|
||||
@@ -333,7 +379,7 @@ export type RouteConfig<
|
||||
| ScreenOptions
|
||||
| ((props: {
|
||||
route: RouteProp<ParamList, RouteName>;
|
||||
navigation: NavigationHelpers<ParamList>;
|
||||
navigation: NavigationHelpersCommon<ParamList>;
|
||||
}) => ScreenOptions);
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,7 @@ import SceneView from './SceneView';
|
||||
import NavigationBuilderContext, {
|
||||
ChildActionListener,
|
||||
} from './NavigationBuilderContext';
|
||||
import { NavigationEventEmitter } from './useEventEmitter';
|
||||
import useNavigationCache from './useNavigationCache';
|
||||
import {
|
||||
Descriptor,
|
||||
@@ -27,6 +28,7 @@ type Options<ScreenOptions extends object> = {
|
||||
addActionListener: (listener: ChildActionListener) => void;
|
||||
removeActionListener: (listener: ChildActionListener) => void;
|
||||
onRouteFocus: (key: string) => void;
|
||||
emitter: NavigationEventEmitter;
|
||||
};
|
||||
|
||||
export default function useDescriptors<ScreenOptions extends object>({
|
||||
@@ -39,6 +41,7 @@ export default function useDescriptors<ScreenOptions extends object>({
|
||||
addActionListener,
|
||||
removeActionListener,
|
||||
onRouteFocus,
|
||||
emitter,
|
||||
}: Options<ScreenOptions>) {
|
||||
const [options, setOptions] = React.useState<{ [key: string]: object }>({});
|
||||
const context = React.useMemo(
|
||||
@@ -60,8 +63,10 @@ export default function useDescriptors<ScreenOptions extends object>({
|
||||
|
||||
const navigations = useNavigationCache({
|
||||
state,
|
||||
getState,
|
||||
navigation,
|
||||
setOptions,
|
||||
emitter,
|
||||
});
|
||||
|
||||
return state.routes.reduce(
|
||||
|
||||
60
src/useEventEmitter.tsx
Normal file
60
src/useEventEmitter.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import * as React from 'react';
|
||||
import { EventEmitter, EventConsumer } from './types';
|
||||
|
||||
export type NavigationEventEmitter = EventEmitter<{ [key: string]: any }> & {
|
||||
create: (target: string) => EventConsumer<{ [key: string]: any }>;
|
||||
};
|
||||
|
||||
type Listeners = Array<(data: any) => void>;
|
||||
|
||||
export default function useEventEmitter(): NavigationEventEmitter {
|
||||
const listeners = React.useRef<{
|
||||
[key: string]: { [key: string]: Listeners };
|
||||
}>({});
|
||||
|
||||
const create = React.useCallback((target: string) => {
|
||||
const removeListener = (type: string, callback: (data: any) => void) => {
|
||||
const callbacks = listeners.current[type]
|
||||
? listeners.current[type][target]
|
||||
: undefined;
|
||||
|
||||
if (!callbacks) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = callbacks.indexOf(callback);
|
||||
|
||||
callbacks.splice(index, 1);
|
||||
};
|
||||
|
||||
const addListener = (type: string, callback: (data: any) => void) => {
|
||||
listeners.current[type] = listeners.current[type] || {};
|
||||
listeners.current[type][target] = listeners.current[type][target] || [];
|
||||
listeners.current[type][target].push(callback);
|
||||
|
||||
return () => removeListener(type, callback);
|
||||
};
|
||||
|
||||
return {
|
||||
addListener,
|
||||
removeListener,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const emit = React.useCallback(
|
||||
({ type, data, target }: { type: string; data?: any; target?: string }) => {
|
||||
const items = listeners.current[type] || {};
|
||||
|
||||
// Copy the current list of callbacks in case they are mutated during execution
|
||||
const callbacks =
|
||||
target !== undefined
|
||||
? items[target] && items[target].slice()
|
||||
: ([] as Listeners).concat(...Object.keys(items).map(t => items[t]));
|
||||
|
||||
callbacks && callbacks.forEach(cb => cb(data));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return React.useMemo(() => ({ create, emit }), [create, emit]);
|
||||
}
|
||||
70
src/useFocusEvents.tsx
Normal file
70
src/useFocusEvents.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import * as React from 'react';
|
||||
import NavigationContext from './NavigationContext';
|
||||
import { NavigationEventEmitter } from './useEventEmitter';
|
||||
import { NavigationState } from './types';
|
||||
|
||||
type Options = {
|
||||
state: NavigationState;
|
||||
emitter: NavigationEventEmitter;
|
||||
};
|
||||
|
||||
export default function useFocusEvents({ state, emitter }: Options) {
|
||||
const navigation = React.useContext(NavigationContext);
|
||||
const lastFocusedKeyRef = React.useRef<string | undefined>();
|
||||
|
||||
const currentFocusedKey = state.routes[state.index].key;
|
||||
|
||||
React.useEffect(
|
||||
() =>
|
||||
navigation &&
|
||||
navigation.addListener('focus', () =>
|
||||
emitter.emit({ type: 'focus', target: currentFocusedKey })
|
||||
),
|
||||
[currentFocusedKey, emitter, navigation]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() =>
|
||||
navigation &&
|
||||
navigation.addListener('blur', () =>
|
||||
emitter.emit({ type: 'blur', target: currentFocusedKey })
|
||||
),
|
||||
[currentFocusedKey, emitter, navigation]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const lastFocusedKey = lastFocusedKeyRef.current;
|
||||
|
||||
lastFocusedKeyRef.current = currentFocusedKey;
|
||||
|
||||
// We wouldn't have `lastFocusedKey` on initial mount
|
||||
// Fire focus event for the current route on mount if there's no parent navigator
|
||||
if (lastFocusedKey === undefined && !navigation) {
|
||||
emitter.emit({ type: 'focus', target: currentFocusedKey });
|
||||
}
|
||||
|
||||
// We should only dispatch events when the focused key changed and navigator is focused
|
||||
// When navigator is not focused, screens inside shouldn't receive focused status either
|
||||
if (
|
||||
lastFocusedKey === currentFocusedKey ||
|
||||
!(navigation ? navigation.isFocused() : true)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.routes.forEach((route, i) => {
|
||||
if (
|
||||
lastFocusedKey === undefined ||
|
||||
(route.key !== lastFocusedKey && route.key !== currentFocusedKey)
|
||||
) {
|
||||
// Only fire events after mount, or if focus state of this route changed
|
||||
return;
|
||||
}
|
||||
|
||||
emitter.emit({
|
||||
type: i === state.index ? 'focus' : 'blur',
|
||||
target: route.key,
|
||||
});
|
||||
});
|
||||
}, [currentFocusedKey, emitter, navigation, state.index, state.routes]);
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import NavigationContext from './NavigationContext';
|
||||
import { NavigationProp, ParamListBase } from './types';
|
||||
|
||||
export default function useNavigation() {
|
||||
export default function useNavigation<
|
||||
T extends NavigationProp<ParamListBase>
|
||||
>(): T {
|
||||
const navigation = React.useContext(NavigationContext);
|
||||
|
||||
if (navigation === undefined) {
|
||||
@@ -10,5 +13,5 @@ export default function useNavigation() {
|
||||
);
|
||||
}
|
||||
|
||||
return navigation;
|
||||
return navigation as T;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import * as React from 'react';
|
||||
import { NavigationStateContext } from './NavigationContainer';
|
||||
import Screen from './Screen';
|
||||
import useEventEmitter from './useEventEmitter';
|
||||
import useRegisterNavigator from './useRegisterNavigator';
|
||||
import useDescriptors from './useDescriptors';
|
||||
import useNavigationHelpers from './useNavigationHelpers';
|
||||
import useOnAction from './useOnAction';
|
||||
import useFocusEvents from './useFocusEvents';
|
||||
import useOnRouteFocus from './useOnRouteFocus';
|
||||
import useChildActionListeners from './useChildActionListeners';
|
||||
import {
|
||||
@@ -155,6 +157,10 @@ export default function useNavigationBuilder<
|
||||
: (currentState as State);
|
||||
}, [getCurrentState, initialState]);
|
||||
|
||||
const emitter = useEventEmitter();
|
||||
|
||||
useFocusEvents({ state, emitter });
|
||||
|
||||
const {
|
||||
listeners: actionListeners,
|
||||
addActionListener,
|
||||
@@ -181,6 +187,7 @@ export default function useNavigationBuilder<
|
||||
onAction,
|
||||
getState,
|
||||
setState,
|
||||
emitter,
|
||||
actionCreators: router.actionCreators,
|
||||
});
|
||||
|
||||
@@ -194,6 +201,7 @@ export default function useNavigationBuilder<
|
||||
onRouteFocus,
|
||||
addActionListener,
|
||||
removeActionListener,
|
||||
emitter,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { NavigationEventEmitter } from './useEventEmitter';
|
||||
import {
|
||||
NavigationAction,
|
||||
NavigationHelpers,
|
||||
@@ -9,30 +10,41 @@ import {
|
||||
|
||||
type Options = {
|
||||
state: NavigationState;
|
||||
getState: () => NavigationState;
|
||||
navigation: NavigationHelpers<ParamListBase>;
|
||||
setOptions: (
|
||||
cb: (options: { [key: string]: object }) => { [key: string]: object }
|
||||
) => void;
|
||||
emitter: NavigationEventEmitter;
|
||||
};
|
||||
|
||||
type NavigationCache = { [key: string]: NavigationProp<ParamListBase> };
|
||||
|
||||
export default function useNavigationCache({
|
||||
state,
|
||||
getState,
|
||||
navigation,
|
||||
setOptions,
|
||||
emitter,
|
||||
}: Options) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const cache = React.useMemo(() => ({ current: {} as NavigationCache }), [
|
||||
getState,
|
||||
navigation,
|
||||
setOptions,
|
||||
emitter,
|
||||
]);
|
||||
|
||||
cache.current = state.routes.reduce<NavigationCache>((acc, route) => {
|
||||
acc[route.key] =
|
||||
cache.current[route.key] ||
|
||||
({
|
||||
...navigation,
|
||||
cache.current = state.routes.reduce<NavigationCache>((acc, route, index) => {
|
||||
if (cache.current[route.key]) {
|
||||
acc[route.key] = cache.current[route.key];
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { emit, ...rest } = navigation;
|
||||
|
||||
acc[route.key] = {
|
||||
...rest,
|
||||
...emitter.create(route.key),
|
||||
dispatch: (
|
||||
action:
|
||||
| NavigationAction
|
||||
@@ -48,7 +60,17 @@ export default function useNavigationCache({
|
||||
...o,
|
||||
[route.key]: { ...o[route.key], ...options },
|
||||
})),
|
||||
} as NavigationProp<ParamListBase>);
|
||||
isFocused: () => {
|
||||
const state = getState();
|
||||
|
||||
if (index !== state.index) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return navigation ? navigation.isFocused() : true;
|
||||
},
|
||||
} as NavigationProp<ParamListBase>;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as React from 'react';
|
||||
import * as BaseActions from './BaseActions';
|
||||
import NavigationContext from './NavigationContext';
|
||||
import { NavigationStateContext } from './NavigationContainer';
|
||||
import { NavigationEventEmitter } from './useEventEmitter';
|
||||
import {
|
||||
NavigationHelpers,
|
||||
NavigationAction,
|
||||
@@ -18,6 +19,7 @@ type Options<Action extends NavigationAction> = {
|
||||
getState: () => NavigationState;
|
||||
setState: (state: NavigationState) => void;
|
||||
actionCreators?: ActionCreators<Action>;
|
||||
emitter: NavigationEventEmitter;
|
||||
};
|
||||
|
||||
export default function useNavigationHelpers<Action extends NavigationAction>({
|
||||
@@ -25,6 +27,7 @@ export default function useNavigationHelpers<Action extends NavigationAction>({
|
||||
getState,
|
||||
setState,
|
||||
actionCreators,
|
||||
emitter,
|
||||
}: Options<Action>) {
|
||||
const parentNavigationHelpers = React.useContext(NavigationContext);
|
||||
const { performTransaction } = React.useContext(NavigationStateContext);
|
||||
@@ -59,10 +62,15 @@ export default function useNavigationHelpers<Action extends NavigationAction>({
|
||||
{} as { [key: string]: () => void }
|
||||
),
|
||||
dispatch,
|
||||
emit: emitter.emit,
|
||||
isFocused: parentNavigationHelpers
|
||||
? parentNavigationHelpers.isFocused
|
||||
: () => true,
|
||||
};
|
||||
}, [
|
||||
actionCreators,
|
||||
parentNavigationHelpers,
|
||||
emitter.emit,
|
||||
performTransaction,
|
||||
setState,
|
||||
getState,
|
||||
|
||||
Reference in New Issue
Block a user