Skip to content

Commit aed4bf0

Browse files
authored
Merge pull request #9587 from getsentry/feedback-name-and-email-required
feat(feedback): Option to make name and email required
2 parents ae07444 + 421e030 commit aed4bf0

File tree

6 files changed

+124
-20
lines changed

6 files changed

+124
-20
lines changed

packages/feedback/src/widget/Dialog.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ export function Dialog({
4545
showBranding,
4646
showName,
4747
showEmail,
48+
isNameRequired,
49+
isEmailRequired,
4850
colorScheme,
4951
isAnonymous,
5052
defaultName,
@@ -102,6 +104,8 @@ export function Dialog({
102104
showEmail,
103105
showName,
104106
isAnonymous,
107+
isEmailRequired,
108+
isNameRequired,
105109

106110
defaultName,
107111
defaultEmail,

packages/feedback/src/widget/Form.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export interface FormComponentProps
88
| 'showName'
99
| 'showEmail'
1010
| 'isAnonymous'
11+
| 'isNameRequired'
12+
| 'isEmailRequired'
1113
| Exclude<keyof FeedbackTextConfiguration, 'buttonLabel' | 'formTitle' | 'successMessageText'>
1214
> {
1315
/**
@@ -58,6 +60,8 @@ export function Form({
5860
showName,
5961
showEmail,
6062
isAnonymous,
63+
isNameRequired,
64+
isEmailRequired,
6165

6266
defaultName,
6367
defaultEmail,
@@ -113,6 +117,7 @@ export function Form({
113117
type: showName ? 'text' : 'hidden',
114118
['aria-hidden']: showName ? 'false' : 'true',
115119
name: 'name',
120+
required: isNameRequired,
116121
className: 'form__input',
117122
placeholder: namePlaceholder,
118123
value: defaultName,
@@ -123,6 +128,7 @@ export function Form({
123128
type: showEmail ? 'text' : 'hidden',
124129
['aria-hidden']: showEmail ? 'false' : 'true',
125130
name: 'email',
131+
required: isEmailRequired,
126132
className: 'form__input',
127133
placeholder: emailPlaceholder,
128134
value: defaultEmail,
@@ -168,7 +174,15 @@ export function Form({
168174
htmlFor: 'name',
169175
className: 'form__label',
170176
},
171-
[nameLabel, nameEl],
177+
[
178+
createElement(
179+
'span',
180+
{ className: 'form__label__text' },
181+
nameLabel,
182+
isNameRequired && createElement('span', { className: 'form__label__text--required' }, ' (required)'),
183+
),
184+
nameEl,
185+
],
172186
),
173187
!isAnonymous && !showName && nameEl,
174188

@@ -180,7 +194,15 @@ export function Form({
180194
htmlFor: 'email',
181195
className: 'form__label',
182196
},
183-
[emailLabel, emailEl],
197+
[
198+
createElement(
199+
'span',
200+
{ className: 'form__label__text' },
201+
emailLabel,
202+
isEmailRequired && createElement('span', { className: 'form__label__text--required' }, ' (required)'),
203+
),
204+
emailEl,
205+
],
184206
),
185207
!isAnonymous && !showEmail && emailEl,
186208

packages/feedback/src/widget/createWidget.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,19 @@ export function createWidget({
8888
return;
8989
}
9090

91-
// Simple validation for now, just check for non-empty message
91+
// Simple validation for now, just check for non-empty required fields
92+
const emptyField = [];
93+
if (options.isNameRequired && !feedback.name) {
94+
emptyField.push(options.nameLabel);
95+
}
96+
if (options.isEmailRequired && !feedback.email) {
97+
emptyField.push(options.emailLabel);
98+
}
9299
if (!feedback.message) {
93-
dialog.showError('Please enter in some feedback before submitting!');
100+
emptyField.push(options.messageLabel);
101+
}
102+
if (emptyField.length != 0) {
103+
dialog.showError(`Please enter in the following required fields: ${emptyField.join(', ')}`);
94104
return;
95105
}
96106

@@ -156,9 +166,11 @@ export function createWidget({
156166
dialog = Dialog({
157167
colorScheme: options.colorScheme,
158168
showBranding: options.showBranding,
159-
showName: options.showName,
160-
showEmail: options.showEmail,
169+
showName: options.showName || options.isNameRequired,
170+
showEmail: options.showEmail || options.isEmailRequired,
161171
isAnonymous: options.isAnonymous,
172+
isNameRequired: options.isNameRequired,
173+
isEmailRequired: options.isEmailRequired,
162174
formTitle: options.formTitle,
163175
cancelButtonLabel: options.cancelButtonLabel,
164176
submitButtonLabel: options.submitButtonLabel,

packages/feedback/test/widget/Dialog.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ function renderDialog({
1010
showEmail = true,
1111
showBranding = false,
1212
isAnonymous = false,
13+
isNameRequired = false,
14+
isEmailRequired = false,
1315
formTitle = 'Feedback',
1416
defaultName = 'Foo Bar',
1517
defaultEmail = '[email protected]',
@@ -30,6 +32,8 @@ function renderDialog({
3032
isAnonymous,
3133
showName,
3234
showEmail,
35+
isNameRequired,
36+
isEmailRequired,
3337
showBranding,
3438
defaultName,
3539
defaultEmail,

packages/feedback/test/widget/Form.test.ts

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ function renderForm({
99
showName = true,
1010
showEmail = true,
1111
isAnonymous = false,
12+
isNameRequired = false,
13+
isEmailRequired = false,
1214
defaultName = 'Foo Bar',
1315
defaultEmail = '[email protected]',
1416
nameLabel = 'Name',
@@ -25,6 +27,8 @@ function renderForm({
2527
isAnonymous,
2628
showName,
2729
showEmail,
30+
isNameRequired,
31+
isEmailRequired,
2832
defaultName,
2933
defaultEmail,
3034
nameLabel,
@@ -80,13 +84,15 @@ describe('Form', () => {
8084
emailPlaceholder: '[email protected]!',
8185
messageLabel: 'Description!',
8286
messagePlaceholder: 'What is the issue?!',
87+
isNameRequired: true,
88+
isEmailRequired: true,
8389
});
8490

8591
const nameLabel = formComponent.el.querySelector('label[htmlFor="name"]') as HTMLLabelElement;
8692
const emailLabel = formComponent.el.querySelector('label[htmlFor="email"]') as HTMLLabelElement;
8793
const messageLabel = formComponent.el.querySelector('label[htmlFor="message"]') as HTMLLabelElement;
88-
expect(nameLabel.textContent).toBe('Name!');
89-
expect(emailLabel.textContent).toBe('Email!');
94+
expect(nameLabel.textContent).toBe('Name! (required)');
95+
expect(emailLabel.textContent).toBe('Email! (required)');
9096
expect(messageLabel.textContent).toBe('Description! (required)');
9197

9298
const nameInput = formComponent.el.querySelector('[name="name"]') as HTMLInputElement;
@@ -98,18 +104,6 @@ describe('Form', () => {
98104
expect(messageInput.placeholder).toBe('What is the issue?!');
99105
});
100106

101-
it('submit is enabled if message is not empty', () => {
102-
const formComponent = renderForm();
103-
104-
const message = formComponent.el.querySelector('[name="message"]') as HTMLTextAreaElement;
105-
106-
message.value = 'Foo (message)';
107-
message.dispatchEvent(new KeyboardEvent('keyup'));
108-
109-
message.value = '';
110-
message.dispatchEvent(new KeyboardEvent('keyup'));
111-
});
112-
113107
it('can show error', () => {
114108
const formComponent = renderForm();
115109
const errorEl = formComponent.el.querySelector('.form__error-container') as HTMLDivElement;
@@ -148,6 +142,8 @@ describe('Form', () => {
148142
it('does not show name or email inputs for anonymous mode', () => {
149143
const onSubmit = jest.fn();
150144
const formComponent = renderForm({
145+
isNameRequired: true,
146+
isEmailRequired: true,
151147
isAnonymous: true,
152148
onSubmit,
153149
});

packages/feedback/test/widget/createWidget.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,72 @@ describe('createWidget', () => {
171171
expect(shadow.querySelector('.success-message')).toBeNull();
172172
});
173173

174+
it('only submits feedback successfully when all required fields are filled', async () => {
175+
const onSubmitSuccess = jest.fn(() => {});
176+
const { shadow, widget } = createShadowAndWidget({
177+
isNameRequired: true,
178+
isEmailRequired: true,
179+
onSubmitSuccess,
180+
});
181+
182+
(sendFeedbackRequest as jest.Mock).mockImplementation(() => {
183+
return true;
184+
});
185+
widget.actor?.el?.dispatchEvent(new Event('click'));
186+
187+
const nameEl = widget.dialog?.el?.querySelector('[name="name"]') as HTMLInputElement;
188+
const emailEl = widget.dialog?.el?.querySelector('[name="email"]') as HTMLInputElement;
189+
const messageEl = widget.dialog?.el?.querySelector('[name="message"]') as HTMLTextAreaElement;
190+
191+
nameEl.value = '';
192+
emailEl.value = '';
193+
messageEl.value = '';
194+
195+
widget.dialog?.el?.querySelector('form')?.dispatchEvent(new Event('submit'));
196+
expect(sendFeedbackRequest).toHaveBeenCalledTimes(0);
197+
198+
// sendFeedbackRequest is async
199+
await flushPromises();
200+
expect(onSubmitSuccess).toHaveBeenCalledTimes(0);
201+
202+
nameEl.value = '';
203+
emailEl.value = '';
204+
messageEl.value = 'My feedback';
205+
206+
widget.dialog?.el?.querySelector('form')?.dispatchEvent(new Event('submit'));
207+
expect(sendFeedbackRequest).toHaveBeenCalledTimes(0);
208+
209+
// sendFeedbackRequest is async
210+
await flushPromises();
211+
expect(onSubmitSuccess).toHaveBeenCalledTimes(0);
212+
213+
nameEl.value = 'Jane Doe';
214+
emailEl.value = '[email protected]';
215+
messageEl.value = 'My feedback';
216+
217+
widget.dialog?.el?.querySelector('form')?.dispatchEvent(new Event('submit'));
218+
expect(sendFeedbackRequest).toHaveBeenCalledWith({
219+
feedback: {
220+
name: 'Jane Doe',
221+
222+
message: 'My feedback',
223+
url: 'http://localhost/',
224+
replay_id: undefined,
225+
source: 'widget',
226+
},
227+
});
228+
229+
// sendFeedbackRequest is async
230+
await flushPromises();
231+
expect(onSubmitSuccess).toHaveBeenCalledTimes(1);
232+
233+
expect(widget.dialog).toBeUndefined();
234+
expect(shadow.querySelector('.success-message')?.textContent).toBe(SUCCESS_MESSAGE_TEXT);
235+
236+
jest.runAllTimers();
237+
expect(shadow.querySelector('.success-message')).toBeNull();
238+
});
239+
174240
it('submits feedback with error on request', async () => {
175241
const onSubmitError = jest.fn(() => {});
176242
const { shadow, widget } = createShadowAndWidget({

0 commit comments

Comments
 (0)