Skip to content
Merged
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
41 changes: 14 additions & 27 deletions packages/base/src/AirbnbRating/SwipeRating.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,25 +56,6 @@ const TYPES: {
// No TYPES entry needed - handled via conditional logic throughout the component
};

//@ts-ignore
const fractionsType: any = (props, propName, componentName) => {
if (props[propName]) {
const value = props[propName];

if (typeof value === 'number') {
return value >= 0 && value <= 20
? null
: new Error(
`\`${propName}\` in \`${componentName}\` must be between 0 and 20`
);
}

return new Error(
`\`${propName}\` in \`${componentName}\` must be a number`
);
}
};

export type SwipeRatingProps = {
/**
* Graphic used for represent a rating
Expand Down Expand Up @@ -171,7 +152,7 @@ export type SwipeRatingProps = {
*
* @default 0
*/
fractions?: typeof fractionsType;
fractions?: number;

/**
* The minimum value the user can select
Expand Down Expand Up @@ -212,7 +193,7 @@ const SwipeRating: React.FC<SwipeRatingProps> = ({
onStartRating = () => {},
onSwipeRating = () => {},
onFinishRating = () => {},
fractions,
fractions = 0,
readonly = false,
style,
showRating = false,
Expand All @@ -229,6 +210,7 @@ const SwipeRating: React.FC<SwipeRatingProps> = ({
const centerX = React.useRef<number>(0);
const [currentRatingValue, setCurrentRatingValue] =
React.useState<number>(startingValue);
const safeFractions = Math.max(0, Math.min(20, fractions));

const setCurrentRating = React.useCallback(
(rating: number) => {
Expand All @@ -255,7 +237,12 @@ const SwipeRating: React.FC<SwipeRatingProps> = ({

useEffect(() => {
setCurrentRating(startingValue);
}, [startingValue, setCurrentRating]);
if (fractions !== undefined && (fractions < 0 || fractions > 20)) {
console.error(
`[SwipeRating] fractions must be between 0-20, received ${fractions}`
);
}
}, [startingValue, setCurrentRating, fractions]);

useEffect(() => {
if (type === 'custom') {
Expand Down Expand Up @@ -284,12 +271,12 @@ const SwipeRating: React.FC<SwipeRatingProps> = ({
const diff = localValue / imageSize;

currentRating = localStartingValue + diff;
currentRating = fractions
? Number(currentRating.toFixed(fractions))
currentRating = safeFractions
? Number(currentRating.toFixed(safeFractions))
: Math.ceil(currentRating);
} else {
currentRating = fractions
? Number(localStartingValue.toFixed(fractions))
currentRating = safeFractions
? Number(localStartingValue.toFixed(safeFractions))
: Math.ceil(localStartingValue);
}
if (jumpValue > 0 && jumpValue < ratingCount) {
Expand All @@ -298,7 +285,7 @@ const SwipeRating: React.FC<SwipeRatingProps> = ({
return currentRating;
}
},
[ratingCount, minValue, imageSize, jumpValue, fractions]
[ratingCount, minValue, imageSize, jumpValue, safeFractions]
);

useEffect(() => {
Expand Down
59 changes: 31 additions & 28 deletions packages/base/src/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,25 @@ import {
ActivityIndicator,
ActivityIndicatorProps,
Platform,
Pressable,
PressableProps,
StyleProp,
StyleSheet,
Text,
TextStyle,
TouchableNativeFeedback,
TouchableNativeFeedbackProps,
TouchableOpacity,
TouchableOpacityProps,
View,
ViewStyle,
} from 'react-native';
import {
color,
defaultTheme,
renderNode,
Theme,
StringOmit,
RneFunctionComponent,
StringOmit,
Theme,
ThemeSpacing,
} from '../helpers';
import { IconNode, Icon } from '../Icon';
import { Icon, IconNode } from '../Icon';
import { TextProps } from '../Text';

const defaultLoadingProps = (
Expand All @@ -42,9 +40,7 @@ const positionStyle = {
right: 'row-reverse',
};

export interface ButtonProps
extends TouchableOpacityProps,
TouchableNativeFeedbackProps {
export interface ButtonProps extends PressableProps {
/** Add button title. */
title?: string | React.ReactElement<{}>;

Expand Down Expand Up @@ -170,13 +166,7 @@ export const Button: RneFunctionComponent<ButtonProps> = ({
[loading, onPress, disabled]
);

// Refactor to Pressable
const TouchableComponentInternal =
TouchableComponent ||
Platform.select({
android: linearGradientProps ? TouchableOpacity : TouchableNativeFeedback,
default: TouchableOpacity,
});
const TouchableComponentInternal = TouchableComponent || Pressable;

const titleStyle: StyleProp<TextStyle> = useMemo(
() =>
Expand All @@ -203,13 +193,22 @@ export const Button: RneFunctionComponent<ButtonProps> = ({
]
);

const background =
Platform.OS === 'android' && Platform.Version >= 21
? TouchableNativeFeedback.Ripple(
Color(titleStyle?.color?.toString()).alpha(0.32).rgb().string(),
false
)
: undefined;
const androidRippleStyles = useMemo(() => {
if (Platform.OS !== 'android' || !!linearGradientProps || disabled) {
return null; // Returning null instead of undefined for android_ripple to explicitly disable
}

try {
const baseColor = titleStyle?.color?.toString() || '#000000';
return {
color: Color(baseColor).alpha(0.32).rgb().string(),
borderless: false,
foreground: true,
};
} catch (e) {
return { color: 'rgba(0,0,0,0.1)', borderless: false, foreground: true };
}
}, [titleStyle?.color, linearGradientProps, disabled]);

const loadingProps: ActivityIndicatorProps = useMemo(
() => ({
Expand Down Expand Up @@ -247,12 +246,16 @@ export const Button: RneFunctionComponent<ButtonProps> = ({
>
<TouchableComponentInternal
onPress={handleOnPress}
delayPressIn={0}
activeOpacity={0.3}
delayLongPress={0}
accessibilityRole="button"
accessibilityState={accessibilityState}
accessibilityState={{ ...accessibilityState, disabled }}
disabled={disabled}
background={background}
style={({ pressed }) => [
rest.style,
{ opacity: pressed && !androidRippleStyles ? 0.3 : 1 },
]}
android_ripple={androidRippleStyles}
testID="RNE_BUTTON_PRESSABLE"
{...rest}
>
<ViewComponent
Expand Down
56 changes: 45 additions & 11 deletions packages/base/src/Button/__tests__/Button.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React from 'react';
import React, { useState } from 'react';
import { fireEvent } from '@testing-library/react-native';
import { TouchableOpacity } from 'react-native';
import { renderWithWrapper } from '../../../.ci/testHelper';
import { Icon } from '../../Icon';
import { Button } from '../index';
import { describe, it, expect, jest } from '@jest/globals';
import { View } from 'react-native';
import { Text } from '../../Text';

describe('Button Component', () => {
it('should match snapshot', () => {
Expand Down Expand Up @@ -32,35 +33,68 @@ describe('Button Component', () => {
const onPress = jest.fn();
const { wrapper } = renderWithWrapper(
<Button onPress={onPress} />,
'RNE_BUTTON_WRAPPER'
'RNE_BUTTON_PRESSABLE'
);
const touchableOpacityTree = wrapper.findByType(TouchableOpacity);
fireEvent(touchableOpacityTree, 'press');
fireEvent(wrapper, 'press');
expect(onPress).toHaveBeenCalled();
});

it('should be NOT call onPress events while loading', () => {
const onPress = jest.fn();
const { wrapper } = renderWithWrapper(
<Button loading onPress={onPress} />,
'RNE_BUTTON_WRAPPER'
'RNE_BUTTON_PRESSABLE'
);
const touchableOpacityTree = wrapper.findByType(TouchableOpacity);
fireEvent(touchableOpacityTree, 'press');
fireEvent(wrapper, 'press');
expect(onPress).not.toHaveBeenCalled();
});

it('should be NOT call onPress events if disabled', () => {
const onPress = jest.fn();
const { wrapper } = renderWithWrapper(
<Button disabled onPress={onPress} />,
'RNE_BUTTON_WRAPPER'
'RNE_BUTTON_PRESSABLE'
);
const touchableOpacityTree = wrapper.findByType(TouchableOpacity);
fireEvent(touchableOpacityTree, 'press');
fireEvent(wrapper, 'press');
expect(onPress).not.toHaveBeenCalled();
});

it('should switch backgroundColor when toggling disabled state', () => {
// 1. Create a Wrapper component to manage the 'disabled' state
const BtnWrapper = () => {
const [isDisabled, setIsDisabled] = useState(true);
return (
<View>
<Button
title="Test"
disabled={isDisabled}
buttonStyle={{ backgroundColor: 'blue' }}
disabledStyle={{ backgroundColor: 'gray' }}
/>
{/* A separate trigger to toggle the state */}
<Text testID="toggle" onPress={() => setIsDisabled(false)}>
Toggle Enable
</Text>
</View>
);
};

const { wrapper, getByTestId } = renderWithWrapper(
<BtnWrapper />,
'RNE_BUTTON_PRESSABLE'
);

// Check disabled (gray)
let viewComponent = wrapper.findByType(View);
expect(viewComponent.props.style.backgroundColor).toBe('gray');

// re-enable and verify it switches back to blue
const toggleText = getByTestId('toggle');
fireEvent.press(toggleText);
viewComponent = wrapper.findByType(View);
expect(viewComponent.props.style.backgroundColor).toBe('blue');
});

describe.each`
type
${'solid'}
Expand Down
Loading