-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Fix handling keyboard should persists taps #4158
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
coado
wants to merge
18
commits into
main
Choose a base branch
from
@coado/keyboardShouldPersistTaps
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
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
b90ef5d
capturing touch event in GH Pressable
coado abd43f1
move onStartShouldSetResponder
coado 6ed0aa7
add to NativeDetector
coado 9b00067
Merge branch 'main' into @coado/keyboardShouldPersistTaps
coado a5cce68
better example
coado 094607c
check if disabled
coado d1e7795
remove logs
coado fdbd840
check if gesture is enabled
coado 3fd544c
chore: polish responder notes and detector condition
Copilot 4b905a5
register only if gesture is tap gesture
coado 4d097a8
Catch JSResponder event right before the ScrollView
coado 3a311f4
tests
coado 5d8f593
fix lint
coado df0a998
change name and add comment
coado 93519a5
support any gesture
coado 874150a
move into single file, rename
coado 3f6f680
Merge branch 'main' into @coado/keyboardShouldPersistTaps
coado 186fb26
subscribe to handlers
coado 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
369 changes: 369 additions & 0 deletions
369
apps/common-app/src/new_api/tests/keyboardShouldPersistTaps/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,369 @@ | ||
| import React, { useRef, useState } from 'react'; | ||
| import { | ||
| Keyboard, | ||
| Pressable as RNPressable, | ||
| ScrollView as RNScrollView, | ||
| StyleSheet, | ||
| Text, | ||
| TextInput as RNTextInput, | ||
| View, | ||
| } from 'react-native'; | ||
| import { | ||
| GestureDetector, | ||
| Pressable as RNGHPressable, | ||
| ScrollView as RNGHScrollView, | ||
| TextInput as RNGHTextInput, | ||
| useTapGesture, | ||
| } from 'react-native-gesture-handler'; | ||
|
|
||
| import type { FeedbackHandle } from '../../../common'; | ||
| import { COLORS, Feedback, InfoSection } from '../../../common'; | ||
|
|
||
| type Mode = 'never' | 'handled' | 'always'; | ||
| type Example = 'pressable' | 'tap'; | ||
|
|
||
| const MODES: Mode[] = ['never', 'handled', 'always']; | ||
| const EXAMPLES: Example[] = ['pressable', 'tap']; | ||
|
|
||
| const EXAMPLE_LABELS: Record<Example, string> = { | ||
| pressable: 'GH Pressable', | ||
| tap: 'useTapGesture', | ||
| }; | ||
|
|
||
| const MODE_DESCRIPTIONS: Record<Mode, string> = { | ||
| never: | ||
| "RN: first tap outside the input dismisses the keyboard AND is swallowed — press doesn't fire. GH: keyboard still dismisses (ScrollView captures the responder normally), but the press ALSO fires because GH's native recognizer runs in parallel to the JS responder system.", | ||
| handled: | ||
| 'Keyboard stays up if a child claims the tap. Tap an input to raise the keyboard, then tap a button — press fires and keyboard stays. RN and GH match here.', | ||
| always: | ||
| "Keyboard never auto-dismisses on tap; children always receive taps. You'd have to call Keyboard.dismiss() yourself. RN and GH match here.", | ||
| }; | ||
|
|
||
| export default function KeyboardShouldPersistTapsExample() { | ||
| const [mode, setMode] = useState<Mode>('handled'); | ||
| const [example, setExample] = useState<Example>('pressable'); | ||
| const feedbackRef = useRef<FeedbackHandle>(null); | ||
|
|
||
| const report = (message: string) => { | ||
| feedbackRef.current?.showMessage(message); | ||
| }; | ||
|
|
||
| return ( | ||
| <View style={styles.container}> | ||
| <View style={styles.topBar}> | ||
| <ModeSelector value={mode} onChange={setMode} /> | ||
| <RNPressable style={styles.dismiss} onPress={() => Keyboard.dismiss()}> | ||
| <Text style={styles.dismissText}>Dismiss KB</Text> | ||
| </RNPressable> | ||
| </View> | ||
|
|
||
| <ExampleSelector value={example} onChange={setExample} /> | ||
|
|
||
| <View style={styles.panelRow}> | ||
| <Panel | ||
| title="React Native" | ||
| accent={COLORS.NAVY} | ||
| ScrollViewComponent={RNScrollView} | ||
| mode={mode}> | ||
| <RNTextInput | ||
| style={styles.input} | ||
| placeholder="RN input" | ||
| placeholderTextColor={COLORS.GRAY} | ||
| /> | ||
| <RNPressable | ||
| style={({ pressed }) => [ | ||
| styles.button, | ||
| { | ||
| backgroundColor: pressed | ||
| ? COLORS.KINDA_BLUE | ||
| : COLORS.LIGHT_BLUE, | ||
| }, | ||
| ]} | ||
| onPress={() => report('RN Pressable onPress')}> | ||
| <Text style={styles.buttonText}>Press me</Text> | ||
| </RNPressable> | ||
| </Panel> | ||
|
|
||
| <Panel | ||
| title={EXAMPLE_LABELS[example]} | ||
| accent={COLORS.DARK_GREEN} | ||
| ScrollViewComponent={RNGHScrollView} | ||
| mode={mode}> | ||
| <RNGHTextInput | ||
| style={styles.input} | ||
| placeholder="GH input" | ||
| placeholderTextColor={COLORS.GRAY} | ||
| /> | ||
| {example === 'pressable' ? ( | ||
| <RNGHPressable | ||
| style={({ pressed }) => [ | ||
| styles.button, | ||
| { | ||
| backgroundColor: pressed ? COLORS.KINDA_GREEN : COLORS.GREEN, | ||
| }, | ||
| ]} | ||
| onPress={() => report('GH Pressable onPress')}> | ||
| <Text style={styles.buttonText}>Press me</Text> | ||
| </RNGHPressable> | ||
| ) : ( | ||
| <GestureTapButton | ||
| onTap={() => report('useTapGesture onActivate')} | ||
| /> | ||
| )} | ||
| </Panel> | ||
| </View> | ||
|
|
||
| <InfoSection description={MODE_DESCRIPTIONS[mode]} /> | ||
|
|
||
| <View style={styles.feedbackArea}> | ||
| <Feedback ref={feedbackRef} duration={1500} /> | ||
| </View> | ||
| </View> | ||
| ); | ||
| } | ||
|
|
||
| type ExampleSelectorProps = { | ||
| value: Example; | ||
| onChange: (next: Example) => void; | ||
| }; | ||
|
|
||
| function ExampleSelector({ value, onChange }: ExampleSelectorProps) { | ||
| return ( | ||
| <View style={styles.exampleRow}> | ||
| {EXAMPLES.map((example) => { | ||
| const active = example === value; | ||
| return ( | ||
| <RNPressable | ||
| key={example} | ||
| onPress={() => onChange(example)} | ||
| style={[styles.exampleTab, active && styles.exampleTabActive]}> | ||
| <Text | ||
| style={[ | ||
| styles.exampleTabLabel, | ||
| active && styles.exampleTabLabelActive, | ||
| ]}> | ||
| {EXAMPLE_LABELS[example]} | ||
| </Text> | ||
| </RNPressable> | ||
| ); | ||
| })} | ||
| </View> | ||
| ); | ||
| } | ||
|
|
||
| type GestureTapButtonProps = { | ||
| onTap: () => void; | ||
| }; | ||
|
|
||
| function GestureTapButton({ onTap }: GestureTapButtonProps) { | ||
| const [pressed, setPressed] = useState(false); | ||
|
|
||
| const tap = useTapGesture({ | ||
| disableReanimated: true, | ||
| onBegin: () => { | ||
| setPressed(true); | ||
| }, | ||
| onActivate: onTap, | ||
| onFinalize: () => { | ||
| setPressed(false); | ||
| }, | ||
| }); | ||
|
|
||
| return ( | ||
| <GestureDetector gesture={tap}> | ||
| <View | ||
| collapsable={false} | ||
| style={[ | ||
| styles.button, | ||
| { | ||
| backgroundColor: pressed ? COLORS.KINDA_GREEN : COLORS.GREEN, | ||
| }, | ||
| ]}> | ||
| <Text style={styles.buttonText}>Tap me</Text> | ||
| </View> | ||
| </GestureDetector> | ||
| ); | ||
| } | ||
|
|
||
| type ModeSelectorProps = { | ||
| value: Mode; | ||
| onChange: (next: Mode) => void; | ||
| }; | ||
|
|
||
| function ModeSelector({ value, onChange }: ModeSelectorProps) { | ||
| return ( | ||
| <View style={styles.modeRow}> | ||
| {MODES.map((m) => { | ||
| const active = m === value; | ||
| return ( | ||
| <RNPressable | ||
| key={m} | ||
| onPress={() => onChange(m)} | ||
| style={[styles.modeChip, active && styles.modeChipActive]}> | ||
| <Text style={[styles.modeLabel, active && styles.modeLabelActive]}> | ||
| {m} | ||
| </Text> | ||
| </RNPressable> | ||
| ); | ||
| })} | ||
| </View> | ||
| ); | ||
| } | ||
|
|
||
| type PanelProps = { | ||
| title: string; | ||
| accent: string; | ||
| mode: Mode; | ||
| ScrollViewComponent: React.ComponentType< | ||
| React.ComponentProps<typeof RNScrollView> | ||
| >; | ||
| children: React.ReactNode; | ||
| }; | ||
|
|
||
| function Panel({ | ||
| title, | ||
| accent, | ||
| mode, | ||
| ScrollViewComponent, | ||
| children, | ||
| }: PanelProps) { | ||
| return ( | ||
| <View style={[styles.panel, { borderColor: accent }]}> | ||
| <View style={[styles.panelHeader, { backgroundColor: accent }]}> | ||
| <Text style={styles.panelTitle}>{title}</Text> | ||
| </View> | ||
| <ScrollViewComponent | ||
| keyboardShouldPersistTaps={mode} | ||
| contentContainerStyle={styles.panelBody}> | ||
| {children} | ||
| </ScrollViewComponent> | ||
| </View> | ||
| ); | ||
| } | ||
|
|
||
| const styles = StyleSheet.create({ | ||
| container: { | ||
| flex: 1, | ||
| padding: 12, | ||
| gap: 10, | ||
| backgroundColor: COLORS.offWhite, | ||
| }, | ||
| topBar: { | ||
| flexDirection: 'row', | ||
| alignItems: 'stretch', | ||
| gap: 8, | ||
| }, | ||
| modeRow: { | ||
| flex: 1, | ||
| flexDirection: 'row', | ||
| gap: 6, | ||
| padding: 4, | ||
| borderRadius: 10, | ||
| backgroundColor: COLORS.headerSeparator, | ||
| }, | ||
| modeChip: { | ||
| flex: 1, | ||
| paddingVertical: 8, | ||
| borderRadius: 7, | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| }, | ||
| modeChipActive: { | ||
| backgroundColor: COLORS.NAVY, | ||
| }, | ||
| modeLabel: { | ||
| fontSize: 13, | ||
| fontWeight: '600', | ||
| color: COLORS.NAVY, | ||
| fontFamily: 'Menlo', | ||
| }, | ||
| modeLabelActive: { | ||
| color: '#ffffff', | ||
| }, | ||
| dismiss: { | ||
| paddingHorizontal: 12, | ||
| justifyContent: 'center', | ||
| borderRadius: 10, | ||
| backgroundColor: COLORS.PURPLE, | ||
| }, | ||
| dismissText: { | ||
| color: '#ffffff', | ||
| fontWeight: '700', | ||
| fontSize: 12, | ||
| }, | ||
| exampleRow: { | ||
| flexDirection: 'row', | ||
| gap: 8, | ||
| }, | ||
| exampleTab: { | ||
| flex: 1, | ||
| paddingVertical: 10, | ||
| borderRadius: 10, | ||
| borderWidth: 2, | ||
| borderColor: COLORS.DARK_GREEN, | ||
| alignItems: 'center', | ||
| backgroundColor: 'transparent', | ||
| }, | ||
| exampleTabActive: { | ||
| backgroundColor: COLORS.DARK_GREEN, | ||
| }, | ||
| exampleTabLabel: { | ||
| color: COLORS.DARK_GREEN, | ||
| fontWeight: '700', | ||
| fontSize: 14, | ||
| }, | ||
| exampleTabLabelActive: { | ||
| color: '#ffffff', | ||
| }, | ||
| panelRow: { | ||
| flexDirection: 'row', | ||
| gap: 10, | ||
| height: 200, | ||
| }, | ||
| panel: { | ||
| flex: 1, | ||
| borderWidth: 2, | ||
| borderRadius: 10, | ||
| overflow: 'hidden', | ||
| backgroundColor: '#ffffff', | ||
| }, | ||
| panelHeader: { | ||
| paddingHorizontal: 10, | ||
| paddingVertical: 6, | ||
| flexDirection: 'row', | ||
| justifyContent: 'space-between', | ||
| alignItems: 'center', | ||
| }, | ||
| panelTitle: { | ||
| color: '#ffffff', | ||
| fontSize: 13, | ||
| fontWeight: '700', | ||
| }, | ||
| panelBody: { | ||
| padding: 10, | ||
| gap: 10, | ||
| }, | ||
| input: { | ||
| borderWidth: 1, | ||
| borderColor: COLORS.GRAY, | ||
| borderRadius: 8, | ||
| paddingHorizontal: 10, | ||
| paddingVertical: 8, | ||
| fontSize: 14, | ||
| backgroundColor: '#ffffff', | ||
| }, | ||
| button: { | ||
| borderRadius: 8, | ||
| paddingVertical: 12, | ||
| alignItems: 'center', | ||
| }, | ||
| buttonText: { | ||
| color: '#ffffff', | ||
| fontWeight: '700', | ||
| fontSize: 14, | ||
| }, | ||
| feedbackArea: { | ||
| alignItems: 'center', | ||
| minHeight: 30, | ||
| }, | ||
| }); | ||
3 changes: 2 additions & 1 deletion
3
packages/docs-gesture-handler/src/components/Hero/StartScreen/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
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.
Can we handle that in RNGH as well?