Skip to content
Open
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
16 changes: 16 additions & 0 deletions packages/icon/src/vaadin-icon-font-size-mixin.d.ts
Original file line number Diff line number Diff line change
@@ -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<T extends Constructor<HTMLElement>>(
base: T,
): Constructor<IconFontSizeMixinClass> & T;

export declare class IconFontSizeMixinClass {}
63 changes: 63 additions & 0 deletions packages/icon/src/vaadin-icon-font-size-mixin.js
Original file line number Diff line number Diff line change
@@ -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`);
}
}
};
62 changes: 62 additions & 0 deletions packages/icon/src/vaadin-icon-helpers.js
Original file line number Diff line number Diff line change
@@ -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 = '<slot></slot>';
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();
}
3 changes: 2 additions & 1 deletion packages/icon/src/vaadin-icon-mixin.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
*/
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';

/**
* A mixin providing common icon functionality.
*/
export declare function IconMixin<T extends Constructor<HTMLElement>>(
base: T,
): Constructor<IconMixinClass> & Constructor<SlotStylesMixinClass> & T;
): Constructor<IconFontSizeMixinClass> & Constructor<IconMixinClass> & Constructor<SlotStylesMixinClass> & T;

export declare class IconMixinClass {
/**
Expand Down
4 changes: 3 additions & 1 deletion packages/icon/src/vaadin-icon-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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 {
/**
Expand Down
82 changes: 81 additions & 1 deletion packages/icon/test/icon-font.test.js
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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('<vaadin-icon icon-class="foo" style="--vaadin-icon-size: 24px"></vaadin-icon>');
await nextFrame();
expect(icon.style.getPropertyValue('--_vaadin-font-icon-size')).to.equal('24px');
});

fallBackIt('should have the custom property (char)', async () => {
icon = fixtureSync('<vaadin-icon char="foo" style="--vaadin-icon-size: 24px"></vaadin-icon>');
await nextFrame();
expect(icon.style.getPropertyValue('--_vaadin-font-icon-size')).to.equal('24px');
});

fallBackIt('should not have the custom property', async () => {
icon = fixtureSync('<vaadin-icon></vaadin-icon>');
await nextFrame();
expect(icon.style.getPropertyValue('--_vaadin-font-icon-size')).to.equal('');
});

fallBackIt('should set the custom property', async () => {
icon = fixtureSync('<vaadin-icon style="--vaadin-icon-size: 24px"></vaadin-icon>');
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('<vaadin-icon icon-class="foo"></vaadin-icon>');
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('<vaadin-icon></vaadin-icon>');
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('<div></div>');
parent.attachShadow({ mode: 'open' });
parent.shadowRoot.innerHTML = '<slot></slot>';

parent.append(icon);
await nextResize(icon);

const fontIconStyle = getComputedStyle(icon, ':before');
expect(parseInt(fontIconStyle.height)).to.be.closeTo(24, 1);
});
});
});