diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c134d8661..33e9a69078 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -391,6 +391,7 @@ Breaking changes in this release: - Downgraded graph upsert conflict checks, by [@compulim](https://github.com/compulim) in PR [#5674](https://github.com/microsoft/BotFramework-WebChat/pull/5674) - Fixed virtual keyboard should show up on tap after being suppressed, in iOS 26.2, by [@compulim](https://github.com/compulim) in PR [#5678](https://github.com/microsoft/BotFramework-WebChat/pull/5678) - Fixed compatibility with `create-react-app` by adding file extension to `core-js` imports, by [@compulim](https://github.com/compulim) in PR [#5680](https://github.com/microsoft/BotFramework-WebChat/pull/5680) +- Fixed virtual keyboard should be collapsed after being suppressed, in iOS 26.3, by [@compulim](https://github.com/compulim) in PR [#5757](https://github.com/microsoft/BotFramework-WebChat/pull/5757) ## [4.18.0] - 2024-07-10 diff --git a/__tests__/html2/fluentTheme/connectivityStatus.html b/__tests__/html2/fluentTheme/connectivityStatus.html index 9c9c63fa85..04ae045673 100644 --- a/__tests__/html2/fluentTheme/connectivityStatus.html +++ b/__tests__/html2/fluentTheme/connectivityStatus.html @@ -21,8 +21,9 @@ } = window; // Imports in UMD fashion. const { directLine, store } = testHelpers.createDirectLineEmulator({ autoConnect: false }); + const styleOptions = { spinnerAnimationBackgroundImage: 'url(/assets/staticspinner.png)' }; - const App = () => ; + const App = () => ; render( diff --git a/__tests__/html2/fluentTheme/connectivityStatus.html.snap-1.png b/__tests__/html2/fluentTheme/connectivityStatus.html.snap-1.png index 60c7aa13bd..30487bf3c2 100644 Binary files a/__tests__/html2/fluentTheme/connectivityStatus.html.snap-1.png and b/__tests__/html2/fluentTheme/connectivityStatus.html.snap-1.png differ diff --git a/__tests__/html2/hooks/useFocus.sendBox.pure.html b/__tests__/html2/hooks/useFocus.sendBox.pure.html index cae5d1c840..5fa87851c2 100644 --- a/__tests__/html2/hooks/useFocus.sendBox.pure.html +++ b/__tests__/html2/hooks/useFocus.sendBox.pure.html @@ -39,13 +39,13 @@ await renderHook(); - expect(document.activeElement).not.toEqual(pageElements.sendBoxTextBox()); + expect(document.activeElement === pageElements.sendBoxTextBox()).toBe(false); const focus = await renderHook(() => useFocus()); - focus('sendBox'); + await focus('sendBox'); - expect(document.activeElement).toEqual(pageElements.sendBoxTextBox()); + expect(document.activeElement === pageElements.sendBoxTextBox()).toBe(true); }); diff --git a/__tests__/html2/sendBox/hideKeyboard.pauseBetweenSetAttributeAndFocus.html b/__tests__/html2/sendBox/hideKeyboard.pauseBetweenSetAttributeAndFocus.html new file mode 100644 index 0000000000..26951b4cc4 --- /dev/null +++ b/__tests__/html2/sendBox/hideKeyboard.pauseBetweenSetAttributeAndFocus.html @@ -0,0 +1,75 @@ + + + + + + + + + + +
+ + + diff --git a/__tests__/html2/transcript/navigation/useObserveTranscriptFocus.html b/__tests__/html2/transcript/navigation/useObserveTranscriptFocus.html index d51e6696a4..a88efd255a 100644 --- a/__tests__/html2/transcript/navigation/useObserveTranscriptFocus.html +++ b/__tests__/html2/transcript/navigation/useObserveTranscriptFocus.html @@ -1,4 +1,4 @@ - + @@ -75,7 +75,7 @@ // THEN: It should send a "transcriptfocus" event with the third-last activity, which is a card activity (#29). expect(transcriptFocusActivityIDHistory).toEqual(['31', '30', '29']); - // WHEN: Pressing ENTER key while focusingo on the card activity (#29). + // WHEN: Pressing ENTER key while focusing on the card activity (#29). await host.sendKeys('ENTER'); // THEN: It should not send another event because the transcript did not gain any new focus. diff --git a/packages/component/src/SendBox/TextBox.tsx b/packages/component/src/SendBox/TextBox.tsx index a9918ad289..0198cbfe01 100644 --- a/packages/component/src/SendBox/TextBox.tsx +++ b/packages/component/src/SendBox/TextBox.tsx @@ -1,11 +1,12 @@ import { hooks } from 'botframework-webchat-api'; +import { usePonyfill } from 'botframework-webchat-api/hook'; import classNames from 'classnames'; import React, { useCallback, useMemo, useRef } from 'react'; import AccessibleInputText from '../Utils/AccessibleInputText'; import navigableEvent from '../Utils/TypeFocusSink/navigableEvent'; -import { useRegisterFocusSendBox, type SendBoxFocusOptions } from '../hooks/sendBoxFocus'; import { useStyleToEmotionObject } from '../hooks/internal/styleToEmotionObject'; +import { useRegisterFocusSendBox, type SendBoxFocusOptions } from '../hooks/sendBoxFocus'; import useScrollDown from '../hooks/useScrollDown'; import useScrollUp from '../hooks/useScrollUp'; import useStyleSet from '../hooks/useStyleSet'; @@ -164,17 +165,47 @@ const TextBox = ({ className = '' }: Readonly<{ className?: string | undefined } [scrollDown, scrollUp] ); + const [{ requestAnimationFrame, requestIdleCallback }] = usePonyfill(); + const requestIdleCallbackWithPonyfill = useMemo( + () => requestIdleCallback ?? ((callback: () => void) => requestAnimationFrame(callback)), + [requestAnimationFrame, requestIdleCallback] + ); + const focusCallback = useCallback( - ({ noKeyboard }: SendBoxFocusOptions) => { - const { current } = inputElementRef; - - // Setting `inputMode` to `none` temporarily to suppress soft keyboard in iOS. - // We will revert the change once the end-user tap on the send box. - // This code path is only triggered when the user press "send" button to send the message, instead of pressing ENTER key. - noKeyboard && current?.setAttribute('inputmode', 'none'); - current?.focus(); + ({ noKeyboard, waitUntil }: SendBoxFocusOptions) => { + waitUntil( + (async () => { + const { current } = inputElementRef; + + if (current) { + // Setting `inputMode` to `none` temporarily to suppress soft keyboard in iOS. + // We will revert the change once the end-user tap on the send box. + // This code path is only triggered when the user press "send" button to send the message, instead of pressing ENTER key. + if (noKeyboard) { + if (current.getAttribute('inputmode') !== 'none') { + // Collapse the virtual keybaord if it was expanded. + current.setAttribute('inputmode', 'none'); + + // iOS 26.3 quirks: `HTMLElement.focus()` does not pickup `inputmode="none"` changes immediately. + // We need to wait for next frame before calling `focus()`. + // This is a regression from iOS 26.2. + await new Promise(resolve => requestIdleCallbackWithPonyfill(resolve)); + } + } else if (current.hasAttribute('inputmode')) { + // Expanding the virtual keyboard if it was collapsed. + // However, we are not pausing here to workaround iOS 26.3 quirks. + // If we pause here, it will not able to handle this scenario: focus on an activity on the transcript, press A, the letter A should be inputted into the send box. + // In other words, if we pause here, the event will be send to the activity/transcript, instead of the newly focused send box. + // This is related to BasicTranscript.handleTranscriptKeyDownCapture(). + current.removeAttribute('inputmode'); + } + + current?.focus(); + } + })() + ); }, - [inputElementRef] + [inputElementRef, requestIdleCallbackWithPonyfill] ); useRegisterFocusSendBox(focusCallback);