fix(ui): use traditional style on ios for adopt-styles

This commit is contained in:
Iván Gómez Alonso 2023-11-24 12:02:06 +01:00 committed by GitHub
parent ef9b1e4c52
commit 322b06521c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 103 additions and 29 deletions

View file

@ -0,0 +1,5 @@
---
'@lion/ui': patch
---
Export isIOS and isMacSafari functions as part of browserDetection utility

View file

@ -0,0 +1,5 @@
---
'@lion/ui': patch
---
Use traditional styleSheet on IOS for overlays

View file

@ -36,4 +36,12 @@ export const browserDetection = {
isIOSChrome: checkChrome('ios'), isIOSChrome: checkChrome('ios'),
isChromium: checkChrome('chromium'), isChromium: checkChrome('chromium'),
isMac: navigator.appVersion.indexOf('Mac') !== -1, 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,
}; };

View file

@ -1,3 +1,5 @@
import { browserDetection } from '@lion/ui/core.js';
/** /**
* @typedef {import('lit').CSSResult} CSSResult * @typedef {import('lit').CSSResult} CSSResult
* @typedef {import('./OverlayController.js').OverlayController} OverlayController * @typedef {import('./OverlayController.js').OverlayController} OverlayController
@ -5,19 +7,6 @@
import { overlayDocumentStyle } from './overlayDocumentStyle.js'; 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 * `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 // eslint-disable-next-line class-methods-use-this
requestToPreventScroll() { requestToPreventScroll() {
const { isIOS, isMacSafari } = _browserDetection; const { isIOS, isMacSafari } = browserDetection;
// no check as classList will dedupe it anyways // no check as classList will dedupe it anyways
document.body.classList.add('overlays-scroll-lock'); document.body.classList.add('overlays-scroll-lock');
if (isIOS || isMacSafari) { if (isIOS || isMacSafari) {
@ -203,7 +192,7 @@ export class OverlaysManager {
return; return;
} }
const { isIOS, isMacSafari } = _browserDetection; const { isIOS, isMacSafari } = browserDetection;
document.body.classList.remove('overlays-scroll-lock'); document.body.classList.remove('overlays-scroll-lock');
if (isIOS || isMacSafari) { if (isIOS || isMacSafari) {
document.body.classList.remove('overlays-scroll-lock-ios-fix'); document.body.classList.remove('overlays-scroll-lock-ios-fix');

View file

@ -1,3 +1,5 @@
import { browserDetection } from '@lion/ui/core.js';
// See: https://github.com/ing-bank/lion/issues/1880 // See: https://github.com/ing-bank/lion/issues/1880
/** /**
@ -106,7 +108,10 @@ export function adoptStyle(renderRoot, style, { teardown = false } = {}) {
return; 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 }); adoptStyleWhenAdoptedStylesheetsNotSupported(renderRoot, style, { teardown });
return; return;
} }

View file

@ -4,7 +4,7 @@ import sinon from 'sinon';
import { overlays as overlaysManager, OverlayController } from '@lion/ui/overlays.js'; import { overlays as overlaysManager, OverlayController } from '@lion/ui/overlays.js';
import '@lion/ui/define/lion-dialog.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 * @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 // 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. // class results in a scrollbar when preventsScroll is true.
// However, fully functioning interacive elements (input fields) in the dialog are more important // 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; return;
} }

View file

@ -1,7 +1,8 @@
import { expect, fixture } from '@open-wc/testing'; import { expect, fixture } from '@open-wc/testing';
import { html } from 'lit/static-html.js'; 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 { OverlayController, OverlaysManager } from '@lion/ui/overlays.js';
import { _browserDetection } from '../src/OverlaysManager.js';
/** /**
* @typedef {import('../types/OverlayConfig.js').OverlayConfig} OverlayConfig * @typedef {import('../types/OverlayConfig.js').OverlayConfig} OverlayConfig
@ -101,24 +102,24 @@ describe('OverlaysManager', () => {
}); });
describe('Browser/device edge cases', () => { describe('Browser/device edge cases', () => {
const isIOSOriginal = _browserDetection.isIOS; const isIOSDetectionStub = sinon.stub(browserDetection, 'isIOS');
const isMacSafariOriginal = _browserDetection.isMacSafari; const isMacSafariDetectionStub = sinon.stub(browserDetection, 'isMacSafari');
function mockIOS() { function mockIOS() {
_browserDetection.isIOS = true; isIOSDetectionStub.value(true);
_browserDetection.isMacSafari = false; isMacSafariDetectionStub.value(false);
} }
function mockMacSafari() { function mockMacSafari() {
// When we are iOS // When we are iOS
_browserDetection.isIOS = false; isIOSDetectionStub.value(false);
_browserDetection.isMacSafari = true; isMacSafariDetectionStub.value(true);
} }
afterEach(() => { afterEach(() => {
// Restore original values // Restore original values
_browserDetection.isIOS = isIOSOriginal; isIOSDetectionStub.restore();
_browserDetection.isMacSafari = isMacSafariOriginal; isMacSafariDetectionStub.restore();
}); });
describe('When initialized with "preventsScroll: true"', () => { describe('When initialized with "preventsScroll: true"', () => {
@ -137,8 +138,8 @@ describe('OverlaysManager', () => {
); );
// When we are not iOS nor MacSafari // When we are not iOS nor MacSafari
_browserDetection.isIOS = false; isIOSDetectionStub.value(false);
_browserDetection.isMacSafari = false; isMacSafariDetectionStub.value(false);
const dialog2 = new OverlayController({ ...defaultOptions, preventsScroll: true }, mngr); const dialog2 = new OverlayController({ ...defaultOptions, preventsScroll: true }, mngr);
await dialog2.show(); await dialog2.show();

View file

@ -1,4 +1,5 @@
import { expect } from '@open-wc/testing'; import { expect } from '@open-wc/testing';
import { browserDetection } from '@lion/ui/core.js';
import { css } from 'lit'; import { css } from 'lit';
import sinon from 'sinon'; import sinon from 'sinon';
import { 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()', () => { describe('adoptStyles()', () => {
it('calls "adoptStyle" for all entries in CSSResult|CSSStylesheet[]', async () => { it('calls "adoptStyle" for all entries in CSSResult|CSSStylesheet[]', async () => {
const spy = sinon.spy(_adoptStyleUtils, 'adoptStyle'); const spy = sinon.spy(_adoptStyleUtils, 'adoptStyle');