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 all 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 @@
{
msmithNI marked this conversation as resolved.
Show resolved Hide resolved
msmithNI marked this conversation as resolved.
Show resolved Hide resolved
"type": "minor",
msmithNI marked this conversation as resolved.
Show resolved Hide resolved
"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"
}
22 changes: 22 additions & 0 deletions packages/nimble-components/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,28 @@ 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

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.

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)

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`
msmithNI marked this conversation as resolved.
Show resolved Hide resolved
- 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
30 changes: 27 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.
msmithNI marked this conversation as resolved.
Show resolved Hide resolved

### Prototyping in a static webpage

Expand Down Expand Up @@ -96,9 +96,33 @@ 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

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.

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 { popupDismissLabel } 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 ?? popupDismissLabel.getValueFor(x)}
</${buttonTag}>
`)}
</div>
Expand Down
54 changes: 53 additions & 1 deletion packages/nimble-components/src/banner/tests/banner.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { html } from '@microsoft/fast-element';
import { fixture, Fixture } from '../../utilities/tests/fixture';
import { Banner } from '..';
import { Banner, bannerTag } from '..';
import { BannerSeverity } from '../types';
import { waitForUpdatesAsync } from '../../testing/async-helpers';
import { createEventListener } from '../../utilities/tests/component';
import { themeProviderTag, type ThemeProvider } from '../../theme-provider';
import {
LabelProviderCore,
labelProviderCoreTag
} from '../../label-provider/core';
import { buttonTag } from '../../button';

async function setup(): Promise<Fixture<Banner>> {
return fixture<Banner>(html`
Expand All @@ -14,6 +20,18 @@ async function setup(): Promise<Fixture<Banner>> {
`);
}

async function setupWithLabelProvider(): Promise<Fixture<ThemeProvider>> {
return fixture<ThemeProvider>(html`
<${themeProviderTag}>
<${labelProviderCoreTag}></${labelProviderCoreTag}>
<nimble-banner>
<span slot="title">Title</span>
Message text
</nimble-banner>
</${themeProviderTag}>
`);
}

describe('Banner', () => {
let element: Banner;
let connect: () => Promise<void>;
Expand Down Expand Up @@ -102,3 +120,37 @@ describe('Banner', () => {
).toBe('status');
});
});

describe('Banner with LabelProviderCore', () => {
let element: Banner;
let labelProvider: LabelProviderCore;
let connect: () => Promise<void>;
let disconnect: () => Promise<void>;

beforeEach(async () => {
let themeProvider: ThemeProvider;
({
element: themeProvider,
connect,
disconnect
} = await setupWithLabelProvider());
await connect();
element = themeProvider.querySelector(bannerTag)!;
labelProvider = themeProvider.querySelector(labelProviderCoreTag)!;
});

afterEach(async () => {
await disconnect();
});

it('uses CoreLabelProvider popupDismissLabel for the close button label when dismissButtonLabel is unset', async () => {
labelProvider.popupDismiss = 'Customized Close';
await waitForUpdatesAsync();

const actualCloseButtonText = element
.shadowRoot!.querySelector('.dismiss')!
.querySelector(buttonTag)!
.textContent!.trim();
expect(actualCloseButtonText).toBe('Customized Close');
});
});
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/tests/label-user-stories-utils';
import { popupDismissLabel } 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, popupDismissLabel);

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. <br>(Equivalent to setting `popup-dismiss` on `nimble-label-provider-core`)',
control: { type: 'none' }
msmithNI marked this conversation as resolved.
Show resolved Hide resolved
},
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
}
};
70 changes: 70 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,70 @@
import { DesignToken, FoundationElement } from '@microsoft/fast-foundation';
import { Notifier, Observable, type Subscriber } from '@microsoft/fast-element';
import { themeProviderTag } from '../../theme-provider';

export type DesignTokensFor<ObjectT> = {
[key in keyof ObjectT]: string | undefined;
};

/**
* Base class for label providers
*/
export abstract class LabelProviderBase<
SupportedLabels extends { [key: string]: DesignToken<string> }
>
extends FoundationElement
implements Subscriber {
protected abstract supportedLabels: SupportedLabels;

private readonly propertyNotifier: Notifier = Observable.getNotifier(this);
private themeProvider?: HTMLElement;

public override connectedCallback(): void {
super.connectedCallback();
this.initializeThemeProvider();
this.propertyNotifier.subscribe(this);
}

public override disconnectedCallback(): void {
this.propertyNotifier.unsubscribe(this);
if (this.themeProvider) {
for (const token of Object.values(this.supportedLabels)) {
token.deleteValueFor(this.themeProvider);
}
this.themeProvider = undefined;
}
}

public handleChange(
_element: LabelProviderBase<SupportedLabels>,
property: keyof LabelProviderBase<SupportedLabels>
): void {
if (this.supportedLabels[property]) {
const token = this.supportedLabels[property]!;
const value = this[property];
if (this.themeProvider) {
if (value === null || value === undefined) {
token.deleteValueFor(this.themeProvider);
} else {
token.setValueFor(this.themeProvider, value as string);
}
}
}
}

private initializeThemeProvider(): void {
this.themeProvider = this.closest(themeProviderTag) ?? undefined;
if (this.themeProvider) {
for (const [property, token] of Object.entries(
this.supportedLabels
)) {
const value = this[property as keyof LabelProviderBase<SupportedLabels>];
if (value === null || value === undefined) {
token.deleteValueFor(this.themeProvider);
} else {
token.setValueFor(this.themeProvider, value as string);
}
}
}
}
}
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));
}
Loading