-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Clickable component
#4018
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
m-bert
wants to merge
48
commits into
main
Choose a base branch
from
@mbert/clickable
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Clickable component
#4018
Changes from all commits
Commits
Show all changes
48 commits
Select commit
Hold shift + click to select a range
9fb1120
Base implementation
m-bert 093630b
Merge branch 'main' into @mbert/clickable
m-bert f1134cb
Do not use animated button if not necessary
m-bert 9461ecc
Use props
m-bert ab6ced1
Types
m-bert 27b3d0c
Additional props
m-bert c0d4960
Working component
m-bert 25ed517
Small refactor
m-bert 5e4262b
Add example
m-bert 5646775
Looks good
m-bert 202a999
Unified, without ripple
m-bert dd2c3bf
Rippleeeee
m-bert 2cf923b
clear activeState also in onFinalize
m-bert 8059e40
Fix borderless
m-bert 6b043a9
Default ripple color
m-bert 1eb680e
Rename file
m-bert b2f7e05
Fix index
m-bert 44bbcc6
Split component and types
m-bert f4d895e
Add presets
m-bert 491c27a
Update gitignore
m-bert e404f8a
Renames
m-bert 583e864
Remove preset
m-bert 769adb4
Ripple
m-bert e4bb348
Example
m-bert 96c016b
Rename in example
m-bert fe9f47f
Opacity fix
m-bert aca162b
JSDocs
m-bert c6f27e7
Vibe stress test
m-bert 4200868
First refactor
m-bert beb6710
Second optimization
m-bert 1a23339
Change props
m-bert 0e5b735
Bring back use callback
m-bert ef24b2f
Merge branch 'main' into @mbert/clickable
m-bert d6113c6
Tests
m-bert ac02b27
Reset longpress outside of if
m-bert 8864e89
Easier stress test
m-bert b74ce61
Deprecate old buttons
m-bert 822ba83
Add onPressIn and onPressOut
m-bert c9aba4d
Revert changes in button
m-bert f2b3bb9
Merge branch 'main' into @mbert/clickable
m-bert 050f122
Fix deprecation message for `RectButton` to reference `underlayActive…
m-bert 270b111
Call onPressIn when e.pointerInsideis true
m-bert 77d411d
Stress test example timeout cleanup
m-bert 08e5e2e
Merge branch '@mbert/clickable' of github.com:software-mansion/react-…
m-bert d80e5cf
Colooooooors
m-bert 95a887f
Update underlayColor description to require underlayActiveOpacity
m-bert 25126cf
Add default values for underlayInitialOpacity and initialOpacity in C…
m-bert 45ffb2f
Remove unnecessary border styles
m-bert File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,7 @@ DerivedData | |
|
|
||
| # Android/IntelliJ | ||
| # | ||
| bin/ | ||
| build/ | ||
| .idea | ||
| .gradle | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
158 changes: 158 additions & 0 deletions
158
apps/common-app/src/new_api/components/clickable/index.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| import React from 'react'; | ||
| import { StyleSheet, Text, View, ScrollView } from 'react-native'; | ||
| import { | ||
| GestureHandlerRootView, | ||
| Clickable, | ||
| ClickableProps, | ||
| } from 'react-native-gesture-handler'; | ||
| import { COLORS } from '../../../common'; | ||
|
|
||
| type ButtonWrapperProps = ClickableProps & { | ||
| name: string; | ||
| color: string; | ||
| }; | ||
|
|
||
| function ClickableWrapper({ name, color, ...rest }: ButtonWrapperProps) { | ||
| return ( | ||
| <Clickable | ||
| style={[styles.button, { backgroundColor: color }]} | ||
| onPressIn={() => console.log(`[${name}] onPressIn`)} | ||
| onPress={() => console.log(`[${name}] onPress`)} | ||
| onLongPress={() => console.log(`[${name}] onLongPress`)} | ||
| onPressOut={() => console.log(`[${name}] onPressOut`)} | ||
| {...rest}> | ||
| <Text style={styles.buttonText}>{name}</Text> | ||
| </Clickable> | ||
| ); | ||
| } | ||
|
|
||
| export default function ClickableExample() { | ||
| return ( | ||
| <GestureHandlerRootView style={styles.container}> | ||
| <ScrollView contentContainerStyle={styles.scrollContent}> | ||
| <View style={styles.section}> | ||
| <Text style={styles.sectionHeader}>Buttons replacements</Text> | ||
| <Text>New component that replaces all buttons and pressables.</Text> | ||
|
|
||
| <View style={styles.row}> | ||
| <ClickableWrapper name="Base" color={COLORS.DARK_PURPLE} /> | ||
|
|
||
| <ClickableWrapper | ||
| name="Rect" | ||
| color={COLORS.WEB_BLUE} | ||
| underlayActiveOpacity={0.105} | ||
m-bert marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| /> | ||
|
|
||
| <ClickableWrapper | ||
| name="Borderless" | ||
| activeOpacity={0.3} | ||
| color={COLORS.RED} | ||
| /> | ||
| </View> | ||
| </View> | ||
|
|
||
| <View style={styles.section}> | ||
| <Text style={styles.sectionHeader}>Custom animations</Text> | ||
| <Text>Animated overlay.</Text> | ||
|
|
||
| <View style={styles.row}> | ||
| <ClickableWrapper | ||
| name="Click me!" | ||
| color={COLORS.YELLOW} | ||
| underlayActiveOpacity={0.3} | ||
| /> | ||
|
|
||
| <ClickableWrapper | ||
| name="Click me!" | ||
| color={COLORS.NAVY} | ||
| underlayInitialOpacity={0.7} | ||
| underlayActiveOpacity={0.5} | ||
| underlayColor={COLORS.DARK_GREEN} | ||
| /> | ||
| </View> | ||
|
|
||
| <Text>Animated component.</Text> | ||
|
|
||
| <View style={styles.row}> | ||
| <ClickableWrapper | ||
| name="Click me!" | ||
| color={COLORS.LIGHT_BLUE} | ||
| initialOpacity={0.3} | ||
| activeOpacity={0.7} | ||
| /> | ||
|
|
||
| <ClickableWrapper | ||
| name="Click me!" | ||
| color={COLORS.DARK_SALMON} | ||
| initialOpacity={0.7} | ||
| activeOpacity={0.5} | ||
| /> | ||
| </View> | ||
| </View> | ||
|
|
||
| <View style={styles.section}> | ||
| <Text style={styles.sectionHeader}>Android ripple</Text> | ||
| <Text>Configurable ripple effect on Clickable component.</Text> | ||
|
|
||
| <View style={styles.row}> | ||
| <ClickableWrapper | ||
| name="Default" | ||
| color={COLORS.ANDROID} | ||
| androidRipple={{}} | ||
| /> | ||
|
|
||
| <ClickableWrapper | ||
| name="Borderless" | ||
| color={COLORS.ANDROID} | ||
| androidRipple={{ | ||
| color: COLORS.KINDA_BLUE, | ||
| borderless: true, | ||
| radius: 55, | ||
| }} | ||
| /> | ||
| </View> | ||
| </View> | ||
| </ScrollView> | ||
| </GestureHandlerRootView> | ||
| ); | ||
| } | ||
|
|
||
| const styles = StyleSheet.create({ | ||
| container: { | ||
| flex: 1, | ||
| }, | ||
| scrollContent: { | ||
| paddingBottom: 40, | ||
| }, | ||
| section: { | ||
| padding: 20, | ||
| borderBottomWidth: StyleSheet.hairlineWidth, | ||
| borderBottomColor: '#ccc', | ||
| alignItems: 'center', | ||
| }, | ||
| row: { | ||
| flexDirection: 'row', | ||
| flexWrap: 'wrap', | ||
| justifyContent: 'center', | ||
| gap: 10, | ||
| marginTop: 20, | ||
| marginBottom: 20, | ||
| }, | ||
| sectionHeader: { | ||
| fontSize: 16, | ||
| fontWeight: 'bold', | ||
| marginBottom: 4, | ||
| }, | ||
| button: { | ||
| width: 110, | ||
| height: 50, | ||
| borderRadius: 12, | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| }, | ||
| buttonText: { | ||
| color: 'white', | ||
| fontSize: 14, | ||
| fontWeight: '600', | ||
| }, | ||
| }); | ||
193 changes: 193 additions & 0 deletions
193
apps/common-app/src/new_api/components/clickable_stress/index.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,193 @@ | ||
| import { Profiler, useCallback, useEffect, useRef, useState } from 'react'; | ||
| import { StyleSheet, Text, View } from 'react-native'; | ||
| import { Clickable, ScrollView } from 'react-native-gesture-handler'; | ||
|
|
||
| const CLICK_COUNT = 2000; | ||
| const N = 25; | ||
| const DROPOUT = 3; | ||
|
|
||
| const STRESS_DATA = Array.from( | ||
| { length: CLICK_COUNT }, | ||
| (_, i) => `stress-${i}` | ||
| ); | ||
|
|
||
| type BenchmarkState = | ||
| | { phase: 'idle' } | ||
| | { phase: 'running'; run: number } | ||
| | { phase: 'done'; results: number[] }; | ||
|
|
||
| function getTrimmedAverage(results: number[], dropout: number): number { | ||
| const sorted = [...results].sort((a, b) => a - b); | ||
| const trimCount = Math.min( | ||
| dropout, | ||
| Math.max(0, Math.floor((sorted.length - 1) / 2)) | ||
| ); | ||
| const trimmed = | ||
| trimCount > 0 ? sorted.slice(trimCount, sorted.length - trimCount) : sorted; | ||
| return trimmed.reduce((sum, v) => sum + v, 0) / trimmed.length; | ||
| } | ||
|
|
||
| type ClickableListProps = { | ||
| run: number; | ||
| onMountDuration: (duration: number) => void; | ||
| }; | ||
|
|
||
| function ClickableList({ run, onMountDuration }: ClickableListProps) { | ||
| const reportedRef = useRef(-1); | ||
|
|
||
| const handleRender = useCallback( | ||
| (_id: string, phase: string, actualDuration: number) => { | ||
| if (phase === 'mount' && reportedRef.current !== run) { | ||
| reportedRef.current = run; | ||
| onMountDuration(actualDuration); | ||
| } | ||
| }, | ||
| [run, onMountDuration] | ||
| ); | ||
|
|
||
| return ( | ||
| <Profiler id="ClickableList" onRender={handleRender}> | ||
| <ScrollView style={{ flex: 1 }}> | ||
| {STRESS_DATA.map((id) => ( | ||
| // <BaseButton key={id} style={styles.button} /> | ||
| <Clickable key={id} style={styles.button} /> | ||
|
|
||
| // <RectButton key={id} style={styles.button} /> | ||
| // <Clickable | ||
| // key={id} | ||
| // style={styles.button} | ||
| // underlayActiveOpacity={0.105} | ||
| // /> | ||
|
|
||
| // <BorderlessButton key={id} style={styles.button} /> | ||
| // <Clickable key={id} style={styles.button} activeOpacity={0.3} /> | ||
| ))} | ||
| </ScrollView> | ||
| </Profiler> | ||
| ); | ||
| } | ||
|
|
||
| export default function ClickableStress() { | ||
| const [state, setState] = useState<BenchmarkState>({ phase: 'idle' }); | ||
| const resultsRef = useRef<number[]>([]); | ||
| const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); | ||
|
|
||
| const start = useCallback(() => { | ||
| resultsRef.current = []; | ||
| setState({ phase: 'running', run: 1 }); | ||
| }, []); | ||
|
|
||
| const handleMountDuration = useCallback((duration: number) => { | ||
| resultsRef.current = [...resultsRef.current, duration]; | ||
| const currentRun = resultsRef.current.length; | ||
|
|
||
| if (currentRun >= N) { | ||
| setState({ phase: 'done', results: resultsRef.current }); | ||
| return; | ||
| } | ||
|
|
||
| // Unmount then remount for next run | ||
| setState({ phase: 'idle' }); | ||
| if (timeoutRef.current !== null) { | ||
| clearTimeout(timeoutRef.current); | ||
| } | ||
| timeoutRef.current = setTimeout(() => { | ||
| setState({ phase: 'running', run: currentRun + 1 }); | ||
| }, 50); | ||
| }, []); | ||
m-bert marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| useEffect(() => { | ||
| return () => { | ||
| if (timeoutRef.current !== null) { | ||
| clearTimeout(timeoutRef.current); | ||
| timeoutRef.current = null; | ||
| } | ||
| }; | ||
| }, []); | ||
|
|
||
| const isRunning = state.phase === 'running'; | ||
| const currentRun = state.phase === 'running' ? state.run : 0; | ||
| const results = state.phase === 'done' ? state.results : null; | ||
| const trimmedAverage = results ? getTrimmedAverage(results, DROPOUT) : null; | ||
|
|
||
| return ( | ||
| <View style={styles.container}> | ||
| <Clickable | ||
| underlayActiveOpacity={0.105} | ||
| style={[styles.startButton, isRunning && styles.startButtonBusy]} | ||
| onPress={start} | ||
| enabled={!isRunning}> | ||
| <Text style={styles.startButtonText}> | ||
| {isRunning ? `Running ${currentRun}/${N}...` : 'Start test'} | ||
| </Text> | ||
| </Clickable> | ||
|
|
||
| {results && ( | ||
| <View style={styles.results}> | ||
| <Text style={styles.resultText}> | ||
| Runs: {results.length} (trimmed ±{DROPOUT}) | ||
| </Text> | ||
| <Text style={styles.resultText}> | ||
| Trimmed avg: {trimmedAverage?.toFixed(2)} ms | ||
| </Text> | ||
| <Text style={styles.resultText}> | ||
| Min: {Math.min(...results).toFixed(2)} ms | ||
| </Text> | ||
| <Text style={styles.resultText}> | ||
| Max: {Math.max(...results).toFixed(2)} ms | ||
| </Text> | ||
| <Text style={styles.resultText}> | ||
| All: {results.map((r) => r.toFixed(1)).join(', ')} ms | ||
| </Text> | ||
| </View> | ||
| )} | ||
|
|
||
| {isRunning && ( | ||
| <ClickableList run={currentRun} onMountDuration={handleMountDuration} /> | ||
| )} | ||
| </View> | ||
| ); | ||
| } | ||
|
|
||
| const styles = StyleSheet.create({ | ||
| container: { | ||
| flex: 1, | ||
| padding: 20, | ||
| alignItems: 'center', | ||
| }, | ||
| startButton: { | ||
| width: 200, | ||
| height: 50, | ||
| backgroundColor: '#167a5f', | ||
| borderRadius: 10, | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| }, | ||
| startButtonBusy: { | ||
| backgroundColor: '#7f879b', | ||
| }, | ||
| startButtonText: { | ||
| color: 'white', | ||
| fontWeight: '700', | ||
| }, | ||
| button: { | ||
| width: 200, | ||
| height: 50, | ||
| backgroundColor: 'lightblue', | ||
| borderRadius: 10, | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| }, | ||
| results: { | ||
| marginTop: 20, | ||
| padding: 16, | ||
| borderRadius: 12, | ||
| backgroundColor: '#eef3fb', | ||
| width: '100%', | ||
| gap: 6, | ||
| }, | ||
| resultText: { | ||
| color: '#33415c', | ||
| fontSize: 13, | ||
| }, | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it works here - callbacks are fired one by one so it is easier to read that on console where you see all of the outputs.