diff --git a/CHANGELOG.md b/CHANGELOG.md index b836f46116..b406bbc022 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] ### Changed + - `*`: Bumps all dev dependencies to latest version, by [@compulim](https://github.com/compulim), in PR [#2182](https://github.com/microsoft/BotFramework-WebChat/pull/2182), notably - [`@babel/*@7.5.4`](https://www.npmjs.com/package/@babel/core) - [`jest@24.8.0`](https://www.npmjs.com/package/jest) @@ -47,6 +48,9 @@ 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) +### Added + +- Added bubble nub and style options, by [@compulim](https://github.com/compulim), in PR [#2137](https://github.com/Microsoft/BotFramework-WebChat/pull/2137) ## [4.5.0] - 2019-07-10 diff --git a/__tests__/__image_snapshots__/chrome-docker/bubble-border-js-bubble-border-with-color-radius-style-and-width-set-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/bubble-border-js-bubble-border-with-color-radius-style-and-width-set-1-snap.png new file mode 100644 index 0000000000..3819f04fad Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/bubble-border-js-bubble-border-with-color-radius-style-and-width-set-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/bubble-border-js-bubble-border-with-deprecated-border-style-of-2-px-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/bubble-border-js-bubble-border-with-deprecated-border-style-of-2-px-1-snap.png new file mode 100644 index 0000000000..15bfb5237e Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/bubble-border-js-bubble-border-with-deprecated-border-style-of-2-px-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/bubble-border-js-bubble-border-with-deprecated-border-style-of-dashed-2-px-red-and-dotted-2-px-green-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/bubble-border-js-bubble-border-with-deprecated-border-style-of-dashed-2-px-red-and-dotted-2-px-green-1-snap.png new file mode 100644 index 0000000000..01cd11022c Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/bubble-border-js-bubble-border-with-deprecated-border-style-of-dashed-2-px-red-and-dotted-2-px-green-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/bubble-border-js-bubble-border-with-deprecated-border-style-of-dashed-and-dotted-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/bubble-border-js-bubble-border-with-deprecated-border-style-of-dashed-and-dotted-1-snap.png new file mode 100644 index 0000000000..be5bd5dcbb Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/bubble-border-js-bubble-border-with-deprecated-border-style-of-dashed-and-dotted-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/bubble-border-js-bubble-border-with-deprecated-border-style-of-red-and-green-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/bubble-border-js-bubble-border-with-deprecated-border-style-of-red-and-green-1-snap.png new file mode 100644 index 0000000000..2504d6a350 Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/bubble-border-js-bubble-border-with-deprecated-border-style-of-red-and-green-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/bubble-border-js-bubble-border-with-deprecated-border-style-with-color-radius-style-and-width-set-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/bubble-border-js-bubble-border-with-deprecated-border-style-with-color-radius-style-and-width-set-1-snap.png new file mode 100644 index 0000000000..3819f04fad Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/bubble-border-js-bubble-border-with-deprecated-border-style-with-color-radius-style-and-width-set-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-at-corner-with-offset-at-bottom-should-have-flipped-nub-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-at-corner-with-offset-at-bottom-should-have-flipped-nub-1-snap.png new file mode 100644 index 0000000000..db4ecd6cda Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-at-corner-with-offset-at-bottom-should-have-flipped-nub-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-at-corner-with-offset-of-10-px-should-have-corner-radius-of-10-px-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-at-corner-with-offset-of-10-px-should-have-corner-radius-of-10-px-1-snap.png new file mode 100644 index 0000000000..489d04017f Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-at-corner-with-offset-of-10-px-should-have-corner-radius-of-10-px-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-at-corner-with-offset-of-5-px-should-have-corner-radius-of-5-px-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-at-corner-with-offset-of-5-px-should-have-corner-radius-of-5-px-1-snap.png new file mode 100644 index 0000000000..88270e767f Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-at-corner-with-offset-of-5-px-should-have-corner-radius-of-5-px-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-at-corner-with-offset-of-minus-10-px-should-have-corner-radius-of-10-px-and-flipped-nub-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-at-corner-with-offset-of-minus-10-px-should-have-corner-radius-of-10-px-and-flipped-nub-1-snap.png new file mode 100644 index 0000000000..70de4ed8a5 Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-at-corner-with-offset-of-minus-10-px-should-have-corner-radius-of-10-px-and-flipped-nub-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-at-corner-with-offset-of-minus-5-px-should-have-corner-radius-of-5-px-and-flipped-nub-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-at-corner-with-offset-of-minus-5-px-should-have-corner-radius-of-5-px-and-flipped-nub-1-snap.png new file mode 100644 index 0000000000..6d20c10a78 Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-at-corner-with-offset-of-minus-5-px-should-have-corner-radius-of-5-px-and-flipped-nub-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-with-avatar-initials-and-a-single-message-should-have-nub-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-with-avatar-initials-and-a-single-message-should-have-nub-1-snap.png new file mode 100644 index 0000000000..f08df118c8 Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-with-avatar-initials-and-a-single-message-should-have-nub-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-with-avatar-initials-and-carousel-with-a-message-should-have-nub-on-message-only-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-with-avatar-initials-and-carousel-with-a-message-should-have-nub-on-message-only-1-snap.png new file mode 100644 index 0000000000..56793ac816 Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-with-avatar-initials-and-carousel-with-a-message-should-have-nub-on-message-only-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-with-avatar-initials-and-carousel-with-a-single-attachment-should-have-nub-on-message-only-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-with-avatar-initials-and-carousel-with-a-single-attachment-should-have-nub-on-message-only-1-snap.png new file mode 100644 index 0000000000..0d841276ed Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-with-avatar-initials-and-carousel-with-a-single-attachment-should-have-nub-on-message-only-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-with-avatar-initials-and-carousel-without-a-message-should-not-have-nubs-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-with-avatar-initials-and-carousel-without-a-message-should-not-have-nubs-1-snap.png new file mode 100644 index 0000000000..1a3eb9a654 Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-with-avatar-initials-and-carousel-without-a-message-should-not-have-nubs-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-with-avatar-initials-and-stacked-without-a-message-should-not-have-nubs-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-with-avatar-initials-and-stacked-without-a-message-should-not-have-nubs-1-snap.png new file mode 100644 index 0000000000..313a1ac590 Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-with-avatar-initials-and-stacked-without-a-message-should-not-have-nubs-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-with-standard-setup-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-with-standard-setup-1-snap.png new file mode 100644 index 0000000000..6f27b03f39 Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-with-standard-setup-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-without-avatar-initials-and-a-single-message-should-have-nub-and-indented-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-without-avatar-initials-and-a-single-message-should-have-nub-and-indented-1-snap.png new file mode 100644 index 0000000000..cf03c59caf Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-without-avatar-initials-and-a-single-message-should-have-nub-and-indented-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-without-avatar-initials-and-carousel-with-a-message-should-have-nub-on-message-only-and-indented-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-without-avatar-initials-and-carousel-with-a-message-should-have-nub-on-message-only-and-indented-1-snap.png new file mode 100644 index 0000000000..e9570d3659 Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-without-avatar-initials-and-carousel-with-a-message-should-have-nub-on-message-only-and-indented-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-without-avatar-initials-and-carousel-with-a-single-attachment-should-have-nub-on-message-only-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-without-avatar-initials-and-carousel-with-a-single-attachment-should-have-nub-on-message-only-1-snap.png new file mode 100644 index 0000000000..089110a2b3 Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-without-avatar-initials-and-carousel-with-a-single-attachment-should-have-nub-on-message-only-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-without-avatar-initials-and-carousel-without-a-message-should-not-have-nubs-and-indented-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-without-avatar-initials-and-carousel-without-a-message-should-not-have-nubs-and-indented-1-snap.png new file mode 100644 index 0000000000..a8bc89351c Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-without-avatar-initials-and-carousel-without-a-message-should-not-have-nubs-and-indented-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-without-avatar-initials-and-stacked-without-a-message-should-not-have-nubs-and-indented-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-without-avatar-initials-and-stacked-without-a-message-should-not-have-nubs-and-indented-1-snap.png new file mode 100644 index 0000000000..6c8e0c5d05 Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/bubble-nub-js-bubble-nub-without-avatar-initials-and-stacked-without-a-message-should-not-have-nubs-and-indented-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/carousel-js-carousel-with-avatar-initials-1-attachment-with-wide-screen-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/carousel-js-carousel-with-avatar-initials-1-attachment-with-wide-screen-1-snap.png index 04bf6e349d..beda61d5f7 100644 Binary files a/__tests__/__image_snapshots__/chrome-docker/carousel-js-carousel-with-avatar-initials-1-attachment-with-wide-screen-1-snap.png and b/__tests__/__image_snapshots__/chrome-docker/carousel-js-carousel-with-avatar-initials-1-attachment-with-wide-screen-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/carousel-js-carousel-without-avatar-initials-1-attachment-with-wide-screen-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/carousel-js-carousel-without-avatar-initials-1-attachment-with-wide-screen-1-snap.png index e01c1d02ff..fbe4aae65f 100644 Binary files a/__tests__/__image_snapshots__/chrome-docker/carousel-js-carousel-without-avatar-initials-1-attachment-with-wide-screen-1-snap.png and b/__tests__/__image_snapshots__/chrome-docker/carousel-js-carousel-without-avatar-initials-1-attachment-with-wide-screen-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/stacked-js-stacked-with-avatar-initials-1-attachment-with-wide-screen-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/stacked-js-stacked-with-avatar-initials-1-attachment-with-wide-screen-1-snap.png index 5675f11f22..084d4ea8b1 100644 Binary files a/__tests__/__image_snapshots__/chrome-docker/stacked-js-stacked-with-avatar-initials-1-attachment-with-wide-screen-1-snap.png and b/__tests__/__image_snapshots__/chrome-docker/stacked-js-stacked-with-avatar-initials-1-attachment-with-wide-screen-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/stacked-js-stacked-without-avatar-initials-1-attachment-with-wide-screen-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/stacked-js-stacked-without-avatar-initials-1-attachment-with-wide-screen-1-snap.png index ce8875b5e8..80f527a1cc 100644 Binary files a/__tests__/__image_snapshots__/chrome-docker/stacked-js-stacked-without-avatar-initials-1-attachment-with-wide-screen-1-snap.png and b/__tests__/__image_snapshots__/chrome-docker/stacked-js-stacked-without-avatar-initials-1-attachment-with-wide-screen-1-snap.png differ diff --git a/__tests__/bubbleBorder.js b/__tests__/bubbleBorder.js new file mode 100644 index 0000000000..39bbb64ef6 --- /dev/null +++ b/__tests__/bubbleBorder.js @@ -0,0 +1,131 @@ +import { imageSnapshotOptions, timeouts } from './constants.json'; + +import allImagesLoaded from './setup/conditions/allImagesLoaded'; +import minNumActivitiesShown from './setup/conditions/minNumActivitiesShown'; +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(3), timeouts.directLine); + + const base64PNG = await driver.takeScreenshot(); + + expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions); +} + +describe('bubble border', () => { + test('with color, radius, style, and width set', async () => { + const { driver, pageObjects } = await setupWebDriver({ + props: { + styleOptions: { + bubbleBorderColor: 'Red', + bubbleBorderRadius: 10, + bubbleBorderStyle: 'dashed', + bubbleBorderWidth: 2, + + bubbleFromUserBorderColor: 'Green', + bubbleFromUserBorderRadius: 20, + bubbleFromUserBorderStyle: 'dotted', + bubbleFromUserBorderWidth: 3 + } + }, + zoom: 3 + }); + + await sendMessageAndMatchSnapshot(driver, pageObjects, 'echo Hello, World!'); + }); + + describe('with deprecated border style', () => { + test('with color, radius, style, and width set', async () => { + const { driver, pageObjects } = await setupWebDriver({ + props: { + styleOptions: { + bubbleBorder: 'dashed 2px Red', + bubbleBorderRadius: 10, + + bubbleFromUserBorder: 'dotted 3px Green', + bubbleFromUserBorderRadius: 20 + } + }, + zoom: 3 + }); + + await sendMessageAndMatchSnapshot(driver, pageObjects, 'echo Hello, World!'); + }); + + test('of "dashed 2px Red" and "dotted 2px Green"', async () => { + const { driver, pageObjects } = await setupWebDriver({ + props: { + styleOptions: { + bubbleBorder: 'dashed 2px Red', + bubbleFromUserBorder: 'dotted 2px Green' + } + } + }); + + await driver.wait(uiConnected(), timeouts.directLine); + await pageObjects.sendMessageViaSendBox('Hello, World!'); + await driver.wait(minNumActivitiesShown(2), timeouts.directLine); + + expect(await driver.takeScreenshot()).toMatchImageSnapshot(imageSnapshotOptions); + }); + + test('of "dashed" and "dotted"', async () => { + const { driver, pageObjects } = await setupWebDriver({ + props: { + styleOptions: { + bubbleBorder: 'dashed', + bubbleFromUserBorder: 'dotted' + } + } + }); + + await driver.wait(uiConnected(), timeouts.directLine); + await pageObjects.sendMessageViaSendBox('Hello, World!'); + await driver.wait(minNumActivitiesShown(2), timeouts.directLine); + + expect(await driver.takeScreenshot()).toMatchImageSnapshot(imageSnapshotOptions); + }); + + test('of "2px"', async () => { + const { driver, pageObjects } = await setupWebDriver({ + props: { + styleOptions: { + bubbleBorder: '2px', + bubbleFromUserBorder: '2px' + } + } + }); + + await driver.wait(uiConnected(), timeouts.directLine); + await pageObjects.sendMessageViaSendBox('Hello, World!'); + await driver.wait(minNumActivitiesShown(2), timeouts.directLine); + + expect(await driver.takeScreenshot()).toMatchImageSnapshot(imageSnapshotOptions); + }); + + test('of "Red" and "Green"', async () => { + const { driver, pageObjects } = await setupWebDriver({ + props: { + styleOptions: { + bubbleBorder: 'Red', + bubbleFromUserBorder: 'Green' + } + } + }); + + await driver.wait(uiConnected(), timeouts.directLine); + await pageObjects.sendMessageViaSendBox('Hello, World!'); + await driver.wait(minNumActivitiesShown(2), timeouts.directLine); + + expect(await driver.takeScreenshot()).toMatchImageSnapshot(imageSnapshotOptions); + }); + }); +}); diff --git a/__tests__/bubbleNub.js b/__tests__/bubbleNub.js new file mode 100644 index 0000000000..806c8e0c8f --- /dev/null +++ b/__tests__/bubbleNub.js @@ -0,0 +1,216 @@ +import { imageSnapshotOptions, timeouts } from './constants.json'; + +import allImagesLoaded from './setup/conditions/allImagesLoaded'; +import minNumActivitiesShown from './setup/conditions/minNumActivitiesShown'; +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.fetch); + + const base64PNG = await driver.takeScreenshot(); + + expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions); +} + +describe('bubble nub', () => { + let props; + + test('with standard setup', async () => { + const { driver, pageObjects } = await setupWebDriver({ + props: { + styleOptions: { + bubbleNubOffset: 0, + bubbleNubSize: 10, + bubbleFromUserNubOffset: 0, + bubbleFromUserNubSize: 10 + } + }, + zoom: 3 + }); + + await sendMessageAndMatchSnapshot(driver, pageObjects, 'layout carousel'); + }); + + beforeEach(() => { + props = { + styleOptions: { + bubbleBorderColor: 'red', + bubbleBorderRadius: 10, + bubbleBorderWidth: 2, + bubbleFromUserBorderColor: 'green', + bubbleFromUserBorderRadius: 10, + bubbleFromUserNubOffset: 0, + bubbleFromUserNubSize: 10, + bubbleFromUserBorderWidth: 2, + bubbleNubOffset: 0, + bubbleNubSize: 10 + } + }; + }); + + describe('with avatar initials', () => { + beforeEach(() => { + props = { + ...props, + styleOptions: { + ...props.styleOptions, + botAvatarInitials: 'WC', + userAvatarInitials: 'WW' + } + }; + }); + + test('and carousel with a message should have nub on message only', async () => { + const { driver, pageObjects } = await setupWebDriver({ props, zoom: 3 }); + + await sendMessageAndMatchSnapshot(driver, pageObjects, 'layout carousel'); + }); + + test('and carousel without a message should not have nubs', async () => { + const { driver, pageObjects } = await setupWebDriver({ props, zoom: 3 }); + + await sendMessageAndMatchSnapshot(driver, pageObjects, 'carousel'); + }); + + test('and stacked without a message should not have nubs', async () => { + const { driver, pageObjects } = await setupWebDriver({ props, zoom: 3 }); + + await sendMessageAndMatchSnapshot(driver, pageObjects, 'layout single'); + }); + + test('and a single message should have nub', async () => { + const { driver, pageObjects } = await setupWebDriver({ props, zoom: 3 }); + + await sendMessageAndMatchSnapshot(driver, pageObjects, 'Hello, World!'); + }); + + test('and carousel with a single attachment should have nub on message only', async () => { + const { driver, pageObjects } = await setupWebDriver({ props, zoom: 3 }); + + await sendMessageAndMatchSnapshot(driver, pageObjects, 'layout single carousel'); + }); + }); + + describe('without avatar initials', () => { + test('and carousel with a message should have nub on message only and indented', async () => { + const { driver, pageObjects } = await setupWebDriver({ props, zoom: 3 }); + + await sendMessageAndMatchSnapshot(driver, pageObjects, 'layout carousel'); + }); + + test('and carousel without a message should not have nubs and indented', async () => { + const { driver, pageObjects } = await setupWebDriver({ props, zoom: 3 }); + + await sendMessageAndMatchSnapshot(driver, pageObjects, 'carousel'); + }); + + test('and stacked without a message should not have nubs and indented', async () => { + const { driver, pageObjects } = await setupWebDriver({ props, zoom: 3 }); + + await sendMessageAndMatchSnapshot(driver, pageObjects, 'layout single'); + }); + + test('and a single message should have nub and indented', async () => { + const { driver, pageObjects } = await setupWebDriver({ props, zoom: 3 }); + + await sendMessageAndMatchSnapshot(driver, pageObjects, 'Hello, World!'); + }); + + test('and carousel with a single attachment should have nub on message only', async () => { + const { driver, pageObjects } = await setupWebDriver({ props, zoom: 3 }); + + await sendMessageAndMatchSnapshot(driver, pageObjects, 'layout single carousel'); + }); + }); + + describe('at corner with offset', () => { + test('of 5px should have corner radius of 5px', async () => { + const { driver, pageObjects } = await setupWebDriver({ + props: { + ...props, + styleOptions: { + ...props.styleOptions, + bubbleFromUserNubOffset: 5, + bubbleNubOffset: 5 + } + }, + zoom: 3 + }); + + await sendMessageAndMatchSnapshot(driver, pageObjects, 'Hello, World!'); + }); + + test('of 10px should have corner radius of 10px', async () => { + const { driver, pageObjects } = await setupWebDriver({ + props: { + ...props, + styleOptions: { + ...props.styleOptions, + bubbleFromUserNubOffset: 10, + bubbleNubOffset: 10 + } + }, + zoom: 3 + }); + + await sendMessageAndMatchSnapshot(driver, pageObjects, 'Hello, World!'); + }); + + test('of minus 5px should have corner radius of 5px and flipped nub', async () => { + const { driver, pageObjects } = await setupWebDriver({ + props: { + ...props, + styleOptions: { + ...props.styleOptions, + bubbleFromUserNubOffset: -5, + bubbleNubOffset: -5 + } + }, + zoom: 3 + }); + + await sendMessageAndMatchSnapshot(driver, pageObjects, 'Hello, World!'); + }); + + test('of minus 10px should have corner radius of 10px and flipped nub', async () => { + const { driver, pageObjects } = await setupWebDriver({ + props: { + ...props, + styleOptions: { + ...props.styleOptions, + bubbleFromUserNubOffset: -10, + bubbleNubOffset: -10 + } + }, + zoom: 3 + }); + + await sendMessageAndMatchSnapshot(driver, pageObjects, 'Hello, World!'); + }); + + test('at bottom should have flipped nub', async () => { + const { driver, pageObjects } = await setupWebDriver({ + props: { + ...props, + styleOptions: { + ...props.styleOptions, + bubbleFromUserNubOffset: 'bottom', + bubbleNubOffset: 'bottom' + } + }, + zoom: 3 + }); + + await sendMessageAndMatchSnapshot(driver, pageObjects, 'Hello, World!'); + }); + }); +}); diff --git a/__tests__/setup/setupTestEnvironment.js b/__tests__/setup/setupTestEnvironment.js index eb13f97b51..ebfd1fa597 100644 --- a/__tests__/setup/setupTestEnvironment.js +++ b/__tests__/setup/setupTestEnvironment.js @@ -1,6 +1,6 @@ import { Options } from 'selenium-webdriver/chrome'; -export default function setupTestEnvironment(browserName, builder, { height, width } = {}) { +export default function setupTestEnvironment(browserName, builder, { height = 640, width = 360, zoom = 1 } = {}) { switch (browserName) { case 'chrome-local': return { @@ -8,7 +8,7 @@ export default function setupTestEnvironment(browserName, builder, { height, wid builder: builder .forBrowser('chrome') .setChromeOptions( - (builder.getChromeOptions() || new Options()).windowSize({ height: height || 640, width: width || 360 }) + (builder.getChromeOptions() || new Options()).windowSize({ height: height * zoom, width: width * zoom }) ) }; @@ -22,7 +22,7 @@ export default function setupTestEnvironment(browserName, builder, { height, wid .setChromeOptions( (builder.getChromeOptions() || new Options()) .headless() - .windowSize({ height: height || 640, width: width || 360 }) + .windowSize({ height: height * zoom, width: width * zoom }) ) }; } diff --git a/__tests__/setup/setupTestFramework.js b/__tests__/setup/setupTestFramework.js index 8f21bd6cf9..2c4d469df9 100644 --- a/__tests__/setup/setupTestFramework.js +++ b/__tests__/setup/setupTestFramework.js @@ -72,6 +72,10 @@ global.setupWebDriver = async options => { (coverage, options, callback) => { window.__coverage__ = coverage; + if (options.zoom) { + document.body.style.zoom = options.zoom; + } + main(options).then( () => callback(), err => { diff --git a/packages/component/package-lock.json b/packages/component/package-lock.json index 1e75b8f14e..16ba476906 100644 --- a/packages/component/package-lock.json +++ b/packages/component/package-lock.json @@ -1608,6 +1608,15 @@ "object-visit": "^1.0.0" } }, + "color": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.1.1.tgz", + "integrity": "sha512-PvUltIXRjehRKPSy89VnDWFKY58xyhTLyxIg21vwQBI6qLwZNPmC8k3C1uytIgFKEpOIzN4y32iPm8231zFHIg==", + "requires": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + } + }, "color-convert": { "version": "1.9.2", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", @@ -1621,6 +1630,15 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=" }, + "color-string": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", + "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "commander": { "version": "2.20.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", @@ -4889,6 +4907,21 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "requires": { + "is-arrayish": "^0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + } + } + }, "simple-update-in": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/simple-update-in/-/simple-update-in-1.3.0.tgz", diff --git a/packages/component/package.json b/packages/component/package.json index 6bfb6b8f9e..4c7581f99a 100644 --- a/packages/component/package.json +++ b/packages/component/package.json @@ -49,6 +49,7 @@ "botframework-webchat-core": "0.0.0-0", "bytes": "^3.0.0", "classnames": "^2.2.6", + "color": "^3.1.1", "eslint": "^5.16.0", "eslint-plugin-react": "^7.13.0", "glamor": "^2.20.40", diff --git a/packages/component/src/Activity/Bubble.js b/packages/component/src/Activity/Bubble.js index e4c1e2ad5b..c8763e39d1 100644 --- a/packages/component/src/Activity/Bubble.js +++ b/packages/component/src/Activity/Bubble.js @@ -1,12 +1,37 @@ +import { css } from 'glamor'; import classNames from 'classnames'; import PropTypes from 'prop-types'; import React from 'react'; import connectToWebChat from '../connectToWebChat'; -const Bubble = ({ 'aria-hidden': ariaHidden, children, className, fromUser, styleSet }) => ( -
- {children} +const ROOT_CSS = css({ + position: 'relative', + + '& > .webchat__bubble__content': { + // This is for hiding content outside of the bubble, for example, content outside of border radius + overflow: 'hidden' + }, + + '& > .webchat__bubble__nub': { + position: 'absolute' + } +}); + +const Bubble = ({ 'aria-hidden': ariaHidden, children, className, fromUser, nub, styleSet }) => ( +
+
{children}
+ {nub && !!(fromUser ? styleSet.options.bubbleFromUserNubSize : styleSet.options.bubbleNubSize) && ( +
+ )}
); @@ -14,7 +39,8 @@ Bubble.defaultProps = { 'aria-hidden': true, children: undefined, className: '', - fromUser: false + fromUser: false, + nub: true }; Bubble.propTypes = { @@ -22,6 +48,7 @@ Bubble.propTypes = { children: PropTypes.any, className: PropTypes.string, fromUser: PropTypes.bool, + nub: PropTypes.bool, styleSet: PropTypes.shape({ bubble: PropTypes.any.isRequired }).isRequired diff --git a/packages/component/src/Activity/CarouselFilmStrip.js b/packages/component/src/Activity/CarouselFilmStrip.js index 629ecf211d..c2b77f9454 100644 --- a/packages/component/src/Activity/CarouselFilmStrip.js +++ b/packages/component/src/Activity/CarouselFilmStrip.js @@ -84,6 +84,7 @@ const connectCarouselFilmStrip = (...selectors) => const WebChatCarouselFilmStrip = ({ activity, + avatarInitials, children, className, itemContainerRef, @@ -101,14 +102,20 @@ const WebChatCarouselFilmStrip = ({ const fromUser = role === 'user'; const activityDisplayText = messageBackDisplayText || text; + const indented = fromUser ? styleSet.options.bubbleFromUserNubSize : styleSet.options.bubbleNubSize; return ( -
+
{!!activityDisplayText && (
- + {children({ activity, attachment: { @@ -120,16 +127,16 @@ const WebChatCarouselFilmStrip = ({
)} -
    +
      {attachments.map((attachment, index) => (
    • - + {children({ attachment })}
    • ))}
    -
    +
    {state === SENDING || state === SEND_FAILED ? ( ) : ( @@ -142,6 +149,7 @@ const WebChatCarouselFilmStrip = ({ }; WebChatCarouselFilmStrip.defaultProps = { + avatarInitials: '', children: undefined, className: '', timestampClassName: '' @@ -163,6 +171,7 @@ WebChatCarouselFilmStrip.propTypes = { textFormat: PropTypes.string, timestamp: PropTypes.string }).isRequired, + avatarInitials: PropTypes.string, children: PropTypes.any, className: PropTypes.string, itemContainerRef: PropTypes.any.isRequired, diff --git a/packages/component/src/Activity/StackedLayout.js b/packages/component/src/Activity/StackedLayout.js index 14a9cb27c2..a45c7e8809 100644 --- a/packages/component/src/Activity/StackedLayout.js +++ b/packages/component/src/Activity/StackedLayout.js @@ -1,5 +1,5 @@ -/* eslint-disable no-sync */ /* eslint react/no-array-index-key: "off" */ +/* eslint-disable no-sync */ import { Constants } from 'botframework-webchat-core'; import { css } from 'glamor'; @@ -103,9 +103,22 @@ const StackedLayout = ({ activity, avatarInitials, children, language, styleSet, avatarInitials, plainText ); + const indented = fromUser ? styleSet.options.bubbleFromUserNubSize : styleSet.options.bubbleNubSize; return ( -
    +
    + {!avatarInitials && !!(fromUser ? styleSet.options.bubbleFromUserNubSize : styleSet.options.bubbleNubSize) && ( +
    + )}
    {type === 'typing' ? ( @@ -119,7 +132,7 @@ const StackedLayout = ({ activity, avatarInitials, children, language, styleSet, ) : ( !!activityDisplayText && (
    - + {children({ activity, attachment: { @@ -133,13 +146,16 @@ const StackedLayout = ({ activity, avatarInitials, children, language, styleSet, ) )} {attachments.map((attachment, index) => ( -
    - +
    + {children({ attachment })}
    ))} -
    +
    {showSendStatus ? ( ) : ( diff --git a/packages/component/src/Styles/StyleSet/Bubble.js b/packages/component/src/Styles/StyleSet/Bubble.js index af1534636f..eab72b68f5 100644 --- a/packages/component/src/Styles/StyleSet/Bubble.js +++ b/packages/component/src/Styles/StyleSet/Bubble.js @@ -1,33 +1,156 @@ +/* eslint no-magic-numbers: ["error", { "ignore": [-1, 0, 1, 2, 10] }] */ + +import Color from 'color'; + +function acuteNubSVG(nubSize = 10, backgroundColor, color, strokeWidth = 1, side = 'bot', upSideDown = false) { + const halfNubSize = nubSize / 2; + const halfStrokeWidth = strokeWidth / 2; + const horizontalTransform = + side === 'bot' ? '' : `translate(${halfNubSize} 0) scale(-1 1) translate(${-halfNubSize} 0)`; + const verticalTransform = upSideDown ? `translate(0 ${halfNubSize}) scale(1 -1) translate(0 ${-halfNubSize})` : ''; + + const p1 = [nubSize, halfStrokeWidth].join(' '); + const p2 = [strokeWidth, halfStrokeWidth].join(' '); + const p3 = [nubSize + strokeWidth, nubSize + halfStrokeWidth].join(' '); + + return ``; +} + +function isPositive(value) { + return 1 / value >= 0; +} + +function svgToDataURI(svg) { + return `data:image/svg+xml;utf8,${svg.replace(/"/gu, "'")}`; +} + export default function createBubbleStyle({ bubbleBackground, - bubbleBorder, + bubbleBorderColor, bubbleBorderRadius, + bubbleBorderStyle, + bubbleBorderWidth, bubbleFromUserBackground, - bubbleFromUserBorder, + bubbleFromUserBorderColor, bubbleFromUserBorderRadius, + bubbleFromUserBorderStyle, + bubbleFromUserBorderWidth, + bubbleFromUserNubOffset, + bubbleFromUserNubSize, bubbleFromUserTextColor, - bubbleMaxWidth, bubbleMinHeight, + bubbleNubOffset, + bubbleNubSize, bubbleTextColor, - messageActivityWordBreak + messageActivityWordBreak, + paddingRegular }) { + if (bubbleFromUserNubOffset === 'top') { + bubbleFromUserNubOffset = 0; + } else if (typeof bubbleFromUserNubOffset !== 'number') { + bubbleFromUserNubOffset = -0; + } + + if (bubbleNubOffset === 'top') { + bubbleNubOffset = 0; + } else if (typeof bubbleNubOffset !== 'number') { + bubbleNubOffset = -0; + } + + const botNubUpSideDown = !isPositive(bubbleNubOffset); + const userNubUpSideDown = !isPositive(bubbleFromUserNubOffset); + + const botNubSVG = acuteNubSVG( + bubbleNubSize, + bubbleBackground, + bubbleBorderColor, + bubbleBorderWidth, + 'bot', + botNubUpSideDown + ); + const userNubSVG = acuteNubSVG( + bubbleFromUserNubSize, + bubbleFromUserBackground, + bubbleFromUserBorderColor, + bubbleFromUserBorderWidth, + 'user', + userNubUpSideDown + ); + + const botNubCornerRadius = Math.min(bubbleBorderRadius, Math.abs(bubbleNubOffset)); + const userNubCornerRadius = Math.min(bubbleFromUserBorderRadius, Math.abs(bubbleFromUserNubOffset)); + return { - maxWidth: bubbleMaxWidth, - minHeight: bubbleMinHeight, - wordBreak: messageActivityWordBreak, + '& > .webchat__bubble__content': { + wordBreak: messageActivityWordBreak + }, '&:not(.from-user)': { - background: bubbleBackground, - border: bubbleBorder, - borderRadius: bubbleBorderRadius, - color: bubbleTextColor + '&.webchat__bubble_has_nub': { + '& > .webchat__bubble__content': bubbleNubSize ? { marginLeft: paddingRegular } : {} + }, + + '& > .webchat__bubble__content': { + background: bubbleBackground, + borderColor: bubbleBorderColor, + borderRadius: bubbleBorderRadius, + borderStyle: bubbleBorderStyle, + borderWidth: bubbleBorderWidth, + color: bubbleTextColor, + minHeight: bubbleMinHeight - bubbleBorderWidth * 2 + }, + + '&.webchat__bubble_has_nub > .webchat__bubble__content': { + // Hide border radius if there is a nub on the top/bottom left corner + ...(bubbleNubSize && botNubUpSideDown ? { borderBottomLeftRadius: botNubCornerRadius } : {}), + ...(bubbleNubSize && !botNubUpSideDown ? { borderTopLeftRadius: botNubCornerRadius } : {}) + }, + + '& > .webchat__bubble__nub': { + backgroundImage: `url("${svgToDataURI(botNubSVG).replace(/"/gu, "'")}")`, + bottom: isPositive(bubbleNubOffset) ? undefined : -bubbleNubOffset, + height: bubbleNubSize, + left: bubbleBorderWidth - bubbleNubSize + paddingRegular, + top: isPositive(bubbleNubOffset) ? bubbleNubOffset : undefined, + width: bubbleNubSize + } }, '&.from-user': { - background: bubbleFromUserBackground, - border: bubbleFromUserBorder, - borderRadius: bubbleFromUserBorderRadius, - color: bubbleFromUserTextColor + '&.webchat__bubble_has_nub': { + '& > .webchat__bubble__content': bubbleNubSize ? { marginRight: paddingRegular } : {} + }, + + '& > .webchat__bubble__content': { + background: bubbleFromUserBackground, + borderColor: bubbleFromUserBorderColor, + borderRadius: bubbleFromUserBorderRadius, + borderStyle: bubbleFromUserBorderStyle, + borderWidth: bubbleFromUserBorderWidth, + color: bubbleFromUserTextColor, + minHeight: bubbleMinHeight - bubbleFromUserBorderWidth * 2 + }, + + '&.webchat__bubble_has_nub > .webchat__bubble__content': { + // Hide border radius if there is a nub on the top/bottom right corner + ...(bubbleFromUserNubSize && userNubUpSideDown ? { borderBottomRightRadius: userNubCornerRadius } : {}), + ...(bubbleFromUserNubSize && !userNubUpSideDown ? { borderTopRightRadius: userNubCornerRadius } : {}) + }, + + '& > .webchat__bubble__nub': { + backgroundImage: `url("${svgToDataURI(userNubSVG).replace(/"/gu, "'")}")`, + height: bubbleFromUserNubSize, + right: bubbleFromUserBorderWidth - bubbleFromUserNubSize + paddingRegular, + bottom: isPositive(bubbleFromUserNubOffset) ? undefined : -bubbleFromUserNubOffset, + top: isPositive(bubbleFromUserNubOffset) ? bubbleFromUserNubOffset : undefined, + width: bubbleFromUserNubSize + } } }; } diff --git a/packages/component/src/Styles/StyleSet/CarouselFilmStrip.js b/packages/component/src/Styles/StyleSet/CarouselFilmStrip.js index 0cfeb21319..e40d679677 100644 --- a/packages/component/src/Styles/StyleSet/CarouselFilmStrip.js +++ b/packages/component/src/Styles/StyleSet/CarouselFilmStrip.js @@ -1,3 +1,5 @@ +/* eslint no-magic-numbers: ["error", { "ignore": [2] }] */ + export default function CarouselFilmStrip({ bubbleMaxWidth, bubbleMinWidth, paddingRegular }) { return { // Browser quirks: Firefox has no way to hide scrollbar and while keeping it in function @@ -6,19 +8,16 @@ export default function CarouselFilmStrip({ bubbleMaxWidth, bubbleMinWidth, padd marginBottom: -17 }, - '& > .avatar': { + paddingLeft: paddingRegular, + + '&.webchat__carousel_indented_content > .content': { marginLeft: paddingRegular }, '& > .content': { - '& > .message': { - marginLeft: paddingRegular - }, + paddingRight: paddingRegular, '& > ul': { - marginLeft: paddingRegular, - marginRight: paddingRegular, - '&:not(:first-child)': { marginTop: paddingRegular }, @@ -33,7 +32,7 @@ export default function CarouselFilmStrip({ bubbleMaxWidth, bubbleMinWidth, padd } }, - '& > .webchat__row': { + '& > .webchat__carousel__item_indented': { marginLeft: paddingRegular } } diff --git a/packages/component/src/Styles/StyleSet/StackedLayout.js b/packages/component/src/Styles/StyleSet/StackedLayout.js index a37f7226db..255035977a 100644 --- a/packages/component/src/Styles/StyleSet/StackedLayout.js +++ b/packages/component/src/Styles/StyleSet/StackedLayout.js @@ -1,7 +1,42 @@ +/* eslint no-magic-numbers: ["error", { "ignore": [2] }] */ + export default function createStackedLayoutStyle({ bubbleMaxWidth, bubbleMinWidth, paddingRegular }) { return { - marginLeft: paddingRegular, - marginRight: paddingRegular, + '&.webchat__stacked_extra_left_indent': { + marginLeft: paddingRegular * 2 + }, + + '&:not(.webchat__stacked_extra_left_indent)': { + marginLeft: paddingRegular + }, + + '&.webchat__stacked_extra_right_indent': { + marginRight: paddingRegular * 2 + }, + + '&:not(.webchat__stacked_extra_right_indent)': { + marginRight: paddingRegular + }, + + '&:not(.from-user)': { + '&.webchat__stacked_indented_content > .avatar': { + marginRight: paddingRegular + }, + + '& > .content > .webchat__stacked_item_indented': { + marginLeft: paddingRegular + } + }, + + '&.from-user': { + '&.webchat__stacked_indented_content > .avatar': { + marginLeft: paddingRegular + }, + + '& > .content > .webchat__stacked_item_indented': { + marginRight: paddingRegular + } + }, '& > .content': { '& > .webchat__row': { @@ -17,14 +52,6 @@ export default function createStackedLayoutStyle({ bubbleMaxWidth, bubbleMinWidt '& > *:not(:first-child):not(:last-child)': { marginTop: paddingRegular } - }, - - '&.from-user > .avatar': { - marginLeft: paddingRegular - }, - - '&:not(.from-user) > .avatar': { - marginRight: paddingRegular } }; } diff --git a/packages/component/src/Styles/createStyleSet.js b/packages/component/src/Styles/createStyleSet.js index ab50b1869a..53f668d28f 100644 --- a/packages/component/src/Styles/createStyleSet.js +++ b/packages/component/src/Styles/createStyleSet.js @@ -42,11 +42,72 @@ import defaultStyleOptions from './defaultStyleOptions'; // "styleSet" is actually CSS stylesheet and it is based on the DOM tree. // DOM tree may change from time to time, thus, maintaining "styleSet" becomes a constant effort. +function parseBorder(border) { + const dummyElement = document.createElement('div'); + + dummyElement.setAttribute('style', `border: ${border}`); + + const { + style: { borderColor: color, borderStyle: style, borderWidth: width } + } = dummyElement; + + return { + color, + style, + width + }; +} + +const PIXEL_UNIT_PATTERN = /^\d+px$/u; + export default function createStyleSet(options) { options = { ...defaultStyleOptions, ...options }; // Keep this list flat (no nested style) and serializable (no functions) + // TODO: [P4] Deprecate this code after bump to v5 + const { bubbleBorder, bubbleFromUserBorder } = options; + + if (bubbleBorder) { + console.warn( + 'Web Chat: styleSet.bubbleBorder is being deprecated. Please use bubbleBorderColor, bubbleBorderStyle, and, bubbleBorderWidth.' + ); + + const { color, style, width } = parseBorder(bubbleBorder); + + if (color && color !== 'initial') { + options.bubbleBorderColor = color; + } + + if (style && style !== 'initial') { + options.bubbleBorderStyle = style; + } + + if (PIXEL_UNIT_PATTERN.test(width)) { + options.bubbleBorderWidth = parseInt(width, 10); + } + } + + if (bubbleFromUserBorder) { + console.warn( + 'Web Chat: styleSet.bubbleFromUserBorder is being deprecated. Please use bubbleFromUserBorderColor, bubbleFromUserBorderStyle, and, bubbleFromUserBorderWidth.' + ); + + const { color, style, width } = parseBorder(bubbleFromUserBorder); + + if (color && color !== 'initial') { + options.bubbleFromUserBorderColor = color; + } + + if (style && style !== 'initial') { + options.bubbleFromUserBorderStyle = style; + } + + if (PIXEL_UNIT_PATTERN.test(width)) { + options.bubbleFromUserBorderWidth = parseInt(width, 10); + } + } + return { activities: createActivitiesStyle(options), activity: createActivityStyle(options), diff --git a/packages/component/src/Styles/defaultStyleOptions.js b/packages/component/src/Styles/defaultStyleOptions.js index 7b418dc99f..84f2e76f92 100644 --- a/packages/component/src/Styles/defaultStyleOptions.js +++ b/packages/component/src/Styles/defaultStyleOptions.js @@ -34,16 +34,24 @@ const DEFAULT_OPTIONS = { // Bubble bubbleBackground: 'White', - bubbleBorder: 'solid 1px #E6E6E6', + bubbleBorderColor: '#E6E6E6', bubbleBorderRadius: 2, + bubbleBorderStyle: 'solid', + bubbleBorderWidth: 1, bubbleFromUserBackground: 'White', - bubbleFromUserBorder: 'solid 1px #E6E6E6', + bubbleFromUserBorderColor: '#E6E6E6', bubbleFromUserBorderRadius: 2, + bubbleFromUserBorderStyle: 'solid', + bubbleFromUserBorderWidth: 1, + bubbleFromUserNubOffset: 'bottom', + bubbleFromUserNubSize: 0, bubbleFromUserTextColor: 'Black', bubbleImageHeight: 240, bubbleMaxWidth: 480, // screen width = 600px bubbleMinHeight: 40, bubbleMinWidth: 250, // min screen width = 300px, Edge requires 372px (https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/13621468/) + bubbleNubOffset: 'bottom', + bubbleNubSize: 0, bubbleTextColor: 'Black', // Markdown diff --git a/packages/playground/src/App.js b/packages/playground/src/App.js index d01a58a2d6..3ea8fc343f 100644 --- a/packages/playground/src/App.js +++ b/packages/playground/src/App.js @@ -55,6 +55,7 @@ export default class extends React.Component { super(props); this.handleBotAvatarInitialsChange = this.handleBotAvatarInitialsChange.bind(this); + this.handleBubbleBorderChange = this.handleBubbleBorderChange.bind(this); this.handleDisabledChange = this.handleDisabledChange.bind(this); this.handleDisconnectClick = this.handleDisconnectClick.bind(this); this.handleErrorClick = this.handleErrorClick.bind(this); @@ -63,20 +64,64 @@ export default class extends React.Component { this.handleLanguageChange = this.handleLanguageChange.bind(this); this.handleReliabilityChange = this.handleReliabilityChange.bind(this); this.handleResetClick = this.handleResetClick.bind(this); + this.handleRichCardWrapTitleChange = this.handleRichCardWrapTitleChange.bind(this); this.handleSendTimeoutChange = this.handleSendTimeoutChange.bind(this); this.handleSendTypingIndicatorChange = this.handleSendTypingIndicatorChange.bind(this); + this.handleShowNubChange = this.handleShowNubChange.bind(this); this.handleUseEmulatorCoreClick = this.handleUseEmulatorCoreClick.bind(this); this.handleUseMockBot = this.handleUseMockBot.bind(this); this.handleUserAvatarInitialsChange = this.handleUserAvatarInitialsChange.bind(this); + this.handleWordBreakChange = this.handleWordBreakChange.bind(this); this.mainRef = React.createRef(); this.activityMiddleware = createDevModeActivityMiddleware(); this.attachmentMiddleware = createDevModeAttachmentMiddleware(); - this.createMemoizedStyleOptions = memoize((hideSendBox, botAvatarInitials, userAvatarInitials) => ({ - botAvatarInitials, - hideSendBox, - userAvatarInitials - })); + this.createMemoizedStyleOptions = memoize( + ( + hideSendBox, + botAvatarInitials, + userAvatarInitials, + showNub, + styleBubbleBorder, + wordBreak, + richCardWrapTitle + ) => ({ + ...(styleBubbleBorder === 'deprecated' + ? { + bubbleBorder: 'dotted 2px Red', + bubbleBorderRadius: 10, + bubbleFromUserBorder: 'dashed 2px Green', + bubbleFromUserBorderRadius: 10 + } + : styleBubbleBorder + ? { + bubbleBorderColor: 'Red', + bubbleBorderRadius: 10, + bubbleBorderStyle: 'dotted', + bubbleBorderWidth: 2, + bubbleFromUserBorderColor: 'Green', + bubbleFromUserBorderRadius: 10, + bubbleFromUserBorderStyle: 'dashed', + bubbleFromUserBorderWidth: 2 + } + : {}), + + ...(showNub + ? { + bubbleFromUserNubSize: 10, + bubbleFromUserNubOffset: -5, + bubbleNubOffset: 5, + bubbleNubSize: 10 + } + : {}), + + botAvatarInitials, + hideSendBox, + userAvatarInitials, + messageActivityWordBreak: wordBreak, + richCardWrapTitle + }) + ); const params = new URLSearchParams(window.location.search); const directLineToken = params.get('t'); @@ -101,12 +146,16 @@ export default class extends React.Component { groupTimestamp: window.sessionStorage.getItem('PLAYGROUND_GROUP_TIMESTAMP'), hideSendBox: false, language: window.sessionStorage.getItem('PLAYGROUND_LANGUAGE') || '', + richCardWrapTitle: false, sendTimeout: window.sessionStorage.getItem('PLAYGROUND_SEND_TIMEOUT') || '', sendTypingIndicator: true, + showNub: true, + styleBubbleBorder: false, userAvatarInitials: 'WC', userID, username: 'Web Chat user', - webSpeechPonyfillFactory: undefined + webSpeechPonyfillFactory: undefined, + workBreak: '' }; } @@ -157,6 +206,10 @@ export default class extends React.Component { this.setState(() => ({ botAvatarInitials: value })); } + handleBubbleBorderChange({ target: { value } }) { + this.setState(() => ({ styleBubbleBorder: value === 'true' || (value === 'deprecated' && 'deprecated') })); + } + handleDisconnectClick() { this.props.store.dispatch({ type: 'DIRECT_LINE/DISCONNECT' }); } @@ -203,6 +256,10 @@ export default class extends React.Component { window.location.reload(); } + handleRichCardWrapTitleChange({ target: { checked } }) { + this.setState(() => ({ richCardWrapTitle: checked })); + } + handleSendTimeoutChange({ target: { value } }) { this.setState( () => ({ sendTimeout: value }), @@ -214,6 +271,10 @@ export default class extends React.Component { this.setState(() => ({ sendTypingIndicator: !!checked })); } + handleShowNubChange({ target: { checked } }) { + this.setState(() => ({ showNub: checked })); + } + handleUseEmulatorCoreClick() { window.sessionStorage.removeItem('REDUX_STORE'); window.location.href = '?domain=http://localhost:5000/v3/directline&websocket=0&u=default-user'; @@ -247,6 +308,10 @@ export default class extends React.Component { } } + handleWordBreakChange({ target: { value } }) { + this.setState(() => ({ wordBreak: value })); + } + render() { const { props: { store }, @@ -258,15 +323,27 @@ export default class extends React.Component { groupTimestamp, hideSendBox, language, + richCardWrapTitle, sendTimeout, sendTypingIndicator, + showNub, + styleBubbleBorder, userAvatarInitials, userID, username, - webSpeechPonyfillFactory + webSpeechPonyfillFactory, + wordBreak } } = this; - const styleOptions = this.createMemoizedStyleOptions(hideSendBox, botAvatarInitials, userAvatarInitials); + const styleOptions = this.createMemoizedStyleOptions( + hideSendBox, + botAvatarInitials, + userAvatarInitials, + showNub, + styleBubbleBorder, + wordBreak, + richCardWrapTitle + ); return (
    @@ -414,6 +491,43 @@ export default class extends React.Component { />
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    );