Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add/uptake label providers in nimble-components #1328

Merged
merged 25 commits into from
Jul 13, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4df8e18
Label changes
msmithNI Jun 26, 2023
c6e727f
Merge branch 'main' into localizable-labels-1
msmithNI Jun 26, 2023
e1d83d0
Remove explicit LabelProviderBase.labelTokens property
msmithNI Jun 26, 2023
4a1404e
Change files
msmithNI Jun 26, 2023
6a518cd
Add missing changes
msmithNI Jun 26, 2023
df11bcb
Update CONTRIBUTING.md
msmithNI Jun 26, 2023
c18d240
Merge branch 'main' into localizable-labels-1
msmithNI Jun 27, 2023
0bcdbb4
Add missing test for deferredTokenUpdates code path (setting label to…
msmithNI Jun 27, 2023
e070de5
Storybook updates
msmithNI Jun 27, 2023
a404c09
Update docs / move story/name utils into tests folder
msmithNI Jun 28, 2023
8dbf4db
Uptake nimble-table for label token stories
msmithNI Jun 28, 2023
61224a6
Merge branch 'main' into localizable-labels-1
msmithNI Jun 28, 2023
ff69c73
PR feedback
msmithNI Jun 28, 2023
636de00
Add per-control tests for labels, minor cleanup, hook up tableCellAct…
msmithNI Jun 29, 2023
9569993
Merge branch 'main' into localizable-labels-1
msmithNI Jun 29, 2023
f9934e8
Merge branch 'main' into localizable-labels-1
msmithNI Jun 30, 2023
79b8cc9
PR feedback to handle token value cascades
msmithNI Jul 1, 2023
078b100
PR feedback
msmithNI Jul 10, 2023
e56df92
Fix test
msmithNI Jul 10, 2023
1e16eec
PR feedback
msmithNI Jul 11, 2023
fbcf8f5
Cleanup
msmithNI Jul 11, 2023
c41c407
Merge branch 'main' into localizable-labels-1
msmithNI Jul 11, 2023
fd4de3d
PR feedback
msmithNI Jul 13, 2023
e56d7d8
Merge branch 'main' into localizable-labels-1
msmithNI Jul 13, 2023
9e0e9d7
Test rebuild
msmithNI Jul 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"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": "[email protected]",
"dependentChangeType": "patch"
}
25 changes: 25 additions & 0 deletions packages/nimble-components/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>`, an `@attr`-backed property, and a `propertyChanged()` function that updates the token value).

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.
Expand Down
31 changes: 28 additions & 3 deletions packages/nimble-components/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: `<nimble-icon-succeeded></nimble-icon-succeeded>`.
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: `<nimble-icon-succeeded></nimble-icon-succeeded>`.
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

Expand Down Expand Up @@ -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
<body>
<nimble-theme-provider theme="light">
<nimble-label-provider-core></nimble-label-provider-core>
<!-- if using nimble-table, include nimble-label-provider-table: -->
<nimble-label-provider-table></nimble-label-provider-table>
<!-- everything else -->
</nimble-theme-provider>
</body>
```
- 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

Expand Down
2 changes: 2 additions & 0 deletions packages/nimble-components/src/all-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
3 changes: 2 additions & 1 deletion packages/nimble-components/src/banner/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Banner>`
Expand Down Expand Up @@ -53,7 +54,7 @@ export const template = html<Banner>`
${when(x => !x.preventDismiss, html<Banner>`
<${buttonTag} appearance="ghost" content-hidden @click="${x => x.dismissBanner()}">
<${iconXmarkTag} slot="start"></${iconXmarkTag}>
${x => x.dismissButtonLabel ?? 'Close'}
${x => x.dismissButtonLabel ?? alertDismissLabel.getValueFor(x)}
</${buttonTag}>
`)}
</div>
Expand Down
19 changes: 15 additions & 4 deletions packages/nimble-components/src/banner/tests/banner.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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;
Expand Down Expand Up @@ -55,6 +61,7 @@ const metadata: Meta<BannerArgs> = {
}
}
};
addLabelUseMetadata(metadata, labelProviderCoreTag, alertDismissLabel);

export default metadata;

Expand Down Expand Up @@ -121,11 +128,13 @@ export const _banner: StoryObj<BannerArgs> = {
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: {
Expand All @@ -135,6 +144,8 @@ export const _banner: StoryObj<BannerArgs> = {
severity: BannerSeverity.error,
action: 'none',
preventDismiss: false,
titleHidden: false
titleHidden: false,
dismissButtonLabel: undefined,
toggle: undefined
}
};
63 changes: 63 additions & 0 deletions packages/nimble-components/src/label-provider/base/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { DesignToken, FoundationElement } from '@microsoft/fast-foundation';
import { themeProviderTag } from '../../theme-provider';

/**
* Base class for label providers
*/
export abstract class LabelProviderBase extends FoundationElement {
private readonly deferredTokenUpdates: Map<
DesignToken<string>,
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<string>,
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<string>,
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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
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 {
bodyFont,
groupHeaderFont,
groupHeaderFontColor,
groupHeaderTextTransform
} from '../../theme-provider/design-tokens';
import { getAttributeName, getPropertyName } from './label-name-utils';

export interface LabelProviderArgs {
labelProviderTag: string;
labelTokens: [string, DesignToken<string>][];
removeNamePrefix(tokenName: string): string;
}

const createTemplate = (
labelProviderTag: string
): ViewTemplate<LabelProviderArgs> => html<LabelProviderArgs>`
<${labelProviderTag}></${labelProviderTag}>
<p>Element name: <code>${x => x.labelProviderTag}</code></p>
<table>
<thead>
<th>Token name</th>
<th>HTML attribute name</th>
<th>JS property name</th>
<th>Default value (English)</th>
</thead>
<tbody>
${repeat(
x => x.labelTokens,
html<[string, DesignToken<string>], LabelProviderArgs>`
<tr>
<td>${x => x[0]}</td>
<td>
${(x, c) => getAttributeName(c.parent.removeNamePrefix(x[0]))}
</td>
<td>
${(x, c) => getPropertyName(c.parent.removeNamePrefix(x[0]))}
</td>
<td>"${x => x[1].getValueFor(document.body)}"</td>
</tr>
`
)}
</tbody>
</table>
`;

export const labelProviderMetadata: Meta<LabelProviderArgs> = {
parameters: {
chromatic: { disableSnapshot: true }
},
// prettier-ignore
render: createUserSelectedThemeStory(html<LabelProviderArgs>`
<div>
<style>
div {
font: var(${bodyFont.cssCustomProperty});
}
thead {
font: var(${groupHeaderFont.cssCustomProperty});
color: var(${groupHeaderFontColor.cssCustomProperty});
text-transform: var(${groupHeaderTextTransform.cssCustomProperty});
}
td {
padding: 10px 20px 10px 10px;
height: 32px;
}
</style>
${x => createTemplate(x.labelProviderTag)}
</div>
`),
argTypes: {
removeNamePrefix: {
table: {
disable: true
}
},
labelProviderTag: {
table: {
disable: true
}
},
labelTokens: {
table: {
disable: true
}
}
},
args: {
removeNamePrefix: jsTokenName => jsTokenName
}
};
Original file line number Diff line number Diff line change
@@ -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<TArgs extends LabelUserArgs>(
metadata: Meta<TArgs>,
labelProviderTag: string,
...labelTokens: DesignToken<string>[]
): 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' }
};
}
Loading