Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add locale support to theme provider and use in date-text column #1410

Merged
merged 26 commits into from
Sep 1, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
97f49ee
Add lang design token
m-akinc Jul 27, 2023
e2bd2bc
Use lang design token in date-text column
m-akinc Jul 27, 2023
889bdbb
Ensure test consistency by making the locale explicit
m-akinc Jul 27, 2023
fc460a2
Detect bad lang code and add tests
m-akinc Jul 27, 2023
fb60542
Change files
m-akinc Aug 1, 2023
dcb11d3
locale for tests: en => en-US
m-akinc Aug 1, 2023
c277413
Feedback
m-akinc Aug 1, 2023
4ab854b
Feedback
m-akinc Aug 2, 2023
ce80a89
Default lang token to page's lang
m-akinc Aug 4, 2023
0ab1544
Handle invalid lang values
m-akinc Aug 4, 2023
82c3c26
Merge branch 'main' into users/makinc/datetime-column-locale
m-akinc Aug 4, 2023
1467ab5
Remove locale validation from date-text column
m-akinc Aug 4, 2023
aa2d793
Apply suggestions from code review
m-akinc Aug 7, 2023
42d3f7c
feedback
m-akinc Aug 7, 2023
86a55b9
Remove suppression
m-akinc Aug 7, 2023
418ce87
Fix build
m-akinc Aug 7, 2023
5cbd5ca
Rename PageLocale and change lang token default
m-akinc Aug 17, 2023
d714a9f
Merge branch 'main' into users/makinc/datetime-column-locale
m-akinc Aug 21, 2023
f9c98e3
Document theme provider attributes
m-akinc Aug 28, 2023
fbf8b88
Lint fix
m-akinc Aug 28, 2023
f158b48
Update packages/nimble-components/src/theme-provider/tests/theme-prov…
m-akinc Aug 31, 2023
e387a9b
Feedback
m-akinc Aug 31, 2023
1526ac0
Replace suppression with non-null assertion
m-akinc Aug 31, 2023
69e4e9e
Apply suggestions from code review
m-akinc Sep 1, 2023
ad4d26b
Feedback
m-akinc Sep 1, 2023
8c7ffe8
Merge branch 'main' into users/makinc/datetime-column-locale
m-akinc Sep 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
"type": "minor",
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
"comment": "Add locale support to theme provider and use in date-text column",
"packageName": "@ni/nimble-components",
"email": "[email protected]",
"dependentChangeType": "patch"
}
3 changes: 2 additions & 1 deletion packages/nimble-components/src/table-column/base/types.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
19 changes: 17 additions & 2 deletions packages/nimble-components/src/table-column/date-text/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -106,11 +110,22 @@ export class TableColumnDateText extends TableColumnTextBase {
@attr({ attribute: 'custom-hour-cycle' })
public customHourCycle: HourCycleFormat;

private readonly langSubscriber: DesignTokenSubscriber<typeof lang> = {
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);
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
}

public override get validity(): TableColumnValidity {
return this.validator.getValidity();
}
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 { lang, themeProviderTag } from '../../../theme-provider';

interface SimpleTableRecord extends TableRecord {
field?: number | null;
Expand All @@ -15,6 +16,8 @@ interface SimpleTableRecord extends TableRecord {

// prettier-ignore
async function setup(): Promise<Fixture<Table<SimpleTableRecord>>> {
const themeProvider = document.createElement(themeProviderTag);
themeProvider.lang = 'en-US';
return fixture<Table<SimpleTableRecord>>(
html`<nimble-table style="width: 700px">
<${tableColumnDateTextTag} field-name="field" group-index="0">
Expand All @@ -23,7 +26,10 @@ async function setup(): Promise<Fixture<Table<SimpleTableRecord>>> {
<${tableColumnDateTextTag} field-name="anotherField">
Squeeze Column 1
</${tableColumnDateTextTag}>
</nimble-table>`
</nimble-table>`,
{
parent: themeProvider
}
);
}

Expand Down Expand Up @@ -269,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() }
Expand Down Expand Up @@ -470,7 +491,7 @@ describe('TableColumnDateText', () => {
expect(column.validity.invalidCustomOptionsCombination).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() }
]);
Expand All @@ -482,7 +503,7 @@ describe('TableColumnDateText', () => {
expect(column.validity.invalidCustomOptionsCombination).toBeTrue();
});

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() }
]);
Expand Down
5 changes: 1 addition & 4 deletions packages/nimble-components/src/table/types.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -51,10 +52,6 @@ export type TableNumberField<FieldName extends TableFieldName> = {
[name in FieldName]: TableNumberFieldValue;
};

export interface ValidityObject {
[key: string]: boolean;
}

export interface TableValidity extends ValidityObject {
readonly duplicateRecordId: boolean;
readonly missingRecordId: boolean;
Expand Down
66 changes: 56 additions & 10 deletions packages/nimble-components/src/theme-provider/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,34 @@ 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 type { ValidityObject } from '../utilities/models/validator';

declare global {
interface HTMLElementTagNameMap {
'nimble-theme-provider': ThemeProvider;
}
}

function isInvalidLang(value: string): boolean {
try {
// eslint-disable-next-line no-new
new Intl.DateTimeFormat(value);
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
return false;
} catch (e) {
return true;
}
}

function getSystemLocale(): string {
return new Intl.DateTimeFormat(undefined).resolvedOptions().locale;
}

export const lang = DesignToken.create<string>({
name: 'lang',
cssCustomPropertyName: null
}).withDefault((): string => (isInvalidLang(pageLocale.lang) ? getSystemLocale() : pageLocale.lang));

// Not represented as a CSS Custom Property, instead available
// as an attribute of theme provider.
export const direction = DesignToken.create<Direction>({
Expand All @@ -33,19 +54,44 @@ export const theme = DesignToken.create<Theme>({
* @internal
*/
export class ThemeProvider extends FoundationElement {
@attr({
attribute: 'direction'
})
@attr()
public override lang = '';
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
m-akinc marked this conversation as resolved.
Show resolved Hide resolved

@attr()
public direction: Direction = Direction.ltr;
m-akinc marked this conversation as resolved.
Show resolved Hide resolved

@attr({
attribute: 'theme'
})
@attr()
public theme: Theme = Theme.light;

public get validity(): ValidityObject {
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
return {
invalidLang: this.langIsInvalid
};
}

private langIsInvalid = false;

public langChanged(
_prev: string | undefined | null,
next: string | undefined | null
): void {
this.langIsInvalid = false;
if (
next !== undefined
&& next !== null
&& next !== ''
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
// eslint-disable-next-line no-cond-assign
&& !(this.langIsInvalid = isInvalidLang(next))
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
) {
lang.setValueFor(this, next);
} else {
lang.deleteValueFor(this);
}
}

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);
Expand All @@ -55,8 +101,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);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +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 } 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(
Expand All @@ -16,6 +19,79 @@ describe('Theme Provider', () => {
);
});

describe('lang token', () => {
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
async function setup(
langValue = 'foo'
): Promise<Fixture<ThemeProvider>> {
return fixture<ThemeProvider>(
html`<${themeProviderTag} lang="${langValue}">
</${themeProviderTag}>`
);
}

let element: ThemeProvider;
let connect: () => Promise<void>;
let disconnect: () => Promise<void>;

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());
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
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 () => {
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
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', () => {
const tokenEntries = designTokenPropertyNames.map(
(name: DesignTokenPropertyName) => ({
Expand Down
27 changes: 27 additions & 0 deletions packages/nimble-components/src/utilities/models/page-locale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { observable } from '@microsoft/fast-element';

/**
* Observable class to subscribe to changes in the page's lang attribute
*/
class PageLocale {
m-akinc marked this conversation as resolved.
Show resolved Hide resolved
@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();
Original file line number Diff line number Diff line change
@@ -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
*/
Expand Down