From 97f49eeb62d2e228e1ce3e2c10022113a7324a4a Mon Sep 17 00:00:00 2001 From: Mert Akinc <7282195+m-akinc@users.noreply.github.com> Date: Thu, 27 Jul 2023 13:25:51 -0500 Subject: [PATCH 01/23] Add lang design token --- .../src/theme-provider/index.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/nimble-components/src/theme-provider/index.ts b/packages/nimble-components/src/theme-provider/index.ts index 846fd538c7..a035425a77 100644 --- a/packages/nimble-components/src/theme-provider/index.ts +++ b/packages/nimble-components/src/theme-provider/index.ts @@ -15,6 +15,11 @@ declare global { } } +export const lang = DesignToken.create({ + name: 'lang', + cssCustomPropertyName: null +}).withDefault(document.documentElement.lang); + // Not represented as a CSS Custom Property, instead available // as an attribute of theme provider. export const direction = DesignToken.create({ @@ -33,6 +38,11 @@ export const theme = DesignToken.create({ * @internal */ export class ThemeProvider extends FoundationElement { + @attr({ + attribute: 'lang' + }) + public override lang: string = document.documentElement.lang; + @attr({ attribute: 'direction' }) @@ -43,6 +53,17 @@ export class ThemeProvider extends FoundationElement { }) public theme: Theme = Theme.light; + public langChanged( + _prev: string | undefined, + next: string | undefined + ): void { + if (next !== undefined && next !== null) { + lang.setValueFor(this, next); + } else { + lang.deleteValueFor(this); + } + } + public directionChanged( _prev: Direction | undefined, next: Direction | undefined From e2bd2bc2c33770d05bbe70062b8f5dae6922e7db Mon Sep 17 00:00:00 2001 From: Mert Akinc <7282195+m-akinc@users.noreply.github.com> Date: Thu, 27 Jul 2023 13:26:11 -0500 Subject: [PATCH 02/23] Use lang design token in date-text column --- .../src/table-column/date-text/index.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/nimble-components/src/table-column/date-text/index.ts b/packages/nimble-components/src/table-column/date-text/index.ts index 1e5694a228..c3d11608fc 100644 --- a/packages/nimble-components/src/table-column/date-text/index.ts +++ b/packages/nimble-components/src/table-column/date-text/index.ts @@ -1,4 +1,7 @@ -import { DesignSystem } from '@microsoft/fast-foundation'; +import { + DesignSystem, + DesignTokenSubscriber +} from '@microsoft/fast-foundation'; import { attr } from '@microsoft/fast-element'; import { styles } from '../base/styles'; import { template } from '../base/template'; @@ -27,6 +30,7 @@ import type { WeekdayFormat } from './types'; import { TableColumnDateTextValidator } from './models/table-column-date-text-validator'; +import { lang } from '../../theme-provider'; export type TableColumnDateTextCellRecord = TableNumberField<'value'>; export interface TableColumnDateTextColumnConfig { @@ -106,11 +110,22 @@ export class TableColumnDateText extends TableColumnTextBase { @attr({ attribute: 'custom-hour-cycle' }) public customHourCycle: HourCycleFormat; + private readonly langSubscriber: DesignTokenSubscriber = { + handleChange: () => { + this.updateColumnConfig(); + } + }; + public override connectedCallback(): void { super.connectedCallback(); + lang.subscribe(this.langSubscriber, this); this.updateColumnConfig(); } + public override disconnectedCallback(): void { + lang.unsubscribe(this.langSubscriber, this); + } + public override get validity(): TableColumnValidity { return this.validator.getValidity(); } @@ -226,7 +241,7 @@ export class TableColumnDateText extends TableColumnTextBase { options = this.getCustomFormattingOptions(); } try { - return new Intl.DateTimeFormat(undefined, options); + return new Intl.DateTimeFormat(lang.getValueFor(this), options); } catch (e) { return undefined; } From 889bdbb89d48cfc1444bf81df88482df96a39476 Mon Sep 17 00:00:00 2001 From: Mert Akinc <7282195+m-akinc@users.noreply.github.com> Date: Thu, 27 Jul 2023 15:59:27 -0500 Subject: [PATCH 03/23] Ensure test consistency by making the locale explicit --- .../date-text/tests/table-column-date-text.spec.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.spec.ts b/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.spec.ts index 2541a6de39..f6f1c483b6 100644 --- a/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.spec.ts +++ b/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.spec.ts @@ -7,6 +7,7 @@ import type { TableRecord } from '../../../table/types'; import { TablePageObject } from '../../../table/testing/table.pageobject'; import { TableColumnDateTextPageObject } from '../testing/table-column-date-text.pageobject'; import { getSpecTypeByNamedList } from '../../../utilities/tests/parameterized'; +import { themeProviderTag } from '../../../theme-provider'; interface SimpleTableRecord extends TableRecord { field?: number | null; @@ -15,6 +16,8 @@ interface SimpleTableRecord extends TableRecord { // prettier-ignore async function setup(): Promise>> { + const themeProvider = document.createElement(themeProviderTag); + themeProvider.lang = 'en'; return fixture>( html` <${tableColumnDateTextTag} field-name="field" group-index="0"> @@ -23,7 +26,10 @@ async function setup(): Promise>> { <${tableColumnDateTextTag} field-name="anotherField"> Squeeze Column 1 - ` + `, + { + parent: themeProvider + } ); } From fc460a2d61e3cd05d78949404da9781a46e9bf80 Mon Sep 17 00:00:00 2001 From: Mert Akinc <7282195+m-akinc@users.noreply.github.com> Date: Thu, 27 Jul 2023 17:20:04 -0500 Subject: [PATCH 04/23] Detect bad lang code and add tests --- .../src/table-column/date-text/index.ts | 24 ++++++++-- .../table-column-date-text-validator.ts | 9 +++- .../tests/table-column-date-text.spec.ts | 45 +++++++++++++++++-- 3 files changed, 70 insertions(+), 8 deletions(-) diff --git a/packages/nimble-components/src/table-column/date-text/index.ts b/packages/nimble-components/src/table-column/date-text/index.ts index c3d11608fc..faf58721cf 100644 --- a/packages/nimble-components/src/table-column/date-text/index.ts +++ b/packages/nimble-components/src/table-column/date-text/index.ts @@ -225,9 +225,7 @@ export class TableColumnDateText extends TableColumnTextBase { formatter: this.createFormatter() }; this.columnInternals.columnConfig = columnConfig; - this.validator.setCustomOptionsValidity( - columnConfig.formatter !== undefined - ); + this.validateConfig(columnConfig.formatter); } private createFormatter(): Intl.DateTimeFormat | undefined { @@ -274,6 +272,26 @@ export class TableColumnDateText extends TableColumnTextBase { }; return options; } + + private validateConfig(formatter: Intl.DateTimeFormat | undefined): void { + let invalidLangCode = false; + let invalidCustomOptions = false; + if (formatter === undefined) { + try { + // We don't know whether the lang code we used was bad, or if the config + // options were bad. To determine which, try constructing a formatter + // with default options. If it throws, the lang code was the problem. + Intl.DateTimeFormat(lang.getValueFor(this)); + invalidLangCode = false; + invalidCustomOptions = true; + } catch (e) { + invalidLangCode = true; + invalidCustomOptions = false; + } + } + this.validator.setCustomOptionsValidity(!invalidCustomOptions); + this.validator.setLangCodeValidity(!invalidLangCode); + } } const nimbleTableColumnDateText = TableColumnDateText.compose({ diff --git a/packages/nimble-components/src/table-column/date-text/models/table-column-date-text-validator.ts b/packages/nimble-components/src/table-column/date-text/models/table-column-date-text-validator.ts index 747594471b..ef98754173 100644 --- a/packages/nimble-components/src/table-column/date-text/models/table-column-date-text-validator.ts +++ b/packages/nimble-components/src/table-column/date-text/models/table-column-date-text-validator.ts @@ -1,7 +1,10 @@ import type { ColumnInternals } from '../../base/models/column-internals'; import { ColumnValidator } from '../../base/models/column-validator'; -const dateTextValidityFlagNames = ['invalidCustomOptionsCombination'] as const; +const dateTextValidityFlagNames = [ + 'invalidCustomOptionsCombination', + 'invalidLangCode' +] as const; /** * Validator for TableColumnDateText. @@ -16,4 +19,8 @@ export class TableColumnDateTextValidator extends ColumnValidator< public setCustomOptionsValidity(valid: boolean): void { this.setConditionValue('invalidCustomOptionsCombination', !valid); } + + public setLangCodeValidity(valid: boolean): void { + this.setConditionValue('invalidLangCode', !valid); + } } diff --git a/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.spec.ts b/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.spec.ts index f6f1c483b6..873e45a289 100644 --- a/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.spec.ts +++ b/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.spec.ts @@ -7,7 +7,7 @@ import type { TableRecord } from '../../../table/types'; import { TablePageObject } from '../../../table/testing/table.pageobject'; import { TableColumnDateTextPageObject } from '../testing/table-column-date-text.pageobject'; import { getSpecTypeByNamedList } from '../../../utilities/tests/parameterized'; -import { themeProviderTag } from '../../../theme-provider'; +import { lang, themeProviderTag } from '../../../theme-provider'; interface SimpleTableRecord extends TableRecord { field?: number | null; @@ -275,6 +275,21 @@ describe('TableColumnDateText', () => { expect(pageObject.getRenderedCellContent(0, 0)).toBe('12/10/2012'); }); + it('updates displayed date when lang token changes', async () => { + await element.setData([ + { field: new Date('Dec 10, 2012, 10:35:05 PM').valueOf() } + ]); + await waitForUpdatesAsync(); + expect(pageObject.getRenderedCellContent(0, 0)).toBe( + 'Dec 10, 2012, 10:35:05 PM' + ); + lang.setValueFor(element, 'fr'); + await waitForUpdatesAsync(); + expect(pageObject.getRenderedCellContent(0, 0)).toBe( + '10 déc. 2012, 22:35:05' + ); + }); + it('honors customDateStyle property', async () => { await element.setData([ { field: new Date('Dec 10, 2012, 10:35:05 PM').valueOf() } @@ -468,15 +483,16 @@ describe('TableColumnDateText', () => { expect(pageObject.getRenderedCellContent(0, 0)).toBe('2012'); }); - it('has no invalid flag on column when using default formatting', async () => { + it('has no invalid flags on column when using default formatting', async () => { await element.setData([ { field: new Date('Dec 10, 2012, 10:35:05 PM').valueOf() } ]); await waitForUpdatesAsync(); expect(column.validity.invalidCustomOptionsCombination).toBeFalse(); + expect(column.validity.invalidLangCode).toBeFalse(); }); - it('sets invalid flag on column when custom options are incompatible', async () => { + it('sets invalidCustomOptionsCombination flag on column when custom options are incompatible', async () => { await element.setData([ { field: new Date('Dec 10, 2012, 10:35:05 PM').valueOf() } ]); @@ -486,9 +502,10 @@ describe('TableColumnDateText', () => { column.customDateStyle = 'full'; await waitForUpdatesAsync(); expect(column.validity.invalidCustomOptionsCombination).toBeTrue(); + expect(column.validity.invalidLangCode).toBeFalse(); }); - it('clears invalid flag on column after fixing custom option incompatibility', async () => { + it('clears invalidCustomOptionsCombination flag on column after fixing custom option incompatibility', async () => { await element.setData([ { field: new Date('Dec 10, 2012, 10:35:05 PM').valueOf() } ]); @@ -501,6 +518,26 @@ describe('TableColumnDateText', () => { await waitForUpdatesAsync(); expect(column.validity.invalidCustomOptionsCombination).toBeFalse(); }); + + it('sets invalidLangCode flag on column when lang code is malformed', async () => { + await element.setData([ + { field: new Date('Dec 10, 2012, 10:35:05 PM').valueOf() } + ]); + lang.setValueFor(element, ''); + await waitForUpdatesAsync(); + expect(column.validity.invalidLangCode).toBeTrue(); + expect(column.validity.invalidCustomOptionsCombination).toBeFalse(); + }); + + it('clears invalidLangCode flag on column after fixing lang code', async () => { + await element.setData([ + { field: new Date('Dec 10, 2012, 10:35:05 PM').valueOf() } + ]); + lang.setValueFor(element, ''); + await waitForUpdatesAsync(); + lang.setValueFor(element, 'en'); + expect(column.validity.invalidLangCode).toBeFalse(); + }); }); describe('with static config', () => { From fb60542f85a569669a1e20654eeabc1888fb7b46 Mon Sep 17 00:00:00 2001 From: Mert Akinc <7282195+m-akinc@users.noreply.github.com> Date: Tue, 1 Aug 2023 15:40:00 -0500 Subject: [PATCH 05/23] Change files --- ...le-components-df093e6a-7cba-46ed-8d5d-07670af90719.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@ni-nimble-components-df093e6a-7cba-46ed-8d5d-07670af90719.json diff --git a/change/@ni-nimble-components-df093e6a-7cba-46ed-8d5d-07670af90719.json b/change/@ni-nimble-components-df093e6a-7cba-46ed-8d5d-07670af90719.json new file mode 100644 index 0000000000..f8c2876c77 --- /dev/null +++ b/change/@ni-nimble-components-df093e6a-7cba-46ed-8d5d-07670af90719.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Add locale support to date-text column", + "packageName": "@ni/nimble-components", + "email": "7282195+m-akinc@users.noreply.github.com", + "dependentChangeType": "patch" +} From dcb11d33af8f47ca9fd0e90f26215eff2433d6ea Mon Sep 17 00:00:00 2001 From: Mert Akinc <7282195+m-akinc@users.noreply.github.com> Date: Tue, 1 Aug 2023 15:58:36 -0500 Subject: [PATCH 06/23] locale for tests: en => en-US --- .../table-column/date-text/tests/table-column-date-text.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.spec.ts b/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.spec.ts index 873e45a289..73a5a30013 100644 --- a/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.spec.ts +++ b/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.spec.ts @@ -17,7 +17,7 @@ interface SimpleTableRecord extends TableRecord { // prettier-ignore async function setup(): Promise>> { const themeProvider = document.createElement(themeProviderTag); - themeProvider.lang = 'en'; + themeProvider.lang = 'en-US'; return fixture>( html` <${tableColumnDateTextTag} field-name="field" group-index="0"> From c277413660c372e5944cdbea3381c0a9c93276b2 Mon Sep 17 00:00:00 2001 From: Mert Akinc <7282195+m-akinc@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:05:08 -0500 Subject: [PATCH 07/23] Feedback --- ...-df093e6a-7cba-46ed-8d5d-07670af90719.json | 2 +- .../src/theme-provider/index.ts | 12 +++-------- .../tests/theme-provider.spec.ts | 21 ++++++++++++++++++- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/change/@ni-nimble-components-df093e6a-7cba-46ed-8d5d-07670af90719.json b/change/@ni-nimble-components-df093e6a-7cba-46ed-8d5d-07670af90719.json index f8c2876c77..ab0dc00aad 100644 --- a/change/@ni-nimble-components-df093e6a-7cba-46ed-8d5d-07670af90719.json +++ b/change/@ni-nimble-components-df093e6a-7cba-46ed-8d5d-07670af90719.json @@ -1,6 +1,6 @@ { "type": "minor", - "comment": "Add locale support to date-text column", + "comment": "Add locale support to theme provider and use in date-text column", "packageName": "@ni/nimble-components", "email": "7282195+m-akinc@users.noreply.github.com", "dependentChangeType": "patch" diff --git a/packages/nimble-components/src/theme-provider/index.ts b/packages/nimble-components/src/theme-provider/index.ts index a035425a77..68a49ce9c5 100644 --- a/packages/nimble-components/src/theme-provider/index.ts +++ b/packages/nimble-components/src/theme-provider/index.ts @@ -38,19 +38,13 @@ export const theme = DesignToken.create({ * @internal */ export class ThemeProvider extends FoundationElement { - @attr({ - attribute: 'lang' - }) + @attr() public override lang: string = document.documentElement.lang; - @attr({ - attribute: 'direction' - }) + @attr() public direction: Direction = Direction.ltr; - @attr({ - attribute: 'theme' - }) + @attr() public theme: Theme = Theme.light; public langChanged( diff --git a/packages/nimble-components/src/theme-provider/tests/theme-provider.spec.ts b/packages/nimble-components/src/theme-provider/tests/theme-provider.spec.ts index aa550962e0..c10f36fbdd 100644 --- a/packages/nimble-components/src/theme-provider/tests/theme-provider.spec.ts +++ b/packages/nimble-components/src/theme-provider/tests/theme-provider.spec.ts @@ -2,7 +2,8 @@ import { spinalCase } from '@microsoft/fast-web-utilities'; import * as designTokensNamespace from '../design-tokens'; import { tokenNames, suffixFromTokenName } from '../design-token-names'; import { getSpecTypeByNamedList } from '../../utilities/tests/parameterized'; -import { ThemeProvider } from '..'; +import { ThemeProvider, lang } from '..'; +import { waitForUpdatesAsync } from '../../testing/async-helpers'; type DesignTokenPropertyName = keyof typeof designTokensNamespace; const designTokenPropertyNames = Object.keys( @@ -16,6 +17,24 @@ describe('Theme Provider', () => { ); }); + it('sets lang token value to "foo" when lang attribute is assigned value "foo"', async () => { + const element: ThemeProvider = document.createElement( + 'nimble-theme-provider' + ); + element.setAttribute('lang', 'foo'); + await waitForUpdatesAsync(); + expect(lang.getValueFor(element)).toBe('foo'); + }); + + it('clears lang token value when lang attribute is removed', async () => { + const element: ThemeProvider = document.createElement( + 'nimble-theme-provider' + ); + element.removeAttribute('lang'); + await waitForUpdatesAsync(); + expect(lang.getValueFor(element)).toBe(''); + }); + describe('design token should match CSSDesignToken', () => { const tokenEntries = designTokenPropertyNames.map( (name: DesignTokenPropertyName) => ({ From 4ab854bdd8e86b4b3d0465c64682a8ca3f71b2cc Mon Sep 17 00:00:00 2001 From: Mert Akinc <7282195+m-akinc@users.noreply.github.com> Date: Wed, 2 Aug 2023 13:26:27 -0500 Subject: [PATCH 08/23] Feedback --- .../src/table-column/date-text/index.ts | 6 +++--- .../nimble-components/src/theme-provider/index.ts | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/nimble-components/src/table-column/date-text/index.ts b/packages/nimble-components/src/table-column/date-text/index.ts index faf58721cf..7b0431fb20 100644 --- a/packages/nimble-components/src/table-column/date-text/index.ts +++ b/packages/nimble-components/src/table-column/date-text/index.ts @@ -225,7 +225,7 @@ export class TableColumnDateText extends TableColumnTextBase { formatter: this.createFormatter() }; this.columnInternals.columnConfig = columnConfig; - this.validateConfig(columnConfig.formatter); + this.validateConfig(columnConfig.formatter !== undefined); } private createFormatter(): Intl.DateTimeFormat | undefined { @@ -273,10 +273,10 @@ export class TableColumnDateText extends TableColumnTextBase { return options; } - private validateConfig(formatter: Intl.DateTimeFormat | undefined): void { + private validateConfig(configIsValid: boolean): void { let invalidLangCode = false; let invalidCustomOptions = false; - if (formatter === undefined) { + if (!configIsValid) { try { // We don't know whether the lang code we used was bad, or if the config // options were bad. To determine which, try constructing a formatter diff --git a/packages/nimble-components/src/theme-provider/index.ts b/packages/nimble-components/src/theme-provider/index.ts index 68a49ce9c5..087e9826d3 100644 --- a/packages/nimble-components/src/theme-provider/index.ts +++ b/packages/nimble-components/src/theme-provider/index.ts @@ -48,8 +48,8 @@ export class ThemeProvider extends FoundationElement { public theme: Theme = Theme.light; public langChanged( - _prev: string | undefined, - next: string | undefined + _prev: string | undefined | null, + next: string | undefined | null ): void { if (next !== undefined && next !== null) { lang.setValueFor(this, next); @@ -59,8 +59,8 @@ export class ThemeProvider extends FoundationElement { } public directionChanged( - _prev: Direction | undefined, - next: Direction | undefined + _prev: Direction | undefined | null, + next: Direction | undefined | null ): void { if (next !== undefined && next !== null) { direction.setValueFor(this, next); @@ -70,8 +70,8 @@ export class ThemeProvider extends FoundationElement { } public themeChanged( - _prev: Theme | undefined, - next: Theme | undefined + _prev: Theme | undefined | null, + next: Theme | undefined | null ): void { if (next !== undefined && next !== null) { theme.setValueFor(this, next); From ce80a89e57e232e6f9b4be1a846be9ecbc7d5568 Mon Sep 17 00:00:00 2001 From: Mert Akinc <7282195+m-akinc@users.noreply.github.com> Date: Fri, 4 Aug 2023 14:56:40 -0500 Subject: [PATCH 09/23] Default lang token to page's lang --- .../src/theme-provider/index.ts | 7 ++--- .../src/utilities/models/page-locale.ts | 27 +++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 packages/nimble-components/src/utilities/models/page-locale.ts diff --git a/packages/nimble-components/src/theme-provider/index.ts b/packages/nimble-components/src/theme-provider/index.ts index 087e9826d3..c0662d1aee 100644 --- a/packages/nimble-components/src/theme-provider/index.ts +++ b/packages/nimble-components/src/theme-provider/index.ts @@ -8,6 +8,7 @@ import { Direction } from '@microsoft/fast-web-utilities'; import { template } from './template'; import { styles } from './styles'; import { Theme } from './types'; +import { pageLocale } from '../utilities/models/page-locale'; declare global { interface HTMLElementTagNameMap { @@ -18,7 +19,7 @@ declare global { export const lang = DesignToken.create({ name: 'lang', cssCustomPropertyName: null -}).withDefault(document.documentElement.lang); +}).withDefault((): string => pageLocale.lang); // Not represented as a CSS Custom Property, instead available // as an attribute of theme provider. @@ -39,7 +40,7 @@ export const theme = DesignToken.create({ */ export class ThemeProvider extends FoundationElement { @attr() - public override lang: string = document.documentElement.lang; + public override lang = ''; @attr() public direction: Direction = Direction.ltr; @@ -51,7 +52,7 @@ export class ThemeProvider extends FoundationElement { _prev: string | undefined | null, next: string | undefined | null ): void { - if (next !== undefined && next !== null) { + if (next !== undefined && next !== null && next !== '') { lang.setValueFor(this, next); } else { lang.deleteValueFor(this); diff --git a/packages/nimble-components/src/utilities/models/page-locale.ts b/packages/nimble-components/src/utilities/models/page-locale.ts new file mode 100644 index 0000000000..fc4685729e --- /dev/null +++ b/packages/nimble-components/src/utilities/models/page-locale.ts @@ -0,0 +1,27 @@ +import { observable } from '@microsoft/fast-element'; + +/** + * Observable class to subscribe to changes in the page's lang attribute + */ +class PageLocale { + @observable + public lang: string = document.documentElement.lang; + + public constructor() { + const observer = new MutationObserver(mutations => { + for (const mutation of mutations) { + if ( + mutation.type === 'attributes' + && mutation.attributeName === 'lang' + ) { + this.lang = (mutation.target as HTMLElement).lang; + } + } + }); + observer.observe(document.documentElement, { + attributeFilter: ['lang'] + }); + } +} + +export const pageLocale = new PageLocale(); From 0ab15449201f0625258803a5fa7d584b0a65fd4d Mon Sep 17 00:00:00 2001 From: Mert Akinc <7282195+m-akinc@users.noreply.github.com> Date: Fri, 4 Aug 2023 16:42:55 -0500 Subject: [PATCH 10/23] Handle invalid lang values --- .../src/table-column/base/types.ts | 3 +- packages/nimble-components/src/table/types.ts | 5 +- .../src/theme-provider/index.ts | 34 ++++++- .../tests/theme-provider.spec.ts | 89 +++++++++++++++---- .../src/utilities/models/validator.ts | 5 +- 5 files changed, 112 insertions(+), 24 deletions(-) diff --git a/packages/nimble-components/src/table-column/base/types.ts b/packages/nimble-components/src/table-column/base/types.ts index 3ae434a3c2..4f7d7f3ac9 100644 --- a/packages/nimble-components/src/table-column/base/types.ts +++ b/packages/nimble-components/src/table-column/base/types.ts @@ -1,4 +1,5 @@ -import type { TableRecord, ValidityObject } from '../../table/types'; +import type { TableRecord } from '../../table/types'; +import type { ValidityObject } from '../../utilities/models/validator'; /** * An object whose fields are defined by a particular TableColumn, which is used by the column's diff --git a/packages/nimble-components/src/table/types.ts b/packages/nimble-components/src/table/types.ts index 89da48cd5e..1795540e4a 100644 --- a/packages/nimble-components/src/table/types.ts +++ b/packages/nimble-components/src/table/types.ts @@ -1,4 +1,5 @@ import type { TableColumn } from '../table-column/base'; +import type { ValidityObject } from '../utilities/models/validator'; /** * TableFieldName describes the type associated with keys within @@ -41,10 +42,6 @@ export type TableNumberField = { [name in FieldName]: TableNumberFieldValue; }; -export interface ValidityObject { - [key: string]: boolean; -} - export interface TableValidity extends ValidityObject { readonly duplicateRecordId: boolean; readonly missingRecordId: boolean; diff --git a/packages/nimble-components/src/theme-provider/index.ts b/packages/nimble-components/src/theme-provider/index.ts index c0662d1aee..6aec00ce93 100644 --- a/packages/nimble-components/src/theme-provider/index.ts +++ b/packages/nimble-components/src/theme-provider/index.ts @@ -9,6 +9,7 @@ import { template } from './template'; import { styles } from './styles'; import { Theme } from './types'; import { pageLocale } from '../utilities/models/page-locale'; +import type { ValidityObject } from '../utilities/models/validator'; declare global { interface HTMLElementTagNameMap { @@ -16,10 +17,24 @@ declare global { } } +function isInvalidLang(value: string): boolean { + try { + // eslint-disable-next-line no-new + new Intl.DateTimeFormat(value); + return false; + } catch (e) { + return true; + } +} + +function getSystemLocale(): string { + return new Intl.DateTimeFormat(undefined).resolvedOptions().locale; +} + export const lang = DesignToken.create({ name: 'lang', cssCustomPropertyName: null -}).withDefault((): string => pageLocale.lang); +}).withDefault((): string => (isInvalidLang(pageLocale.lang) ? getSystemLocale() : pageLocale.lang)); // Not represented as a CSS Custom Property, instead available // as an attribute of theme provider. @@ -48,11 +63,26 @@ export class ThemeProvider extends FoundationElement { @attr() public theme: Theme = Theme.light; + public get validity(): ValidityObject { + return { + invalidLang: this.langIsInvalid + }; + } + + private langIsInvalid = false; + public langChanged( _prev: string | undefined | null, next: string | undefined | null ): void { - if (next !== undefined && next !== null && next !== '') { + this.langIsInvalid = false; + if ( + next !== undefined + && next !== null + && next !== '' + // eslint-disable-next-line no-cond-assign + && !(this.langIsInvalid = isInvalidLang(next)) + ) { lang.setValueFor(this, next); } else { lang.deleteValueFor(this); diff --git a/packages/nimble-components/src/theme-provider/tests/theme-provider.spec.ts b/packages/nimble-components/src/theme-provider/tests/theme-provider.spec.ts index c10f36fbdd..932c51231c 100644 --- a/packages/nimble-components/src/theme-provider/tests/theme-provider.spec.ts +++ b/packages/nimble-components/src/theme-provider/tests/theme-provider.spec.ts @@ -1,9 +1,11 @@ +import { html } from '@microsoft/fast-element'; import { spinalCase } from '@microsoft/fast-web-utilities'; import * as designTokensNamespace from '../design-tokens'; import { tokenNames, suffixFromTokenName } from '../design-token-names'; import { getSpecTypeByNamedList } from '../../utilities/tests/parameterized'; -import { ThemeProvider, lang } from '..'; +import { ThemeProvider, lang, themeProviderTag } from '..'; import { waitForUpdatesAsync } from '../../testing/async-helpers'; +import { fixture, type Fixture } from '../../utilities/tests/fixture'; type DesignTokenPropertyName = keyof typeof designTokensNamespace; const designTokenPropertyNames = Object.keys( @@ -17,22 +19,77 @@ describe('Theme Provider', () => { ); }); - it('sets lang token value to "foo" when lang attribute is assigned value "foo"', async () => { - const element: ThemeProvider = document.createElement( - 'nimble-theme-provider' - ); - element.setAttribute('lang', 'foo'); - await waitForUpdatesAsync(); - expect(lang.getValueFor(element)).toBe('foo'); - }); + describe('lang token', () => { + async function setup( + langValue = 'foo' + ): Promise> { + return fixture( + html`<${themeProviderTag} lang="${langValue}"> + ` + ); + } - it('clears lang token value when lang attribute is removed', async () => { - const element: ThemeProvider = document.createElement( - 'nimble-theme-provider' - ); - element.removeAttribute('lang'); - await waitForUpdatesAsync(); - expect(lang.getValueFor(element)).toBe(''); + let element: ThemeProvider; + let connect: () => Promise; + let disconnect: () => Promise; + + afterEach(async () => { + await disconnect(); + }); + + it('value is set to "foo" when theme provider lang attribute is assigned value "foo"', async () => { + ({ element, connect, disconnect } = await setup()); + await connect(); + await waitForUpdatesAsync(); + expect(lang.getValueFor(element)).toBe('foo'); + }); + + it('value defaults to page lang when theme provider lang attribute is removed', async () => { + document.documentElement.lang = 'bar'; + ({ element, connect, disconnect } = await setup()); + await connect(); + element.removeAttribute('lang'); + await waitForUpdatesAsync(); + expect(lang.getValueFor(element)).toBe('bar'); + }); + + it('value defaults to page lang when theme provider lang attribute is empty string', async () => { + document.documentElement.lang = 'bar'; + ({ element, connect, disconnect } = await setup('')); + await connect(); + await waitForUpdatesAsync(); + expect(lang.getValueFor(element)).toBe('bar'); + expect(element.validity.invalidLang).toBeFalse(); + }); + + it('value defaults to page lang when theme provider lang attribute is malformed', async () => { + document.documentElement.lang = 'bar'; + ({ element, connect, disconnect } = await setup('123')); + await connect(); + await waitForUpdatesAsync(); + expect(lang.getValueFor(element)).toBe('bar'); + expect(element.validity.invalidLang).toBeTrue(); + }); + + it('value updates when theme provider lang attribute goes from bad to good', async () => { + document.documentElement.lang = 'bar'; + ({ element, connect, disconnect } = await setup('123')); + await connect(); + await waitForUpdatesAsync(); + element.setAttribute('lang', 'fr-CA'); + await waitForUpdatesAsync(); + expect(lang.getValueFor(element)).toBe('fr-CA'); + expect(element.validity.invalidLang).toBeFalse(); + }); + + it('value defaults to system default (en-US) when page lang is malformed and theme provider lang is malformed', async () => { + document.documentElement.lang = '123'; + ({ element, connect, disconnect } = await setup('456')); + await connect(); + await waitForUpdatesAsync(); + // our karma config and GitHub actions config set the lang to en-US + expect(lang.getValueFor(element)).toBe('en-US'); + }); }); describe('design token should match CSSDesignToken', () => { diff --git a/packages/nimble-components/src/utilities/models/validator.ts b/packages/nimble-components/src/utilities/models/validator.ts index e4546336a5..8cc8489cba 100644 --- a/packages/nimble-components/src/utilities/models/validator.ts +++ b/packages/nimble-components/src/utilities/models/validator.ts @@ -1,6 +1,9 @@ -import type { ValidityObject } from '../../table/types'; import { Tracker } from './tracker'; +export interface ValidityObject { + [key: string]: boolean; +} + /** * Generic Validator Utility extends Tracker Utility for validation purposes */ From 1467ab50f5e366e84cc01fc8fc816b3896f51e46 Mon Sep 17 00:00:00 2001 From: Mert Akinc <7282195+m-akinc@users.noreply.github.com> Date: Fri, 4 Aug 2023 17:10:20 -0500 Subject: [PATCH 11/23] Remove locale validation from date-text column --- .../src/table-column/date-text/index.ts | 24 +++---------------- .../table-column-date-text-validator.ts | 9 +------ .../tests/table-column-date-text.spec.ts | 24 +------------------ 3 files changed, 5 insertions(+), 52 deletions(-) diff --git a/packages/nimble-components/src/table-column/date-text/index.ts b/packages/nimble-components/src/table-column/date-text/index.ts index 9b359616a8..2db575c91b 100644 --- a/packages/nimble-components/src/table-column/date-text/index.ts +++ b/packages/nimble-components/src/table-column/date-text/index.ts @@ -225,7 +225,9 @@ export class TableColumnDateText extends TableColumnTextBase { formatter: this.createFormatter() }; this.columnInternals.columnConfig = columnConfig; - this.validateConfig(columnConfig.formatter !== undefined); + this.validator.setCustomOptionsValidity( + columnConfig.formatter !== undefined + ); } private createFormatter(): Intl.DateTimeFormat | undefined { @@ -272,26 +274,6 @@ export class TableColumnDateText extends TableColumnTextBase { }; return options; } - - private validateConfig(configIsValid: boolean): void { - let invalidLangCode = false; - let invalidCustomOptions = false; - if (!configIsValid) { - try { - // We don't know whether the lang code we used was bad, or if the config - // options were bad. To determine which, try constructing a formatter - // with default options. If it throws, the lang code was the problem. - Intl.DateTimeFormat(lang.getValueFor(this)); - invalidLangCode = false; - invalidCustomOptions = true; - } catch (e) { - invalidLangCode = true; - invalidCustomOptions = false; - } - } - this.validator.setCustomOptionsValidity(!invalidCustomOptions); - this.validator.setLangCodeValidity(!invalidLangCode); - } } const nimbleTableColumnDateText = TableColumnDateText.compose({ diff --git a/packages/nimble-components/src/table-column/date-text/models/table-column-date-text-validator.ts b/packages/nimble-components/src/table-column/date-text/models/table-column-date-text-validator.ts index ef98754173..747594471b 100644 --- a/packages/nimble-components/src/table-column/date-text/models/table-column-date-text-validator.ts +++ b/packages/nimble-components/src/table-column/date-text/models/table-column-date-text-validator.ts @@ -1,10 +1,7 @@ import type { ColumnInternals } from '../../base/models/column-internals'; import { ColumnValidator } from '../../base/models/column-validator'; -const dateTextValidityFlagNames = [ - 'invalidCustomOptionsCombination', - 'invalidLangCode' -] as const; +const dateTextValidityFlagNames = ['invalidCustomOptionsCombination'] as const; /** * Validator for TableColumnDateText. @@ -19,8 +16,4 @@ export class TableColumnDateTextValidator extends ColumnValidator< public setCustomOptionsValidity(valid: boolean): void { this.setConditionValue('invalidCustomOptionsCombination', !valid); } - - public setLangCodeValidity(valid: boolean): void { - this.setConditionValue('invalidLangCode', !valid); - } } diff --git a/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.spec.ts b/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.spec.ts index 73a5a30013..f92dd6a578 100644 --- a/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.spec.ts +++ b/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.spec.ts @@ -483,13 +483,12 @@ describe('TableColumnDateText', () => { expect(pageObject.getRenderedCellContent(0, 0)).toBe('2012'); }); - it('has no invalid flags on column when using default formatting', async () => { + it('has no invalid flag on column when using default formatting', async () => { await element.setData([ { field: new Date('Dec 10, 2012, 10:35:05 PM').valueOf() } ]); await waitForUpdatesAsync(); expect(column.validity.invalidCustomOptionsCombination).toBeFalse(); - expect(column.validity.invalidLangCode).toBeFalse(); }); it('sets invalidCustomOptionsCombination flag on column when custom options are incompatible', async () => { @@ -502,7 +501,6 @@ describe('TableColumnDateText', () => { column.customDateStyle = 'full'; await waitForUpdatesAsync(); expect(column.validity.invalidCustomOptionsCombination).toBeTrue(); - expect(column.validity.invalidLangCode).toBeFalse(); }); it('clears invalidCustomOptionsCombination flag on column after fixing custom option incompatibility', async () => { @@ -518,26 +516,6 @@ describe('TableColumnDateText', () => { await waitForUpdatesAsync(); expect(column.validity.invalidCustomOptionsCombination).toBeFalse(); }); - - it('sets invalidLangCode flag on column when lang code is malformed', async () => { - await element.setData([ - { field: new Date('Dec 10, 2012, 10:35:05 PM').valueOf() } - ]); - lang.setValueFor(element, ''); - await waitForUpdatesAsync(); - expect(column.validity.invalidLangCode).toBeTrue(); - expect(column.validity.invalidCustomOptionsCombination).toBeFalse(); - }); - - it('clears invalidLangCode flag on column after fixing lang code', async () => { - await element.setData([ - { field: new Date('Dec 10, 2012, 10:35:05 PM').valueOf() } - ]); - lang.setValueFor(element, ''); - await waitForUpdatesAsync(); - lang.setValueFor(element, 'en'); - expect(column.validity.invalidLangCode).toBeFalse(); - }); }); describe('with static config', () => { From aa2d79302b7c7cbc92e376cc9823c39be2d1465c Mon Sep 17 00:00:00 2001 From: m-akinc <7282195+m-akinc@users.noreply.github.com> Date: Mon, 7 Aug 2023 12:06:16 -0500 Subject: [PATCH 12/23] Apply suggestions from code review Co-authored-by: mollykreis <20542556+mollykreis@users.noreply.github.com> --- .../nimble-components/src/table-column/date-text/index.ts | 1 + packages/nimble-components/src/theme-provider/index.ts | 7 ++----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/nimble-components/src/table-column/date-text/index.ts b/packages/nimble-components/src/table-column/date-text/index.ts index 2db575c91b..d620a89d8a 100644 --- a/packages/nimble-components/src/table-column/date-text/index.ts +++ b/packages/nimble-components/src/table-column/date-text/index.ts @@ -123,6 +123,7 @@ export class TableColumnDateText extends TableColumnTextBase { } public override disconnectedCallback(): void { + super.disconnectedCallback(); lang.unsubscribe(this.langSubscriber, this); } diff --git a/packages/nimble-components/src/theme-provider/index.ts b/packages/nimble-components/src/theme-provider/index.ts index 6aec00ce93..49d823a305 100644 --- a/packages/nimble-components/src/theme-provider/index.ts +++ b/packages/nimble-components/src/theme-provider/index.ts @@ -19,8 +19,7 @@ declare global { function isInvalidLang(value: string): boolean { try { - // eslint-disable-next-line no-new - new Intl.DateTimeFormat(value); + Intl.DateTimeFormat(value); return false; } catch (e) { return true; @@ -77,9 +76,7 @@ export class ThemeProvider extends FoundationElement { ): void { this.langIsInvalid = false; if ( - next !== undefined - && next !== null - && next !== '' + next // eslint-disable-next-line no-cond-assign && !(this.langIsInvalid = isInvalidLang(next)) ) { From 42d3f7c6faf6db3de94f9c6f73e34a92a69805f4 Mon Sep 17 00:00:00 2001 From: Mert Akinc <7282195+m-akinc@users.noreply.github.com> Date: Mon, 7 Aug 2023 13:00:39 -0500 Subject: [PATCH 13/23] feedback --- .../src/theme-provider/index.ts | 4 +++ .../tests/theme-provider.spec.ts | 31 +++++++++++++------ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/packages/nimble-components/src/theme-provider/index.ts b/packages/nimble-components/src/theme-provider/index.ts index 49d823a305..6325a7213f 100644 --- a/packages/nimble-components/src/theme-provider/index.ts +++ b/packages/nimble-components/src/theme-provider/index.ts @@ -70,6 +70,10 @@ export class ThemeProvider extends FoundationElement { private langIsInvalid = false; + public checkValidity(): boolean { + return !this.langIsInvalid; + } + public langChanged( _prev: string | undefined | null, next: string | undefined | null diff --git a/packages/nimble-components/src/theme-provider/tests/theme-provider.spec.ts b/packages/nimble-components/src/theme-provider/tests/theme-provider.spec.ts index 932c51231c..acd868a133 100644 --- a/packages/nimble-components/src/theme-provider/tests/theme-provider.spec.ts +++ b/packages/nimble-components/src/theme-provider/tests/theme-provider.spec.ts @@ -21,7 +21,7 @@ describe('Theme Provider', () => { describe('lang token', () => { async function setup( - langValue = 'foo' + langValue = 'fr-FR' ): Promise> { return fixture( html`<${themeProviderTag} lang="${langValue}"> @@ -37,42 +37,42 @@ describe('Theme Provider', () => { await disconnect(); }); - it('value is set to "foo" when theme provider lang attribute is assigned value "foo"', async () => { + it('value is set to "fr-FR" when theme provider lang attribute is assigned value "fr-FR"', async () => { ({ element, connect, disconnect } = await setup()); await connect(); await waitForUpdatesAsync(); - expect(lang.getValueFor(element)).toBe('foo'); + expect(lang.getValueFor(element)).toBe('fr-FR'); }); it('value defaults to page lang when theme provider lang attribute is removed', async () => { - document.documentElement.lang = 'bar'; + document.documentElement.lang = 'de-DE'; ({ element, connect, disconnect } = await setup()); await connect(); element.removeAttribute('lang'); await waitForUpdatesAsync(); - expect(lang.getValueFor(element)).toBe('bar'); + expect(lang.getValueFor(element)).toBe('de-DE'); }); it('value defaults to page lang when theme provider lang attribute is empty string', async () => { - document.documentElement.lang = 'bar'; + document.documentElement.lang = 'de-DE'; ({ element, connect, disconnect } = await setup('')); await connect(); await waitForUpdatesAsync(); - expect(lang.getValueFor(element)).toBe('bar'); + expect(lang.getValueFor(element)).toBe('de-DE'); expect(element.validity.invalidLang).toBeFalse(); }); it('value defaults to page lang when theme provider lang attribute is malformed', async () => { - document.documentElement.lang = 'bar'; + document.documentElement.lang = 'de-DE'; ({ element, connect, disconnect } = await setup('123')); await connect(); await waitForUpdatesAsync(); - expect(lang.getValueFor(element)).toBe('bar'); + expect(lang.getValueFor(element)).toBe('de-DE'); expect(element.validity.invalidLang).toBeTrue(); }); it('value updates when theme provider lang attribute goes from bad to good', async () => { - document.documentElement.lang = 'bar'; + document.documentElement.lang = 'de-DE'; ({ element, connect, disconnect } = await setup('123')); await connect(); await waitForUpdatesAsync(); @@ -82,6 +82,17 @@ describe('Theme Provider', () => { expect(element.validity.invalidLang).toBeFalse(); }); + it('value updates when theme provider lang attribute goes from good to bad', async () => { + document.documentElement.lang = 'de-DE'; + ({ element, connect, disconnect } = await setup()); + await connect(); + await waitForUpdatesAsync(); + element.setAttribute('lang', '123'); + await waitForUpdatesAsync(); + expect(lang.getValueFor(element)).toBe('de-DE'); + expect(element.validity.invalidLang).toBeTrue(); + }); + it('value defaults to system default (en-US) when page lang is malformed and theme provider lang is malformed', async () => { document.documentElement.lang = '123'; ({ element, connect, disconnect } = await setup('456')); From 86a55b92d490f8543a9b841909ce93a9725d29dc Mon Sep 17 00:00:00 2001 From: Mert Akinc <7282195+m-akinc@users.noreply.github.com> Date: Mon, 7 Aug 2023 17:06:30 -0500 Subject: [PATCH 14/23] Remove suppression --- .../nimble-components/src/theme-provider/index.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/nimble-components/src/theme-provider/index.ts b/packages/nimble-components/src/theme-provider/index.ts index 6325a7213f..302c1a736e 100644 --- a/packages/nimble-components/src/theme-provider/index.ts +++ b/packages/nimble-components/src/theme-provider/index.ts @@ -79,11 +79,15 @@ export class ThemeProvider extends FoundationElement { next: string | undefined | null ): void { this.langIsInvalid = false; - if ( - next - // eslint-disable-next-line no-cond-assign - && !(this.langIsInvalid = isInvalidLang(next)) - ) { + let shouldSetNewTokenValue = false; + if (next) { + this.langIsInvalid = isInvalidLang(next); + if (!this.langIsInvalid) { + shouldSetNewTokenValue = true; + } + } + + if (shouldSetNewTokenValue) { lang.setValueFor(this, next); } else { lang.deleteValueFor(this); From 418ce874d3efe1e2e4dc4686528a78e3941da0ff Mon Sep 17 00:00:00 2001 From: Mert Akinc <7282195+m-akinc@users.noreply.github.com> Date: Mon, 7 Aug 2023 17:15:20 -0500 Subject: [PATCH 15/23] Fix build --- packages/nimble-components/src/theme-provider/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nimble-components/src/theme-provider/index.ts b/packages/nimble-components/src/theme-provider/index.ts index 302c1a736e..04932f2feb 100644 --- a/packages/nimble-components/src/theme-provider/index.ts +++ b/packages/nimble-components/src/theme-provider/index.ts @@ -88,7 +88,7 @@ export class ThemeProvider extends FoundationElement { } if (shouldSetNewTokenValue) { - lang.setValueFor(this, next); + lang.setValueFor(this, next!); } else { lang.deleteValueFor(this); } From 5cbd5ca8b33aa39c94888dea84da548fc668bf4d Mon Sep 17 00:00:00 2001 From: Mert Akinc <7282195+m-akinc@users.noreply.github.com> Date: Thu, 17 Aug 2023 16:46:07 -0500 Subject: [PATCH 16/23] Rename PageLocale and change lang token default --- packages/nimble-components/src/theme-provider/index.ts | 8 ++------ .../models/{page-locale.ts => document-element-lang.ts} | 4 ++-- 2 files changed, 4 insertions(+), 8 deletions(-) rename packages/nimble-components/src/utilities/models/{page-locale.ts => document-element-lang.ts} (88%) diff --git a/packages/nimble-components/src/theme-provider/index.ts b/packages/nimble-components/src/theme-provider/index.ts index 04932f2feb..6b46433145 100644 --- a/packages/nimble-components/src/theme-provider/index.ts +++ b/packages/nimble-components/src/theme-provider/index.ts @@ -8,7 +8,7 @@ import { Direction } from '@microsoft/fast-web-utilities'; import { template } from './template'; import { styles } from './styles'; import { Theme } from './types'; -import { pageLocale } from '../utilities/models/page-locale'; +import { documentElementLang } from '../utilities/models/document-element-lang'; import type { ValidityObject } from '../utilities/models/validator'; declare global { @@ -26,14 +26,10 @@ function isInvalidLang(value: string): boolean { } } -function getSystemLocale(): string { - return new Intl.DateTimeFormat(undefined).resolvedOptions().locale; -} - export const lang = DesignToken.create({ name: 'lang', cssCustomPropertyName: null -}).withDefault((): string => (isInvalidLang(pageLocale.lang) ? getSystemLocale() : pageLocale.lang)); +}).withDefault((): string => (isInvalidLang(documentElementLang.lang) ? 'en-US' : documentElementLang.lang)); // Not represented as a CSS Custom Property, instead available // as an attribute of theme provider. diff --git a/packages/nimble-components/src/utilities/models/page-locale.ts b/packages/nimble-components/src/utilities/models/document-element-lang.ts similarity index 88% rename from packages/nimble-components/src/utilities/models/page-locale.ts rename to packages/nimble-components/src/utilities/models/document-element-lang.ts index fc4685729e..a0451f1ded 100644 --- a/packages/nimble-components/src/utilities/models/page-locale.ts +++ b/packages/nimble-components/src/utilities/models/document-element-lang.ts @@ -3,7 +3,7 @@ import { observable } from '@microsoft/fast-element'; /** * Observable class to subscribe to changes in the page's lang attribute */ -class PageLocale { +class DocumentElementLang { @observable public lang: string = document.documentElement.lang; @@ -24,4 +24,4 @@ class PageLocale { } } -export const pageLocale = new PageLocale(); +export const documentElementLang = new DocumentElementLang(); From f9c98e3a0c5b3737a395a5c873496c933ee300e8 Mon Sep 17 00:00:00 2001 From: Mert Akinc <7282195+m-akinc@users.noreply.github.com> Date: Mon, 28 Aug 2023 12:58:53 -0500 Subject: [PATCH 17/23] Document theme provider attributes --- .../theme-provider/tests/theme-provider.mdx | 9 ++++ .../tests/theme-provider.stories.ts | 50 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 packages/nimble-components/src/theme-provider/tests/theme-provider.mdx create mode 100644 packages/nimble-components/src/theme-provider/tests/theme-provider.stories.ts diff --git a/packages/nimble-components/src/theme-provider/tests/theme-provider.mdx b/packages/nimble-components/src/theme-provider/tests/theme-provider.mdx new file mode 100644 index 0000000000..50305ca6f4 --- /dev/null +++ b/packages/nimble-components/src/theme-provider/tests/theme-provider.mdx @@ -0,0 +1,9 @@ +import { Controls, Meta, Title } from '@storybook/blocks'; +import { themeProvider } from './theme-provider.stories' + + +Theme Provider + +The theme provider element allows configuring certain token values for the contained html tree. + + \ No newline at end of file diff --git a/packages/nimble-components/src/theme-provider/tests/theme-provider.stories.ts b/packages/nimble-components/src/theme-provider/tests/theme-provider.stories.ts new file mode 100644 index 0000000000..d8dfbd5556 --- /dev/null +++ b/packages/nimble-components/src/theme-provider/tests/theme-provider.stories.ts @@ -0,0 +1,50 @@ +import { html } from '@microsoft/fast-element'; +import type { Meta, StoryObj } from '@storybook/html'; +import { createUserSelectedThemeStory } from '../../utilities/tests/storybook'; +import { hiddenWrapper } from '../../utilities/tests/hidden'; + +const metadata: Meta = { + title: 'Internal/Theme Provider', + parameters: { + docs: { + description: { + component: '' + } + } + } +}; + +export default metadata; + +const langDescription = `Defines the language of the element. See [external documentation](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang) for details. + +Users should set \`lang\` on the root \`html\` element of the page to reflect the language of the content. If necessary, users may override the language for a subtree by inserting a \`nimble-theme-provider\` element and setting its \`lang\` attribute. Nimble elements will not honor a \`lang\` value set on any other type of ancestor element.`; + +export const themeProvider: StoryObj = { + render: createUserSelectedThemeStory(hiddenWrapper(html``)), + argTypes: { + theme: { + description: + 'The display theme to use. One of `Theme.light`, `Theme.dark`, or `Theme.color`.', + defaultValue: { + summary: 'Theme.light' + }, + control: { type: 'none' } + }, + lang: { + description: langDescription, + defaultValue: { + summary: + '`lang` of the document element if set, otherwise "en-US".' + } + }, + direction: { + description: + 'The text direction of the element. Either `Direction.ltr` or `Direction.rtl`.', + defaultValue: { + summary: 'Direction.ltr' + } + } + }, + args: {} +}; From fbf8b88dd7a71112ac0a57bc609028b0c9255dfe Mon Sep 17 00:00:00 2001 From: Mert Akinc <7282195+m-akinc@users.noreply.github.com> Date: Mon, 28 Aug 2023 13:17:08 -0500 Subject: [PATCH 18/23] Lint fix --- .../src/theme-provider/tests/theme-provider.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nimble-components/src/theme-provider/tests/theme-provider.mdx b/packages/nimble-components/src/theme-provider/tests/theme-provider.mdx index 50305ca6f4..1009ce35e7 100644 --- a/packages/nimble-components/src/theme-provider/tests/theme-provider.mdx +++ b/packages/nimble-components/src/theme-provider/tests/theme-provider.mdx @@ -1,9 +1,9 @@ import { Controls, Meta, Title } from '@storybook/blocks'; -import { themeProvider } from './theme-provider.stories' +import { themeProvider } from './theme-provider.stories'; Theme Provider The theme provider element allows configuring certain token values for the contained html tree. - \ No newline at end of file + From f158b48a779bfa17254767a691043430a11b8401 Mon Sep 17 00:00:00 2001 From: m-akinc <7282195+m-akinc@users.noreply.github.com> Date: Thu, 31 Aug 2023 12:20:59 -0500 Subject: [PATCH 19/23] Update packages/nimble-components/src/theme-provider/tests/theme-provider.mdx Co-authored-by: Jesse Attas --- .../src/theme-provider/tests/theme-provider.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nimble-components/src/theme-provider/tests/theme-provider.mdx b/packages/nimble-components/src/theme-provider/tests/theme-provider.mdx index 1009ce35e7..d83218ba88 100644 --- a/packages/nimble-components/src/theme-provider/tests/theme-provider.mdx +++ b/packages/nimble-components/src/theme-provider/tests/theme-provider.mdx @@ -4,6 +4,6 @@ import { themeProvider } from './theme-provider.stories'; Theme Provider -The theme provider element allows configuring certain token values for the contained html tree. +The theme provider element allows configuring certain token values for the contained HTML tree. From e387a9b2adad85e5c0621750939f8138b2e923be Mon Sep 17 00:00:00 2001 From: Mert Akinc <7282195+m-akinc@users.noreply.github.com> Date: Thu, 31 Aug 2023 17:40:09 -0500 Subject: [PATCH 20/23] Feedback --- .../tests/table-column-date-text.stories.ts | 2 +- .../src/theme-provider/index.ts | 36 +++---- .../theme-provider/tests/theme-provider.mdx | 3 +- .../tests/theme-provider.spec.ts | 48 ++++++++-- .../tests/theme-provider.stories.ts | 93 ++++++++++++++++--- 5 files changed, 143 insertions(+), 39 deletions(-) diff --git a/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.stories.ts b/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.stories.ts index a8b81c26f9..6a8c0819a4 100644 --- a/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.stories.ts +++ b/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.stories.ts @@ -106,7 +106,7 @@ interface TextColumnTableArgs extends SharedTableArgs { validity: () => void; } -const dateTextColumnDescription = 'The `nimble-table-column-date-text` column is used to display date-time fields as text in the `nimble-table`. The date-time values must be of type `number` and represent the number of milliseconds since January 1, 1970 UTC. This is the representation used by the [JavaScript `Date` type](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date).'; +const dateTextColumnDescription = 'The `nimble-table-column-date-text` column is used to display date-time fields as text in the `nimble-table`. The date-time values must be of type `number` and represent the number of milliseconds since January 1, 1970 UTC. This is the representation used by the [JavaScript `Date` type](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date). Dates are formatted in a locale-specific way based on the value of the `lang` token, which can be set via the [`nimble-theme-provider`](/docs/tokens-theme-provider--docs).'; const validityDescription = `Readonly object of boolean values that represents the validity states that the column's configuration can be in. The object's type is \`TableColumnValidity\`, and it contains the following boolean properties: diff --git a/packages/nimble-components/src/theme-provider/index.ts b/packages/nimble-components/src/theme-provider/index.ts index 6b46433145..16a5334322 100644 --- a/packages/nimble-components/src/theme-provider/index.ts +++ b/packages/nimble-components/src/theme-provider/index.ts @@ -11,25 +11,29 @@ import { Theme } from './types'; import { documentElementLang } from '../utilities/models/document-element-lang'; import type { ValidityObject } from '../utilities/models/validator'; +export { Direction }; + declare global { interface HTMLElementTagNameMap { 'nimble-theme-provider': ThemeProvider; } } -function isInvalidLang(value: string): boolean { +function isValidLang(value: string): boolean { try { - Intl.DateTimeFormat(value); - return false; - } catch (e) { + // We are relying on the Locale constructor to validate the value + // eslint-disable-next-line no-new + new Intl.Locale(value); return true; + } catch (e) { + return false; } } export const lang = DesignToken.create({ name: 'lang', cssCustomPropertyName: null -}).withDefault((): string => (isInvalidLang(documentElementLang.lang) ? 'en-US' : documentElementLang.lang)); +}).withDefault((): string => (isValidLang(documentElementLang.lang) ? documentElementLang.lang : 'en-US')); // Not represented as a CSS Custom Property, instead available // as an attribute of theme provider. @@ -50,10 +54,11 @@ export const theme = DesignToken.create({ */ export class ThemeProvider extends FoundationElement { @attr() - public override lang = ''; + // @ts-expect-error: Do not want to initialize, but type cannot include undefined because of override + public override lang: string; @attr() - public direction: Direction = Direction.ltr; + public direction?: Direction; @attr() public theme: Theme = Theme.light; @@ -74,19 +79,18 @@ export class ThemeProvider extends FoundationElement { _prev: string | undefined | null, next: string | undefined | null ): void { - this.langIsInvalid = false; - let shouldSetNewTokenValue = false; - if (next) { - this.langIsInvalid = isInvalidLang(next); - if (!this.langIsInvalid) { - shouldSetNewTokenValue = true; - } + if (next === null || next === undefined) { + lang.deleteValueFor(this); + this.langIsInvalid = false; + return; } - if (shouldSetNewTokenValue) { - lang.setValueFor(this, next!); + if (isValidLang(next)) { + lang.setValueFor(this, next); + this.langIsInvalid = false; } else { lang.deleteValueFor(this); + this.langIsInvalid = true; } } diff --git a/packages/nimble-components/src/theme-provider/tests/theme-provider.mdx b/packages/nimble-components/src/theme-provider/tests/theme-provider.mdx index d83218ba88..adfbbc4158 100644 --- a/packages/nimble-components/src/theme-provider/tests/theme-provider.mdx +++ b/packages/nimble-components/src/theme-provider/tests/theme-provider.mdx @@ -1,4 +1,4 @@ -import { Controls, Meta, Title } from '@storybook/blocks'; +import { Controls, DocsStory, Meta, Title } from '@storybook/blocks'; import { themeProvider } from './theme-provider.stories'; @@ -6,4 +6,5 @@ import { themeProvider } from './theme-provider.stories'; The theme provider element allows configuring certain token values for the contained HTML tree. + diff --git a/packages/nimble-components/src/theme-provider/tests/theme-provider.spec.ts b/packages/nimble-components/src/theme-provider/tests/theme-provider.spec.ts index acd868a133..f260eb99ad 100644 --- a/packages/nimble-components/src/theme-provider/tests/theme-provider.spec.ts +++ b/packages/nimble-components/src/theme-provider/tests/theme-provider.spec.ts @@ -21,10 +21,12 @@ describe('Theme Provider', () => { describe('lang token', () => { async function setup( - langValue = 'fr-FR' + langValue: string | undefined ): Promise> { return fixture( - html`<${themeProviderTag} lang="${langValue}"> + html`<${themeProviderTag} ${ + langValue === undefined ? '' : `lang="${langValue}"` + }> ` ); } @@ -32,13 +34,23 @@ describe('Theme Provider', () => { let element: ThemeProvider; let connect: () => Promise; let disconnect: () => Promise; + let pageLangToRestore: string | undefined; + + beforeEach(() => { + pageLangToRestore = document.documentElement.lang; + }); afterEach(async () => { await disconnect(); + if (pageLangToRestore) { + document.documentElement.lang = pageLangToRestore; + } else { + document.documentElement.removeAttribute('lang'); + } }); it('value is set to "fr-FR" when theme provider lang attribute is assigned value "fr-FR"', async () => { - ({ element, connect, disconnect } = await setup()); + ({ element, connect, disconnect } = await setup('fr-FR')); await connect(); await waitForUpdatesAsync(); expect(lang.getValueFor(element)).toBe('fr-FR'); @@ -46,20 +58,29 @@ describe('Theme Provider', () => { it('value defaults to page lang when theme provider lang attribute is removed', async () => { document.documentElement.lang = 'de-DE'; - ({ element, connect, disconnect } = await setup()); + ({ element, connect, disconnect } = await setup('fr-FR')); await connect(); element.removeAttribute('lang'); await waitForUpdatesAsync(); expect(lang.getValueFor(element)).toBe('de-DE'); }); + it('value defaults to page lang when theme provider lang attribute is undefined', async () => { + document.documentElement.lang = 'de-DE'; + ({ element, connect, disconnect } = await setup(undefined)); + await connect(); + await waitForUpdatesAsync(); + expect(lang.getValueFor(element)).toBe('de-DE'); + expect(element.validity.invalidLang).toBeFalse(); + }); + it('value defaults to page lang when theme provider lang attribute is empty string', async () => { document.documentElement.lang = 'de-DE'; ({ element, connect, disconnect } = await setup('')); await connect(); await waitForUpdatesAsync(); expect(lang.getValueFor(element)).toBe('de-DE'); - expect(element.validity.invalidLang).toBeFalse(); + expect(element.validity.invalidLang).toBeTrue(); }); it('value defaults to page lang when theme provider lang attribute is malformed', async () => { @@ -71,7 +92,18 @@ describe('Theme Provider', () => { expect(element.validity.invalidLang).toBeTrue(); }); - it('value updates when theme provider lang attribute goes from bad to good', async () => { + it('value updates when page lang changes (while theme provider lang unset)', async () => { + document.documentElement.lang = 'de-DE'; + ({ element, connect, disconnect } = await setup(undefined)); + await connect(); + await waitForUpdatesAsync(); + document.documentElement.lang = 'fr-FR'; + await waitForUpdatesAsync(); + expect(lang.getValueFor(element)).toBe('fr-FR'); + expect(element.validity.invalidLang).toBeFalse(); + }); + + it('value updates when theme provider lang attribute goes from invalid to valid', async () => { document.documentElement.lang = 'de-DE'; ({ element, connect, disconnect } = await setup('123')); await connect(); @@ -82,9 +114,9 @@ describe('Theme Provider', () => { expect(element.validity.invalidLang).toBeFalse(); }); - it('value updates when theme provider lang attribute goes from good to bad', async () => { + it('value updates when theme provider lang attribute goes from valid to invalid', async () => { document.documentElement.lang = 'de-DE'; - ({ element, connect, disconnect } = await setup()); + ({ element, connect, disconnect } = await setup('fr-FR')); await connect(); await waitForUpdatesAsync(); element.setAttribute('lang', '123'); diff --git a/packages/nimble-components/src/theme-provider/tests/theme-provider.stories.ts b/packages/nimble-components/src/theme-provider/tests/theme-provider.stories.ts index d8dfbd5556..10ad48c8aa 100644 --- a/packages/nimble-components/src/theme-provider/tests/theme-provider.stories.ts +++ b/packages/nimble-components/src/theme-provider/tests/theme-provider.stories.ts @@ -1,16 +1,50 @@ -import { html } from '@microsoft/fast-element'; +import { html, ref } from '@microsoft/fast-element'; import type { Meta, StoryObj } from '@storybook/html'; import { createUserSelectedThemeStory } from '../../utilities/tests/storybook'; -import { hiddenWrapper } from '../../utilities/tests/hidden'; +import { tableTag } from '../../table'; +import { tableColumnDateTextTag } from '../../table-column/date-text'; +import { + sharedTableArgTypes, + type SharedTableArgs, + sharedTableArgs +} from '../../table-column/base/tests/table-column-stories-utils'; +import { Theme } from '../types'; +import { Direction, themeProviderTag } from '..'; + +const simpleData = [ + { + date: new Date(1984, 4, 12, 14, 34, 19, 377).valueOf() + }, + { + date: new Date(1984, 2, 19, 7, 6, 48, 584).valueOf() + }, + { + date: new Date(2013, 3, 1, 20, 4, 37, 975).valueOf() + }, + { + date: new Date(2022, 0, 12, 20, 4, 37, 975).valueOf() + } +]; const metadata: Meta = { - title: 'Internal/Theme Provider', + title: 'Tokens/Theme Provider', parameters: { docs: { description: { component: '' } } + }, + argTypes: { + ...sharedTableArgTypes, + selectionMode: { + table: { + disable: true + } + } + }, + args: { + ...sharedTableArgs(simpleData) } }; @@ -20,31 +54,64 @@ const langDescription = `Defines the language of the element. See [external docu Users should set \`lang\` on the root \`html\` element of the page to reflect the language of the content. If necessary, users may override the language for a subtree by inserting a \`nimble-theme-provider\` element and setting its \`lang\` attribute. Nimble elements will not honor a \`lang\` value set on any other type of ancestor element.`; -export const themeProvider: StoryObj = { - render: createUserSelectedThemeStory(hiddenWrapper(html``)), +interface ThemeProviderArgs extends SharedTableArgs { + theme: keyof typeof Theme; + lang: string; + direction: keyof typeof Direction; +} + +export const themeProvider: StoryObj = { + render: createUserSelectedThemeStory(html` + <${themeProviderTag} + theme="${x => Theme[x.theme]}" + lang="${x => x.lang}" + direction="${x => Direction[x.direction]}" + > + <${tableTag} + ${ref('tableRef')} + data-unused="${x => x.updateData(x)}" + style="height: 200px" + > + <${tableColumnDateTextTag} + field-name="date" + > + Date + + + + `), argTypes: { theme: { description: - 'The display theme to use. One of `Theme.light`, `Theme.dark`, or `Theme.color`.', + 'The display theme to use. One of `"light"`, `"dark"`, or `"color"`. The `Theme` type exposes these values.', defaultValue: { - summary: 'Theme.light' + summary: '"light"' }, - control: { type: 'none' } + options: Object.keys(Theme), + control: { type: 'radio' } }, lang: { description: langDescription, defaultValue: { summary: '`lang` of the document element if set, otherwise "en-US".' - } + }, + options: ['en-US', 'fr-FR', 'de-DE'], + control: { type: 'radio' } }, direction: { description: - 'The text direction of the element. Either `Direction.ltr` or `Direction.rtl`.', + 'The text direction of the element. Either `"ltr"` or `"rtl"`. The `Direction` type exposes these values.', defaultValue: { - summary: 'Direction.ltr' - } + summary: '"ltr"' + }, + options: Object.keys(Direction), + control: { type: 'radio' } } }, - args: {} + args: { + theme: Theme.light, + lang: 'en-US', + direction: Direction.ltr + } }; From 1526ac0cf9ef36c884d1f2b21987efe77671e006 Mon Sep 17 00:00:00 2001 From: Mert Akinc <7282195+m-akinc@users.noreply.github.com> Date: Thu, 31 Aug 2023 17:53:05 -0500 Subject: [PATCH 21/23] Replace suppression with non-null assertion --- packages/nimble-components/src/theme-provider/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/nimble-components/src/theme-provider/index.ts b/packages/nimble-components/src/theme-provider/index.ts index 16a5334322..c1f49f87b0 100644 --- a/packages/nimble-components/src/theme-provider/index.ts +++ b/packages/nimble-components/src/theme-provider/index.ts @@ -54,8 +54,7 @@ export const theme = DesignToken.create({ */ export class ThemeProvider extends FoundationElement { @attr() - // @ts-expect-error: Do not want to initialize, but type cannot include undefined because of override - public override lang: string; + public override lang!: string; @attr() public direction?: Direction; From 69e4e9e33a79e9982ec30948d2b089e265715802 Mon Sep 17 00:00:00 2001 From: m-akinc <7282195+m-akinc@users.noreply.github.com> Date: Fri, 1 Sep 2023 13:22:26 -0500 Subject: [PATCH 22/23] Apply suggestions from code review Co-authored-by: Jesse Attas --- .../date-text/tests/table-column-date-text.stories.ts | 2 +- .../src/theme-provider/tests/theme-provider.stories.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.stories.ts b/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.stories.ts index 6a8c0819a4..ddb22cf60b 100644 --- a/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.stories.ts +++ b/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.stories.ts @@ -106,7 +106,7 @@ interface TextColumnTableArgs extends SharedTableArgs { validity: () => void; } -const dateTextColumnDescription = 'The `nimble-table-column-date-text` column is used to display date-time fields as text in the `nimble-table`. The date-time values must be of type `number` and represent the number of milliseconds since January 1, 1970 UTC. This is the representation used by the [JavaScript `Date` type](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date). Dates are formatted in a locale-specific way based on the value of the `lang` token, which can be set via the [`nimble-theme-provider`](/docs/tokens-theme-provider--docs).'; +const dateTextColumnDescription = 'The `nimble-table-column-date-text` column is used to display date-time fields as text in the `nimble-table`. The date-time values must be of type `number` and represent the number of milliseconds since January 1, 1970 UTC. This is the representation used by the [JavaScript `Date` type](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date). Dates are formatted in a locale-specific way based on the value of the `lang` token, which can be set via the [`nimble-theme-provider`](?path=/docs/tokens-theme-provider--docs).'; const validityDescription = `Readonly object of boolean values that represents the validity states that the column's configuration can be in. The object's type is \`TableColumnValidity\`, and it contains the following boolean properties: diff --git a/packages/nimble-components/src/theme-provider/tests/theme-provider.stories.ts b/packages/nimble-components/src/theme-provider/tests/theme-provider.stories.ts index 10ad48c8aa..1082092e53 100644 --- a/packages/nimble-components/src/theme-provider/tests/theme-provider.stories.ts +++ b/packages/nimble-components/src/theme-provider/tests/theme-provider.stories.ts @@ -52,7 +52,7 @@ export default metadata; const langDescription = `Defines the language of the element. See [external documentation](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang) for details. -Users should set \`lang\` on the root \`html\` element of the page to reflect the language of the content. If necessary, users may override the language for a subtree by inserting a \`nimble-theme-provider\` element and setting its \`lang\` attribute. Nimble elements will not honor a \`lang\` value set on any other type of ancestor element.`; +Applications should set \`lang\` on the root \`html\` element of the page to reflect the language of the content. If necessary, users may override the language for a subtree by inserting a \`nimble-theme-provider\` element and setting its \`lang\` attribute. Nimble elements will not honor a \`lang\` value set on any other type of ancestor element.`; interface ThemeProviderArgs extends SharedTableArgs { theme: keyof typeof Theme; @@ -101,7 +101,7 @@ export const themeProvider: StoryObj = { }, direction: { description: - 'The text direction of the element. Either `"ltr"` or `"rtl"`. The `Direction` type exposes these values.', + 'The text direction of the element. Either `"ltr"` or `"rtl"`. The `Direction` type exposes these values. Note: Right-to-left support in Nimble is untested. If you need this capability, please file an issue.', defaultValue: { summary: '"ltr"' }, From ad4d26b69b4ef6cb29ba3f6dd47610e84567df8b Mon Sep 17 00:00:00 2001 From: Mert Akinc <7282195+m-akinc@users.noreply.github.com> Date: Fri, 1 Sep 2023 13:23:01 -0500 Subject: [PATCH 23/23] Feedback --- .../src/theme-provider/tests/theme-provider.spec.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/nimble-components/src/theme-provider/tests/theme-provider.spec.ts b/packages/nimble-components/src/theme-provider/tests/theme-provider.spec.ts index f260eb99ad..d9add6ab2b 100644 --- a/packages/nimble-components/src/theme-provider/tests/theme-provider.spec.ts +++ b/packages/nimble-components/src/theme-provider/tests/theme-provider.spec.ts @@ -34,18 +34,21 @@ describe('Theme Provider', () => { let element: ThemeProvider; let connect: () => Promise; let disconnect: () => Promise; - let pageLangToRestore: string | undefined; + let pageLangToRestore: string | null; beforeEach(() => { - pageLangToRestore = document.documentElement.lang; + pageLangToRestore = document.documentElement.getAttribute('lang'); }); afterEach(async () => { await disconnect(); - if (pageLangToRestore) { - document.documentElement.lang = pageLangToRestore; - } else { + if (pageLangToRestore === null) { document.documentElement.removeAttribute('lang'); + } else { + document.documentElement.setAttribute( + 'lang', + pageLangToRestore + ); } });