From 9bab087878ea141b21535edc3c1668f5de7af090 Mon Sep 17 00:00:00 2001 From: Irene Ryu Date: Sun, 24 Mar 2024 14:04:22 +0900 Subject: [PATCH 1/5] feat: dismiss mobile keyboard after sending a message --- src/components/CustomChannelComponent.tsx | 4 + .../useAutoDismissMobileKyeboardHandler.ts | 75 +++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 src/hooks/useAutoDismissMobileKyeboardHandler.ts diff --git a/src/components/CustomChannelComponent.tsx b/src/components/CustomChannelComponent.tsx index fd429e996..2a911bd17 100644 --- a/src/components/CustomChannelComponent.tsx +++ b/src/components/CustomChannelComponent.tsx @@ -15,6 +15,7 @@ import CustomMessage from './CustomMessage'; import DynamicRepliesPanel from './DynamicRepliesPanel'; import StaticRepliesPanel from './StaticRepliesPanel'; import { useConstantState } from '../context/ConstantContext'; +import useAutoDismissMobileKyeboardHandler from '../hooks/useAutoDismissMobileKyeboardHandler'; import { useScrollOnStreaming } from '../hooks/useScrollOnStreaming'; import { hideChatBottomBanner, isIOSMobile } from '../utils'; import { @@ -126,6 +127,9 @@ export function CustomChannelComponent(props: CustomChannelComponentProps) { scrollToBottom, } = useGroupChannelContext(); const lastMessageRef = useRef(null); + + useAutoDismissMobileKyeboardHandler(); + const lastMessage = allMessages?.[allMessages?.length - 1] as | SendableMessage | undefined; diff --git a/src/hooks/useAutoDismissMobileKyeboardHandler.ts b/src/hooks/useAutoDismissMobileKyeboardHandler.ts new file mode 100644 index 000000000..fa28a4b8d --- /dev/null +++ b/src/hooks/useAutoDismissMobileKyeboardHandler.ts @@ -0,0 +1,75 @@ +import { useEffect, useRef } from 'react'; + +import { isIOSMobile } from '../utils'; + +const INPUT_ELEMENT_SELECTOR = '.sendbird-message-input'; +const SEND_BUTTON_SELECTOR = '.sendbird-message-input--send'; + +function useAutoDismissMobileKeyboardHandler(): void { + const observerRef = useRef(null); + + useEffect(() => { + const handleDismissKeyboard = (): void => { + setTimeout(() => { + const inputElement = document.querySelector(INPUT_ELEMENT_SELECTOR); + if ( + document.activeElement instanceof HTMLElement && + inputElement instanceof HTMLElement + ) { + document.activeElement.blur(); // blur the active element to dismiss the keyboard on mobile + inputElement.blur(); // ensure the input element is also blurred + } + }, 200); + }; + + const observerCallback = (mutations: MutationRecord[]): void => { + for (const mutation of mutations) { + if (mutation.type === 'childList') { + const addedNodes = Array.from(mutation.addedNodes) as HTMLElement[]; + // Why we're searching the button in this way? + // Because the send button is not rendered in the DOM tree until the user starts typing + const sendButton = addedNodes.find( + (node) => + node.nodeType === Node.ELEMENT_NODE && + node.matches(SEND_BUTTON_SELECTOR) + ); + + if (sendButton instanceof HTMLElement) { + sendButton.addEventListener('click', handleDismissKeyboard); + } + } + } + }; + + observerRef.current = new MutationObserver(observerCallback); + const config = { childList: true, subtree: true }; + + const inputElement = document.querySelector( + INPUT_ELEMENT_SELECTOR + ); + if (inputElement) { + observerRef.current.observe(inputElement, config); + inputElement.addEventListener('keydown', (event: KeyboardEvent) => { + if ( + event.key === 'Enter' && + // Pressing Enter key on Android keyboard does't trigger the sending message event but carriage return event is fired instead + isIOSMobile + ) { + handleDismissKeyboard(); + } + }); + } else { + console.warn('Input element not found for mutation observer'); + } + + return () => { + observerRef.current?.disconnect(); + if (inputElement) { + // Clean up event listener when the component is unmounted + inputElement.removeEventListener('keydown', handleDismissKeyboard); + } + }; + }, []); +} + +export default useAutoDismissMobileKeyboardHandler; From cd7bb593d5c987469905c9e62711199e3938b385 Mon Sep 17 00:00:00 2001 From: Irene Ryu Date: Mon, 25 Mar 2024 10:04:41 +0900 Subject: [PATCH 2/5] Drop useRef --- src/hooks/useAutoDismissMobileKyeboardHandler.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/hooks/useAutoDismissMobileKyeboardHandler.ts b/src/hooks/useAutoDismissMobileKyeboardHandler.ts index fa28a4b8d..21856d99d 100644 --- a/src/hooks/useAutoDismissMobileKyeboardHandler.ts +++ b/src/hooks/useAutoDismissMobileKyeboardHandler.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react'; +import { useEffect } from 'react'; import { isIOSMobile } from '../utils'; @@ -6,8 +6,6 @@ const INPUT_ELEMENT_SELECTOR = '.sendbird-message-input'; const SEND_BUTTON_SELECTOR = '.sendbird-message-input--send'; function useAutoDismissMobileKeyboardHandler(): void { - const observerRef = useRef(null); - useEffect(() => { const handleDismissKeyboard = (): void => { setTimeout(() => { @@ -41,14 +39,14 @@ function useAutoDismissMobileKeyboardHandler(): void { } }; - observerRef.current = new MutationObserver(observerCallback); + const observerRef = new MutationObserver(observerCallback); const config = { childList: true, subtree: true }; const inputElement = document.querySelector( INPUT_ELEMENT_SELECTOR ); if (inputElement) { - observerRef.current.observe(inputElement, config); + observerRef.observe(inputElement, config); inputElement.addEventListener('keydown', (event: KeyboardEvent) => { if ( event.key === 'Enter' && @@ -63,7 +61,7 @@ function useAutoDismissMobileKeyboardHandler(): void { } return () => { - observerRef.current?.disconnect(); + observerRef.disconnect(); if (inputElement) { // Clean up event listener when the component is unmounted inputElement.removeEventListener('keydown', handleDismissKeyboard); From 066185e1ee780eaae0a69e6c8f8af5f6182d3552 Mon Sep 17 00:00:00 2001 From: Irene Ryu Date: Mon, 25 Mar 2024 11:47:46 +0900 Subject: [PATCH 3/5] Cleanup click event on unmount --- .../useAutoDismissMobileKyeboardHandler.ts | 63 +++++++++---------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/src/hooks/useAutoDismissMobileKyeboardHandler.ts b/src/hooks/useAutoDismissMobileKyeboardHandler.ts index 21856d99d..e6966a876 100644 --- a/src/hooks/useAutoDismissMobileKyeboardHandler.ts +++ b/src/hooks/useAutoDismissMobileKyeboardHandler.ts @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { isIOSMobile } from '../utils'; @@ -6,37 +6,42 @@ const INPUT_ELEMENT_SELECTOR = '.sendbird-message-input'; const SEND_BUTTON_SELECTOR = '.sendbird-message-input--send'; function useAutoDismissMobileKeyboardHandler(): void { + const addedButtons = useRef([]); + useEffect(() => { const handleDismissKeyboard = (): void => { setTimeout(() => { - const inputElement = document.querySelector(INPUT_ELEMENT_SELECTOR); - if ( - document.activeElement instanceof HTMLElement && - inputElement instanceof HTMLElement - ) { - document.activeElement.blur(); // blur the active element to dismiss the keyboard on mobile - inputElement.blur(); // ensure the input element is also blurred + if (document.activeElement instanceof HTMLElement) { + // blur the active element(send button) to dismiss the keyboard on mobile + document.activeElement.blur(); } }, 200); }; + const handleKeyDown = (event: KeyboardEvent): void => { + if (event.key === 'Enter' && isIOSMobile) { + handleDismissKeyboard(); + } + }; + const observerCallback = (mutations: MutationRecord[]): void => { - for (const mutation of mutations) { + mutations.forEach((mutation) => { if (mutation.type === 'childList') { - const addedNodes = Array.from(mutation.addedNodes) as HTMLElement[]; - // Why we're searching the button in this way? - // Because the send button is not rendered in the DOM tree until the user starts typing - const sendButton = addedNodes.find( - (node) => + mutation.addedNodes.forEach((node) => { + if ( node.nodeType === Node.ELEMENT_NODE && - node.matches(SEND_BUTTON_SELECTOR) - ); - - if (sendButton instanceof HTMLElement) { - sendButton.addEventListener('click', handleDismissKeyboard); - } + (node as Element).matches(SEND_BUTTON_SELECTOR) + ) { + (node as HTMLElement).addEventListener( + 'click', + handleDismissKeyboard + ); + // Store added node for later removal + addedButtons.current.push(node as HTMLElement); + } + }); } - } + }); }; const observerRef = new MutationObserver(observerCallback); @@ -47,24 +52,18 @@ function useAutoDismissMobileKeyboardHandler(): void { ); if (inputElement) { observerRef.observe(inputElement, config); - inputElement.addEventListener('keydown', (event: KeyboardEvent) => { - if ( - event.key === 'Enter' && - // Pressing Enter key on Android keyboard does't trigger the sending message event but carriage return event is fired instead - isIOSMobile - ) { - handleDismissKeyboard(); - } - }); + inputElement.addEventListener('keydown', handleKeyDown); } else { console.warn('Input element not found for mutation observer'); } return () => { observerRef.disconnect(); + addedButtons.current.forEach((button) => + button.removeEventListener('click', handleDismissKeyboard) + ); if (inputElement) { - // Clean up event listener when the component is unmounted - inputElement.removeEventListener('keydown', handleDismissKeyboard); + inputElement.removeEventListener('keydown', handleKeyDown); } }; }, []); From bda037feb29c829a05b43ed1028a4c1fd988e828 Mon Sep 17 00:00:00 2001 From: Irene Ryu Date: Mon, 25 Mar 2024 13:40:01 +0900 Subject: [PATCH 4/5] Remove listner before adding --- src/hooks/useAutoDismissMobileKyeboardHandler.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/hooks/useAutoDismissMobileKyeboardHandler.ts b/src/hooks/useAutoDismissMobileKyeboardHandler.ts index e6966a876..aa3cd0ad5 100644 --- a/src/hooks/useAutoDismissMobileKyeboardHandler.ts +++ b/src/hooks/useAutoDismissMobileKyeboardHandler.ts @@ -32,6 +32,10 @@ function useAutoDismissMobileKeyboardHandler(): void { node.nodeType === Node.ELEMENT_NODE && (node as Element).matches(SEND_BUTTON_SELECTOR) ) { + (node as HTMLElement).removeEventListener( + 'click', + handleDismissKeyboard + ); (node as HTMLElement).addEventListener( 'click', handleDismissKeyboard @@ -52,6 +56,7 @@ function useAutoDismissMobileKeyboardHandler(): void { ); if (inputElement) { observerRef.observe(inputElement, config); + inputElement.removeEventListener('keydown', handleKeyDown); inputElement.addEventListener('keydown', handleKeyDown); } else { console.warn('Input element not found for mutation observer'); From 25e3e89e5e15ebdbbc7d1b4b40272355522bb948 Mon Sep 17 00:00:00 2001 From: Irene Ryu Date: Mon, 25 Mar 2024 14:40:07 +0900 Subject: [PATCH 5/5] Add comment about https://github.com/sendbird/chat-ai-widget/pull/129\#pullrequestreview-1956874526 --- src/hooks/useAutoDismissMobileKyeboardHandler.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/hooks/useAutoDismissMobileKyeboardHandler.ts b/src/hooks/useAutoDismissMobileKyeboardHandler.ts index aa3cd0ad5..5c8669c1b 100644 --- a/src/hooks/useAutoDismissMobileKyeboardHandler.ts +++ b/src/hooks/useAutoDismissMobileKyeboardHandler.ts @@ -19,7 +19,13 @@ function useAutoDismissMobileKeyboardHandler(): void { }; const handleKeyDown = (event: KeyboardEvent): void => { - if (event.key === 'Enter' && isIOSMobile) { + if ( + event.key === 'Enter' && + // TODO: Pressing Enter key on Android keyboard does't trigger the sending message event + // but carriage return event is fired instead which is a different behavior from UIKit React. + // Need to find a way to handle this case. + isIOSMobile + ) { handleDismissKeyboard(); } };