From 4df8e18597788c157a8037b26a1c5d25e51e10fb Mon Sep 17 00:00:00 2001 From: Malcolm Smith <20709258+msmithNI@users.noreply.github.com> Date: Mon, 26 Jun 2023 15:52:46 -0500 Subject: [PATCH 01/18] Label changes --- packages/nimble-components/CONTRIBUTING.md | 25 ++++ packages/nimble-components/README.md | 31 +++- .../nimble-components/src/all-components.ts | 2 + .../nimble-components/src/banner/template.ts | 3 +- .../src/banner/tests/banner.stories.ts | 19 ++- .../src/label-provider/base/index.ts | 70 +++++++++ .../label-provider/base/label-name-utils.ts | 9 ++ .../base/label-provider-stories-utils.ts | 113 +++++++++++++++ .../base/label-user-stories-utils.ts | 33 +++++ .../src/label-provider/core/index.ts | 79 +++++++++++ .../core/label-token-defaults.ts | 9 ++ .../src/label-provider/core/label-tokens.ts | 17 +++ .../core/tests/label-provider-core.spec.ts | 127 +++++++++++++++++ .../core/tests/label-provider-core.stories.ts | 20 +++ .../src/label-provider/table/index.ts | 104 ++++++++++++++ .../table/label-token-defaults.ts | 11 ++ .../src/label-provider/table/label-tokens.ts | 27 ++++ .../src/label-provider/table/name-utils.ts | 12 ++ .../table/tests/label-provider-table.spec.ts | 134 ++++++++++++++++++ .../tests/label-provider-table.stories.ts | 21 +++ .../src/number-field/index.ts | 12 +- .../tests/number-field.stories.ts | 17 ++- .../table/components/group-row/template.ts | 5 + .../src/table/components/header/template.ts | 5 +- .../nimble-components/src/table/template.ts | 2 + .../src/table/tests/table.stories.ts | 8 +- 26 files changed, 899 insertions(+), 16 deletions(-) create mode 100644 packages/nimble-components/src/label-provider/base/index.ts create mode 100644 packages/nimble-components/src/label-provider/base/label-name-utils.ts create mode 100644 packages/nimble-components/src/label-provider/base/label-provider-stories-utils.ts create mode 100644 packages/nimble-components/src/label-provider/base/label-user-stories-utils.ts create mode 100644 packages/nimble-components/src/label-provider/core/index.ts create mode 100644 packages/nimble-components/src/label-provider/core/label-token-defaults.ts create mode 100644 packages/nimble-components/src/label-provider/core/label-tokens.ts create mode 100644 packages/nimble-components/src/label-provider/core/tests/label-provider-core.spec.ts create mode 100644 packages/nimble-components/src/label-provider/core/tests/label-provider-core.stories.ts create mode 100644 packages/nimble-components/src/label-provider/table/index.ts create mode 100644 packages/nimble-components/src/label-provider/table/label-token-defaults.ts create mode 100644 packages/nimble-components/src/label-provider/table/label-tokens.ts create mode 100644 packages/nimble-components/src/label-provider/table/name-utils.ts create mode 100644 packages/nimble-components/src/label-provider/table/tests/label-provider-table.spec.ts create mode 100644 packages/nimble-components/src/label-provider/table/tests/label-provider-table.stories.ts diff --git a/packages/nimble-components/CONTRIBUTING.md b/packages/nimble-components/CONTRIBUTING.md index 5f1463d73e..bd92345db1 100644 --- a/packages/nimble-components/CONTRIBUTING.md +++ b/packages/nimble-components/CONTRIBUTING.md @@ -428,6 +428,31 @@ Nimble includes three NI-brand aligned themes (i.e. `light`, `dark`, & `color`). When creating a new component, create a `*-matrix.stories.ts` Storybook file to confirm that the component reflects the design intent across all themes and states. +## Localization + +Nimble components may need to use some strings/labels (such as the label for a close button in a component's template, or menu items in a component-provided menu). Nimble exposes these localizable labels as design tokens, to support both localization and the ability for clients to override the strings. + +Nimble provides English strings as the token defaults, and provides `nimble-label-provider-*` elements with APIs for overriding those values. +There are currently 2 label providers: + +- `nimble-label-provider-core`: Used for labels for all components besides the table +- `nimble-label-provider-table`: Used for labels for the table (and table sub-components / column types) + +To add new labels to a label provider, follow the existing patterns (create a `DesignToken`, an `@attr`-backed property, a `propertyChanged()` function that updates the token value, and add it to the `labelTokens` collection). + +The expected format for label token names is: + +- element/type(s) to which the token applies, e.g. `number-field` or `table` + - This may not be an exact element name, if this label applies to multiple elements or will be used in multiple contexts +- component part/category (optional), e.g. `column-header` +- specific functionality or sub-part, e.g. `decrement` +- the suffix `label` (will be omitted from the label-provider properties/attributes) + +Components using localized labels should document them in Storybook. To add a "Localizable Labels" section: + +- Their story `Args` should extend `LabelUserArgs` +- Call `addLabelUseMetadata()` and pass their declared metadata object, the applicable label provider tag, and the label tokens that they're using + ## Component naming Component custom element names are specified in `index.ts` when registering the element. Use the following structure when naming components. diff --git a/packages/nimble-components/README.md b/packages/nimble-components/README.md index 937c3db223..b9a1ab73b2 100644 --- a/packages/nimble-components/README.md +++ b/packages/nimble-components/README.md @@ -23,8 +23,8 @@ If you have an existing application that incorporates a module bundler like [Web 1. Install the package from [the public NPM registry](https://www.npmjs.com/package/@ni/nimble-components) by running `npm install @ni/nimble-components`. 2. Import the component you want to use from the file you want to use it in. For example: `import '@ni/nimble-components/dist/esm/icons/succeeded';` -3. Add the HTML for the component to your page. You can see sample code for each component in the [Nimble Storybook](https://ni.github.io/nimble/storybook/) by going to the **Docs** tab for the component and clicking **Show code**. For example: ``. -4. Nimble components are [standard web components (custom elements)](https://developer.mozilla.org/en-US/docs/Web/Web_Components) so you can configure them via normal DOM APIs like attributes, properties, events, and methods. The [Storybook documentation](https://ni.github.io/nimble/storybook/) for each component describes its custom API. +3. Add the HTML for the component to your page. You can see sample code for each component in the [Nimble Storybook](https://nimble.ni.dev/storybook/) by going to the **Docs** page for the component and clicking **Show code**. For example: ``. +4. Nimble components are [standard web components (custom elements)](https://developer.mozilla.org/en-US/docs/Web/Web_Components) so you can configure them via normal DOM APIs like attributes, properties, events, and methods. The [Storybook documentation](https://nimble.ni.dev/storybook/) for each component describes its custom API. ### Prototyping in a static webpage @@ -96,9 +96,34 @@ The theming system is composed of: The goal of the Nimble design system is to provide a consistent style for applications. If you find that Nimble does not expose colors, fonts, sizes, etc. that you need in an application get in touch with the Nimble squad. +## Localization + +Some Nimble components use strings/labels that need to be localized, if the consuming application supports localization. Nimble exposes these localizable labels as design tokens, to support both localization and the ability for clients to override the strings. + +Nimble provides English strings as the token defaults, and provides `nimble-label-provider-*` elements with APIs for overriding those values. +There are currently 2 label providers: + +- `nimble-label-provider-core`: Used for labels for all components besides the table +- `nimble-label-provider-table`: Used for labels for the table (and table sub-components / column types) + +If a client is localized, it should: + +- Add the `label-provider` element(s) as children of their root theme provider: + ```html + + + + + + + + + ``` +- For each label token on the label provider API, localize the English string, and set the corresponding HTML attribute or JS property on the label provider to the localized values. A list of all label tokens for each label provider (and their corresponding attribute/property names and English strings) can be found in the [Tokens/Label Providers section of Storybook](http://nimble.ni.dev/storybook/?path=/docs/tokens-label-providers--docs). + ## Accessibility -For accessibility information related to nimble components, see [ACCESSIBILITY.md](docs/ACCESSIBILITY.md). +For accessibility information related to nimble components, see [accessibility.md](/packages/nimble-components/docs/accessibility.md). ## Contributing diff --git a/packages/nimble-components/src/all-components.ts b/packages/nimble-components/src/all-components.ts index 6abd2196f2..91e0b5bb4d 100644 --- a/packages/nimble-components/src/all-components.ts +++ b/packages/nimble-components/src/all-components.ts @@ -21,6 +21,8 @@ import './combobox'; import './dialog'; import './drawer'; import './icons/all-icons'; +import './label-provider/core'; +import './label-provider/table'; import './list-option'; import './menu'; import './menu-button'; diff --git a/packages/nimble-components/src/banner/template.ts b/packages/nimble-components/src/banner/template.ts index 1047ec2d10..bb8ae99d02 100644 --- a/packages/nimble-components/src/banner/template.ts +++ b/packages/nimble-components/src/banner/template.ts @@ -6,6 +6,7 @@ import { iconInfoTag } from '../icons/info'; import { iconTriangleFilledTag } from '../icons/triangle-filled'; import { iconXmarkTag } from '../icons/xmark'; import { BannerSeverity } from './types'; +import { alertDismissLabel } from '../label-provider/core/label-tokens'; // prettier-ignore export const template = html` @@ -53,7 +54,7 @@ export const template = html` ${when(x => !x.preventDismiss, html` <${buttonTag} appearance="ghost" content-hidden @click="${x => x.dismissBanner()}"> <${iconXmarkTag} slot="start"> - ${x => x.dismissButtonLabel ?? 'Close'} + ${x => x.dismissButtonLabel ?? alertDismissLabel.getValueFor(x)} `)} diff --git a/packages/nimble-components/src/banner/tests/banner.stories.ts b/packages/nimble-components/src/banner/tests/banner.stories.ts index 5e2dba711c..ab6222421d 100644 --- a/packages/nimble-components/src/banner/tests/banner.stories.ts +++ b/packages/nimble-components/src/banner/tests/banner.stories.ts @@ -8,6 +8,12 @@ import { bannerTag } from '..'; import { iconKeyTag } from '../../icons/key'; import { buttonTag } from '../../button'; import { anchorTag } from '../../anchor'; +import { labelProviderCoreTag } from '../../label-provider/core'; +import { + LabelUserArgs, + addLabelUseMetadata +} from '../../label-provider/base/label-user-stories-utils'; +import { alertDismissLabel } from '../../label-provider/core/label-tokens'; // eslint-disable-next-line @typescript-eslint/naming-convention const ActionType = { @@ -20,7 +26,7 @@ const ActionType = { // eslint-disable-next-line @typescript-eslint/no-redeclare type ActionType = (typeof ActionType)[keyof typeof ActionType]; -interface BannerArgs { +interface BannerArgs extends LabelUserArgs { open: boolean; title: string; text: string; @@ -55,6 +61,7 @@ const metadata: Meta = { } } }; +addLabelUseMetadata(metadata, labelProviderCoreTag, alertDismissLabel); export default metadata; @@ -121,11 +128,13 @@ export const _banner: StoryObj = { dismissButtonLabel: { name: 'dismiss-button-label', description: - 'Set to a localized label (e.g. `"Close"`) for the dismiss button. This provides an accessible name for assistive technologies.' + 'Set to a localized label (e.g. `"Close"`) for the dismiss button. This provides an accessible name for assistive technologies.', + control: { type: 'none' } }, toggle: { description: - 'Event emitted by the banner when the `open` state changes. The event details include the booleans `oldState` and `newState`.' + 'Event emitted by the banner when the `open` state changes. The event details include the booleans `oldState` and `newState`.', + control: { type: 'none' } } }, args: { @@ -135,6 +144,8 @@ export const _banner: StoryObj = { severity: BannerSeverity.error, action: 'none', preventDismiss: false, - titleHidden: false + titleHidden: false, + dismissButtonLabel: undefined, + toggle: undefined } }; diff --git a/packages/nimble-components/src/label-provider/base/index.ts b/packages/nimble-components/src/label-provider/base/index.ts new file mode 100644 index 0000000000..345a274d0d --- /dev/null +++ b/packages/nimble-components/src/label-provider/base/index.ts @@ -0,0 +1,70 @@ +import { DesignToken, FoundationElement } from '@microsoft/fast-foundation'; +import { themeProviderTag } from '../../theme-provider'; + +/** + * Base class for label providers + */ +export abstract class LabelProviderBase extends FoundationElement { + /** + * @internal + */ + public abstract readonly labelTokens: { + [labelName: string]: DesignToken + }; + + private readonly deferredTokenUpdates: Map< + DesignToken, + string | undefined + > = new Map(); + + private themeProvider?: HTMLElement; + + public override connectedCallback(): void { + super.connectedCallback(); + this.updateThemeProvider(); + } + + public override disconnectedCallback(): void { + this.themeProvider = undefined; + } + + /** + * Called from the [tokenPropertyName]Changed() methods to update the backing design token + * @param token DesignToken for the label + * @param newValue New value for the label + */ + protected handleTokenChanged( + token: DesignToken, + newValue: string | undefined + ): void { + if (this.themeProvider) { + this.updateTokenValue(this.themeProvider, token, newValue); + } else { + // For Blazor this method is called before this element is in the DOM. So cache the token value sets, and + // apply them after we're connected + this.deferredTokenUpdates.set(token, newValue); + } + } + + private updateTokenValue( + target: HTMLElement, + token: DesignToken, + newValue: string | undefined + ): void { + if (newValue !== undefined && newValue !== null) { + token.setValueFor(target, newValue); + } else { + token.deleteValueFor(target); + } + } + + private updateThemeProvider(): void { + this.themeProvider = this.closest(themeProviderTag) ?? undefined; + if (this.themeProvider) { + for (const entry of this.deferredTokenUpdates.entries()) { + this.updateTokenValue(this.themeProvider, entry[0], entry[1]); + } + this.deferredTokenUpdates.clear(); + } + } +} diff --git a/packages/nimble-components/src/label-provider/base/label-name-utils.ts b/packages/nimble-components/src/label-provider/base/label-name-utils.ts new file mode 100644 index 0000000000..420a6c35f0 --- /dev/null +++ b/packages/nimble-components/src/label-provider/base/label-name-utils.ts @@ -0,0 +1,9 @@ +import { spinalCase } from '@microsoft/fast-web-utilities'; + +export function getPropertyName(jsKey: string): string { + return jsKey.replace(/Label$/, ''); +} + +export function getAttributeName(jsKey: string): string { + return spinalCase(getPropertyName(jsKey)); +} diff --git a/packages/nimble-components/src/label-provider/base/label-provider-stories-utils.ts b/packages/nimble-components/src/label-provider/base/label-provider-stories-utils.ts new file mode 100644 index 0000000000..39b0db8326 --- /dev/null +++ b/packages/nimble-components/src/label-provider/base/label-provider-stories-utils.ts @@ -0,0 +1,113 @@ +import { ViewTemplate, html, ref, repeat } from '@microsoft/fast-element'; +import type { Meta } from '@storybook/html'; +import type { DesignToken } from '@microsoft/fast-foundation'; +import { createUserSelectedThemeStory } from '../../utilities/tests/storybook'; +import type { LabelProviderBase } from '.'; +import { + bodyFont, + groupHeaderFont, + groupHeaderFontColor, + groupHeaderTextTransform +} from '../../theme-provider/design-tokens'; +import { getAttributeName, getPropertyName } from './label-name-utils'; + +export interface LabelProviderArgs { + labelProviderTag: string; + labelProviderRef: LabelProviderBase; + removeNamePrefix(tokenName: string): string; +} + +const overviewText = `Some Nimble components use strings/labels that need to be localized, if the consuming application +supports localization. Nimble exposes these localizable labels as design tokens, to support both localization and the +ability for clients to override the strings in specific contexts. + +Nimble provides English strings as the token defaults, and provides \`nimble-label-provider-*\` elements with APIs for +overriding those values with localized versions. + +See the nimble-components readme and +contributing docs for more information.`; + +const createTemplate = ( + labelProviderTag: string +): ViewTemplate => html` +<${labelProviderTag} ${ref('labelProviderRef')}> +

${x => x.labelProviderTag}

+
+ + + + + + + + + ${repeat( + x => Object.entries(x.labelProviderRef.labelTokens), + html<[string, DesignToken], LabelProviderArgs>` + + + + + + + ` + )} + +
Token nameHTML attribute nameJS property nameDefault value (English)
${x => x[0]} + ${(x, c) => getAttributeName(c.parent.removeNamePrefix(x[0]))} + + ${(x, c) => getPropertyName(c.parent.removeNamePrefix(x[0]))} + "${x => x[1].getValueFor(document.body)}"
+`; + +export const labelProviderMetadata: Meta = { + parameters: { + docs: { + description: { + component: overviewText + } + }, + chromatic: { disableSnapshot: true } + }, + // prettier-ignore + render: createUserSelectedThemeStory(html` +
+ + ${x => createTemplate(x.labelProviderTag)} +
+ `), + argTypes: { + removeNamePrefix: { + table: { + disable: true + } + }, + labelProviderTag: { + table: { + disable: true + } + }, + labelProviderRef: { + table: { + disable: true + } + } + }, + args: { + labelProviderTag: undefined, + removeNamePrefix: jsTokenName => jsTokenName + } +}; diff --git a/packages/nimble-components/src/label-provider/base/label-user-stories-utils.ts b/packages/nimble-components/src/label-provider/base/label-user-stories-utils.ts new file mode 100644 index 0000000000..87ddd4726b --- /dev/null +++ b/packages/nimble-components/src/label-provider/base/label-user-stories-utils.ts @@ -0,0 +1,33 @@ +import type { Meta } from '@storybook/html'; +import type { DesignToken } from '@microsoft/fast-foundation'; + +export interface LabelUserArgs { + usedLabels: null; +} + +export function addLabelUseMetadata( + metadata: Meta, + labelProviderTag: string, + ...labelTokens: DesignToken[] +): void { + let tokenContent: string; + if (labelTokens.length === 0) { + tokenContent = '- (All tokens on this provider are used)'; + } else { + tokenContent = labelTokens + .map(token => `- \`${token.name}\``) + .join('\n'); + } + if (metadata.argTypes === undefined) { + metadata.argTypes = {}; + } + metadata.argTypes.usedLabels = { + name: 'Localizable labels', + description: `Label Provider:\`${labelProviderTag}\` +${tokenContent} + +See the "Tokens/Label Providers" docs page for more information. +`, + control: { type: 'none' } + }; +} diff --git a/packages/nimble-components/src/label-provider/core/index.ts b/packages/nimble-components/src/label-provider/core/index.ts new file mode 100644 index 0000000000..c1005a9cf9 --- /dev/null +++ b/packages/nimble-components/src/label-provider/core/index.ts @@ -0,0 +1,79 @@ +import { attr } from '@microsoft/fast-element'; +import { DesignSystem, DesignToken } from '@microsoft/fast-foundation'; +import { LabelProviderBase } from '../base'; +import type * as TokensNamespace from './label-tokens'; +import { + alertDismissLabel, + numberFieldDecrementLabel, + numberFieldIncrementLabel +} from './label-tokens'; +import { coreLabelDefaults } from './label-token-defaults'; + +type TokenName = keyof typeof TokensNamespace; + +declare global { + interface HTMLElementTagNameMap { + 'nimble-label-provider-core': LabelProviderCore; + } +} + +/** + * Core label provider for Nimble + */ +export class LabelProviderCore extends LabelProviderBase { + public override readonly labelTokens: { + readonly [key in TokenName]: DesignToken; + } = { + alertDismissLabel, + numberFieldIncrementLabel, + numberFieldDecrementLabel + }; + + @attr({ + attribute: 'alert-dismiss', + mode: 'fromView' + }) + public alertDismiss: string = coreLabelDefaults.alertDismissLabel; + + @attr({ + attribute: 'number-field-decrement', + mode: 'fromView' + }) + public numberFieldDecrement: string = coreLabelDefaults.numberFieldDecrementLabel; + + @attr({ + attribute: 'number-field-increment', + mode: 'fromView' + }) + public numberFieldIncrement: string = coreLabelDefaults.numberFieldIncrementLabel; + + protected alertDismissChanged( + _prev: string | undefined, + next: string | undefined + ): void { + this.handleTokenChanged(alertDismissLabel, next); + } + + protected numberFieldDecrementChanged( + _prev: string | undefined, + next: string | undefined + ): void { + this.handleTokenChanged(numberFieldDecrementLabel, next); + } + + protected numberFieldIncrementChanged( + _prev: string | undefined, + next: string | undefined + ): void { + this.handleTokenChanged(numberFieldIncrementLabel, next); + } +} + +const nimbleLabelProviderCore = LabelProviderCore.compose({ + baseName: 'label-provider-core' +}); + +DesignSystem.getOrCreate() + .withPrefix('nimble') + .register(nimbleLabelProviderCore()); +export const labelProviderCoreTag = DesignSystem.tagFor(LabelProviderCore); diff --git a/packages/nimble-components/src/label-provider/core/label-token-defaults.ts b/packages/nimble-components/src/label-provider/core/label-token-defaults.ts new file mode 100644 index 0000000000..82ceeaf97f --- /dev/null +++ b/packages/nimble-components/src/label-provider/core/label-token-defaults.ts @@ -0,0 +1,9 @@ +import type * as TokensNamespace from './label-tokens'; + +type TokenName = keyof typeof TokensNamespace; + +export const coreLabelDefaults: { readonly [key in TokenName]: string } = { + alertDismissLabel: 'Close', + numberFieldIncrementLabel: 'Increment', + numberFieldDecrementLabel: 'Decrement' +}; diff --git a/packages/nimble-components/src/label-provider/core/label-tokens.ts b/packages/nimble-components/src/label-provider/core/label-tokens.ts new file mode 100644 index 0000000000..988fb566ea --- /dev/null +++ b/packages/nimble-components/src/label-provider/core/label-tokens.ts @@ -0,0 +1,17 @@ +import { DesignToken } from '@microsoft/fast-foundation'; +import { coreLabelDefaults } from './label-token-defaults'; + +export const alertDismissLabel = DesignToken.create({ + name: 'alert-dismiss-label', + cssCustomPropertyName: null +}).withDefault(coreLabelDefaults.alertDismissLabel); + +export const numberFieldDecrementLabel = DesignToken.create({ + name: 'number-field-decrement-label', + cssCustomPropertyName: null +}).withDefault(coreLabelDefaults.numberFieldDecrementLabel); + +export const numberFieldIncrementLabel = DesignToken.create({ + name: 'number-field-increment-label', + cssCustomPropertyName: null +}).withDefault(coreLabelDefaults.numberFieldIncrementLabel); diff --git a/packages/nimble-components/src/label-provider/core/tests/label-provider-core.spec.ts b/packages/nimble-components/src/label-provider/core/tests/label-provider-core.spec.ts new file mode 100644 index 0000000000..a3cb9203ed --- /dev/null +++ b/packages/nimble-components/src/label-provider/core/tests/label-provider-core.spec.ts @@ -0,0 +1,127 @@ +import { spinalCase } from '@microsoft/fast-web-utilities'; +import { html } from '@microsoft/fast-element'; +import * as labelTokensNamespace from '../label-tokens'; +import { LabelProviderCore, labelProviderCoreTag } from '..'; +import { getSpecTypeByNamedList } from '../../../utilities/tests/parameterized'; +import { getAttributeName, getPropertyName } from '../../base/label-name-utils'; +import { ThemeProvider, themeProviderTag } from '../../../theme-provider'; +import { Fixture, fixture } from '../../../utilities/tests/fixture'; + +type DesignTokenPropertyName = keyof typeof labelTokensNamespace; +const designTokenPropertyNames = Object.keys( + labelTokensNamespace +) as DesignTokenPropertyName[]; + +async function setup(): Promise> { + return fixture(html` + <${themeProviderTag}> + <${labelProviderCoreTag}> + + `); +} + +describe('Label Provider Core', () => { + let element: LabelProviderCore; + let themeProvider: ThemeProvider; + let connect: () => Promise; + let disconnect: () => Promise; + + beforeEach(async () => { + ({ element: themeProvider, connect, disconnect } = await setup()); + element = themeProvider.querySelector(labelProviderCoreTag)!; + await connect(); + }); + + afterEach(async () => { + await disconnect(); + }); + + it('can construct an element instance', () => { + expect( + document.createElement('nimble-label-provider-core') + ).toBeInstanceOf(LabelProviderCore); + }); + + describe('token JS key should match DesignToken.name', () => { + const tokenEntries = designTokenPropertyNames.map( + (name: DesignTokenPropertyName) => ({ + name, + labelToken: labelTokensNamespace[name] + }) + ); + + for (const tokenEntry of tokenEntries) { + const focused: DesignTokenPropertyName[] = []; + const disabled: DesignTokenPropertyName[] = []; + const specType = getSpecTypeByNamedList( + tokenEntry, + focused, + disabled + ); + specType(`for token name ${tokenEntry.name}`, () => { + const convertedTokenValue = spinalCase(tokenEntry.name); + expect(tokenEntry.labelToken.name).toBe(convertedTokenValue); + }); + } + }); + + describe('token JS key should match a LabelProvider property/attribute', () => { + const tokenEntries = designTokenPropertyNames.map( + (name: DesignTokenPropertyName) => ({ + name, + labelToken: labelTokensNamespace[name] + }) + ); + + for (const tokenEntry of tokenEntries) { + const focused: DesignTokenPropertyName[] = []; + const disabled: DesignTokenPropertyName[] = []; + const specType = getSpecTypeByNamedList( + tokenEntry, + focused, + disabled + ); + // eslint-disable-next-line @typescript-eslint/no-loop-func + specType(`for token name ${tokenEntry.name}`, () => { + const expectedPropertyName = getPropertyName(tokenEntry.name); + const expectedAttributeName = getAttributeName(tokenEntry.name); + const attributeDefinition = element.$fastController.definition.attributes.find( + a => a.name === expectedPropertyName + ); + expect(attributeDefinition).toBeDefined(); + expect(expectedAttributeName).toEqual( + attributeDefinition!.attribute + ); + }); + } + }); + + describe('token value is updated after setting corresponding LabelProvider attribute', () => { + const tokenEntries = designTokenPropertyNames.map( + (name: DesignTokenPropertyName) => ({ + name, + labelToken: labelTokensNamespace[name] + }) + ); + + for (const tokenEntry of tokenEntries) { + const focused: DesignTokenPropertyName[] = []; + const disabled: DesignTokenPropertyName[] = []; + const specType = getSpecTypeByNamedList( + tokenEntry, + focused, + disabled + ); + // eslint-disable-next-line @typescript-eslint/no-loop-func + specType(`for token name ${tokenEntry.name}`, () => { + const attributeName = getAttributeName(tokenEntry.name); + const updatedValue = `NewString-${tokenEntry.name}`; + element.setAttribute(attributeName, updatedValue); + + expect(tokenEntry.labelToken.getValueFor(themeProvider)).toBe( + updatedValue + ); + }); + } + }); +}); diff --git a/packages/nimble-components/src/label-provider/core/tests/label-provider-core.stories.ts b/packages/nimble-components/src/label-provider/core/tests/label-provider-core.stories.ts new file mode 100644 index 0000000000..6954d31b34 --- /dev/null +++ b/packages/nimble-components/src/label-provider/core/tests/label-provider-core.stories.ts @@ -0,0 +1,20 @@ +import type { StoryObj } from '@storybook/html'; +import { + LabelProviderArgs, + labelProviderMetadata +} from '../../base/label-provider-stories-utils'; +import { labelProviderCoreTag } from '..'; + +const metadata = { + ...labelProviderMetadata, + title: 'Tokens/Label Providers', + tags: ['autodocs'] +}; + +export default metadata; + +export const coreLabelProvider: StoryObj = { + args: { + labelProviderTag: labelProviderCoreTag + } +}; diff --git a/packages/nimble-components/src/label-provider/table/index.ts b/packages/nimble-components/src/label-provider/table/index.ts new file mode 100644 index 0000000000..0f64b30cc4 --- /dev/null +++ b/packages/nimble-components/src/label-provider/table/index.ts @@ -0,0 +1,104 @@ +import { attr } from '@microsoft/fast-element'; +import { DesignSystem } from '@microsoft/fast-foundation'; +import { LabelProviderBase } from '../base'; +import { + tableCellActionMenuLabel, + tableColumnHeaderGroupedIndicatorLabel, + tableGroupCollapseLabel, + tableGroupExpandLabel, + tableGroupsCollapseAllLabel +} from './label-tokens'; +import { tableLabelDefaults } from './label-token-defaults'; + +declare global { + interface HTMLElementTagNameMap { + 'nimble-label-provider-table': LabelProviderTable; + } +} + +/** + * Label provider for the Nimble table (and its sub-components and columns) + */ +export class LabelProviderTable extends LabelProviderBase { + public override readonly labelTokens = { + tableGroupCollapseLabel, + tableGroupExpandLabel, + tableGroupsCollapseAllLabel, + tableCellActionMenuLabel, + tableColumnHeaderGroupedIndicatorLabel + }; + + @attr({ + attribute: 'group-collapse', + mode: 'fromView' + }) + public groupCollapse = tableLabelDefaults.tableGroupCollapseLabel; + + @attr({ + attribute: 'group-expand', + mode: 'fromView' + }) + public groupExpand = tableLabelDefaults.tableGroupExpandLabel; + + @attr({ + attribute: 'groups-collapse-all', + mode: 'fromView' + }) + public groupsCollapseAll = tableLabelDefaults.tableGroupsCollapseAllLabel; + + @attr({ + attribute: 'cell-action-menu', + mode: 'fromView' + }) + public cellActionMenu = tableLabelDefaults.tableCellActionMenuLabel; + + @attr({ + attribute: 'column-header-grouped-indicator', + mode: 'fromView' + }) + public columnHeaderGroupedIndicator = tableLabelDefaults.tableColumnHeaderGroupedIndicatorLabel; + + protected groupCollapseChanged( + _prev: string | undefined, + next: string | undefined + ): void { + this.handleTokenChanged(tableGroupCollapseLabel, next); + } + + protected groupExpandChanged( + _prev: string | undefined, + next: string | undefined + ): void { + this.handleTokenChanged(tableGroupExpandLabel, next); + } + + protected groupsCollapseAllChanged( + _prev: string | undefined, + next: string | undefined + ): void { + this.handleTokenChanged(tableGroupsCollapseAllLabel, next); + } + + protected cellActionMenuChanged( + _prev: string | undefined, + next: string | undefined + ): void { + this.handleTokenChanged(tableCellActionMenuLabel, next); + } + + protected columnHeaderGroupedIndicatorChanged( + _prev: string | undefined, + next: string | undefined + ): void { + this.handleTokenChanged(tableColumnHeaderGroupedIndicatorLabel, next); + } +} + +const nimbleLabelProviderTable = LabelProviderTable.compose({ + baseName: 'label-provider-table' +}); + +DesignSystem.getOrCreate() + .withPrefix('nimble') + .register(nimbleLabelProviderTable()); +export const labelProviderTableTag = DesignSystem.tagFor(LabelProviderTable); diff --git a/packages/nimble-components/src/label-provider/table/label-token-defaults.ts b/packages/nimble-components/src/label-provider/table/label-token-defaults.ts new file mode 100644 index 0000000000..b2a31d3de2 --- /dev/null +++ b/packages/nimble-components/src/label-provider/table/label-token-defaults.ts @@ -0,0 +1,11 @@ +import type * as TokensNamespace from './label-tokens'; + +type TokenName = keyof typeof TokensNamespace; + +export const tableLabelDefaults: { readonly [key in TokenName]: string } = { + tableGroupCollapseLabel: 'Collapse group', + tableGroupExpandLabel: 'Expand group', + tableGroupsCollapseAllLabel: 'Collapse all groups', + tableCellActionMenuLabel: 'Options', + tableColumnHeaderGroupedIndicatorLabel: 'Grouped' +}; diff --git a/packages/nimble-components/src/label-provider/table/label-tokens.ts b/packages/nimble-components/src/label-provider/table/label-tokens.ts new file mode 100644 index 0000000000..5d8a41394a --- /dev/null +++ b/packages/nimble-components/src/label-provider/table/label-tokens.ts @@ -0,0 +1,27 @@ +import { DesignToken } from '@microsoft/fast-foundation'; +import { tableLabelDefaults } from './label-token-defaults'; + +export const tableGroupCollapseLabel = DesignToken.create({ + name: 'table-group-collapse-label', + cssCustomPropertyName: null +}).withDefault(tableLabelDefaults.tableGroupCollapseLabel); + +export const tableGroupExpandLabel = DesignToken.create({ + name: 'table-group-expand-label', + cssCustomPropertyName: null +}).withDefault(tableLabelDefaults.tableGroupExpandLabel); + +export const tableGroupsCollapseAllLabel = DesignToken.create({ + name: 'table-groups-collapse-all-label', + cssCustomPropertyName: null +}).withDefault(tableLabelDefaults.tableGroupsCollapseAllLabel); + +export const tableCellActionMenuLabel = DesignToken.create({ + name: 'table-cell-action-menu-label', + cssCustomPropertyName: null +}).withDefault(tableLabelDefaults.tableCellActionMenuLabel); + +export const tableColumnHeaderGroupedIndicatorLabel = DesignToken.create({ + name: 'table-column-header-grouped-indicator-label', + cssCustomPropertyName: null +}).withDefault(tableLabelDefaults.tableColumnHeaderGroupedIndicatorLabel); diff --git a/packages/nimble-components/src/label-provider/table/name-utils.ts b/packages/nimble-components/src/label-provider/table/name-utils.ts new file mode 100644 index 0000000000..27fa0784ca --- /dev/null +++ b/packages/nimble-components/src/label-provider/table/name-utils.ts @@ -0,0 +1,12 @@ +/** + * Removes the table prefix and camelCases the input token name. + * (The design token name has the table prefix, but the properties on the LabelProviderTable do not, as they're already + * scoped to the table only) + */ +export function removeTablePrefixAndCamelCase(jsTokenName: string): string { + // Example: 'tableGroupExpandLabel' => 'groupExpandLabel' + return jsTokenName.replace( + /^table(\w)(\w+)/, + (_match: string, firstChar: string, restOfString: string) => `${firstChar.toLowerCase()}${restOfString}` + ); +} diff --git a/packages/nimble-components/src/label-provider/table/tests/label-provider-table.spec.ts b/packages/nimble-components/src/label-provider/table/tests/label-provider-table.spec.ts new file mode 100644 index 0000000000..c5fee4e330 --- /dev/null +++ b/packages/nimble-components/src/label-provider/table/tests/label-provider-table.spec.ts @@ -0,0 +1,134 @@ +import { spinalCase } from '@microsoft/fast-web-utilities'; +import { html } from '@microsoft/fast-element'; +import * as labelTokensNamespace from '../label-tokens'; +import { LabelProviderTable, labelProviderTableTag } from '..'; +import { getSpecTypeByNamedList } from '../../../utilities/tests/parameterized'; +import { getAttributeName, getPropertyName } from '../../base/label-name-utils'; +import { ThemeProvider, themeProviderTag } from '../../../theme-provider'; +import { Fixture, fixture } from '../../../utilities/tests/fixture'; +import { removeTablePrefixAndCamelCase } from '../name-utils'; + +type DesignTokenPropertyName = keyof typeof labelTokensNamespace; +const designTokenPropertyNames = Object.keys( + labelTokensNamespace +) as DesignTokenPropertyName[]; + +async function setup(): Promise> { + return fixture(html` + <${themeProviderTag}> + <${labelProviderTableTag}> + + `); +} + +describe('Label Provider Table', () => { + let element: LabelProviderTable; + let themeProvider: ThemeProvider; + let connect: () => Promise; + let disconnect: () => Promise; + + beforeEach(async () => { + ({ element: themeProvider, connect, disconnect } = await setup()); + element = themeProvider.querySelector(labelProviderTableTag)!; + await connect(); + }); + + afterEach(async () => { + await disconnect(); + }); + + it('can construct an element instance', () => { + expect( + document.createElement('nimble-label-provider-table') + ).toBeInstanceOf(LabelProviderTable); + }); + + describe('token JS key should match DesignToken.name', () => { + const tokenEntries = designTokenPropertyNames.map( + (name: DesignTokenPropertyName) => ({ + name, + labelToken: labelTokensNamespace[name] + }) + ); + + for (const tokenEntry of tokenEntries) { + const focused: DesignTokenPropertyName[] = []; + const disabled: DesignTokenPropertyName[] = []; + const specType = getSpecTypeByNamedList( + tokenEntry, + focused, + disabled + ); + specType(`for token name ${tokenEntry.name}`, () => { + const convertedTokenValue = spinalCase(tokenEntry.name); + expect(tokenEntry.labelToken.name).toBe(convertedTokenValue); + }); + } + }); + + describe('token JS key should match a LabelProvider property/attribute', () => { + const tokenEntries = designTokenPropertyNames.map( + (name: DesignTokenPropertyName) => ({ + name, + labelToken: labelTokensNamespace[name] + }) + ); + + for (const tokenEntry of tokenEntries) { + const focused: DesignTokenPropertyName[] = []; + const disabled: DesignTokenPropertyName[] = []; + const specType = getSpecTypeByNamedList( + tokenEntry, + focused, + disabled + ); + // eslint-disable-next-line @typescript-eslint/no-loop-func + specType(`for token name ${tokenEntry.name}`, () => { + const tokenName = removeTablePrefixAndCamelCase( + tokenEntry.name + ); + const expectedPropertyName = getPropertyName(tokenName); + const expectedAttributeName = getAttributeName(tokenName); + const attributeDefinition = element.$fastController.definition.attributes.find( + a => a.name === expectedPropertyName + ); + expect(attributeDefinition).toBeDefined(); + expect(expectedAttributeName).toEqual( + attributeDefinition!.attribute + ); + }); + } + }); + + describe('token value is updated after setting corresponding LabelProvider attribute', () => { + const tokenEntries = designTokenPropertyNames.map( + (name: DesignTokenPropertyName) => ({ + name, + labelToken: labelTokensNamespace[name] + }) + ); + + for (const tokenEntry of tokenEntries) { + const focused: DesignTokenPropertyName[] = []; + const disabled: DesignTokenPropertyName[] = []; + const specType = getSpecTypeByNamedList( + tokenEntry, + focused, + disabled + ); + // eslint-disable-next-line @typescript-eslint/no-loop-func + specType(`for token name ${tokenEntry.name}`, () => { + const tokenName = removeTablePrefixAndCamelCase( + tokenEntry.name + ); + const attributeName = getAttributeName(tokenName); + const updatedValue = `NewString-${tokenName}`; + element.setAttribute(attributeName, updatedValue); + + expect(tokenEntry.labelToken.getValueFor(themeProvider)).toBe( + updatedValue + ); + }); + } + }); +}); diff --git a/packages/nimble-components/src/label-provider/table/tests/label-provider-table.stories.ts b/packages/nimble-components/src/label-provider/table/tests/label-provider-table.stories.ts new file mode 100644 index 0000000000..95dd6eb688 --- /dev/null +++ b/packages/nimble-components/src/label-provider/table/tests/label-provider-table.stories.ts @@ -0,0 +1,21 @@ +import type { StoryObj } from '@storybook/html'; +import { + LabelProviderArgs, + labelProviderMetadata +} from '../../base/label-provider-stories-utils'; +import { labelProviderTableTag } from '..'; +import { removeTablePrefixAndCamelCase } from '../name-utils'; + +const metadata = { + ...labelProviderMetadata, + title: 'Tokens/Label Providers' +}; + +export default metadata; + +export const tableLabelProvider: StoryObj = { + args: { + labelProviderTag: labelProviderTableTag, + removeNamePrefix: removeTablePrefixAndCamelCase + } +}; diff --git a/packages/nimble-components/src/number-field/index.ts b/packages/nimble-components/src/number-field/index.ts index 9aecd6daae..063f7030c2 100644 --- a/packages/nimble-components/src/number-field/index.ts +++ b/packages/nimble-components/src/number-field/index.ts @@ -13,6 +13,10 @@ import { buttonTag } from '../button'; import { iconMinusWideTag } from '../icons/minus-wide'; import { iconAddTag } from '../icons/add'; import { iconExclamationMarkTag } from '../icons/exclamation-mark'; +import { + numberFieldDecrementLabel, + numberFieldIncrementLabel +} from '../label-provider/core/label-tokens'; declare global { interface HTMLElementTagNameMap { @@ -64,28 +68,28 @@ const nimbleNumberField = NumberField.compose({ shadowOptions: { delegatesFocus: true }, - stepDownGlyph: html` + stepDownGlyph: html` <${buttonTag} class="step-up-down-button" appearance="ghost" content-hidden tabindex="-1" > - "Decrement" + ${x => numberFieldDecrementLabel.getValueFor(x)} <${iconMinusWideTag} slot="start" > `, - stepUpGlyph: html` + stepUpGlyph: html` <${buttonTag} class="step-up-down-button" appearance="ghost" content-hidden tabindex="-1" > - "Increment" + ${x => numberFieldIncrementLabel.getValueFor(x)} <${iconAddTag} slot="start"> diff --git a/packages/nimble-components/src/number-field/tests/number-field.stories.ts b/packages/nimble-components/src/number-field/tests/number-field.stories.ts index 9fbfe9c48e..96e3464350 100644 --- a/packages/nimble-components/src/number-field/tests/number-field.stories.ts +++ b/packages/nimble-components/src/number-field/tests/number-field.stories.ts @@ -4,8 +4,17 @@ import type { Meta, StoryObj } from '@storybook/html'; import { createUserSelectedThemeStory } from '../../utilities/tests/storybook'; import { NumberFieldAppearance } from '../types'; import { numberFieldTag } from '..'; +import { labelProviderCoreTag } from '../../label-provider/core'; +import { + addLabelUseMetadata, + type LabelUserArgs +} from '../../label-provider/base/label-user-stories-utils'; +import { + numberFieldDecrementLabel, + numberFieldIncrementLabel +} from '../../label-provider/core/label-tokens'; -interface NumberFieldArgs { +interface NumberFieldArgs extends LabelUserArgs { label: string; value: number; step: number; @@ -71,6 +80,12 @@ const metadata: Meta = { errorText: 'Value is invalid' } }; +addLabelUseMetadata( + metadata, + labelProviderCoreTag, + numberFieldDecrementLabel, + numberFieldIncrementLabel +); export default metadata; diff --git a/packages/nimble-components/src/table/components/group-row/template.ts b/packages/nimble-components/src/table/components/group-row/template.ts index 4f80284dcc..01c23cea95 100644 --- a/packages/nimble-components/src/table/components/group-row/template.ts +++ b/packages/nimble-components/src/table/components/group-row/template.ts @@ -5,6 +5,10 @@ import { buttonTag } from '../../../button'; import { ButtonAppearance } from '../../../button/types'; import { iconArrowExpanderRightTag } from '../../../icons/arrow-expander-right'; import { checkboxTag } from '../../../checkbox'; +import { + tableGroupCollapseLabel, + tableGroupExpandLabel +} from '../../../label-provider/table/label-tokens'; // prettier-ignore export const template = html` @@ -33,6 +37,7 @@ export const template = html` tabindex="-1" > <${iconArrowExpanderRightTag} ${ref('expandIcon')} slot="start" class="expander-icon ${x => x.animationClass}"> + ${x => (x.expanded ? tableGroupCollapseLabel.getValueFor(x) : tableGroupExpandLabel.getValueFor(x))} diff --git a/packages/nimble-components/src/table/components/header/template.ts b/packages/nimble-components/src/table/components/header/template.ts index f7a1c0afc0..6e2104dddf 100644 --- a/packages/nimble-components/src/table/components/header/template.ts +++ b/packages/nimble-components/src/table/components/header/template.ts @@ -4,6 +4,7 @@ import { iconArrowDownTag } from '../../../icons/arrow-down'; import { iconArrowUpTag } from '../../../icons/arrow-up'; import { iconTwoSquaresInBracketsTag } from '../../../icons/two-squares-in-brackets'; import { TableColumnSortDirection } from '../../types'; +import { tableColumnHeaderGroupedIndicatorLabel } from '../../../label-provider/table/label-tokens'; // prettier-ignore export const template = html` @@ -20,8 +21,8 @@ export const template = html` ${when(x => x.sortDirection === TableColumnSortDirection.descending, html` <${iconArrowDownTag} class="sort-indicator" aria-hidden="true"> `)} - ${when(x => x.isGrouped, html` - <${iconTwoSquaresInBracketsTag} class="grouped-indicator"> + ${when(x => x.isGrouped, html` + <${iconTwoSquaresInBracketsTag} class="grouped-indicator" title="${x => tableColumnHeaderGroupedIndicatorLabel.getValueFor(x)}"> `)} `; diff --git a/packages/nimble-components/src/table/template.ts b/packages/nimble-components/src/table/template.ts index 77368bef7c..34f1cb4b92 100644 --- a/packages/nimble-components/src/table/template.ts +++ b/packages/nimble-components/src/table/template.ts @@ -23,6 +23,7 @@ import { buttonTag } from '../button'; import { ButtonAppearance } from '../button/types'; import { iconTriangleTwoLinesHorizontalTag } from '../icons/triangle-two-lines-horizontal'; import { checkboxTag } from '../checkbox'; +import { tableGroupsCollapseAllLabel } from '../label-provider/table/label-tokens'; // prettier-ignore export const template = html` @@ -59,6 +60,7 @@ export const template = html
` @click="${x => x.handleCollapseAllGroupRows()}" > <${iconTriangleTwoLinesHorizontalTag} slot="start"> + ${x => tableGroupsCollapseAllLabel.getValueFor(x)} diff --git a/packages/nimble-components/src/table/tests/table.stories.ts b/packages/nimble-components/src/table/tests/table.stories.ts index b30d3afb51..5a916fe533 100644 --- a/packages/nimble-components/src/table/tests/table.stories.ts +++ b/packages/nimble-components/src/table/tests/table.stories.ts @@ -12,8 +12,13 @@ import { iconUserTag } from '../../icons/user'; import { menuTag } from '../../menu'; import { menuItemTag } from '../../menu-item'; import { tableColumnTextTag } from '../../table-column/text'; +import { + addLabelUseMetadata, + type LabelUserArgs +} from '../../label-provider/base/label-user-stories-utils'; +import { labelProviderTableTag } from '../../label-provider/table'; -interface TableArgs { +interface TableArgs extends LabelUserArgs { data: ExampleDataType; selectionMode: keyof typeof TableRowSelectionMode; idFieldName: undefined; @@ -268,6 +273,7 @@ const metadata: Meta = { } } }; +addLabelUseMetadata(metadata, labelProviderTableTag); export default metadata; From e1d83d038b2169fe1f8dd20a06c48a8ebe1b7eb1 Mon Sep 17 00:00:00 2001 From: Malcolm Smith <20709258+msmithNI@users.noreply.github.com> Date: Mon, 26 Jun 2023 16:21:11 -0500 Subject: [PATCH 02/18] Remove explicit LabelProviderBase.labelTokens property --- .../src/label-provider/base/index.ts | 7 ------- .../base/label-provider-stories-utils.ts | 12 +++++------- .../core/tests/label-provider-core.stories.ts | 4 +++- .../src/label-provider/table/index.ts | 8 -------- .../table/tests/label-provider-table.stories.ts | 2 ++ 5 files changed, 10 insertions(+), 23 deletions(-) diff --git a/packages/nimble-components/src/label-provider/base/index.ts b/packages/nimble-components/src/label-provider/base/index.ts index 345a274d0d..0b419e4eff 100644 --- a/packages/nimble-components/src/label-provider/base/index.ts +++ b/packages/nimble-components/src/label-provider/base/index.ts @@ -5,13 +5,6 @@ import { themeProviderTag } from '../../theme-provider'; * Base class for label providers */ export abstract class LabelProviderBase extends FoundationElement { - /** - * @internal - */ - public abstract readonly labelTokens: { - [labelName: string]: DesignToken - }; - private readonly deferredTokenUpdates: Map< DesignToken, string | undefined diff --git a/packages/nimble-components/src/label-provider/base/label-provider-stories-utils.ts b/packages/nimble-components/src/label-provider/base/label-provider-stories-utils.ts index 39b0db8326..55e968eb26 100644 --- a/packages/nimble-components/src/label-provider/base/label-provider-stories-utils.ts +++ b/packages/nimble-components/src/label-provider/base/label-provider-stories-utils.ts @@ -1,8 +1,7 @@ -import { ViewTemplate, html, ref, repeat } from '@microsoft/fast-element'; +import { ViewTemplate, html, repeat } from '@microsoft/fast-element'; import type { Meta } from '@storybook/html'; import type { DesignToken } from '@microsoft/fast-foundation'; import { createUserSelectedThemeStory } from '../../utilities/tests/storybook'; -import type { LabelProviderBase } from '.'; import { bodyFont, groupHeaderFont, @@ -13,7 +12,7 @@ import { getAttributeName, getPropertyName } from './label-name-utils'; export interface LabelProviderArgs { labelProviderTag: string; - labelProviderRef: LabelProviderBase; + labelTokens: [string, DesignToken][]; removeNamePrefix(tokenName: string): string; } @@ -30,7 +29,7 @@ contributing docs for more information.`; const createTemplate = ( labelProviderTag: string ): ViewTemplate => html` -<${labelProviderTag} ${ref('labelProviderRef')}> +<${labelProviderTag}>

${x => x.labelProviderTag}


@@ -42,7 +41,7 @@ const createTemplate = ( ${repeat( - x => Object.entries(x.labelProviderRef.labelTokens), + x => x.labelTokens, html<[string, DesignToken], LabelProviderArgs>` @@ -100,14 +99,13 @@ export const labelProviderMetadata: Meta = { disable: true } }, - labelProviderRef: { + labelTokens: { table: { disable: true } } }, args: { - labelProviderTag: undefined, removeNamePrefix: jsTokenName => jsTokenName } }; diff --git a/packages/nimble-components/src/label-provider/core/tests/label-provider-core.stories.ts b/packages/nimble-components/src/label-provider/core/tests/label-provider-core.stories.ts index 6954d31b34..8a3f3e45f9 100644 --- a/packages/nimble-components/src/label-provider/core/tests/label-provider-core.stories.ts +++ b/packages/nimble-components/src/label-provider/core/tests/label-provider-core.stories.ts @@ -4,6 +4,7 @@ import { labelProviderMetadata } from '../../base/label-provider-stories-utils'; import { labelProviderCoreTag } from '..'; +import * as labelTokensNamespace from '../label-tokens'; const metadata = { ...labelProviderMetadata, @@ -15,6 +16,7 @@ export default metadata; export const coreLabelProvider: StoryObj = { args: { - labelProviderTag: labelProviderCoreTag + labelProviderTag: labelProviderCoreTag, + labelTokens: Object.entries(labelTokensNamespace) } }; diff --git a/packages/nimble-components/src/label-provider/table/index.ts b/packages/nimble-components/src/label-provider/table/index.ts index 0f64b30cc4..9bb95077c4 100644 --- a/packages/nimble-components/src/label-provider/table/index.ts +++ b/packages/nimble-components/src/label-provider/table/index.ts @@ -20,14 +20,6 @@ declare global { * Label provider for the Nimble table (and its sub-components and columns) */ export class LabelProviderTable extends LabelProviderBase { - public override readonly labelTokens = { - tableGroupCollapseLabel, - tableGroupExpandLabel, - tableGroupsCollapseAllLabel, - tableCellActionMenuLabel, - tableColumnHeaderGroupedIndicatorLabel - }; - @attr({ attribute: 'group-collapse', mode: 'fromView' diff --git a/packages/nimble-components/src/label-provider/table/tests/label-provider-table.stories.ts b/packages/nimble-components/src/label-provider/table/tests/label-provider-table.stories.ts index 95dd6eb688..ca8a04a6f8 100644 --- a/packages/nimble-components/src/label-provider/table/tests/label-provider-table.stories.ts +++ b/packages/nimble-components/src/label-provider/table/tests/label-provider-table.stories.ts @@ -5,6 +5,7 @@ import { } from '../../base/label-provider-stories-utils'; import { labelProviderTableTag } from '..'; import { removeTablePrefixAndCamelCase } from '../name-utils'; +import * as labelTokensNamespace from '../label-tokens'; const metadata = { ...labelProviderMetadata, @@ -16,6 +17,7 @@ export default metadata; export const tableLabelProvider: StoryObj = { args: { labelProviderTag: labelProviderTableTag, + labelTokens: Object.entries(labelTokensNamespace), removeNamePrefix: removeTablePrefixAndCamelCase } }; From 4a1404e49d2c34ccc5827b1ed52b00e3c3483aaf Mon Sep 17 00:00:00 2001 From: Malcolm Smith <20709258+msmithNI@users.noreply.github.com> Date: Mon, 26 Jun 2023 16:23:17 -0500 Subject: [PATCH 03/18] Change files --- ...le-components-6eb6eef1-eec2-4f6d-a98f-f7182ecdad50.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@ni-nimble-components-6eb6eef1-eec2-4f6d-a98f-f7182ecdad50.json diff --git a/change/@ni-nimble-components-6eb6eef1-eec2-4f6d-a98f-f7182ecdad50.json b/change/@ni-nimble-components-6eb6eef1-eec2-4f6d-a98f-f7182ecdad50.json new file mode 100644 index 0000000000..9fb64fece6 --- /dev/null +++ b/change/@ni-nimble-components-6eb6eef1-eec2-4f6d-a98f-f7182ecdad50.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Add nimble-label-provider-core and -table, along with localizable DesignTokens for labels/strings declared by Nimble components. Add docs for Localization.", + "packageName": "@ni/nimble-components", + "email": "20709258+msmithNI@users.noreply.github.com", + "dependentChangeType": "patch" +} From 6a518cd62bd9361dcb0e7045a93446812e71159e Mon Sep 17 00:00:00 2001 From: Malcolm Smith <20709258+msmithNI@users.noreply.github.com> Date: Mon, 26 Jun 2023 16:58:49 -0500 Subject: [PATCH 04/18] Add missing changes --- .../src/label-provider/core/index.ts | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/packages/nimble-components/src/label-provider/core/index.ts b/packages/nimble-components/src/label-provider/core/index.ts index c1005a9cf9..98a0ba92a2 100644 --- a/packages/nimble-components/src/label-provider/core/index.ts +++ b/packages/nimble-components/src/label-provider/core/index.ts @@ -1,7 +1,6 @@ import { attr } from '@microsoft/fast-element'; -import { DesignSystem, DesignToken } from '@microsoft/fast-foundation'; +import { DesignSystem } from '@microsoft/fast-foundation'; import { LabelProviderBase } from '../base'; -import type * as TokensNamespace from './label-tokens'; import { alertDismissLabel, numberFieldDecrementLabel, @@ -9,8 +8,6 @@ import { } from './label-tokens'; import { coreLabelDefaults } from './label-token-defaults'; -type TokenName = keyof typeof TokensNamespace; - declare global { interface HTMLElementTagNameMap { 'nimble-label-provider-core': LabelProviderCore; @@ -21,14 +18,6 @@ declare global { * Core label provider for Nimble */ export class LabelProviderCore extends LabelProviderBase { - public override readonly labelTokens: { - readonly [key in TokenName]: DesignToken; - } = { - alertDismissLabel, - numberFieldIncrementLabel, - numberFieldDecrementLabel - }; - @attr({ attribute: 'alert-dismiss', mode: 'fromView' From df11bcb8f26f23f88b4187298a13993121d1a1d3 Mon Sep 17 00:00:00 2001 From: Malcolm Smith <20709258+msmithNI@users.noreply.github.com> Date: Mon, 26 Jun 2023 17:05:50 -0500 Subject: [PATCH 05/18] Update CONTRIBUTING.md --- packages/nimble-components/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nimble-components/CONTRIBUTING.md b/packages/nimble-components/CONTRIBUTING.md index bd92345db1..b94ef19a9a 100644 --- a/packages/nimble-components/CONTRIBUTING.md +++ b/packages/nimble-components/CONTRIBUTING.md @@ -438,7 +438,7 @@ There are currently 2 label providers: - `nimble-label-provider-core`: Used for labels for all components besides the table - `nimble-label-provider-table`: Used for labels for the table (and table sub-components / column types) -To add new labels to a label provider, follow the existing patterns (create a `DesignToken`, an `@attr`-backed property, a `propertyChanged()` function that updates the token value, and add it to the `labelTokens` collection). +To add new labels to a label provider, follow the existing patterns (create a `DesignToken`, an `@attr`-backed property, and a `propertyChanged()` function that updates the token value). The expected format for label token names is: From 0bcdbb4a0ac0449bd547d22630bae93f02e6ea02 Mon Sep 17 00:00:00 2001 From: Malcolm Smith <20709258+msmithNI@users.noreply.github.com> Date: Tue, 27 Jun 2023 17:22:51 -0500 Subject: [PATCH 06/18] Add missing test for deferredTokenUpdates code path (setting label tokens before attached to DOM) --- .../base/tests/label-provider-base.spec.ts | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 packages/nimble-components/src/label-provider/base/tests/label-provider-base.spec.ts diff --git a/packages/nimble-components/src/label-provider/base/tests/label-provider-base.spec.ts b/packages/nimble-components/src/label-provider/base/tests/label-provider-base.spec.ts new file mode 100644 index 0000000000..e298a176c0 --- /dev/null +++ b/packages/nimble-components/src/label-provider/base/tests/label-provider-base.spec.ts @@ -0,0 +1,68 @@ +import { attr, customElement, html } from '@microsoft/fast-element'; +import { DesignToken } from '@microsoft/fast-foundation'; +import { LabelProviderBase } from '..'; +import { ThemeProvider, themeProviderTag } from '../../../theme-provider'; +import { + Fixture, + fixture, + uniqueElementName +} from '../../../utilities/tests/fixture'; + +async function setup(): Promise> { + return fixture(html` + <${themeProviderTag}> + + `); +} + +const exampleLabelDefaultValue = 'Initial Value'; +const exampleLabel = DesignToken.create({ + name: 'test-example-label', + cssCustomPropertyName: null +}).withDefault(exampleLabelDefaultValue); + +describe('Label Provider Base', () => { + let themeProvider: ThemeProvider; + let connect: () => Promise; + let disconnect: () => Promise; + + const testLabelProviderTag = uniqueElementName(); + + @customElement({ + name: testLabelProviderTag + }) + class LabelProviderTest extends LabelProviderBase { + @attr({ + attribute: 'test-example', + mode: 'fromView' + }) + public exampleLabel: string = exampleLabelDefaultValue; + + protected exampleLabelChanged( + _prev: string | undefined, + next: string | undefined + ): void { + this.handleTokenChanged(exampleLabel, next); + } + } + + beforeEach(async () => { + ({ element: themeProvider, connect, disconnect } = await setup()); + await connect(); + }); + + afterEach(async () => { + await disconnect(); + }); + + it('token values can be set before connected to DOM, and are set on target theme provider once attached', () => { + const element = document.createElement( + testLabelProviderTag + ) as LabelProviderTest; + element.exampleLabel = 'Updated Value'; + themeProvider.appendChild(element); + + const tokenValue = exampleLabel.getValueFor(themeProvider); + expect(tokenValue).toBe('Updated Value'); + }); +}); From e070de537d61dfd326176d73e7f1070b39ca0c70 Mon Sep 17 00:00:00 2001 From: Malcolm Smith <20709258+msmithNI@users.noreply.github.com> Date: Tue, 27 Jun 2023 18:49:41 -0500 Subject: [PATCH 07/18] Storybook updates --- .../base/label-provider-stories-utils.ts | 18 +----------- .../base/tests/label-providers.mdx | 28 +++++++++++++++++++ .../core/tests/label-provider-core.stories.ts | 3 +- 3 files changed, 30 insertions(+), 19 deletions(-) create mode 100644 packages/nimble-components/src/label-provider/base/tests/label-providers.mdx diff --git a/packages/nimble-components/src/label-provider/base/label-provider-stories-utils.ts b/packages/nimble-components/src/label-provider/base/label-provider-stories-utils.ts index 55e968eb26..6a7caaa8a6 100644 --- a/packages/nimble-components/src/label-provider/base/label-provider-stories-utils.ts +++ b/packages/nimble-components/src/label-provider/base/label-provider-stories-utils.ts @@ -16,22 +16,11 @@ export interface LabelProviderArgs { removeNamePrefix(tokenName: string): string; } -const overviewText = `Some Nimble components use strings/labels that need to be localized, if the consuming application -supports localization. Nimble exposes these localizable labels as design tokens, to support both localization and the -ability for clients to override the strings in specific contexts. - -Nimble provides English strings as the token defaults, and provides \`nimble-label-provider-*\` elements with APIs for -overriding those values with localized versions. - -See the nimble-components readme and -contributing docs for more information.`; - const createTemplate = ( labelProviderTag: string ): ViewTemplate => html` <${labelProviderTag}> -

${x => x.labelProviderTag}

-
+

Element name: ${x => x.labelProviderTag}

${x => x[0]}
@@ -61,11 +50,6 @@ const createTemplate = ( export const labelProviderMetadata: Meta = { parameters: { - docs: { - description: { - component: overviewText - } - }, chromatic: { disableSnapshot: true } }, // prettier-ignore diff --git a/packages/nimble-components/src/label-provider/base/tests/label-providers.mdx b/packages/nimble-components/src/label-provider/base/tests/label-providers.mdx new file mode 100644 index 0000000000..b34a192e8b --- /dev/null +++ b/packages/nimble-components/src/label-provider/base/tests/label-providers.mdx @@ -0,0 +1,28 @@ +import { Canvas, Meta, Title } from '@storybook/blocks'; +import { coreLabelProvider } from '../../core/tests/label-provider-core.stories'; +import { tableLabelProvider } from '../../table/tests/label-provider-table.stories'; + + +Label Providers + +Some Nimble components use strings/labels that need to be localized, if the consuming application +supports localization. Nimble exposes these localizable labels as design tokens, to support both localization and the +ability for clients to override the strings in specific contexts. + +Nimble provides English strings as the token defaults, and provides `nimble-label-provider-*` elements with APIs for +overriding those values with localized versions. + +See the nimble-components readme and +contributing docs for more information. + +## Core Label Provider + +Provides labels for all Nimble components besides the table component. + + + +## Table Label Provider + +Provides labels for the `nimble-table` and all associated table sub-components / column types. + + diff --git a/packages/nimble-components/src/label-provider/core/tests/label-provider-core.stories.ts b/packages/nimble-components/src/label-provider/core/tests/label-provider-core.stories.ts index 8a3f3e45f9..1eb02078c2 100644 --- a/packages/nimble-components/src/label-provider/core/tests/label-provider-core.stories.ts +++ b/packages/nimble-components/src/label-provider/core/tests/label-provider-core.stories.ts @@ -8,8 +8,7 @@ import * as labelTokensNamespace from '../label-tokens'; const metadata = { ...labelProviderMetadata, - title: 'Tokens/Label Providers', - tags: ['autodocs'] + title: 'Tokens/Label Providers' }; export default metadata; From a404c09b6d59cd28269652be6993995038f9344b Mon Sep 17 00:00:00 2001 From: Malcolm Smith <20709258+msmithNI@users.noreply.github.com> Date: Wed, 28 Jun 2023 15:52:17 -0500 Subject: [PATCH 08/18] Update docs / move story/name utils into tests folder --- packages/nimble-components/CONTRIBUTING.md | 3 +-- packages/nimble-components/README.md | 3 +-- .../nimble-components/src/banner/tests/banner.stories.ts | 2 +- .../label-provider/base/{ => tests}/label-name-utils.ts | 0 .../base/{ => tests}/label-provider-stories-utils.ts | 4 ++-- .../src/label-provider/base/tests/label-providers.mdx | 7 +------ .../base/{ => tests}/label-user-stories-utils.ts | 0 .../label-provider/core/tests/label-provider-core.spec.ts | 5 ++++- .../core/tests/label-provider-core.stories.ts | 2 +- .../table/tests/label-provider-table.spec.ts | 5 ++++- .../table/tests/label-provider-table.stories.ts | 2 +- .../src/number-field/tests/number-field.stories.ts | 2 +- .../nimble-components/src/table/tests/table.stories.ts | 2 +- 13 files changed, 18 insertions(+), 19 deletions(-) rename packages/nimble-components/src/label-provider/base/{ => tests}/label-name-utils.ts (100%) rename packages/nimble-components/src/label-provider/base/{ => tests}/label-provider-stories-utils.ts (95%) rename packages/nimble-components/src/label-provider/base/{ => tests}/label-user-stories-utils.ts (100%) diff --git a/packages/nimble-components/CONTRIBUTING.md b/packages/nimble-components/CONTRIBUTING.md index b94ef19a9a..2a06f2eca0 100644 --- a/packages/nimble-components/CONTRIBUTING.md +++ b/packages/nimble-components/CONTRIBUTING.md @@ -430,9 +430,8 @@ When creating a new component, create a `*-matrix.stories.ts` Storybook file to ## Localization -Nimble components may need to use some strings/labels (such as the label for a close button in a component's template, or menu items in a component-provided menu). Nimble exposes these localizable labels as design tokens, to support both localization and the ability for clients to override the strings. +Most user-visible strings displayed by Nimble components are provided by the client application and are expected to be localized by the application if necessary. However, some strings are built into Nimble components and are provided only in English. An application can provide localized versions of these strings by using design tokens set on label provider elements. -Nimble provides English strings as the token defaults, and provides `nimble-label-provider-*` elements with APIs for overriding those values. There are currently 2 label providers: - `nimble-label-provider-core`: Used for labels for all components besides the table diff --git a/packages/nimble-components/README.md b/packages/nimble-components/README.md index b9a1ab73b2..3529674cfa 100644 --- a/packages/nimble-components/README.md +++ b/packages/nimble-components/README.md @@ -98,9 +98,8 @@ The goal of the Nimble design system is to provide a consistent style for applic ## Localization -Some Nimble components use strings/labels that need to be localized, if the consuming application supports localization. Nimble exposes these localizable labels as design tokens, to support both localization and the ability for clients to override the strings. +Most user-visible strings displayed by Nimble components are provided by the client application and are expected to be localized by the application if necessary. However, some strings are built into Nimble components and are provided only in English. An application can provide localized versions of these strings by using design tokens set on label provider elements. -Nimble provides English strings as the token defaults, and provides `nimble-label-provider-*` elements with APIs for overriding those values. There are currently 2 label providers: - `nimble-label-provider-core`: Used for labels for all components besides the table diff --git a/packages/nimble-components/src/banner/tests/banner.stories.ts b/packages/nimble-components/src/banner/tests/banner.stories.ts index ab6222421d..cb5a9f7cab 100644 --- a/packages/nimble-components/src/banner/tests/banner.stories.ts +++ b/packages/nimble-components/src/banner/tests/banner.stories.ts @@ -12,7 +12,7 @@ import { labelProviderCoreTag } from '../../label-provider/core'; import { LabelUserArgs, addLabelUseMetadata -} from '../../label-provider/base/label-user-stories-utils'; +} from '../../label-provider/base/tests/label-user-stories-utils'; import { alertDismissLabel } from '../../label-provider/core/label-tokens'; // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/packages/nimble-components/src/label-provider/base/label-name-utils.ts b/packages/nimble-components/src/label-provider/base/tests/label-name-utils.ts similarity index 100% rename from packages/nimble-components/src/label-provider/base/label-name-utils.ts rename to packages/nimble-components/src/label-provider/base/tests/label-name-utils.ts diff --git a/packages/nimble-components/src/label-provider/base/label-provider-stories-utils.ts b/packages/nimble-components/src/label-provider/base/tests/label-provider-stories-utils.ts similarity index 95% rename from packages/nimble-components/src/label-provider/base/label-provider-stories-utils.ts rename to packages/nimble-components/src/label-provider/base/tests/label-provider-stories-utils.ts index 6a7caaa8a6..3b6866395f 100644 --- a/packages/nimble-components/src/label-provider/base/label-provider-stories-utils.ts +++ b/packages/nimble-components/src/label-provider/base/tests/label-provider-stories-utils.ts @@ -1,13 +1,13 @@ import { ViewTemplate, html, repeat } from '@microsoft/fast-element'; import type { Meta } from '@storybook/html'; import type { DesignToken } from '@microsoft/fast-foundation'; -import { createUserSelectedThemeStory } from '../../utilities/tests/storybook'; +import { createUserSelectedThemeStory } from '../../../utilities/tests/storybook'; import { bodyFont, groupHeaderFont, groupHeaderFontColor, groupHeaderTextTransform -} from '../../theme-provider/design-tokens'; +} from '../../../theme-provider/design-tokens'; import { getAttributeName, getPropertyName } from './label-name-utils'; export interface LabelProviderArgs { diff --git a/packages/nimble-components/src/label-provider/base/tests/label-providers.mdx b/packages/nimble-components/src/label-provider/base/tests/label-providers.mdx index b34a192e8b..26e742daa3 100644 --- a/packages/nimble-components/src/label-provider/base/tests/label-providers.mdx +++ b/packages/nimble-components/src/label-provider/base/tests/label-providers.mdx @@ -5,12 +5,7 @@ import { tableLabelProvider } from '../../table/tests/label-provider-table.stori Label Providers -Some Nimble components use strings/labels that need to be localized, if the consuming application -supports localization. Nimble exposes these localizable labels as design tokens, to support both localization and the -ability for clients to override the strings in specific contexts. - -Nimble provides English strings as the token defaults, and provides `nimble-label-provider-*` elements with APIs for -overriding those values with localized versions. +Most user-visible strings displayed by Nimble components are provided by the client application and are expected to be localized by the application if necessary. However, some strings are built into Nimble components and are provided only in English. An application can provide localized versions of these strings by using design tokens set on label provider elements. See the nimble-components readme and contributing docs for more information. diff --git a/packages/nimble-components/src/label-provider/base/label-user-stories-utils.ts b/packages/nimble-components/src/label-provider/base/tests/label-user-stories-utils.ts similarity index 100% rename from packages/nimble-components/src/label-provider/base/label-user-stories-utils.ts rename to packages/nimble-components/src/label-provider/base/tests/label-user-stories-utils.ts diff --git a/packages/nimble-components/src/label-provider/core/tests/label-provider-core.spec.ts b/packages/nimble-components/src/label-provider/core/tests/label-provider-core.spec.ts index a3cb9203ed..5dca1cf8d0 100644 --- a/packages/nimble-components/src/label-provider/core/tests/label-provider-core.spec.ts +++ b/packages/nimble-components/src/label-provider/core/tests/label-provider-core.spec.ts @@ -3,7 +3,10 @@ import { html } from '@microsoft/fast-element'; import * as labelTokensNamespace from '../label-tokens'; import { LabelProviderCore, labelProviderCoreTag } from '..'; import { getSpecTypeByNamedList } from '../../../utilities/tests/parameterized'; -import { getAttributeName, getPropertyName } from '../../base/label-name-utils'; +import { + getAttributeName, + getPropertyName +} from '../../base/tests/label-name-utils'; import { ThemeProvider, themeProviderTag } from '../../../theme-provider'; import { Fixture, fixture } from '../../../utilities/tests/fixture'; diff --git a/packages/nimble-components/src/label-provider/core/tests/label-provider-core.stories.ts b/packages/nimble-components/src/label-provider/core/tests/label-provider-core.stories.ts index 1eb02078c2..6eff6fb29a 100644 --- a/packages/nimble-components/src/label-provider/core/tests/label-provider-core.stories.ts +++ b/packages/nimble-components/src/label-provider/core/tests/label-provider-core.stories.ts @@ -2,7 +2,7 @@ import type { StoryObj } from '@storybook/html'; import { LabelProviderArgs, labelProviderMetadata -} from '../../base/label-provider-stories-utils'; +} from '../../base/tests/label-provider-stories-utils'; import { labelProviderCoreTag } from '..'; import * as labelTokensNamespace from '../label-tokens'; diff --git a/packages/nimble-components/src/label-provider/table/tests/label-provider-table.spec.ts b/packages/nimble-components/src/label-provider/table/tests/label-provider-table.spec.ts index c5fee4e330..1659d7e4db 100644 --- a/packages/nimble-components/src/label-provider/table/tests/label-provider-table.spec.ts +++ b/packages/nimble-components/src/label-provider/table/tests/label-provider-table.spec.ts @@ -3,7 +3,10 @@ import { html } from '@microsoft/fast-element'; import * as labelTokensNamespace from '../label-tokens'; import { LabelProviderTable, labelProviderTableTag } from '..'; import { getSpecTypeByNamedList } from '../../../utilities/tests/parameterized'; -import { getAttributeName, getPropertyName } from '../../base/label-name-utils'; +import { + getAttributeName, + getPropertyName +} from '../../base/tests/label-name-utils'; import { ThemeProvider, themeProviderTag } from '../../../theme-provider'; import { Fixture, fixture } from '../../../utilities/tests/fixture'; import { removeTablePrefixAndCamelCase } from '../name-utils'; diff --git a/packages/nimble-components/src/label-provider/table/tests/label-provider-table.stories.ts b/packages/nimble-components/src/label-provider/table/tests/label-provider-table.stories.ts index ca8a04a6f8..4904d06b56 100644 --- a/packages/nimble-components/src/label-provider/table/tests/label-provider-table.stories.ts +++ b/packages/nimble-components/src/label-provider/table/tests/label-provider-table.stories.ts @@ -2,7 +2,7 @@ import type { StoryObj } from '@storybook/html'; import { LabelProviderArgs, labelProviderMetadata -} from '../../base/label-provider-stories-utils'; +} from '../../base/tests/label-provider-stories-utils'; import { labelProviderTableTag } from '..'; import { removeTablePrefixAndCamelCase } from '../name-utils'; import * as labelTokensNamespace from '../label-tokens'; diff --git a/packages/nimble-components/src/number-field/tests/number-field.stories.ts b/packages/nimble-components/src/number-field/tests/number-field.stories.ts index 96e3464350..7d48e23c82 100644 --- a/packages/nimble-components/src/number-field/tests/number-field.stories.ts +++ b/packages/nimble-components/src/number-field/tests/number-field.stories.ts @@ -8,7 +8,7 @@ import { labelProviderCoreTag } from '../../label-provider/core'; import { addLabelUseMetadata, type LabelUserArgs -} from '../../label-provider/base/label-user-stories-utils'; +} from '../../label-provider/base/tests/label-user-stories-utils'; import { numberFieldDecrementLabel, numberFieldIncrementLabel diff --git a/packages/nimble-components/src/table/tests/table.stories.ts b/packages/nimble-components/src/table/tests/table.stories.ts index 5a916fe533..c099704e6f 100644 --- a/packages/nimble-components/src/table/tests/table.stories.ts +++ b/packages/nimble-components/src/table/tests/table.stories.ts @@ -15,7 +15,7 @@ import { tableColumnTextTag } from '../../table-column/text'; import { addLabelUseMetadata, type LabelUserArgs -} from '../../label-provider/base/label-user-stories-utils'; +} from '../../label-provider/base/tests/label-user-stories-utils'; import { labelProviderTableTag } from '../../label-provider/table'; interface TableArgs extends LabelUserArgs { From 8dbf4db338dc5795eb53e3af0191cb24c90efdfe Mon Sep 17 00:00:00 2001 From: Malcolm Smith <20709258+msmithNI@users.noreply.github.com> Date: Wed, 28 Jun 2023 16:22:45 -0500 Subject: [PATCH 09/18] Uptake nimble-table for label token stories --- .../tests/label-provider-stories-utils.ts | 114 +++++++++++------- 1 file changed, 71 insertions(+), 43 deletions(-) diff --git a/packages/nimble-components/src/label-provider/base/tests/label-provider-stories-utils.ts b/packages/nimble-components/src/label-provider/base/tests/label-provider-stories-utils.ts index 3b6866395f..be9696b401 100644 --- a/packages/nimble-components/src/label-provider/base/tests/label-provider-stories-utils.ts +++ b/packages/nimble-components/src/label-provider/base/tests/label-provider-stories-utils.ts @@ -1,19 +1,18 @@ -import { ViewTemplate, html, repeat } from '@microsoft/fast-element'; +import { ViewTemplate, html, ref } from '@microsoft/fast-element'; import type { Meta } from '@storybook/html'; import type { DesignToken } from '@microsoft/fast-foundation'; import { createUserSelectedThemeStory } from '../../../utilities/tests/storybook'; -import { - bodyFont, - groupHeaderFont, - groupHeaderFontColor, - groupHeaderTextTransform -} from '../../../theme-provider/design-tokens'; +import { bodyFont } from '../../../theme-provider/design-tokens'; import { getAttributeName, getPropertyName } from './label-name-utils'; +import { Table, tableTag } from '../../../table'; +import { tableColumnTextTag } from '../../../table-column/text'; export interface LabelProviderArgs { + tableRef: Table; labelProviderTag: string; labelTokens: [string, DesignToken][]; removeNamePrefix(tokenName: string): string; + updateData(args: LabelProviderArgs): void; } const createTemplate = ( @@ -21,32 +20,35 @@ const createTemplate = ( ): ViewTemplate => html` <${labelProviderTag}>

Element name: ${x => x.labelProviderTag}

-
Token name
- - - - - - - - ${repeat( - x => x.labelTokens, - html<[string, DesignToken], LabelProviderArgs>` - - - - - - - ` - )} - -
Token nameHTML attribute nameJS property nameDefault value (English)
${x => x[0]} - ${(x, c) => getAttributeName(c.parent.removeNamePrefix(x[0]))} - - ${(x, c) => getPropertyName(c.parent.removeNamePrefix(x[0]))} - "${x => x[1].getValueFor(document.body)}"
-`; +<${tableTag} + ${ref('tableRef')} + data-unused="${x => x.updateData(x)}" +> + <${tableColumnTextTag} + column-id="token-name-column" + field-name="tokenName" + > + Token name + + <${tableColumnTextTag} + column-id="attribute-name-column" + field-name="htmlAttributeName" + > + HTML attribute name + + <${tableColumnTextTag} + column-id="property-name-column" + field-name="jsPropertyName" + > + JS property name + + <${tableColumnTextTag} + column-id="default-value-column" + field-name="defaultValue" + > + Default value (English) + +`; export const labelProviderMetadata: Meta = { parameters: { @@ -56,17 +58,11 @@ export const labelProviderMetadata: Meta = { render: createUserSelectedThemeStory(html`
${x => createTemplate(x.labelProviderTag)} @@ -87,9 +83,41 @@ export const labelProviderMetadata: Meta = { table: { disable: true } + }, + tableRef: { + table: { + disable: true + } + }, + updateData: { + table: { + disable: true + } } }, args: { - removeNamePrefix: jsTokenName => jsTokenName + removeNamePrefix: jsTokenName => jsTokenName, + tableRef: undefined, + updateData: x => { + void (async () => { + // Safari workaround: the table element instance is made at this point + // but doesn't seem to be upgraded to a custom element yet + await customElements.whenDefined('nimble-table'); + + const data = x.labelTokens.map(token => { + return { + tokenName: token[0], + htmlAttributeName: getAttributeName( + x.removeNamePrefix(token[0]) + ), + jsPropertyName: getPropertyName( + x.removeNamePrefix(token[0]) + ), + defaultValue: token[1].getValueFor(document.body) + }; + }); + await x.tableRef.setData(data); + })(); + } } }; From ff69c73a6aba1efa1886d1bbe47ea86455f497fd Mon Sep 17 00:00:00 2001 From: Malcolm Smith <20709258+msmithNI@users.noreply.github.com> Date: Wed, 28 Jun 2023 18:54:44 -0500 Subject: [PATCH 10/18] PR feedback --- .../src/label-provider/base/index.ts | 66 ++++++-------- .../base/tests/label-provider-base.spec.ts | 18 ++-- .../tests/label-provider-stories-utils.ts | 4 - .../src/label-provider/core/index.ts | 57 ++++-------- .../src/label-provider/table/index.ts | 91 +++++-------------- 5 files changed, 79 insertions(+), 157 deletions(-) diff --git a/packages/nimble-components/src/label-provider/base/index.ts b/packages/nimble-components/src/label-provider/base/index.ts index 0b419e4eff..6db6133d54 100644 --- a/packages/nimble-components/src/label-provider/base/index.ts +++ b/packages/nimble-components/src/label-provider/base/index.ts @@ -1,63 +1,57 @@ import { DesignToken, FoundationElement } from '@microsoft/fast-foundation'; +import { Observable, Subscriber } from '@microsoft/fast-element'; import { themeProviderTag } from '../../theme-provider'; /** * Base class for label providers */ -export abstract class LabelProviderBase extends FoundationElement { - private readonly deferredTokenUpdates: Map< - DesignToken, - string | undefined - > = new Map(); +export abstract class LabelProviderBase + extends FoundationElement + implements Subscriber { + protected abstract readonly supportedLabels: { + [P in keyof LabelProviderBase]?: DesignToken; + }; private themeProvider?: HTMLElement; + public constructor() { + super(); + Observable.getNotifier(this).subscribe(this); + } + public override connectedCallback(): void { super.connectedCallback(); - this.updateThemeProvider(); + this.initializeThemeProvider(); } public override disconnectedCallback(): void { this.themeProvider = undefined; } - /** - * Called from the [tokenPropertyName]Changed() methods to update the backing design token - * @param token DesignToken for the label - * @param newValue New value for the label - */ - protected handleTokenChanged( - token: DesignToken, - newValue: string | undefined + public handleChange( + _element: LabelProviderBase, + property: keyof LabelProviderBase ): void { - if (this.themeProvider) { - this.updateTokenValue(this.themeProvider, token, newValue); - } else { - // For Blazor this method is called before this element is in the DOM. So cache the token value sets, and - // apply them after we're connected - this.deferredTokenUpdates.set(token, newValue); - } - } - - private updateTokenValue( - target: HTMLElement, - token: DesignToken, - newValue: string | undefined - ): void { - if (newValue !== undefined && newValue !== null) { - token.setValueFor(target, newValue); - } else { - token.deleteValueFor(target); + if (this.supportedLabels[property]) { + const token = this.supportedLabels[property]!; + let value = this[property] as string | undefined; + if (value === undefined) { + value = token.getValueFor(this); + } + token.setValueFor(this, value); + if (this.themeProvider) { + token.setValueFor(this.themeProvider, value); + } } } - private updateThemeProvider(): void { + private initializeThemeProvider(): void { this.themeProvider = this.closest(themeProviderTag) ?? undefined; if (this.themeProvider) { - for (const entry of this.deferredTokenUpdates.entries()) { - this.updateTokenValue(this.themeProvider, entry[0], entry[1]); + for (const token of Object.values(this.supportedLabels)) { + const value = token.getValueFor(this); + token.setValueFor(this.themeProvider, value); } - this.deferredTokenUpdates.clear(); } } } diff --git a/packages/nimble-components/src/label-provider/base/tests/label-provider-base.spec.ts b/packages/nimble-components/src/label-provider/base/tests/label-provider-base.spec.ts index e298a176c0..7ccd4c51b6 100644 --- a/packages/nimble-components/src/label-provider/base/tests/label-provider-base.spec.ts +++ b/packages/nimble-components/src/label-provider/base/tests/label-provider-base.spec.ts @@ -32,18 +32,14 @@ describe('Label Provider Base', () => { name: testLabelProviderTag }) class LabelProviderTest extends LabelProviderBase { - @attr({ - attribute: 'test-example', - mode: 'fromView' - }) - public exampleLabel: string = exampleLabelDefaultValue; + @attr({ attribute: 'test-example' }) + public exampleLabel?: string; - protected exampleLabelChanged( - _prev: string | undefined, - next: string | undefined - ): void { - this.handleTokenChanged(exampleLabel, next); - } + protected override readonly supportedLabels: { + [P in keyof LabelProviderTest]?: DesignToken; + } = { + exampleLabel + }; } beforeEach(async () => { diff --git a/packages/nimble-components/src/label-provider/base/tests/label-provider-stories-utils.ts b/packages/nimble-components/src/label-provider/base/tests/label-provider-stories-utils.ts index be9696b401..658f244ba9 100644 --- a/packages/nimble-components/src/label-provider/base/tests/label-provider-stories-utils.ts +++ b/packages/nimble-components/src/label-provider/base/tests/label-provider-stories-utils.ts @@ -51,10 +51,6 @@ const createTemplate = ( `; export const labelProviderMetadata: Meta = { - parameters: { - chromatic: { disableSnapshot: true } - }, - // prettier-ignore render: createUserSelectedThemeStory(html`