Skip to content

Commit

Permalink
Add bubbleImageMaxHeight and bubbleImageMinHeight (#5236)
Browse files Browse the repository at this point in the history
* Add bubbleImageMaxHeight and bubbleImageMinHeight

* Update PR number

* Default bubbleImageMinHeight to 240px

* Update breaking change for bubbleImageMinHeight

* Add test for freestyle

* Fixed width but cropped height

* Fix tests

* Add aspect ratio to test images

* Add CSS variable name
  • Loading branch information
compulim authored Aug 6, 2024
1 parent c312f7f commit 516e73e
Show file tree
Hide file tree
Showing 25 changed files with 387 additions and 98 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,18 @@ Notes: web developers are advised to use [`~` (tilde range)](https://github.com/

## [Unreleased]

### Breaking changes

- `styleOptions.bubbleImageHeight` is being deprecated in favor of `styleOptions.bubbleImageMaxHeight` and `styleOptions.bubbleImageMinHeight`. The option will be removed on or after 2026-07-05

## [4.18.0] - 2024-07-10

### Added

- (Experimental) Added initial decorators support, in PR [#5205](https://github.com/microsoft/BotFramework-WebChat/pull/5205), by [@OEvgeny](https://github.com/OEvgeny)
- Introduced internal `botframework-webchat-api/decorator` import, in PR [#5205](https://github.com/microsoft/BotFramework-WebChat/pull/5205), by [@OEvgeny](https://github.com/OEvgeny)
- Added `DecoratorComposer` and `ActivityDecorator` to be used for decorating activity border, in PR [#5205](https://github.com/microsoft/BotFramework-WebChat/pull/5205), by [@OEvgeny](https://github.com/OEvgeny)
- Added `styleOptions.bubbleImageMaxHeight` and `styleOptions.bubbleImageMinHeight` for variable image height, in PR [#5236](https://github.com/microsoft/BotFramework-WebChat/pull/5236), by [@compulim](https://github.com/compulim)

### Fixed

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.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
Expand Down Expand Up @@ -51,12 +51,12 @@
await pageConditions.minNumActivitiesShown(2);

// Replace the animation with a static image.
document
.querySelector('.webchat__bubble__content img')
.setAttribute(
for (const imageElement of document.querySelectorAll('.webchat__bubble__content img')) {
imageElement.setAttribute(
'src',
'https://raw.githubusercontent.com/compulim/BotFramework-MockBot/master/public/assets/surface1.jpg'
);
}

await pageConditions.allImagesLoaded();
await pageConditions.allOutgoingActivitiesSent();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<!doctype html>
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
<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>
</head>
<body>
<script>
function expectWidthHeight(element, width, height) {
try {
expect(Math.abs(element.clientWidth - width) <= 1).toBe(true);
} catch {
expect(element.clientWidth).toBe(width); // Better message.
}
}

run(async function () {
const container = document.createElement('main');

container.setAttribute('id', 'webchat');

const { directLine, store } = testHelpers.createDirectLineEmulator();

const render = overrideProps => WebChat.renderWebChat({ directLine, store, ...overrideProps }, container);

render({
styleOptions: {
bubbleImageMaxHeight: 240,
bubbleImageMinHeight: 180
}
});

document.body.append(container);

await host.windowSize(360, 1280, document.getElementById('webchat'));

await pageConditions.uiConnected();

// Generated from https://placeholder.pics/svg/320x180.
const LANDSCAPE_SVG = `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="320" height="180"><rect x="2" y="2" width="316" height="176" style="fill:%23DEDEDE;stroke:%23555555;stroke-width:2"/><text x="50%" y="50%" font-size="18" text-anchor="middle" alignment-baseline="middle" font-family="monospace, sans-serif" fill="%23555555">320×180 (16:9)</text></svg>`;

// Generated from https://placeholder.pics/svg/180x320.
const PORTRAIT_SVG = `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="180" height="320"><rect x="2" y="2" width="176" height="316" style="fill:%23DEDEDE;stroke:%23555555;stroke-width:2"/><text x="50%" y="50%" font-size="18" text-anchor="middle" alignment-baseline="middle" font-family="monospace, sans-serif" fill="%23555555">180×320 (9:16)</text></svg>`;

// Generated from https://placeholder.pics/svg/640x180.
const WIDE_SVG = `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="640" height="180"><rect x="2" y="2" width="636" height="176" style="fill:%23DEDEDE;stroke:%23555555;stroke-width:2"/><text x="50%" y="50%" font-size="18" text-anchor="middle" alignment-baseline="middle" font-family="monospace, sans-serif" fill="%23555555">640×180 (32:9)</text></svg>`;

await directLine.emulateIncomingActivity({
attachments: [
{
contentType: 'image/svg+xml',
contentUrl: WIDE_SVG,
thumbnailUrl: WIDE_SVG
},
{
contentType: 'image/svg+xml',
contentUrl: LANDSCAPE_SVG,
thumbnailUrl: LANDSCAPE_SVG
},
{
contentType: 'image/svg+xml',
contentUrl: PORTRAIT_SVG,
thumbnailUrl: PORTRAIT_SVG
}
],
from: { role: 'user' },
text: 'Images with varies `bubbleImageMaxHeight` and `bubbleImageMinHeight`.',
type: 'message'
});

await pageConditions.allImagesLoaded();
await pageConditions.scrollToBottomCompleted();

const imageElements = pageElements.activities()[0].querySelectorAll('.webchat__fixed-width-image');
const wideImage = imageElements[0];
const landscapeImage = imageElements[1];
const portraitImage = imageElements[2];

expectWidthHeight(wideImage, 339, 180);
expectWidthHeight(landscapeImage, 339, 339 / 16 / 9);
expectWidthHeight(portraitImage, 339, 240);

await host.snapshot();

render({
styleOptions: {
bubbleImageMinHeight: 240,
bubbleImageMaxHeight: 360
}
});

await pageConditions.scrollToBottomCompleted();

await pageConditions.became('image resized', () => wideImage.clientHeight === 240, 5_000);

expectWidthHeight(wideImage, 338, 240);
expectWidthHeight(landscapeImage, 338, 240);
expectWidthHeight(portraitImage, 338, 360);

await host.snapshot();

render({
styleOptions: {
bubbleImageMinHeight: 120,
bubbleImageMaxHeight: 180
}
});

await pageConditions.scrollToBottomCompleted();

await pageConditions.became('image resized', () => wideImage.clientHeight === 120, 5_000);

expectWidthHeight(wideImage, 338, 120);
expectWidthHeight(landscapeImage, 338, 180);
expectWidthHeight(portraitImage, 338, 180);

await host.snapshot();

render({
styleOptions: {
bubbleImageMinHeight: 0,
bubbleImageMaxHeight: Infinity
}
});

await pageConditions.scrollToBottomCompleted();

await pageConditions.became('image resized', () => Math.abs(wideImage.clientHeight - 95) <= 1, 5_000);

expectWidthHeight(wideImage, 338, 338 / (32 / 9));
expectWidthHeight(landscapeImage, 338, 338 / (16 / 9));
expectWidthHeight(portraitImage, 338, 338 / (9 / 16));

await host.snapshot();
});
</script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */

describe('styleOptions.bubbleImageMinMaxHeight', () => {
test('should resize images properly', () =>
runHTML('styleOptions/bubbleImageHeight/bubbleImageHeight.comprehensive'));
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<!doctype html>
<html lang="en-US">
<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
<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>
</head>
<body>
<script>
function expectWidthHeight(element, width, height) {
try {
expect(Math.abs(element.clientWidth - width) <= 1).toBe(true);
} catch {
expect(element.clientWidth).toBe(width); // Better message.
}
}

run(
async function () {
const container = document.createElement('main');

container.setAttribute('id', 'webchat');

const { directLine, store } = testHelpers.createDirectLineEmulator();

WebChat.renderWebChat(
{
directLine,
store,
styleOptions: { bubbleImageHeight: 240, bubbleImageMaxHeight: 10, bubbleImageMinHeight: 10 }
},
container
);

document.body.append(container);

await host.windowSize(360, 1280, document.getElementById('webchat'));

await pageConditions.uiConnected();

// Generated from https://placeholder.pics/svg/320x180.
const LANDSCAPE_SVG = `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="320" height="180"><rect x="2" y="2" width="316" height="176" style="fill:%23DEDEDE;stroke:%23555555;stroke-width:2"/><text x="50%" y="50%" font-size="18" text-anchor="middle" alignment-baseline="middle" font-family="monospace, sans-serif" fill="%23555555">320×180 (16:9)</text></svg>`;

// Generated from https://placeholder.pics/svg/180x320.
const PORTRAIT_SVG = `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="180" height="320"><rect x="2" y="2" width="176" height="316" style="fill:%23DEDEDE;stroke:%23555555;stroke-width:2"/><text x="50%" y="50%" font-size="18" text-anchor="middle" alignment-baseline="middle" font-family="monospace, sans-serif" fill="%23555555">180×320 (9:16)</text></svg>`;

// Generated from https://placeholder.pics/svg/640x180.
const WIDE_SVG = `data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="640" height="180"><rect x="2" y="2" width="636" height="176" style="fill:%23DEDEDE;stroke:%23555555;stroke-width:2"/><text x="50%" y="50%" font-size="18" text-anchor="middle" alignment-baseline="middle" font-family="monospace, sans-serif" fill="%23555555">640×180 (32:9)</text></svg>`;

await directLine.emulateIncomingActivity({
attachments: [
{
contentType: 'image/svg+xml',
contentUrl: WIDE_SVG,
thumbnailUrl: WIDE_SVG
},
{
contentType: 'image/svg+xml',
contentUrl: LANDSCAPE_SVG,
thumbnailUrl: LANDSCAPE_SVG
},
{
contentType: 'image/svg+xml',
contentUrl: PORTRAIT_SVG,
thumbnailUrl: PORTRAIT_SVG
}
],
from: { role: 'user' },
text: 'Images with fixed `bubbleImageHeight`.',
type: 'message'
});

await pageConditions.allImagesLoaded();

const imageElements = pageElements.activities()[0].querySelectorAll('.webchat__fixed-width-image');
const wideImage = imageElements[0];
const landscapeImage = imageElements[1];
const portraitImage = imageElements[2];

expectWidthHeight(wideImage, 338, 240);
expectWidthHeight(landscapeImage, 338, 240);
expectWidthHeight(portraitImage, 338, 240);

await host.snapshot();
},
{ expectDeprecations: true }
);
</script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */

describe('styleOptions.bubbleImageHeight', () => {
test('should resize images with deprecation', () =>
runHTML('styleOptions/bubbleImageHeight/bubbleImageHeight.deprecated'));
});
31 changes: 29 additions & 2 deletions packages/api/src/StyleOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,32 @@ type StyleOptions = {
bubbleFromUserNubSize?: number;

bubbleFromUserTextColor?: string;
bubbleImageHeight?: number;

/**
* Specifies the fixed height of the bubble for image, default to unset.
*
* @deprecated Use `bubbleImageMaxHeight` and `bubbleImageMinHeight` instead. To mimick behavior before deprecation, set both options to 240px.
*/
bubbleImageHeight?: number | undefined;

/**
* Specifies the maximum height of the bubble for image, default to 240px.
*
* CSS variable: `--webchat__max-height--image-bubble`.
*
* New in 4.18.0.
*/
bubbleImageMaxHeight?: number | undefined;

/**
* Specifies the minimum height of the bubble for image, default to 240px.
*
* CSS variable: `--webchat__min-height--image-bubble`.
*
* New in 4.18.0.
*/
bubbleImageMinHeight?: number | undefined;

bubbleMaxWidth?: number;
bubbleMinHeight?: number;
bubbleMinWidth?: number;
Expand Down Expand Up @@ -853,7 +878,9 @@ type StyleOptions = {
// 1. Allow developers to set the "bubbleNubOffset" option as "top" (string), but when we normalize them, we will convert it to 0 (number);
// 2. Renamed/deprecated options, only the newer option will be kept, the older option will be dropped.
// Internally, no code should use the deprecated value except the migration code.
type StrictStyleOptions = Required<Omit<StyleOptions, 'hideScrollToEndButton' | 'newMessagesButtonFontSize'>> & {
type StrictStyleOptions = Required<
Omit<StyleOptions, 'bubbleImageHeight' | 'hideScrollToEndButton' | 'newMessagesButtonFontSize'>
> & {
bubbleFromUserNubOffset: number;
bubbleNubOffset: number;
emojiSet: false | Record<string, string>;
Expand Down
4 changes: 3 additions & 1 deletion packages/api/src/defaultStyleOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ const DEFAULT_OPTIONS: Required<StyleOptions> = {
bubbleFromUserNubOffset: 0,
bubbleFromUserNubSize: undefined,
bubbleFromUserTextColor: 'Black',
bubbleImageHeight: 240,
bubbleImageHeight: undefined,
bubbleImageMaxHeight: 240, // Based on previously default `bubbleImageHeight` of 240px.
bubbleImageMinHeight: 240, // TODO: Should change to 180px. Based on 320px bubble width showing a 16:9 image, or `320 / (16 / 9)`. 320px bubble width is based on 360px wide of the chat canvas.
bubbleMaxWidth: 480, // Based off screen width = 600px
bubbleMinHeight: 40,
bubbleMinWidth: 250, // min screen width = 300px; Microsoft Edge requires 372px (https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/13621468/)
Expand Down
13 changes: 13 additions & 0 deletions packages/api/src/normalizeStyleOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { warnOnce } from 'botframework-webchat-core';
import defaultStyleOptions from './defaultStyleOptions';
import StyleOptions, { StrictStyleOptions } from './StyleOptions';

const bubbleImageHeightDeprecation = warnOnce(
'"styleOptions.bubbleImageHeight" has been deprecated. Use "styleOptions.bubbleImageMaxHeight" and "styleOptions.bubbleImageMinHeight" instead. This deprecation migration will be removed on or after 2026-07-05.'
);

const hideScrollToEndButtonDeprecation = warnOnce(
'"styleOptions.hideScrollToEndButton" has been deprecated. To hide scroll to end button, set "scrollToEndBehavior" to false. This deprecation migration will be removed on or after 2023-06-02.'
);
Expand Down Expand Up @@ -177,6 +181,15 @@ export default function normalizeStyleOptions({
options.suggestedActionTextColorOnDisabled || options.suggestedActionDisabledTextColor;
}

if (options.bubbleImageHeight) {
bubbleImageHeightDeprecation();

filledOptions.bubbleImageMaxHeight = options.bubbleImageHeight;
filledOptions.bubbleImageMinHeight = options.bubbleImageHeight;

filledOptions.bubbleImageHeight = undefined;
}

return {
...filledOptions,
bubbleFromUserNubOffset: normalizedBubbleFromUserNubOffset,
Expand Down
Loading

0 comments on commit 516e73e

Please sign in to comment.