diff --git a/.changeset/khaki-scissors-refuse.md b/.changeset/khaki-scissors-refuse.md new file mode 100644 index 000000000..fc83b149d --- /dev/null +++ b/.changeset/khaki-scissors-refuse.md @@ -0,0 +1,5 @@ +--- +'@lion/ui': patch +--- + +Export isIOS and isMacSafari functions as part of browserDetection utility diff --git a/.changeset/nice-spoons-rush.md b/.changeset/nice-spoons-rush.md new file mode 100644 index 000000000..8cabaeace --- /dev/null +++ b/.changeset/nice-spoons-rush.md @@ -0,0 +1,5 @@ +--- +'@lion/ui': patch +--- + +Use traditional styleSheet on IOS for overlays diff --git a/packages/ui/components/core/src/browserDetection.js b/packages/ui/components/core/src/browserDetection.js index 8aa417142..820564e2b 100644 --- a/packages/ui/components/core/src/browserDetection.js +++ b/packages/ui/components/core/src/browserDetection.js @@ -36,4 +36,12 @@ export const browserDetection = { isIOSChrome: checkChrome('ios'), isChromium: checkChrome('chromium'), isMac: navigator.appVersion.indexOf('Mac') !== -1, + isIOS: /iPhone|iPad|iPod/i.test(navigator.userAgent), + isMacSafari: + navigator.vendor && + navigator.vendor.indexOf('Apple') > -1 && + navigator.userAgent && + navigator.userAgent.indexOf('CriOS') === -1 && + navigator.userAgent.indexOf('FxiOS') === -1 && + navigator.appVersion.indexOf('Mac') !== -1, }; diff --git a/packages/ui/components/overlays/src/OverlaysManager.js b/packages/ui/components/overlays/src/OverlaysManager.js index bbeb4dfbd..610b0aa1f 100644 --- a/packages/ui/components/overlays/src/OverlaysManager.js +++ b/packages/ui/components/overlays/src/OverlaysManager.js @@ -1,3 +1,5 @@ +import { browserDetection } from '@lion/ui/core.js'; + /** * @typedef {import('lit').CSSResult} CSSResult * @typedef {import('./OverlayController.js').OverlayController} OverlayController @@ -5,19 +7,6 @@ import { overlayDocumentStyle } from './overlayDocumentStyle.js'; -// Export this as protected var, so that we can easily mock it in tests -// TODO: combine with browserDetection of core? -export const _browserDetection = { - isIOS: /iPhone|iPad|iPod/i.test(navigator.userAgent), - isMacSafari: - navigator.vendor && - navigator.vendor.indexOf('Apple') > -1 && - navigator.userAgent && - navigator.userAgent.indexOf('CriOS') === -1 && - navigator.userAgent.indexOf('FxiOS') === -1 && - navigator.appVersion.indexOf('Mac') !== -1, -}; - /** * `OverlaysManager` which manages overlays which are rendered into the body */ @@ -182,7 +171,7 @@ export class OverlaysManager { // eslint-disable-next-line class-methods-use-this requestToPreventScroll() { - const { isIOS, isMacSafari } = _browserDetection; + const { isIOS, isMacSafari } = browserDetection; // no check as classList will dedupe it anyways document.body.classList.add('overlays-scroll-lock'); if (isIOS || isMacSafari) { @@ -203,7 +192,7 @@ export class OverlaysManager { return; } - const { isIOS, isMacSafari } = _browserDetection; + const { isIOS, isMacSafari } = browserDetection; document.body.classList.remove('overlays-scroll-lock'); if (isIOS || isMacSafari) { document.body.classList.remove('overlays-scroll-lock-ios-fix'); diff --git a/packages/ui/components/overlays/src/utils/adopt-styles.js b/packages/ui/components/overlays/src/utils/adopt-styles.js index 20644d9a8..bdececfc4 100644 --- a/packages/ui/components/overlays/src/utils/adopt-styles.js +++ b/packages/ui/components/overlays/src/utils/adopt-styles.js @@ -1,3 +1,5 @@ +import { browserDetection } from '@lion/ui/core.js'; + // See: https://github.com/ing-bank/lion/issues/1880 /** @@ -106,7 +108,10 @@ export function adoptStyle(renderRoot, style, { teardown = false } = {}) { return; } - if (!_adoptStyleUtils.supportsAdoptingStyleSheets) { + // ios seems to have issues when using the adoptedStyleSheets where some styles are applied + // while others are ignored so the overlays are rendered incorrectly, to mitigate it we use + // traditional "stylesheet". + if (!_adoptStyleUtils.supportsAdoptingStyleSheets || browserDetection.isIOS) { adoptStyleWhenAdoptedStylesheetsNotSupported(renderRoot, style, { teardown }); return; } diff --git a/packages/ui/components/overlays/test-suites/OverlayMixin.suite.js b/packages/ui/components/overlays/test-suites/OverlayMixin.suite.js index 8b5d38436..912078952 100644 --- a/packages/ui/components/overlays/test-suites/OverlayMixin.suite.js +++ b/packages/ui/components/overlays/test-suites/OverlayMixin.suite.js @@ -4,7 +4,7 @@ import sinon from 'sinon'; import { overlays as overlaysManager, OverlayController } from '@lion/ui/overlays.js'; import '@lion/ui/define/lion-dialog.js'; -import { _browserDetection } from '../src/OverlaysManager.js'; +import { browserDetection } from '@lion/ui/core.js'; /** * @typedef {import('../types/OverlayConfig.js').OverlayConfig} OverlayConfig @@ -99,7 +99,7 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) { // For now, we skip this test for MacSafari, since the body.overlays-scroll-lock-ios-fix // class results in a scrollbar when preventsScroll is true. // However, fully functioning interacive elements (input fields) in the dialog are more important - if (_browserDetection.isMacSafari && elWithBigParent._overlayCtrl.preventsScroll) { + if (browserDetection.isMacSafari && elWithBigParent._overlayCtrl.preventsScroll) { return; } diff --git a/packages/ui/components/overlays/test/OverlaysManager.test.js b/packages/ui/components/overlays/test/OverlaysManager.test.js index e393865d1..6573f4277 100644 --- a/packages/ui/components/overlays/test/OverlaysManager.test.js +++ b/packages/ui/components/overlays/test/OverlaysManager.test.js @@ -1,7 +1,8 @@ import { expect, fixture } from '@open-wc/testing'; import { html } from 'lit/static-html.js'; +import sinon from 'sinon'; +import { browserDetection } from '@lion/ui/core.js'; import { OverlayController, OverlaysManager } from '@lion/ui/overlays.js'; -import { _browserDetection } from '../src/OverlaysManager.js'; /** * @typedef {import('../types/OverlayConfig.js').OverlayConfig} OverlayConfig @@ -101,24 +102,24 @@ describe('OverlaysManager', () => { }); describe('Browser/device edge cases', () => { - const isIOSOriginal = _browserDetection.isIOS; - const isMacSafariOriginal = _browserDetection.isMacSafari; + const isIOSDetectionStub = sinon.stub(browserDetection, 'isIOS'); + const isMacSafariDetectionStub = sinon.stub(browserDetection, 'isMacSafari'); function mockIOS() { - _browserDetection.isIOS = true; - _browserDetection.isMacSafari = false; + isIOSDetectionStub.value(true); + isMacSafariDetectionStub.value(false); } function mockMacSafari() { // When we are iOS - _browserDetection.isIOS = false; - _browserDetection.isMacSafari = true; + isIOSDetectionStub.value(false); + isMacSafariDetectionStub.value(true); } afterEach(() => { // Restore original values - _browserDetection.isIOS = isIOSOriginal; - _browserDetection.isMacSafari = isMacSafariOriginal; + isIOSDetectionStub.restore(); + isMacSafariDetectionStub.restore(); }); describe('When initialized with "preventsScroll: true"', () => { @@ -137,8 +138,8 @@ describe('OverlaysManager', () => { ); // When we are not iOS nor MacSafari - _browserDetection.isIOS = false; - _browserDetection.isMacSafari = false; + isIOSDetectionStub.value(false); + isMacSafariDetectionStub.value(false); const dialog2 = new OverlayController({ ...defaultOptions, preventsScroll: true }, mngr); await dialog2.show(); diff --git a/packages/ui/components/overlays/test/utils-tests/adopt-styles.test.js b/packages/ui/components/overlays/test/utils-tests/adopt-styles.test.js index a962e7aef..c8f98d291 100644 --- a/packages/ui/components/overlays/test/utils-tests/adopt-styles.test.js +++ b/packages/ui/components/overlays/test/utils-tests/adopt-styles.test.js @@ -1,4 +1,5 @@ import { expect } from '@open-wc/testing'; +import { browserDetection } from '@lion/ui/core.js'; import { css } from 'lit'; import sinon from 'sinon'; import { @@ -164,6 +165,66 @@ describe('adoptStyle()', () => { }); }); +describe('Fallback when IOS user opens overlay', () => { + it('adds a "traditional" stylesheet to the body', async () => { + const browserDetectionStub = sinon.stub(browserDetection, 'isIOS').value(true); + + const myCssResult = css` + .check { + color: blue; + } + `; + const root = document; + adoptStyle(root, myCssResult); + + const sheets = Array.from(document.body.querySelectorAll('style')); + const lastAddedSheet = sheets[sheets.length - 1]; + expect(lastAddedSheet.textContent).to.equal(myCssResult.cssText); + browserDetectionStub.restore(); + }); + + describe('Teardown', () => { + it('removes a "traditional" stylesheet from the shadowRoot', async () => { + const browserDetectionStub = sinon.stub(browserDetection, 'isIOS').value(true); + + const { shadowHost, cleanupShadowHost } = createShadowHost(); + const myCssResult = css` + .check { + color: blue; + } + `; + const root = /** @type {ShadowRoot} */ (shadowHost.shadowRoot); + + adoptStyle(root, myCssResult); + const sheets1 = Array.from(root.querySelectorAll('style')); + const lastAddedSheet1 = sheets1[sheets1.length - 1]; + expect(lastAddedSheet1.textContent).to.equal(myCssResult.cssText); + adoptStyle(root, myCssResult, { teardown: true }); + const sheets2 = Array.from(root.querySelectorAll('style')); + const lastAddedSheet2 = sheets2[sheets2.length - 1]; + expect(lastAddedSheet2?.textContent).to.not.equal(myCssResult.cssText); + + const myCSSStyleSheet = new CSSStyleSheet(); + myCSSStyleSheet.insertRule('.check { color: blue; }'); + + adoptStyle(root, myCSSStyleSheet); + const sheets3 = Array.from(root.querySelectorAll('style')); + const lastAddedSheet3 = sheets3[sheets3.length - 1]; + expect(lastAddedSheet3.textContent).to.equal( + serializeConstructableStylesheet(myCSSStyleSheet), + ); + adoptStyle(root, myCSSStyleSheet, { teardown: true }); + const sheets4 = Array.from(root.querySelectorAll('style')); + const lastAddedSheet4 = sheets4[sheets4.length - 1]; + expect(lastAddedSheet4?.textContent).to.not.equal(myCSSStyleSheet); + + cleanupShadowHost(); + + browserDetectionStub.restore(); + }); + }); +}); + describe('adoptStyles()', () => { it('calls "adoptStyle" for all entries in CSSResult|CSSStylesheet[]', async () => { const spy = sinon.spy(_adoptStyleUtils, 'adoptStyle');