diff --git a/packages/icon/src/vaadin-icon-font-size-mixin.d.ts b/packages/icon/src/vaadin-icon-font-size-mixin.d.ts new file mode 100644 index 0000000000..a147db0bef --- /dev/null +++ b/packages/icon/src/vaadin-icon-font-size-mixin.d.ts @@ -0,0 +1,16 @@ +/** + * @license + * Copyright (c) 2021 - 2025 Vaadin Ltd. + * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/ + */ +import type { Constructor } from '@open-wc/dedupe-mixin'; + +/** + * Mixin which enables the font icon sizing fallback for browsers that do not support CSS Container Queries. + * The mixin does nothing if the browser supports CSS Container Query units for pseudo elements. + */ +export declare function IconFontSizeMixin>( + base: T, +): Constructor & T; + +export declare class IconFontSizeMixinClass {} diff --git a/packages/icon/src/vaadin-icon-font-size-mixin.js b/packages/icon/src/vaadin-icon-font-size-mixin.js new file mode 100644 index 0000000000..5f78eb4b39 --- /dev/null +++ b/packages/icon/src/vaadin-icon-font-size-mixin.js @@ -0,0 +1,63 @@ +/** + * @license + * Copyright (c) 2021 - 2025 Vaadin Ltd. + * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/ + */ +import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js'; +import { css, registerStyles } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js'; +import { needsFontIconSizingFallback } from './vaadin-icon-helpers.js'; + +const usesFontIconSizingFallback = needsFontIconSizingFallback(); + +if (usesFontIconSizingFallback) { + registerStyles( + 'vaadin-icon', + css` + :host::after, + :host::before { + font-size: var(--_vaadin-font-icon-size); + } + `, + 'vaadin-icon-font-size-mixin-styles', + ); +} + +/** + * Mixin which enables the font icon sizing fallback for browsers that do not support CSS Container Queries. + * The mixin does nothing if the browser supports CSS Container Query units for pseudo elements. + * + * @polymerMixin + */ +export const IconFontSizeMixin = (superclass) => + !usesFontIconSizingFallback + ? superclass + : class extends ResizeMixin(superclass) { + updated(props) { + super.updated(props); + if (props.has('char') || props.has('iconClass') || props.has('ligature')) { + this.__updateFontIconSize(); + } + } + + /** + * @protected + * @override + */ + _onResize() { + // Update when the element is resized + this.__updateFontIconSize(); + } + + /** + * Updates the --_vaadin-font-icon-size CSS variable value if font icons are used. + * + * @private + */ + __updateFontIconSize() { + if (this.char || this.iconClass || this.ligature) { + const { paddingTop, paddingBottom, height } = getComputedStyle(this); + const fontIconSize = parseFloat(height) - parseFloat(paddingTop) - parseFloat(paddingBottom); + this.style.setProperty('--_vaadin-font-icon-size', `${fontIconSize}px`); + } + } + }; diff --git a/packages/icon/src/vaadin-icon-helpers.js b/packages/icon/src/vaadin-icon-helpers.js new file mode 100644 index 0000000000..7d9a7a59e5 --- /dev/null +++ b/packages/icon/src/vaadin-icon-helpers.js @@ -0,0 +1,62 @@ +/** + * @license + * Copyright (c) 2016 - 2025 Vaadin Ltd. + * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/ + */ + +import { isSafari } from '@vaadin/component-base/src/browser-utils.js'; + +/** + * Checks if the current browser supports CSS Container Query units for pseudo elements. + * i.e. if the fix for https://bugs.webkit.org/show_bug.cgi?id=253939 is available. + */ +export function supportsCQUnitsForPseudoElements() { + const testStyle = document.createElement('style'); + testStyle.textContent = ` + .vaadin-icon-test-element { + container-type: size; + height: 2px; + visibility: hidden; + position: fixed; + } + + .vaadin-icon-test-element::before { + content: ''; + display: block; + height: 100cqh; + `; + const testElement = document.createElement('div'); + testElement.classList.add('vaadin-icon-test-element'); + + const shadowParent = document.createElement('div'); + shadowParent.attachShadow({ mode: 'open' }); + shadowParent.shadowRoot.innerHTML = ''; + shadowParent.append(testElement.cloneNode()); + + document.body.append(testStyle, testElement, shadowParent); + + const needsFallback = [...document.querySelectorAll('.vaadin-icon-test-element')].find( + (el) => getComputedStyle(el, '::before').height !== '2px', + ); + + testStyle.remove(); + testElement.remove(); + shadowParent.remove(); + return !needsFallback; +} + +/** + * Checks if the current browser needs a fallback for sizing font icons instead of relying on CSS Container Queries. + */ +export function needsFontIconSizingFallback() { + if (!CSS.supports('container-type: inline-size')) { + // The browser does not support CSS Container Queries at all. + return true; + } + if (!isSafari) { + // Browsers other than Safari support CSS Container Queries as expected. + return false; + } + // Check if the browser does not support CSS Container Query units for pseudo elements. + return !supportsCQUnitsForPseudoElements(); +} diff --git a/packages/icon/src/vaadin-icon-mixin.d.ts b/packages/icon/src/vaadin-icon-mixin.d.ts index 325240105d..4b14c1f873 100644 --- a/packages/icon/src/vaadin-icon-mixin.d.ts +++ b/packages/icon/src/vaadin-icon-mixin.d.ts @@ -5,6 +5,7 @@ */ import type { Constructor } from '@open-wc/dedupe-mixin'; import type { SlotStylesMixinClass } from '@vaadin/component-base/src/slot-styles-mixin.js'; +import type { IconFontSizeMixinClass } from './vaadin-icon-font-size-mixin.js'; import type { IconSvgLiteral } from './vaadin-icon-svg.js'; /** @@ -12,7 +13,7 @@ import type { IconSvgLiteral } from './vaadin-icon-svg.js'; */ export declare function IconMixin>( base: T, -): Constructor & Constructor & T; +): Constructor & Constructor & Constructor & T; export declare class IconMixinClass { /** diff --git a/packages/icon/src/vaadin-icon-mixin.js b/packages/icon/src/vaadin-icon-mixin.js index 4475b46910..d39e74c608 100644 --- a/packages/icon/src/vaadin-icon-mixin.js +++ b/packages/icon/src/vaadin-icon-mixin.js @@ -5,6 +5,7 @@ */ import { SlotStylesMixin } from '@vaadin/component-base/src/slot-styles-mixin.js'; import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.js'; +import { IconFontSizeMixin } from './vaadin-icon-font-size-mixin.js'; import { unsafeSvgLiteral } from './vaadin-icon-svg.js'; const srcCache = new Map(); @@ -14,9 +15,10 @@ const Iconset = customElements.get('vaadin-iconset'); /** * @polymerMixin * @mixes SlotStylesMixin + * @mixes IconFontSizeMixin */ export const IconMixin = (superClass) => - class extends SlotStylesMixin(superClass) { + class extends IconFontSizeMixin(SlotStylesMixin(superClass)) { static get properties() { return { /** diff --git a/packages/icon/test/icon-font.test.js b/packages/icon/test/icon-font.test.js index 3f1d70448a..52c0a3440d 100644 --- a/packages/icon/test/icon-font.test.js +++ b/packages/icon/test/icon-font.test.js @@ -1,6 +1,7 @@ import { expect } from '@vaadin/chai-plugins'; -import { fixtureSync, nextFrame, nextResize } from '@vaadin/testing-helpers'; +import { fixtureSync, isChrome, nextFrame, nextResize } from '@vaadin/testing-helpers'; import '../src/vaadin-icon.js'; +import { needsFontIconSizingFallback, supportsCQUnitsForPseudoElements } from '../src/vaadin-icon-helpers.js'; import { iconFontCss } from './test-icon-font.js'; describe('vaadin-icon - icon fonts', () => { @@ -267,4 +268,83 @@ describe('vaadin-icon - icon fonts', () => { expect(['"My icons 6"', 'My icons 6']).to.include(fontIconStyle.fontFamily); }); }); + + // These tests make sure that the heavy container query fallback is only used + // when font icons are used. + describe('container query fallback', () => { + // Tests for browsers that require the fallback + const fallBackIt = needsFontIconSizingFallback() ? it : it.skip; + // Tests for browsers that we know for sure not to require the fallback + const supportedIt = isChrome ? it : it.skip; + + let icon; + + supportedIt('should support CQ width units on pseudo elements', () => { + expect(supportsCQUnitsForPseudoElements()).to.be.true; + }); + + supportedIt('should not need the fallback', () => { + expect(needsFontIconSizingFallback()).to.be.false; + }); + + fallBackIt('should not support CQ width units on pseudo elements', () => { + expect(supportsCQUnitsForPseudoElements()).to.be.false; + }); + + fallBackIt('should have the custom property (iconClass)', async () => { + icon = fixtureSync(''); + await nextFrame(); + expect(icon.style.getPropertyValue('--_vaadin-font-icon-size')).to.equal('24px'); + }); + + fallBackIt('should have the custom property (char)', async () => { + icon = fixtureSync(''); + await nextFrame(); + expect(icon.style.getPropertyValue('--_vaadin-font-icon-size')).to.equal('24px'); + }); + + fallBackIt('should not have the custom property', async () => { + icon = fixtureSync(''); + await nextFrame(); + expect(icon.style.getPropertyValue('--_vaadin-font-icon-size')).to.equal(''); + }); + + fallBackIt('should set the custom property', async () => { + icon = fixtureSync(''); + await nextFrame(); + icon.iconClass = 'foo'; + expect(icon.style.getPropertyValue('--_vaadin-font-icon-size')).to.equal('24px'); + }); + + fallBackIt('should update the custom property', async () => { + icon = fixtureSync(''); + await nextFrame(); + icon.style.width = '100px'; + icon.style.height = '100px'; + await nextResize(icon); + expect(icon.style.getPropertyValue('--_vaadin-font-icon-size')).to.equal('100px'); + }); + + fallBackIt('should not update the custom property', async () => { + icon = fixtureSync(''); + await nextFrame(); + icon.style.width = '100px'; + icon.style.height = '100px'; + await nextFrame(icon); + await nextFrame(icon); + expect(icon.style.getPropertyValue('--_vaadin-font-icon-size')).to.equal(''); + }); + + fallBackIt('should have the same height as the host with shadow root', async () => { + const parent = fixtureSync('
'); + parent.attachShadow({ mode: 'open' }); + parent.shadowRoot.innerHTML = ''; + + parent.append(icon); + await nextResize(icon); + + const fontIconStyle = getComputedStyle(icon, ':before'); + expect(parseInt(fontIconStyle.height)).to.be.closeTo(24, 1); + }); + }); });