Skip to content
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## Unreleased
Fixed: inline banners did not show on pages whose `body` clips its overflow, the usual app-shell layout (`html, body { height: 100%; overflow: hidden }`). The banner sits in the body's top margin, which the body cut off, so only an empty strip appeared above the page. On such pages the banner now goes into the body's top padding (added to the page's own padding), which the body does not clip; with `box-sizing: border-box` the app also keeps fitting the viewport instead of losing the banner's height at the bottom. Pages that scroll normally and floating banners are unchanged.

## 19.1.1
Fixed: the product tour and tooltip visual editor now works from the dashboard on app.gleap.ai. The SDK accepts the editor's messages from exactly `https://app.gleap.ai` and `https://app.gleap.io` (no lookalike domains), loads the editor from the dashboard that opened it, and sends its replies only to that dashboard.

Expand Down
42 changes: 42 additions & 0 deletions src/GleapBannerManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@ import Gleap, { GleapFrameManager } from './Gleap';
import { isOutboundActionBlocked } from './GleapCaptureSettings';
import { bootstrapGleapFrame } from './GleapHelper';

const clipsOverflow = (style) => {
if (!style) {
return false;
}

return [style.overflowX || style.overflow, style.overflowY || style.overflow].some(
(value) => !!value && value !== 'visible'
);
};

/**
* Whether body clips content outside its padding box. Body's overflow only clips when the root
* element's overflow is not `visible`; otherwise it is propagated to the viewport.
*/
export const bodyClipsInlineBanner = (htmlStyle, bodyStyle) => clipsOverflow(htmlStyle) && clipsOverflow(bodyStyle);

export default class GleapBannerManager {
bannerUrl = 'https://outboundmedia.gleap.io';
bannerContainer = null;
Expand Down Expand Up @@ -61,6 +77,8 @@ export default class GleapBannerManager {

if (this.bannerData?.format === 'floating') {
document.body.classList.add('gleap-b-f');
} else {
this.applyInlineClipLayout();
}
}
if (data.name === 'banner-close') {
Expand Down Expand Up @@ -111,6 +129,30 @@ export default class GleapBannerManager {

document.body.classList.remove('gleap-b-shown');
document.body.classList.remove('gleap-b-f');
document.body.classList.remove('gleap-b-clip');
}

/**
* The inline banner sits in body's top margin, above body's padding box. On pages whose
* body clips its overflow (app shells with `html, body { overflow: hidden }`) that area is
* cut off, so only an empty strip shows. There the banner moves into body's top padding,
* which body does not clip, on top of the page's own padding.
*/
applyInlineClipLayout() {
// Already applied: body's padding now includes the banner, so it is no longer the page's own.
if (document.body.classList.contains('gleap-b-clip')) {
return;
}

try {
const bodyStyle = window.getComputedStyle(document.body);
if (!bodyClipsInlineBanner(window.getComputedStyle(document.documentElement), bodyStyle)) {
return;
}

document.documentElement.style.setProperty('--gleap-b-body-padding-top', bodyStyle.paddingTop || '0px');
document.body.classList.add('gleap-b-clip');
} catch (exp) {}
}

disable() {
Expand Down
92 changes: 92 additions & 0 deletions src/GleapBannerManager.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* @jest-environment jsdom
*/
import GleapBannerManager, { bodyClipsInlineBanner } from './GleapBannerManager';

jest.mock('./Gleap', () => ({
__esModule: true,
default: {},
GleapFrameManager: { getInstance: jest.fn(() => ({ urlHandler: jest.fn() })) },
}));

jest.mock('./GleapHelper', () => ({
bootstrapGleapFrame: jest.fn(),
}));

const style = (overflowX, overflowY = overflowX) => ({ overflowX, overflowY });

describe('bodyClipsInlineBanner', () => {
test('body overflow is propagated to the viewport while the root is visible', () => {
expect(bodyClipsInlineBanner(style('visible'), style('hidden'))).toBe(false);
expect(bodyClipsInlineBanner(style('visible'), style('visible'))).toBe(false);
});

test('a visible body never clips', () => {
expect(bodyClipsInlineBanner(style('hidden'), style('visible'))).toBe(false);
});

test('body clips when both root and body overflow are not visible', () => {
expect(bodyClipsInlineBanner(style('hidden'), style('hidden'))).toBe(true);
expect(bodyClipsInlineBanner(style('auto'), style('clip'))).toBe(true);
expect(bodyClipsInlineBanner(style('hidden'), style('visible', 'auto'))).toBe(true);
});

test('missing styles do not clip', () => {
expect(bodyClipsInlineBanner(null, style('hidden'))).toBe(false);
expect(bodyClipsInlineBanner(style('hidden'), {})).toBe(false);
});
});

describe('GleapBannerManager inline clip layout', () => {
const manager = GleapBannerManager.getInstance();
let styleEl;

const show = (format) => {
manager.injectBannerUI({ format });
const frame = document.querySelector('.gleap-b-frame');
window.dispatchEvent(
new MessageEvent('message', {
data: JSON.stringify({ type: 'BANNER', name: 'banner-data-set' }),
source: frame.contentWindow,
})
);
};

afterEach(() => {
manager.removeBannerUI();
styleEl?.remove();
styleEl = null;
document.documentElement.style.removeProperty('--gleap-b-body-padding-top');
});

const setPageCss = (css) => {
styleEl = document.createElement('style');
styleEl.textContent = css;
document.head.appendChild(styleEl);
};

test('app shells that clip body get the padding layout, keeping their own padding', () => {
setPageCss('html, body { height: 100%; overflow: hidden; } body { padding-top: 12px; }');
show('inline');
expect(document.body.classList.contains('gleap-b-shown')).toBe(true);
expect(document.body.classList.contains('gleap-b-clip')).toBe(true);
expect(document.documentElement.style.getPropertyValue('--gleap-b-body-padding-top')).toBe('12px');

manager.removeBannerUI();
expect(document.body.classList.contains('gleap-b-clip')).toBe(false);
});

test('scrolling pages keep the margin layout', () => {
setPageCss('body { overflow: hidden; }');
show('inline');
expect(document.body.classList.contains('gleap-b-shown')).toBe(true);
expect(document.body.classList.contains('gleap-b-clip')).toBe(false);
});

test('floating banners are not affected', () => {
setPageCss('html, body { overflow: hidden; }');
show('floating');
expect(document.body.classList.contains('gleap-b-f')).toBe(true);
expect(document.body.classList.contains('gleap-b-clip')).toBe(false);
});
});
10 changes: 10 additions & 0 deletions src/UI.js
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,16 @@ export const injectStyledCSS = (
margin-top: 0px;
}

.gleap-b-shown.gleap-b-clip {
transition: padding 0.3s ease-out;
margin-top: 0px;
padding-top: calc(var(--gleap-margin-top) + var(--gleap-b-body-padding-top, 0px));
}

.gleap-b-clip .gleap-b {
top: 0px;
}

.gleap-b {
display: none;
position: absolute;
Expand Down
Loading