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

Allow carousel's scrollable content to be tab-able with visual focus #3841

Merged
merged 13 commits into from
Apr 16, 2021
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# https://blog.github.com/2017-07-06-introducing-code-owners/

* @a-b-r-o-w-n @amal-khalaf @compulim @corinagum @cwhitten @srinaath @tdurnford @tonyanziano @beyackle
* @a-b-r-o-w-n @compulim @corinagum @cwhitten @srinaath @tdurnford @tonyanziano @beyackle
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Fixed
- Fixes [#3814](https://github.com/microsoft/BotFramework-WebChat/issues/3814). Allow carousel's scrollable content to be tabbable, by [@corinagum](https://github.com/corinagum) in PR [#3841](https://github.com/microsoft/BotFramework-WebChat/pull/3841)
- Fixes [#3834](https://github.com/microsoft/BotFramework-WebChat/issues/3834). Ensure carousel attachments are read by AT on tab focus, by [@corinagum](https://github.com/corinagum) in PR [#3841](https://github.com/microsoft/BotFramework-WebChat/pull/3841)

### Changed

- Bumped all dependencies to the latest versions and sample bumps, by [@compulim](https://github.com/compulim) in PR [#3831](https://github.com/microsoft/BotFramework-WebChat/pull/3831) and PR [#3846](https://github.com/microsoft/BotFramework-WebChat/pull/3846)
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ window.WebChat.renderWebChat(

## Visual focus changes to transcript in Web Chat 4.12.0

A new accessibility update has been added to Web Chat from PR [#3703](https://github.com/microsoft/BotFramework-WebChat/pull/3703). This change creates visual focus for the transcript (bold black border) and `aria-activedescendent` focused activity (black dashed border) by default.
A new accessibility update has been added to Web Chat from PR [#3703](https://github.com/microsoft/BotFramework-WebChat/pull/3703). This change creates visual focus for the transcript (bold black border) and `aria-activedescendent` focused activity (black dashed border) by default. Where applicable, `transcriptVisualKeyboardIndicator...` values will also be applied to carousel (`CarouselFilmStrip.js`) children. This is done in order to match current default focus styling for Adaptive Cards, which may be a child of a carousel.

To modify these styles, you can change the following props via `styleOptions`:

Expand All @@ -55,7 +55,7 @@ To modify these styles, you can change the following props via `styleOptions`:
transcriptVisualKeyboardIndicatorWidth: 2,
```

The above code shows the default values you will see on Web Chat.
The above code shows the default values you will see in Web Chat.

## API refactor into new package in Web Chat 4.11.0

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.
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
await pageObjects.wait(conditions.minNumActivitiesShown(2), timeouts.directLine);
await pageObjects.wait(conditions.scrollToBottomCompleted(), timeouts.directLine);

const attachmentRole = document.querySelector('.webchat__carousel-filmstrip__attachment').getAttribute('role');
const attachmentRole = document.querySelector('.webchat__carousel-filmstrip-attachment').getAttribute('role');

expect(attachmentRole).toBeTruthy();

Expand Down
48 changes: 48 additions & 0 deletions __tests__/html/carousel.navigation.tab.cardInput.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<script crossorigin="anonymous" src="/__dist__/testharness.js"></script>
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
<script crossorigin="anonymous" src="assets/transcript.navigation.js" type="text/babel"></script>
</head>
<body>
<div id="webchat"></div>
<script type="text/babel" data-presets="env,stage-3,react">
const {
TestAsset: { waitForFocusedActivityInView },
WebChatTest: { conditions, createDirectline, createStore, host, pageObjects, timeouts, token }
} = window;

(async function () {
window.WebChat.renderWebChat(
{
directLine: createDirectLine({ token: await token.fetchDirectLineToken() }),
store: createStore()
},
document.getElementById('webchat')
);

await pageObjects.wait(conditions.uiConnected(), timeouts.directLine);

await pageObjects.sendMessageViaSendBox('carousel', { waitForSend: true });

// Move focus to carousel activity
await host.sendShiftTab();
await host.sendShiftTab();
await host.sendShiftTab();
await waitForFocusedActivityInView();

await host.sendKeys('ENTER');
// focus on first card button
await host.sendTab();
await host.snapshot();

await host.done();
})().catch(async err => {
console.error(err);

await host.error(err);
});
</script>
</body>
</html>
9 changes: 9 additions & 0 deletions __tests__/html/carousel.navigation.tab.cardInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* @jest-environment ./__tests__/html/__jest__/WebChatEnvironment.js
*/

describe('carousel navigation', () => {
describe('should focus on card button when present', () => {
test('carousel', () => runHTMLTest('carousel.navigation.tab.cardInput.html'));
});
});
63 changes: 63 additions & 0 deletions __tests__/html/carousel.navigation.tab.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<script crossorigin="anonymous" src="/__dist__/testharness.js"></script>
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
<script crossorigin="anonymous" src="assets/transcript.navigation.js" type="text/babel"></script>
</head>
<body>
<div id="webchat"></div>
<script type="text/babel" data-presets="env,stage-3,react">
const {
TestAsset: { waitForFocusedActivityInView },
WebChatTest: { conditions, createDirectline, createStore, host, pageObjects, timeouts, token }
} = window;

(async function () {
window.WebChat.renderWebChat(
{
directLine: createDirectLine({ token: await token.fetchDirectLineToken() }),
store: createStore()
},
document.getElementById('webchat')
);

await pageObjects.wait(conditions.uiConnected(), timeouts.directLine);

await pageObjects.sendMessageViaSendBox('layout carousel', { waitForSend: true });

// Move focus to carousel activity
await host.sendShiftTab();
await host.sendShiftTab();
await host.sendShiftTab();
await waitForFocusedActivityInView();

await host.sendKeys('ENTER');
// first focus inside carousel
await host.snapshot();

// second focus inside carousel
await host.sendTab();
// Wait for carousel flippers to disappear.
await new Promise(resolve => setTimeout(resolve, 4000));
await host.snapshot();

// third focus inside carousel
await host.sendTab();
// Wait for carousel flippers to disappear.
await new Promise(resolve => setTimeout(resolve, 4000));
await host.snapshot();

// Press ESCAPE key should focus the activity.
await host.sendKeys('ESCAPE');
await host.snapshot();

await host.done();
})().catch(async err => {
console.error(err);

await host.error(err);
});
</script>
</body>
</html>
7 changes: 7 additions & 0 deletions __tests__/html/carousel.navigation.tab.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* @jest-environment ./__tests__/html/__jest__/WebChatEnvironment.js
*/

describe('carousel navigation', () => {
test('should show focus when tabbing inside carousel', () => runHTMLTest('carousel.navigation.tab.html'));
});
1 change: 0 additions & 1 deletion __tests__/html/focusManagement.sendFailedRetry.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
<body>
<div id="webchat"></div>
<script type="text/babel" data-presets="env,stage-3,react">
const { Simulate } = window.ReactTestUtils;
compulim marked this conversation as resolved.
Show resolved Hide resolved
const { conditions, createStore, elements, expect, host, pageObjects, timeouts, token } = window.WebChatTest;

(async function() {
Expand Down
2 changes: 1 addition & 1 deletion __tests__/html/transcript.activityGrouping.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ describe('transcript', () => {
test('with activity grouping test 46', () =>
runHTMLTest(
'transcript.activityGrouping#bi=1&bn=1&bt=1&g=status&l=carousel&rtl=0&t=markdown-message.json&ui=1&un=1&ut=1&w=1',
{ height: 1280, width: 720 }
{ height: 1280, ignoreConsoleError: true, width: 720 }
));

test('with activity grouping test 47', () =>
Expand Down
26 changes: 12 additions & 14 deletions packages/component/src/Activity/CarouselFilmStrip.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import PropTypes from 'prop-types';
import React from 'react';

import Bubble from './Bubble';
import CarouselFilmStripAttachment from './CarouselFilmStripAttachment';
import connectToWebChat from '../connectToWebChat';
import isZeroOrPositive from '../Utils/isZeroOrPositive';
import ScreenReaderText from '../ScreenReaderText';
Expand Down Expand Up @@ -35,7 +36,7 @@ const ROOT_STYLE = {
flexShrink: 0
},

'& .webchat__carousel-filmstrip__attachment': {
'& .webchat__carousel-filmstrip-attachment': {
flex: 1
},

Expand Down Expand Up @@ -144,7 +145,6 @@ const CarouselFilmStrip = ({
const activityDisplayText = messageBackDisplayText || text;
const fromUser = role === 'user';

const attachedAlt = localize(fromUser ? 'ACTIVITY_YOU_ATTACHED_ALT' : 'ACTIVITY_BOT_ATTACHED_ALT');
const greetingAlt = (fromUser
? localize('ACTIVITY_YOU_SAID_ALT')
: localize('ACTIVITY_BOT_SAID_ALT', botInitials || '')
Expand Down Expand Up @@ -219,20 +219,18 @@ const CarouselFilmStrip = ({
ref={itemContainerCallbackRef}
>
{attachments.map((attachment, index) => (
<li
aria-roledescription="attachment"
className="webchat__carousel-filmstrip__attachment react-film__filmstrip__item"
<CarouselFilmStripAttachment
activity={activity}
attachment={attachment}
fromUser={fromUser}
hasAvatar={hasAvatar}
index={index}
/* Attachments do not have an ID; it is always indexed by number */
/* eslint-disable-next-line react/no-array-index-key */
// eslint-disable-next-line react/no-array-index-key
key={index}
role="listitem"
>
<ScreenReaderText text={attachedAlt} />
{/* eslint-disable-next-line react/no-array-index-key */}
<Bubble fromUser={fromUser} key={index} nub={false}>
{renderAttachment({ activity, attachment })}
</Bubble>
</li>
renderAttachment={renderAttachment}
showAvatar={showAvatar}
/>
))}
</ul>
</div>
Expand Down
86 changes: 86 additions & 0 deletions packages/component/src/Activity/CarouselFilmStripAttachment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { hooks } from 'botframework-webchat-api';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';

import Bubble from './Bubble';
import ScreenReaderText from '../ScreenReaderText';
import useStyleSet from '../hooks/useStyleSet';

const { useDirection, useLocalizer } = hooks;

const CarouselFilmStripAttachment = ({
activity,
attachment,
className,
fromUser,
hasAvatar,
index,
renderAttachment,
showAvatar
}) => {
const [direction] = useDirection();
const localize = useLocalizer();
const [{ carouselFilmStripAttachment: carouselFilmStripAttachmentStyleSet }] = useStyleSet();

const attachedAlt = localize(fromUser ? 'ACTIVITY_YOU_ATTACHED_ALT' : 'ACTIVITY_BOT_ATTACHED_ALT');

return (
<li
aria-roledescription="attachment"
className={classNames(
'webchat__carousel-filmstrip-attachment',
{
'webchat__carousel-filmstrip-attachment--hide-avatar': hasAvatar && !showAvatar,
'webchat__carousel-filmstrip-attachment--rtl': direction === 'rtl',
'webchat__carousel-filmstrip-attachment--show-avatar': showAvatar
},
'react-film__filmstrip__item',
carouselFilmStripAttachmentStyleSet + '',
(className || '') + ''
)}
role="listitem"
tabIndex={0}
>
<ScreenReaderText text={attachedAlt} />
{/* eslint-disable-next-line react/no-array-index-key */}
<Bubble fromUser={fromUser} key={index} nub={false}>
{renderAttachment({ activity, attachment })}
<div className="webchat__carousel-filmstrip-attachment--focus" />
</Bubble>
</li>
);
};

CarouselFilmStripAttachment.defaultProps = {
className: ''
};

CarouselFilmStripAttachment.propTypes = {
activity: PropTypes.shape({
attachments: PropTypes.array,
channelData: PropTypes.shape({
messageBack: PropTypes.shape({
displayText: PropTypes.string
}),
state: PropTypes.string
}),
from: PropTypes.shape({
role: PropTypes.string.isRequired
}).isRequired,
text: PropTypes.string,
textFormat: PropTypes.string,
timestamp: PropTypes.string
}).isRequired,
attachment: PropTypes.shape({
content: PropTypes.any.isRequired
}).isRequired,
className: PropTypes.string,
fromUser: PropTypes.any.isRequired,
hasAvatar: PropTypes.any.isRequired,
index: PropTypes.number.isRequired,
renderAttachment: PropTypes.func.isRequired,
showAvatar: PropTypes.bool.isRequired
};

export default CarouselFilmStripAttachment;
20 changes: 0 additions & 20 deletions packages/component/src/Styles/StyleSet/CarouselFilmStrip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import mirrorStyle from '../mirrorStyle';
export default function CarouselFilmStrip({
avatarSize,
bubbleMaxWidth,
bubbleMinWidth,
paddingRegular,
transitionDuration
}: StrictStyleOptions) {
Expand All @@ -18,13 +17,6 @@ export default function CarouselFilmStrip({
marginBottom: -17
},

'& .webchat__carousel-filmstrip__attachment': {
minWidth: bubbleMinWidth,
maxWidth: bubbleMaxWidth,
transitionDuration,
transitionProperty: 'max-width, min-width'
},

'& .webchat__carousel-filmstrip__message': {
maxWidth: bubbleMaxWidth,
transitionDuration,
Expand Down Expand Up @@ -88,28 +80,16 @@ export default function CarouselFilmStrip({
marginLeft: -paddingRegular
},

'& .webchat__carousel-filmstrip__attachment': {
paddingLeft: paddingRegular
},

'&.webchat__carousel-filmstrip--hide-avatar, &.webchat__carousel-filmstrip--show-avatar': {
'& .webchat__carousel-filmstrip__attachments': {
marginLeft: -(avatarSize + paddingRegular * 2)
},

'& .webchat__carousel-filmstrip__attachment:first-child': {
paddingLeft: avatarSize + paddingRegular * 2
}
},

'&.webchat__carousel-filmstrip--hide-nub, &.webchat__carousel-filmstrip--show-nub': {
'&:not(.webchat__carousel-filmstrip--hide-avatar.webchat__carousel-filmstrip--show-avatar)': {
'& .webchat__carousel-filmstrip__attachments': {
marginLeft: -paddingRegular * 2
},

'& .webchat__carousel-filmstrip__attachment:first-child': {
paddingLeft: paddingRegular * 2
}
}
}
Expand Down
Loading