Skip to content

Commit

Permalink
Add RTL support (#2890)
Browse files Browse the repository at this point in the history
* Add Hebrew to list of languages

* Minor styling adjustments to playground

* Update Basic Web Chat to function component

* move prettier-readmes into lint-staged

* Add getRTLList.js to fetch RTL languages

* update package.jsono

* Determine direction based off language if dir='auto'

* Support manually setting direction in playground

* Add margins to both side of components, ex spinner, for RTL

* Deprecate spinnerAnimationPaddingRight in favor of spinnerAnimationPadding

* Fix CHANGELOG rebase error

* Refactor useDirection

* Suggested actions RTL

* Fix SpinnerAnimation styling for rtl and ltr

* tranform SendIcon and TypingAnimation based on dir

* Update LOCALIZATION file

* Update playground App.js with ar-JO

* Add ar-JO to localization with rtl support

* Add rtl languages to embed locale

* Add rtl support for Carousel

* Add rtl support for bubble nub

* Add rtl support to Scroll to end button

* Fix carousel film strip timestamp in rtl

* Add useDirection documentation to HOOKS.md

* md file cleanup

* Refactor fixes

* RTL support for icons

* Refactor RTL support in Bubble.js

* Refactor RTL support in carousel

* Refactor RTL for stacked and error

* Refactor other components for RTL

* Add unofficial Adaptive Card RTL support

* Add RTL to FileContent component

* File cleanup

* Restore change-locale sample & create change-locale-direction

* Carousel padding fix & readme cleanup

* Linting fixes

* Update CHANGELOG & documentation

* Fix linting errors

* New RTL tests

* Add retaken screenshots

* Remove echos from tableflip command

* Add more rtl tests

* Fix #2903

* Fix #2902

* Apply suggestions from code review

Co-Authored-By: William Wong <[email protected]>

* Comment cleanup

* Update packages/component/src/Styles/StyleSet/SpinnerAnimation.js

Co-Authored-By: William Wong <[email protected]>

Co-authored-by: William Wong <[email protected]>
  • Loading branch information
corinagum and compulim authored Feb 14, 2020
1 parent 17f4069 commit 7415e74
Show file tree
Hide file tree
Showing 75 changed files with 1,623 additions and 463 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Added custom hooks - `useTimer` and `useIntervalSince` - to replace the headless `Timer` component, by [@tdurnford](https://github.com/tdurnford), in PR [#2771](https://github.com/microsoft/BotFramework-WebChat/pull/2771)
- Resolves [#2720](https://github.com/Microsoft/BotFramework-WebChat/issues/2720), added customizable activity status using `activityStatusMiddleware` props, by [@compulim](https://github.com/compulim), in PR [#2788](https://github.com/microsoft/BotFramework-WebChat/pull/2788)
- Added default `onError` prop to the `Dictation` component, by [@tonyanziano](https://github.com/tonyanziano), in PR [#2866](https://github.com/microsoft/BotFramework-WebChat/pull/2866)
- Resolves [#1976](https://github.com/microsoft/BotFramework-WebChat/issues/1976). Added RTL support with localization for Hebrew and Arabic, by [@corinagum](https://github.com/corinagum), in PR [#2890](https://github.com/microsoft/BotFramework-WebChat/pull/2890)

### Fixed

Expand Down
4 changes: 3 additions & 1 deletion LOCALIZATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ If you want to help to translate Web Chat to different language, please submit a

| Language code | Translator |
| ------------- | ---------------------------------------------------------- |
| ar-EG | @midineo |
| ar-eg | @midineo |
| ar-jo | muminasaad, Odai Hatem AbuGaith |
| bg-bg | @kalin.krustev |
| cs-cz | @msimecek |
| da-dk | @Simon_lfr, Thomas Skødt Andersen |
Expand All @@ -15,6 +16,7 @@ If you want to help to translate Web Chat to different language, please submit a
| es-es | @SantiEspada, @ckgrafico, @renrous, @axelsrz, @munozemilio |
| fi-fi | @jsur, @sk91swd |
| fr-fr | @meulta, @tao1 |
| he-il | @geea-develop |
| hu-hu | |
| it-it | Maurizio Moriconi, @Andrea-Orimoto, @AntoT84 |
| ja-jp | @bigplants, @corinagum |
Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ Here is how how you can add Web Chat control to your website:
body {
height: 100%;
}
body {
margin: 0;
}
Expand Down
2 changes: 1 addition & 1 deletion __tests__/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Automated testing in Web Chat is using multiple open-source technologies.

- Install Docker
- On Windows, set environment variable `COMPOSE_CONVERT_WINDOWS_PATHS=1`
- `npm run start:docker`
- `docker-compose up --build`
- In a separate terminal, run:
- `npm test`

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.
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.
158 changes: 158 additions & 0 deletions __tests__/rtl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { By } from 'selenium-webdriver';

import { imageSnapshotOptions, timeouts } from './constants.json';

import allImagesLoaded from './setup/conditions/allImagesLoaded';
import minNumActivitiesShown from './setup/conditions/minNumActivitiesShown';
import suggestedActionsShown from './setup/conditions/suggestedActionsShown';
import uiConnected from './setup/conditions/uiConnected';

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

jest.setTimeout(timeouts.test);

async function sendMessageAndMatchSnapshot(driver, pageObjects, message) {
await driver.wait(uiConnected(), timeouts.directLine);
await pageObjects.sendMessageViaSendBox(message);

await driver.wait(minNumActivitiesShown(2), timeouts.directLine);
await driver.wait(allImagesLoaded(), timeouts.fetchImage);

const base64PNG = await driver.takeScreenshot();

expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions);
}

describe('rtl UI', () => {
let props;

beforeEach(() => {
props = {
locale: 'ar-EG'
};
});

test('should show "unable to connect" UI in Arabic when credentials are incorrect', async () => {
const { driver } = await setupWebDriver({
props: {
...props
},
createDirectLine: () => {
return window.WebChat.createDirectLine({ token: 'INVALID-TOKEN' });
},
pingBotOnLoad: false,
setup: () =>
new Promise(resolve => {
const scriptElement = document.createElement('script');

scriptElement.onload = resolve;
scriptElement.setAttribute('src', 'https://unpkg.com/[email protected]/client/core.min.js');

document.head.appendChild(scriptElement);
})
});

await driver.wait(async driver => {
return await driver.executeScript(
() => !!~window.WebChatTest.actions.findIndex(({ type }) => type === 'DIRECT_LINE/CONNECT_REJECTED')
);
}, timeouts.directLine);

const base64PNG = await driver.takeScreenshot();

expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions);
});

test('unknown command with nubs should display correctly', async () => {
const { driver, pageObjects } = await setupWebDriver({
props: {
...props,
styleOptions: {
bubbleNubOffset: 0,
bubbleNubSize: 10,
bubbleFromUserNubOffset: 0,
bubbleFromUserNubSize: 10
}
}
});

await sendMessageAndMatchSnapshot(driver, pageObjects, 'صباح الخير');
});

test('carousel with avatar initials should display user and bot in reversed positions', async () => {
const { driver, pageObjects } = await setupWebDriver({
props: {
...props,
styleOptions: {
botAvatarInitials: 'WC',
userAvatarInitials: 'WW'
}
}
});

await sendMessageAndMatchSnapshot(driver, pageObjects, 'arabic carousel');
});

test('carousel should scroll to the left instead of right', async () => {
const { driver, pageObjects } = await setupWebDriver({
props: {
...props
}
});

await driver.wait(uiConnected(), timeouts.directLine);
await pageObjects.sendMessageViaSendBox('arabic carousel', { waitForSend: true });

await driver.wait(minNumActivitiesShown(2), timeouts.directLine);
await driver.wait(allImagesLoaded(), timeouts.fetchImage);

const leftFlipper = await driver.findElement(By.css('button[aria-label="يسار"]'));

await leftFlipper.click();
await leftFlipper.click();
await leftFlipper.click();
await leftFlipper.click();

// Wait for carousel animation to finish
await driver.sleep(timeouts.ui);

expect(await driver.takeScreenshot()).toMatchImageSnapshot(imageSnapshotOptions);
});

test('with Adaptive Card should be displayed correctly', async () => {
const { driver, pageObjects } = await setupWebDriver({
props: {
...props
}
});

await sendMessageAndMatchSnapshot(driver, pageObjects, 'card arabicgreeting');
});

test('with Audio Card should be displayed correctly', async () => {
const { driver, pageObjects } = await setupWebDriver({
props: {
...props
}
});

await sendMessageAndMatchSnapshot(driver, pageObjects, 'audiocard');
});

test('should show suggested actions with images', async () => {
const { driver, pageObjects } = await setupWebDriver({
props: { ...props }
});

await driver.wait(uiConnected(), timeouts.directLine);
await pageObjects.sendMessageViaSendBox('emptycard', { waitForSend: true });

await driver.wait(suggestedActionsShown(), timeouts.directLine);
await driver.wait(allImagesLoaded(), timeouts.fetchImage);

const base64PNG = await driver.takeScreenshot();

expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions);
});
});
18 changes: 17 additions & 1 deletion docs/HOOKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Following is the list of hooks supported by Web Chat API.
- [`useConnectivityStatus`](#useconnectivitystatus)
- [`useDictateInterims`](#usedictateinterims)
- [`useDictateState`](#usedictatestate)
- [`useDirection`](#useDirection)
- [`useDisabled`](#usedisabled)
- [`useEmitTypingIndicator`](#useemittypingindicator)
- [`useFocusSendBox`](#usefocussendbox)
Expand Down Expand Up @@ -192,6 +193,21 @@ This function will return one of the following dictation states:
To control dictate state, use the [`useStartDictate`](#usestartdictate) and [`useStopDictate`](#usestopdictate) hooks.

## `useDirection`

```js
useDirection(): [string]
```

This function will return one of two language directions:

- `ltr` or otherwise: Web Chat UI will display as left-to-right
- `rtl`: Web Chat UI will display as right-to-left

This value will be automatically configured based on the `locale` of Web Chat.

If you would prefer to set this property manually, change the value `dir` prop passed to Web Chat.

## `useDisabled`

```js
Expand Down Expand Up @@ -248,7 +264,7 @@ useLanguage(): [string]

This function will return the language of the UI. All UI components should honor this value.

To modify this value, change the value in the style options prop passed to Web Chat.
To modify this value, change the value in the `locale` prop passed to Web Chat.

## `useLastTypingAt`

Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
"prettier --write __tests__/**/*.{js,jsx,ts,tsx}",
"prettier --write samples/**/*.{js,html}",
"git add"
],
"*.{md}": [
"prettier --write **/**/*.md --tab-width 3 --single-quote true"
]
},
"scripts": {
Expand All @@ -37,9 +40,7 @@
"posteslint": "npm run prettier-readmes",
"prettier-readmes": "prettier --write **/**/*.md --tab-width 3 --single-quote true",
"start": "concurrently --kill-others --raw \"serve\" \"lerna run --ignore playground --parallel --stream start\"",
"tableflip": "npm run tableflip:start && npx lerna clean --yes --concurrency 8 && npx rimraf node_modules && npm ci && npm run bootstrap -- --concurrency 2 && npm run tableflip:end",
"tableflip:end": "echo ┬──┬ ノ( ゜-゜ノ) Tableflip complete. Now run npm start",
"tableflip:start": "echo (╯ರ ~ ರ)╯︵ ┻━┻ Begin tableflip.",
"tableflip": "npx lerna clean --yes --concurrency 8 && npx rimraf node_modules && npm ci && npm run bootstrap -- --concurrency 8",
"test": "jest --watch",
"test:ci": "npm run test -- --ci --coverage true --no-watch",
"watch": "echo NPM script \"watch\" has been replaced with \"start\"."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import React, { useMemo } from 'react';
import AdaptiveCardRenderer from './AdaptiveCardRenderer';
import useAdaptiveCardsPackage from '../hooks/useAdaptiveCardsPackage';

import { hooks } from 'botframework-webchat-component';

const { useDirection } = hooks;

function stripSubmitAction(card) {
if (!card.actions) {
return card;
Expand All @@ -17,8 +21,27 @@ function stripSubmitAction(card) {
return { ...card, nextActions };
}

function updateRTLInline(element, rtl, adaptiveCardsPackage) {
if (element instanceof adaptiveCardsPackage.Container) {
element.rtl = rtl;
}

// Tree traversal to add rtl boolean to child elements
if (element.getItemAt && element.getItemCount) {
const count = element.getItemCount();

for (let index = 0; index < count; index++) {
const child = element.getItemAt(index);

updateRTLInline(child, rtl, adaptiveCardsPackage);
}
}
}

const AdaptiveCardAttachment = ({ attachment: { content } }) => {
const [{ AdaptiveCard }] = useAdaptiveCardsPackage();
const [adaptiveCardsPackage] = useAdaptiveCardsPackage();
const { AdaptiveCard } = adaptiveCardsPackage;
const [direction] = useDirection();
const { card } = useMemo(() => {
if (content) {
const card = new AdaptiveCard();
Expand All @@ -34,6 +57,9 @@ const AdaptiveCardAttachment = ({ attachment: { content } }) => {
})
);

// Add rtl to Adaptive Card and child elements if Web Chat direction is 'rtl'
updateRTLInline(card, direction === 'rtl', adaptiveCardsPackage);

AdaptiveCard.onParseError = null;

return {
Expand All @@ -43,7 +69,7 @@ const AdaptiveCardAttachment = ({ attachment: { content } }) => {
}

return {};
}, [AdaptiveCard, content]);
}, [AdaptiveCard, adaptiveCardsPackage, content, direction]);

return !!card && <AdaptiveCardRenderer adaptiveCard={card} />;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,10 @@ export default class AdaptiveCardBuilder {
container: Container;
styleOptions: any;

constructor(adaptiveCards, styleOptions) {
constructor(adaptiveCards, styleOptions, direction = 'ltr') {
this.card = new adaptiveCards.AdaptiveCard();
this.container = new Container();
this.container.rtl = direction === 'rtl';
this.styleOptions = styleOptions;

this.card.addItem(this.container);
Expand Down
7 changes: 4 additions & 3 deletions packages/bundle/src/adaptiveCards/Attachment/CommonCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,22 @@ import AdaptiveCardBuilder from './AdaptiveCardBuilder';
import AdaptiveCardRenderer from './AdaptiveCardRenderer';
import useAdaptiveCardsPackage from '../hooks/useAdaptiveCardsPackage';

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

const CommonCard = ({ attachment: { content } }) => {
const [adaptiveCardsPackage] = useAdaptiveCardsPackage();
const [direction] = useDirection();
const [styleOptions] = useStyleOptions();

const builtCard = useMemo(() => {
if (content) {
const builder = new AdaptiveCardBuilder(adaptiveCardsPackage, styleOptions);
const builder = new AdaptiveCardBuilder(adaptiveCardsPackage, styleOptions, direction);

builder.addCommon(content);

return builder.card;
}
}, [adaptiveCardsPackage, content, styleOptions]);
}, [adaptiveCardsPackage, content, direction, styleOptions]);

return <AdaptiveCardRenderer adaptiveCard={builtCard} tapAction={content && content.tap} />;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import AdaptiveCardBuilder from './AdaptiveCardBuilder';
import AdaptiveCardRenderer from './AdaptiveCardRenderer';
import useAdaptiveCardsPackage from '../hooks/useAdaptiveCardsPackage';

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

const HeroCardAttachment = ({ attachment: { content } = {} }) => {
const [adaptiveCardsPackage] = useAdaptiveCardsPackage();
const [styleOptions] = useStyleOptions();
const [direction] = useDirection();
const builtCard = useMemo(() => {
const builder = new AdaptiveCardBuilder(adaptiveCardsPackage, styleOptions);
const builder = new AdaptiveCardBuilder(adaptiveCardsPackage, styleOptions, direction);

if (content) {
(content.images || []).forEach(image => builder.addImage(image.url, null, image.tap));
Expand All @@ -22,7 +23,7 @@ const HeroCardAttachment = ({ attachment: { content } = {} }) => {

return builder.card;
}
}, [adaptiveCardsPackage, content, styleOptions]);
}, [adaptiveCardsPackage, content, direction, styleOptions]);

return <AdaptiveCardRenderer adaptiveCard={builtCard} tapAction={content && content.tap} />;
};
Expand Down
Loading

0 comments on commit 7415e74

Please sign in to comment.