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

Fluent: suggested actions roving focus #5154

Merged
merged 4 commits into from
May 1, 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Added an information message to the telephone keypad, in PR [#5140](https://github.com/microsoft/BotFramework-WebChat/pull/5140)
- Added animation to focus indicator and pixel-perfected, in PR [#5143](https://github.com/microsoft/BotFramework-WebChat/pull/5143)
- Integrated focus management for send box, in PR [#5150](https://github.com/microsoft/BotFramework-WebChat/pull/5150), by [@OEvgeny](https://github.com/OEvgeny)
- Added keyboard navigation support into suggested actions, in PR [#5154](https://github.com/microsoft/BotFramework-WebChat/pull/5154), by [@OEvgeny](https://github.com/OEvgeny)
- (Experimental) Added `<LocalizeString />` component which can be used to localize strings, by [@OEvgeny](https://github.com/OEvgeny) in PR [#5140](https://github.com/microsoft/BotFramework-WebChat/pull/5140)
- Added `<ThemeProvider>` component to apply theme pack to Web Chat, by [@compulim](https://github.com/compulim), in PR [#5120](https://github.com/microsoft/BotFramework-WebChat/pull/5120)
- Added `useMakeThumbnail` hook option to create a thumbnail from the file given, by [@compulim](https://github.com/compulim), in PR [#5123](https://github.com/microsoft/BotFramework-WebChat/pull/5123) and [#5122](https://github.com/microsoft/BotFramework-WebChat/pull/5122)
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
156 changes: 156 additions & 0 deletions __tests__/html/fluentTheme/suggestedActions.focus.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<!doctype html>
<html lang="en-US">

<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
<script crossorigin="anonymous" src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/react@16.8.6/umd/react.production.min.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/react-dom@16.8.6/umd/react-dom.production.min.js"></script>
<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 crossorigin="anonymous" src="/__dist__/botframework-webchat-fluent-theme.production.min.js"></script>
</head>

<body>
<main id="webchat"></main>
<script type="text/babel">
run(async function () {
const {
React,
ReactDOM: { render },
WebChat: { FluentThemeProvider, ReactWebChat }
} = window; // Imports in UMD fashion.

const { directLine, store } = testHelpers.createDirectLineEmulator();

const App = () => <ReactWebChat directLine={directLine} store={store} />;

render(
<FluentThemeProvider>
<App />
</FluentThemeProvider>,
document.getElementById('webchat')
);

await pageConditions.uiConnected();

await directLine.emulateIncomingActivity({
type: 'message',
textFormat: 'plain',
text: 'Please select one of the actions below',
suggestedActions: {
actions: [
{
image: `https://raw.githubusercontent.com/compulim/BotFramework-MockBot/master/public/assets/square-icon.png`,
imageAltText: 'a blue square',
title: 'IM back as string',
type: 'imBack',
value: 'postback imback-string'
},
{
image: `https://raw.githubusercontent.com/compulim/BotFramework-MockBot/master/public/assets/square-icon-red.png`,
imageAltText: 'a red square',
title: 'Post back as string',
type: 'postBack',
value: 'postback postback-string'
},
{
image: `https://raw.githubusercontent.com/compulim/BotFramework-MockBot/master/public/assets/square-icon-green.png`,
imageAltText: 'a green square',
title: 'Post back as JSON',
text: 'Some text',
type: 'postBack',
value: {
hello: 'World!'
}
},
{
image: `https://raw.githubusercontent.com/compulim/BotFramework-MockBot/master/public/assets/square-icon-purple.png`,
imageAltText: 'a purple square',
displayText: 'say Hello World!',
title: 'Message back as JSON with display text',
text: 'Some text',
type: 'messageBack',
value: {
hello: 'World!'
}
},
{
image: `https://raw.githubusercontent.com/compulim/BotFramework-MockBot/master/public/assets/square-icon-purple.png`,
imageAltText: 'a purple square',
title: 'Message back as JSON without display text',
type: 'messageBack',
value: {
hello: 'World!'
}
},
{
displayText: 'Aloha',
image: `https://raw.githubusercontent.com/compulim/BotFramework-MockBot/master/public/assets/square-icon-purple.png`,
imageAltText: 'a purple square',
text: 'echo Hello',
title: 'Aloha',
type: 'messageBack'
}
],
to: []
}
});

document.querySelector(`[data-testid="${WebChat.testIds.sendBoxTextBox}"]`).focus();

// WHEN: Focus suggested actions
await host.sendShiftTab();

// THEN: Should focus first suggested action
await host.snapshot();
const firstAction = document.activeElement;

// WHEN: Press arrow right key four times:
await host.sendKeys('ARROW_RIGHT'); // 2nd
await host.sendKeys('ARROW_RIGHT'); // 3rd
await host.sendKeys('ARROW_RIGHT'); // 4th
await host.sendKeys('ARROW_RIGHT'); // 5th
await host.sendKeys('ARROW_RIGHT'); // 6th

// THEN: Should focus the last suggested action
expect(document.activeElement?.innerText).toContain('Aloha');
const lastAction = document.activeElement;
await host.snapshot();

// WHEN: escape key is pressed
await host.sendKeys('ESCAPE');

// THEN: Should focus sendbox
expect(document.activeElement).toBe(
document.querySelector(`[data-testid="${WebChat.testIds.sendBoxTextBox}"]`)
);
await host.snapshot();

// WHEN: Focus suggested actions
await host.sendShiftTab();

// THEN: Should focus the last suggested action
expect(document.activeElement).toBe(lastAction);

// WHEN: Press arrow right again
await host.sendKeys('ARROW_RIGHT');

// THEN: Should wrap around to the first action
expect(document.activeElement).toBe(firstAction);

// WHEN: Press arrow left and space keys
await host.sendKeys('ARROW_LEFT');
await (await directLine.actPostActivity(() => host.sendKeys(' '))).resolveAll();

// THEN: Should wrap around, send last action and focus sendbox
expect(document.activeElement).toBe(
document.querySelector(`[data-testid="${WebChat.testIds.sendBoxTextBox}"]`)
);
await host.snapshot();
});
</script>
</body>

</html>
5 changes: 5 additions & 0 deletions __tests__/html/fluentTheme/suggestedActions.focus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */

describe('Fluent theme applied', () => {
test('suggested actions roving focus', () => runHTML('fluentTheme/suggestedActions.focus'));
});
100 changes: 100 additions & 0 deletions __tests__/html/fluentTheme/suggestedActions.layout.flow.focus.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<!doctype html>
<html lang="en-US">

<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
<script crossorigin="anonymous" src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/react@16.8.6/umd/react.production.min.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/react-dom@16.8.6/umd/react-dom.production.min.js"></script>
<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 crossorigin="anonymous" src="/__dist__/botframework-webchat-fluent-theme.production.min.js"></script>
</head>

<body>
<main id="webchat"></main>
<script type="text/babel">
run(async function () {
const {
React,
ReactDOM: { render },
WebChat: { FluentThemeProvider, ReactWebChat }
} = window; // Imports in UMD fashion.

const { directLine, store } = testHelpers.createDirectLineEmulator();

const App = () => (
<ReactWebChat directLine={directLine} store={store} styleOptions={{ suggestedActionLayout: 'flow' }} />
);

render(
<FluentThemeProvider>
<App />
</FluentThemeProvider>,
document.getElementById('webchat')
);

await pageConditions.uiConnected();

await directLine.emulateIncomingActivity({
type: 'message',
textFormat: 'plain',
text: 'Please select one of the actions below',
suggestedActions: {
actions: [
{ title: 'One', value: 'One', type: 'imBack' },
{ title: 'Two', value: 'Two', type: 'imBack' },
{ title: 'Three', value: 'Three', type: 'imBack' },
{ title: 'Four', value: 'Four', type: 'imBack' },
{ title: 'Five', value: 'Five', type: 'imBack' }
],
to: []
}
});

document.querySelector(`[data-testid="${WebChat.testIds.sendBoxTextBox}"]`).focus();

// WHEN: Focus suggested actions
await host.sendShiftTab();

// THEN: Should focus first suggested action
await host.snapshot();
const firstAction = document.activeElement;

// WHEN: Press arrow right key four times:
await host.sendKeys('ARROW_RIGHT'); // 2nd
await host.sendKeys('ARROW_RIGHT'); // 3rd
await host.sendKeys('ARROW_RIGHT'); // 4th
await host.sendKeys('ARROW_RIGHT'); // 5th

// THEN: Should focus the last suggested action
expect(document.activeElement?.innerText).toContain('Five');
await host.snapshot();

// WHEN: Press arrow right again
await host.sendKeys('ARROW_RIGHT');

// THEN: Should wrap around to the first action
expect(document.activeElement).toBe(firstAction);
await host.snapshot();

// WHEN: Press arrow left key
await host.sendKeys('ARROW_LEFT');

// THEN: Should wrap around to the last action
expect(document.activeElement?.innerText).toContain('Five');

// WHEN: Press the space key
await (await directLine.actPostActivity(() => host.sendKeys(' '))).resolveAll();

// THEN: Should send last action and focus sendbox
expect(document.activeElement).toBe(
document.querySelector(`[data-testid="${WebChat.testIds.sendBoxTextBox}"]`)
);
await host.snapshot();
});
</script>
</body>

</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */

describe('Fluent theme applied', () => {
test('suggested actions roving focus in flow layout', () => runHTML('fluentTheme/suggestedActions.layout.flow.focus'));
});
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { hooks } from 'botframework-webchat-component';
import { type DirectLineCardAction } from 'botframework-webchat-core';
import cx from 'classnames';
import React, { MouseEventHandler, memo, useCallback, useRef } from 'react';
import React, { MouseEventHandler, memo, useCallback } from 'react';
OEvgeny marked this conversation as resolved.
Show resolved Hide resolved
import styles from './SuggestedAction.module.css';
import { useStyles } from '../../styles';
import AccessibleButton from './AccessibleButton';
import { useRovingFocusItemRef } from './private/rovingFocus';

const { useDisabled, useFocus, usePerformCardAction, useScrollToEnd, useStyleSet, useSuggestedActions } = hooks;

Expand Down Expand Up @@ -36,6 +37,7 @@ function SuggestedAction({
displayText,
image,
imageAlt,
itemIndex,
text,
type,
value
Expand All @@ -44,7 +46,7 @@ function SuggestedAction({
const [{ suggestedAction: suggestedActionStyleSet }] = useStyleSet();
const [disabled] = useDisabled();
const focus = useFocus();
const focusRef = useRef<HTMLButtonElement>(null);
const focusRef = useRovingFocusItemRef<HTMLButtonElement>(itemIndex);
const performCardAction = usePerformCardAction();
const classNames = useStyles(styles);
const scrollToEnd = useScrollToEnd();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { hooks } from 'botframework-webchat-component';
import cx from 'classnames';
import React, { memo, type ReactNode } from 'react';
import React, { memo, useCallback, type ReactNode } from 'react';
import SuggestedAction from './SuggestedAction';
import computeSuggestedActionText from './private/computeSuggestedActionText';
import styles from './SuggestedActions.module.css';
import { useStyles } from '../../styles';
import RovingFocusProvider from './private/rovingFocus';

const { useLocalizer, useStyleOptions, useStyleSet, useSuggestedActions } = hooks;
const { useFocus, useLocalizer, useStyleOptions, useStyleSet, useSuggestedActions } = hooks;

function SuggestedActionStackedOrFlowContainer(
props: Readonly<{
Expand Down Expand Up @@ -44,6 +45,12 @@ function SuggestedActions() {
const classNames = useStyles(styles);
const localize = useLocalizer();
const [suggestedActions] = useSuggestedActions();
const focus = useFocus();

const handleEscapeKey = useCallback(() => {
focus('sendBox');
}, [focus]);

const children = suggestedActions.map((cardAction, index) => {
const { displayText, image, imageAltText, text, type, value } = cardAction as {
displayText?: string;
Expand Down Expand Up @@ -85,13 +92,16 @@ function SuggestedActions() {
/>
);
});

return (
<SuggestedActionStackedOrFlowContainer
aria-label={localize('SUGGESTED_ACTIONS_LABEL_ALT')}
className={classNames['suggested-actions']}
>
{children}
</SuggestedActionStackedOrFlowContainer>
<RovingFocusProvider onEscapeKey={handleEscapeKey}>
<SuggestedActionStackedOrFlowContainer
aria-label={localize('SUGGESTED_ACTIONS_LABEL_ALT')}
className={classNames['suggested-actions']}
>
{children}
</SuggestedActionStackedOrFlowContainer>
</RovingFocusProvider>
);
}

Expand Down
Loading
Loading