From e915e6a056cbb5071ce4503b6f17ad73e53c2927 Mon Sep 17 00:00:00 2001 From: Lukas Boehler Date: Thu, 8 Oct 2026 09:37:56 +0200 Subject: [PATCH] Inline Banner Not Working in App MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Session 838b19cb9112e4892679e2e9 ยท run on MacBook-Pro-von-Lukas --- CHANGELOG.md | 3 ++ src/GleapBannerManager.js | 42 ++++++++++++++++ src/GleapBannerManager.test.js | 92 ++++++++++++++++++++++++++++++++++ src/UI.js | 10 ++++ 4 files changed, 147 insertions(+) create mode 100644 src/GleapBannerManager.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 81b5e4bf..b8ef443e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/GleapBannerManager.js b/src/GleapBannerManager.js index 870f81a8..c3da0bad 100644 --- a/src/GleapBannerManager.js +++ b/src/GleapBannerManager.js @@ -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; @@ -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') { @@ -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() { diff --git a/src/GleapBannerManager.test.js b/src/GleapBannerManager.test.js new file mode 100644 index 00000000..761e95cf --- /dev/null +++ b/src/GleapBannerManager.test.js @@ -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); + }); +}); diff --git a/src/UI.js b/src/UI.js index 07a14247..a5757654 100644 --- a/src/UI.js +++ b/src/UI.js @@ -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;