Skip to content

Commit

Permalink
refactor: handle links in PlatformPressable
Browse files Browse the repository at this point in the history
  • Loading branch information
satya164 committed Oct 23, 2023
1 parent 5561b2b commit 02aa2b8
Show file tree
Hide file tree
Showing 14 changed files with 100 additions and 150 deletions.
55 changes: 15 additions & 40 deletions 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,
Expand Down Expand Up @@ -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 (
<Link
{...rest}
href={href}
action={CommonActions.navigate(route.name, route.params)}
style={[styles.button, style]}
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);
}
}}
>
{children}
</Link>
);
} else {
return (
<Pressable
{...rest}
accessibilityRole={accessibilityRole}
onPress={onPress}
style={style}
>
{children}
</Pressable>
);
}
return (
<PlatformPressable
{...rest}
android_ripple={{ borderless: true }}
pressOpacity={1}
href={href}
accessibilityRole={accessibilityRole}
onPress={onPress}
style={style}
>
{children}
</PlatformPressable>
);
},
accessibilityLabel,
testID,
Expand Down Expand Up @@ -324,7 +302,4 @@ const styles = StyleSheet.create({
fontSize: 13,
marginLeft: 20,
},
button: {
display: 'flex',
},
});
76 changes: 5 additions & 71 deletions 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,
Expand Down Expand Up @@ -96,73 +95,13 @@ type Props = {
testID?: string;
};

const LinkPressable = ({
route,
href,
children,
style,
onPress,
onLongPress,
onPressIn,
onPressOut,
accessibilityRole,
...rest
}: Omit<React.ComponentProps<typeof PlatformPressable>, 'style'> & {
style: StyleProp<ViewStyle>;
} & {
route: Route<string>;
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 (
<Link
{...rest}
href={href}
action={CommonActions.navigate(route.name, route.params)}
style={[styles.button, style]}
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);
}
}}
// 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}
</Link>
);
} else {
return (
<PlatformPressable
{...rest}
accessibilityRole={accessibilityRole}
onPress={onPress}
>
<View style={style}>{children}</View>
</PlatformPressable>
);
}
};

/**
* A component used to show an action item with an icon and a label in a navigation drawer.
*/
export function DrawerItem(props: Props) {
const { colors, fonts } = useTheme();

const {
route,
href,
icon,
label,
Expand Down Expand Up @@ -196,19 +135,17 @@ export function DrawerItem(props: Props) {
{...rest}
style={[styles.container, { borderRadius, backgroundColor }, style]}
>
<LinkPressable
<PlatformPressable
testID={testID}
onPress={onPress}
style={[styles.wrapper, { borderRadius }]}
accessibilityLabel={accessibilityLabel}
accessibilityRole="button"
accessibilityState={{ selected: focused }}
pressColor={pressColor}
pressOpacity={pressOpacity}
route={route}
href={href}
>
<React.Fragment>
<View style={[styles.wrapper, { borderRadius }]}>
{iconNode}
<View
style={[
Expand All @@ -228,8 +165,8 @@ export function DrawerItem(props: Props) {
label({ color, focused })
)}
</View>
</React.Fragment>
</LinkPressable>
</View>
</PlatformPressable>
</View>
);
}
Expand All @@ -249,7 +186,4 @@ const styles = StyleSheet.create({
marginRight: 32,
flex: 1,
},
button: {
display: 'flex',
},
});
2 changes: 0 additions & 2 deletions packages/drawer/src/views/DrawerToggleButton.tsx
Expand Up @@ -22,8 +22,6 @@ export function DrawerToggleButton({ tintColor, ...rest }: Props) {
return (
<PlatformPressable
{...rest}
accessible
accessibilityRole="button"
android_ripple={{ borderless: true }}
onPress={() => navigation.dispatch(DrawerActions.toggleDrawer())}
style={styles.touchable}
Expand Down
16 changes: 4 additions & 12 deletions packages/elements/src/Header/HeaderBackButton.tsx
Expand Up @@ -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());

Check warning on line 153 in packages/elements/src/Header/HeaderBackButton.tsx

View check run for this annotation

Codecov / codecov/patch

packages/elements/src/Header/HeaderBackButton.tsx#L153

Added line #L153 was not covered by tests
}

return onPress && requestAnimationFrame(onPress);
};

return (
<PlatformPressable
disabled={disabled}
href={href}
accessible
accessibilityRole={Platform.OS === 'web' && href ? 'link' : 'button'}
accessibilityLabel={accessibilityLabel}
testID={testID}
onPress={disabled ? undefined : handlePress}
onPress={handlePress}
pressColor={pressColor}
pressOpacity={pressOpacity}
android_ripple={androidRipple}
Expand Down
2 changes: 1 addition & 1 deletion packages/elements/src/Header/HeaderBackContext.tsx
@@ -1,5 +1,5 @@
import { getNamedContext } from '../getNamedContext';

export const HeaderBackContext = getNamedContext<
{ title: string; href?: string } | undefined
{ title: string | undefined; href: string | undefined } | undefined
>('HeaderBackContext', undefined);
27 changes: 27 additions & 0 deletions packages/elements/src/PlatformPressable.tsx
Expand Up @@ -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,
Expand All @@ -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(

Check warning on line 63 in packages/elements/src/PlatformPressable.tsx

View check run for this annotation

Codecov / codecov/patch

packages/elements/src/PlatformPressable.tsx#L63

Added line #L63 was not covered by tests
// @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);

Check warning on line 71 in packages/elements/src/PlatformPressable.tsx

View check run for this annotation

Codecov / codecov/patch

packages/elements/src/PlatformPressable.tsx#L70-L71

Added lines #L70 - L71 were not covered by tests
}
} else {
onPress?.(e);

Check warning on line 74 in packages/elements/src/PlatformPressable.tsx

View check run for this annotation

Codecov / codecov/patch

packages/elements/src/PlatformPressable.tsx#L73-L74

Added lines #L73 - L74 were not covered by tests
}
};

const handlePressIn = (e: GestureResponderEvent) => {
animateTo(pressOpacity, 0);
onPressIn?.(e);
Expand All @@ -65,6 +87,11 @@ export function PlatformPressable({

return (
<AnimatedPressable
accessible
accessibilityRole={
Platform.OS === 'web' && rest.href != null ? 'link' : 'button'
}
onPress={disabled ? undefined : handlePress}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
android_ripple={
Expand Down
6 changes: 5 additions & 1 deletion packages/native-stack/src/types.tsx
Expand Up @@ -72,7 +72,11 @@ export type NativeStackHeaderProps = {
/**
* Title of the previous screen.
*/
title: string;
title: string | undefined;
/**
* The `href` to use for the anchor tag on web
*/
href: string | undefined;
};
/**
* Options for the current screen.
Expand Down
21 changes: 13 additions & 8 deletions packages/native-stack/src/views/NativeStackView.native.tsx
Expand Up @@ -237,14 +237,19 @@ const SceneView = ({

const headerTopInsetEnabled = topInset !== 0;
const headerHeight = header ? customHeaderHeight : defaultHeaderHeight;
const headerBack = previousDescriptor
? {
title: getHeaderTitle(
previousDescriptor.options,
previousDescriptor.route.name
),
}
: parentHeaderBack;

const backTitle = previousDescriptor
? getHeaderTitle(previousDescriptor.options, previousDescriptor.route.name)
: parentHeaderBack?.title;

const headerBack = React.useMemo(
() => ({
// No href needed for native
href: undefined,
title: backTitle,
}),
[backTitle]
);

const isRemovePrevented = preventedRoutes[route.key]?.preventRemove;

Expand Down
1 change: 1 addition & 0 deletions packages/native-stack/src/views/NativeStackView.tsx
Expand Up @@ -130,6 +130,7 @@ export function NativeStackView({ state, descriptors }: Props) {
)
: undefined
}
canGoBack={canGoBack}
onPress={navigation.goBack}
href={headerBack.href}
/>
Expand Down
18 changes: 12 additions & 6 deletions packages/native/src/useLinkProps.tsx
Expand Up @@ -80,18 +80,24 @@ export function useLinkProps<ParamList extends ReactNavigation.RootParamList>({
const onPress = (
e?: React.MouseEvent<HTMLAnchorElement, 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;
Expand Down
4 changes: 2 additions & 2 deletions packages/stack/src/types.tsx
Expand Up @@ -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.
Expand Down

0 comments on commit 02aa2b8

Please sign in to comment.