diff --git a/packages/base/src/UI5Element.ts b/packages/base/src/UI5Element.ts index 5170eba6530f9..d673cb91f2c87 100644 --- a/packages/base/src/UI5Element.ts +++ b/packages/base/src/UI5Element.ts @@ -898,6 +898,16 @@ abstract class UI5Element extends HTMLElement { */ _render() { const ctor = this.constructor as typeof UI5Element; + + // Skip rendering language-aware components while a language change (CLDR + i18n fetch) is + // still in flight. reRenderAllUI5Elements({ languageAware: true }) will re-render them once + // the data is ready. Without this guard, a component added to the render queue *before* the + // language change started (e.g. via renderDeferred) can still call onBeforeRendering and + // onAfterRendering with stale or missing locale data. + if (ctor.getMetadata().isLanguageAware() && getLanguageChangePending()) { + return; + } + const hasIndividualSlots = ctor.getMetadata().hasIndividualSlots(); // restore properties that were initialized before `define` by calling the setter diff --git a/packages/base/src/config/Language.ts b/packages/base/src/config/Language.ts index 1ab2896adbe81..65b0b9d4067bd 100644 --- a/packages/base/src/config/Language.ts +++ b/packages/base/src/config/Language.ts @@ -24,16 +24,19 @@ attachConfigurationReset(() => { let languageChangePending: Promise | null = null; const startLanguageChange = (language: string): Promise => { - const changePromise = fireLanguageChange(language).then(() => { - if (isBooted()) { - return reRenderAllUI5Elements({ languageAware: true }); - } - }).finally(() => { - // Only clear if no newer change has already replaced us - if (languageChangePending === changePromise) { - languageChangePending = null; - } - }); + const changePromise = fireLanguageChange(language) + .then(() => { + // Clear if there is no other language change in flight. Re-render all language-aware components + // so they pick up the newly loaded CLDR and i18n data. + if (languageChangePending === changePromise) { + languageChangePending = null; + + if (isBooted()) { + return reRenderAllUI5Elements({ languageAware: true }); + } + } + }); + languageChangePending = changePromise; return changePromise; }; diff --git a/packages/main/cypress/specs/DatePicker.cy.tsx b/packages/main/cypress/specs/DatePicker.cy.tsx index 81eaf98efca30..cb107d06be99a 100644 --- a/packages/main/cypress/specs/DatePicker.cy.tsx +++ b/packages/main/cypress/specs/DatePicker.cy.tsx @@ -4,7 +4,7 @@ import DatePicker from "../../src/DatePicker.js"; import Label from "../../src/Label.js"; import { DATEPICKER_POPOVER_ACCESSIBLE_NAME } from "../../src/generated/i18n/i18n-defaults.js"; -describe.skip("Date Picker Tests", () => { +describe("Date Picker Tests", () => { it("input renders", () => { cy.mount(); @@ -34,7 +34,9 @@ describe.skip("Date Picker Tests", () => { it("input receives value in format pattern depending on the set language", () => { cy.wrap({ setLanguage }) - .then(api => api.setLanguage("bg")); + .then(api => { + return api.setLanguage("bg"); + }); cy.mount(); @@ -58,7 +60,9 @@ describe.skip("Date Picker Tests", () => { .should("have.class", "ui5-dp-item--selected"); cy.wrap({ setLanguage }) - .then(api => api.setLanguage("en")); + .then(api => { + return api.setLanguage("en"); + }); }); it("custom formatting", () => { @@ -318,7 +322,9 @@ describe.skip("Date Picker Tests", () => { it("respect first day of the week - monday", () => { cy.wrap({ setLanguage }) - .then(api => api.setLanguage("bg")); + .then(api => { + return api.setLanguage("bg"); + }); cy.mount(); @@ -338,7 +344,9 @@ describe.skip("Date Picker Tests", () => { .should("have.class", "ui5-dp-wday6"); cy.wrap({ setLanguage }) - .then(api => api.setLanguage("en")); + .then(api => { + return api.setLanguage("en"); + }); }); it("if today is 30 jan, clicking next month does not skip feb", () => { diff --git a/packages/main/cypress/specs/DateRangePicker.cy.tsx b/packages/main/cypress/specs/DateRangePicker.cy.tsx index 8527c3a33c5f3..8a1c10cd177f6 100644 --- a/packages/main/cypress/specs/DateRangePicker.cy.tsx +++ b/packages/main/cypress/specs/DateRangePicker.cy.tsx @@ -18,7 +18,9 @@ function DateRangePickerTemplate(options: DateTimePickerTemplateOptions) { describe("DateRangePicker general interaction", () => { afterEach(() => { - cy.wrap({ setLanguage }).then(api => api.setLanguage("en")); + cy.wrap({ setLanguage }).then(api => { + return api.setLanguage("en"); + }); }); it("Custom Validation Error", () => { diff --git a/packages/main/cypress/specs/StepInput.cy.tsx b/packages/main/cypress/specs/StepInput.cy.tsx index d589a5a2624bc..eb3de855f7416 100644 --- a/packages/main/cypress/specs/StepInput.cy.tsx +++ b/packages/main/cypress/specs/StepInput.cy.tsx @@ -626,22 +626,22 @@ describe("StepInput events", () => { }); describe("StepInput thousand separator formatting", () => { - it("should display value with thousand separator", () => { - cy.mount( + it("should display value with thousand separator", () => { + cy.mount( ); - cy.get("[ui5-step-input]") + cy.get("[ui5-step-input]") .ui5StepInputGetInnerInput() .should($input => { - const val = $input.val(); - // Accepts both comma and dot as separator depending on locale - expect(val).to.match(/12[,.]345/); - }); - }); - - it("should parse formatted value correctly", () => { - cy.mount( + const val = $input.val(); + // Accepts both comma and dot as separator depending on locale + expect(val).to.match(/12[,.]345/); + }); + }); + + it("should parse formatted value correctly", () => { + cy.mount( ); @@ -651,10 +651,10 @@ describe("StepInput thousand separator formatting", () => { cy.get("@stepInput") .ui5StepInputGetInnerInput() .should($input => { - const val = $input.val() as string; + const val = $input.val() as string; const num = Number(val.replace(/[^\d]/g, "")); - expect(num).to.equal(12345); - }); + expect(num).to.equal(12345); + }); cy.get("@stepInput") .realClick({ "clickCount": 2 }) @@ -666,18 +666,18 @@ describe("StepInput thousand separator formatting", () => { cy.get("@stepInput") .ui5StepInputGetInnerInput() .should($input => { - const val = $input.val() as string; - expect(val).to.equal("10,000"); - }); + const val = $input.val() as string; + expect(val).to.equal("10,000"); + }); cy.get("@stepInput") .should("have.prop", "value", 10000); - }); + }); it("should update input value when language is changed", () => { cy.wrap({ setLanguage }) - .then(async ({ setLanguage }) => { - await setLanguage("en"); + .then(({ setLanguage }) => { + return setLanguage("en"); }); cy.mount( @@ -691,24 +691,23 @@ describe("StepInput thousand separator formatting", () => { .should($input => { const val = $input.val() as string; expect(val).to.equal("10,000.56"); - }); + }); cy.wrap({ setLanguage }) - .then(async ({ setLanguage }) => { - await setLanguage("de"); - }) - .then(() => { - cy.get("@stepInput") - .ui5StepInputGetInnerInput() - .should($input => { - const val = $input.val() as string; - expect(val).to.equal("10.000,56"); - }); + .then(({ setLanguage }) => { + return setLanguage("de"); + }); + + cy.get("@stepInput") + .ui5StepInputGetInnerInput() + .should($input => { + const val = $input.val() as string; + expect(val).to.equal("10.000,56"); }); - + cy.wrap({ setLanguage }) - .then(async ({ setLanguage }) => { - await setLanguage("en"); + .then(({ setLanguage }) => { + return setLanguage("en"); }); }); }); @@ -780,40 +779,40 @@ describe("StepInput property propagation", () => { }); it("should increase value on mouse wheel up", () => { - cy.mount( + cy.mount( ); - cy.get("[ui5-step-input]") + cy.get("[ui5-step-input]") .as("stepInput"); - cy.get("@stepInput") + cy.get("@stepInput") .ui5StepInputScrollToChangeValue(7, false); - }); + }); - it("should decrease value on mouse wheel down", () => { - cy.mount( + it("should decrease value on mouse wheel down", () => { + cy.mount( ); - cy.get("[ui5-step-input]") + cy.get("[ui5-step-input]") .as("stepInput"); - cy.get("@stepInput") + cy.get("@stepInput") .ui5StepInputScrollToChangeValue(3, true); - }); + }); - it("should not change value when readonly", () => { - cy.mount( + it("should not change value when readonly", () => { + cy.mount( ); - cy.get("[ui5-step-input]") + cy.get("[ui5-step-input]") .as("stepInput"); - cy.get("@stepInput") + cy.get("@stepInput") .ui5StepInputScrollToChangeValue(5, true); - }); + }); }); describe("Validation inside form", () => { diff --git a/packages/main/src/StepInput.ts b/packages/main/src/StepInput.ts index e125e14433e85..99a2fed4e332c 100644 --- a/packages/main/src/StepInput.ts +++ b/packages/main/src/StepInput.ts @@ -533,7 +533,7 @@ class StepInput extends UI5Element implements IFormInputElement { _updateValueState() { const isWithinRange = (this.min === undefined || this._parseNumber(this.input.value) >= this.min) - && (this.max === undefined || this._parseNumber(this.input.value) <= this.max); + && (this.max === undefined || this._parseNumber(this.input.value) <= this.max); const isValueWithCorrectPrecision = this._isValueWithCorrectPrecision; const previousValueState = this.valueState; const isValid = isWithinRange && isValueWithCorrectPrecision; @@ -595,17 +595,17 @@ class StepInput extends UI5Element implements IFormInputElement { } /** - * Formats a number with thousands separator based on current locale - * @private - */ + * Formats a number with thousands separator based on current locale + * @private + */ _formatNumber(value: number): string { return this.formatter.format(value); } /** - * Parses formatted number string back to numeric value - * @private - */ + * Parses formatted number string back to numeric value + * @private + */ _parseNumber(formattedValue: string): number { return this.formatter.parse(formattedValue) as number; }