Skip to content

[MOB-11427] Add maxWidth to getInAppMessages payload #491

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2360,12 +2360,24 @@ For example:
- If your in-app is positioned in the center and your browser if at 700px, your
in-app message will grow to take up 100% of the screen.

This chart also implies that yout in-app message is taking 100% of its container.
This chart also implies that your in-app message is taking 100% of its container.
Your results may vary if you add, for example, a `max-width: 200px` CSS rule to
your message HTML.
your message HTML, as it would not apply to the enclosing iframe.

Regardless of how you write your CSS, these rules take effect. So, when creating
an in-app message, it is best to stick with percentage-based CSS widths.
For Center, Top-Right, and Bottom-Right positions, you can set a custom `maxWidth`
by including it in your getInAppMessages payload like this:

```ts
const { request } = getInAppMessages({
count: 20,
packageName: 'my-website',
maxWidth: '900px'
});
```

`maxWidth` accepts any valid CSS max-width value (e.g., px, em, %, ch).
It is optional—-most in-app messages render well with default sizing, so set it
only if needed for your use case.

## How do I add custom callbacks to handle link clicks on in-app and embedded messages?

Expand Down
19 changes: 11 additions & 8 deletions src/inapp/inapp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export function getInAppMessages(
delete dupedPayload.animationDuration;
delete dupedPayload.handleLinks;
delete dupedPayload.closeButton;
delete dupedPayload.maxWidth;

if (options?.display) {
addStyleSheet(document, ANIMATION_STYLESHEET(payload.animationDuration));
Expand Down Expand Up @@ -159,15 +160,17 @@ export function getInAppMessages(
};

/* add the message's html to an iframe and paint it to the DOM */
return paintIFrame(
activeMessage.content.html as string,
messagePosition,
return paintIFrame({
html: activeMessage.content.html as string,
position: messagePosition,
shouldAnimate,
payload.onOpenScreenReaderMessage || 'in-app iframe message opened',
payload.topOffset,
payload.bottomOffset,
payload.rightOffset
).then((activeIframe) => {
srMessage:
payload.onOpenScreenReaderMessage || 'in-app iframe message opened',
topOffset: payload.topOffset,
bottomOffset: payload.bottomOffset,
rightOffset: payload.rightOffset,
maxWidth: payload.maxWidth
}).then((activeIframe) => {
const activeIframeDocument = activeIframe?.contentDocument;

const throttledResize =
Expand Down
197 changes: 104 additions & 93 deletions src/inapp/tests/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -717,147 +717,157 @@ describe('Utils', () => {
});

describe('painting the iframe', () => {
const defaultStyles = {
position: 'fixed',
left: '0%',
right: '0%',
top: '0%',
bottom: '0%',
zIndex: '9999',
width: '100%',
maxHeight: '95vh',
maxWidth: '100%'
};

const checkStyles = (
styles: CSSStyleDeclaration,
overrides?: Partial<typeof defaultStyles> & { height?: string }
) => {
Object.entries(defaultStyles).forEach(([key, value]) => {
const styleValue = styles[key as keyof typeof styles];
expect(styleValue).toBe(
overrides && key in overrides
? overrides[key as keyof typeof overrides]
: value
);
});
};

it('should paint the iframe in the center of the screen', async () => {
const iframe = await paintIFrame(
mockMarkup,
DisplayPosition.Center,
false,
'hi'
);
const iframe = await paintIFrame({
html: mockMarkup,
position: DisplayPosition.Center,
srMessage: 'hi'
});
jest.advanceTimersByTime(2000);

/* speed up time to past the setTimeout */
const styles = getComputedStyle(iframe);
expect(styles.position).toBe('fixed');
expect(styles.left).toBe('0%');
expect(styles.right).toBe('0%');
expect(styles.top).toBe('0%');
expect(styles.bottom).toBe('0%');
expect(styles.zIndex).toBe('9999');
expect(styles.width).toBe('100%');
expect(styles.maxHeight).toBe('95vh');
checkStyles(styles);
});

it('should paint the iframe with custom maxWidth', async () => {
const { Center, TopRight, BottomRight } = DisplayPosition;
[Center, TopRight, BottomRight].forEach(async (position) => {
const iframe = await paintIFrame({
html: mockMarkup,
position,
maxWidth: '350px'
});
jest.advanceTimersByTime(2000);
const styles = getComputedStyle(iframe);
expect(styles.maxWidth).toBe('350px');
});
});

it('should paint the iframe in the top-right of the screen', async () => {
const iframe = await paintIFrame(
mockMarkup,
DisplayPosition.TopRight,
false,
'hi'
);
const iframe = await paintIFrame({
html: mockMarkup,
position: DisplayPosition.TopRight,
srMessage: 'hi'
});
jest.advanceTimersByTime(2000);

/* speed up time to past the setTimeout */
const styles = getComputedStyle(iframe);
expect(styles.position).toBe('fixed');
expect(styles.left).toBe('');
expect(styles.right).toBe('0%');
expect(styles.top).toBe('0%');
expect(styles.bottom).toBe('');
expect(styles.zIndex).toBe('9999');
expect(styles.width).toBe('100%');
expect(styles.maxHeight).toBe('95vh');
checkStyles(styles, { left: '', bottom: '' });
});

it('should paint the iframe in the bottom-right of the screen', async () => {
const iframe = await paintIFrame(
mockMarkup,
DisplayPosition.BottomRight,
false,
'hi'
);
const iframe = await paintIFrame({
html: mockMarkup,
position: DisplayPosition.BottomRight,
srMessage: 'hi'
});
jest.advanceTimersByTime(2000);

/* speed up time to past the setTimeout */
const styles = getComputedStyle(iframe);
expect(styles.position).toBe('fixed');
expect(styles.left).toBe('');
expect(styles.right).toBe('0%');
expect(styles.bottom).toBe('0%');
expect(styles.top).toBe('');
expect(styles.zIndex).toBe('9999');
expect(styles.width).toBe('100%');
expect(styles.maxHeight).toBe('95vh');
checkStyles(styles, { left: '', top: '' });
});

it('should paint the iframe full-screen', async () => {
const iframe = await paintIFrame(
mockMarkup,
DisplayPosition.Full,
false,
''
);
const iframe = await paintIFrame({
html: mockMarkup,
position: DisplayPosition.Full
});
jest.advanceTimersByTime(2000);

/* speed up time to past the setTimeout */
const styles = getComputedStyle(iframe);
expect(styles.position).toBe('fixed');
expect(styles.left).toBe('0%');
expect(styles.right).toBe('');
expect(styles.bottom).toBe('');
expect(styles.top).toBe('0%');
expect(styles.zIndex).toBe('9999');
expect(styles.height).toBe('100%');
expect(styles.width).toBe('100%');
expect(styles.maxHeight).toBe('');
checkStyles(styles, {
right: '',
bottom: '',
height: '100%',
maxHeight: ''
});
});

it('should paint TopRight iframes with custom offsets', async () => {
const iframe = await paintIFrame(
mockMarkup,
DisplayPosition.TopRight,
false,
'',
'10px',
'10px',
'10px'
);
const iframe = await paintIFrame({
html: mockMarkup,
position: DisplayPosition.TopRight,
topOffset: '10px',
bottomOffset: '10px',
rightOffset: '10px'
});
jest.advanceTimersByTime(2000);

/* speed up time to past the setTimeout */
const styles = getComputedStyle(iframe);
expect(styles.position).toBe('fixed');
expect(styles.left).toBe('');
expect(styles.right).toBe('10px');
expect(styles.bottom).toBe('');
expect(styles.top).toBe('10px');
expect(styles.zIndex).toBe('9999');
expect(styles.width).toBe('100%');
expect(styles.maxHeight).toBe('95vh');
checkStyles(styles, {
left: '',
right: '10px',
bottom: '',
top: '10px'
});
});

it('should paint BottomRight iframes with custom offsets', async () => {
const iframe = await paintIFrame(
mockMarkup,
DisplayPosition.BottomRight,
false,
'',
'10px',
'10px',
'10px'
);
const iframe = await paintIFrame({
html: mockMarkup,
position: DisplayPosition.BottomRight,
topOffset: '10px',
bottomOffset: '10px',
rightOffset: '10px'
});
jest.advanceTimersByTime(2000);

/* speed up time to past the setTimeout */
const styles = getComputedStyle(iframe);
expect(styles.position).toBe('fixed');
expect(styles.left).toBe('');
expect(styles.right).toBe('10px');
expect(styles.bottom).toBe('10px');
expect(styles.top).toBe('');
expect(styles.zIndex).toBe('9999');
expect(styles.width).toBe('100%');
expect(styles.maxHeight).toBe('95vh');
checkStyles(styles, {
left: '',
right: '10px',
bottom: '10px',
top: ''
});
});

it('should call srSpeak if screen reader text passed', async () => {
await paintIFrame(mockMarkup, DisplayPosition.Center, false, 'hi');
await paintIFrame({
html: mockMarkup,
position: DisplayPosition.Center,
srMessage: 'hi'
});

expect((srSpeak as any).mock.calls.length).toBe(1);
});

it('should not call srSpeak if no screen reader text passed', async () => {
await paintIFrame(mockMarkup, DisplayPosition.Center, false);
await paintIFrame({
html: mockMarkup,
position: DisplayPosition.Center
});

expect((srSpeak as any).mock.calls.length).toBe(0);
});
Expand All @@ -869,6 +879,7 @@ describe('Utils', () => {

expect(el.getAttribute('aria-label')).toBe('hello');
expect(el.getAttribute('role')).toBe('button');
// eslint-disable-next-line no-script-url
expect(el.getAttribute('href')).toBe('javascript:undefined');
});

Expand Down
Loading
Loading