Skip to content

Clickable component#4018

Open
m-bert wants to merge 40 commits intomainfrom
@mbert/clickable
Open

Clickable component#4018
m-bert wants to merge 40 commits intomainfrom
@mbert/clickable

Conversation

@m-bert
Copy link
Contributor

@m-bert m-bert commented Mar 9, 2026

Description

This PR introduces new Clickable component, which is meant to be a replacement for buttons.

Note

Docs for Clickable will be added in #4022, as I don't want to release them right away after merging this PR.

borderless

For now, borderless doesn't work. I've tested clickable with some changes that allow borderless ripple to be visible, however we don't want to introduce them here because it would break other things. Also it should be generl fix, not in the PR with new component.

Stress test

Render list with 2000 buttons 50 times (50ms delay between renders), drop 5 best and 5 worst results. Then calculate average.

On android tests were limited to 25 repetitions with dropout of 3 best and worst results. This is because of OOM error.

Stress test example is available in this PR.

Android

$t_{Button}$ $t_{Clickable}$ $\Delta{t}$
BaseButton 1112.44 1194.20 +81.76
RectButton 1517.90 1718.42 +200.52
BorderlessButton 1362.34 1418.16 +55.82

iOS

$t_{Button}$ $t_{Clickable}$ $\Delta{t}$
BaseButton 1114.27 1100.80 -13.47
RectButton 1463.40 1612.50 +149.10
BorderlessButton 1206.60 1272.34 +65.74

Web

$t_{Button}$ $t_{Clickable}$ $\Delta{t}$
BaseButton 53.67 57.99 +4.32
RectButton 93.43 102.40 +8.97
BorderlessButton 99.80 75.06 -24.74

Test plan

New examples

Copilot AI review requested due to automatic review settings March 9, 2026 16:12
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new Clickable component in the react-native-gesture-handler library, intended to serve as a unified replacement for BaseButton, RectButton, and BorderlessButton. The component supports configurable visual feedback (underlay or whole-component opacity changes) and native Android ripple effects. Supporting changes include extracting visual style properties (background color, border radius, etc.) through to the native button in GestureHandlerButton, type improvements to RawButton and BorderlessButton, and a new example screen.

Changes:

  • New Clickable component with configurable animation feedback (feedbackTarget, feedbackType, activeOpacity, underlayColor) and long-press support
  • GestureHandlerButton now passes visual style properties (backgroundColor, borderRadius, border styles) to the native ButtonComponent, and RawButton gets proper type generics
  • Example screen demonstrating Clickable in various configurations (base, rect-like, borderless-like, custom)

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/v3/components/Clickable.tsx New Clickable component with animation, long-press, and ripple support
src/v3/components/index.ts Exports Clickable and ClickableProps (from incorrect path)
src/v3/index.ts Re-exports Clickable from components
src/components/GestureHandlerButton.tsx Extracts visual properties (backgroundColor, border*) into buttonStyle passed to native button
src/v3/components/GestureButtons.tsx Adds type generics to RawButton, extracts ref in BorderlessButton
src/v3/types/NativeWrapperType.ts Adds `
apps/common-app/src/new_api/components/clickable/index.tsx Example screen showcasing Clickable configurations

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +224 to +227
left,
start,
end,
overflow,
}),
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overflow was previously included in layoutStyle and passed to the ButtonComponent. It is now removed from layoutStyle but not added to buttonStyle, so it is no longer forwarded to the native button at all. It is only used for the wrapper View's conditional clipping logic (line 254). If this is intentional (the native button shouldn't get overflow), this is fine. But if it was previously needed by the native button, removing it here is a regression.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 10 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@m-bert m-bert marked this pull request as ready for review March 13, 2026 11:16
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


export const RawButton = createNativeWrapper(GestureHandlerButton, {
export const RawButton = createNativeWrapper<
ReturnType<typeof GestureHandlerButton>,
});

/**
* @deprecated `RectButton` is deprecated, use `Clickable` with `underlayInitialOpacity={0.7}` instead
Comment on lines +65 to +69
onPressIn?.(e);

if (e.pointerInside) {
startLongPressTimer();

Comment on lines +125 to +130
onPressOut?.(e);

if (longPressTimeout.current !== undefined) {
clearTimeout(longPressTimeout.current);
longPressTimeout.current = undefined;
}
Comment on lines +88 to +93
// Unmount then remount for next run
setState({ phase: 'idle' });
setTimeout(() => {
setState({ phase: 'running', run: currentRun + 1 });
}, 50);
}, []);
onPressIn={() => console.log(`[${name}] onPressIn`)}
onPress={() => console.log(`[${name}] onPress`)}
onLongPress={() => console.log(`[${name}] onLongPress`)}
onPressOut={() => console.log(`[${name}] onPressOut`)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using the feedback component.

KINDA_BLUE: '#5f97c8',
ANDROID: '#34a853',
WEB: '#1067c4',
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the common colours, if needed add new colours there.

@@ -105,6 +107,8 @@ export const NEW_EXAMPLES: ExamplesSection[] = [
{ name: 'FlatList example', component: FlatListExample },
{ name: 'ScrollView example', component: ScrollViewExample },
{ name: 'Buttons example', component: ButtonsExample },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to remove the buttons from v3 ButtonsExample should be removed

<ClickableWrapper
name="Rect"
color={COLORS.WEB}
underlayActiveOpacity={0.105}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I see we set all the config values manually, didn't we discuss adding some presets which would mimick old behaviour? Manually set values would of course take precedence. I think it would simplify transition. Robots may know what to plug as the values anyway, but ordinary people will have to do some research before they get the same behaviour when migrating.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the buttons be included in v3 at all?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, so you don't need to change your code after upgrading just to get the app running.

Comment on lines +18 to +20
/**
* Background color of underlay. Works only when `animationTarget` is set to `UNDERLAY`.
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems outdated

Comment on lines +11 to +33
const {
underlayColor,
underlayInitialOpacity,
underlayActiveOpacity,
initialOpacity,
activeOpacity,
androidRipple,
delayLongPress = 600,
onLongPress,
onPress,
onPressIn,
onPressOut,
onActiveStateChange,
style,
children,
ref,
...rest
} = props;

const animatedValue = useRef(new Animated.Value(0)).current;

const underlayStartOpacity = underlayInitialOpacity ?? 0;
const componentStartOpacity = initialOpacity ?? 1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const {
underlayColor,
underlayInitialOpacity,
underlayActiveOpacity,
initialOpacity,
activeOpacity,
androidRipple,
delayLongPress = 600,
onLongPress,
onPress,
onPressIn,
onPressOut,
onActiveStateChange,
style,
children,
ref,
...rest
} = props;
const animatedValue = useRef(new Animated.Value(0)).current;
const underlayStartOpacity = underlayInitialOpacity ?? 0;
const componentStartOpacity = initialOpacity ?? 1;
const {
underlayColor,
underlayInitialOpacity = 0,
underlayActiveOpacity,
initialOpacity = 1,
activeOpacity,
androidRipple,
delayLongPress = 600,
onLongPress,
onPress,
onPressIn,
onPressOut,
onActiveStateChange,
style,
children,
ref,
...rest
} = props;
const animatedValue = useRef(new Animated.Value(0)).current;

Comment on lines +146 to +150
borderRadius: resolvedStyle.borderRadius,
borderTopLeftRadius: resolvedStyle.borderTopLeftRadius,
borderTopRightRadius: resolvedStyle.borderTopRightRadius,
borderBottomLeftRadius: resolvedStyle.borderBottomLeftRadius,
borderBottomRightRadius: resolvedStyle.borderBottomRightRadius,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed? button content should be clipped, no?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants