|
1 | 1 | import { hooks } from 'botframework-webchat-api'; |
| 2 | +import { usePonyfill } from 'botframework-webchat-api/hook'; |
2 | 3 | import classNames from 'classnames'; |
3 | 4 | import React, { useCallback, useMemo, useRef } from 'react'; |
4 | 5 |
|
5 | 6 | import AccessibleInputText from '../Utils/AccessibleInputText'; |
6 | 7 | import navigableEvent from '../Utils/TypeFocusSink/navigableEvent'; |
7 | | -import { useRegisterFocusSendBox, type SendBoxFocusOptions } from '../hooks/sendBoxFocus'; |
8 | 8 | import { useStyleToEmotionObject } from '../hooks/internal/styleToEmotionObject'; |
| 9 | +import { useRegisterFocusSendBox, type SendBoxFocusOptions } from '../hooks/sendBoxFocus'; |
9 | 10 | import useScrollDown from '../hooks/useScrollDown'; |
10 | 11 | import useScrollUp from '../hooks/useScrollUp'; |
11 | 12 | import useStyleSet from '../hooks/useStyleSet'; |
@@ -164,17 +165,47 @@ const TextBox = ({ className = '' }: Readonly<{ className?: string | undefined } |
164 | 165 | [scrollDown, scrollUp] |
165 | 166 | ); |
166 | 167 |
|
| 168 | + const [{ requestAnimationFrame, requestIdleCallback }] = usePonyfill(); |
| 169 | + const requestIdleCallbackWithPonyfill = useMemo( |
| 170 | + () => requestIdleCallback ?? ((callback: () => void) => requestAnimationFrame(callback)), |
| 171 | + [requestAnimationFrame, requestIdleCallback] |
| 172 | + ); |
| 173 | + |
167 | 174 | const focusCallback = useCallback( |
168 | | - ({ noKeyboard }: SendBoxFocusOptions) => { |
169 | | - const { current } = inputElementRef; |
170 | | - |
171 | | - // Setting `inputMode` to `none` temporarily to suppress soft keyboard in iOS. |
172 | | - // We will revert the change once the end-user tap on the send box. |
173 | | - // This code path is only triggered when the user press "send" button to send the message, instead of pressing ENTER key. |
174 | | - noKeyboard && current?.setAttribute('inputmode', 'none'); |
175 | | - current?.focus(); |
| 175 | + ({ noKeyboard, waitUntil }: SendBoxFocusOptions) => { |
| 176 | + waitUntil( |
| 177 | + (async () => { |
| 178 | + const { current } = inputElementRef; |
| 179 | + |
| 180 | + if (current) { |
| 181 | + // Setting `inputMode` to `none` temporarily to suppress soft keyboard in iOS. |
| 182 | + // We will revert the change once the end-user tap on the send box. |
| 183 | + // This code path is only triggered when the user press "send" button to send the message, instead of pressing ENTER key. |
| 184 | + if (noKeyboard) { |
| 185 | + if (current.getAttribute('inputmode') !== 'none') { |
| 186 | + // Collapse the virtual keybaord if it was expanded. |
| 187 | + current.setAttribute('inputmode', 'none'); |
| 188 | + |
| 189 | + // iOS 26.3 quirks: `HTMLElement.focus()` does not pickup `inputmode="none"` changes immediately. |
| 190 | + // We need to wait for next frame before calling `focus()`. |
| 191 | + // This is a regression from iOS 26.2. |
| 192 | + await new Promise<void>(resolve => requestIdleCallbackWithPonyfill(resolve)); |
| 193 | + } |
| 194 | + } else if (current.hasAttribute('inputmode')) { |
| 195 | + // Expanding the virtual keyboard if it was collapsed. |
| 196 | + // However, we are not pausing here to workaround iOS 26.3 quirks. |
| 197 | + // 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. |
| 198 | + // In other words, if we pause here, the event will be send to the activity/transcript, instead of the newly focused send box. |
| 199 | + // This is related to BasicTranscript.handleTranscriptKeyDownCapture(). |
| 200 | + current.removeAttribute('inputmode'); |
| 201 | + } |
| 202 | + |
| 203 | + current?.focus(); |
| 204 | + } |
| 205 | + })() |
| 206 | + ); |
176 | 207 | }, |
177 | | - [inputElementRef] |
| 208 | + [inputElementRef, requestIdleCallbackWithPonyfill] |
178 | 209 | ); |
179 | 210 |
|
180 | 211 | useRegisterFocusSendBox(focusCallback); |
|
0 commit comments