Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Focus should not blur briefly after tapping on a suggested action #5097

Merged
merged 2 commits into from
Mar 28, 2024
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
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

### Added

- Resolves [#5081](https://github.com/microsoft/BotFramework-WebChat/issues/5081). Added `uploadAccept` and `uploadMultiple` style options, by [@ms-jb](https://github.com/ms-jb)
- Resolves [#5081](https://github.com/microsoft/BotFramework-WebChat/issues/5081). Added `uploadAccept` and `uploadMultiple` style options, by [@ms-jb](https://github.com/ms-jb)

### Fixed

- Fixes [#5050](https://github.com/microsoft/BotFramework-WebChat/issues/5050). Fixed focus should not blur briefly after tapping on a suggested action, by [@compulim](https://github.com/compulim), in PR [#5097](https://github.com/microsoft/BotFramework-WebChat/issues/pull/5097)

### Changed

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<!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 () {
WebChat.renderWebChat(
{
directLine: testHelpers.createDirectLineWithTranscript([
{
from: {
id: 'bot',
role: 'bot'
},
suggestedActions: {
actions: [
{
type: 'imBack',
value: 'What can I say?'
},
{
type: 'imBack',
value: 'What is the weather?'
}
]
},
textFormat: 'markdown',
timestamp: new Date(2000, 0, 1, 12, 34, 56, 789).toISOString(),
type: 'message'
}
]),
store: testHelpers.createStore(),
styleOptions: {
suggestedActionLayout: 'stacked'
}
},
document.getElementById('webchat')
);

await pageConditions.uiConnected();
await pageConditions.suggestedActionsShown();

// THEN: Suggested actions container in stacked layout should be of `role="toolbar"` with `aria-orientation="vertical"`
const [firstSuggestedAction] = pageElements.suggestedActions();

let elementBeforeClick;

pageElements.sendBoxTextBox().focus = () => {
elementBeforeClick = document.activeElement;
};

await host.click(firstSuggestedAction);

expect(elementBeforeClick).not.toBe(document.body);
expect(elementBeforeClick).toBe(firstSuggestedAction);
});
</script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */

describe('accessibility requirement', () => {
describe('after clicking on suggested action', () => {
test('should send the focus to send box immediately', () => runHTML('accessibility.suggestedActions.sendFocusImmediately.html'));
});
});
19 changes: 19 additions & 0 deletions packages/component/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/component/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
"concurrently": "^8.2.2",
"core-js": "^3.34.0",
"node-dev": "^8.0.0",
"type-fest": "^4.14.0",
"typescript": "^5.3.2"
},
"dependencies": {
Expand Down
37 changes: 20 additions & 17 deletions packages/component/src/Composer.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,44 @@
import createEmotion from '@emotion/css/create-instance';
import { Composer as APIComposer, hooks, WebSpeechPonyfillFactory } from 'botframework-webchat-api';
import { Composer as SayComposer } from 'react-say';
import { singleToArray } from 'botframework-webchat-core';
import classNames from 'classnames';
import createEmotion from '@emotion/css/create-instance';
import createStyleSet from './Styles/createStyleSet';
import MarkdownIt from 'markdown-it';
import PropTypes from 'prop-types';
import React, { memo, useCallback, useMemo, useRef, useState } from 'react';
import { Composer as SayComposer } from 'react-say';
import createStyleSet from './Styles/createStyleSet';

import createDefaultAttachmentMiddleware from './Attachment/createMiddleware';
import Dictation from './Dictation';
import ErrorBox from './ErrorBox';
import {
speechSynthesis as bypassSpeechSynthesis,
SpeechSynthesisUtterance as BypassSpeechSynthesisUtterance
} from './hooks/internal/BypassSpeechSynthesisPonyfill';
import ActivityTreeComposer from './providers/ActivityTree/ActivityTreeComposer';
import addTargetBlankToHyperlinksMarkdown from './Utils/addTargetBlankToHyperlinksMarkdown';
import createCSSKey from './Utils/createCSSKey';
import UITracker from './hooks/internal/UITracker';
import WebChatUIContext from './hooks/internal/WebChatUIContext';
import useStyleSet from './hooks/useStyleSet';
import createDefaultActivityMiddleware from './Middleware/Activity/createCoreMiddleware';
import createDefaultActivityStatusMiddleware from './Middleware/ActivityStatus/createCoreMiddleware';
import createDefaultAttachmentForScreenReaderMiddleware from './Middleware/AttachmentForScreenReader/createCoreMiddleware';
import createDefaultAttachmentMiddleware from './Attachment/createMiddleware';
import createDefaultAvatarMiddleware from './Middleware/Avatar/createCoreMiddleware';
import createDefaultCardActionMiddleware from './Middleware/CardAction/createCoreMiddleware';
import createDefaultScrollToEndButtonMiddleware from './Middleware/ScrollToEndButton/createScrollToEndButtonMiddleware';
import createDefaultToastMiddleware from './Middleware/Toast/createCoreMiddleware';
import createDefaultTypingIndicatorMiddleware from './Middleware/TypingIndicator/createCoreMiddleware';
import Dictation from './Dictation';
import ActivityTreeComposer from './providers/ActivityTree/ActivityTreeComposer';
import SendBoxComposer from './providers/internal/SendBox/SendBoxComposer';
import ModalDialogComposer from './providers/ModalDialog/ModalDialogComposer';
import addTargetBlankToHyperlinksMarkdown from './Utils/addTargetBlankToHyperlinksMarkdown';
import createCSSKey from './Utils/createCSSKey';
import downscaleImageToDataURL from './Utils/downscaleImageToDataURL';
import ErrorBox from './ErrorBox';
import mapMap from './Utils/mapMap';
import ModalDialogComposer from './providers/ModalDialog/ModalDialogComposer';
import SendBoxComposer from './providers/internal/SendBox/SendBoxComposer';
import UITracker from './hooks/internal/UITracker';
import useStyleSet from './hooks/useStyleSet';
import WebChatUIContext from './hooks/internal/WebChatUIContext';

import type { ComposerProps as APIComposerProps } from 'botframework-webchat-api';
import type { FC, ReactNode } from 'react';
import type { ContextOf } from './types/ContextOf';
import { type FocusSendBoxInit } from './types/internal/FocusSendBoxInit';
import { type FocusTranscriptInit } from './types/internal/FocusTranscriptInit';

const { useGetActivityByKey, useReferenceGrammarID, useStyleOptions } = hooks;

Expand Down Expand Up @@ -97,8 +100,8 @@ const ComposerCore: FC<ComposerCoreProps> = ({
const [dictateAbortable, setDictateAbortable] = useState();
const [referenceGrammarID] = useReferenceGrammarID();
const [styleOptions] = useStyleOptions();
const focusSendBoxCallbacksRef = useRef([]);
const focusTranscriptCallbacksRef = useRef([]);
const focusSendBoxCallbacksRef = useRef<((init: FocusSendBoxInit) => Promise<void>)[]>([]);
const focusTranscriptCallbacksRef = useRef<((init: FocusTranscriptInit) => Promise<void>)[]>([]);
const internalMarkdownIt = useMemo(() => new MarkdownIt(), []);
const scrollToCallbacksRef = useRef([]);
const scrollToEndCallbacksRef = useRef([]);
Expand Down Expand Up @@ -202,7 +205,7 @@ const ComposerCore: FC<ComposerCoreProps> = ({
[transcriptFocusObserversRef, setNumTranscriptFocusObservers]
);

const context = useMemo(
const context = useMemo<ContextOf<typeof WebChatUIContext>>(
() => ({
dictateAbortable,
dispatchScrollPosition,
Expand Down
34 changes: 20 additions & 14 deletions packages/component/src/SendBox/SuggestedAction.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { hooks } from 'botframework-webchat-api';
import type { DirectLineCardAction } from 'botframework-webchat-core';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { MouseEventHandler, useCallback, VFC } from 'react';
import type { DirectLineCardAction } from 'botframework-webchat-core';

import AccessibleButton from '../Utils/AccessibleButton';
import connectToWebChat from '../connectToWebChat';
import useFocus from '../hooks/useFocus';
import useFocusAccessKeyEffect from '../Utils/AccessKeySink/useFocusAccessKeyEffect';
import useFocusVisible from '../hooks/internal/useFocusVisible';
import useItemRef from '../providers/RovingTabIndex/useItemRef';
import useLocalizeAccessKey from '../hooks/internal/useLocalizeAccessKey';
import useScrollToEnd from '../hooks/useScrollToEnd';
import useStyleSet from '../hooks/useStyleSet';
import useStyleToEmotionObject from '../hooks/internal/useStyleToEmotionObject';
import useSuggestedActionsAccessKey from '../hooks/internal/useSuggestedActionsAccessKey';
import useFocus from '../hooks/useFocus';
import useScrollToEnd from '../hooks/useScrollToEnd';
import useStyleSet from '../hooks/useStyleSet';
import useItemRef from '../providers/RovingTabIndex/useItemRef';
import AccessibleButton from '../Utils/AccessibleButton';
import useFocusAccessKeyEffect from '../Utils/AccessKeySink/useFocusAccessKeyEffect';

const { useDirection, useDisabled, usePerformCardAction, useStyleOptions, useSuggestedActions } = hooks;

Expand Down Expand Up @@ -90,15 +90,21 @@ const SuggestedAction: VFC<SuggestedActionProps> = ({

const handleClick = useCallback<MouseEventHandler<HTMLButtonElement>>(
({ target }) => {
// TODO: [P3] #XXX We should not destruct DirectLineCardAction into React props and pass them in. It makes typings difficult.
// Instead, we should pass a "cardAction" props.
performCardAction({ displayText, text, type, value } as DirectLineCardAction, { target });
(async function () {
// We need to focus to the send box before we are performing this card action.
// The will make sure the focus is always on Web Chat.
// Otherwise, the focus may momentarily send to `document.body` and screen reader will be confused.
await focus('sendBoxWithoutKeyboard');

// TODO: [P3] #XXX We should not destruct DirectLineCardAction into React props and pass them in. It makes typings difficult.
// Instead, we should pass a "cardAction" props.
performCardAction({ displayText, text, type, value } as DirectLineCardAction, { target });

// Since "openUrl" action do not submit, the suggested action buttons do not hide after click.
type === 'openUrl' && setSuggestedActions([]);
// Since "openUrl" action do not submit, the suggested action buttons do not hide after click.
type === 'openUrl' && setSuggestedActions([]);

focus('sendBoxWithoutKeyboard');
scrollToEnd();
scrollToEnd();
})();
},
[displayText, focus, performCardAction, scrollToEnd, setSuggestedActions, text, type, value]
);
Expand Down
35 changes: 19 additions & 16 deletions packages/component/src/SendBox/TextBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { useCallback, useMemo, useRef } from 'react';

import { ie11 } from '../Utils/detectBrowser';
import AccessibleInputText from '../Utils/AccessibleInputText';
import AutoResizeTextArea from './AutoResizeTextArea';
import navigableEvent from '../Utils/TypeFocusSink/navigableEvent';
import { ie11 } from '../Utils/detectBrowser';
import useRegisterFocusSendBox from '../hooks/internal/useRegisterFocusSendBox';
import useStyleToEmotionObject from '../hooks/internal/useStyleToEmotionObject';
import useScrollDown from '../hooks/useScrollDown';
import useScrollUp from '../hooks/useScrollUp';
import useStyleSet from '../hooks/useStyleSet';
import useStyleToEmotionObject from '../hooks/internal/useStyleToEmotionObject';
import useSubmit from '../providers/internal/SendBox/useSubmit';
import withEmoji from '../withEmoji/withEmoji';
import AutoResizeTextArea from './AutoResizeTextArea';

import type { MutableRefObject } from 'react';

Expand Down Expand Up @@ -163,8 +163,9 @@ const TextBox = ({ className }) => {
[scrollDown, scrollUp]
);

const focusCallback = useCallback<(options?: { noKeyboard?: boolean }) => void>(
({ noKeyboard } = {}) => {
const focusCallback = useCallback<Parameters<typeof useRegisterFocusSendBox>[0]>(
options => {
const { noKeyboard } = options;
const { current } = inputElementRef;

if (current) {
Expand All @@ -178,17 +179,19 @@ const TextBox = ({ className }) => {

current.setAttribute('readonly', 'readonly');

// 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`.
setTimeout(() => {
const { current } = inputElementRef;

if (current) {
current.focus();
readOnly ? current.setAttribute('readonly', readOnly) : current.removeAttribute('readonly');
}
}, 0);
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();
}
Expand Down
5 changes: 0 additions & 5 deletions packages/component/src/hooks/internal/WebChatUIContext.js

This file was deleted.

13 changes: 13 additions & 0 deletions packages/component/src/hooks/internal/WebChatUIContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createContext, type MutableRefObject } from 'react';

import { type FocusSendBoxInit } from '../../types/internal/FocusSendBoxInit';
import { type FocusTranscriptInit } from '../../types/internal/FocusTranscriptInit';

export type ContextType = {
focusSendBoxCallbacksRef: MutableRefObject<((init: FocusSendBoxInit) => Promise<void>)[]>;
focusTranscriptCallbacksRef: MutableRefObject<((init: FocusTranscriptInit) => Promise<void>)[]>;
};

const context = createContext<ContextType>(undefined as ContextType);

export default context;
Loading
Loading