Skip to content

Commit

Permalink
Add locale support to theme provider and use in date-text column (#1410)
Browse files Browse the repository at this point in the history
## 🤨 Rationale

Fixes #1293
Part of #1014 
Adding design token ("lang") for locale and honoring that token's value
when formatting dates in the date-text column.

## 👩‍💻 Implementation

- introduced "lang" design token with dynamic default that uses the
page's `lang` property, if valid, otherwise uses "en-US"
- new `DocumentElementLang` singleton watches page element's `lang`
property
- added `lang` property to theme provider which sets design token value
- using design token value when creating formatter used by date-text
column
- added new validation flag to theme provider that is set when the
locale is malformed
- setting 'en-US' locale for all date-text unit tests (which they were
already assuming)
- added unit tests

## 🧪 Testing

Unit tests pass (even when system locale is something other than 'en').

## ✅ Checklist

- [x] I have updated the project documentation to reflect my changes or
determined no changes are needed.

---------

Co-authored-by: mollykreis <[email protected]>
Co-authored-by: Jesse Attas <[email protected]>
  • Loading branch information
3 people authored Sep 1, 2023
1 parent c8119e8 commit 5e462b1
Show file tree
Hide file tree
Showing 12 changed files with 396 additions and 25 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"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
20 changes: 18 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 {
WeekdayFormat
} from './types';
import { TableColumnDateTextValidator } from './models/table-column-date-text-validator';
import { lang } from '../../theme-provider';
import { optionalBooleanConverter } from '../../utilities/models/converter';

export type TableColumnDateTextCellRecord = TableNumberField<'value'>;
Expand Down Expand Up @@ -110,11 +114,23 @@ 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 {
super.disconnectedCallback();
lang.unsubscribe(this.langSubscriber, this);
}

public override get validity(): TableColumnValidity {
return this.validator.getValidity();
}
Expand Down Expand Up @@ -235,7 +251,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 @@ -23,6 +24,8 @@ describe('TableColumnDateText', () => {

// 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 @@ -31,7 +34,10 @@ describe('TableColumnDateText', () => {
<${tableColumnDateTextTag} field-name="anotherField">
Squeeze Column 1
</${tableColumnDateTextTag}>
</nimble-table>`
</nimble-table>`,
{
parent: themeProvider
}
);
}

Expand Down Expand Up @@ -246,6 +252,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 @@ -465,7 +486,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 @@ -477,7 +498,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
Original file line number Diff line number Diff line change
Expand Up @@ -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`](?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:
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
74 changes: 62 additions & 12 deletions packages/nimble-components/src/theme-provider/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,33 @@ import { Direction } from '@microsoft/fast-web-utilities';
import { template } from './template';
import { styles } from './styles';
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 isValidLang(value: string): boolean {
try {
// 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<string>({
name: 'lang',
cssCustomPropertyName: null
}).withDefault((): string => (isValidLang(documentElementLang.lang) ? documentElementLang.lang : 'en-US'));

// 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 +53,49 @@ export const theme = DesignToken.create<Theme>({
* @internal
*/
export class ThemeProvider extends FoundationElement {
@attr({
attribute: 'direction'
})
public direction: Direction = Direction.ltr;

@attr({
attribute: 'theme'
})
@attr()
public override lang!: string;

@attr()
public direction?: Direction;

@attr()
public theme: Theme = Theme.light;

public get validity(): ValidityObject {
return {
invalidLang: this.langIsInvalid
};
}

private langIsInvalid = false;

public checkValidity(): boolean {
return !this.langIsInvalid;
}

public langChanged(
_prev: string | undefined | null,
next: string | undefined | null
): void {
if (next === null || next === undefined) {
lang.deleteValueFor(this);
this.langIsInvalid = false;
return;
}

if (isValidLang(next)) {
lang.setValueFor(this, next);
this.langIsInvalid = false;
} else {
lang.deleteValueFor(this);
this.langIsInvalid = true;
}
}

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 +105,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
@@ -0,0 +1,10 @@
import { Controls, DocsStory, Meta, Title } from '@storybook/blocks';
import { themeProvider } from './theme-provider.stories';

<Meta title="Tokens/Theme Provider" />
<Title>Theme Provider</Title>

The theme provider element allows configuring certain token values for the contained HTML tree.

<DocsStory of={themeProvider} expanded={false} />
<Controls of={themeProvider} sourceState="none" />
Loading

0 comments on commit 5e462b1

Please sign in to comment.