Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion __tests__/html2/fluentTheme/connectivityStatus.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => <ReactWebChat directLine={directLine} store={store} />;
const App = () => <ReactWebChat directLine={directLine} store={store} styleOptions={styleOptions} />;

render(
<FluentThemeProvider>
Expand Down
Binary file modified __tests__/html2/fluentTheme/connectivityStatus.html.snap-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions __tests__/html2/hooks/useFocus.sendBox.pure.html
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
</script>
</body>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<!doctype html>
<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>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
Expand Down Expand Up @@ -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.
Expand Down
51 changes: 41 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,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<void>(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);
Expand Down
Loading