Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions src/components/IconButton/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,16 @@ const getIconColor = ({
selected,
customIconColor,
}: BaseProps & { customIconColor?: ColorValue }) => {
if (disabled) {
return theme.colors.onSurface;
}

// An explicitly passed color is an instruction, so it outranks the disabled
// default. Disabled is still conveyed by the reduced icon opacity.
if (typeof customIconColor !== 'undefined') {
return customIconColor;
}

if (disabled) {
return theme.colors.onSurface;
}

if (isMode('contained')) {
if (selected) {
return theme.colors.onPrimary;
Expand Down
5 changes: 2 additions & 3 deletions src/components/TextInput/TextInputIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,6 @@ const TextInputIcon = ({
isDisabled: disabled,
});

const onPressHandler = disabled ? undefined : onPress;

return (
<View style={styles.iconWrapper}>
<IconButton
Expand All @@ -88,7 +86,8 @@ const TextInputIcon = ({
iconColor={color}
size={iconSize}
style={[styles.icon, style]}
onPress={onPressHandler}
disabled={disabled}
onPress={onPress}
/>
</View>
);
Expand Down
56 changes: 42 additions & 14 deletions src/components/TouchableRipple/TouchableRipple.native.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import { Platform, StyleSheet, View } from 'react-native';
import { Animated, Platform, StyleSheet, View } from 'react-native';
import type {
PressableAndroidRippleConfig,
StyleProp,
Expand Down Expand Up @@ -61,7 +61,12 @@ const TouchableRipple = ({
onPressOut,
});

const disabled = disabledProp || !hasPassedTouchHandler;
// With no touch handler and no explicit disabled this is not a control, so it
// renders as a plain View. A Pressable is wrong here either way: keep the old
// disabled flag and it gets announced as a disabled control, drop the flag and
// it starts claiming the touch, swallowing taps meant for whatever wraps it.
const isControl = hasPassedTouchHandler || Boolean(disabledProp);
const isInteractive = hasPassedTouchHandler && !disabledProp;

const { calculatedRippleColor, calculatedUnderlayColor } =
getTouchableRippleColors({
Expand All @@ -78,21 +83,44 @@ const TouchableRipple = ({
const useForeground =
Platform.OS === 'android' && Platform.Version >= ANDROID_VERSION_PIE;

const containerStyle = TouchableRipple.supported
? [useForeground && styles.overflowHidden, style]
: [borderless && styles.overflowHidden, style];

if (!isControl) {
return (
<Animated.View
{...rest}
ref={ref}
// Pressable defaults this to true, so keep it to preserve any role and
// state the caller set, e.g. a read only checked CheckboxItem.
accessible={rest.accessible !== false}
// A consumer role of button makes react-native-web render a real
// <button>, which is tabbable by default. Nothing to activate here.
focusable={rest.focusable ?? false}
style={containerStyle}
>
{React.Children.only(children)}
</Animated.View>
);
}

if (TouchableRipple.supported) {
const androidRipple = rippleEffectEnabled
? (background ?? {
color: calculatedRippleColor,
borderless,
foreground: useForeground,
})
: undefined;
const androidRipple =
rippleEffectEnabled && isInteractive
? (background ?? {
color: calculatedRippleColor,
borderless,
foreground: useForeground,
})
: undefined;

return (
<Pressable
{...rest}
ref={ref}
disabled={disabled}
style={[useForeground && styles.overflowHidden, style]}
disabled={disabledProp}
style={containerStyle}
android_ripple={androidRipple}
>
{React.Children.only(children)}
Expand All @@ -104,12 +132,12 @@ const TouchableRipple = ({
<Pressable
{...rest}
ref={ref}
disabled={disabled}
style={[borderless && styles.overflowHidden, style]}
disabled={disabledProp}
style={containerStyle}
>
{({ pressed }) => (
<>
{pressed && rippleEffectEnabled && (
{pressed && rippleEffectEnabled && isInteractive && (
<View
testID="touchable-ripple-underlay"
style={[
Expand Down
59 changes: 46 additions & 13 deletions src/components/TouchableRipple/TouchableRipple.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import { Platform, StyleSheet, View } from 'react-native';
import { Animated, Platform, StyleSheet, View } from 'react-native';
import type {
ColorValue,
GestureResponderEvent,
Expand Down Expand Up @@ -37,7 +37,8 @@ export type Props = PressableProps & {
*/
disabled?: boolean;
/**
* Function to execute on press. If not set, will cause the touchable to be disabled.
* Function to execute on press. If not set, the touchable renders as a plain
* container and is not exposed as a control.
*/
onPress?: (e: GestureResponderEvent) => void;
/**
Expand Down Expand Up @@ -129,6 +130,20 @@ const TouchableRipple = ({

const { onPress, onLongPress, onPressIn, onPressOut } = rest;

const hasPassedTouchHandler = hasTouchHandler({
onPress,
onLongPress,
onPressIn,
onPressOut,
});

// With no touch handler and no explicit disabled this is not a control, so it
// renders as a plain View. A Pressable is wrong here either way: keep the old
// disabled flag and it gets announced as a disabled control, drop the flag and
// it starts claiming the touch, swallowing taps meant for whatever wraps it.
const isControl = hasPassedTouchHandler || Boolean(disabledProp);
const isInteractive = hasPassedTouchHandler && !disabledProp;

const handlePressIn = React.useCallback(
(e: any) => {
onPressIn?.(e);
Expand Down Expand Up @@ -264,29 +279,47 @@ const TouchableRipple = ({
[onPressOut, rippleEffectEnabled]
);

const hasPassedTouchHandler = hasTouchHandler({
onPress,
onLongPress,
onPressIn,
onPressOut,
});

const disabled = disabledProp || !hasPassedTouchHandler;
if (!isControl) {
const state = { pressed: false, hovered: false, focused: false };

return (
<Animated.View
{...rest}
ref={ref}
// Pressable defaults this to true, so keep it to preserve any role and
// state the caller set, e.g. a read only checked CheckboxItem.
accessible={rest.accessible !== false}
// A consumer role of button makes react-native-web render a real
// <button>, which is tabbable by default. Nothing to activate here.
focusable={rest.focusable ?? false}
style={[
styles.touchable,
borderless && styles.borderless,
styles.disabled,
typeof style === 'function' ? style(state) : style,
]}
>
{React.Children.only(
typeof children === 'function' ? children(state) : children
)}
</Animated.View>
);
}

return (
<Pressable
{...rest}
ref={ref}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
disabled={disabled}
disabled={disabledProp}
style={(state) => [
styles.touchable,
borderless && styles.borderless,
// focused state is not ready yet: https://github.com/necolas/react-native-web/issues/1849
// state.focused && { backgroundColor: ___ },
state.hovered && { backgroundColor: hoverColor },
disabled && styles.disabled,
state.hovered && isInteractive && { backgroundColor: hoverColor },
!isInteractive && styles.disabled,
typeof style === 'function' ? style(state) : style,
]}
>
Expand Down
55 changes: 11 additions & 44 deletions src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -122,28 +122,11 @@ exports[`Appbar does not pass any additional props to Searchbar 1`] = `
testID="search-bar-icon-container"
>
<View
accessibilityLabel="search"
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": true,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
aria-label="search"
centered={true}
collapsable={false}
focusable={true}
focusable={false}
hitSlop={
{
"bottom": 6,
Expand All @@ -152,30 +135,14 @@ exports[`Appbar does not pass any additional props to Searchbar 1`] = `
"top": 6,
}
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
role="button"
style={
[
{
"overflow": "hidden",
},
[
{
"alignItems": "center",
"flexGrow": 1,
"justifyContent": "center",
},
undefined,
],
]
{
"alignItems": "center",
"flexGrow": 1,
"justifyContent": "center",
"overflow": "hidden",
}
}
testID="search-bar-icon"
>
Expand Down Expand Up @@ -317,7 +284,7 @@ exports[`Appbar does not pass any additional props to Searchbar 1`] = `
{
"busy": undefined,
"checked": undefined,
"disabled": false,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
Expand Down Expand Up @@ -506,7 +473,7 @@ exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and A
{
"busy": undefined,
"checked": undefined,
"disabled": false,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
Expand Down Expand Up @@ -749,7 +716,7 @@ exports[`Appbar passes additional props to AppbarBackAction, AppbarContent and A
{
"busy": undefined,
"checked": undefined,
"disabled": false,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
Expand Down
14 changes: 12 additions & 2 deletions src/components/__tests__/Button.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,18 @@ it('renders disabled button', async () => {
expect(tree).toMatchSnapshot();
});

it('renders disabled button if there is no touch handler passed', async () => {
await render(<Button testID="disabled-button">Disabled button</Button>);
it('does not mark a button without a touch handler as disabled', async () => {
await render(<Button testID="plain-button">Plain button</Button>);

expect(screen.getByTestId('plain-button')).not.toBeDisabled();
});

it('renders disabled button when the disabled prop is passed', async () => {
await render(
<Button disabled onPress={() => {}} testID="disabled-button">
Disabled button
</Button>
);

expect(screen.getByTestId('disabled-button')).toBeDisabled();
});
Expand Down
Loading