Skip to content

Commit

Permalink
Add React hooks for customization (part 3) (#2542)
Browse files Browse the repository at this point in the history
* Typo

* Update entry

* Add useActivities, useReferenceGrammarID and useSendBoxDictationStarted

* Add useStyleOptions and useStyleSet

* Fix errors

* Use useStyleOptions and useStyleSet

* Add useStyleOptions and useStyleSet

* Add useStyleOptions and useStyleSet

* Add useLanguage and useLocalize

* Fix ESLint

* Typo

* Add useLocalizeDate

* Use useLocalize

* Fix ESLint

* Cleanup

* Clean up

* Fix useLocalize

* Revert dev build

* Update setter test

* Add test

* Better error message on runHook

* Apply PR comments

* Remove useLocalizeDate test

* Improve reliability
  • Loading branch information
compulim authored Nov 12, 2019
1 parent 09a63ee commit ca2107c
Show file tree
Hide file tree
Showing 33 changed files with 296 additions and 205 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Resolves [#2539](https://github.com/Microsoft/BotFramework-WebChat/issues/2539), added React hooks for customziation, by [@compulim](https://github.com/compulim) and [@corinagum](https://github.com/corinagum), in the following PRs:
- PR [#2540](https://github.com/microsoft/BotFramework-WebChat/pull/2540): `useActivities`, `useReferenceGrammarID`, `useSendBoxDictationStarted`
- PR [#2541](https://github.com/microsoft/BotFramework-WebChat/pull/2541): `useStyleOptions`, `useStyleSet`
- PR [#2542](https://github.com/microsoft/BotFramework-WebChat/pull/2542): `useLanguage`, `useLocalize`, `useLocalizeDate`

### Fixed

Expand Down
5 changes: 3 additions & 2 deletions __tests__/hooks/useActivity.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ test('should return list of activities', async () => {
`);
});

test('setter should throw exception', async () => {
test('setter should be falsy', async () => {
const { pageObjects } = await setupWebDriver();
const [_, setActivities] = await pageObjects.runHook('useActivities');

await expect(pageObjects.runHook('useActivities', [], result => result[1]())).rejects.toThrow();
expect(setActivities).toBeFalsy();
});
33 changes: 33 additions & 0 deletions __tests__/hooks/useLanguage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { timeouts } from '../constants.json';

// selenium-webdriver API doc:
// https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebDriver.html

jest.setTimeout(timeouts.test);

test('getter should return language set in props', async () => {
const { pageObjects } = await setupWebDriver({
props: {
locale: 'zh-YUE'
}
});

const [groupTimestamp] = await pageObjects.runHook('useLanguage');

expect(groupTimestamp).toMatchInlineSnapshot(`"zh-YUE"`);
});

test('getter should return default language if not set in props', async () => {
const { pageObjects } = await setupWebDriver();

const [groupTimestamp] = await pageObjects.runHook('useLanguage');

expect(groupTimestamp).toMatchInlineSnapshot(`"en-US"`);
});

test('setter should be undefined', async () => {
const { pageObjects } = await setupWebDriver();
const [_, setLanguage] = await pageObjects.runHook('useLanguage');

expect(setLanguage).toBeUndefined();
});
18 changes: 18 additions & 0 deletions __tests__/hooks/useLocalize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { timeouts } from '../constants.json';

// selenium-webdriver API doc:
// https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebDriver.html

jest.setTimeout(timeouts.test);

test('calling localize should return a localized string', async () => {
const { pageObjects } = await setupWebDriver();

await expect(pageObjects.runHook('useLocalize', ['Chat'])).resolves.toMatchInlineSnapshot(`"Chat"`);
});

test('calling localize on zh-YUE should return a localized string', async () => {
const { pageObjects } = await setupWebDriver({ props: { locale: 'zh-YUE' } });

await expect(pageObjects.runHook('useLocalize', ['Chat'])).resolves.toMatchInlineSnapshot(`"傾偈"`);
});
5 changes: 3 additions & 2 deletions __tests__/hooks/useReferenceGrammarId.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ test('getter should return reference grammar ID', async () => {
expect(referenceGrammarID).toBe('12345678-1234-5678-abcd-12345678abcd');
});

test('setter should throw exception', async () => {
test('setter should be falsy', async () => {
const { pageObjects } = await setupWebDriver();
const [_, setReferenceGrammarID] = await pageObjects.runHook('useReferenceGrammarID');

await expect(pageObjects.runHook('useReferenceGrammarID', [], result => result[1]())).rejects.toThrow();
expect(setReferenceGrammarID).toBeFalsy();
});
5 changes: 3 additions & 2 deletions __tests__/hooks/useStyleOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ test('getter should get styleOptions from props', async () => {
);
});

test('setter should throw exception', async () => {
test('setter should be falsy', async () => {
const { pageObjects } = await setupWebDriver();
const [_, setStyleOptions] = await pageObjects.runHook('useStyleOptions');

await expect(pageObjects.runHook('useStyleOptions', [], result => result[1]())).rejects.toThrow();
expect(setStyleOptions).toBeFalsy();
});
5 changes: 3 additions & 2 deletions __tests__/hooks/useStyleSet.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ test('getter should get styleSet from props', async () => {
`);
});

test('setter should throw exception', async () => {
test('setter should be falsy', async () => {
const { pageObjects } = await setupWebDriver();
const [_, setStyleSet] = await pageObjects.runHook('useStyleSet');

await expect(pageObjects.runHook('useStyleSet', [], result => result[1]())).rejects.toThrow();
expect(setStyleSet).toBeFalsy();
});
13 changes: 12 additions & 1 deletion __tests__/setup/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -159,14 +159,25 @@

const activityMiddleware = () => next => card => {
const { activity } = card;

if (activity.type === 'event' && activity.name === 'hook') {
return children => React.createElement(() => {
const { args, name, reject, resolve } = activity.value;

try {
resolve(window.WebChat.hooks[name](args));
const hookFn = window.WebChat.hooks[name];

if (!hookFn) {
console.log(`No hooks named "${ name }" were found. Valid hooks are:`, Object.keys(window.WebChat.hooks).sort());

throw new Error(`No hooks named "${ name }" were found.`);
}

resolve(hookFn(args));
} catch (err) {
reject(err);
}

return false;
});
}
Expand Down
10 changes: 7 additions & 3 deletions __tests__/video.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,16 @@ test('video', async () => {
.sendKeys('j')
.perform();

// Hide the spinner animation
await driver.executeScript(() => document.querySelector('.ytp-spinner').remove());

// Wait for YouTube play/pause/rewind animation to complete
await driver.sleep(1000);

// Hide the spinner animation
await driver.executeScript(() => {
const spinner = document.querySelector('.ytp-spinner');

spinner && spinner.remove();
});

const base64PNG = await driver.takeScreenshot();

expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { HostConfig } from 'adaptivecards';
import PropTypes from 'prop-types';
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';

import { Components, connectToWebChat, getTabIndex, hooks, localize } from 'botframework-webchat-component';
import { Components, connectToWebChat, getTabIndex, hooks } from 'botframework-webchat-component';

const { ErrorBox } = Components;
const { useStyleSet } = hooks;
const { useLocalize, useStyleSet } = hooks;

function isPlainObject(obj) {
return Object.getPrototypeOf(obj) === Object.prototype;
Expand Down Expand Up @@ -66,12 +66,13 @@ const AdaptiveCardRenderer = ({
adaptiveCard,
adaptiveCardHostConfig,
disabled,
language,
performCardAction,
renderMarkdown,
tapAction
}) => {
const [{ adaptiveCardRenderer: adaptiveCardRendererStyleSet }] = useStyleSet();
const errorMessage = useLocalize('Adaptive Card render error');

const [error, setError] = useState();
const contentRef = useRef();
const inputValuesRef = useRef([]);
Expand Down Expand Up @@ -196,7 +197,7 @@ const AdaptiveCardRenderer = ({
}, [adaptiveCard, adaptiveCardHostConfig, contentRef, disabled, error, handleExecuteAction, renderMarkdown]);

return error ? (
<ErrorBox message={localize('Adaptive Card render error', language)}>
<ErrorBox message={errorMessage}>
<pre>{JSON.stringify(error, null, 2)}</pre>
</ErrorBox>
) : (
Expand All @@ -208,7 +209,6 @@ AdaptiveCardRenderer.propTypes = {
adaptiveCard: PropTypes.any.isRequired,
adaptiveCardHostConfig: PropTypes.any.isRequired,
disabled: PropTypes.bool,
language: PropTypes.string.isRequired,
performCardAction: PropTypes.func.isRequired,
renderMarkdown: PropTypes.func.isRequired,
tapAction: PropTypes.shape({
Expand All @@ -222,9 +222,8 @@ AdaptiveCardRenderer.defaultProps = {
tapAction: undefined
};

export default connectToWebChat(({ disabled, language, onCardAction, renderMarkdown, tapAction }) => ({
export default connectToWebChat(({ disabled, onCardAction, renderMarkdown, tapAction }) => ({
disabled,
language,
performCardAction: onCardAction,
renderMarkdown,
tapAction
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
/* eslint no-magic-numbers: ["error", { "ignore": [0, 1, 10, 15, 25, 75] }] */

import { connectToWebChat, hooks, localize } from 'botframework-webchat-component';
import { hooks } from 'botframework-webchat-component';
import PropTypes from 'prop-types';
import React, { useMemo } from 'react';

import AdaptiveCardBuilder from './AdaptiveCardBuilder';
import AdaptiveCardRenderer from './AdaptiveCardRenderer';

const { useStyleOptions } = hooks;
const { useLocalize, useStyleOptions } = hooks;

function nullOrUndefined(obj) {
return obj === null || typeof obj === 'undefined';
}

const ReceiptCardAttachment = ({ adaptiveCardHostConfig, adaptiveCards, attachment: { content }, language }) => {
const ReceiptCardAttachment = ({ adaptiveCardHostConfig, adaptiveCards, attachment: { content } }) => {
const [styleOptions] = useStyleOptions();
const taxText = useLocalize('Tax');
const totalText = useLocalize('Total');
const vatText = useLocalize('VAT');

const builtCard = useMemo(() => {
const builder = new AdaptiveCardBuilder(adaptiveCards, styleOptions);
const { HorizontalAlignment, TextSize, TextWeight } = adaptiveCards;
Expand Down Expand Up @@ -64,33 +68,21 @@ const ReceiptCardAttachment = ({ adaptiveCardHostConfig, adaptiveCards, attachme
if (!nullOrUndefined(vat)) {
const vatCol = builder.addColumnSet([75, 25]);

builder.addTextBlock(
localize('VAT', language),
{ size: TextSize.Medium, weight: TextWeight.Bolder },
vatCol[0]
);
builder.addTextBlock(vatText, { size: TextSize.Medium, weight: TextWeight.Bolder }, vatCol[0]);
builder.addTextBlock(vat, { horizontalAlignment: HorizontalAlignment.Right }, vatCol[1]);
}

if (!nullOrUndefined(tax)) {
const taxCol = builder.addColumnSet([75, 25]);

builder.addTextBlock(
localize('Tax', language),
{ size: TextSize.Medium, weight: TextWeight.Bolder },
taxCol[0]
);
builder.addTextBlock(taxText, { size: TextSize.Medium, weight: TextWeight.Bolder }, taxCol[0]);
builder.addTextBlock(tax, { horizontalAlignment: HorizontalAlignment.Right }, taxCol[1]);
}

if (!nullOrUndefined(total)) {
const totalCol = builder.addColumnSet([75, 25]);

builder.addTextBlock(
localize('Total', language),
{ size: TextSize.Medium, weight: TextWeight.Bolder },
totalCol[0]
);
builder.addTextBlock(totalText, { size: TextSize.Medium, weight: TextWeight.Bolder }, totalCol[0]);
builder.addTextBlock(
total,
{ horizontalAlignment: HorizontalAlignment.Right, size: TextSize.Medium, weight: TextWeight.Bolder },
Expand All @@ -102,7 +94,7 @@ const ReceiptCardAttachment = ({ adaptiveCardHostConfig, adaptiveCards, attachme

return builder.card;
}
}, [adaptiveCards, content, language, styleOptions]);
}, [adaptiveCards, content, styleOptions, taxText, totalText, vatText]);

return (
<AdaptiveCardRenderer
Expand Down Expand Up @@ -142,8 +134,7 @@ ReceiptCardAttachment.propTypes = {
total: PropTypes.string,
vat: PropTypes.string
}).isRequired
}).isRequired,
language: PropTypes.string.isRequired
}).isRequired
};

export default connectToWebChat(({ language }) => ({ language }))(ReceiptCardAttachment);
export default ReceiptCardAttachment;
18 changes: 10 additions & 8 deletions packages/component/src/Activity/CarouselFilmStrip.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import React from 'react';

import { Constants } from 'botframework-webchat-core';

import { localize } from '../Localization/Localize';
import Avatar from './Avatar';
import Bubble from './Bubble';
import connectToWebChat from '../connectToWebChat';
import ScreenReaderText from '../ScreenReaderText';
import SendStatus from './SendStatus';
import textFormatToContentType from '../Utils/textFormatToContentType';
import Timestamp from './Timestamp';
import useLocalize from '../hooks/useLocalize';
import useStyleOptions from '../hooks/useStyleOptions';
import useStyleSet from '../hooks/useStyleSet';

Expand Down Expand Up @@ -92,13 +92,15 @@ const WebChatCarouselFilmStrip = ({
children,
className,
itemContainerRef,
language,
scrollableRef,
timestampClassName
}) => {
const [{ bubbleNubSize, bubbleFromUserNubSize }] = useStyleOptions();
const [{ carouselFilmStrip: carouselFilmStripStyleSet }] = useStyleSet();

const botRoleLabel = useLocalize('BotSent');
const userRoleLabel = useLocalize('UserSent');

const {
attachments = [],
channelData: { messageBack: { displayText: messageBackDisplayText } = {}, state } = {},
Expand All @@ -111,6 +113,8 @@ const WebChatCarouselFilmStrip = ({
const activityDisplayText = messageBackDisplayText || text;
const indented = fromUser ? bubbleFromUserNubSize : bubbleNubSize;

const roleLabel = fromUser ? userRoleLabel : botRoleLabel;

return (
<div
className={classNames(ROOT_CSS + '', carouselFilmStripStyleSet + '', className + '', {
Expand All @@ -122,7 +126,7 @@ const WebChatCarouselFilmStrip = ({
<div className="content">
{!!activityDisplayText && (
<div className="message">
<ScreenReaderText text={fromUser ? localize('UserSent', language) : localize('BotSent', language)} />
<ScreenReaderText text={roleLabel} />
<Bubble className="bubble" fromUser={fromUser} nub={true}>
{children({
activity,
Expand All @@ -138,7 +142,7 @@ const WebChatCarouselFilmStrip = ({
<ul className={classNames({ webchat__carousel__item_indented: indented })} ref={itemContainerRef}>
{attachments.map((attachment, index) => (
<li key={index}>
<ScreenReaderText text={fromUser ? localize('UserSent', language) : localize('BotSent', language)} />
<ScreenReaderText text={roleLabel} />
<Bubble fromUser={fromUser} key={index} nub={false}>
{children({ attachment })}
</Bubble>
Expand Down Expand Up @@ -184,14 +188,12 @@ WebChatCarouselFilmStrip.propTypes = {
children: PropTypes.any,
className: PropTypes.string,
itemContainerRef: PropTypes.any.isRequired,
language: PropTypes.string.isRequired,
scrollableRef: PropTypes.any.isRequired,
timestampClassName: PropTypes.string
};

const ConnectedCarouselFilmStrip = connectCarouselFilmStrip(({ avatarInitials, language }) => ({
avatarInitials,
language
const ConnectedCarouselFilmStrip = connectCarouselFilmStrip(({ avatarInitials }) => ({
avatarInitials
}))(WebChatCarouselFilmStrip);

const CarouselFilmStrip = props => (
Expand Down
Loading

0 comments on commit ca2107c

Please sign in to comment.