From 02aa2b838bf0be31bdc1b99a16a4029ce90e885e Mon Sep 17 00:00:00 2001 From: Satyajit Sahoo Date: Mon, 23 Oct 2023 10:54:20 +0200 Subject: [PATCH] refactor: handle links in PlatformPressable --- .../bottom-tabs/src/views/BottomTabItem.tsx | 55 ++++---------- packages/drawer/src/views/DrawerItem.tsx | 76 ++----------------- .../drawer/src/views/DrawerToggleButton.tsx | 2 - .../elements/src/Header/HeaderBackButton.tsx | 16 +--- .../elements/src/Header/HeaderBackContext.tsx | 2 +- packages/elements/src/PlatformPressable.tsx | 27 +++++++ packages/native-stack/src/types.tsx | 6 +- .../src/views/NativeStackView.native.tsx | 21 +++-- .../src/views/NativeStackView.tsx | 1 + packages/native/src/useLinkProps.tsx | 18 +++-- packages/stack/src/types.tsx | 4 +- packages/stack/src/views/Header/Header.tsx | 2 +- .../stack/src/views/Header/HeaderSegment.tsx | 6 +- .../stack/src/views/Stack/CardContainer.tsx | 14 +++- 14 files changed, 100 insertions(+), 150 deletions(-) diff --git a/packages/bottom-tabs/src/views/BottomTabItem.tsx b/packages/bottom-tabs/src/views/BottomTabItem.tsx index af050d837f..c4e915c1d5 100644 --- a/packages/bottom-tabs/src/views/BottomTabItem.tsx +++ b/packages/bottom-tabs/src/views/BottomTabItem.tsx @@ -1,11 +1,10 @@ -import { getLabel, Label } from '@react-navigation/elements'; -import { CommonActions, Link, Route, useTheme } from '@react-navigation/native'; +import { getLabel, Label, PlatformPressable } from '@react-navigation/elements'; +import { Route, useTheme } from '@react-navigation/native'; import Color from 'color'; import React from 'react'; import { GestureResponderEvent, Platform, - Pressable, StyleProp, StyleSheet, TextStyle, @@ -145,40 +144,19 @@ export function BottomTabItem({ accessibilityRole, ...rest }: BottomTabBarButtonProps) => { - if (Platform.OS === 'web') { - // React Native Web doesn't forward `onClick` if we use `TouchableWithoutFeedback`. - // We need to use `onClick` to be able to prevent default browser handling of links. - return ( - { - 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); - } - }} - > - {children} - - ); - } else { - return ( - - {children} - - ); - } + return ( + + {children} + + ); }, accessibilityLabel, testID, @@ -324,7 +302,4 @@ const styles = StyleSheet.create({ fontSize: 13, marginLeft: 20, }, - button: { - display: 'flex', - }, }); diff --git a/packages/drawer/src/views/DrawerItem.tsx b/packages/drawer/src/views/DrawerItem.tsx index 88ea3ef10e..4f04cfa8ec 100644 --- a/packages/drawer/src/views/DrawerItem.tsx +++ b/packages/drawer/src/views/DrawerItem.tsx @@ -1,9 +1,8 @@ import { PlatformPressable } from '@react-navigation/elements'; -import { CommonActions, Link, Route, useTheme } from '@react-navigation/native'; +import { Route, useTheme } from '@react-navigation/native'; import Color from 'color'; import * as React from 'react'; import { - Platform, StyleProp, StyleSheet, Text, @@ -96,65 +95,6 @@ type Props = { testID?: string; }; -const LinkPressable = ({ - route, - href, - children, - style, - onPress, - onLongPress, - onPressIn, - onPressOut, - accessibilityRole, - ...rest -}: Omit, 'style'> & { - style: StyleProp; -} & { - route: Route; - href?: string; - children: React.ReactNode; - onPress?: () => void; -}) => { - if (Platform.OS === 'web') { - // React Native Web doesn't forward `onClick` if we use `TouchableWithoutFeedback`. - // We need to use `onClick` to be able to prevent default browser handling of links. - return ( - { - 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); - } - }} - // types for PressableProps and TextProps are incompatible with each other by `null` so we - // can't use {...rest} for these 3 props - onLongPress={onLongPress ?? undefined} - onPressIn={onPressIn ?? undefined} - onPressOut={onPressOut ?? undefined} - > - {children} - - ); - } else { - return ( - - {children} - - ); - } -}; - /** * A component used to show an action item with an icon and a label in a navigation drawer. */ @@ -162,7 +102,6 @@ export function DrawerItem(props: Props) { const { colors, fonts } = useTheme(); const { - route, href, icon, label, @@ -196,19 +135,17 @@ export function DrawerItem(props: Props) { {...rest} style={[styles.container, { borderRadius, backgroundColor }, style]} > - - + {iconNode} - - + + ); } @@ -249,7 +186,4 @@ const styles = StyleSheet.create({ marginRight: 32, flex: 1, }, - button: { - display: 'flex', - }, }); diff --git a/packages/drawer/src/views/DrawerToggleButton.tsx b/packages/drawer/src/views/DrawerToggleButton.tsx index 8e5f980151..a23649bf3b 100644 --- a/packages/drawer/src/views/DrawerToggleButton.tsx +++ b/packages/drawer/src/views/DrawerToggleButton.tsx @@ -22,8 +22,6 @@ export function DrawerToggleButton({ tintColor, ...rest }: Props) { return ( navigation.dispatch(DrawerActions.toggleDrawer())} style={styles.touchable} diff --git a/packages/elements/src/Header/HeaderBackButton.tsx b/packages/elements/src/Header/HeaderBackButton.tsx index b890023282..7df9715b62 100644 --- a/packages/elements/src/Header/HeaderBackButton.tsx +++ b/packages/elements/src/Header/HeaderBackButton.tsx @@ -148,27 +148,19 @@ export function HeaderBackButton({ ); }; - const handlePress = (e: any) => { - const ignoreEvents = - !(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) && // ignore clicks with modifier keys - (e.button == null || e.button === 0); // ignore everything but left clicks - - if (Platform.OS === 'web' && href && ignoreEvents) { - e.preventDefault(); + const handlePress = () => { + if (onPress) { + requestAnimationFrame(() => onPress()); } - - return onPress && requestAnimationFrame(onPress); }; return ( ('HeaderBackContext', undefined); diff --git a/packages/elements/src/PlatformPressable.tsx b/packages/elements/src/PlatformPressable.tsx index 125a9b1036..698a4b4249 100644 --- a/packages/elements/src/PlatformPressable.tsx +++ b/packages/elements/src/PlatformPressable.tsx @@ -29,6 +29,8 @@ const ANDROID_SUPPORTS_RIPPLE = * PlatformPressable provides an abstraction on top of Pressable to handle platform differences. */ export function PlatformPressable({ + disabled, + onPress, onPressIn, onPressOut, android_ripple, @@ -53,6 +55,26 @@ export function PlatformPressable({ }).start(); }; + const handlePress = (e: GestureResponderEvent) => { + // @ts-expect-error: these properties exist on web, but not in React Native + const hasModifierKey = e.metaKey || e.altKey || e.ctrlKey || e.shiftKey; // ignore clicks with modifier keys + // @ts-expect-error: these properties exist on web, but not in React Native + const isLeftClick = e.button == null || e.button === 0; // only handle left clicks + const isSelfTarget = [undefined, null, '', 'self'].includes( + // @ts-expect-error: these properties exist on web, but not in React Native + e.currentTarget?.target + ); // let browser handle "target=_blank" etc. + + if (Platform.OS === 'web' && rest.href != null) { + if (!hasModifierKey && isLeftClick && isSelfTarget) { + e.preventDefault(); + onPress?.(e); + } + } else { + onPress?.(e); + } + }; + const handlePressIn = (e: GestureResponderEvent) => { animateTo(pressOpacity, 0); onPressIn?.(e); @@ -65,6 +87,11 @@ export function PlatformPressable({ return ( ({ + // No href needed for native + href: undefined, + title: backTitle, + }), + [backTitle] + ); const isRemovePrevented = preventedRoutes[route.key]?.preventRemove; diff --git a/packages/native-stack/src/views/NativeStackView.tsx b/packages/native-stack/src/views/NativeStackView.tsx index 41e25cd378..c9c6bc7792 100644 --- a/packages/native-stack/src/views/NativeStackView.tsx +++ b/packages/native-stack/src/views/NativeStackView.tsx @@ -130,6 +130,7 @@ export function NativeStackView({ state, descriptors }: Props) { ) : undefined } + canGoBack={canGoBack} onPress={navigation.goBack} href={headerBack.href} /> diff --git a/packages/native/src/useLinkProps.tsx b/packages/native/src/useLinkProps.tsx index 33592bfc20..3e911ed3de 100644 --- a/packages/native/src/useLinkProps.tsx +++ b/packages/native/src/useLinkProps.tsx @@ -80,18 +80,24 @@ export function useLinkProps({ const onPress = ( e?: React.MouseEvent | GestureResponderEvent ) => { + // @ts-expect-error: these properties exist on web, but not in React Native + const hasModifierKey = e.metaKey || e.altKey || e.ctrlKey || e.shiftKey; // ignore clicks with modifier keys + // @ts-expect-error: these properties exist on web, but not in React Native + const isLeftClick = e.button == null || e.button === 0; // only handle left clicks + const isSelfTarget = [undefined, null, '', 'self'].includes( + // @ts-expect-error: these properties exist on web, but not in React Native + e.currentTarget?.target + ); // let browser handle "target=_blank" etc. + let shouldHandle = false; if (Platform.OS !== 'web' || !e) { shouldHandle = e ? !e.defaultPrevented : true; } else if ( !e.defaultPrevented && // onPress prevented default - // @ts-expect-error: these properties exist on web, but not in React Native - !(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) && // ignore clicks with modifier keys - // @ts-expect-error: these properties exist on web, but not in React Native - (e.button == null || e.button === 0) && // ignore everything but left clicks - // @ts-expect-error: these properties exist on web, but not in React Native - [undefined, null, '', 'self'].includes(e.currentTarget?.target) // let browser handle "target=_blank" etc. + !hasModifierKey && + isLeftClick && + isSelfTarget ) { e.preventDefault(); shouldHandle = true; diff --git a/packages/stack/src/types.tsx b/packages/stack/src/types.tsx index 6fa3e9566b..e742773aba 100644 --- a/packages/stack/src/types.tsx +++ b/packages/stack/src/types.tsx @@ -197,11 +197,11 @@ export type StackHeaderProps = { /** * Title of the previous screen. */ - title: string; + title: string | undefined; /** * The `href` to use for the anchor tag on web */ - href?: string; + href: string | undefined; }; /** * Animated nodes representing the progress of the animation. diff --git a/packages/stack/src/views/Header/Header.tsx b/packages/stack/src/views/Header/Header.tsx index e5f0325f93..1ca2dd58f3 100644 --- a/packages/stack/src/views/Header/Header.tsx +++ b/packages/stack/src/views/Header/Header.tsx @@ -66,7 +66,7 @@ export const Header = React.memo(function Header({ } headerStatusBarHeight={statusBarHeight} onGoBack={back ? goBack : undefined} - href={back ? back.href : undefined} + backHref={back ? back.href : undefined} styleInterpolator={styleInterpolator} /> ); diff --git a/packages/stack/src/views/Header/HeaderSegment.tsx b/packages/stack/src/views/Header/HeaderSegment.tsx index a9ee734d8a..8e0e3cfbb0 100644 --- a/packages/stack/src/views/Header/HeaderSegment.tsx +++ b/packages/stack/src/views/Header/HeaderSegment.tsx @@ -29,7 +29,7 @@ type Props = Omit & { title: string; modal: boolean; onGoBack?: () => void; - href?: string; + backHref?: string; progress: SceneProgress; styleInterpolator: StackHeaderStyleInterpolator; }; @@ -106,11 +106,11 @@ export function HeaderSegment(props: Props) { layout, modal, onGoBack, - href, + backHref, headerTitle: title, headerLeft: left = onGoBack ? (props: HeaderBackButtonProps) => ( - + ) : undefined, headerRight: right, diff --git a/packages/stack/src/views/Stack/CardContainer.tsx b/packages/stack/src/views/Stack/CardContainer.tsx index 4da1526d80..eaeaf26a8b 100644 --- a/packages/stack/src/views/Stack/CardContainer.tsx +++ b/packages/stack/src/views/Stack/CardContainer.tsx @@ -4,7 +4,12 @@ import { HeaderHeightContext, HeaderShownContext, } from '@react-navigation/elements'; -import { Route, useLocale, useTheme } from '@react-navigation/native'; +import { + Route, + useLinkTools, + useLocale, + useTheme, +} from '@react-navigation/native'; import * as React from 'react'; import { Animated, StyleSheet, View } from 'react-native'; @@ -202,19 +207,22 @@ function CardContainerInner({ transitionSpec, } = scene.descriptor.options; + const { buildHref } = useLinkTools(); const previousScene = getPreviousScene({ route: scene.descriptor.route }); let backTitle: string | undefined; + let href: string | undefined; if (previousScene) { const { options, route } = previousScene.descriptor; backTitle = getHeaderTitle(options, route.name); + href = buildHref(route.name, route.params); } const headerBack = React.useMemo( - () => (backTitle !== undefined ? { title: backTitle } : undefined), - [backTitle] + () => ({ title: backTitle, href }), + [backTitle, href] ); return (