mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-04-28 20:35:19 +08:00
feat: let the navigator specify if default can be prevented
This commit is contained in:
@@ -17,11 +17,11 @@ export type BottomTabNavigationEventMap = {
|
||||
/**
|
||||
* Event which fires on tapping on the tab in the tab bar.
|
||||
*/
|
||||
tabPress: undefined;
|
||||
tabPress: { data: undefined; canPreventDefault: true };
|
||||
/**
|
||||
* Event which fires on long press on the tab in the tab bar.
|
||||
*/
|
||||
tabLongPress: undefined;
|
||||
tabLongPress: { data: undefined };
|
||||
};
|
||||
|
||||
export type LabelPosition = 'beside-icon' | 'below-icon';
|
||||
|
||||
@@ -205,6 +205,7 @@ export default function BottomTabBar({
|
||||
const event = navigation.emit({
|
||||
type: 'tabPress',
|
||||
target: route.key,
|
||||
canPreventDefault: true,
|
||||
});
|
||||
|
||||
if (!focused && !event.defaultPrevented) {
|
||||
|
||||
@@ -391,6 +391,8 @@ it('fires custom events', () => {
|
||||
expect(thirdCallback).toBeCalledTimes(1);
|
||||
expect(thirdCallback.mock.calls[0][0].type).toBe('someSuperCoolEvent');
|
||||
expect(thirdCallback.mock.calls[0][0].data).toBe(42);
|
||||
expect(thirdCallback.mock.calls[0][0].defaultPrevented).toBe(undefined);
|
||||
expect(thirdCallback.mock.calls[0][0].preventDefault).toBe(undefined);
|
||||
|
||||
act(() => {
|
||||
ref.current.navigation.emit({ type: eventName });
|
||||
@@ -400,3 +402,62 @@ it('fires custom events', () => {
|
||||
expect(secondCallback).toBeCalledTimes(1);
|
||||
expect(thirdCallback).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('has option to prevent default', () => {
|
||||
expect.assertions(5);
|
||||
|
||||
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 callback = (e: any) => {
|
||||
expect(e.type).toBe('someSuperCoolEvent');
|
||||
expect(e.data).toBe(42);
|
||||
expect(e.defaultPrevented).toBe(false);
|
||||
expect(e.preventDefault).not.toBe(undefined);
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
expect(e.defaultPrevented).toBe(true);
|
||||
};
|
||||
|
||||
const Test = ({ 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={Test} />
|
||||
</TestNavigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
render(element);
|
||||
|
||||
act(() => {
|
||||
ref.current.navigation.emit({
|
||||
type: eventName,
|
||||
data: 42,
|
||||
canPreventDefault: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -207,31 +207,51 @@ export type Router<
|
||||
|
||||
export type ParamListBase = Record<string, object | undefined>;
|
||||
|
||||
export type EventMapBase = {
|
||||
focus: undefined;
|
||||
blur: undefined;
|
||||
export type EventMapBase = Record<
|
||||
string,
|
||||
{ data?: any; canPreventDefault?: boolean }
|
||||
>;
|
||||
|
||||
export type EventMapCore = {
|
||||
focus: { data: undefined };
|
||||
blur: { data: undefined };
|
||||
};
|
||||
|
||||
export type EventArg<EventName extends string, Data = undefined> = {
|
||||
export type EventArg<
|
||||
EventName extends string,
|
||||
CanPreventDefault extends boolean | undefined = false,
|
||||
Data = undefined
|
||||
> = {
|
||||
/**
|
||||
* Type of the event (e.g. `focus`, `blur`)
|
||||
*/
|
||||
readonly type: EventName;
|
||||
/**
|
||||
* Whether `event.preventDefault()` was called on this event object.
|
||||
*/
|
||||
readonly defaultPrevented: boolean;
|
||||
/**
|
||||
* Prevent the default action which happens on this event.
|
||||
*/
|
||||
preventDefault(): void;
|
||||
} & (Data extends undefined ? {} : { readonly data: Data });
|
||||
} & (CanPreventDefault extends true
|
||||
? {
|
||||
/**
|
||||
* Whether `event.preventDefault()` was called on this event object.
|
||||
*/
|
||||
readonly defaultPrevented: boolean;
|
||||
/**
|
||||
* Prevent the default action which happens on this event.
|
||||
*/
|
||||
preventDefault(): void;
|
||||
}
|
||||
: {}) &
|
||||
(Data extends undefined ? {} : { readonly data: Data });
|
||||
|
||||
export type EventListenerCallback<EventName extends string, Data> = (
|
||||
e: EventArg<EventName, Data>
|
||||
export type EventListenerCallback<
|
||||
EventMap extends EventMapBase,
|
||||
EventName extends keyof EventMap
|
||||
> = (
|
||||
e: EventArg<
|
||||
Extract<EventName, string>,
|
||||
EventMap[EventName]['canPreventDefault'],
|
||||
EventMap[EventName]['data']
|
||||
>
|
||||
) => void;
|
||||
|
||||
export type EventConsumer<EventMap extends Record<string, any>> = {
|
||||
export type EventConsumer<EventMap extends EventMapBase> = {
|
||||
/**
|
||||
* Subscribe to events from the parent navigator.
|
||||
*
|
||||
@@ -240,15 +260,15 @@ export type EventConsumer<EventMap extends Record<string, any>> = {
|
||||
*/
|
||||
addListener<EventName extends Extract<keyof EventMap, string>>(
|
||||
type: EventName,
|
||||
callback: EventListenerCallback<EventName, EventMap[EventName]>
|
||||
callback: EventListenerCallback<EventMap, EventName>
|
||||
): () => void;
|
||||
removeListener<EventName extends Extract<keyof EventMap, string>>(
|
||||
type: EventName,
|
||||
callback: EventListenerCallback<EventName, EventMap[EventName]>
|
||||
callback: EventListenerCallback<EventMap, EventName>
|
||||
): void;
|
||||
};
|
||||
|
||||
export type EventEmitter<EventMap extends Record<string, any>> = {
|
||||
export type EventEmitter<EventMap extends EventMapBase> = {
|
||||
/**
|
||||
* Emit an event to child screens.
|
||||
*
|
||||
@@ -257,14 +277,19 @@ export type EventEmitter<EventMap extends Record<string, any>> = {
|
||||
* @param [options.target] Key of the target route which should receive the event.
|
||||
* If not specified, all routes receive the event.
|
||||
*/
|
||||
emit<EventName extends Extract<keyof EventMap, string>>(
|
||||
emit<
|
||||
EventName extends Extract<keyof EventMap, string>,
|
||||
CanPreventDefault extends EventMap[EventName]['canPreventDefault'],
|
||||
Data extends EventMap[EventName]['data']
|
||||
>(
|
||||
options: {
|
||||
type: EventName;
|
||||
target?: string;
|
||||
} & (EventMap[EventName] extends undefined
|
||||
? {}
|
||||
: { data: EventMap[EventName] })
|
||||
): EventArg<EventName, EventMap[EventName]>;
|
||||
} & (CanPreventDefault extends true
|
||||
? { canPreventDefault: CanPreventDefault }
|
||||
: {}) &
|
||||
(Data extends undefined ? {} : { data: Data })
|
||||
): EventArg<EventName, CanPreventDefault, Data>;
|
||||
};
|
||||
|
||||
export class PrivateValueStore<A, B, C> {
|
||||
@@ -368,7 +393,7 @@ type NavigationHelpersCommon<
|
||||
|
||||
export type NavigationHelpers<
|
||||
ParamList extends ParamListBase,
|
||||
EventMap extends Record<string, any> = {}
|
||||
EventMap extends EventMapBase = {}
|
||||
> = NavigationHelpersCommon<ParamList> &
|
||||
EventEmitter<EventMap> & {
|
||||
/**
|
||||
@@ -408,7 +433,7 @@ export type NavigationProp<
|
||||
RouteName extends keyof ParamList = string,
|
||||
State extends NavigationState = NavigationState,
|
||||
ScreenOptions extends object = {},
|
||||
EventMap extends Record<string, any> = {}
|
||||
EventMap extends EventMapBase = {}
|
||||
> = NavigationHelpersCommon<ParamList, State> & {
|
||||
/**
|
||||
* Update the param object for the route.
|
||||
@@ -439,7 +464,7 @@ export type NavigationProp<
|
||||
* Note that this method doesn't re-render screen when the result changes. So don't use it in `render`.
|
||||
*/
|
||||
dangerouslyGetState(): State;
|
||||
} & EventConsumer<EventMap & EventMapBase> &
|
||||
} & EventConsumer<EventMap & EventMapCore> &
|
||||
PrivateValueStore<ParamList, RouteName, EventMap>;
|
||||
|
||||
export type RouteProp<
|
||||
@@ -493,7 +518,7 @@ export type Descriptor<
|
||||
RouteName extends keyof ParamList = string,
|
||||
State extends NavigationState = NavigationState,
|
||||
ScreenOptions extends object = {},
|
||||
EventMap extends Record<string, any> = {}
|
||||
EventMap extends EventMapBase = {}
|
||||
> = {
|
||||
/**
|
||||
* Render the component associated with this route.
|
||||
|
||||
@@ -43,7 +43,17 @@ export default function useEventEmitter(): NavigationEventEmitter {
|
||||
}, []);
|
||||
|
||||
const emit = React.useCallback(
|
||||
({ type, data, target }: { type: string; data?: any; target?: string }) => {
|
||||
({
|
||||
type,
|
||||
data,
|
||||
target,
|
||||
canPreventDefault,
|
||||
}: {
|
||||
type: string;
|
||||
data?: any;
|
||||
target?: string;
|
||||
canPreventDefault?: boolean;
|
||||
}) => {
|
||||
const items = listeners.current[type] || {};
|
||||
|
||||
// Copy the current list of callbacks in case they are mutated during execution
|
||||
@@ -52,26 +62,40 @@ export default function useEventEmitter(): NavigationEventEmitter {
|
||||
? items[target] && items[target].slice()
|
||||
: ([] as Listeners).concat(...Object.keys(items).map(t => items[t]));
|
||||
|
||||
let defaultPrevented = false;
|
||||
|
||||
const event: EventArg<any, any> = {
|
||||
const event: EventArg<any, any, any> = {
|
||||
get type() {
|
||||
return type;
|
||||
},
|
||||
get data() {
|
||||
return data;
|
||||
},
|
||||
get defaultPrevented() {
|
||||
return defaultPrevented;
|
||||
},
|
||||
preventDefault() {
|
||||
defaultPrevented = true;
|
||||
},
|
||||
};
|
||||
|
||||
if (data !== undefined) {
|
||||
Object.defineProperty(event, 'data', {
|
||||
get() {
|
||||
return data;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (canPreventDefault) {
|
||||
let defaultPrevented = false;
|
||||
|
||||
Object.defineProperties(event, {
|
||||
defaultPrevented: {
|
||||
get() {
|
||||
return defaultPrevented;
|
||||
},
|
||||
},
|
||||
preventDefault: {
|
||||
value() {
|
||||
defaultPrevented = true;
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
callbacks?.forEach(cb => cb(event));
|
||||
|
||||
return event;
|
||||
return event as any;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
@@ -173,11 +173,11 @@ export type DrawerNavigationEventMap = {
|
||||
/**
|
||||
* Event which fires when the drawer opens.
|
||||
*/
|
||||
drawerOpen: undefined;
|
||||
drawerOpen: { data: undefined };
|
||||
/**
|
||||
* Event which fires when the drawer closes.
|
||||
*/
|
||||
drawerClose: undefined;
|
||||
drawerClose: { data: undefined };
|
||||
};
|
||||
|
||||
export type DrawerNavigationHelpers = NavigationHelpers<
|
||||
|
||||
@@ -11,7 +11,7 @@ export type MaterialBottomTabNavigationEventMap = {
|
||||
/**
|
||||
* Event which fires on tapping on the tab in the tab bar.
|
||||
*/
|
||||
tabPress: undefined;
|
||||
tabPress: { data: undefined };
|
||||
};
|
||||
|
||||
export type MaterialBottomTabNavigationHelpers = NavigationHelpers<
|
||||
|
||||
@@ -13,19 +13,19 @@ export type MaterialTopTabNavigationEventMap = {
|
||||
/**
|
||||
* Event which fires on tapping on the tab in the tab bar.
|
||||
*/
|
||||
tabPress: undefined;
|
||||
tabPress: { data: undefined; canPreventDefault: true };
|
||||
/**
|
||||
* Event which fires on long press on the tab in the tab bar.
|
||||
*/
|
||||
tabLongPress: undefined;
|
||||
tabLongPress: { data: undefined };
|
||||
/**
|
||||
* Event which fires when a swipe gesture starts, i.e. finger touches the screen.
|
||||
*/
|
||||
swipeStart: undefined;
|
||||
swipeStart: { data: undefined };
|
||||
/**
|
||||
* Event which fires when a swipe gesture ends, i.e. finger leaves the screen.
|
||||
*/
|
||||
swipeEnd: undefined;
|
||||
swipeEnd: { data: undefined };
|
||||
};
|
||||
|
||||
export type MaterialTopTabNavigationHelpers = NavigationHelpers<
|
||||
|
||||
@@ -49,6 +49,7 @@ export default function TabBarTop(props: MaterialTopTabBarProps) {
|
||||
const event = navigation.emit({
|
||||
type: 'tabPress',
|
||||
target: route.key,
|
||||
canPreventDefault: true,
|
||||
});
|
||||
|
||||
if (event.defaultPrevented) {
|
||||
|
||||
@@ -44,13 +44,17 @@ function NativeStackNavigator(props: NativeStackNavigatorProps) {
|
||||
React.useEffect(
|
||||
() =>
|
||||
navigation.addListener &&
|
||||
navigation.addListener('tabPress', (e: EventArg<'tabPress'>) => {
|
||||
navigation.addListener('tabPress', e => {
|
||||
const isFocused = navigation.isFocused();
|
||||
|
||||
// Run the operation in the next frame so we're sure all listeners have been run
|
||||
// This is necessary to know if preventDefault() has been called
|
||||
requestAnimationFrame(() => {
|
||||
if (state.index > 0 && isFocused && !e.defaultPrevented) {
|
||||
if (
|
||||
state.index > 0 &&
|
||||
isFocused &&
|
||||
!(e as EventArg<'tabPress', true>).defaultPrevented
|
||||
) {
|
||||
// When user taps on already focused tab and we're inside the tab,
|
||||
// reset the stack to replicate native behaviour
|
||||
navigation.dispatch({
|
||||
|
||||
@@ -66,7 +66,7 @@ export default function useScrollToTop(
|
||||
// in addition, there are multiple tab implementations
|
||||
// @ts-ignore
|
||||
'tabPress',
|
||||
(e: EventArg<'tabPress'>) => {
|
||||
(e: EventArg<'tabPress', true>) => {
|
||||
// We should scroll to top only when the screen is focused
|
||||
const isFocused = navigation.isFocused();
|
||||
|
||||
|
||||
@@ -42,13 +42,17 @@ function StackNavigator({
|
||||
React.useEffect(
|
||||
() =>
|
||||
navigation.addListener &&
|
||||
navigation.addListener('tabPress', (e: EventArg<'tabPress'>) => {
|
||||
navigation.addListener('tabPress', e => {
|
||||
const isFocused = navigation.isFocused();
|
||||
|
||||
// Run the operation in the next frame so we're sure all listeners have been run
|
||||
// This is necessary to know if preventDefault() has been called
|
||||
requestAnimationFrame(() => {
|
||||
if (state.index > 0 && isFocused && !e.defaultPrevented) {
|
||||
if (
|
||||
state.index > 0 &&
|
||||
isFocused &&
|
||||
!(e as EventArg<'tabPress', true>).defaultPrevented
|
||||
) {
|
||||
// When user taps on already focused tab and we're inside the tab,
|
||||
// reset the stack to replicate native behaviour
|
||||
navigation.dispatch({
|
||||
|
||||
@@ -20,11 +20,11 @@ export type StackNavigationEventMap = {
|
||||
/**
|
||||
* Event which fires when a transition animation starts.
|
||||
*/
|
||||
transitionStart: { closing: boolean };
|
||||
transitionStart: { data: { closing: boolean } };
|
||||
/**
|
||||
* Event which fires when a transition animation ends.
|
||||
*/
|
||||
transitionEnd: { closing: boolean };
|
||||
transitionEnd: { data: { closing: boolean } };
|
||||
};
|
||||
|
||||
export type StackNavigationHelpers = NavigationHelpers<
|
||||
|
||||
Reference in New Issue
Block a user