Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<!doctype html>
Comment thread
OEvgeny marked this conversation as resolved.
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
<script crossorigin="anonymous" src="/test-harness.js"></script>
<script crossorigin="anonymous" src="/test-page-object.js"></script>
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
<script type="importmap">
{
"imports": {
"jest-mock": "https://esm.sh/jest-mock"
}
}
</script>
</head>
<body>
<main id="webchat"></main>
<script type="module">
import { fn, spyOn } from 'jest-mock';

run(async function () {
const {
testHelpers: { createDirectLineEmulator }
} = window;

const { directLine, store } = createDirectLineEmulator();

const timeline = [];

const originalRequestIdleCallback = window.requestIdleCallback;

const requestIdleCallback = spyOn(window, 'requestIdleCallback').mockImplementation(callback => {
timeline.push('requestIdleCallback()');
originalRequestIdleCallback.call(window, callback);
});

WebChat.renderWebChat({ directLine, store }, document.getElementById('webchat'));

await pageConditions.uiConnected();

await directLine.actPostActivity(async () => {
const sendBoxTextBox = pageElements.sendBoxTextBox();

const originalFocus = sendBoxTextBox.focus;
const originalSetAttribute = sendBoxTextBox.setAttribute;

const focus = spyOn(sendBoxTextBox, 'focus').mockImplementation(() => {
timeline.push('focus()');
originalFocus.call(sendBoxTextBox);
});

const setAttribute = spyOn(sendBoxTextBox, 'setAttribute').mockImplementation((name, value) => {
timeline.push(`setAttribute(${JSON.stringify(name)}, ${JSON.stringify(value)})`);
originalSetAttribute.call(sendBoxTextBox, name, value);
});

await host.click(pageElements.sendBoxTextBox());
await host.sendKeys('Hello, World!');

// WHEN: Click on the send button.
await host.click(pageElements.sendButton());

expect(timeline).toEqual([
'setAttribute(\"inputmode\", \"text\")', // THEN: `setAttribute()` is called when click on the text box.
'setAttribute(\"inputmode\", \"none\")', // THEN: Tap on the send button should hide the virtual keyboard.
'requestIdleCallback()', // THEN: Make sure there is a pause between `setAttribute()` and `focus()`
'focus()' // THEN: Should focus on the send box.
]);

expect(document.activeElement).toBe(sendBoxTextBox);
});
});
</script>
</body>
</html>
37 changes: 27 additions & 10 deletions packages/component/src/SendBox/TextBox.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -164,17 +165,33 @@ 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;

// 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?.removeAttribute('inputmode');

// 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<void>(resolve => requestIdleCallbackWithPonyfill(resolve));

current?.focus();
})()
);
},
[inputElementRef]
[inputElementRef, requestIdleCallbackWithPonyfill]
);

useRegisterFocusSendBox(focusCallback);
Expand Down
Loading