Skip to content

Commit 68e9c93

Browse files
authored
fix: restore icon size workaround for Safari 26 (#10296)
1 parent 5606cf0 commit 68e9c93

File tree

7 files changed

+233
-4
lines changed

7 files changed

+233
-4
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* @license
3+
* Copyright (c) 2021 - 2025 Vaadin Ltd.
4+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5+
*/
6+
import type { Constructor } from '@open-wc/dedupe-mixin';
7+
8+
/**
9+
* Mixin which enables the font icon sizing fallback for browsers that do not support CSS Container Queries.
10+
* In older versions of Safari, it didn't support Container Queries units used in pseudo-elements. It has been fixed in
11+
* recent versions, but there's an regression in Safari 26, which caused the same issue to happen when the icon is
12+
* attached to an element with shadow root.
13+
* The mixin does nothing if the browser supports CSS Container Query units for pseudo elements.
14+
*/
15+
export declare function IconFontSizeMixin<T extends Constructor<HTMLElement>>(
16+
base: T,
17+
): Constructor<IconFontSizeMixinClass> & T;
18+
19+
export declare class IconFontSizeMixinClass {}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* @license
3+
* Copyright (c) 2021 - 2025 Vaadin Ltd.
4+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5+
*/
6+
import { css } from 'lit';
7+
import { ResizeMixin } from '@vaadin/component-base/src/resize-mixin.js';
8+
import { needsFontIconSizingFallback } from './vaadin-icon-helpers.js';
9+
10+
const usesFontIconSizingFallback = needsFontIconSizingFallback();
11+
12+
/**
13+
* Mixin which enables the font icon sizing fallback for browsers that do not support CSS Container Queries.
14+
* In older versions of Safari, it didn't support Container Queries units used in pseudo-elements. It has been fixed in
15+
* recent versions, but there's an regression in Safari 26, which caused the same issue to happen when the icon is
16+
* attached to an element with shadow root.
17+
* The mixin does nothing if the browser supports CSS Container Query units for pseudo elements.
18+
*
19+
* @polymerMixin
20+
*/
21+
export const IconFontSizeMixin = (superclass) =>
22+
!usesFontIconSizingFallback
23+
? superclass
24+
: class extends ResizeMixin(superclass) {
25+
static get styles() {
26+
return css`
27+
:host::after,
28+
:host::before {
29+
font-size: var(--vaadin-icon-visual-size, var(--_vaadin-font-icon-size));
30+
}
31+
`;
32+
}
33+
34+
/** @protected */
35+
updated(props) {
36+
super.updated(props);
37+
38+
if (props.has('char') || props.has('iconClass') || props.has('ligature')) {
39+
this.__updateFontIconSize();
40+
}
41+
}
42+
43+
/**
44+
* @protected
45+
* @override
46+
*/
47+
_onResize() {
48+
// Update when the element is resized
49+
this.__updateFontIconSize();
50+
}
51+
52+
/**
53+
* Updates the --_vaadin-font-icon-size CSS variable value if font icons are used.
54+
*
55+
* @private
56+
*/
57+
__updateFontIconSize() {
58+
if (this.char || this.iconClass || this.ligature) {
59+
const { paddingTop, paddingBottom, height } = getComputedStyle(this);
60+
const fontIconSize = parseFloat(height) - parseFloat(paddingTop) - parseFloat(paddingBottom);
61+
this.style.setProperty('--_vaadin-font-icon-size', `${fontIconSize}px`);
62+
}
63+
}
64+
};
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* @license
3+
* Copyright (c) 2021 - 2025 Vaadin Ltd.
4+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
5+
*/
6+
import { isSafari } from '@vaadin/component-base/src/browser-utils.js';
7+
8+
/**
9+
* Checks if the current browser supports CSS Container Query units for pseudo elements.
10+
* i.e. if the fix for https://bugs.webkit.org/show_bug.cgi?id=253939 is available.
11+
*/
12+
export function supportsCQUnitsForPseudoElements() {
13+
const testStyle = document.createElement('style');
14+
testStyle.textContent = `
15+
.vaadin-icon-test-element {
16+
container-type: size;
17+
height: 2px;
18+
visibility: hidden;
19+
position: fixed;
20+
}
21+
22+
.vaadin-icon-test-element::before {
23+
content: '';
24+
display: block;
25+
height: 100cqh;
26+
`;
27+
const testElement = document.createElement('div');
28+
testElement.classList.add('vaadin-icon-test-element');
29+
30+
const shadowParent = document.createElement('div');
31+
shadowParent.attachShadow({ mode: 'open' });
32+
shadowParent.shadowRoot.innerHTML = '<slot></slot>';
33+
shadowParent.append(testElement.cloneNode());
34+
35+
document.body.append(testStyle, testElement, shadowParent);
36+
37+
const needsFallback = [...document.querySelectorAll('.vaadin-icon-test-element')].find(
38+
(el) => getComputedStyle(el, '::before').height !== '2px',
39+
);
40+
41+
testStyle.remove();
42+
testElement.remove();
43+
shadowParent.remove();
44+
return !needsFallback;
45+
}
46+
47+
/**
48+
* Checks if the current browser needs a fallback for sizing font icons instead of relying on CSS Container Queries.
49+
*/
50+
export function needsFontIconSizingFallback() {
51+
if (!CSS.supports('container-type: inline-size')) {
52+
// The browser does not support CSS Container Queries at all.
53+
return true;
54+
}
55+
if (!isSafari) {
56+
// Browsers other than Safari support CSS Container Queries as expected.
57+
return false;
58+
}
59+
// Check if the browser does not support CSS Container Query units for pseudo elements.
60+
return !supportsCQUnitsForPseudoElements();
61+
}

packages/icon/src/vaadin-icon-mixin.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
55
*/
66
import type { Constructor } from '@open-wc/dedupe-mixin';
77
import type { SlotStylesMixinClass } from '@vaadin/component-base/src/slot-styles-mixin.js';
8+
import type { IconFontSizeMixinClass } from './vaadin-icon-font-size-mixin.js';
89
import type { IconSvgLiteral } from './vaadin-icon-svg.js';
910

1011
/**
1112
* A mixin providing common icon functionality.
1213
*/
1314
export declare function IconMixin<T extends Constructor<HTMLElement>>(
1415
base: T,
15-
): Constructor<IconMixinClass> & Constructor<SlotStylesMixinClass> & T;
16+
): Constructor<IconFontSizeMixinClass> & Constructor<IconMixinClass> & Constructor<SlotStylesMixinClass> & T;
1617

1718
export declare class IconMixinClass {
1819
/**

packages/icon/src/vaadin-icon-mixin.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
import { SlotStylesMixin } from '@vaadin/component-base/src/slot-styles-mixin.js';
77
import { TooltipController } from '@vaadin/component-base/src/tooltip-controller.js';
8+
import { IconFontSizeMixin } from './vaadin-icon-font-size-mixin.js';
89
import { unsafeSvgLiteral } from './vaadin-icon-svg.js';
910

1011
const srcCache = new Map();
@@ -14,9 +15,10 @@ const Iconset = customElements.get('vaadin-iconset');
1415
/**
1516
* @polymerMixin
1617
* @mixes SlotStylesMixin
18+
* @mixes IconFontSizeMixin
1719
*/
1820
export const IconMixin = (superClass) =>
19-
class extends SlotStylesMixin(superClass) {
21+
class extends IconFontSizeMixin(SlotStylesMixin(superClass)) {
2022
static get properties() {
2123
return {
2224
/**

packages/icon/src/vaadin-icon.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ class Icon extends IconMixin(ElementMixin(ThemableMixin(PolylitMixin(LumoInjecti
8484
}
8585

8686
static get styles() {
87-
return iconStyles;
87+
// Apply `super.styles` only if the fallback is used
88+
return [iconStyles, super.styles].filter(Boolean);
8889
}
8990

9091
static get lumoInjector() {

packages/icon/test/icon-font.test.js

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { expect } from '@vaadin/chai-plugins';
2-
import { fixtureSync, nextFrame, nextResize } from '@vaadin/testing-helpers';
2+
import { fixtureSync, isChrome, nextFrame, nextResize } from '@vaadin/testing-helpers';
33
import '../src/vaadin-icon.js';
4+
import { needsFontIconSizingFallback, supportsCQUnitsForPseudoElements } from '../src/vaadin-icon-helpers.js';
45
import { iconFontCss } from './test-icon-font.js';
56

67
describe('vaadin-icon - icon fonts', () => {
@@ -267,4 +268,84 @@ describe('vaadin-icon - icon fonts', () => {
267268
expect(['"My icons 6"', 'My icons 6']).to.include(fontIconStyle.fontFamily);
268269
});
269270
});
271+
272+
// These tests make sure that the heavy container query fallback is only used
273+
// when font icons are used.
274+
describe('container query fallback', () => {
275+
// Tests for browsers that require the fallback
276+
const fallBackIt = needsFontIconSizingFallback() ? it : it.skip;
277+
// Tests for browsers that we know for sure not to require the fallback
278+
const supportedIt = isChrome ? it : it.skip;
279+
280+
let icon;
281+
282+
supportedIt('should support CQ width units on pseudo elements', () => {
283+
expect(supportsCQUnitsForPseudoElements()).to.be.true;
284+
});
285+
286+
supportedIt('should not need the fallback', () => {
287+
expect(needsFontIconSizingFallback()).to.be.false;
288+
});
289+
290+
fallBackIt('should not support CQ width units on pseudo elements', () => {
291+
expect(supportsCQUnitsForPseudoElements()).to.be.false;
292+
});
293+
294+
fallBackIt('should have the custom property (iconClass)', async () => {
295+
icon = fixtureSync('<vaadin-icon icon-class="foo" style="--vaadin-icon-size: 24px"></vaadin-icon>');
296+
await nextFrame();
297+
expect(icon.style.getPropertyValue('--_vaadin-font-icon-size')).to.equal('24px');
298+
});
299+
300+
fallBackIt('should have the custom property (char)', async () => {
301+
icon = fixtureSync('<vaadin-icon char="foo" style="--vaadin-icon-size: 24px"></vaadin-icon>');
302+
await nextFrame();
303+
expect(icon.style.getPropertyValue('--_vaadin-font-icon-size')).to.equal('24px');
304+
});
305+
306+
fallBackIt('should not have the custom property', async () => {
307+
icon = fixtureSync('<vaadin-icon></vaadin-icon>');
308+
await nextFrame();
309+
expect(icon.style.getPropertyValue('--_vaadin-font-icon-size')).to.equal('');
310+
});
311+
312+
fallBackIt('should set the custom property', async () => {
313+
icon = fixtureSync('<vaadin-icon style="--vaadin-icon-size: 24px"></vaadin-icon>');
314+
await nextFrame();
315+
icon.iconClass = 'foo';
316+
expect(icon.style.getPropertyValue('--_vaadin-font-icon-size')).to.equal('24px');
317+
});
318+
319+
fallBackIt('should update the custom property', async () => {
320+
icon = fixtureSync('<vaadin-icon icon-class="foo"></vaadin-icon>');
321+
await nextFrame();
322+
icon.style.width = '100px';
323+
icon.style.height = '100px';
324+
await nextResize(icon);
325+
expect(icon.style.getPropertyValue('--_vaadin-font-icon-size')).to.equal('100px');
326+
});
327+
328+
fallBackIt('should not update the custom property', async () => {
329+
icon = fixtureSync('<vaadin-icon></vaadin-icon>');
330+
await nextFrame();
331+
icon.style.width = '100px';
332+
icon.style.height = '100px';
333+
await nextFrame(icon);
334+
await nextFrame(icon);
335+
expect(icon.style.getPropertyValue('--_vaadin-font-icon-size')).to.equal('');
336+
});
337+
338+
fallBackIt('should have the same height as the host with shadow root', async () => {
339+
icon = fixtureSync('<vaadin-icon char="foo" style="--vaadin-icon-size: 24px"></vaadin-icon>');
340+
const parent = fixtureSync('<div></div>');
341+
parent.attachShadow({ mode: 'open' });
342+
parent.shadowRoot.innerHTML = '<slot></slot>';
343+
344+
parent.append(icon);
345+
await nextResize(icon);
346+
347+
const fontIconStyle = getComputedStyle(icon, ':before');
348+
expect(parseInt(fontIconStyle.height)).to.be.closeTo(24, 1);
349+
});
350+
});
270351
});

0 commit comments

Comments
 (0)