diff --git a/CHANGELOG.md b/CHANGELOG.md index 730d0ee811..021ea178c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -386,6 +386,7 @@ Breaking changes in this release: - Fixed Content Security Policy documentation and sample in PR, by [@compulim](https://github.com/compulim) in PR [#5648](https://github.com/microsoft/BotFramework-WebChat/pull/5648) - Added `img-src data:`, required for icons - 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) ## [4.18.0] - 2024-07-10 diff --git a/__tests__/html2/fluentTheme/uiState.html.snap-3.png b/__tests__/html2/fluentTheme/uiState.html.snap-3.png index ff7fa334e1..7c929272ae 100644 Binary files a/__tests__/html2/fluentTheme/uiState.html.snap-3.png and b/__tests__/html2/fluentTheme/uiState.html.snap-3.png differ diff --git a/__tests__/html2/fluentTheme/uiState.html.snap-4.png b/__tests__/html2/fluentTheme/uiState.html.snap-4.png index f9a5cc1bfb..4bd0b6dfa6 100644 Binary files a/__tests__/html2/fluentTheme/uiState.html.snap-4.png and b/__tests__/html2/fluentTheme/uiState.html.snap-4.png differ diff --git a/__tests__/html2/sendBox/hideKeyboard.html b/__tests__/html2/sendBox/hideKeyboard.html new file mode 100644 index 0000000000..37d193ce3e --- /dev/null +++ b/__tests__/html2/sendBox/hideKeyboard.html @@ -0,0 +1,64 @@ + + + + + + + + + +
+ + + diff --git a/packages/component/src/SendBox/AutoResizeTextArea.tsx b/packages/component/src/SendBox/AutoResizeTextArea.tsx index cf59b83d2c..7ed5957703 100644 --- a/packages/component/src/SendBox/AutoResizeTextArea.tsx +++ b/packages/component/src/SendBox/AutoResizeTextArea.tsx @@ -4,7 +4,8 @@ import React, { FocusEventHandler, forwardRef, KeyboardEventHandler, - ReactEventHandler + ReactEventHandler, + type MouseEventHandler } from 'react'; import AccessibleTextArea from '../Utils/AccessibleTextArea'; @@ -21,6 +22,7 @@ type AutoResizeTextAreaProps = Readonly<{ enterKeyHint?: string | undefined; inputMode?: 'text' | 'none' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search' | undefined; onChange?: ChangeEventHandler | undefined; + onClick?: MouseEventHandler | undefined; onFocus?: FocusEventHandler | undefined; onKeyDown?: KeyboardEventHandler | undefined; onKeyDownCapture?: KeyboardEventHandler | undefined; @@ -45,6 +47,7 @@ const AutoResizeTextArea = forwardRef = useRef(); const localize = useLocalizer(); @@ -165,44 +165,25 @@ const TextBox = ({ className = '' }: Readonly<{ className?: string | undefined } ); const focusCallback = useCallback( - (options: SendBoxFocusOptions) => { - const { noKeyboard } = options; + ({ noKeyboard }: SendBoxFocusOptions) => { const { current } = inputElementRef; - if (current) { - // The "disable soft keyboard on mobile devices" logic will not work on IE11. It will cause the to become read-only until next focus. - // Thus, no mobile devices carry IE11 so we don't need to explicitly disable soft keyboard on IE11. - // See #3757 for repro and details. - if (noKeyboard && !ie11) { - // To not activate the virtual keyboard while changing focus to an input, we will temporarily set it as read-only and flip it back. - // https://stackoverflow.com/questions/7610758/prevent-iphone-default-keyboard-when-focusing-an-input/7610923 - const readOnly = current.getAttribute('readonly'); - - current.setAttribute('readonly', 'readonly'); - - options.waitUntil( - (async function () { - // TODO: [P2] We should update this logic to handle quickly-successive `focusCallback`. - // If a succeeding `focusCallback` is being called, the `setTimeout` should run immediately. - // Or the second `focusCallback` should not set `readonly` to `true`. - await new Promise(resolve => setTimeout(resolve, 0)); - - if (current) { - current.focus(); - readOnly ? current.setAttribute('readonly', readOnly) : current.removeAttribute('readonly'); - } - })() - ); - } else { - current.focus(); - } - } + // 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(); }, - [inputElementRef, setTimeout] + [inputElementRef] ); useRegisterFocusSendBox(focusCallback); + const handleClick = useCallback( + ({ currentTarget }) => currentTarget.setAttribute('inputmode', DEFAULT_INPUT_MODE), + [] + ); + const emojiMap = useMemo(() => new Map(Object.entries(emojiSet)), [emojiSet]); return ( @@ -225,8 +206,9 @@ const TextBox = ({ className = '' }: Readonly<{ className?: string | undefined } disabled={disabled} emojiMap={emojiMap} enterKeyHint="send" - inputMode="text" + inputMode={DEFAULT_INPUT_MODE} onChange={setValue} + onClick={handleClick} onKeyDownCapture={disabled ? undefined : handleKeyDownCapture} onKeyPress={disabled ? undefined : handleKeyPress} placeholder={typeYourMessageString} @@ -244,8 +226,9 @@ const TextBox = ({ className = '' }: Readonly<{ className?: string | undefined } disabled={disabled} emojiMap={emojiMap} enterKeyHint="send" - inputMode="text" + inputMode={DEFAULT_INPUT_MODE} onChange={setValue} + onClick={handleClick} onKeyDownCapture={disabled ? undefined : handleKeyDownCapture} onKeyPress={disabled ? undefined : handleKeyPress} placeholder={typeYourMessageString} diff --git a/packages/component/src/TextArea/TextArea.tsx b/packages/component/src/TextArea/TextArea.tsx index fd870ef711..15ab98a91c 100644 --- a/packages/component/src/TextArea/TextArea.tsx +++ b/packages/component/src/TextArea/TextArea.tsx @@ -8,6 +8,7 @@ import React, { useRef, type FormEventHandler, type KeyboardEventHandler, + type MouseEventHandler, type ReactNode } from 'react'; @@ -33,6 +34,7 @@ const TextArea = forwardRef< * This ensures the flow of focus did not sent to document body */ hidden?: boolean | undefined; + onClick?: MouseEventHandler | undefined; onInput?: FormEventHandler | undefined; placeholder?: string | undefined; startRows?: number | undefined; @@ -90,6 +92,7 @@ const TextArea = forwardRef< aria-placeholder={props.placeholder} className={cx(classNames['text-area-input'], classNames['text-area-shared'])} data-testid={props['data-testid']} + onClick={props.onClick} onCompositionEnd={handleCompositionEnd} onCompositionStart={handleCompositionStart} onInput={props.onInput} diff --git a/packages/component/src/Utils/AccessibleInputText.tsx b/packages/component/src/Utils/AccessibleInputText.tsx index 3e19f3c57d..da9221baae 100644 --- a/packages/component/src/Utils/AccessibleInputText.tsx +++ b/packages/component/src/Utils/AccessibleInputText.tsx @@ -1,11 +1,12 @@ /* eslint no-magic-numbers: ["error", { "ignore": [-1] }] */ import React, { - ChangeEventHandler, - FocusEventHandler, + type ChangeEventHandler, + type FocusEventHandler, forwardRef, - KeyboardEventHandler, - ReactEventHandler, + type KeyboardEventHandler, + type MouseEventHandler, + type ReactEventHandler, useRef } from 'react'; @@ -38,6 +39,7 @@ type AccessibleInputTextProps = Readonly<{ enterKeyHint?: string | undefined; inputMode?: 'text' | 'none' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search' | undefined; onChange?: ChangeEventHandler | undefined; + onClick?: MouseEventHandler | undefined; onFocus?: FocusEventHandler | undefined; onKeyDown?: KeyboardEventHandler | undefined; onKeyDownCapture?: KeyboardEventHandler | undefined; @@ -60,6 +62,7 @@ const AccessibleInputText = forwardRef, the developer need to filter out some `onSubmit` event caused by this widget -type AccessibleTextAreaProps = { +type AccessibleTextAreaProps = Readonly<{ className?: string; disabled?: boolean; inputMode?: 'text' | 'none' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search'; onChange?: ChangeEventHandler; + onClick?: MouseEventHandler | undefined; onFocus?: FocusEventHandler; onKeyDown?: KeyboardEventHandler; onKeyDownCapture?: KeyboardEventHandler; @@ -43,7 +44,7 @@ type AccessibleTextAreaProps = { rows?: number; tabIndex?: number; value?: string; -}; +}>; const AccessibleTextArea = forwardRef( ( @@ -52,6 +53,7 @@ const AccessibleTextArea = forwardRef { - if (!inputRef.current) { - return; - } - if (noKeyboard) { - waitUntil( - (async () => { - const previousReadOnly = inputRef.current?.getAttribute('readonly'); - inputRef.current?.setAttribute('readonly', 'true'); - // TODO: [P2] We should update this logic to handle quickly-successive `focusCallback`. - // If a succeeding `focusCallback` is being called, the `setTimeout` should run immediately. - // Or the second `focusCallback` should not set `readonly` to `true`. - // eslint-disable-next-line no-restricted-globals - await new Promise(resolve => setTimeout(resolve, 0)); - inputRef.current?.focus(); - if (typeof previousReadOnly !== 'string') { - inputRef.current?.removeAttribute('readonly'); - } else { - inputRef.current?.setAttribute('readonly', previousReadOnly); - } - })() - ); - } else { - inputRef.current?.focus(); - } + ({ noKeyboard }) => { + const { current } = inputRef; + + // 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(); }, [inputRef] ) @@ -144,6 +127,8 @@ function SendBox(props: Props) { [attachmentsRef, makeThumbnail, setAttachments] ); + const handleClick = useCallback(({ currentTarget }) => currentTarget.removeAttribute('inputmode'), []); + const handleFormSubmit: FormEventHandler = useCallback( event => { event.preventDefault(); @@ -206,6 +191,7 @@ function SendBox(props: Props) { completion={props.completion} data-testid={testIds.sendBoxTextBox} hidden={shouldShowTelephoneKeypad} + onClick={handleClick} onInput={handleMessageChange} placeholder={props.placeholder ?? localize('TEXT_INPUT_PLACEHOLDER')} ref={inputRef}