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

Rich Text Editor | Add label provider in nimble-rich-text-editor #1459

Merged
merged 20 commits into from
Sep 6, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
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": "patch",
"comment": "Add label provider for rich-text-editor",
"packageName": "@ni/nimble-components",
"email": "[email protected]",
"dependentChangeType": "patch"
}
5 changes: 3 additions & 2 deletions packages/nimble-components/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -445,9 +445,10 @@ When creating a new component, create a `*-matrix.stories.ts` Storybook file to

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:
The current label providers:

- `nimble-label-provider-core`: Used for labels for all components besides the table
- `nimble-label-provider-core`: Used for labels for all components without a dedicated label provider
- `nimble-label-provider-rich-text`: Used for labels for the rich text components
- `nimble-label-provider-table`: Used for labels for the table (and table sub-components / column types)

The expected format for label token names is:
Expand Down
5 changes: 3 additions & 2 deletions packages/nimble-components/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,10 @@ The goal of the Nimble design system is to provide a consistent style for applic

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:
The current label providers:

- `nimble-label-provider-core`: Used for labels for all components besides the table
- `nimble-label-provider-core`: Used for labels for all components without a dedicated label provider
- `nimble-label-provider-rich-text`: Used for labels for the rich text components
- `nimble-label-provider-table`: Used for labels for the table (and table sub-components / column types)

If a client is localized, it should:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,18 @@ export function getPropertyName(jsKey: string): string {
export function getAttributeName(jsKey: string): string {
return spinalCase(getPropertyName(jsKey));
}

/**
* Removes the prefix and camelCases the input token name.
* (The design token name has the element/types prefix, but the properties do not, as they're already
* scoped to the respective element)
aagash-ni marked this conversation as resolved.
Show resolved Hide resolved
*/
export function removePrefixAndCamelCase(
jsTokenName: string,
prefix: string
): string {
return jsTokenName.replace(
new RegExp(`^${prefix}(\\w)(\\w+)`),
aagash-ni marked this conversation as resolved.
Show resolved Hide resolved
(_match: string, firstChar: string, restOfString: string) => `${firstChar.toLowerCase()}${restOfString}`
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export interface LabelProviderArgs {
tableRef: Table;
labelProviderTag: string;
labelTokens: [string, DesignToken<string>][];
removeNamePrefix(tokenName: string): string;
prefixSubstring: string;
removeNamePrefix(tokenName: string, elementName?: string): string;
updateData(args: LabelProviderArgs): void;
}

Expand Down Expand Up @@ -89,11 +90,17 @@ export const labelProviderMetadata: Meta<LabelProviderArgs> = {
table: {
disable: true
}
},
prefixSubstring: {
table: {
disable: true
}
}
},
args: {
removeNamePrefix: jsTokenName => jsTokenName,
aagash-ni marked this conversation as resolved.
Show resolved Hide resolved
tableRef: undefined,
prefixSubstring: undefined,
updateData: x => {
void (async () => {
// Safari workaround: the table element instance is made at this point
Expand All @@ -104,10 +111,10 @@ export const labelProviderMetadata: Meta<LabelProviderArgs> = {
return {
tokenName: token[0],
htmlAttributeName: getAttributeName(
x.removeNamePrefix(token[0])
x.removeNamePrefix(token[0], x.prefixSubstring)
aagash-ni marked this conversation as resolved.
Show resolved Hide resolved
),
jsPropertyName: getPropertyName(
x.removeNamePrefix(token[0])
x.removeNamePrefix(token[0], x.prefixSubstring)
),
defaultValue: token[1].getValueFor(document.body)
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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';
import { richTextLabelProvider } from '../../rich-text/tests/label-provider-rich-text.stories';

<Meta title="Tokens/Label Providers" />
<Title>Label Providers</Title>
Expand All @@ -20,10 +21,16 @@ See the [Localization section of the nimble-blazor readme](https://github.com/ni

## Core Label Provider

Provides labels for all Nimble components besides the table component.
Provides labels for all Nimble components that do not have a dedicated label provider.

<Canvas of={coreLabelProvider} sourceState="none" />

## Rich Text Label Provider

Provides labels for the the rich text components.

<Canvas of={richTextLabelProvider} sourceState="none" />

aagash-ni marked this conversation as resolved.
Show resolved Hide resolved
## Table Label Provider

Provides labels for the `nimble-table` and all associated table sub-components / column types.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { attr } from '@microsoft/fast-element';
import { DesignSystem } from '@microsoft/fast-foundation';
import { DesignTokensFor, LabelProviderBase } from '../base';
import {
richTextToggleBoldLabel,
richTextToggleItalicsLabel,
richTextToggleBulletedListLabel,
richTextToggleNumberedListLabel
} from './label-tokens';

declare global {
interface HTMLElementTagNameMap {
'nimble-label-provider-rich-text': LabelProviderRichText;
}
}

const supportedLabels = {
toggleBold: richTextToggleBoldLabel,
toggleItalics: richTextToggleItalicsLabel,
toggleBulletedList: richTextToggleBulletedListLabel,
toggleNumberedList: richTextToggleNumberedListLabel
} as const;

/**
* Label provider for the Nimble rich text component
*/
export class LabelProviderRichText
extends LabelProviderBase<typeof supportedLabels>
implements DesignTokensFor<typeof supportedLabels> {
@attr({ attribute: 'toggle-bold' })
public toggleBold: string | undefined;

@attr({ attribute: 'toggle-italics' })
public toggleItalics: string | undefined;

@attr({ attribute: 'toggle-bulleted-list' })
public toggleBulletedList: string | undefined;

@attr({ attribute: 'toggle-numbered-list' })
public toggleNumberedList: string | undefined;

protected override readonly supportedLabels = supportedLabels;
}

const nimbleLabelProviderRichText = LabelProviderRichText.compose({
baseName: 'label-provider-rich-text'
});

DesignSystem.getOrCreate()
.withPrefix('nimble')
.register(nimbleLabelProviderRichText());
export const labelProviderRichTextTag = DesignSystem.tagFor(
LabelProviderRichText
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type * as TokensNamespace from './label-tokens';

type TokenName = keyof typeof TokensNamespace;

export const richTextLabelDefaults: {
readonly [key in TokenName]: string;
} = {
richTextToggleBoldLabel: 'Bold',
richTextToggleItalicsLabel: 'Italics',
richTextToggleBulletedListLabel: 'Bulleted List',
richTextToggleNumberedListLabel: 'Numbered List'
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { DesignToken } from '@microsoft/fast-foundation';
import { richTextLabelDefaults } from './label-token-defaults';

export const richTextToggleBoldLabel = DesignToken.create<string>({
name: 'rich-text-toggle-bold-label',
cssCustomPropertyName: null
}).withDefault(richTextLabelDefaults.richTextToggleBoldLabel);

export const richTextToggleItalicsLabel = DesignToken.create<string>({
name: 'rich-text-toggle-italics-label',
cssCustomPropertyName: null
}).withDefault(richTextLabelDefaults.richTextToggleItalicsLabel);

export const richTextToggleBulletedListLabel = DesignToken.create<string>({
name: 'rich-text-toggle-bulleted-list-label',
cssCustomPropertyName: null
}).withDefault(richTextLabelDefaults.richTextToggleBulletedListLabel);

export const richTextToggleNumberedListLabel = DesignToken.create<string>({
name: 'rich-text-toggle-numbered-list-label',
cssCustomPropertyName: null
}).withDefault(richTextLabelDefaults.richTextToggleNumberedListLabel);
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { spinalCase } from '@microsoft/fast-web-utilities';
import { html } from '@microsoft/fast-element';
import * as labelTokensNamespace from '../label-tokens';
import { LabelProviderRichText, labelProviderRichTextTag } from '..';
import { getSpecTypeByNamedList } from '../../../utilities/tests/parameterized';
import {
getAttributeName,
getPropertyName,
removePrefixAndCamelCase
} from '../../base/tests/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<Fixture<ThemeProvider>> {
return fixture<ThemeProvider>(html`
<${themeProviderTag}>
<${labelProviderRichTextTag}></${labelProviderRichTextTag}>
</${themeProviderTag}>
`);
}

describe('Label Provider Rich Text', () => {
let element: LabelProviderRichText;
let themeProvider: ThemeProvider;
let connect: () => Promise<void>;
let disconnect: () => Promise<void>;

beforeEach(async () => {
({ element: themeProvider, connect, disconnect } = await setup());
element = themeProvider.querySelector(labelProviderRichTextTag)!;
await connect();
});

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

it('can construct an element instance', () => {
expect(
document.createElement('nimble-label-provider-rich-text')
).toBeInstanceOf(LabelProviderRichText);
});

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 = removePrefixAndCamelCase(
tokenEntry.name,
'richText'
);
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 = removePrefixAndCamelCase(
tokenEntry.name,
'richText'
);
const attributeName = getAttributeName(tokenName);
const updatedValue = `NewString-${tokenName}`;
element.setAttribute(attributeName, updatedValue);

expect(tokenEntry.labelToken.getValueFor(themeProvider)).toBe(
updatedValue
);
});
}
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { StoryObj } from '@storybook/html';
import {
LabelProviderArgs,
labelProviderMetadata
} from '../../base/tests/label-provider-stories-utils';
import { labelProviderRichTextTag } from '..';
import * as labelTokensNamespace from '../label-tokens';
import { removePrefixAndCamelCase } from '../../base/tests/label-name-utils';

const metadata = {
...labelProviderMetadata,
title: 'Tokens/Label Providers'
};

export default metadata;

export const richTextLabelProvider: StoryObj<LabelProviderArgs> = {
args: {
labelProviderTag: labelProviderRichTextTag,
labelTokens: Object.entries(labelTokensNamespace),
prefixSubstring: 'richText',
removeNamePrefix: removePrefixAndCamelCase
}
};

This file was deleted.

Loading