Skip to content

Commit

Permalink
Fix Narrator reading out attachments and timestamps in transcript (#2226
Browse files Browse the repository at this point in the history
)

* Fix README sample link

* Update Legacy lifecycle methods to UNSAFE_

* fix #2193: Attachments not spoken by Narrator

* Add timestamp back to aria-labels

* fixes

* PR fixes

* Apply PR fixes
  • Loading branch information
corinagum authored Jul 29, 2019
1 parent a98f074 commit f5b8db5
Show file tree
Hide file tree
Showing 24 changed files with 169 additions and 87 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

- Fix [#2160](https://github.com/microsoft/BotFramework-WebChat/issues/2160). Clear suggested actions after clicking on a suggested actions of type `openUrl`, by [@tdurnford](https://github.com/tdurnford) in PR [#2190](https://github.com/microsoft/BotFramework-WebChat/pull/2190)
- Fix [#2187](https://github.com/microsoft/BotFramework-WebChat/issues/2187). Bump core-js and update core-js modules on index-es5.js, by [@corinagum](https://github.com/corinagum) in PR [#2195](https://github.com/microsoft/BotFramework-WebChat/pull/2195)
- Fix [#1954](https://github.com/microsoft/BotFramework-WebChat/issues/1954). Estimate clock skew and adjust timestamp for outgoing activity, by [@compulim](https://github.com/compulim) in PR [#2208](https://github.com/microsoft/BotFramework-WebChat/pull/2208) and [#2230](https://github.com/microsoft/BotFramework-WebChat/pull/2230)
- Fix [#1954](https://github.com/microsoft/BotFramework-WebChat/issues/1954). Estimate clock skew and adjust timestamp for outgoing activity, by [@compulim](https://github.com/compulim) in PR [#2208](https://github.com/microsoft/BotFramework-WebChat/pull/2208)
- Fix [#2193](https://github.com/microsoft/BotFramework-WebChat/issues/2193). Fix Adaptive Card/attachments do not get read by Narrator, by [@corinagum](https://github.com/corinagum) in PR [#2226](https://github.com/microsoft/BotFramework-WebChat/pull/2226)
- Fix [#2150](https://github.com/microsoft/BotFramework-WebChat/issues/2193). Fix timestamps read by Narrator, by [@corinagum](https://github.com/corinagum) in PR [#2226](https://github.com/microsoft/BotFramework-WebChat/pull/2226)


### Added

Expand Down
2 changes: 1 addition & 1 deletion packages/component/src/Activity/Bubble.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const Bubble = ({ 'aria-hidden': ariaHidden, children, className, fromUser, nub,
);

Bubble.defaultProps = {
'aria-hidden': true,
'aria-hidden': false,
children: undefined,
className: '',
fromUser: false,
Expand Down
4 changes: 2 additions & 2 deletions packages/component/src/Activity/CarouselFilmStrip.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ const WebChatCarouselFilmStrip = ({
<ul className={classNames({ webchat__carousel__item_indented: indented })} ref={itemContainerRef}>
{attachments.map((attachment, index) => (
<li key={index}>
<Bubble fromUser={fromUser} key={index} nub={false}>
<Bubble aria-hidden={true} fromUser={fromUser} key={index} nub={false}>
{children({ attachment })}
</Bubble>
</li>
Expand All @@ -140,7 +140,7 @@ const WebChatCarouselFilmStrip = ({
{state === SENDING || state === SEND_FAILED ? (
<SendStatus activity={activity} />
) : (
<Timestamp activity={activity} aria-hidden={true} className={timestampClassName} />
<Timestamp activity={activity} className={timestampClassName} />
)}
</div>
</div>
Expand Down
44 changes: 25 additions & 19 deletions packages/component/src/Activity/SendStatus.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { Constants } from 'botframework-webchat-core';
import PropTypes from 'prop-types';
import React from 'react';

import { localize } from '../Localization/Localize';
import connectToWebChat from '../connectToWebChat';
import Localize, { localize } from '../Localization/Localize';

const {
ActivityClientState: { SEND_FAILED, SENDING }
Expand All @@ -29,31 +29,37 @@ const connectSendStatus = (...selectors) =>
const SendStatus = ({ activity: { channelData: { state } = {} }, language, retrySend, styleSet }) => {
// TODO: [P4] Currently, this is the only place which use a templated string
// We could refactor this into a general component if there are more templated strings
const localizedSending = localize('Sending', language);
const localizedSendStatus = localize('SendStatus', language);
const sendFailedText = localize('SEND_FAILED_KEY', language);
const sendFailedRetryMatch = /\{Retry\}/u.exec(sendFailedText);

return (
<span className={styleSet.sendStatus}>
{state === SENDING ? (
<Localize text="Sending" />
) : state === SEND_FAILED ? (
sendFailedRetryMatch ? (
<React.Fragment>
{sendFailedText.substr(0, sendFailedRetryMatch.index)}
<React.Fragment>
{/* Because of differences in browser implementations, <span aria-label> is used to make the screen reader perform the same on different browsers in Edge v44 */}
<span aria-label={localizedSendStatus + localizedSending} />
<span aria-hidden={true} className={styleSet.sendStatus}>
{state === SENDING ? (
localizedSending
) : state === SEND_FAILED ? (
sendFailedRetryMatch ? (
<React.Fragment>
{sendFailedText.substr(0, sendFailedRetryMatch.index)}
<button onClick={retrySend} type="button">
{localize('Retry', language)}
</button>
{sendFailedText.substr(sendFailedRetryMatch.index + sendFailedRetryMatch[0].length)}
</React.Fragment>
) : (
<button onClick={retrySend} type="button">
{localize('Retry', language)}
{sendFailedText}
</button>
{sendFailedText.substr(sendFailedRetryMatch.index + sendFailedRetryMatch[0].length)}
</React.Fragment>
)
) : (
<button onClick={retrySend} type="button">
{sendFailedText}
</button>
)
) : (
false
)}
</span>
false
)}
</span>
</React.Fragment>
);
};

Expand Down
12 changes: 9 additions & 3 deletions packages/component/src/Activity/StackedLayout.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,9 @@ const StackedLayout = ({ activity, avatarInitials, children, language, styleSet,
</div>
) : (
!!activityDisplayText && (
<div aria-label={ariaLabel} className="webchat__row message">
<div className="webchat__row message">
{/* Because of differences in browser implementations, <span aria-label> is used to make the screen reader perform the same on different browsers in Edge v44 */}
<span aria-label={ariaLabel} />
<Bubble aria-hidden={true} className="bubble" fromUser={fromUser} nub={true}>
{children({
activity,
Expand All @@ -145,12 +147,16 @@ const StackedLayout = ({ activity, avatarInitials, children, language, styleSet,
</div>
)
)}
{/* Because of differences in browser implementations, aria-label=" " is used to make the screen reader not repeat the same text multiple times in Chrome v75 */}
{attachments.map((attachment, index) => (
<div
aria-label=" "
className={classNames('webchat__row attachment', { webchat__stacked_item_indented: indented })}
key={index}
>
<Bubble aria-hidden={true} className="attachment bubble" fromUser={fromUser} key={index} nub={false}>
{/* Because of differences in browser implementations, <span aria-label> is used to make the screen reader perform the same on different browsers in Edge v44 */}
<span aria-label={fromUser ? localize('UserSent', language) : localize('BotSent', language)} />
<Bubble className="attachment bubble" fromUser={fromUser} key={index} nub={false}>
{children({ attachment })}
</Bubble>
</div>
Expand All @@ -159,7 +165,7 @@ const StackedLayout = ({ activity, avatarInitials, children, language, styleSet,
{showSendStatus ? (
<SendStatus activity={activity} className="timestamp" />
) : (
<Timestamp activity={activity} aria-hidden={true} className={classNames('timestamp', timestampClassName)} />
<Timestamp activity={activity} className={classNames('timestamp', timestampClassName)} />
)}
<div className="filler" />
</div>
Expand Down
6 changes: 2 additions & 4 deletions packages/component/src/Activity/Timestamp.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,20 @@ import React from 'react';
import connectToWebChat from '../connectToWebChat';
import TimeAgo from '../Utils/TimeAgo';

const Timestamp = ({ activity: { timestamp }, 'aria-hidden': ariaHidden, className, styleSet }) => (
<span aria-hidden={ariaHidden} className={classNames(styleSet.timestamp + '', className + '')}>
const Timestamp = ({ activity: { timestamp }, className, styleSet }) => (
<span className={classNames(styleSet.timestamp + '', (className || '') + '')}>
<TimeAgo value={timestamp} />
</span>
);

Timestamp.defaultProps = {
'aria-hidden': true,
className: ''
};

Timestamp.propTypes = {
activity: PropTypes.shape({
timestamp: PropTypes.string.isRequired
}).isRequired,
'aria-hidden': PropTypes.bool,
className: PropTypes.string,
styleSet: PropTypes.shape({
timestamp: PropTypes.any.isRequired
Expand Down
12 changes: 2 additions & 10 deletions packages/component/src/Attachment/Assets/DownloadIcon.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,19 @@ import React from 'react';

const ICON_SIZE_FACTOR = 22;

const DownloadIcon = ({ className, label, size }) => (
<svg
aria-label={label}
className={className}
height={ICON_SIZE_FACTOR * size}
viewBox="0 0 31.8 46"
width={ICON_SIZE_FACTOR * size}
>
const DownloadIcon = ({ className, size }) => (
<svg className={className} height={ICON_SIZE_FACTOR * size} viewBox="0 0 31.8 46" width={ICON_SIZE_FACTOR * size}>
<path d="M26.8,23.8l-10.9,11L5,23.8l1.6-1.6l8.2,8.3V5H17v25.5l8.2-8.3L26.8,23.8z M5.8,41v-2.2H26V41H5.8z" />
</svg>
);

DownloadIcon.defaultProps = {
className: '',
label: '',
size: 1
};

DownloadIcon.propTypes = {
className: PropTypes.string,
label: PropTypes.string,
size: PropTypes.number
};

Expand Down
8 changes: 7 additions & 1 deletion packages/component/src/Attachment/Assets/TypingAnimation.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@ import React from 'react';

import connectToWebChat from '../../connectToWebChat';

const TypingAnimation = ({ styleSet }) => <div className={styleSet.typingAnimation} />;
const TypingAnimation = ({ 'aria-label': ariaLabel, styleSet }) => (
<React.Fragment>
<span aria-label={ariaLabel} />
<div aria-hidden={true} className={styleSet.typingAnimation} />
</React.Fragment>
);

TypingAnimation.propTypes = {
'aria-label': PropTypes.string.isRequired,
styleSet: PropTypes.shape({
typingAnimation: PropTypes.any.isRequired
}).isRequired
Expand Down
14 changes: 12 additions & 2 deletions packages/component/src/Attachment/AudioContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,27 @@ import React from 'react';

import connectToWebChat from '../connectToWebChat';

const AudioContent = ({ autoPlay, loop, poster, src, styleSet }) => (
<audio autoPlay={autoPlay} className={styleSet.audioContent} controls={true} loop={loop} poster={poster} src={src} />
const AudioContent = ({ alt, autoPlay, loop, poster, src, styleSet }) => (
<audio
aria-label={alt}
autoPlay={autoPlay}
className={styleSet.audioContent}
controls={true}
loop={loop}
poster={poster}
src={src}
/>
);

AudioContent.defaultProps = {
alt: '',
autoPlay: false,
loop: false,
poster: ''
};

AudioContent.propTypes = {
alt: PropTypes.string,
autoPlay: PropTypes.bool,
loop: PropTypes.bool,
poster: PropTypes.string,
Expand Down
33 changes: 22 additions & 11 deletions packages/component/src/Attachment/DownloadAttachment.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,30 @@ const DownloadAttachment = ({
styleSet
}) => {
const attachmentIndex = attachments.indexOf(attachment);
const label = localize('Download file', language);
const downloadLabel = localize('Download file', language);
const size = attachmentSizes[attachmentIndex];

const formattedSize = typeof size === 'number' && format(size);
const downloadFileWithFileSizeLabel = localize(
'DownloadFileWithFileSize',
language,
downloadLabel,
attachment.name,
formattedSize
);
return (
<div className={styleSet.downloadAttachment}>
<a href={attachment.contentUrl} rel="noopener noreferrer" target="_blank">
<div className="details">
<div className="name">{attachment.name}</div>
{typeof size === 'number' && <div className="size">{format(size)}</div>}
</div>
<DownloadIcon className="icon" label={label} size={1.5} />
</a>
</div>
<React.Fragment>
{/* Because of differences in browser implementations, <span aria-label> is used to make the screen reader perform the same on different browsers in Edge v44 */}
<span aria-label={downloadFileWithFileSizeLabel} />
<div aria-hidden={true} className={styleSet.downloadAttachment}>
<a href={attachment.contentUrl} rel="noopener noreferrer" target="_blank">
<div className="details">
<div className="name">{attachment.name}</div>
<div className="size">{formattedSize}</div>
</div>
<DownloadIcon className="icon" size={1.5} />
</a>
</div>
</React.Fragment>
);
};

Expand Down
23 changes: 16 additions & 7 deletions packages/component/src/Attachment/TextContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,24 @@ import connectToWebChat from '../connectToWebChat';

const TextContent = ({ contentType, renderMarkdown, styleSet, text }) =>
contentType === 'text/markdown' && renderMarkdown ? (
<div
className={classNames('markdown', styleSet.textContent + '')}
dangerouslySetInnerHTML={{ __html: renderMarkdown(text || '') }}
/>
<React.Fragment>
{/* Because of differences in browser implementations, <span aria-label> is used to make the screen reader perform the same on different browsers in Edge v44 */}
<span aria-label={text} />
<div
aria-hidden={true}
className={classNames('markdown', styleSet.textContent + '')}
dangerouslySetInnerHTML={{ __html: renderMarkdown(text || '') }}
/>
</React.Fragment>
) : (
(text || '').split('\n').map((line, index) => (
<p className={classNames('plain', styleSet.textContent + '')} key={index}>
{line.trim()}
</p>
<React.Fragment key={index}>
{/* Because of differences in browser implementations, <span aria-label> is used to make the screen reader perform the same on different browsers in Edge v44 */}
<span aria-label={text} />
<p aria-hidden={true} className={classNames('plain', styleSet.textContent + '')}>
{line.trim()}
</p>
</React.Fragment>
))
);

Expand Down
6 changes: 4 additions & 2 deletions packages/component/src/Attachment/TypingActivity.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import PropTypes from 'prop-types';
import React from 'react';

import { localize } from '../Localization/Localize';
import connectToWebChat from '../connectToWebChat';
import TypingAnimation from './Assets/TypingAnimation';

const TypingActivity = ({ styleSet }) => (
const TypingActivity = ({ language, styleSet }) => (
<div className={styleSet.typingActivity}>
<TypingAnimation />
<TypingAnimation ariaLabel={localize('TypingIndicator', language)} />
</div>
);

TypingActivity.propTypes = {
language: PropTypes.string.isRequired,
styleSet: PropTypes.shape({
typingActivity: PropTypes.any.isRequired
}).isRequired
Expand Down
2 changes: 2 additions & 0 deletions packages/component/src/BasicTranscript.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ const BasicTranscript = ({
>
{activityElements.map(({ activity, element }, index) => (
<li
/* Because of differences in browser implementations, aria-label=" " is used to make the screen reader not repeat the same text multiple times in Chrome v75 */
aria-label=" "
className={classNames(styleSet.activity + '', {
// Hide timestamp if same timestamp group with the next activity
'hide-timestamp': sameTimestampGroup(
Expand Down
3 changes: 2 additions & 1 deletion packages/component/src/BasicWebChat.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint no-magic-numbers: ["error", { "ignore": [0, 1, 2] }] */
/* eslint react/no-unsafe: off */

import { css } from 'glamor';
import classNames from 'classnames';
Expand Down Expand Up @@ -80,7 +81,7 @@ export default class BasicWebChat extends React.Component {
}

// TODO: [P2] Move to React 16 APIs
componentWillReceiveProps({
UNSAFE_componentWillReceiveProps({
activityMiddleware: nextActivityMiddleware,
attachmentMiddleware: nextAttachmentMiddleware
}) {
Expand Down
4 changes: 3 additions & 1 deletion packages/component/src/Composer.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint react/no-unsafe: off */

import {
Composer as ScrollToBottomComposer,
FunctionContext as ScrollToBottomFunctionContext
Expand Down Expand Up @@ -205,7 +207,7 @@ class Composer extends React.Component {
};
}

componentWillMount() {
UNSAFE_componentWillMount() {
const { props } = this;
const { directLine, userID, username } = props;

Expand Down
Loading

0 comments on commit f5b8db5

Please sign in to comment.