Skip to content

Commit 6a59414

Browse files
committed
Update feedback flow and add email validation
1 parent 53954c5 commit 6a59414

File tree

7 files changed

+184
-44
lines changed

7 files changed

+184
-44
lines changed

CHANGELOG.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,24 @@
55
### Added
66
- HTML rendering support for clickable ticket links in success messages
77
- ProForma form field mapping for JSM integration
8-
- Added some accessibility enhancements for ketyboard navigation and screenreaders
8+
- Comprehensive email validation with user-friendly error messages for all forms
9+
- Keyboard navigation hints for checkbox selections
10+
- Email validation using InputValidator plugin with toast notifications and input highlighting
911

1012
### Changed
1113
- Update to react-chatbotify v2.1.0
1214
- Cleaned up development flow, comments, and debug logging
15+
- **Feedback Flow Reordering**: File upload question now appears before role question for better UX
16+
- **Community Interest Targeting**: Community involvement question now only shown to users who provide contact information
17+
- **Anonymous Feedback Improvements**: Community interest field hidden from summary for anonymous submissions
18+
- **Focus Management**: Fixed keyboard focus jumping to previous questions; now properly targets current question only
19+
20+
### Fixed
21+
- Resolved form state persistence issues where previous session data would interfere with new submissions
22+
- Fixed multiple checkbox selection display in feedback summary (was showing only one selection)
23+
- Corrected timing issues with form context updates using chatState.userInput pattern
24+
- Fixed anonymous feedback showing previous contact information in summary
25+
- Improved keyboard accessibility for checkboxes with proper focus management
1326

1427
## [2.0.0-rc.1] - 2025-05-28
1528

src/hooks/useKeyboardNavigation.js

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -93,36 +93,30 @@ const useKeyboardNavigation = () => {
9393
if (!chatWindow) return;
9494

9595
// Special handling for checkboxes - but only if they're in the most recent message
96-
const checkboxContainer = chatWindow.querySelector('.rcb-checkbox-container');
97-
if (checkboxContainer && checkboxContainer.offsetParent !== null) {
98-
// Check if this checkbox container is in the last message
99-
const allMessages = Array.from(chatWindow.querySelectorAll('.rcb-message-container, .rcb-bot-message-container, .rcb-user-message-container'))
100-
.filter(el => el.offsetParent !== null);
96+
const allMessages = Array.from(chatWindow.querySelectorAll('.rcb-message-container, .rcb-bot-message-container, .rcb-user-message-container'))
97+
.filter(el => el.offsetParent !== null);
98+
99+
if (allMessages.length > 0) {
100+
const lastMessage = allMessages[allMessages.length - 1];
101+
const checkboxContainer = lastMessage.querySelector('.rcb-checkbox-container');
101102

102-
if (allMessages.length > 0) {
103-
const lastMessage = allMessages[allMessages.length - 1];
103+
if (checkboxContainer && checkboxContainer.offsetParent !== null) {
104+
const checkboxes = Array.from(checkboxContainer.querySelectorAll('.rcb-checkbox-row-container'));
105+
const nextButton = checkboxContainer.querySelector('.rcb-checkbox-next-button');
104106

105-
// Only handle checkboxes if they're in the last message
106-
if (lastMessage.contains(checkboxContainer)) {
107-
const checkboxes = Array.from(checkboxContainer.querySelectorAll('.rcb-checkbox-row-container'));
108-
const nextButton = checkboxContainer.querySelector('.rcb-checkbox-next-button');
109-
110-
if (checkboxes.length > 0) {
111-
// Include next button in navigable elements
112-
const allElements = [...checkboxes];
113-
if (nextButton) {
114-
allElements.push(nextButton);
115-
}
116-
handleCheckboxNavigation(event, allElements);
117-
return;
107+
if (checkboxes.length > 0) {
108+
// Include next button in navigable elements
109+
const allElements = [...checkboxes];
110+
if (nextButton) {
111+
allElements.push(nextButton);
118112
}
113+
handleCheckboxNavigation(event, allElements);
114+
return;
119115
}
120116
}
121117
}
122118

123119
// Check if chat window is visible and if there are options available - only in the most recent message
124-
const allMessages = Array.from(chatWindow.querySelectorAll('.rcb-message-container, .rcb-bot-message-container, .rcb-user-message-container'))
125-
.filter(el => el.offsetParent !== null);
126120

127121
let options = [];
128122
if (allMessages.length > 0) {
@@ -313,9 +307,13 @@ const useKeyboardNavigation = () => {
313307
options = Array.from(lastContainer.querySelectorAll('.rcb-options'));
314308
}
315309

316-
// If no regular options, look for checkboxes and set them up - but only in the last message
310+
// If no regular options, look for checkboxes and set them up
317311
if (options.length === 0) {
318-
const checkboxContainer = lastMessage.querySelector('.rcb-checkbox-container');
312+
// First try to find checkbox container in last message, but fallback to chatWindow if not found
313+
let checkboxContainer = lastMessage.querySelector('.rcb-checkbox-container');
314+
if (!checkboxContainer && chatWindow) {
315+
checkboxContainer = chatWindow.querySelector('.rcb-checkbox-container');
316+
}
319317

320318
if (checkboxContainer && checkboxContainer.offsetParent !== null) {
321319
const checkboxElements = Array.from(checkboxContainer.querySelectorAll('.rcb-checkbox-row-container'))
@@ -339,6 +337,20 @@ const useKeyboardNavigation = () => {
339337
}
340338
});
341339

340+
// Add keyboard navigation hint for checkboxes if there are multiple elements
341+
if (checkboxElements.length > 1) {
342+
// Remove any existing hints first
343+
const existingHints = checkboxContainer.querySelectorAll('.keyboard-nav-hint');
344+
existingHints.forEach(hint => hint.remove());
345+
346+
// Add new hint
347+
const hintElement = document.createElement('div');
348+
hintElement.className = 'keyboard-nav-hint';
349+
hintElement.textContent = 'Use arrow keys ↕ to navigate, Enter to select/deselect, or click any option';
350+
hintElement.style.cssText = 'font-size: 12px !important; color: #666 !important; margin-bottom: 8px !important; font-style: italic !important; display: block !important;';
351+
checkboxContainer.insertBefore(hintElement, checkboxContainer.firstChild);
352+
}
353+
342354
// Focus first element
343355
setTimeout(() => {
344356
if (allElements[0] && allElements[0].offsetParent !== null) {

src/utils/flows/feedback-flow.js

Lines changed: 76 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react';
22
import FileUploadComponent from '../../components/FileUploadComponent';
33
import { getCurrentFeedbackForm } from '../flow-context-utils';
44
import { createOptionalFieldValidator, processOptionalInput } from '../optional-field-utils';
5+
import { validateEmail, isValidEmail } from '../validation-utils';
56

67
/**
78
* Creates the feedback conversation flow
@@ -31,13 +32,32 @@ export const createFeedbackFlow = ({
3132
return {
3233
feedback: {
3334
message: "We appreciate your feedback about ACCESS.\n\nFeedback submitted through this form will only be shared within the ACCESS teams. We encourage the sharing of contact information for potential follow-up, but understand that anonymity may be preferred when providing feedback on sensitive issues.\n\nPlease provide your detailed feedback:",
34-
function: (chatState) => setFeedbackForm({...(feedbackForm || {}), feedback: chatState.userInput}),
35+
function: (chatState) => {
36+
// Clear any previous feedback form state to start fresh
37+
setFeedbackForm({
38+
feedback: chatState.userInput,
39+
// Explicitly clear all other fields to prevent state persistence
40+
recommendations: undefined,
41+
primaryRole: undefined,
42+
needsCustomRole: false,
43+
customRole: undefined,
44+
communityInterest: undefined,
45+
upload: undefined,
46+
uploadConfirmed: false,
47+
uploadedFiles: undefined,
48+
wantsContact: undefined,
49+
useCustomContactInfo: false,
50+
customName: undefined,
51+
customEmail: undefined,
52+
customAccessId: undefined
53+
});
54+
},
3555
path: "feedback_please_tell_us_more"
3656
},
3757
feedback_please_tell_us_more: {
3858
message: "Please detail any recommendations for improvement:",
3959
function: (chatState) => setFeedbackForm({...(feedbackForm || {}), recommendations: chatState.userInput}),
40-
path: "feedback_primary_role"
60+
path: "feedback_upload"
4161
},
4262
feedback_primary_role: {
4363
message: "What is your primary role pertaining to ACCESS?",
@@ -60,7 +80,7 @@ export const createFeedbackFlow = ({
6080
}
6181
},
6282
path: (chatState) => {
63-
return chatState.userInput === "Other" ? "feedback_custom_role" : "feedback_community_interest";
83+
return chatState.userInput === "Other" ? "feedback_custom_role" : "feedback_contact_choice";
6484
}
6585
},
6686
feedback_custom_role: {
@@ -69,7 +89,7 @@ export const createFeedbackFlow = ({
6989
const currentForm = getCurrentFeedbackForm();
7090
setFeedbackForm({...currentForm, customRole: chatState.userInput});
7191
},
72-
path: "feedback_community_interest"
92+
path: "feedback_contact_choice"
7393
},
7494
feedback_community_interest: {
7595
message: "Are you interested, or potentially interested, in serving the ACCESS community and helping us improve in any of these roles? (This is not a commitment. We'll reach out with more information.) Select all that apply:",
@@ -91,7 +111,7 @@ export const createFeedbackFlow = ({
91111
const currentForm = getCurrentFeedbackForm();
92112
setFeedbackForm({...currentForm, communityInterest: chatState.userInput});
93113
},
94-
path: "feedback_upload"
114+
path: "feedback_summary"
95115
},
96116
feedback_upload: {
97117
message: "Would you like to upload a screenshot or file to help us better understand your feedback?",
@@ -102,7 +122,7 @@ export const createFeedbackFlow = ({
102122
if (chatState.userInput === "Yes") {
103123
return "feedback_upload_yes";
104124
} else {
105-
return "feedback_contact_choice";
125+
return "feedback_primary_role";
106126
}
107127
}
108128
},
@@ -112,7 +132,7 @@ export const createFeedbackFlow = ({
112132
options: ["Continue"],
113133
chatDisabled: true,
114134
function: (chatState) => setFeedbackForm({...(feedbackForm || {}), uploadConfirmed: true}),
115-
path: "feedback_contact_choice"
135+
path: "feedback_primary_role"
116136
},
117137
feedback_contact_choice: {
118138
message: "Would you like to provide your contact information for follow up?",
@@ -134,8 +154,14 @@ export const createFeedbackFlow = ({
134154
return "feedback_contact_confirm";
135155
}
136156
// Otherwise collect missing info - set flag to use custom info
157+
// Use the updatedForm from the function above to preserve wantsContact
137158
const currentForm = getCurrentFeedbackForm();
138-
setFeedbackForm({...currentForm, useCustomContactInfo: true});
159+
const wantsContact = chatState.userInput === "Include my contact info";
160+
setFeedbackForm({
161+
...currentForm,
162+
useCustomContactInfo: true,
163+
wantsContact: wantsContact ? "Yes" : "No"
164+
});
139165
if (!userInfo.name) return "feedback_name";
140166
if (!userInfo.email) return "feedback_email";
141167
if (!userInfo.username) return "feedback_accessid";
@@ -163,7 +189,7 @@ export const createFeedbackFlow = ({
163189
},
164190
path: (chatState) => {
165191
if (chatState.userInput === "Use this information") {
166-
return "feedback_summary";
192+
return "feedback_community_interest";
167193
} else {
168194
return "feedback_name";
169195
}
@@ -179,6 +205,7 @@ export const createFeedbackFlow = ({
179205
},
180206
feedback_email: {
181207
message: "What is your email address?",
208+
validateTextInput: (email) => validateEmail(email),
182209
function: (chatState) => {
183210
const currentForm = getCurrentFeedbackForm();
184211
setFeedbackForm({...currentForm, customEmail: chatState.userInput});
@@ -192,7 +219,7 @@ export const createFeedbackFlow = ({
192219
const currentForm = getCurrentFeedbackForm();
193220
setFeedbackForm({...currentForm, customAccessId: processOptionalInput(chatState.userInput)});
194221
},
195-
path: "feedback_summary"
222+
path: "feedback_community_interest"
196223
},
197224
feedback_summary: {
198225
message: (chatState) => {
@@ -229,7 +256,7 @@ export const createFeedbackFlow = ({
229256
`ACCESS ID: ${finalAccessId}\n` +
230257
'';
231258
} else {
232-
contactInfo = `Contact Information: Not provided\n`;
259+
contactInfo = `Contact Information: Anonymous submission\n`;
233260
}
234261

235262
// Format primary role
@@ -238,20 +265,30 @@ export const createFeedbackFlow = ({
238265
primaryRole = `Other: ${currentForm.customRole}`;
239266
}
240267

241-
// Format community interest
242-
let communityInterest = 'Not provided';
243-
if (currentForm.communityInterest && Array.isArray(currentForm.communityInterest)) {
244-
communityInterest = currentForm.communityInterest.join(', ');
245-
} else if (currentForm.communityInterest) {
246-
communityInterest = currentForm.communityInterest;
268+
// Format community interest - handle timing issue like ACCESS ID (only for non-anonymous)
269+
let communityInterestLine = '';
270+
if (currentForm.wantsContact === "Yes") {
271+
let communityInterest = 'Not provided';
272+
273+
// If coming directly from community interest step, use chatState.userInput
274+
if (chatState.prevPath === 'feedback_community_interest') {
275+
communityInterest = chatState.userInput || 'Not provided';
276+
} else if (currentForm.communityInterest && Array.isArray(currentForm.communityInterest)) {
277+
communityInterest = currentForm.communityInterest.join(', ');
278+
} else if (currentForm.communityInterest) {
279+
communityInterest = currentForm.communityInterest;
280+
}
281+
282+
communityInterestLine = `Community Interest: ${communityInterest}\n`;
247283
}
248284

249285
return `Thank you for providing your feedback. Here's a summary:\n\n` +
250286
contactInfo +
251287
`Feedback: ${currentForm.feedback || 'Not provided'}\n` +
252288
`Recommendations: ${currentForm.recommendations || 'Not provided'}\n` +
253289
`Primary Role: ${primaryRole}\n` +
254-
`Community Interest: ${communityInterest}${fileInfo}\n\n` +
290+
communityInterestLine +
291+
`${fileInfo}\n\n` +
255292
`Would you like to submit this feedback?`;
256293
},
257294
options: ["Submit Feedback", "Back to Main Menu"],
@@ -265,7 +302,27 @@ export const createFeedbackFlow = ({
265302
}
266303
},
267304
feedback_success: {
268-
message: "Thank you for your feedback! If you provided your contact information, we will follow up with you shortly.",
305+
message: () => {
306+
const currentForm = getCurrentFeedbackForm();
307+
const baseMessage = "Thank you for your feedback!";
308+
309+
// Determine if they provided a valid email (using same logic as summary)
310+
let finalEmail;
311+
if (currentForm.wantsContact === "No") {
312+
finalEmail = null;
313+
} else if (currentForm.useCustomContactInfo) {
314+
finalEmail = currentForm.customEmail;
315+
} else {
316+
finalEmail = userInfo.email;
317+
}
318+
319+
// Only add follow-up message if they provided a valid email
320+
if (finalEmail && finalEmail.trim() && isValidEmail(finalEmail)) {
321+
return `${baseMessage} We will follow up with you shortly.`;
322+
}
323+
324+
return baseMessage;
325+
},
269326
options: ["Back to Main Menu"],
270327
chatDisabled: true,
271328
path: "start"

src/utils/flows/tickets/access-login-flow.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from './ticket-flow-utils';
77
import { getCurrentTicketForm, getCurrentFormWithUserInfo } from '../../flow-context-utils';
88
import { createOptionalFieldValidator, processOptionalInput } from '../../optional-field-utils';
9+
import { validateEmail } from '../../validation-utils';
910

1011
/**
1112
* Creates the ACCESS login help ticket flow
@@ -110,6 +111,7 @@ export const createAccessLoginFlow = ({ ticketForm = {}, setTicketForm = () => {
110111
},
111112
access_login_email: {
112113
message: "What is your email?",
114+
validateTextInput: (email) => validateEmail(email),
113115
function: (chatState) => {
114116
const currentForm = getCurrentTicketForm();
115117
setTicketForm({...currentForm, email: chatState.userInput});

src/utils/flows/tickets/affiliated-login-flow.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from './ticket-flow-utils';
77
import { getCurrentTicketForm, getCurrentFormWithUserInfo } from '../../flow-context-utils';
88
import { createOptionalFieldValidator, processOptionalInput } from '../../optional-field-utils';
9+
import { validateEmail } from '../../validation-utils';
910

1011
/**
1112
* Creates the affiliated/resource provider login help ticket flow
@@ -129,6 +130,7 @@ export const createAffiliatedLoginFlow = ({ ticketForm = {}, setTicketForm = ()
129130
},
130131
affiliated_login_email: {
131132
message: "What is your email?",
133+
validateTextInput: (email) => validateEmail(email),
132134
function: (chatState) => {
133135
const currentForm = getCurrentTicketForm();
134136
setTicketForm({...currentForm, email: chatState.userInput});

src/utils/flows/tickets/general-help-flow.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from './ticket-flow-utils';
77
import { getCurrentTicketForm, getCurrentFormWithUserInfo } from '../../flow-context-utils';
88
import { createOptionalFieldValidator, processOptionalInput } from '../../optional-field-utils';
9+
import { validateEmail } from '../../validation-utils';
910

1011
/**
1112
* Creates the enhanced general help ticket flow with ProForma field support
@@ -505,6 +506,7 @@ export const createGeneralHelpFlow = ({ ticketForm = {}, setTicketForm = () => {
505506
},
506507
general_help_email: {
507508
message: "What is your email address?",
509+
validateTextInput: (email) => validateEmail(email),
508510
function: (chatState) => {
509511
const currentForm = getCurrentTicketForm();
510512
setTicketForm({...currentForm, email: chatState.userInput});

0 commit comments

Comments
 (0)