Skip to content

Commit 1680042

Browse files
committed
refactor: enable keyboard navigation for multiple bot instances
Remove containerRef dependency and auto-detect target bot containers to support multiple QA bots on the same page.
1 parent 374eb60 commit 1680042

File tree

2 files changed

+40
-58
lines changed

2 files changed

+40
-58
lines changed

src/components/QABot.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ const QABotInternal = React.forwardRef((props, botRef) => {
139139
useUpdateHeader(isBotLoggedIn, containerRef);
140140
useRingEffect(ringEffect, containerRef);
141141
useFocusableSendButton();
142-
useKeyboardNavigation(containerRef);
142+
useKeyboardNavigation();
143143

144144
// Handle tooltip session tracking
145145
useEffect(() => {

src/hooks/useKeyboardNavigation.js

Lines changed: 39 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useEffect } from 'react';
44
* Custom hook for keyboard navigation in chatbot options
55
* Provides arrow key navigation, Enter/Space selection, and accessibility features
66
*/
7-
const useKeyboardNavigation = (containerRef = null) => {
7+
const useKeyboardNavigation = () => {
88
useEffect(() => {
99
// Dedicated checkbox navigation handler
1010
const handleCheckboxNavigation = (event, elements) => {
@@ -277,22 +277,11 @@ const useKeyboardNavigation = (containerRef = null) => {
277277
};
278278

279279
// Auto-focus first option when new options appear
280-
const handleNewOptions = () => {
281-
// Use the provided container ref, or fall back to document search as last resort
282-
let chatWindow = null;
280+
const handleNewOptions = (targetContainer = null) => {
281+
let chatWindow = targetContainer;
283282

284-
if (containerRef && containerRef.current) {
285-
// First try to find chat window within the specific bot container
286-
chatWindow = containerRef.current.querySelector('.rcb-chat-window');
287-
if (!chatWindow) {
288-
chatWindow = containerRef.current.querySelector('[class*="rcb-chat"]');
289-
}
290-
if (!chatWindow) {
291-
// If this container IS the qa-bot, use it directly
292-
chatWindow = containerRef.current.classList.contains('qa-bot') ? containerRef.current : containerRef.current.querySelector('.qa-bot');
293-
}
294-
} else {
295-
// Fallback to global search (for backwards compatibility)
283+
// If no specific container provided, try to find one
284+
if (!chatWindow) {
296285
chatWindow = document.querySelector('.rcb-chat-window');
297286
if (!chatWindow) {
298287
chatWindow = document.querySelector('[class*="rcb-chat"]');
@@ -456,40 +445,33 @@ const useKeyboardNavigation = (containerRef = null) => {
456445

457446

458447
if (hasNewOptions || hasNewCheckboxes) {
459-
// Clear tabindex from existing elements, but scope to this bot's container
460-
const scopeElement = (containerRef && containerRef.current) ? containerRef.current : document;
448+
// Find which bot container this mutation occurred in
449+
let botContainer = mutation.target;
450+
while (botContainer && !botContainer.classList?.contains('qa-bot') && !botContainer.classList?.contains('rcb-chat-window')) {
451+
botContainer = botContainer.parentElement;
452+
}
453+
454+
// If we found a bot container, scope operations to it; otherwise fallback to global
455+
const scopeElement = botContainer || document;
456+
457+
// Clear tabindex from existing elements within this bot
461458
scopeElement.querySelectorAll('.rcb-options[tabindex], .rcb-checkbox-row-container[tabindex], .rcb-checkbox-next-button[tabindex]').forEach(el => {
462459
el.setAttribute('tabindex', '-1');
463460
el.classList.remove('keyboard-focused');
464461
});
465462

466-
setTimeout(handleNewOptions, 100);
463+
// Handle new options for this specific bot
464+
setTimeout(() => handleNewOptions(botContainer), 100);
467465
}
468466
}
469467
});
470468
});
471469

472-
// Start observing - scope to this bot's container
473-
let observeTarget = null;
474-
475-
if (containerRef && containerRef.current) {
476-
// First try to find chat window within the specific bot container
477-
observeTarget = containerRef.current.querySelector('.rcb-chat-window');
478-
if (!observeTarget) {
479-
// If no chat window found, observe the container itself
480-
observeTarget = containerRef.current;
481-
}
482-
} else {
483-
// Fallback to global search (for backwards compatibility)
484-
observeTarget = document.querySelector('.rcb-chat-window');
485-
}
486-
487-
if (observeTarget) {
488-
observer.observe(observeTarget, {
489-
childList: true,
490-
subtree: true
491-
});
492-
}
470+
// Start observing the document body for changes in any bot
471+
observer.observe(document.body, {
472+
childList: true,
473+
subtree: true
474+
});
493475

494476
// Add keyboard event listener to document only
495477
document.addEventListener('keydown', handleKeyboardNavigation);
@@ -500,29 +482,29 @@ const useKeyboardNavigation = (containerRef = null) => {
500482

501483
// Periodic check as backup to catch any missed options and checkboxes
502484
const periodicCheck = setInterval(() => {
503-
// Scope the periodic check to this bot's container
504-
const scopeElement = (containerRef && containerRef.current) ? containerRef.current : document;
485+
// Check each bot container individually
486+
document.querySelectorAll('.qa-bot').forEach(botContainer => {
487+
const hasVisibleOptions = botContainer.querySelectorAll('.rcb-options-container .rcb-options').length > 0;
488+
const hasVisibleCheckboxes = botContainer.querySelectorAll('.rcb-checkbox-row-container').length > 0;
505489

506-
const hasVisibleOptions = scopeElement.querySelectorAll('.rcb-options-container .rcb-options').length > 0;
507-
const hasVisibleCheckboxes = scopeElement.querySelectorAll('.rcb-checkbox-row-container').length > 0;
490+
if (hasVisibleOptions) {
491+
const lastProcessedOptions = botContainer.querySelectorAll('.rcb-options[tabindex]').length;
492+
const currentOptions = botContainer.querySelectorAll('.rcb-options').length;
508493

509-
if (hasVisibleOptions) {
510-
const lastProcessedOptions = scopeElement.querySelectorAll('.rcb-options[tabindex]').length;
511-
const currentOptions = scopeElement.querySelectorAll('.rcb-options').length;
512-
513-
if (currentOptions > lastProcessedOptions) {
514-
handleNewOptions();
494+
if (currentOptions > lastProcessedOptions) {
495+
handleNewOptions(botContainer);
496+
}
515497
}
516-
}
517498

518-
if (hasVisibleCheckboxes) {
519-
const lastProcessedCheckboxes = scopeElement.querySelectorAll('.rcb-checkbox-row-container[tabindex]').length;
520-
const currentCheckboxes = scopeElement.querySelectorAll('.rcb-checkbox-row-container').length;
499+
if (hasVisibleCheckboxes) {
500+
const lastProcessedCheckboxes = botContainer.querySelectorAll('.rcb-checkbox-row-container[tabindex]').length;
501+
const currentCheckboxes = botContainer.querySelectorAll('.rcb-checkbox-row-container').length;
521502

522-
if (currentCheckboxes > lastProcessedCheckboxes) {
523-
handleNewOptions();
503+
if (currentCheckboxes > lastProcessedCheckboxes) {
504+
handleNewOptions(botContainer);
505+
}
524506
}
525-
}
507+
});
526508
}, 1000);
527509

528510
// Cleanup

0 commit comments

Comments
 (0)