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 @@ -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

Expand Down
Binary file modified __tests__/html2/fluentTheme/uiState.html.snap-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified __tests__/html2/fluentTheme/uiState.html.snap-4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
64 changes: 64 additions & 0 deletions __tests__/html2/sendBox/hideKeyboard.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<!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>
</head>
<body>
<main id="webchat"></main>
<script>
run(async function () {
const {
testHelpers: { createDirectLineEmulator }
} = window;

const { directLine, store } = createDirectLineEmulator();

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

await pageConditions.uiConnected();

const { resolveAll } = await directLine.actPostActivity(async () => {
await host.click(pageElements.sendBoxTextBox());
await host.sendKeys('suggested-actions');
// WHEN: Click on the send button.
await host.click(pageElements.sendButton());
});

// THEN: Should send the focus back to the send box.
expect(document.activeElement).toBe(pageElements.sendBoxTextBox());

// THEN: Should set `inputmode="none"`, thus, hide the virtual keyboard.
// Notes: We cannot test this functionality inside WebDriver. We assume the `inputmode` attribute will show/hide the virtual keyboard.
expect(pageElements.sendBoxTextBox().getAttribute('inputmode')).toBe('none');

await resolveAll();

await directLine.emulateIncomingActivity({
suggestedActions: {
actions: [
{
title: 'Hello, World!',
type: 'imBack'
},
{
title: 'Aloha!',
type: 'imBack'
}
]
},
text: 'Select one of the following options.',
type: 'message'
});

// WHEN: Click on the send box.
await host.click(pageElements.sendBoxTextBox());

// THEN: Should revert `inputmode` attribute, thus, show the virtual keyboard.
expect(pageElements.sendBoxTextBox().getAttribute('inputmode')).not.toBe('none');
});
</script>
</body>
</html>
6 changes: 5 additions & 1 deletion packages/component/src/SendBox/AutoResizeTextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import React, {
FocusEventHandler,
forwardRef,
KeyboardEventHandler,
ReactEventHandler
ReactEventHandler,
type MouseEventHandler
} from 'react';

import AccessibleTextArea from '../Utils/AccessibleTextArea';
Expand All @@ -21,6 +22,7 @@ type AutoResizeTextAreaProps = Readonly<{
enterKeyHint?: string | undefined;
inputMode?: 'text' | 'none' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search' | undefined;
onChange?: ChangeEventHandler<HTMLTextAreaElement> | undefined;
onClick?: MouseEventHandler<HTMLTextAreaElement> | undefined;
onFocus?: FocusEventHandler<HTMLTextAreaElement> | undefined;
onKeyDown?: KeyboardEventHandler<HTMLTextAreaElement> | undefined;
onKeyDownCapture?: KeyboardEventHandler<HTMLTextAreaElement> | undefined;
Expand All @@ -45,6 +47,7 @@ const AutoResizeTextArea = forwardRef<HTMLTextAreaElement, AutoResizeTextAreaPro
enterKeyHint,
inputMode,
onChange,
onClick,
onFocus,
onKeyDown,
onKeyDownCapture,
Expand Down Expand Up @@ -77,6 +80,7 @@ const AutoResizeTextArea = forwardRef<HTMLTextAreaElement, AutoResizeTextAreaPro
disabled={disabled}
inputMode={inputMode}
onChange={onChange}
onClick={onClick}
onFocus={onFocus}
onKeyDown={onKeyDown}
onKeyDownCapture={onKeyDownCapture}
Expand Down
55 changes: 19 additions & 36 deletions packages/component/src/SendBox/TextBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import React, { useCallback, useMemo, useRef } from 'react';

import AccessibleInputText from '../Utils/AccessibleInputText';
import navigableEvent from '../Utils/TypeFocusSink/navigableEvent';
import { ie11 } from '../Utils/detectBrowser';
import { useRegisterFocusSendBox, type SendBoxFocusOptions } from '../hooks/sendBoxFocus';
import { useStyleToEmotionObject } from '../hooks/internal/styleToEmotionObject';
import useScrollDown from '../hooks/useScrollDown';
Expand All @@ -17,7 +16,9 @@ import AutoResizeTextArea from './AutoResizeTextArea';
import type { MutableRefObject } from 'react';
import testIds from '../testIds';

const { useLocalizer, usePonyfill, useSendBoxValue, useStopDictate, useStyleOptions, useUIState } = hooks;
const { useLocalizer, useSendBoxValue, useStopDictate, useStyleOptions, useUIState } = hooks;

const DEFAULT_INPUT_MODE = 'text';

const ROOT_STYLE = {
'&.webchat__send-box-text-box': {
Expand Down Expand Up @@ -83,7 +84,6 @@ const TextBox = ({ className = '' }: Readonly<{ className?: string | undefined }
const [value, setValue] = useSendBoxValue();
const [{ sendBoxTextBox: sendBoxTextBoxStyleSet }] = useStyleSet();
const [{ emojiSet, sendBoxTextWrap }] = useStyleOptions();
const [{ setTimeout }] = usePonyfill();
const [uiState] = useUIState();
const inputElementRef: MutableRefObject<HTMLInputElement & HTMLTextAreaElement> = useRef();
const localize = useLocalizer();
Expand Down Expand Up @@ -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 <input> 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<string, string>(Object.entries(emojiSet)), [emojiSet]);

return (
Expand All @@ -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}
Expand All @@ -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}
Expand Down
3 changes: 3 additions & 0 deletions packages/component/src/TextArea/TextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import React, {
useRef,
type FormEventHandler,
type KeyboardEventHandler,
type MouseEventHandler,
type ReactNode
} from 'react';

Expand All @@ -33,6 +34,7 @@ const TextArea = forwardRef<
* This ensures the flow of focus did not sent to document body
*/
hidden?: boolean | undefined;
onClick?: MouseEventHandler<HTMLTextAreaElement> | undefined;
onInput?: FormEventHandler<HTMLTextAreaElement> | undefined;
placeholder?: string | undefined;
startRows?: number | undefined;
Expand Down Expand Up @@ -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}
Expand Down
12 changes: 8 additions & 4 deletions packages/component/src/Utils/AccessibleInputText.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -38,6 +39,7 @@ type AccessibleInputTextProps = Readonly<{
enterKeyHint?: string | undefined;
inputMode?: 'text' | 'none' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search' | undefined;
onChange?: ChangeEventHandler<HTMLInputElement> | undefined;
onClick?: MouseEventHandler<HTMLInputElement> | undefined;
onFocus?: FocusEventHandler<HTMLInputElement> | undefined;
onKeyDown?: KeyboardEventHandler<HTMLInputElement> | undefined;
onKeyDownCapture?: KeyboardEventHandler<HTMLInputElement> | undefined;
Expand All @@ -60,6 +62,7 @@ const AccessibleInputText = forwardRef<HTMLInputElement, AccessibleInputTextProp
disabled,
enterKeyHint,
onChange,
onClick,
onFocus,
onKeyDown,
onKeyDownCapture,
Expand Down Expand Up @@ -87,6 +90,7 @@ const AccessibleInputText = forwardRef<HTMLInputElement, AccessibleInputTextProp
data-id={dataId}
data-testid={dataTestId}
onChange={disabled ? undefined : onChange}
onClick={onClick}
onFocus={disabled ? undefined : onFocus}
onKeyDown={disabled ? undefined : onKeyDown}
onKeyDownCapture={disabled ? undefined : onKeyDownCapture}
Expand Down
51 changes: 10 additions & 41 deletions packages/component/src/Utils/AccessibleTextArea.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/* eslint no-magic-numbers: ["error", { "ignore": [-1] }] */

import PropTypes from 'prop-types';
import React, {
ChangeEventHandler,
FocusEventHandler,
type ChangeEventHandler,
type FocusEventHandler,
forwardRef,
KeyboardEventHandler,
ReactEventHandler,
type KeyboardEventHandler,
type MouseEventHandler,
type ReactEventHandler,
useRef
} from 'react';

Expand All @@ -28,11 +28,12 @@ import React, {
// - aria-disabled="true" is the source of truth
// - If the widget is contained by a <form>, 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<HTMLTextAreaElement>;
onClick?: MouseEventHandler<HTMLTextAreaElement> | undefined;
onFocus?: FocusEventHandler<HTMLTextAreaElement>;
onKeyDown?: KeyboardEventHandler<HTMLTextAreaElement>;
onKeyDownCapture?: KeyboardEventHandler<HTMLTextAreaElement>;
Expand All @@ -43,7 +44,7 @@ type AccessibleTextAreaProps = {
rows?: number;
tabIndex?: number;
value?: string;
};
}>;

const AccessibleTextArea = forwardRef<HTMLTextAreaElement, AccessibleTextAreaProps>(
(
Expand All @@ -52,6 +53,7 @@ const AccessibleTextArea = forwardRef<HTMLTextAreaElement, AccessibleTextAreaPro
disabled,
inputMode,
onChange,
onClick,
onFocus,
onKeyDown,
onKeyDownCapture,
Expand All @@ -75,6 +77,7 @@ const AccessibleTextArea = forwardRef<HTMLTextAreaElement, AccessibleTextAreaPro
className={className}
inputMode={inputMode}
onChange={disabled ? undefined : onChange}
onClick={onClick}
onFocus={disabled ? undefined : onFocus}
onKeyDown={disabled ? undefined : onKeyDown}
onKeyDownCapture={disabled ? undefined : onKeyDownCapture}
Expand All @@ -91,40 +94,6 @@ const AccessibleTextArea = forwardRef<HTMLTextAreaElement, AccessibleTextAreaPro
}
);

AccessibleTextArea.defaultProps = {
className: undefined,
disabled: undefined,
inputMode: undefined,
onChange: undefined,
onFocus: undefined,
onKeyDown: undefined,
onKeyDownCapture: undefined,
onKeyPress: undefined,
onSelect: undefined,
placeholder: undefined,
readOnly: undefined,
rows: undefined,
tabIndex: undefined,
value: undefined
};

AccessibleTextArea.displayName = 'AccessibleTextArea';

AccessibleTextArea.propTypes = {
className: PropTypes.string,
disabled: PropTypes.bool,
inputMode: PropTypes.oneOf(['text', 'none', 'tel', 'url', 'email', 'numeric', 'decimal', 'search']),
onChange: PropTypes.func,
onFocus: PropTypes.func,
onKeyDown: PropTypes.func,
onKeyDownCapture: PropTypes.func,
onKeyPress: PropTypes.func,
onSelect: PropTypes.func,
placeholder: PropTypes.string,
readOnly: PropTypes.bool,
rows: PropTypes.number,
tabIndex: PropTypes.number,
value: PropTypes.string
};

export default AccessibleTextArea;
Loading
Loading