diff --git a/packages/elements/src/flag/__test__/flag.cdn-prefix.test.js b/packages/elements/src/flag/__test__/flag.cdn-prefix.test.js
new file mode 100644
index 000000000..5fee40f07
--- /dev/null
+++ b/packages/elements/src/flag/__test__/flag.cdn-prefix.test.js
@@ -0,0 +1,64 @@
+import sinon from 'sinon';
+
+// import element and theme
+import '@refinitiv-ui/elements/flag';
+import { FlagLoader } from '@refinitiv-ui/elements/flag';
+
+import '@refinitiv-ui/halo-theme/light/ef-flag.js';
+import { expect, nextFrame } from '@refinitiv-ui/test-helpers';
+
+import {
+ checkRequestedUrl,
+ createAndWaitForLoad,
+ createFakeResponse,
+ flagName,
+ gbSvg,
+ generateUniqueName,
+ isEqualSvg,
+ responseConfigSuccess
+} from './helpers/helpers.js';
+
+// Must match DefaultStyle.CDN_PREFIX in src/flag/const.ts
+const DEFAULT_CDN_PREFIX = 'https://cdn.refinitiv.net/public/libs/elf/assets/elf-theme-halo/resources/flags/';
+
+// CDN prefix is resolved once per page by the FlagLoader singleton, so this case needs its own test file
+describe('flag/cdn-prefix', function () {
+ let fetch;
+ const resetLoaders = async () => {
+ FlagLoader.reset();
+ await nextFrame(5);
+ };
+ beforeEach(async function () {
+ await resetLoaders();
+ fetch = sinon.stub(window, 'fetch');
+ });
+ afterEach(async function () {
+ window.fetch.restore(); // remove stub
+ await resetLoaders();
+ });
+
+ it('Should fall back to the hardcoded CDN prefix when --cdn-prefix resolves to an empty value', async function () {
+ createFakeResponse(gbSvg, responseConfigSuccess);
+ const uniqueFlagName = generateUniqueName(flagName); // to avoid caching
+ // `initial` makes the custom property guaranteed-invalid, so getComputedStyle returns an empty value
+ const el = await createAndWaitForLoad(
+ ``
+ );
+ const expectedSrc = `${DEFAULT_CDN_PREFIX}${uniqueFlagName}.svg`;
+
+ expect(el.getComputedVariable('--cdn-prefix')).to.equal(
+ '',
+ 'CSS variable should resolve to an empty value'
+ );
+ expect(fetch.callCount).to.equal(1, 'Should make one request');
+ expect(checkRequestedUrl(fetch.args, expectedSrc)).to.equal(
+ true,
+ `Requested URL should use the fallback prefix: ${expectedSrc}`
+ );
+
+ const svg = el.shadowRoot.querySelector('svg');
+
+ expect(svg).to.not.equal(null, 'SVG element should exist when falling back to the default prefix');
+ expect(isEqualSvg(svg.outerHTML, gbSvg)).to.equal(true, 'Should render SVG, from the mock response');
+ });
+});
diff --git a/packages/elements/src/flag/const.ts b/packages/elements/src/flag/const.ts
new file mode 100644
index 000000000..3b6457b08
--- /dev/null
+++ b/packages/elements/src/flag/const.ts
@@ -0,0 +1,4 @@
+// Default to Halo theme as Elemental and Solar theme are deprecated.
+export enum DefaultStyle {
+ CDN_PREFIX = 'https://cdn.refinitiv.net/public/libs/elf/assets/elf-theme-halo/resources/flags/'
+}
diff --git a/packages/elements/src/flag/index.ts b/packages/elements/src/flag/index.ts
index 5c814bbbe..91eaaae95 100644
--- a/packages/elements/src/flag/index.ts
+++ b/packages/elements/src/flag/index.ts
@@ -12,10 +12,13 @@ import { property } from '@refinitiv-ui/core/decorators/property.js';
import { unsafeHTML } from '@refinitiv-ui/core/directives/unsafe-html.js';
import { VERSION } from '../version.js';
+import { DefaultStyle } from './const.js';
import { FlagLoader } from './utils/FlagLoader.js';
export { preload } from './utils/FlagLoader.js';
+export { FlagLoader } from './utils/FlagLoader.js';
+
const EmptyTemplate = svg``;
@customElement('ef-flag')
@@ -131,11 +134,17 @@ export class Flag extends BasicElement {
*/
protected override firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
- /**
- * We have to call this here because
- * polyfilled browsers only get variables at this point.
- */
- this.setPrefix();
+ // Chromium only issue: starting from version 151,
+ // when Flag is slotted into an unregistered custom element,
+ // getComputedStyle() will always return empty string as a style value.
+ // It needs to wait for the registration of the parent custom element first.
+ // In practice, this happens when Flag class and its style are imported before the parent's ones as following:
+ // import '@refinitiv-ui/elements/flag';
+ // import '@refinitiv-ui/elements/panel';
+
+ // import '@refinitiv-ui/elements/flag/themes/halo/dark';
+ // import '@refinitiv-ui/elements/panel/themes/halo/dark';
+ setTimeout(() => this.setPrefix());
}
/**
@@ -167,7 +176,7 @@ export class Flag extends BasicElement {
*/
private setPrefix(): void {
if (!FlagLoader.isPrefixSet) {
- const CDNPrefix = this.getComputedVariable('--cdn-prefix').replace(/^('|")|('|")$/g, '');
+ const CDNPrefix = this.getComputedVariable('--cdn-prefix', DefaultStyle.CDN_PREFIX);
FlagLoader.setCdnPrefix(CDNPrefix);
}
diff --git a/packages/elements/src/icon/__test__/icon.cdn-prefix-fallback.test.js b/packages/elements/src/icon/__test__/icon.cdn-prefix-fallback.test.js
new file mode 100644
index 000000000..17961d2b4
--- /dev/null
+++ b/packages/elements/src/icon/__test__/icon.cdn-prefix-fallback.test.js
@@ -0,0 +1,72 @@
+import sinon from 'sinon';
+
+// import element and theme
+import '@refinitiv-ui/elements/configuration';
+import { IconLoader, SpriteLoader, iconTemplateCache } from '@refinitiv-ui/elements/icon';
+
+import '@refinitiv-ui/halo-theme/light/ef-icon.js';
+import { expect, nextFrame } from '@refinitiv-ui/test-helpers';
+
+import {
+ checkRequestedUrl,
+ createAndWaitForLoad,
+ createFakeResponse,
+ iconName,
+ isEqualSvg,
+ responseConfigSuccess,
+ tickSvgSprite
+} from './helpers/helpers.js';
+
+// Must match DefaultStyle.CDN_SPRITE_PREFIX in src/icon/const.ts
+const DEFAULT_CDN_SPRITE_PREFIX =
+ 'https://cdn.refinitiv.net/public/libs/elf/assets/elf-theme-halo/resources/sprites/icons.svg';
+
+// CDN prefixes and the sprite are resolved once per page by the loader singletons,
+// so they must be reset to make this case independent of other tests sharing the same page
+describe('icon/cdn-prefix-fallback', function () {
+ let fetch;
+ const resetLoaders = async () => {
+ IconLoader.reset();
+ SpriteLoader.reset();
+ iconTemplateCache.clear();
+ await nextFrame(5);
+ };
+ beforeEach(async function () {
+ await resetLoaders();
+ fetch = sinon.stub(window, 'fetch');
+ });
+ afterEach(async function () {
+ window.fetch.restore(); // remove stub
+ await resetLoaders();
+ });
+
+ it('Should fall back to the sprite when CDN prefixes resolve to empty values', async function () {
+ createFakeResponse(tickSvgSprite, responseConfigSuccess);
+ // `initial` makes the custom properties guaranteed-invalid, so getComputedStyle returns empty values
+ const el = await createAndWaitForLoad(
+ ``
+ );
+
+ expect(el.getComputedVariable('--cdn-prefix')).to.equal(
+ '',
+ 'CSS variable should resolve to an empty value'
+ );
+ expect(el.getComputedVariable('--cdn-sprite-prefix')).to.equal(
+ '',
+ 'CSS variable should resolve to an empty value'
+ );
+ expect(fetch.callCount).to.equal(1, 'Should make one request');
+ expect(checkRequestedUrl(fetch.args, DEFAULT_CDN_SPRITE_PREFIX)).to.equal(
+ true,
+ `Requested URL should use the fallback sprite prefix: ${DEFAULT_CDN_SPRITE_PREFIX}`
+ );
+
+ const svg = el.shadowRoot.querySelector('svg');
+
+ expect(svg).to.not.equal(null, 'SVG element should exist when falling back to the sprite');
+ expect(isEqualSvg(svg.outerHTML, tickSvgSprite)).to.equal(
+ true,
+ 'Should render SVG, from the mock response'
+ );
+ });
+});
diff --git a/packages/elements/src/icon/const.ts b/packages/elements/src/icon/const.ts
new file mode 100644
index 000000000..98b52cc02
--- /dev/null
+++ b/packages/elements/src/icon/const.ts
@@ -0,0 +1,5 @@
+// Default to Halo theme as Elemental and Solar theme are deprecated.
+export enum DefaultStyle {
+ CDN_PREFIX = '',
+ CDN_SPRITE_PREFIX = 'https://cdn.refinitiv.net/public/libs/elf/assets/elf-theme-halo/resources/sprites/icons.svg'
+}
diff --git a/packages/elements/src/icon/index.ts b/packages/elements/src/icon/index.ts
index 66f1731d5..3255e5565 100644
--- a/packages/elements/src/icon/index.ts
+++ b/packages/elements/src/icon/index.ts
@@ -20,6 +20,7 @@ import { Deferred, isBase64svg, isUrl } from '@refinitiv-ui/utils/loader.js';
import { efConfig } from '../configuration/index.js';
import type { Config } from '../configuration/index.js';
import { VERSION } from '../version.js';
+import { DefaultStyle } from './const.js';
import { IconLoader } from './utils/IconLoader.js';
import { SpriteLoader } from './utils/SpriteLoader.js';
@@ -94,7 +95,10 @@ export class Icon extends BasicElement {
if (oldValue !== value) {
this.deferIconReady();
this._icon = value;
- requestAnimationFrame(() => this.updateRenderer());
+ // Wait for setPrefix() resolving both sprite & icon CDN prefix value before updating the renderer
+ void Promise.all([SpriteLoader.getCdnPrefix(), IconLoader.getCdnPrefix()]).then(() => {
+ this.updateRenderer();
+ });
this.requestUpdate('icon', oldValue);
}
}
@@ -180,12 +184,17 @@ export class Icon extends BasicElement {
*/
protected override firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
-
- /**
- * We have to call this here because
- * polyfilled browsers only get variables at this point.
- */
- this.setPrefix();
+ // Chromium only issue: starting from version 151,
+ // when Icon is slotted into an unregistered custom element,
+ // getComputedStyle() will always return empty string as a style value.
+ // It needs to wait for the registration of the parent custom element first.
+ // In practice, this happens when Icon class and its style are imported before the parent's ones as following:
+ // import '@refinitiv-ui/elements/icon';
+ // import '@refinitiv-ui/elements/panel';
+
+ // import '@refinitiv-ui/elements/icon/themes/halo/dark';
+ // import '@refinitiv-ui/elements/panel/themes/halo/dark';
+ setTimeout(() => this.setPrefix());
}
protected override async getUpdateComplete(): Promise {
@@ -284,12 +293,12 @@ export class Icon extends BasicElement {
private setPrefix(): void {
// This prefix for individual icons allows supporting custom prefix of self-managed icons.
if (IconLoader.isPrefixPending) {
- const CDNPrefix = this.getComputedVariable('--cdn-prefix');
+ const CDNPrefix = this.getComputedVariable('--cdn-prefix', DefaultStyle.CDN_PREFIX);
IconLoader.setCdnPrefix(CDNPrefix);
}
if (SpriteLoader.isPrefixPending) {
- const CDNSpritePrefix = this.getComputedVariable('--cdn-sprite-prefix');
+ const CDNSpritePrefix = this.getComputedVariable('--cdn-sprite-prefix', DefaultStyle.CDN_SPRITE_PREFIX);
SpriteLoader.setCdnPrefix(CDNSpritePrefix);
}
}