Skip to content

Commit 6f2c6cb

Browse files
authored
[iOS 26.2] Suppressed virtual keyboard should show after tap on send box (#5678)
* Fix virtual keyboard for iOS 26.2 * Add PR number * Fix ESLint * Revert * Sort * Sort * Update scrollbar color
1 parent 81c9765 commit 6f2c6cb

File tree

10 files changed

+121
-107
lines changed

10 files changed

+121
-107
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,7 @@ Breaking changes in this release:
386386
- 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)
387387
- Added `img-src data:`, required for icons
388388
- Downgraded graph upsert conflict checks, by [@compulim](https://github.com/compulim) in PR [#5674](https://github.com/microsoft/BotFramework-WebChat/pull/5674)
389+
- 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)
389390

390391
## [4.18.0] - 2024-07-10
391392

4 Bytes
Loading
2 Bytes
Loading
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<!doctype html>
2+
<html lang="en-US">
3+
<head>
4+
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
5+
<script crossorigin="anonymous" src="/test-harness.js"></script>
6+
<script crossorigin="anonymous" src="/test-page-object.js"></script>
7+
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
8+
</head>
9+
<body>
10+
<main id="webchat"></main>
11+
<script>
12+
run(async function () {
13+
const {
14+
testHelpers: { createDirectLineEmulator }
15+
} = window;
16+
17+
const { directLine, store } = createDirectLineEmulator();
18+
19+
WebChat.renderWebChat({ directLine, store }, document.getElementById('webchat'));
20+
21+
await pageConditions.uiConnected();
22+
23+
const { resolveAll } = await directLine.actPostActivity(async () => {
24+
await host.click(pageElements.sendBoxTextBox());
25+
await host.sendKeys('suggested-actions');
26+
// WHEN: Click on the send button.
27+
await host.click(pageElements.sendButton());
28+
});
29+
30+
// THEN: Should send the focus back to the send box.
31+
expect(document.activeElement).toBe(pageElements.sendBoxTextBox());
32+
33+
// THEN: Should set `inputmode="none"`, thus, hide the virtual keyboard.
34+
// Notes: We cannot test this functionality inside WebDriver. We assume the `inputmode` attribute will show/hide the virtual keyboard.
35+
expect(pageElements.sendBoxTextBox().getAttribute('inputmode')).toBe('none');
36+
37+
await resolveAll();
38+
39+
await directLine.emulateIncomingActivity({
40+
suggestedActions: {
41+
actions: [
42+
{
43+
title: 'Hello, World!',
44+
type: 'imBack'
45+
},
46+
{
47+
title: 'Aloha!',
48+
type: 'imBack'
49+
}
50+
]
51+
},
52+
text: 'Select one of the following options.',
53+
type: 'message'
54+
});
55+
56+
// WHEN: Click on the send box.
57+
await host.click(pageElements.sendBoxTextBox());
58+
59+
// THEN: Should revert `inputmode` attribute, thus, show the virtual keyboard.
60+
expect(pageElements.sendBoxTextBox().getAttribute('inputmode')).not.toBe('none');
61+
});
62+
</script>
63+
</body>
64+
</html>

packages/component/src/SendBox/AutoResizeTextArea.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import React, {
44
FocusEventHandler,
55
forwardRef,
66
KeyboardEventHandler,
7-
ReactEventHandler
7+
ReactEventHandler,
8+
type MouseEventHandler
89
} from 'react';
910

1011
import AccessibleTextArea from '../Utils/AccessibleTextArea';
@@ -21,6 +22,7 @@ type AutoResizeTextAreaProps = Readonly<{
2122
enterKeyHint?: string | undefined;
2223
inputMode?: 'text' | 'none' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search' | undefined;
2324
onChange?: ChangeEventHandler<HTMLTextAreaElement> | undefined;
25+
onClick?: MouseEventHandler<HTMLTextAreaElement> | undefined;
2426
onFocus?: FocusEventHandler<HTMLTextAreaElement> | undefined;
2527
onKeyDown?: KeyboardEventHandler<HTMLTextAreaElement> | undefined;
2628
onKeyDownCapture?: KeyboardEventHandler<HTMLTextAreaElement> | undefined;
@@ -45,6 +47,7 @@ const AutoResizeTextArea = forwardRef<HTMLTextAreaElement, AutoResizeTextAreaPro
4547
enterKeyHint,
4648
inputMode,
4749
onChange,
50+
onClick,
4851
onFocus,
4952
onKeyDown,
5053
onKeyDownCapture,
@@ -77,6 +80,7 @@ const AutoResizeTextArea = forwardRef<HTMLTextAreaElement, AutoResizeTextAreaPro
7780
disabled={disabled}
7881
inputMode={inputMode}
7982
onChange={onChange}
83+
onClick={onClick}
8084
onFocus={onFocus}
8185
onKeyDown={onKeyDown}
8286
onKeyDownCapture={onKeyDownCapture}

packages/component/src/SendBox/TextBox.tsx

Lines changed: 19 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import React, { useCallback, useMemo, useRef } from 'react';
44

55
import AccessibleInputText from '../Utils/AccessibleInputText';
66
import navigableEvent from '../Utils/TypeFocusSink/navigableEvent';
7-
import { ie11 } from '../Utils/detectBrowser';
87
import { useRegisterFocusSendBox, type SendBoxFocusOptions } from '../hooks/sendBoxFocus';
98
import { useStyleToEmotionObject } from '../hooks/internal/styleToEmotionObject';
109
import useScrollDown from '../hooks/useScrollDown';
@@ -17,7 +16,9 @@ import AutoResizeTextArea from './AutoResizeTextArea';
1716
import type { MutableRefObject } from 'react';
1817
import testIds from '../testIds';
1918

20-
const { useLocalizer, usePonyfill, useSendBoxValue, useStopDictate, useStyleOptions, useUIState } = hooks;
19+
const { useLocalizer, useSendBoxValue, useStopDictate, useStyleOptions, useUIState } = hooks;
20+
21+
const DEFAULT_INPUT_MODE = 'text';
2122

2223
const ROOT_STYLE = {
2324
'&.webchat__send-box-text-box': {
@@ -83,7 +84,6 @@ const TextBox = ({ className = '' }: Readonly<{ className?: string | undefined }
8384
const [value, setValue] = useSendBoxValue();
8485
const [{ sendBoxTextBox: sendBoxTextBoxStyleSet }] = useStyleSet();
8586
const [{ emojiSet, sendBoxTextWrap }] = useStyleOptions();
86-
const [{ setTimeout }] = usePonyfill();
8787
const [uiState] = useUIState();
8888
const inputElementRef: MutableRefObject<HTMLInputElement & HTMLTextAreaElement> = useRef();
8989
const localize = useLocalizer();
@@ -165,44 +165,25 @@ const TextBox = ({ className = '' }: Readonly<{ className?: string | undefined }
165165
);
166166

167167
const focusCallback = useCallback(
168-
(options: SendBoxFocusOptions) => {
169-
const { noKeyboard } = options;
168+
({ noKeyboard }: SendBoxFocusOptions) => {
170169
const { current } = inputElementRef;
171170

172-
if (current) {
173-
// The "disable soft keyboard on mobile devices" logic will not work on IE11. It will cause the <input> to become read-only until next focus.
174-
// Thus, no mobile devices carry IE11 so we don't need to explicitly disable soft keyboard on IE11.
175-
// See #3757 for repro and details.
176-
if (noKeyboard && !ie11) {
177-
// To not activate the virtual keyboard while changing focus to an input, we will temporarily set it as read-only and flip it back.
178-
// https://stackoverflow.com/questions/7610758/prevent-iphone-default-keyboard-when-focusing-an-input/7610923
179-
const readOnly = current.getAttribute('readonly');
180-
181-
current.setAttribute('readonly', 'readonly');
182-
183-
options.waitUntil(
184-
(async function () {
185-
// TODO: [P2] We should update this logic to handle quickly-successive `focusCallback`.
186-
// If a succeeding `focusCallback` is being called, the `setTimeout` should run immediately.
187-
// Or the second `focusCallback` should not set `readonly` to `true`.
188-
await new Promise(resolve => setTimeout(resolve, 0));
189-
190-
if (current) {
191-
current.focus();
192-
readOnly ? current.setAttribute('readonly', readOnly) : current.removeAttribute('readonly');
193-
}
194-
})()
195-
);
196-
} else {
197-
current.focus();
198-
}
199-
}
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();
200176
},
201-
[inputElementRef, setTimeout]
177+
[inputElementRef]
202178
);
203179

204180
useRegisterFocusSendBox(focusCallback);
205181

182+
const handleClick = useCallback(
183+
({ currentTarget }) => currentTarget.setAttribute('inputmode', DEFAULT_INPUT_MODE),
184+
[]
185+
);
186+
206187
const emojiMap = useMemo(() => new Map<string, string>(Object.entries(emojiSet)), [emojiSet]);
207188

208189
return (
@@ -225,8 +206,9 @@ const TextBox = ({ className = '' }: Readonly<{ className?: string | undefined }
225206
disabled={disabled}
226207
emojiMap={emojiMap}
227208
enterKeyHint="send"
228-
inputMode="text"
209+
inputMode={DEFAULT_INPUT_MODE}
229210
onChange={setValue}
211+
onClick={handleClick}
230212
onKeyDownCapture={disabled ? undefined : handleKeyDownCapture}
231213
onKeyPress={disabled ? undefined : handleKeyPress}
232214
placeholder={typeYourMessageString}
@@ -244,8 +226,9 @@ const TextBox = ({ className = '' }: Readonly<{ className?: string | undefined }
244226
disabled={disabled}
245227
emojiMap={emojiMap}
246228
enterKeyHint="send"
247-
inputMode="text"
229+
inputMode={DEFAULT_INPUT_MODE}
248230
onChange={setValue}
231+
onClick={handleClick}
249232
onKeyDownCapture={disabled ? undefined : handleKeyDownCapture}
250233
onKeyPress={disabled ? undefined : handleKeyPress}
251234
placeholder={typeYourMessageString}

packages/component/src/TextArea/TextArea.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import React, {
88
useRef,
99
type FormEventHandler,
1010
type KeyboardEventHandler,
11+
type MouseEventHandler,
1112
type ReactNode
1213
} from 'react';
1314

@@ -33,6 +34,7 @@ const TextArea = forwardRef<
3334
* This ensures the flow of focus did not sent to document body
3435
*/
3536
hidden?: boolean | undefined;
37+
onClick?: MouseEventHandler<HTMLTextAreaElement> | undefined;
3638
onInput?: FormEventHandler<HTMLTextAreaElement> | undefined;
3739
placeholder?: string | undefined;
3840
startRows?: number | undefined;
@@ -90,6 +92,7 @@ const TextArea = forwardRef<
9092
aria-placeholder={props.placeholder}
9193
className={cx(classNames['text-area-input'], classNames['text-area-shared'])}
9294
data-testid={props['data-testid']}
95+
onClick={props.onClick}
9396
onCompositionEnd={handleCompositionEnd}
9497
onCompositionStart={handleCompositionStart}
9598
onInput={props.onInput}

packages/component/src/Utils/AccessibleInputText.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
/* eslint no-magic-numbers: ["error", { "ignore": [-1] }] */
22

33
import React, {
4-
ChangeEventHandler,
5-
FocusEventHandler,
4+
type ChangeEventHandler,
5+
type FocusEventHandler,
66
forwardRef,
7-
KeyboardEventHandler,
8-
ReactEventHandler,
7+
type KeyboardEventHandler,
8+
type MouseEventHandler,
9+
type ReactEventHandler,
910
useRef
1011
} from 'react';
1112

@@ -38,6 +39,7 @@ type AccessibleInputTextProps = Readonly<{
3839
enterKeyHint?: string | undefined;
3940
inputMode?: 'text' | 'none' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search' | undefined;
4041
onChange?: ChangeEventHandler<HTMLInputElement> | undefined;
42+
onClick?: MouseEventHandler<HTMLInputElement> | undefined;
4143
onFocus?: FocusEventHandler<HTMLInputElement> | undefined;
4244
onKeyDown?: KeyboardEventHandler<HTMLInputElement> | undefined;
4345
onKeyDownCapture?: KeyboardEventHandler<HTMLInputElement> | undefined;
@@ -60,6 +62,7 @@ const AccessibleInputText = forwardRef<HTMLInputElement, AccessibleInputTextProp
6062
disabled,
6163
enterKeyHint,
6264
onChange,
65+
onClick,
6366
onFocus,
6467
onKeyDown,
6568
onKeyDownCapture,
@@ -87,6 +90,7 @@ const AccessibleInputText = forwardRef<HTMLInputElement, AccessibleInputTextProp
8790
data-id={dataId}
8891
data-testid={dataTestId}
8992
onChange={disabled ? undefined : onChange}
93+
onClick={onClick}
9094
onFocus={disabled ? undefined : onFocus}
9195
onKeyDown={disabled ? undefined : onKeyDown}
9296
onKeyDownCapture={disabled ? undefined : onKeyDownCapture}

packages/component/src/Utils/AccessibleTextArea.tsx

Lines changed: 10 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
/* eslint no-magic-numbers: ["error", { "ignore": [-1] }] */
22

3-
import PropTypes from 'prop-types';
43
import React, {
5-
ChangeEventHandler,
6-
FocusEventHandler,
4+
type ChangeEventHandler,
5+
type FocusEventHandler,
76
forwardRef,
8-
KeyboardEventHandler,
9-
ReactEventHandler,
7+
type KeyboardEventHandler,
8+
type MouseEventHandler,
9+
type ReactEventHandler,
1010
useRef
1111
} from 'react';
1212

@@ -28,11 +28,12 @@ import React, {
2828
// - aria-disabled="true" is the source of truth
2929
// - If the widget is contained by a <form>, the developer need to filter out some `onSubmit` event caused by this widget
3030

31-
type AccessibleTextAreaProps = {
31+
type AccessibleTextAreaProps = Readonly<{
3232
className?: string;
3333
disabled?: boolean;
3434
inputMode?: 'text' | 'none' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search';
3535
onChange?: ChangeEventHandler<HTMLTextAreaElement>;
36+
onClick?: MouseEventHandler<HTMLTextAreaElement> | undefined;
3637
onFocus?: FocusEventHandler<HTMLTextAreaElement>;
3738
onKeyDown?: KeyboardEventHandler<HTMLTextAreaElement>;
3839
onKeyDownCapture?: KeyboardEventHandler<HTMLTextAreaElement>;
@@ -43,7 +44,7 @@ type AccessibleTextAreaProps = {
4344
rows?: number;
4445
tabIndex?: number;
4546
value?: string;
46-
};
47+
}>;
4748

4849
const AccessibleTextArea = forwardRef<HTMLTextAreaElement, AccessibleTextAreaProps>(
4950
(
@@ -52,6 +53,7 @@ const AccessibleTextArea = forwardRef<HTMLTextAreaElement, AccessibleTextAreaPro
5253
disabled,
5354
inputMode,
5455
onChange,
56+
onClick,
5557
onFocus,
5658
onKeyDown,
5759
onKeyDownCapture,
@@ -75,6 +77,7 @@ const AccessibleTextArea = forwardRef<HTMLTextAreaElement, AccessibleTextAreaPro
7577
className={className}
7678
inputMode={inputMode}
7779
onChange={disabled ? undefined : onChange}
80+
onClick={onClick}
7881
onFocus={disabled ? undefined : onFocus}
7982
onKeyDown={disabled ? undefined : onKeyDown}
8083
onKeyDownCapture={disabled ? undefined : onKeyDownCapture}
@@ -91,40 +94,6 @@ const AccessibleTextArea = forwardRef<HTMLTextAreaElement, AccessibleTextAreaPro
9194
}
9295
);
9396

94-
AccessibleTextArea.defaultProps = {
95-
className: undefined,
96-
disabled: undefined,
97-
inputMode: undefined,
98-
onChange: undefined,
99-
onFocus: undefined,
100-
onKeyDown: undefined,
101-
onKeyDownCapture: undefined,
102-
onKeyPress: undefined,
103-
onSelect: undefined,
104-
placeholder: undefined,
105-
readOnly: undefined,
106-
rows: undefined,
107-
tabIndex: undefined,
108-
value: undefined
109-
};
110-
11197
AccessibleTextArea.displayName = 'AccessibleTextArea';
11298

113-
AccessibleTextArea.propTypes = {
114-
className: PropTypes.string,
115-
disabled: PropTypes.bool,
116-
inputMode: PropTypes.oneOf(['text', 'none', 'tel', 'url', 'email', 'numeric', 'decimal', 'search']),
117-
onChange: PropTypes.func,
118-
onFocus: PropTypes.func,
119-
onKeyDown: PropTypes.func,
120-
onKeyDownCapture: PropTypes.func,
121-
onKeyPress: PropTypes.func,
122-
onSelect: PropTypes.func,
123-
placeholder: PropTypes.string,
124-
readOnly: PropTypes.bool,
125-
rows: PropTypes.number,
126-
tabIndex: PropTypes.number,
127-
value: PropTypes.string
128-
};
129-
13099
export default AccessibleTextArea;

0 commit comments

Comments
 (0)