Skip to content

Commit ee3aa70

Browse files
misc(feedback): Improve Feedback Sheet interactions (#4571)
1 parent ef4be9e commit ee3aa70

File tree

4 files changed

+1701
-1953
lines changed

4 files changed

+1701
-1953
lines changed

packages/core/src/js/feedback/FeedbackWidget.styles.ts

+14-15
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,28 @@ import type { ViewStyle } from 'react-native';
33
import type { FeedbackWidgetStyles } from './FeedbackWidget.types';
44

55
const PURPLE = 'rgba(88, 74, 192, 1)';
6-
const FORGROUND_COLOR = '#2b2233';
7-
const BACKROUND_COLOR = '#ffffff';
6+
const FOREGROUND_COLOR = '#2b2233';
7+
const BACKGROUND_COLOR = '#ffffff';
88
const BORDER_COLOR = 'rgba(41, 35, 47, 0.13)';
99

1010
const defaultStyles: FeedbackWidgetStyles = {
1111
container: {
1212
flex: 1,
1313
padding: 20,
14-
backgroundColor: BACKROUND_COLOR,
14+
backgroundColor: BACKGROUND_COLOR,
1515
},
1616
title: {
1717
fontSize: 24,
1818
fontWeight: 'bold',
1919
marginBottom: 20,
2020
textAlign: 'left',
2121
flex: 1,
22-
color: FORGROUND_COLOR,
22+
color: FOREGROUND_COLOR,
2323
},
2424
label: {
2525
marginBottom: 4,
2626
fontSize: 16,
27-
color: FORGROUND_COLOR,
27+
color: FOREGROUND_COLOR,
2828
},
2929
input: {
3030
height: 50,
@@ -34,12 +34,12 @@ const defaultStyles: FeedbackWidgetStyles = {
3434
paddingHorizontal: 10,
3535
marginBottom: 15,
3636
fontSize: 16,
37-
color: FORGROUND_COLOR,
37+
color: FOREGROUND_COLOR,
3838
},
3939
textArea: {
4040
height: 100,
4141
textAlignVertical: 'top',
42-
color: FORGROUND_COLOR,
42+
color: FOREGROUND_COLOR,
4343
},
4444
screenshotButton: {
4545
backgroundColor: '#eee',
@@ -72,15 +72,15 @@ const defaultStyles: FeedbackWidgetStyles = {
7272
marginBottom: 10,
7373
},
7474
submitText: {
75-
color: BACKROUND_COLOR,
75+
color: BACKGROUND_COLOR,
7676
fontSize: 18,
7777
},
7878
cancelButton: {
7979
paddingVertical: 15,
8080
alignItems: 'center',
8181
},
8282
cancelText: {
83-
color: FORGROUND_COLOR,
83+
color: FOREGROUND_COLOR,
8484
fontSize: 16,
8585
},
8686
titleContainer: {
@@ -101,23 +101,22 @@ export const modalWrapper: ViewStyle = {
101101
bottom: 0,
102102
};
103103

104-
export const modalBackground: ViewStyle = {
105-
flex: 1,
106-
justifyContent: 'flex-end',
107-
};
108-
109104
export const modalSheetContainer: ViewStyle = {
110105
backgroundColor: '#ffffff',
111106
borderTopLeftRadius: 16,
112107
borderTopRightRadius: 16,
113108
overflow: 'hidden',
114109
alignSelf: 'stretch',
115-
height: '92%',
116110
shadowColor: '#000',
117111
shadowOffset: { width: 0, height: -3 },
118112
shadowOpacity: 0.1,
119113
shadowRadius: 4,
120114
elevation: 5,
115+
flex: 1,
116+
};
117+
118+
export const topSpacer: ViewStyle = {
119+
height: 64, // magic number
121120
};
122121

123122
export default defaultStyles;

packages/core/src/js/feedback/FeedbackWidget.tsx

+75-89
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@ import type { KeyboardTypeOptions } from 'react-native';
55
import {
66
Image,
77
Keyboard,
8-
KeyboardAvoidingView,
9-
Platform,
10-
SafeAreaView,
11-
ScrollView,
128
Text,
139
TextInput,
1410
TouchableOpacity,
@@ -222,97 +218,87 @@ export class FeedbackWidget extends React.Component<FeedbackWidgetProps, Feedbac
222218
}
223219

224220
return (
225-
<SafeAreaView style={[styles.container, { padding: 0 }]}>
226-
<KeyboardAvoidingView
227-
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
228-
style={[styles.container, { padding: 0 }]}
229-
enabled={notWeb()}
230-
>
231-
<ScrollView bounces={false}>
232-
<TouchableWithoutFeedback onPress={notWeb() ? Keyboard.dismiss: undefined}>
233-
<View style={styles.container}>
234-
<View style={styles.titleContainer}>
235-
<Text style={styles.title}>{text.formTitle}</Text>
236-
{config.showBranding && (
237-
<Image
238-
source={{ uri: sentryLogo }}
239-
style={styles.sentryLogo}
240-
testID='sentry-logo'
241-
/>
242-
)}
243-
</View>
221+
<TouchableWithoutFeedback onPress={notWeb() ? Keyboard.dismiss: undefined}>
222+
<View style={styles.container}>
223+
<View style={styles.titleContainer}>
224+
<Text style={styles.title}>{text.formTitle}</Text>
225+
{config.showBranding && (
226+
<Image
227+
source={{ uri: sentryLogo }}
228+
style={styles.sentryLogo}
229+
testID='sentry-logo'
230+
/>
231+
)}
232+
</View>
244233

245-
{config.showName && (
246-
<>
247-
<Text style={styles.label}>
248-
{text.nameLabel}
249-
{config.isNameRequired && ` ${text.isRequiredLabel}`}
250-
</Text>
251-
<TextInput
252-
style={styles.input}
253-
placeholder={text.namePlaceholder}
254-
value={name}
255-
onChangeText={(value) => this.setState({ name: value })}
256-
/>
257-
</>
258-
)}
234+
{config.showName && (
235+
<>
236+
<Text style={styles.label}>
237+
{text.nameLabel}
238+
{config.isNameRequired && ` ${text.isRequiredLabel}`}
239+
</Text>
240+
<TextInput
241+
style={styles.input}
242+
placeholder={text.namePlaceholder}
243+
value={name}
244+
onChangeText={(value) => this.setState({ name: value })}
245+
/>
246+
</>
247+
)}
259248

260-
{config.showEmail && (
261-
<>
262-
<Text style={styles.label}>
263-
{text.emailLabel}
264-
{config.isEmailRequired && ` ${text.isRequiredLabel}`}
265-
</Text>
266-
<TextInput
267-
style={styles.input}
268-
placeholder={text.emailPlaceholder}
269-
keyboardType={'email-address' as KeyboardTypeOptions}
270-
value={email}
271-
onChangeText={(value) => this.setState({ email: value })}
272-
/>
273-
</>
274-
)}
249+
{config.showEmail && (
250+
<>
251+
<Text style={styles.label}>
252+
{text.emailLabel}
253+
{config.isEmailRequired && ` ${text.isRequiredLabel}`}
254+
</Text>
255+
<TextInput
256+
style={styles.input}
257+
placeholder={text.emailPlaceholder}
258+
keyboardType={'email-address' as KeyboardTypeOptions}
259+
value={email}
260+
onChangeText={(value) => this.setState({ email: value })}
261+
/>
262+
</>
263+
)}
275264

276-
<Text style={styles.label}>
277-
{text.messageLabel}
278-
{` ${text.isRequiredLabel}`}
279-
</Text>
280-
<TextInput
281-
style={[styles.input, styles.textArea]}
282-
placeholder={text.messagePlaceholder}
283-
value={description}
284-
onChangeText={(value) => this.setState({ description: value })}
285-
multiline
286-
/>
287-
{(config.enableScreenshot || imagePickerConfiguration.imagePicker) && (
288-
<View style={styles.screenshotContainer}>
289-
{this.state.attachmentUri && (
290-
<Image
291-
source={{ uri: this.state.attachmentUri }}
292-
style={styles.screenshotThumbnail}
293-
/>
294-
)}
295-
<TouchableOpacity style={styles.screenshotButton} onPress={this.onScreenshotButtonPress}>
296-
<Text style={styles.screenshotText}>
297-
{!this.state.filename && !this.state.attachment
298-
? text.addScreenshotButtonLabel
299-
: text.removeScreenshotButtonLabel}
300-
</Text>
301-
</TouchableOpacity>
302-
</View>
265+
<Text style={styles.label}>
266+
{text.messageLabel}
267+
{` ${text.isRequiredLabel}`}
268+
</Text>
269+
<TextInput
270+
style={[styles.input, styles.textArea]}
271+
placeholder={text.messagePlaceholder}
272+
value={description}
273+
onChangeText={(value) => this.setState({ description: value })}
274+
multiline
275+
/>
276+
{(config.enableScreenshot || imagePickerConfiguration.imagePicker) && (
277+
<View style={styles.screenshotContainer}>
278+
{this.state.attachmentUri && (
279+
<Image
280+
source={{ uri: this.state.attachmentUri }}
281+
style={styles.screenshotThumbnail}
282+
/>
303283
)}
304-
<TouchableOpacity style={styles.submitButton} onPress={this.handleFeedbackSubmit}>
305-
<Text style={styles.submitText}>{text.submitButtonLabel}</Text>
306-
</TouchableOpacity>
307-
308-
<TouchableOpacity style={styles.cancelButton} onPress={onCancel}>
309-
<Text style={styles.cancelText}>{text.cancelButtonLabel}</Text>
284+
<TouchableOpacity style={styles.screenshotButton} onPress={this.onScreenshotButtonPress}>
285+
<Text style={styles.screenshotText}>
286+
{!this.state.filename && !this.state.attachment
287+
? text.addScreenshotButtonLabel
288+
: text.removeScreenshotButtonLabel}
289+
</Text>
310290
</TouchableOpacity>
311291
</View>
312-
</TouchableWithoutFeedback>
313-
</ScrollView>
314-
</KeyboardAvoidingView>
315-
</SafeAreaView>
292+
)}
293+
<TouchableOpacity style={styles.submitButton} onPress={this.handleFeedbackSubmit}>
294+
<Text style={styles.submitText}>{text.submitButtonLabel}</Text>
295+
</TouchableOpacity>
296+
297+
<TouchableOpacity style={styles.cancelButton} onPress={onCancel}>
298+
<Text style={styles.cancelText}>{text.cancelButtonLabel}</Text>
299+
</TouchableOpacity>
300+
</View>
301+
</TouchableWithoutFeedback>
316302
);
317303
}
318304

packages/core/src/js/feedback/FeedbackWidgetManager.tsx

+32-25
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import { logger } from '@sentry/core';
22
import * as React from 'react';
3-
import { Animated, Dimensions, Easing, KeyboardAvoidingView, Modal, PanResponder, Platform } from 'react-native';
3+
import type { NativeScrollEvent, NativeSyntheticEvent} from 'react-native';
4+
import { Animated, Dimensions, Easing, Modal, PanResponder, Platform, ScrollView, View } from 'react-native';
45

56
import { notWeb } from '../utils/environment';
67
import { FeedbackWidget } from './FeedbackWidget';
7-
import { modalBackground, modalSheetContainer, modalWrapper } from './FeedbackWidget.styles';
8+
import { modalSheetContainer, modalWrapper, topSpacer } from './FeedbackWidget.styles';
89
import type { FeedbackWidgetStyles } from './FeedbackWidget.types';
910
import { getFeedbackOptions } from './integration';
1011
import { isModalSupported } from './utils';
1112

12-
const PULL_DOWN_CLOSE_THREESHOLD = 200;
13-
const PULL_DOWN_ANDROID_ACTIVATION_HEIGHT = 150;
13+
const PULL_DOWN_CLOSE_THRESHOLD = 200;
1414
const SLIDE_ANIMATION_DURATION = 200;
1515
const BACKGROUND_ANIMATION_DURATION = 200;
1616

@@ -50,38 +50,41 @@ interface FeedbackWidgetProviderState {
5050
isVisible: boolean;
5151
backgroundOpacity: Animated.Value;
5252
panY: Animated.Value;
53+
isScrollAtTop: boolean;
5354
}
5455

5556
class FeedbackWidgetProvider extends React.Component<FeedbackWidgetProviderProps> {
5657
public state: FeedbackWidgetProviderState = {
5758
isVisible: false,
5859
backgroundOpacity: new Animated.Value(0),
5960
panY: new Animated.Value(Dimensions.get('screen').height),
61+
isScrollAtTop: true,
6062
};
6163

6264
private _panResponder = PanResponder.create({
63-
onStartShouldSetPanResponder: (evt, _gestureState) => {
64-
// On Android allow pulling down only from the top to avoid breaking native gestures
65-
return notWeb() && (Platform.OS !== 'android' || evt.nativeEvent.pageY < PULL_DOWN_ANDROID_ACTIVATION_HEIGHT);
65+
onStartShouldSetPanResponder: (_, gestureState) => {
66+
return notWeb() && this.state.isScrollAtTop && gestureState.dy > 0;
6667
},
67-
onMoveShouldSetPanResponder: (evt, _gestureState) => {
68-
return notWeb() && (Platform.OS !== 'android' || evt.nativeEvent.pageY < PULL_DOWN_ANDROID_ACTIVATION_HEIGHT);
68+
onMoveShouldSetPanResponder: (_, gestureState) => {
69+
return notWeb() && this.state.isScrollAtTop && gestureState.dy > 0;
6970
},
7071
onPanResponderMove: (_, gestureState) => {
7172
if (gestureState.dy > 0) {
7273
this.state.panY.setValue(gestureState.dy);
7374
}
7475
},
7576
onPanResponderRelease: (_, gestureState) => {
76-
if (gestureState.dy > PULL_DOWN_CLOSE_THREESHOLD) { // Close on swipe below a certain threshold
77+
if (gestureState.dy > PULL_DOWN_CLOSE_THRESHOLD) {
78+
// Close on swipe below a certain threshold
7779
Animated.timing(this.state.panY, {
7880
toValue: Dimensions.get('screen').height,
7981
duration: SLIDE_ANIMATION_DURATION,
8082
useNativeDriver: true,
8183
}).start(() => {
8284
this._handleClose();
8385
});
84-
} else { // Animate it back to the original position
86+
} else {
87+
// Animate it back to the original position
8588
Animated.spring(this.state.panY, {
8689
toValue: 0,
8790
useNativeDriver: true,
@@ -142,31 +145,35 @@ class FeedbackWidgetProvider extends React.Component<FeedbackWidgetProviderProps
142145
return (
143146
<>
144147
{this.props.children}
145-
{isVisible && (
148+
{isVisible &&
146149
<Animated.View style={[modalWrapper, { backgroundColor }]} >
147150
<Modal visible={isVisible} transparent animationType="none" onRequestClose={this._handleClose} testID="feedback-form-modal">
148-
<KeyboardAvoidingView
149-
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
150-
style={modalBackground}
151-
enabled={notWeb()}
152-
>
153-
<Animated.View
154-
style={[modalSheetContainer, { transform: [{ translateY: this.state.panY }] }]}
155-
{...this._panResponder.panHandlers}
156-
>
151+
<View style={topSpacer}/>
152+
<Animated.View
153+
style={[modalSheetContainer, { transform: [{ translateY: this.state.panY }] }]}
154+
{...this._panResponder.panHandlers}>
155+
<ScrollView
156+
bounces={false}
157+
keyboardShouldPersistTaps="handled"
158+
automaticallyAdjustKeyboardInsets={Platform.OS === 'ios'}
159+
onScroll={this._handleScroll}>
157160
<FeedbackWidget {...getFeedbackOptions()}
158161
onFormClose={this._handleClose}
159162
onFormSubmitted={this._handleClose}
160-
/>
161-
</Animated.View>
162-
</KeyboardAvoidingView>
163+
/>
164+
</ScrollView>
165+
</Animated.View>
163166
</Modal>
164167
</Animated.View>
165-
)}
168+
}
166169
</>
167170
);
168171
}
169172

173+
private _handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>): void => {
174+
this.setState({ isScrollAtTop: event.nativeEvent.contentOffset.y <= 0 });
175+
};
176+
170177
private _setVisibilityFunction = (visible: boolean): void => {
171178
const updateState = (): void => {
172179
this.setState({ isVisible: visible });

0 commit comments

Comments
 (0)