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: add telephone keypad information message #5140

Merged
merged 9 commits into from
Apr 19, 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Fixed to keep telephone keypad on-screen on click, in PR [#5132](https://github.com/microsoft/BotFramework-WebChat/pull/5132)
- Disabled send button and hid message length when telephone keypad is shown, in PR [#5136](https://github.com/microsoft/BotFramework-WebChat/pull/5136)
- Added dark theme support, in PR [#5138](https://github.com/microsoft/BotFramework-WebChat/pull/5138)
- Added an information message to the telephone keypad, in PR [#5140](https://github.com/microsoft/BotFramework-WebChat/pull/5140)
- (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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<!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/[email protected]/umd/react.production.min.js"></script>
<script crossorigin="anonymous" src="https://unpkg.com/[email protected]/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={{ hideUploadButton: true, hideTelephoneKeypadButton: false }}
overrideLocalizedStrings={{
TELEPHONE_KEYPAD_INPUT_MESSAGE: 'Only supports single-digit input. [Learn more](https://aka.ms/ms)'
}}
/>
);

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

await pageConditions.uiConnected();

await directLine.emulateIncomingActivity(
'Eiusmod anim adipisicing cupidatat adipisicing officia sint qui consequat veniam id aute.'
);

await pageConditions.numActivitiesShown(1);

// WHEN: Telephone keypad button is clicked.
await host.click(
document.querySelector(`[data-testid="${WebChat.testIds.sendBoxTelephoneKeypadToolbarButton}"]`)
);

// THEN: Should show the telephone keypad with custom link
await host.snapshot();

// WHEN: The link is focused
document.querySelector(`[data-testid="${WebChat.testIds.sendBoxTelephoneKeypadToolbarButton}"]`).focus();
await host.sendShiftTab();

// THEN: Should display a focused state
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('telephone keypad should show custom message', () => runHTML('fluentTheme/telephoneKeypad.customInputMessage'));
});
4 changes: 3 additions & 1 deletion docs/LOCALIZATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,9 @@ function convertFromOldStringId(js) {
TEXT_INPUT_UPLOAD_BUTTON_ALT: js['Upload file'],
TEXT_INPUT_TELEPHON_KEYPAD_BUTTON_ALT: js['Telephone keypad'],
TEXT_INPUT_DROP_ZONE: js['Drop files'],
TEXT_INPUT_LENGTH_EXCEEDED_ALT: js['Message lengths exceeded'],
TEXT_INPUT_LENGTH_EXCEEDED_ALT: js['Message length exceeded'],

TELEPHONE_KEYPAD_INPUT_MESSAGE: js['Only supports single-digit input'],

TEXT_INPUT_ATTACHMENTS_FEW: js['attachments'],
TEXT_INPUT_ATTACHMENTS_MANY: js['attachments'],
Expand Down
3 changes: 2 additions & 1 deletion packages/api/src/localization/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@
"TEXT_INPUT_UPLOAD_BUTTON_ALT": "Upload file",
"TEXT_INPUT_TELEPHONE_KEYPAD_BUTTON_ALT": "Telephone keypad",
"TEXT_INPUT_DROP_ZONE": "Drop files",
"TEXT_INPUT_LENGTH_EXCEEDED_ALT": "Message lengths exceeded",
"TEXT_INPUT_LENGTH_EXCEEDED_ALT": "Message length exceeded",
"TELEPHONE_KEYPAD_INPUT_MESSAGE": "Only supports single-digit input",
"TEXT_INPUT_ATTACHMENTS_FEW": "$1 attachments",
"_TEXT_INPUT_ATTACHMENTS_FEW.comment": "$1 is the number of attachments. This is for plural rule of \"few\".",
"TEXT_INPUT_ATTACHMENTS_MANY": "$1 attachments",
Expand Down
143 changes: 143 additions & 0 deletions packages/component/src/Utils/LocalizedString.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/* eslint react/no-danger: "off" */

import { hooks } from 'botframework-webchat-api';
import { onErrorResumeNext } from 'botframework-webchat-core';
import MarkdownIt from 'markdown-it';
import React, { memo, useMemo } from 'react';
import betterLinks, { type BetterLinkEnv, type LinkOptions } from './betterLinks';

const allowedSchemes = ['data', 'http', 'https', 'ftp', 'mailto', 'sip', 'tel'];

const linkDefinitions = [];

const externalLinkAlt = '';

const defaultDecorateLink = (href: string, textContent: string, linkOptions?: LinkOptions): LinkOptions | undefined => {
const decoration: LinkOptions = {
rel: 'noopener noreferrer',
target: '_blank',
wrapZeroWidthSpace: true,
...linkOptions
};

const ariaLabelSegments: string[] = [textContent];
const classes: Set<string> = new Set();
const linkDefinition = linkDefinitions.find(({ url }) => url === href);
const protocol = onErrorResumeNext(() => new URL(href).protocol);

if (linkDefinition) {
ariaLabelSegments.push(
linkDefinition.title || onErrorResumeNext(() => new URL(linkDefinition.url).host) || linkDefinition.url
);

// linkDefinition.identifier is uppercase, while linkDefinition.label is as-is.
linkDefinition.label === textContent && classes.add('webchat__render-markdown__pure-identifier');
}

// For links that would be sanitized out, let's turn them into a button so we could handle them later.
if (!allowedSchemes.map(scheme => `${scheme}:`).includes(protocol)) {
decoration.asButton ??= true;

classes.add('webchat__render-markdown__citation');
} else if (protocol === 'http:' || protocol === 'https:') {
decoration.iconClassName = [decoration.iconClassName, 'webchat__render-markdown__external-link-icon']
.filter((className: string | undefined) => className)
.join(' ');

ariaLabelSegments.push(externalLinkAlt);
}

// The first segment is textContent. Putting textContent is aria-label is useless.
if (ariaLabelSegments.length > 1) {
// If "aria-label" is already applied, do not overwrite it.
decoration.ariaLabel ??= (value: string) => value || ariaLabelSegments.join(' ');
}

if (typeof linkOptions?.className === 'string') {
classes.add(linkOptions.className);
}

// Resolve className
const classNamesString = Array.from(classes).join(' ');
if (linkOptions?.className && linkOptions?.className instanceof Function) {
decoration.className = linkOptions.className(classNamesString);
} else {
decoration.className = classNamesString;
}

// By default, Markdown-It will set "title" to the link title in link definition.

// However, "title" may be narrated by screen reader:
// - Edge
// - <a> will narrate "aria-label" but not "title"
// - <button> will narrate both "aria-label" and "title"
// - NVDA
// - <a> will narrate both "aria-label" and "title"
// - <button> will narrate both "aria-label" and "title"

// Title makes it very difficult to control narrations by the screen reader. Thus, we are disabling it in favor of "aria-label".
// This will not affect our accessibility compliance but UX. We could use a non-native tooltip or other forms of visual hint.

decoration.title ??= false;

return decoration;
};

const { useLocalizer } = hooks;

type Plural = {
zero?: string;
one?: string;
two?: string;
few?: string;
many?: string;
other: string;
};

const markdownIt = new MarkdownIt().use(betterLinks);

type PluralProps = Readonly<{
stringIds: Plural;
values: readonly [number, ...(number | string)[]] | undefined;
}>;

type SingularProps = Readonly<{
stringIds: string;
values?: readonly (number | string)[] | undefined;
}>;

type Props = Readonly<{
className?: string | undefined;
linkClassName?: string | undefined;
onDecorateLink?: ((href: string, textContent: string) => LinkOptions | undefined) | undefined;
}> &
(SingularProps | PluralProps);

function isPlural(props: Props): props is PluralProps {
return typeof props.stringIds !== 'string';
}

const LocalizedString = (props: Props) => {
const { className, linkClassName, onDecorateLink = defaultDecorateLink, stringIds, values } = props;
const localize = useLocalizer(isPlural(props) && { plural: true });
const env = useMemo<BetterLinkEnv>(
() => ({
linkOptions: {
className: linkClassName
},
decorateLink: onDecorateLink
}),
[linkClassName, onDecorateLink]
);

const html = useMemo(
() => ({
__html: markdownIt.renderer.render(markdownIt.parseInline(localize(stringIds, ...(values ?? [])), env), env)
}),
[env, localize, stringIds, values]
);

return <span className={className} dangerouslySetInnerHTML={html} />;
};

export default memo(LocalizedString);
Loading
Loading