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 8 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"
}
3 changes: 2 additions & 1 deletion packages/nimble-components/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -445,10 +445,11 @@ 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:
There are currently 3 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)
- `nimble-label-provider-rich-text-editor`: Used for labels for the rich text editor
aagash-ni marked this conversation as resolved.
Show resolved Hide resolved
aagash-ni marked this conversation as resolved.
Show resolved Hide resolved

The expected format for label token names is:

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 { richTextEditorLabelProvider } from '../../rich-text-editor/tests/label-provider-rich-text-editor.stories';

<Meta title="Tokens/Label Providers" />
<Title>Label Providers</Title>
Expand All @@ -20,7 +21,7 @@ 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 besides the table and the rich text editor component.
aagash-ni marked this conversation as resolved.
Show resolved Hide resolved

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

Expand All @@ -29,3 +30,9 @@ Provides labels for all Nimble components besides the table component.
Provides labels for the `nimble-table` and all associated table sub-components / column types.

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

aagash-ni marked this conversation as resolved.
Show resolved Hide resolved
## Rich Text Editor Label Provider

Provides labels for the `nimble-rich-text-editor`.

<Canvas of={richTextEditorLabelProvider} sourceState="none" />
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 {
richTextEditorToggleBoldLabel,
richTextEditorToggleItalicsLabel,
richTextEditorToggleBulletListLabel,
richTextEditorToggleNumberedListLabel
} from './label-tokens';

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

const supportedLabels = {
toggleBold: richTextEditorToggleBoldLabel,
toggleItalics: richTextEditorToggleItalicsLabel,
toggleBulletList: richTextEditorToggleBulletListLabel,
toggleNumberedList: richTextEditorToggleNumberedListLabel
} as const;

/**
* Label provider for the Nimble rich text editor
*/
export class LabelProviderRichTextEditor
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-bullet-list' })
public toggleBulletList: string | undefined;

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

protected override readonly supportedLabels = supportedLabels;
}

const nimbleLabelProviderRichTextEditor = LabelProviderRichTextEditor.compose({
baseName: 'label-provider-rich-text-editor'
});

DesignSystem.getOrCreate()
.withPrefix('nimble')
.register(nimbleLabelProviderRichTextEditor());
export const labelProviderRichTextEditorTag = DesignSystem.tagFor(
LabelProviderRichTextEditor
);
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 richTextEditorLabelDefaults: {
readonly [key in TokenName]: string;
} = {
richTextEditorToggleBoldLabel: 'Bold',
richTextEditorToggleItalicsLabel: 'Italics',
richTextEditorToggleBulletListLabel: 'Bullet List',
aagash-ni marked this conversation as resolved.
Show resolved Hide resolved
richTextEditorToggleNumberedListLabel: 'Numbered List'
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { DesignToken } from '@microsoft/fast-foundation';
import { richTextEditorLabelDefaults } from './label-token-defaults';

aagash-ni marked this conversation as resolved.
Show resolved Hide resolved
export const richTextEditorToggleBoldLabel = DesignToken.create<string>({
name: 'rich-text-editor-toggle-bold-label',
cssCustomPropertyName: null
}).withDefault(richTextEditorLabelDefaults.richTextEditorToggleBoldLabel);

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

export const richTextEditorToggleBulletListLabel = DesignToken.create<string>({
name: 'rich-text-editor-toggle-bullet-list-label',
cssCustomPropertyName: null
}).withDefault(richTextEditorLabelDefaults.richTextEditorToggleBulletListLabel);

export const richTextEditorToggleNumberedListLabel = DesignToken.create<string>(
{
name: 'rich-text-editor-toggle-numbered-list-label',
cssCustomPropertyName: null
}
).withDefault(
richTextEditorLabelDefaults.richTextEditorToggleNumberedListLabel
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Removes the richTextEditor prefix and camelCases the input token name.
* (The design token name has the richTextEditor prefix, but the properties on the LabelProviderRichTextEditor do not, as they're already
* scoped to the rich-text-editor only)
*/
export function removeRichTextEditorPrefixAndCamelCase(
jsTokenName: string
): string {
// Example: 'richTextEditorToggleBulletListLabel' => 'toggleBulletListLabel'
return jsTokenName.replace(
/^richTextEditor(\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
@@ -0,0 +1,140 @@
import { spinalCase } from '@microsoft/fast-web-utilities';
import { html } from '@microsoft/fast-element';
import * as labelTokensNamespace from '../label-tokens';
import {
LabelProviderRichTextEditor,
labelProviderRichTextEditorTag
} from '..';
import { getSpecTypeByNamedList } from '../../../utilities/tests/parameterized';
import {
getAttributeName,
getPropertyName
} from '../../base/tests/label-name-utils';
import { ThemeProvider, themeProviderTag } from '../../../theme-provider';
import { Fixture, fixture } from '../../../utilities/tests/fixture';
import { removeRichTextEditorPrefixAndCamelCase } from '../name-utils';

type DesignTokenPropertyName = keyof typeof labelTokensNamespace;
const designTokenPropertyNames = Object.keys(
labelTokensNamespace
) as DesignTokenPropertyName[];

async function setup(): Promise<Fixture<ThemeProvider>> {
return fixture<ThemeProvider>(html`
<${themeProviderTag}>
<${labelProviderRichTextEditorTag}></${labelProviderRichTextEditorTag}>
</${themeProviderTag}>
`);
}

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

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

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

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

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 = removeRichTextEditorPrefixAndCamelCase(
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 = removeRichTextEditorPrefixAndCamelCase(
tokenEntry.name
);
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,23 @@
import type { StoryObj } from '@storybook/html';
import {
LabelProviderArgs,
labelProviderMetadata
} from '../../base/tests/label-provider-stories-utils';
import { labelProviderRichTextEditorTag } from '..';
import * as labelTokensNamespace from '../label-tokens';
import { removeRichTextEditorPrefixAndCamelCase } from '../name-utils';

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

export default metadata;

export const richTextEditorLabelProvider: StoryObj<LabelProviderArgs> = {
args: {
labelProviderTag: labelProviderRichTextEditorTag,
labelTokens: Object.entries(labelTokensNamespace),
removeNamePrefix: removeRichTextEditorPrefixAndCamelCase
}
};
22 changes: 14 additions & 8 deletions packages/nimble-components/src/rich-text-editor/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import { iconBoldBTag } from '../icons/bold-b';
import { iconItalicITag } from '../icons/italic-i';
import { iconListTag } from '../icons/list';
import { iconNumberListTag } from '../icons/number-list';
import {
richTextEditorToggleBoldLabel,
richTextEditorToggleItalicsLabel,
richTextEditorToggleBulletListLabel,
richTextEditorToggleNumberedListLabel
} from '../label-provider/rich-text-editor/label-tokens';

// prettier-ignore
export const template = html<RichTextEditor>`
Expand All @@ -20,51 +26,51 @@ export const template = html<RichTextEditor>`
appearance="ghost"
content-hidden
slot="start"
title="Bold"
title=${x => richTextEditorToggleBoldLabel.getValueFor(x)}
@click=${x => x.boldButtonClick()}
@change=${(x, c) => x.stopEventPropagation(c.event)}
@keydown=${(x, c) => x.boldButtonKeyDown(c.event as KeyboardEvent)}
>
Bold
${x => richTextEditorToggleBoldLabel.getValueFor(x)}
<${iconBoldBTag} slot="start"></${iconBoldBTag}>
</${toggleButtonTag}>
<${toggleButtonTag}
${ref('italicsButton')}
appearance="ghost"
content-hidden
slot="start"
title="Italics"
title=${x => richTextEditorToggleItalicsLabel.getValueFor(x)}
@click=${x => x.italicsButtonClick()}
@change=${(x, c) => x.stopEventPropagation(c.event)}
@keydown=${(x, c) => x.italicsButtonKeyDown(c.event as KeyboardEvent)}
>
Italics
${x => richTextEditorToggleItalicsLabel.getValueFor(x)}
<${iconItalicITag} slot="start"></${iconItalicITag}>
</${toggleButtonTag}>
<${toggleButtonTag}
${ref('bulletListButton')}
appearance="ghost"
content-hidden
slot="start"
title="Bullet List"
title=${x => richTextEditorToggleBulletListLabel.getValueFor(x)}
@click=${x => x.bulletListButtonClick()}
@change=${(x, c) => x.stopEventPropagation(c.event)}
@keydown=${(x, c) => x.bulletListButtonKeyDown(c.event as KeyboardEvent)}
aagash-ni marked this conversation as resolved.
Show resolved Hide resolved
>
Bullet List
${x => richTextEditorToggleBulletListLabel.getValueFor(x)}
<${iconListTag} slot="start"></${iconListTag}>
</${toggleButtonTag}>
<${toggleButtonTag}
${ref('numberedListButton')}
appearance="ghost"
content-hidden
slot="start"
title="Numbered List"
title=${x => richTextEditorToggleNumberedListLabel.getValueFor(x)}
@click=${x => x.numberedListButtonClick()}
@change=${(x, c) => x.stopEventPropagation(c.event)}
@keydown=${(x, c) => x.numberedListButtonKeyDown(c.event as KeyboardEvent)}
>
Numbered List
${x => richTextEditorToggleNumberedListLabel.getValueFor(x)}
<${iconNumberListTag} slot="start"></${iconNumberListTag}>
</${toggleButtonTag}>
</${toolbarTag}>
Expand Down
Loading