diff --git a/packages/nimble-components/src/banner/tests/banner-matrix.stories.ts b/packages/nimble-components/src/banner/tests/banner-matrix.stories.ts
index d5e8cbdbbf..0d8fcdf28c 100644
--- a/packages/nimble-components/src/banner/tests/banner-matrix.stories.ts
+++ b/packages/nimble-components/src/banner/tests/banner-matrix.stories.ts
@@ -1,4 +1,4 @@
-import type { Story, Meta } from '@storybook/html';
+import type { StoryFn, Meta } from '@storybook/html';
import { html, ViewTemplate, when } from '@microsoft/fast-element';
import { pascalCase } from '@microsoft/fast-web-utilities';
import { createStory } from '../../utilities/tests/storybook';
@@ -81,7 +81,7 @@ const component = (
`;
-export const bannerThemeMatrix: Story = createMatrixThemeStory(
+export const bannerThemeMatrix: StoryFn = createMatrixThemeStory(
createMatrix(component, [
severityStates,
actionStates,
@@ -90,7 +90,7 @@ export const bannerThemeMatrix: Story = createMatrixThemeStory(
])
);
-export const hiddenBanner: Story = createStory(
+export const hiddenBanner: StoryFn = createStory(
hiddenWrapper(
html`<${bannerTag} hidden>
Hidden banner
diff --git a/packages/nimble-components/src/breadcrumb-item/styles.ts b/packages/nimble-components/src/breadcrumb-item/styles.ts
index f65b78859d..091265edf3 100644
--- a/packages/nimble-components/src/breadcrumb-item/styles.ts
+++ b/packages/nimble-components/src/breadcrumb-item/styles.ts
@@ -61,6 +61,10 @@ export const styles = css`
display: none;
}
+ .content {
+ pointer-events: none;
+ }
+
[part='end'] {
display: none;
}
diff --git a/packages/nimble-components/src/button/tests/button-matrix.stories.ts b/packages/nimble-components/src/button/tests/button-matrix.stories.ts
index 353dd05d6c..fc0e33694d 100644
--- a/packages/nimble-components/src/button/tests/button-matrix.stories.ts
+++ b/packages/nimble-components/src/button/tests/button-matrix.stories.ts
@@ -1,7 +1,5 @@
import type { StoryFn, Meta } from '@storybook/html';
import { html, ViewTemplate, when } from '@microsoft/fast-element';
-import { pascalCase } from '@microsoft/fast-web-utilities';
-import { ButtonAppearance, ButtonAppearanceVariant } from '../types';
import {
createMatrix,
sharedMatrixParameters,
@@ -15,6 +13,14 @@ import { buttonTag } from '..';
import { iconKeyTag } from '../../icons/key';
import { iconArrowExpanderDownTag } from '../../icons/arrow-expander-down';
import { bodyFont } from '../../theme-provider/design-tokens';
+import {
+ appearanceStates,
+ type AppearanceState,
+ type AppearanceVariantState,
+ type PartVisibilityState,
+ appearanceVariantStates,
+ partVisibilityStates
+} from '../../patterns/button/tests/states';
const metadata: Meta = {
title: 'Tests/Button',
@@ -25,26 +31,6 @@ const metadata: Meta = {
export default metadata;
-/* array of iconVisible, labelVisible, endIconVisible */
-const partVisibilityStates = [
- [true, true, false],
- [true, false, false],
- [false, true, false],
- [true, true, true],
- [false, true, true]
-] as const;
-type PartVisibilityState = (typeof partVisibilityStates)[number];
-
-const appearanceStates: [string, string | undefined][] = Object.entries(
- ButtonAppearance
-).map(([key, value]) => [pascalCase(key), value]);
-type AppearanceState = (typeof appearanceStates)[number];
-
-const appearanceVariantStates: [string, string | undefined][] = Object.entries(
- ButtonAppearanceVariant
-).map(([key, value]) => [pascalCase(key), value]);
-type AppearanceVariantState = (typeof appearanceVariantStates)[number];
-
// prettier-ignore
const component = (
[disabledName, disabled]: DisabledState,
diff --git a/packages/nimble-components/src/button/tests/button.mdx b/packages/nimble-components/src/button/tests/button.mdx
index e8a22b9ca6..bc9156eb54 100644
--- a/packages/nimble-components/src/button/tests/button.mdx
+++ b/packages/nimble-components/src/button/tests/button.mdx
@@ -1,7 +1,6 @@
import { Canvas, Meta, Controls, Title } from '@storybook/blocks';
import ContentHiddenDocs from '../../patterns/button/tests/content-hidden-docs.mdx';
-import { NimbleButton } from './button.react';
-import { NimbleIconKey } from '../../icons/tests/key.react';
+import StylingDocs from '../../patterns/button/tests/styling-docs.mdx';
import { buttonTag } from '..';
import { anchorButtonTag } from '../../anchor-button';
import * as buttonStories from './button.stories';
@@ -18,209 +17,12 @@ If you want a button that triggers navigation to a URL, use the
-## Styling
-
-### Appearances
-
-These appearances have the default styling of the
. Each should be considered for use before using appearance variant buttons.
-
-#### Ghost Button:
-
-
-
-
- {`Ghost Button`}
-
-
- {`Ghost Button`}
-
-
-
-
-
- Ghost is the default appearance and should be the first considered for use.
-
- Use as the default and standard option to create a clean airy and open UI
- feel. Ghost buttons fit comfortably in tight spaces and help control the
- visual density of the UI.
-
-
- Be careful when using that the surrounding context does not cause this
- button to be confused for emphasized body text, tabs or a standalone links.
-
- Use in combination with a primary outline or primary block buttons to create a hierarchy of importance. There is no primary ghost button.
-
-
-
-
-
-
-#### Outline Button:
-
-
-
-
- {`Outline Button`}
-
-
- {`Outline Button`}
-
-
-
-
-
- Outline is the secondary style and should be considered for
- use when ghost button is not sufficient.
-
-
- Use as an alternative standard button when a ghost button is
- not suitable. Use like a ghost button to create a clean,
- light and airy feel.
-
-
- The outline button is more visually direct about the
- control's functionality than a ghost button.
-
-
- Use in combination with ghost buttons (but not block
- buttons) to create hierarchy.
-
-
-
-
-
-
-#### Block Button:
-
-
-
-
- {`Block Button`}
-
-
- {`Block Button`}
-
-
-
-
-
- Block is the tertiary style used for creating the most eye
- catching and functionally direct button. Use in areas where
- controls are not often present or obvious, or when lots of
- busy information can cause an important control to be
- overlooked.
-
-
- Use as a standard button when the most visible solution is
- required. Use as an alternative to overly subtle button
- solutions when it is important to emphasize an action and
- the functionality of the control.
-
-
- Use in combination with ghost buttons (but not outline
- buttons) to create hierarchy.
-
-
-
-
-
-
-### Appearance Variants
-
-Button appearance variants are mainly used when a button needs be distinguished for one of the following reasons:
-
-- To indicate the action that allows the user to accomplish their most common or important goal
-- To indicate the action that allows the user to complete their task
-
-There are currently two available values for `appearance-variant`: `primary` and `accent`.
-
-#### Accent Button:
-
-
-
-
- {`Ghost`}
- {`Ghost`}
- {`Outline Accent`}
-
- {`Ghost`}
- {`Ghost`}
- {`Block Accent`}
-
-
-
- Use only 1 accent button in a section. It should be used when
- trying to achieve the most prominent eye-catching approach.
- Consider the contextual implications of using a green colored
- button in relation to its name or metaphor. Remember that color
- has accessibility constraints and the color alone should not be
- relied on.
-
-
- Use in situations that lack color and enthusiasm to help support
- the brand.
-
- Use in combination with ghost buttons to create hierarchy.
-
- Do not use an outline button with a block button to create
- hierarchy.
-
-
-
-
-
-#### Primary Button:
-
-
-
-
- {`Ghost`}
- {`Ghost`}
-
- {`Outline Primary`}
-
-
- {`Ghost`}
- {`Ghost`}
-
- {`Block Primary`}
-
-
-
-
- Use only 1 primary button in a section. It should be used when
- there is a conflict with color and its context.
-
- Use in combination with ghost buttons to create hierarchy.
-
- Do not use an outline button with a block button to create
- hierarchy.
-
-
-
-
-
-
-#### Examples:
-
-To see more examples of appearance variant button hierarchy, see the nimble-button Figma docs for [Primary and Standard Actions](https://www.figma.com/file/PO9mFOu5BCl8aJvFchEeuN/Nimble_Components?type=design&node-id=1604-74603&mode=design&t=SQ3lyK83VHBUaOkg-0).
+
## Sizing
Nimble Buttons are currently always 32px tall. Designs exist for other sizes; if you need these in an application, please comment on [Configurable height for nimble controls (#610)](https://github.com/ni/nimble/issues/610).
-## Styling / Theme
-
-Use the Color UI version for backgrounds with color (e.g. purple, blue).
-
-See the usage details for more information on button styling / usage.
-
## Accessibility
Please work with your designer and ensure you have a 4.5:1
diff --git a/packages/nimble-components/src/card/tests/card.mdx b/packages/nimble-components/src/card/tests/card.mdx
index 4113b8eb7d..a5c8085597 100644
--- a/packages/nimble-components/src/card/tests/card.mdx
+++ b/packages/nimble-components/src/card/tests/card.mdx
@@ -6,8 +6,8 @@ import * as cardStories from './card.stories';
-The is a container that is designed to contain arbitrary content that is specified by a client
-application. The is intended for grouping related content.
+The is a container that is designed to contain arbitrary content that is specified by a client
+application. The is intended for grouping related content.
diff --git a/packages/nimble-components/src/combobox/tests/combobox.mdx b/packages/nimble-components/src/combobox/tests/combobox.mdx
index 254ea40c8e..0870896932 100644
--- a/packages/nimble-components/src/combobox/tests/combobox.mdx
+++ b/packages/nimble-components/src/combobox/tests/combobox.mdx
@@ -19,11 +19,11 @@ import { listOptionTag } from '../../list-option/';
### Native element and Blazor
-The value of the combobox comes from the text content of the selected autocomplete , or, if no matching autocomplete is found, the value is the user-entered text.
+The value of the combobox comes from the text content of the selected autocomplete , or, if no matching autocomplete is found, the value is the user-entered text.
### Angular
-In Angular, an autocomplete can be created with an `ngValue`. In that case the `ngModel` of the combobox will be the `ngValue` of the matched autocomplete option.
+In Angular, an autocomplete can be created with an `ngValue`. In that case the `ngModel` of the combobox will be the `ngValue` of the matched autocomplete option.
If no matching autocomplete option is found, the `ngModel` of the combobox will be set to `OPTION_NOT_FOUND`, and if the application needs to access the user-entered text, that should be done
through the `value` property on the `NimbleComboboxDirective`.
diff --git a/packages/nimble-components/src/combobox/tests/combobox.spec.ts b/packages/nimble-components/src/combobox/tests/combobox.spec.ts
index 242d147c33..15973059fc 100644
--- a/packages/nimble-components/src/combobox/tests/combobox.spec.ts
+++ b/packages/nimble-components/src/combobox/tests/combobox.spec.ts
@@ -2,7 +2,6 @@ import { html, repeat } from '@microsoft/fast-element';
import { keyArrowDown, keyEnter } from '@microsoft/fast-web-utilities';
import { fixture, Fixture } from '../../utilities/tests/fixture';
import { Combobox, comboboxTag } from '..';
-import { listOptionTag } from '../../list-option';
import { ComboboxAutocomplete } from '../types';
import { waitForUpdatesAsync } from '../../testing/async-helpers';
import {
@@ -10,6 +9,7 @@ import {
waitAnimationFrame
} from '../../utilities/tests/component';
import { checkFullyInViewport } from '../../utilities/tests/intersection-observer';
+import { listOptionTag } from '../../list-option';
async function setup(
position?: string,
diff --git a/packages/nimble-components/src/dialog/tests/dialog.spec.ts b/packages/nimble-components/src/dialog/tests/dialog.spec.ts
index 1dc9f0f293..89a81e6cdc 100644
--- a/packages/nimble-components/src/dialog/tests/dialog.spec.ts
+++ b/packages/nimble-components/src/dialog/tests/dialog.spec.ts
@@ -272,8 +272,8 @@ describe('Dialog', () => {
await disconnect();
});
- // Firefox skipped, see: https://github.com/ni/nimble/issues/1075
- it('focuses the first button on the dialog when it opens #SkipFirefox', async () => {
+ // Some browsers skipped, see: https://github.com/ni/nimble/issues/1936
+ it('focuses the first button on the dialog when it opens #SkipFirefox #SkipWebkit', async () => {
const { element, connect, disconnect } = await setup();
await connect();
const okButton = document.getElementById('ok')!;
@@ -285,8 +285,8 @@ describe('Dialog', () => {
await disconnect();
});
- // Firefox skipped, see: https://github.com/ni/nimble/issues/1075
- it('focuses the button with autofocus when the dialog opens #SkipFirefox', async () => {
+ // Some browsers skipped, see: https://github.com/ni/nimble/issues/1936
+ it('focuses the button with autofocus when the dialog opens #SkipFirefox #SkipWebkit', async () => {
const { element, connect, disconnect } = await setup();
await connect();
const cancelButton = document.getElementById('cancel')!;
@@ -300,8 +300,8 @@ describe('Dialog', () => {
await disconnect();
});
- // Firefox skipped, see: https://github.com/ni/nimble/issues/1075
- it('supports opening multiple dialogs on top of each other #SkipFirefox', async () => {
+ // Some browsers skipped, see: https://github.com/ni/nimble/issues/1943
+ it('supports opening multiple dialogs on top of each other #SkipFirefox #SkipWebkit', async () => {
const { element, connect, disconnect } = await setup();
await connect();
const secondDialog = document.createElement('nimble-dialog');
diff --git a/packages/nimble-components/src/drawer/tests/drawer.spec.ts b/packages/nimble-components/src/drawer/tests/drawer.spec.ts
index 99a99ff98f..eefa3b0f52 100644
--- a/packages/nimble-components/src/drawer/tests/drawer.spec.ts
+++ b/packages/nimble-components/src/drawer/tests/drawer.spec.ts
@@ -133,7 +133,7 @@ describe('Drawer', () => {
await expectAsync(promise).toBePending();
});
- // Firefox skipped, see: https://github.com/ni/nimble/issues/1075
+ // Firefox skipped, see: https://github.com/ni/nimble/issues/1937
it('should resolve promise if drawer completely opens before being closed #SkipFirefox', async () => {
const promise = element.show();
await completeAnimationAsync(element);
@@ -235,15 +235,15 @@ describe('Drawer', () => {
expect(afterDrawerCloseActiveElement).toBe(button2);
});
- // Firefox skipped, see: https://github.com/ni/nimble/issues/1075
- it('focuses the first button on the drawer when it opens #SkipFirefox', () => {
+ // Some browsers skipped, see: https://github.com/ni/nimble/issues/1936
+ it('focuses the first button on the drawer when it opens #SkipFirefox #SkipWebkit', () => {
const okButton = document.getElementById('ok')!;
void element.show();
expect(document.activeElement).toBe(okButton);
});
- // Firefox skipped, see: https://github.com/ni/nimble/issues/1075
- it('focuses the button with autofocus when the drawer opens #SkipFirefox', () => {
+ // Some browsers skipped, see: https://github.com/ni/nimble/issues/1936
+ it('focuses the button with autofocus when the drawer opens #SkipFirefox #SkipWebkit', () => {
const cancelButton = document.getElementById('cancel')!;
cancelButton.setAttribute('autofocus', '');
processUpdates();
@@ -251,8 +251,8 @@ describe('Drawer', () => {
expect(document.activeElement).toBe(cancelButton);
});
- // Firefox skipped, see: https://github.com/ni/nimble/issues/1075
- it('supports opening multiple drawers on top of each other #SkipFirefox', () => {
+ // Some browsers skipped, see: https://github.com/ni/nimble/issues/1943
+ it('supports opening multiple drawers on top of each other #SkipFirefox #SkipWebkit', () => {
const secondDrawer = document.createElement('nimble-drawer');
const secondDrawerButton = document.createElement('nimble-button');
secondDrawer.append(secondDrawerButton);
diff --git a/packages/nimble-components/src/icon-base/tests/icons.stories.ts b/packages/nimble-components/src/icon-base/tests/icons.stories.ts
index d59f18b9bd..ed53a74c52 100644
--- a/packages/nimble-components/src/icon-base/tests/icons.stories.ts
+++ b/packages/nimble-components/src/icon-base/tests/icons.stories.ts
@@ -56,6 +56,7 @@ const updateData = (tableRef: Table): void => {
// 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');
+ tableRef.style.setProperty('--data-length', data.length.toString());
await tableRef.setData(data);
})();
};
@@ -82,7 +83,7 @@ export const icons: StoryObj = {
<${tableTag}
${ref('tableRef')}
${/* Make the table big enough to remove vertical scrollbar */ ''}
- style="height: 6325px;"
+ style="height: calc((34px * var(--data-length)) + 32px);"
data-unused="${x => updateData(x.tableRef)}"
>
<${tableColumnIconTag} field-name="tag" key-type="string">
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 c439cfaba4..f8fb5f4144 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
@@ -63,7 +63,7 @@ export const labelProviderMetadata: Meta = {
}
${tableTag} {
/* Make the table big enough to remove vertical scrollbar */
- height: 550px;
+ height: calc((34px * var(--data-length)) + 32px);
}
${x => createTemplate(x.labelProviderTag)}
@@ -123,6 +123,10 @@ export const labelProviderMetadata: Meta = {
defaultValue: token[1].getValueFor(document.body)
};
});
+ x.tableRef.style.setProperty(
+ '--data-length',
+ data.length.toString()
+ );
await x.tableRef.setData(data);
})();
}
diff --git a/packages/nimble-components/src/label-provider/core/index.ts b/packages/nimble-components/src/label-provider/core/index.ts
index f7c901dc12..b0a872a5e7 100644
--- a/packages/nimble-components/src/label-provider/core/index.ts
+++ b/packages/nimble-components/src/label-provider/core/index.ts
@@ -5,9 +5,9 @@ import {
popupDismissLabel,
numericDecrementLabel,
numericIncrementLabel,
- errorIconLabel,
- warningIconLabel,
- informationIconLabel,
+ popupIconErrorLabel,
+ popupIconWarningLabel,
+ popupIconInformationLabel,
filterSearchLabel,
filterNoResultsLabel
} from './label-tokens';
@@ -22,9 +22,9 @@ const supportedLabels = {
popupDismiss: popupDismissLabel,
numericDecrement: numericDecrementLabel,
numericIncrement: numericIncrementLabel,
- errorIcon: errorIconLabel,
- warningIcon: warningIconLabel,
- informationIcon: informationIconLabel,
+ popupIconError: popupIconErrorLabel,
+ popupIconWarning: popupIconWarningLabel,
+ popupIconInformation: popupIconInformationLabel,
filterSearch: filterSearchLabel,
filterNoResults: filterNoResultsLabel
} as const;
@@ -44,14 +44,14 @@ export class LabelProviderCore
@attr({ attribute: 'numeric-increment' })
public numericIncrement: string | undefined;
- @attr({ attribute: 'error-icon' })
- public errorIcon: string | undefined;
+ @attr({ attribute: 'popup-icon-error' })
+ public popupIconError: string | undefined;
- @attr({ attribute: 'warning-icon' })
- public warningIcon: string | undefined;
+ @attr({ attribute: 'popup-icon-warning' })
+ public popupIconWarning: string | undefined;
- @attr({ attribute: 'information-icon' })
- public informationIcon: string | undefined;
+ @attr({ attribute: 'popup-icon-information' })
+ public popupIconInformation: string | undefined;
@attr({ attribute: 'filter-search' })
public filterSearch: string | undefined;
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
index 91ed6e5993..b555c28f27 100644
--- a/packages/nimble-components/src/label-provider/core/label-token-defaults.ts
+++ b/packages/nimble-components/src/label-provider/core/label-token-defaults.ts
@@ -6,9 +6,9 @@ export const coreLabelDefaults: { readonly [key in TokenName]: string } = {
popupDismissLabel: 'Close',
numericIncrementLabel: 'Increment',
numericDecrementLabel: 'Decrement',
- errorIconLabel: 'Error',
- warningIconLabel: 'Warning',
- informationIconLabel: 'Information',
+ popupIconErrorLabel: 'Error',
+ popupIconWarningLabel: 'Warning',
+ popupIconInformationLabel: 'Information',
filterSearchLabel: 'Search',
filterNoResultsLabel: 'No items found'
};
diff --git a/packages/nimble-components/src/label-provider/core/label-tokens.ts b/packages/nimble-components/src/label-provider/core/label-tokens.ts
index 2cfe2debce..2f6f518ec5 100644
--- a/packages/nimble-components/src/label-provider/core/label-tokens.ts
+++ b/packages/nimble-components/src/label-provider/core/label-tokens.ts
@@ -16,20 +16,20 @@ export const numericIncrementLabel = DesignToken.create({
cssCustomPropertyName: null
}).withDefault(coreLabelDefaults.numericIncrementLabel);
-export const errorIconLabel = DesignToken.create({
- name: 'error-icon-label',
+export const popupIconErrorLabel = DesignToken.create({
+ name: 'popup-icon-error-label',
cssCustomPropertyName: null
-}).withDefault(coreLabelDefaults.errorIconLabel);
+}).withDefault(coreLabelDefaults.popupIconErrorLabel);
-export const warningIconLabel = DesignToken.create({
- name: 'warning-icon-label',
+export const popupIconWarningLabel = DesignToken.create({
+ name: 'popup-icon-warning-label',
cssCustomPropertyName: null
-}).withDefault(coreLabelDefaults.warningIconLabel);
+}).withDefault(coreLabelDefaults.popupIconWarningLabel);
-export const informationIconLabel = DesignToken.create({
- name: 'information-icon-label',
+export const popupIconInformationLabel = DesignToken.create({
+ name: 'popup-icon-information-label',
cssCustomPropertyName: null
-}).withDefault(coreLabelDefaults.informationIconLabel);
+}).withDefault(coreLabelDefaults.popupIconInformationLabel);
export const filterSearchLabel = DesignToken.create({
name: 'filter-search-label',
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 6eff6fb29a..046fca9c22 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
@@ -1,4 +1,4 @@
-import type { StoryObj } from '@storybook/html';
+import type { StoryObj, Meta } from '@storybook/html';
import {
LabelProviderArgs,
labelProviderMetadata
@@ -6,7 +6,7 @@ import {
import { labelProviderCoreTag } from '..';
import * as labelTokensNamespace from '../label-tokens';
-const metadata = {
+const metadata: Meta = {
...labelProviderMetadata,
title: 'Tokens/Label Providers'
};
diff --git a/packages/nimble-components/src/label-provider/rich-text/tests/label-provider-rich-text.stories.ts b/packages/nimble-components/src/label-provider/rich-text/tests/label-provider-rich-text.stories.ts
index 6f4527ec25..12c436b58c 100644
--- a/packages/nimble-components/src/label-provider/rich-text/tests/label-provider-rich-text.stories.ts
+++ b/packages/nimble-components/src/label-provider/rich-text/tests/label-provider-rich-text.stories.ts
@@ -1,4 +1,4 @@
-import type { StoryObj } from '@storybook/html';
+import type { StoryObj, Meta } from '@storybook/html';
import {
LabelProviderArgs,
labelProviderMetadata
@@ -6,7 +6,7 @@ import {
import { labelProviderRichTextTag } from '..';
import * as labelTokensNamespace from '../label-tokens';
-const metadata = {
+const metadata: Meta = {
...labelProviderMetadata,
title: 'Tokens/Label Providers'
};
diff --git a/packages/nimble-components/src/label-provider/table/index.ts b/packages/nimble-components/src/label-provider/table/index.ts
index 8081845910..20668452e6 100644
--- a/packages/nimble-components/src/label-provider/table/index.ts
+++ b/packages/nimble-components/src/label-provider/table/index.ts
@@ -15,7 +15,9 @@ import {
tableRowOperationColumnLabel,
tableRowSelectLabel,
tableSelectAllLabel,
- tableRowLoadingLabel
+ tableRowLoadingLabel,
+ tableGroupRowPlaceholderEmptyLabel,
+ tableGroupRowPlaceholderNoValueLabel
} from './label-tokens';
declare global {
@@ -38,7 +40,9 @@ const supportedLabels = {
groupSelectAll: tableGroupSelectAllLabel,
rowSelect: tableRowSelectLabel,
rowOperationColumn: tableRowOperationColumnLabel,
- rowLoading: tableRowLoadingLabel
+ rowLoading: tableRowLoadingLabel,
+ groupRowPlaceholderNoValue: tableGroupRowPlaceholderNoValueLabel,
+ groupRowPlaceholderEmpty: tableGroupRowPlaceholderEmptyLabel
} as const;
/**
@@ -89,6 +93,12 @@ export class LabelProviderTable
@attr({ attribute: 'row-loading' })
public rowLoading: string | undefined;
+ @attr({ attribute: 'group-row-placeholder-no-value' })
+ public groupRowPlaceholderNoValue: string | undefined;
+
+ @attr({ attribute: 'group-row-placeholder-empty' })
+ public groupRowPlaceholderEmpty: string | undefined;
+
protected override readonly supportedLabels = supportedLabels;
}
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
index 79b553c018..ff243302a7 100644
--- a/packages/nimble-components/src/label-provider/table/label-token-defaults.ts
+++ b/packages/nimble-components/src/label-provider/table/label-token-defaults.ts
@@ -16,5 +16,7 @@ export const tableLabelDefaults: { readonly [key in TokenName]: string } = {
tableGroupSelectAllLabel: 'Select all rows in group',
tableRowSelectLabel: 'Select row',
tableRowOperationColumnLabel: 'Row operations',
- tableRowLoadingLabel: 'Loading'
+ tableRowLoadingLabel: 'Loading',
+ tableGroupRowPlaceholderNoValueLabel: 'No value',
+ tableGroupRowPlaceholderEmptyLabel: 'Empty'
};
diff --git a/packages/nimble-components/src/label-provider/table/label-tokens.ts b/packages/nimble-components/src/label-provider/table/label-tokens.ts
index 26edb26e30..e7d8bffb19 100644
--- a/packages/nimble-components/src/label-provider/table/label-tokens.ts
+++ b/packages/nimble-components/src/label-provider/table/label-tokens.ts
@@ -72,3 +72,13 @@ export const tableRowLoadingLabel = DesignToken.create({
name: 'table-row-loading-label',
cssCustomPropertyName: null
}).withDefault(tableLabelDefaults.tableRowLoadingLabel);
+
+export const tableGroupRowPlaceholderNoValueLabel = DesignToken.create({
+ name: 'table-group-row-placeholder-no-value-label',
+ cssCustomPropertyName: null
+}).withDefault(tableLabelDefaults.tableGroupRowPlaceholderNoValueLabel);
+
+export const tableGroupRowPlaceholderEmptyLabel = DesignToken.create({
+ name: 'table-group-row-placeholder-empty-label',
+ cssCustomPropertyName: null
+}).withDefault(tableLabelDefaults.tableGroupRowPlaceholderEmptyLabel);
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 5d97a08dfa..6ccabbece5 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
@@ -1,4 +1,4 @@
-import type { StoryObj } from '@storybook/html';
+import type { StoryObj, Meta } from '@storybook/html';
import {
LabelProviderArgs,
labelProviderMetadata
@@ -6,7 +6,7 @@ import {
import { labelProviderTableTag } from '..';
import * as labelTokensNamespace from '../label-tokens';
-const metadata = {
+const metadata: Meta = {
...labelProviderMetadata,
title: 'Tokens/Label Providers'
};
diff --git a/packages/nimble-components/src/list-option/index.ts b/packages/nimble-components/src/list-option/index.ts
index 724e564514..6f60d343e2 100644
--- a/packages/nimble-components/src/list-option/index.ts
+++ b/packages/nimble-components/src/list-option/index.ts
@@ -5,6 +5,7 @@ import {
import { observable, attr } from '@microsoft/fast-element';
import { styles } from './styles';
import { template } from './template';
+import type { ListOptionOwner } from '../patterns/dropdown/types';
declare global {
interface HTMLElementTagNameMap {
@@ -51,6 +52,23 @@ export class ListOption extends FoundationListboxOption {
.map(node => node.textContent?.trim())
.join(' ');
}
+
+ public override connectedCallback(): void {
+ super.connectedCallback();
+ if (this.isListOptionOwner(this.parentElement)) {
+ this.parentElement.registerOption(this);
+ }
+ }
+
+ private isListOptionOwner(
+ parent: HTMLElement | null
+ ): parent is ListOptionOwner {
+ if (!parent) {
+ return false;
+ }
+
+ return typeof (parent as ListOptionOwner).registerOption === 'function';
+ }
}
const nimbleListOption = ListOption.compose({
diff --git a/packages/nimble-components/src/menu-button/index.ts b/packages/nimble-components/src/menu-button/index.ts
index de3bbef6d3..d0fec46402 100644
--- a/packages/nimble-components/src/menu-button/index.ts
+++ b/packages/nimble-components/src/menu-button/index.ts
@@ -6,11 +6,15 @@ import {
keyArrowUp,
keyEscape
} from '@microsoft/fast-web-utilities';
-import { ButtonAppearance } from '../button/types';
+import {
+ ButtonAppearance,
+ ButtonAppearanceVariant,
+ MenuButtonToggleEventDetail,
+ MenuButtonPosition
+} from './types';
import type { ToggleButton } from '../toggle-button';
import { styles } from './styles';
import { template } from './template';
-import { MenuButtonToggleEventDetail, MenuButtonPosition } from './types';
import type { ButtonPattern } from '../patterns/button/types';
import type { AnchoredRegion } from '../anchored-region';
@@ -27,6 +31,9 @@ export class MenuButton extends FoundationElement implements ButtonPattern {
@attr
public appearance: ButtonAppearance = ButtonAppearance.outline;
+ @attr({ attribute: 'appearance-variant' })
+ public appearanceVariant: ButtonAppearanceVariant;
+
@attr({ mode: 'boolean' })
public disabled = false;
diff --git a/packages/nimble-components/src/menu-button/template.ts b/packages/nimble-components/src/menu-button/template.ts
index a32ccc5659..891527ec98 100644
--- a/packages/nimble-components/src/menu-button/template.ts
+++ b/packages/nimble-components/src/menu-button/template.ts
@@ -12,6 +12,7 @@ export const template = html`
<${toggleButtonTag}
part="button"
appearance="${x => x.appearance}"
+ appearance-variant="${x => x.appearanceVariant}"
?content-hidden="${x => x.contentHidden}"
?checked="${x => x.open}"
?disabled="${x => x.disabled}"
diff --git a/packages/nimble-components/src/menu-button/tests/menu-button-matrix.stories.ts b/packages/nimble-components/src/menu-button/tests/menu-button-matrix.stories.ts
index 3b9de718a7..14f7d570cd 100644
--- a/packages/nimble-components/src/menu-button/tests/menu-button-matrix.stories.ts
+++ b/packages/nimble-components/src/menu-button/tests/menu-button-matrix.stories.ts
@@ -1,7 +1,5 @@
import type { StoryFn, Meta } from '@storybook/html';
import { html, ViewTemplate, when } from '@microsoft/fast-element';
-import { pascalCase } from '@microsoft/fast-web-utilities';
-import { ButtonAppearance } from '../types';
import {
createMatrix,
sharedMatrixParameters,
@@ -15,6 +13,14 @@ import { iconKeyTag } from '../../icons/key';
import { menuButtonTag } from '..';
import { menuTag } from '../../menu';
import { menuItemTag } from '../../menu-item';
+import {
+ appearanceStates,
+ type AppearanceState,
+ type AppearanceVariantState,
+ type PartVisibilityState,
+ appearanceVariantStates,
+ partVisibilityStates
+} from '../../patterns/button/tests/states';
const metadata: Meta = {
title: 'Tests/Menu Button',
@@ -25,34 +31,21 @@ const metadata: Meta = {
export default metadata;
-/* array of iconVisible, labelVisible, endIconVisible */
-const partVisibilityStates = [
- [true, true, false],
- [true, false, false],
- [false, true, false],
- [true, true, true],
- [false, true, true]
-] as const;
-type PartVisibilityState = (typeof partVisibilityStates)[number];
-
-const appearanceStates = Object.entries(ButtonAppearance).map(
- ([key, value]) => [pascalCase(key), value]
-);
-type AppearanceState = (typeof appearanceStates)[number];
-
// prettier-ignore
const component = (
[iconVisible, labelVisible, endIconVisible]: PartVisibilityState,
[disabledName, disabled]: DisabledState,
- [appearanceName, appearance]: AppearanceState
+ [appearanceName, appearance]: AppearanceState,
+ [appearanceVariantName, appearanceVariant]: AppearanceVariantState,
): ViewTemplate => html`
<${menuButtonTag}
appearance="${() => appearance}"
+ appearance-variant="${() => appearanceVariant}"
?disabled=${() => disabled}
?content-hidden=${() => !labelVisible}
style="margin-right: 8px; margin-bottom: 8px;">
${when(() => iconVisible, html`<${iconKeyTag} slot="start">${iconKeyTag}>`)}
- ${() => `${appearanceName!} Menu Button ${disabledName}`}
+ ${() => `${appearanceVariantName} ${appearanceName} Menu Button ${disabledName}`}
${when(() => endIconVisible, html`<${iconArrowExpanderDownTag} slot="end">${iconArrowExpanderDownTag}>`)}
<${menuTag} slot="menu">
@@ -66,7 +59,8 @@ export const menuButtonThemeMatrix: StoryFn = createMatrixThemeStory(
createMatrix(component, [
partVisibilityStates,
disabledStates,
- appearanceStates
+ appearanceStates,
+ appearanceVariantStates
])
);
diff --git a/packages/nimble-components/src/menu-button/tests/menu-button.mdx b/packages/nimble-components/src/menu-button/tests/menu-button.mdx
index 7c39ab4534..319b1a9e7b 100644
--- a/packages/nimble-components/src/menu-button/tests/menu-button.mdx
+++ b/packages/nimble-components/src/menu-button/tests/menu-button.mdx
@@ -1,5 +1,7 @@
import { Controls, Canvas, Meta, Title } from '@storybook/blocks';
import ContentHiddenDocs from '../../patterns/button/tests/content-hidden-docs.mdx';
+import StylingDocs from '../../patterns/button/tests/styling-docs.mdx';
+import { menuButtonTag } from '..';
import * as menuButtonStories from './menu-button.stories';
@@ -11,9 +13,7 @@ often styled as a typical push button with a downward pointing arrow or triangle
-{/* ## Appearances */}
-
-{/* ## Appearance Variants */}
+
{/* ## Usage */}
diff --git a/packages/nimble-components/src/menu-button/tests/menu-button.stories.ts b/packages/nimble-components/src/menu-button/tests/menu-button.stories.ts
index 8a8e6bfcd3..74aa21800c 100644
--- a/packages/nimble-components/src/menu-button/tests/menu-button.stories.ts
+++ b/packages/nimble-components/src/menu-button/tests/menu-button.stories.ts
@@ -5,16 +5,25 @@ import {
createUserSelectedThemeStory,
disableStorybookZoomTransform
} from '../../utilities/tests/storybook';
-import { ButtonAppearance, MenuButtonPosition } from '../types';
+import {
+ ButtonAppearance,
+ ButtonAppearanceVariant,
+ MenuButtonPosition
+} from '../types';
import { iconArrowExpanderDownTag } from '../../icons/arrow-expander-down';
import { iconKeyTag } from '../../icons/key';
import { menuButtonTag } from '..';
import { menuTag } from '../../menu';
import { menuItemTag } from '../../menu-item';
+import {
+ appearanceDescription,
+ appearanceVariantDescription
+} from '../../patterns/button/tests/doc-strings';
interface MenuButtonArgs {
label: string;
- appearance: string;
+ appearance: keyof typeof ButtonAppearance;
+ appearanceVariant: keyof typeof ButtonAppearanceVariant;
open: boolean;
disabled: boolean;
icon: boolean;
@@ -41,8 +50,15 @@ const metadata: Meta = {
},
argTypes: {
appearance: {
- options: Object.values(ButtonAppearance),
- control: { type: 'radio' }
+ options: Object.keys(ButtonAppearance),
+ control: { type: 'radio' },
+ description: appearanceDescription
+ },
+ appearanceVariant: {
+ name: 'appearance-variant',
+ options: Object.keys(ButtonAppearanceVariant),
+ control: { type: 'radio' },
+ description: appearanceVariantDescription
},
icon: {
description:
@@ -63,7 +79,8 @@ const metadata: Meta = {
?open="${x => x.open}"
?disabled="${x => x.disabled}"
?content-hidden="${x => x.contentHidden}"
- appearance="${x => x.appearance}"
+ appearance="${x => ButtonAppearance[x.appearance]}"
+ appearance-variant="${x => ButtonAppearanceVariant[x.appearanceVariant]}"
position="${x => x.menuPosition}"
>
${when(x => x.icon, html`<${iconKeyTag} slot="start">${iconKeyTag}>`)}
@@ -88,8 +105,9 @@ const metadata: Meta = {
${menuButtonTag}>
`),
args: {
- label: 'Ghost Menu Button',
- appearance: 'ghost',
+ label: 'Menu Button',
+ appearance: 'outline',
+ appearanceVariant: 'default',
open: false,
disabled: false,
icon: false,
diff --git a/packages/nimble-components/src/menu-button/types.ts b/packages/nimble-components/src/menu-button/types.ts
index d9a1d76fcb..4d2371dee1 100644
--- a/packages/nimble-components/src/menu-button/types.ts
+++ b/packages/nimble-components/src/menu-button/types.ts
@@ -2,7 +2,10 @@
* Types of menu button appearance.
* @public
*/
-export { ButtonAppearance } from '../patterns/button/types';
+export {
+ ButtonAppearance,
+ ButtonAppearanceVariant
+} from '../patterns/button/types';
/**
* The options of where to position the menu relative to the menu button.
diff --git a/packages/nimble-components/src/patterns/anchor/tests/anchor-patterns.mdx b/packages/nimble-components/src/patterns/anchor/tests/anchor-patterns.mdx
new file mode 100644
index 0000000000..9a403b7dfa
--- /dev/null
+++ b/packages/nimble-components/src/patterns/anchor/tests/anchor-patterns.mdx
@@ -0,0 +1,38 @@
+import { Controls, Canvas, Meta, Title } from '@storybook/blocks';
+import * as anchorPatternStories from './anchor-patterns.stories';
+
+
+
+
+Anchor components in nimble should behave like native anchors in a number of different ways that are difficult to unit test in an automated manner.
+Therefore, this story contains a native anchor element along with all of the nimble components that are anchors or contain anchors.
+The following behaviors should be true of the nimble components:
+
+Mouse interactions:
+
+- Dragging the nimble component should behave like the native anchor
+ - Known behavior exceptions:
+ - Chromium - The drag preview contains the href only, not the label of the anchor. See [chromium issue 329489154](https://issues.chromium.org/issues/329489154).
+ - Firefox - The drag preview does not contain the label, and in most cases, is blank. See [mozilla bug 1589364](https://bugzilla.mozilla.org/show_bug.cgi?id=1589364).
+ - Safari - The drag preview contains the href only, not the label of the anchor.
+- CTRL + CLICK opens in a new tab (⌘ + CLICK on macOS)
+- Right clicking should open the link menu
+- Hovering over the component should show the URL preview at the bottom of the browser window
+- Copying the link through the right-click link menu should result in the expected string to be copied to the clipboard
+- The hover state of the component and mouse pointer should match the area of the component that navigates when clicked (i.e. clicking the white space around the control should not navigate)
+
+Keyboard interactions:
+
+- Focusing via tab should show the URL preview at the bottom of the browser window
+ - Known behavior exceptions:
+ - Firefox on macOS - Links are not focusable via tab by default. [See this stackoverflow page for how to allow tab focus of links](https://stackoverflow.com/questions/11704828/how-to-allow-keyboard-focus-of-links-in-firefox).
+ - Safari - Focusing via tab does not show the URL preview at the bottom of the browser, which matches the behavior of native anchors.
+- Pressing ENTER when focused should activate the link
+- Pressing the MENU KEY on the keyboard while the link is focused should open the same link menu as a right click
+
+Interactions specific to anchors visualized as text:
+
+- Selecting the link through click and drag and then copying it should result in the expected string being copied to the clipboard
+
+
+
diff --git a/packages/nimble-components/src/patterns/anchor/tests/anchor-patterns.stories.ts b/packages/nimble-components/src/patterns/anchor/tests/anchor-patterns.stories.ts
new file mode 100644
index 0000000000..ea0bf6842e
--- /dev/null
+++ b/packages/nimble-components/src/patterns/anchor/tests/anchor-patterns.stories.ts
@@ -0,0 +1,169 @@
+import type { Meta, StoryObj } from '@storybook/html';
+import { html, ref } from '@microsoft/fast-element';
+import { createUserSelectedThemeStory } from '../../../utilities/tests/storybook';
+import {
+ bodyFont,
+ bodyFontColor,
+ controlLabelFont,
+ controlLabelFontColor,
+ mediumPadding,
+ standardPadding
+} from '../../../theme-provider/design-tokens';
+import { anchorTag } from '../../../anchor';
+import { anchorButtonTag } from '../../../anchor-button';
+import { anchorTabsTag } from '../../../anchor-tabs';
+import { anchorTabTag } from '../../../anchor-tab';
+import { breadcrumbTag } from '../../../breadcrumb';
+import { breadcrumbItemTag } from '../../../breadcrumb-item';
+import { RichTextViewer, richTextViewerTag } from '../../../rich-text/viewer';
+import { anchorTreeItemTag } from '../../../anchor-tree-item';
+import { treeViewTag } from '../../../tree-view';
+import { anchorMenuItemTag } from '../../../anchor-menu-item';
+import { menuTag } from '../../../menu';
+import { Table, tableTag } from '../../../table';
+import { tableColumnAnchorTag } from '../../../table-column/anchor';
+
+interface AnchorPatternsArgs {
+ label: string;
+ disabled: boolean;
+ tableRef: Table;
+ setTableData: (args: AnchorPatternsArgs) => void;
+ richTextViewerRef: RichTextViewer;
+ setRichTextViewerData: (args: AnchorPatternsArgs) => void;
+}
+
+const metadata: Meta = {
+ title: 'Tests/Anchor Patterns',
+ parameters: {
+ actions: {}
+ },
+ // prettier-ignore
+ render: createUserSelectedThemeStory(html`
+
+
+
+
+
${anchorTag}
+ <${anchorTag} href="${x => (x.disabled ? undefined : 'https://nimble.ni.dev?type=nimble-anchor-1')}">${x => x.label}${anchorTag}>
+
Text that contains a <${anchorTag} href="${x => (x.disabled ? undefined : 'https://nimble.ni.dev?type=nimble-anchor-2')}">nimble anchor element${anchorTag}>.
+
+
+
+
${anchorButtonTag}
+ <${anchorButtonTag} href="https://nimble.ni.dev?type=nimble-anchor-button" ?disabled="${x => x.disabled}">${x => x.label}${anchorButtonTag}>
+
+
+
+
${anchorTabsTag}
+ <${anchorTabsTag}>
+ <${anchorTabTag} href="https://nimble.ni.dev?type=nimble-anchor-tab-1" ?disabled="${x => x.disabled}">${x => x.label} - 1${anchorTabTag}>
+ <${anchorTabTag} href="https://nimble.ni.dev?type=nimble-anchor-tab-2" ?disabled="${x => x.disabled}">${x => x.label} - 2${anchorTabTag}>
+ ${anchorTabsTag}>
+
+
+
+
${breadcrumbTag}
+ <${breadcrumbTag}>
+ <${breadcrumbItemTag} href="${x => (x.disabled ? undefined : 'https://nimble.ni.dev?type=nimble-breadcrumb-item')}">${x => x.label}${breadcrumbItemTag}>
+ <${breadcrumbItemTag}>Current page (no link)${breadcrumbItemTag}>
+ ${breadcrumbTag}>
+
+
+
+
${anchorTreeItemTag}
+ <${treeViewTag}>
+ <${anchorTreeItemTag} href="https://nimble.ni.dev?type=nimble-anchor-tree-item" ?disabled="${x => x.disabled}">${x => x.label}${anchorTreeItemTag}>
+ ${treeViewTag}>
+
+
+
+
${anchorMenuItemTag}
+ <${menuTag}>
+ <${anchorMenuItemTag} href="https://nimble.ni.dev?type=nimble-anchor-menu-item" ?disabled="${x => x.disabled}">${x => x.label}${anchorMenuItemTag}>
+ ${menuTag}>
+
+
+
+
${tableColumnAnchorTag}
+ <${tableTag} ${ref('tableRef')} data-unused="${x => x.setTableData(x)}" style="height: 100px;">
+ <${tableColumnAnchorTag} label-field-name="label" href-field-name="href">Anchor${tableColumnAnchorTag}>
+ ${tableTag}>
+
+
+
+
${richTextViewerTag}
+ <${richTextViewerTag} ${ref('richTextViewerRef')}
+ data-unused="${x => x.setRichTextViewerData(x)}"
+ >
+ ${x => x.label}
+ ${richTextViewerTag}>
+
+ `),
+ argTypes: {
+ tableRef: {
+ table: { disable: true }
+ },
+ setTableData: {
+ table: { disable: true }
+ },
+ richTextViewerRef: {
+ table: { disable: true }
+ },
+ setRichTextViewerData: {
+ table: { disable: true }
+ }
+ },
+ args: {
+ label: 'link',
+ disabled: false,
+ setTableData: x => {
+ void (async () => {
+ // Safari workaround: the nimble-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 = [
+ {
+ label: x.label,
+ href: x.disabled
+ ? undefined
+ : 'https://nimble.ni.dev?type=nimble-table-column-anchor'
+ }
+ ];
+ void x.tableRef.setData(data);
+ })();
+ },
+ setRichTextViewerData: x => {
+ void (async () => {
+ // Safari workaround: the nimble-rich-text-viewer element instance is made at this point
+ // but doesn't seem to be upgraded to a custom element yet
+ await customElements.whenDefined('nimble-rich-text-viewer');
+ const data = `Absolute link: <${x.disabled ? '' : 'https://nimble.ni.dev?type=nimble-rich-text-viewer'}>`;
+ x.richTextViewerRef.markdown = data;
+ })();
+ }
+ }
+};
+
+export default metadata;
+
+export const anchorPatterns: StoryObj = {};
diff --git a/packages/nimble-components/src/patterns/button/styles.ts b/packages/nimble-components/src/patterns/button/styles.ts
index 481ab08275..ee8e4b4310 100644
--- a/packages/nimble-components/src/patterns/button/styles.ts
+++ b/packages/nimble-components/src/patterns/button/styles.ts
@@ -16,10 +16,8 @@ import {
standardPadding,
buttonPrimaryFontColor,
buttonFillPrimaryColor,
- buttonFillActivePrimaryColor,
buttonFillAccentColor,
buttonAccentBlockFontColor,
- buttonFillAccentActiveColor,
buttonBorderAccentOutlineColor,
buttonAccentOutlineFontColor
} from '../../theme-provider/design-tokens';
@@ -28,7 +26,7 @@ import { ButtonAppearance } from './types';
import { accessiblyHidden } from '../../utilities/style/accessibly-hidden';
export const styles = css`
- @layer base, hover, focusVisible, active, disabled, top;
+ @layer base, checked, hover, focusVisible, active, disabled, top;
@layer base {
${display('inline-flex')}
@@ -102,15 +100,15 @@ export const styles = css`
display: contents;
}
- :host([content-hidden]) .content {
- ${accessiblyHidden}
- }
-
[part='start'] {
display: contents;
${iconColor.cssCustomProperty}: ${buttonLabelFontColor};
}
+ :host([content-hidden]) .content {
+ ${accessiblyHidden}
+ }
+
[part='end'] {
display: contents;
${iconColor.cssCustomProperty}: ${buttonLabelFontColor};
@@ -139,6 +137,8 @@ export const styles = css`
@layer active {
.control:active {
box-shadow: none;
+ color: ${buttonLabelFontColor};
+ border-color: ${borderHoverColor};
background-image: linear-gradient(
${fillSelectedColor},
${fillSelectedColor}
@@ -149,29 +149,30 @@ export const styles = css`
.control:active::before {
outline: none;
}
+
+ .control:active [part='start'],
+ .control:active [part='end'] {
+ ${iconColor.cssCustomProperty}: ${buttonLabelFontColor};
+ }
}
@layer disabled {
:host([disabled]) {
- color: ${buttonLabelDisabledFontColor};
cursor: default;
}
:host([disabled]) .control {
+ color: ${buttonLabelDisabledFontColor};
box-shadow: none;
background-image: none;
- color: rgba(${actionRgbPartialColor}, 0.3);
+ background-size: 100% 100%;
}
:host([disabled]) .control::before {
box-shadow: none;
}
- :host([disabled]) slot[name='start']::slotted(*) {
- opacity: 0.3;
- ${iconColor.cssCustomProperty}: ${buttonLabelFontColor};
- }
-
+ :host([disabled]) slot[name='start']::slotted(*),
:host([disabled]) slot[name='end']::slotted(*) {
opacity: 0.3;
${iconColor.cssCustomProperty}: ${buttonLabelFontColor};
@@ -247,7 +248,6 @@ export const styles = css`
rgba(${borderRgbPartialColor}, 0.1),
rgba(${borderRgbPartialColor}, 0.1)
);
- background-size: 100% 100%;
border-color: rgba(${borderRgbPartialColor}, 0.1);
}
}
@@ -268,6 +268,26 @@ export const buttonAppearanceVariantStyles = css``.withBehaviors(
border-color: ${buttonBorderAccentOutlineColor};
color: ${buttonAccentOutlineFontColor};
}
+
+ :host([appearance-variant='accent']) [part='start'],
+ :host([appearance-variant='accent']) [part='end'] {
+ ${iconColor.cssCustomProperty}: ${buttonAccentOutlineFontColor};
+ }
+ }
+
+ @layer active {
+ :host([appearance-variant='accent']) .control:active {
+ color: ${buttonAccentOutlineFontColor};
+ }
+
+ :host([appearance-variant='accent'])
+ .control:active
+ [part='start'],
+ :host([appearance-variant='accent'])
+ .control:active
+ [part='end'] {
+ ${iconColor.cssCustomProperty}: ${buttonAccentOutlineFontColor};
+ }
}
`
),
@@ -275,10 +295,6 @@ export const buttonAppearanceVariantStyles = css``.withBehaviors(
ButtonAppearance.block,
css`
@layer base {
- :host([appearance-variant='primary']) [part='start'] {
- ${iconColor.cssCustomProperty}: white;
- }
-
:host([appearance-variant='primary']) .control {
background-image: linear-gradient(
${buttonFillPrimaryColor},
@@ -288,14 +304,6 @@ export const buttonAppearanceVariantStyles = css``.withBehaviors(
border-color: ${buttonFillPrimaryColor};
}
- :host([appearance-variant='primary']) [part='end'] {
- ${iconColor.cssCustomProperty}: white;
- }
-
- :host([appearance-variant='accent']) [part='start'] {
- ${iconColor.cssCustomProperty}: white;
- }
-
:host([appearance-variant='accent']) .control {
background-image: linear-gradient(
${buttonFillAccentColor},
@@ -305,24 +313,14 @@ export const buttonAppearanceVariantStyles = css``.withBehaviors(
border-color: ${buttonFillAccentColor};
}
- :host([appearance-variant='accent']) [part='end'] {
- ${iconColor.cssCustomProperty}: white;
- }
- }
-
- @layer active {
- :host([appearance-variant='primary']) .control:active {
- background-image: linear-gradient(
- ${buttonFillActivePrimaryColor},
- ${buttonFillActivePrimaryColor}
- );
+ :host([appearance-variant='primary']) [part='start'],
+ :host([appearance-variant='primary']) [part='end'] {
+ ${iconColor.cssCustomProperty}: ${buttonPrimaryFontColor};
}
- :host([appearance-variant='accent']) .control:active {
- background-image: linear-gradient(
- ${buttonFillAccentActiveColor},
- ${buttonFillAccentActiveColor}
- );
+ :host([appearance-variant='accent']) [part='start'],
+ :host([appearance-variant='accent']) [part='end'] {
+ ${iconColor.cssCustomProperty}: ${buttonPrimaryFontColor};
}
}
`
diff --git a/packages/nimble-components/src/patterns/button/tests/states.ts b/packages/nimble-components/src/patterns/button/tests/states.ts
new file mode 100644
index 0000000000..7ea753741f
--- /dev/null
+++ b/packages/nimble-components/src/patterns/button/tests/states.ts
@@ -0,0 +1,25 @@
+import { ButtonAppearance, ButtonAppearanceVariant } from '../types';
+
+/* array of iconVisible, labelVisible, endIconVisible */
+export const partVisibilityStates = [
+ [true, true, false],
+ [true, false, false],
+ [false, true, false],
+ [true, true, true],
+ [false, true, true]
+] as const;
+export type PartVisibilityState = (typeof partVisibilityStates)[number];
+
+export const appearanceStates = [
+ ['Outline', ButtonAppearance.outline],
+ ['Ghost', ButtonAppearance.ghost],
+ ['Block', ButtonAppearance.block]
+] as const;
+export type AppearanceState = (typeof appearanceStates)[number];
+
+export const appearanceVariantStates = [
+ ['Default', ButtonAppearanceVariant.default],
+ ['Primary', ButtonAppearanceVariant.primary],
+ ['Accent', ButtonAppearanceVariant.accent]
+] as const;
+export type AppearanceVariantState = (typeof appearanceVariantStates)[number];
diff --git a/packages/nimble-components/src/patterns/button/tests/styling-docs.mdx b/packages/nimble-components/src/patterns/button/tests/styling-docs.mdx
new file mode 100644
index 0000000000..8de2c9a749
--- /dev/null
+++ b/packages/nimble-components/src/patterns/button/tests/styling-docs.mdx
@@ -0,0 +1,194 @@
+import { NimbleIconKey } from '../../../icons/tests/key.react';
+
+## Styling
+
+### Appearances
+
+These are the standard styling options for the button. Each should be considered for use before employing an appearance variant.
+
+#### Ghost Button:
+
+
+
+
+ {`Ghost Button`}
+
+
+ {`Ghost Button`}
+
+
+
+
+
+ Ghost is the default appearance and should be the first considered for use.
+
+ Use as the default and standard option to create a clean airy and open UI
+ feel. Ghost buttons fit comfortably in tight spaces and help control the
+ visual density of the UI.
+
+
+ Be careful when using that the surrounding context does not cause this
+ button to be confused for emphasized body text, tabs or a standalone links.
+
+ Use in combination with a primary outline or primary block buttons to create a hierarchy of importance. There is no primary ghost button.
+
+
+
+
+
+
+#### Outline Button:
+
+
+
+
+ {`Outline Button`}
+
+
+ {`Outline Button`}
+
+
+
+
+
+ Outline is the secondary style and should be considered for
+ use when ghost button is not sufficient.
+
+
+ Use as an alternative standard button when a ghost button is
+ not suitable. Use like a ghost button to create a clean,
+ light and airy feel.
+
+
+ The outline button is more visually direct about the
+ control's functionality than a ghost button.
+
+
+ Use in combination with ghost buttons (but not block
+ buttons) to create hierarchy.
+
+
+
+
+
+
+#### Block Button:
+
+
+
+
+ {`Block Button`}
+
+
+ {`Block Button`}
+
+
+
+
+
+ Block is the tertiary style used for creating the most eye
+ catching and functionally direct button. Use in areas where
+ controls are not often present or obvious, or when lots of
+ busy information can cause an important control to be
+ overlooked.
+
+
+ Use as a standard button when the most visible solution is
+ required. Use as an alternative to overly subtle button
+ solutions when it is important to emphasize an action and
+ the functionality of the control.
+
+
+ Use in combination with ghost buttons (but not outline
+ buttons) to create hierarchy.
+
+
+
+
+
+
+### Appearance Variants
+
+Button appearance variants are mainly used when a button needs be distinguished for one of the following reasons:
+
+- To indicate the action that allows the user to accomplish their most common or important goal
+- To indicate the action that allows the user to complete their task
+
+There are two available values for `appearance-variant`: `primary` and `accent`.
+
+#### Accent Button:
+
+
+
+
+ {`Ghost`}
+ {`Ghost`}
+ {`Outline Accent`}
+
+ {`Ghost`}
+ {`Ghost`}
+ {`Block Accent`}
+
+
+
+ Use only 1 accent button in a section. It should be used when
+ trying to achieve the most prominent eye-catching approach.
+ Consider the contextual implications of using a green colored
+ button in relation to its name or metaphor. Remember that color
+ has accessibility constraints and the color alone should not be
+ relied on.
+
+
+ Use in situations that lack color and enthusiasm to help support
+ the brand.
+
+ Use in combination with ghost buttons to create hierarchy.
+
+ Do not use an outline button with a block button to create
+ hierarchy.
+
+
+
+
+
+#### Primary Button:
+
+
+
+
+ {`Ghost`}
+ {`Ghost`}
+
+ {`Outline Primary`}
+
+
+ {`Ghost`}
+ {`Ghost`}
+
+ {`Block Primary`}
+
+
+
+
+ Use only 1 primary button in a section. It should be used when
+ there is a conflict with color and its context.
+
+ Use in combination with ghost buttons to create hierarchy.
+
+ Do not use an outline button with a block button to create
+ hierarchy.
+
+
+
+
+
+
+#### Examples:
+
+To see more examples of appearance variant button hierarchy, see the Figma docs for [Primary and Standard Actions](https://www.figma.com/file/PO9mFOu5BCl8aJvFchEeuN/Nimble_Components?type=design&node-id=1604-74603&mode=design&t=SQ3lyK83VHBUaOkg-0).
diff --git a/packages/nimble-components/src/patterns/dropdown/types.ts b/packages/nimble-components/src/patterns/dropdown/types.ts
index 7cebcb4e7d..3e1178b895 100644
--- a/packages/nimble-components/src/patterns/dropdown/types.ts
+++ b/packages/nimble-components/src/patterns/dropdown/types.ts
@@ -1,3 +1,4 @@
+import type { ListOption } from '../../list-option';
import type { ErrorPattern } from '../error/types';
/**
@@ -22,3 +23,17 @@ export const DropdownAppearance = {
} as const;
export type DropdownAppearance =
(typeof DropdownAppearance)[keyof typeof DropdownAppearance];
+
+/**
+ * @internal
+ *
+ * This interface is used to register options with their parent once their
+ * 'connectedCallback' method is run. This allows for the "owner", like the
+ * Select, to have its value set to that newly registered option earlier than it
+ * might otherwise in certain situations. One such scenario is in an Angular
+ * reactive form, where the form value is set to an option immediately after
+ * dynamically adding it.
+ */
+export interface ListOptionOwner extends HTMLElement {
+ registerOption: (option: ListOption) => void;
+}
diff --git a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-mention.spec.ts b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-mention.spec.ts
index f149d9bae7..40839a1bd4 100644
--- a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-mention.spec.ts
+++ b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-mention.spec.ts
@@ -353,7 +353,8 @@ describe('RichTextEditorMention', () => {
expect(pageObject.getMentionButtonLabel(0)).toBe('');
});
- it('should have button title and text when `button-label` updated', async () => {
+ // WebKit skipped, see https://github.com/ni/nimble/issues/1938
+ it('should have button title and text when `button-label` updated #SkipWebkit', async () => {
const { userMentionElement } = await appendUserMentionConfiguration(element);
userMentionElement.buttonLabel = 'at mention';
await waitForUpdatesAsync();
@@ -1032,7 +1033,8 @@ describe('RichTextEditor user mention via template', () => {
expect(pageObject.getEditorFirstChildTextContent()).toBe('User @');
});
- it('should get `@` text without a preceding whitespace after a hard break, when button clicked', async () => {
+ // WebKit skipped, see https://github.com/ni/nimble/issues/1938
+ it('should get `@` text without a preceding whitespace after a hard break, when button clicked #SkipWebkit', async () => {
await pageObject.setEditorTextContent('User');
await pageObject.pressShiftEnterKeysInEditor();
await pageObject.clickUserMentionButton();
@@ -1045,7 +1047,8 @@ describe('RichTextEditor user mention via template', () => {
expect(pageObject.getEditorFirstChildTextContent()).toBe('User@');
});
- it('should get `@` text with a single preceding whitespace after a hard break with a text, when button clicked', async () => {
+ // WebKit skipped, see https://github.com/ni/nimble/issues/1938
+ it('should get `@` text with a single preceding whitespace after a hard break with a text, when button clicked #SkipWebkit', async () => {
await pageObject.setEditorTextContent('User');
await pageObject.pressShiftEnterKeysInEditor();
await pageObject.setEditorTextContent('Text');
@@ -1354,7 +1357,8 @@ describe('RichTextEditorMentionListbox', () => {
expect(pageObject.isMentionListboxOpened()).toBeFalse();
});
- it('setting `disabled` should close the mention popup', async () => {
+ // WebKit skipped, see https://github.com/ni/nimble/issues/1938
+ it('setting `disabled` should close the mention popup #SkipWebkit', async () => {
await appendUserMentionConfiguration(element, [
{ key: 'user:1', displayName: 'username1' },
{ key: 'user:2', displayName: 'username2' }
diff --git a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts
index 2aebf54f11..a7bccd0633 100644
--- a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts
+++ b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts
@@ -215,20 +215,28 @@ describe('RichTextEditor', () => {
describe('keyboard shortcuts should update the checked state of the buttons', () => {
parameterizeSpec(formattingButtons, (spec, name, value) => {
- spec(`"${name}" button keyboard shortcut check`, async () => {
- expect(
- pageObject.getButtonCheckedState(value.toolbarButtonIndex)
- ).toBeFalse();
+ spec(
+ // WebKit skipped, see https://github.com/ni/nimble/issues/1938
+ `"${name}" button keyboard shortcut check #SkipWebkit`,
+ async () => {
+ expect(
+ pageObject.getButtonCheckedState(
+ value.toolbarButtonIndex
+ )
+ ).toBeFalse();
- await pageObject.clickEditorShortcutKeys(
- value.shortcutKey,
- value.shiftKey
- );
+ await pageObject.clickEditorShortcutKeys(
+ value.shortcutKey,
+ value.shiftKey
+ );
- expect(
- pageObject.getButtonCheckedState(value.toolbarButtonIndex)
- ).toBeTrue();
- });
+ expect(
+ pageObject.getButtonCheckedState(
+ value.toolbarButtonIndex
+ )
+ ).toBeTrue();
+ }
+ );
});
});
@@ -255,7 +263,8 @@ describe('RichTextEditor', () => {
});
describe('rich text formatting options to its respective HTML elements', () => {
- it('should have "br" tag name when clicking shift + enter', async () => {
+ // WebKit skipped, see https://github.com/ni/nimble/issues/1938
+ it('should have br tag name when pressing shift + enter #SkipWebkit', async () => {
await pageObject.setEditorTextContent('Plain text 1');
await pageObject.pressShiftEnterKeysInEditor();
await pageObject.setEditorTextContent('Plain text 2');
@@ -272,7 +281,8 @@ describe('RichTextEditor', () => {
expect(pageObject.getEditorLeafContents()).toEqual(['bold']);
});
- it('should have br tag name when pressing shift + Enter with bold content', async () => {
+ // WebKit skipped, see https://github.com/ni/nimble/issues/1938
+ it('should have br tag name when pressing shift + Enter with bold content #SkipWebkit', async () => {
await pageObject.toggleFooterButton(ToolbarButton.bold);
await pageObject.setEditorTextContent('bold1');
await pageObject.pressShiftEnterKeysInEditor();
@@ -294,7 +304,8 @@ describe('RichTextEditor', () => {
expect(pageObject.getEditorLeafContents()).toEqual(['italics']);
});
- it('should have br tag name when pressing shift + Enter with Italics content', async () => {
+ // WebKit skipped, see https://github.com/ni/nimble/issues/1938
+ it('should have br tag name when pressing shift + Enter with Italics content #SkipWebkit', async () => {
await pageObject.toggleFooterButton(ToolbarButton.italics);
await pageObject.setEditorTextContent('italics1');
await pageObject.pressShiftEnterKeysInEditor();
@@ -437,7 +448,8 @@ describe('RichTextEditor', () => {
});
});
- it('should have br tag name when pressing shift + Enter with numbered list content', async () => {
+ // WebKit skipped, see https://github.com/ni/nimble/issues/1938
+ it('should have br tag name when pressing shift + Enter with numbered list content #SkipWebkit', async () => {
await pageObject.setEditorTextContent('numbered list1');
await pageObject.toggleFooterButton(ToolbarButton.numberedList);
await pageObject.pressShiftEnterKeysInEditor();
@@ -496,7 +508,8 @@ describe('RichTextEditor', () => {
).toBeTrue();
});
- it('should have br tag name when pressing shift + Enter with nested numbered lists content', async () => {
+ // WebKit skipped, see https://github.com/ni/nimble/issues/1938
+ it('should have br tag name when pressing shift + Enter with nested numbered lists content #SkipWebkit', async () => {
await pageObject.setEditorTextContent('List');
await pageObject.toggleFooterButton(ToolbarButton.numberedList);
await pageObject.pressEnterKeyInEditor();
@@ -585,7 +598,8 @@ describe('RichTextEditor', () => {
expect(pageObject.getEditorLeafContents()).toEqual(['Bullet List']);
});
- it('should have br tag name when pressing shift + Enter with bulleted list content', async () => {
+ // WebKit skipped, see https://github.com/ni/nimble/issues/1938
+ it('should have br tag name when pressing shift + Enter with bulleted list content #SkipWebkit', async () => {
await pageObject.setEditorTextContent('Bulleted List 1');
await pageObject.toggleFooterButton(ToolbarButton.bulletList);
await pageObject.pressShiftEnterKeysInEditor();
@@ -644,7 +658,8 @@ describe('RichTextEditor', () => {
).toBeTrue();
});
- it('should have br tag name when pressing shift + Enter with nested bulleted lists content', async () => {
+ // WebKit skipped, see https://github.com/ni/nimble/issues/1938
+ it('should have br tag name when pressing shift + Enter with nested bulleted lists content #SkipWebkit', async () => {
await pageObject.setEditorTextContent('List');
await pageObject.toggleFooterButton(ToolbarButton.bulletList);
await pageObject.pressEnterKeyInEditor();
diff --git a/packages/nimble-components/src/rich-text/mention-listbox/tests/mention-listbox.spec.ts b/packages/nimble-components/src/rich-text/mention-listbox/tests/mention-listbox.spec.ts
index efb226a31f..09d9d5d10d 100644
--- a/packages/nimble-components/src/rich-text/mention-listbox/tests/mention-listbox.spec.ts
+++ b/packages/nimble-components/src/rich-text/mention-listbox/tests/mention-listbox.spec.ts
@@ -1,8 +1,7 @@
-import { html, repeat } from '@microsoft/fast-element';
+import { html, ref, repeat } from '@microsoft/fast-element';
import { RichTextMentionListbox, richTextMentionListboxTag } from '..';
import { waitForUpdatesAsync } from '../../../testing/async-helpers';
import { type Fixture, fixture } from '../../../utilities/tests/fixture';
-import { waitAnimationFrame } from '../../../utilities/tests/component';
import { listOptionTag } from '../../../list-option';
describe('RichTextMentionListbox', () => {
@@ -18,39 +17,49 @@ describe('RichTextMentionListbox', () => {
).toBeInstanceOf(RichTextMentionListbox);
});
- async function setup500Options(): Promise> {
+ class Model {
+ public mentionListbox!: RichTextMentionListbox;
+ public anchorDiv!: HTMLDivElement;
+ }
+
+ async function setup500Options(
+ source: Model
+ ): Promise> {
// prettier-ignore
- const viewTemplate = html`
- <${richTextMentionListboxTag}>
- ${repeat(() => [...Array(500).keys()], html`
- <${listOptionTag} value="${x => x}">${x => x}${listOptionTag}>`)}
- ${richTextMentionListboxTag}>
- `;
- return fixture(viewTemplate);
+ return fixture(
+ html`
+
+
+ <${richTextMentionListboxTag} ${ref('mentionListbox')}>
+ ${repeat(() => [...Array(500).keys()], html
`
+ <${listOptionTag} value="${x => x}">${x => x}${listOptionTag}>`)}
+ ${richTextMentionListboxTag}>
+ `,
+ { source }
+ );
}
async function waitForSelectionUpdateAsync(): Promise {
await waitForUpdatesAsync();
await waitForUpdatesAsync();
- await waitAnimationFrame(); // necessary because scrolling is queued with requestAnimationFrame
}
- // Intermittent test. See tech debt issue: https://github.com/ni/nimble/issues/1891
- xit('should scroll the selected option into view when opened', async () => {
- const { element, connect, disconnect } = await setup500Options();
+ it('should scroll the selected option into view when opened', async () => {
+ const model = new Model();
+ const { connect, disconnect } = await setup500Options(model);
await connect();
- element.show({
- anchorNode: document.documentElement,
+ model.mentionListbox.show({
+ anchorNode: model.anchorDiv,
filter: ''
});
await waitForSelectionUpdateAsync(); // showing filters the options and modifies the selection
- element.selectedIndex = 300;
+ model.mentionListbox.selectedIndex = 300;
await waitForSelectionUpdateAsync();
- const listbox = element.region!.querySelector('.listbox')!;
+ const listbox = model.mentionListbox.region!.querySelector('.listbox')!;
expect(listbox.scrollTop).toBeGreaterThan(8000);
- element.selectedIndex = 0;
+ model.mentionListbox.selectedIndex = 0;
await waitForSelectionUpdateAsync();
expect(listbox.scrollTop).toBeCloseTo(4);
diff --git a/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts b/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts
index 493afc3567..121625c218 100644
--- a/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts
+++ b/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts
@@ -273,7 +273,8 @@ describe('Markdown serializer', () => {
* Nested bulleted list`);
});
- it('Hard break', async () => {
+ // WebKit skipped, see https://github.com/ni/nimble/issues/1938
+ it('Hard break #SkipWebkit', async () => {
await pageObject.setEditorTextContent('Plain text 1');
await pageObject.pressShiftEnterKeysInEditor();
await pageObject.setEditorTextContent('Plain text 2');
@@ -284,7 +285,8 @@ Plain text 2\
Plain text 3`);
});
- it('Hard break with bold', async () => {
+ // WebKit skipped, see https://github.com/ni/nimble/issues/1938
+ it('Hard break with bold #SkipWebkit', async () => {
await pageObject.toggleFooterButton(ToolbarButton.bold);
await pageObject.setEditorTextContent('Bold');
await pageObject.pressShiftEnterKeysInEditor();
@@ -293,7 +295,8 @@ Plain text 3`);
**Bold**`);
});
- it('Hard break with italics', async () => {
+ // WebKit skipped, see https://github.com/ni/nimble/issues/1938
+ it('Hard break with italics #SkipWebkit', async () => {
await pageObject.toggleFooterButton(ToolbarButton.italics);
await pageObject.setEditorTextContent('Italics');
await pageObject.pressShiftEnterKeysInEditor();
@@ -302,7 +305,8 @@ Plain text 3`);
*Italics*`);
});
- it('Hard break with bulleted list', async () => {
+ // WebKit skipped, see https://github.com/ni/nimble/issues/1938
+ it('Hard break with bulleted list #SkipWebkit', async () => {
await pageObject.setEditorTextContent('Bulleted');
await pageObject.toggleFooterButton(ToolbarButton.bulletList);
await pageObject.pressShiftEnterKeysInEditor();
@@ -311,7 +315,8 @@ Plain text 3`);
list`);
});
- it('Hard break with numbered list', async () => {
+ // WebKit skipped, see https://github.com/ni/nimble/issues/1938
+ it('Hard break with numbered list #SkipWebkit', async () => {
await pageObject.setEditorTextContent('Numbered');
await pageObject.toggleFooterButton(ToolbarButton.numberedList);
await pageObject.pressShiftEnterKeysInEditor();
@@ -320,7 +325,8 @@ Plain text 3`);
list`);
});
- it('Hard break with mention node', async () => {
+ // WebKit skipped, see https://github.com/ni/nimble/issues/1938
+ it('Hard break with mention node #SkipWebkit', async () => {
await appendUserMentionConfiguration(element, [
{ key: 'user:1', displayName: 'username1' }
]);
diff --git a/packages/nimble-components/src/select/index.ts b/packages/nimble-components/src/select/index.ts
index 5370b1f06f..5cd9a29f4b 100644
--- a/packages/nimble-components/src/select/index.ts
+++ b/packages/nimble-components/src/select/index.ts
@@ -30,7 +30,10 @@ import {
} from '@microsoft/fast-web-utilities';
import { arrowExpanderDown16X16 } from '@ni/nimble-tokens/dist/icons/js';
import { styles } from './styles';
-import { DropdownAppearance } from '../patterns/dropdown/types';
+import {
+ DropdownAppearance,
+ ListOptionOwner
+} from '../patterns/dropdown/types';
import { errorTextTemplate } from '../patterns/error/template';
import type { ErrorPattern } from '../patterns/error/types';
import { iconExclamationMarkTag } from '../icons/exclamation-mark';
@@ -57,7 +60,9 @@ const isNimbleListOption = (el: Element): el is ListOption => {
/**
* A nimble-styled HTML select.
*/
-export class Select extends FormAssociatedSelect implements ErrorPattern {
+export class Select
+ extends FormAssociatedSelect
+ implements ErrorPattern, ListOptionOwner {
@attr
public appearance: DropdownAppearance = DropdownAppearance.underline;
@@ -668,6 +673,23 @@ export class Select extends FormAssociatedSelect implements ErrorPattern {
}
}
+ /**
+ * @internal
+ */
+ public registerOption(option: ListOption): void {
+ if (this.options.includes(option)) {
+ return;
+ }
+
+ // Adding an option to the end, ultimately, isn't the correct
+ // thing to do, as this will mean the option's index in the options,
+ // at least temporarily, does not match the DOM order. However, it
+ // is expected that a successive run of `slottedOptionsChanged` will
+ // correct this order issue. See 'https://github.com/ni/nimble/issues/1915'
+ // for more info.
+ this.options.push(option);
+ }
+
// Prevents parent classes from resetting selectedIndex to a positive
// value while filtering, which can result in a disabled option being
// selected.
diff --git a/packages/nimble-components/src/select/testing/select.pageobject.ts b/packages/nimble-components/src/select/testing/select.pageobject.ts
index c8a5d236f0..4ea4cda6db 100644
--- a/packages/nimble-components/src/select/testing/select.pageobject.ts
+++ b/packages/nimble-components/src/select/testing/select.pageobject.ts
@@ -23,7 +23,7 @@ export class SelectPageObject {
'Can not set filter text with filterMode set to "none".'
);
}
- await this.clickSelect();
+ this.clickSelect();
const filterInput = this.getFilterInput();
if (filterInput) {
filterInput.value = filterText;
@@ -59,9 +59,8 @@ export class SelectPageObject {
/**
* Either opens or closes the dropdown depending on its current state
*/
- public async clickSelect(): Promise {
+ public clickSelect(): void {
this.selectElement.click();
- await waitForUpdatesAsync();
}
public clickSelectedItem(): void {
@@ -96,11 +95,9 @@ export class SelectPageObject {
* Click the option with the text provided by the 'displayText' parameter.
* @param value The text of the option to be selected
*/
- public async clickOptionWithDisplayText(
- displayText: string
- ): Promise {
+ public clickOptionWithDisplayText(displayText: string): void {
if (!this.selectElement.open) {
- await this.clickSelect();
+ this.clickSelect();
}
const optionIndex = this.selectElement.options.findIndex(
o => o.text === displayText
diff --git a/packages/nimble-components/src/select/tests/select.spec.ts b/packages/nimble-components/src/select/tests/select.spec.ts
index e754dbe5cb..5b58069bf4 100644
--- a/packages/nimble-components/src/select/tests/select.spec.ts
+++ b/packages/nimble-components/src/select/tests/select.spec.ts
@@ -168,7 +168,7 @@ describe('Select', () => {
const { element, connect, disconnect } = await setup();
await connect();
const pageObject = new SelectPageObject(element);
- await pageObject.clickSelect();
+ await clickAndWaitForOpen(element);
expect(pageObject.isDropdownVisible()).toBeTrue();
expect(pageObject.isFilterInputVisible()).toBeFalse();
@@ -179,7 +179,7 @@ describe('Select', () => {
const { element, connect, disconnect } = await setup();
await connect();
const pageObject = new SelectPageObject(element);
- await pageObject.clickSelect();
+ pageObject.clickSelect();
pageObject.pressArrowDownKey();
await waitForUpdatesAsync();
expect(element.selectedIndex).toBe(1);
@@ -195,7 +195,7 @@ describe('Select', () => {
const { element, connect, disconnect } = await setup();
await connect();
const pageObject = new SelectPageObject(element);
- await pageObject.clickSelect();
+ pageObject.clickSelect();
pageObject.pressArrowDownKey();
await waitForUpdatesAsync();
@@ -207,7 +207,7 @@ describe('Select', () => {
const { element, connect, disconnect } = await setup();
await connect();
const pageObject = new SelectPageObject(element);
- await pageObject.clickSelect();
+ pageObject.clickSelect();
pageObject.pressArrowDownKey();
await pageObject.pressSpaceKey();
expect(element.value).toBe('two');
@@ -294,6 +294,33 @@ describe('Select', () => {
await disconnect();
});
+ it('option added directly to DOM synchronously registers with Select', async () => {
+ const { element, connect, disconnect } = await setup();
+ await connect();
+ await waitForUpdatesAsync();
+ const newOption = new ListOption('foo', 'foo');
+ const registerOptionSpy = spyOn(
+ element,
+ 'registerOption'
+ ).and.callThrough();
+ registerOptionSpy.calls.reset();
+ element.insertBefore(newOption, element.options[0]!);
+
+ expect(registerOptionSpy.calls.count()).toBe(1);
+ expect(element.options).toContain(newOption);
+
+ // While the option is registered synchronously as shown above,
+ // properties like selectedIndex will only be correct asynchronously
+ // See https://github.com/ni/nimble/issues/1915
+ expect(element.selectedIndex).toBe(0);
+ await waitForUpdatesAsync();
+ expect(element.value).toBe('one');
+ // This assertion shows that after 'slottedOptionsChanged' runs, the
+ // 'selectedIndex' state has been corrected to expected DOM order.
+ expect(element.selectedIndex).toBe(1);
+ await disconnect();
+ });
+
describe('with 500 options', () => {
async function setup500Options(): Promise> {
// prettier-ignore
@@ -482,25 +509,25 @@ describe('Select', () => {
expect(element.open).toBeTrue();
});
- it('after pressing to close dropdown, will re-open dropdown', async () => {
+ it('after pressing to close dropdown, will re-open dropdown', () => {
element.filterMode = testData.filter;
- await pageObject.clickSelect();
+ pageObject.clickSelect();
pageObject.pressEscapeKey();
expect(element.open).toBeFalse();
pageObject.pressEnterKey();
expect(element.open).toBeTrue();
});
- it('after closing dropdown by pressing , activeElement is Select element', async () => {
+ it('after closing dropdown by pressing , activeElement is Select element', () => {
element.filterMode = testData.filter;
- await pageObject.clickSelect();
+ pageObject.clickSelect();
pageObject.pressEscapeKey();
expect(document.activeElement).toBe(element);
});
- it('after closing dropdown by committing a value with , activeElement is Select element', async () => {
+ it('after closing dropdown by committing a value with , activeElement is Select element', () => {
element.filterMode = testData.filter;
- await pageObject.clickSelect();
+ pageObject.clickSelect();
pageObject.pressArrowDownKey();
pageObject.pressEnterKey();
expect(document.activeElement).toBe(element);
@@ -571,7 +598,7 @@ describe('Select', () => {
expect(currentSelection?.text).toBe('Two');
pageObject.pressEscapeKey();
- await pageObject.clickSelect();
+ pageObject.clickSelect();
currentSelection = pageObject.getSelectedOption();
expect(currentSelection?.text).toBe('One');
});
@@ -584,7 +611,7 @@ describe('Select', () => {
await pageObject.openAndSetFilterText('One'); // Matches 'One'
pageObject.pressEnterKey();
- await pageObject.clickSelect();
+ pageObject.clickSelect();
currentSelection = pageObject.getSelectedOption();
expect(currentSelection?.selected).toBeTrue();
});
@@ -641,10 +668,8 @@ describe('Select', () => {
});
it('pressing after dropdown is open will enter " " as filter text and keep dropdown open', async () => {
- await pageObject.clickSelect();
- await waitForUpdatesAsync();
+ pageObject.clickSelect();
await pageObject.pressSpaceKey();
- await waitForUpdatesAsync();
expect(element.open).toBeTrue();
expect(pageObject.getFilterInputText()).toBe(' ');
});
@@ -652,7 +677,7 @@ describe('Select', () => {
it('opening dropdown after applying filter previously starts with empty filter', async () => {
await pageObject.openAndSetFilterText('T'); // Matches 'Two' and 'Three'
await pageObject.closeDropdown();
- await pageObject.clickSelect();
+ pageObject.clickSelect();
expect(pageObject.getFilterInputText()).toBe('');
expect(pageObject.getFilteredOptions().length).toBe(6);
@@ -669,7 +694,7 @@ describe('Select', () => {
});
it('opening dropdown with no filter does not display "not items found" element', async () => {
- await pageObject.clickSelect();
+ await clickAndWaitForOpen(element);
expect(pageObject.isNoResultsLabelVisible()).toBeFalse();
});
@@ -723,7 +748,7 @@ describe('Select', () => {
});
it('clicking in filter input after dropdown is open, does not close dropdown', async () => {
- await pageObject.clickSelect();
+ await clickAndWaitForOpen(element);
await pageObject.clickFilterInput();
expect(element.open).toBeTrue();
});
@@ -736,7 +761,7 @@ describe('Select', () => {
});
it('filter input "aria-controls" and "aria-activedescendant" attributes are set to element state', async () => {
- await pageObject.clickSelect();
+ await clickAndWaitForOpen(element);
const filterInput = element.shadowRoot?.querySelector('.filter-input');
expect(filterInput?.getAttribute('aria-controls')).toBe(
element.ariaControls
@@ -821,10 +846,9 @@ describe('Select', () => {
await disconnect();
});
- it('exercise clickOptionWithDisplayText', async () => {
- await pageObject.clickSelect();
- await waitForUpdatesAsync();
- await pageObject.clickOptionWithDisplayText('Two');
+ it('exercise clickOptionWithDisplayText', () => {
+ pageObject.clickSelect();
+ pageObject.clickOptionWithDisplayText('Two');
expect(element.value).toBe('two');
expect(element.selectedIndex).toBe(1);
});
diff --git a/packages/nimble-components/src/spinner/tests/spinner.mdx b/packages/nimble-components/src/spinner/tests/spinner.mdx
index ca16347852..a93463cef2 100644
--- a/packages/nimble-components/src/spinner/tests/spinner.mdx
+++ b/packages/nimble-components/src/spinner/tests/spinner.mdx
@@ -27,7 +27,7 @@ Multiple 16px spinners can be used on a single screen at one time.
## Styling / Theme
-Use the `default` appearance Color UI version for backgrounds with color (e.g. purple, blue). Do not use the `accent` appearance on any colored backgrounds, instead use the `default` Color UI version.
+Use the `default` appearance and Color theme for UIs with a strong background color. Do not use the `accent` appearance with the Color theme.
See the `size` and `appearance` usage details for information on customizing the spinner size and guidance for which appearance to use.
diff --git a/packages/nimble-components/src/table-column/anchor/tests/table-column-anchor-matrix.stories.ts b/packages/nimble-components/src/table-column/anchor/tests/table-column-anchor-matrix.stories.ts
index 84e03ecd55..a2606a7d3f 100644
--- a/packages/nimble-components/src/table-column/anchor/tests/table-column-anchor-matrix.stories.ts
+++ b/packages/nimble-components/src/table-column/anchor/tests/table-column-anchor-matrix.stories.ts
@@ -32,11 +32,16 @@ const data = [
},
{
id: '1',
+ firstName: 'https://nimble.ni.dev',
link: 'https://nimble.ni.dev'
},
{
id: '2',
firstName: null
+ },
+ {
+ id: '3',
+ firstName: ''
}
] as const;
@@ -57,7 +62,7 @@ const component = (
[underlineHiddenName, underlineHidden]: UnderlineHiddenState
): ViewTemplate => html`
${appearanceName} ${underlineHiddenName} Anchor Table Column
- <${tableTag} id-field-name="id" style="height: 300px">
+ <${tableTag} id-field-name="id" style="height: 320px">
<${tableColumnAnchorTag}
label-field-name="firstName"
href-field-name="link"
diff --git a/packages/nimble-components/src/table-column/date-text/group-header-view/index.ts b/packages/nimble-components/src/table-column/date-text/group-header-view/index.ts
index 9e9ebbf99b..d39d19961b 100644
--- a/packages/nimble-components/src/table-column/date-text/group-header-view/index.ts
+++ b/packages/nimble-components/src/table-column/date-text/group-header-view/index.ts
@@ -18,15 +18,7 @@ export class TableColumnDateTextGroupHeaderView extends TableColumnTextGroupHead
TableNumberFieldValue,
TableColumnDateTextColumnConfig
> {
- private columnConfigChanged(): void {
- this.updateText();
- }
-
- private groupHeaderValueChanged(): void {
- this.updateText();
- }
-
- private updateText(): void {
+ protected updateText(): void {
if (this.columnConfig) {
this.text = formatNumericDate(
this.columnConfig.formatter,
diff --git a/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.spec.ts b/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.spec.ts
index 17da6cf4de..9bcaefed03 100644
--- a/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.spec.ts
+++ b/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.spec.ts
@@ -8,6 +8,7 @@ import type { TableRecord } from '../../../table/types';
import { TablePageObject } from '../../../table/testing/table.pageobject';
import { TableColumnDateTextPageObject } from '../testing/table-column-date-text.pageobject';
import { lang, themeProviderTag } from '../../../theme-provider';
+import { DateTextFormat } from '../types';
interface SimpleTableRecord extends TableRecord {
field?: number | null;
@@ -119,7 +120,8 @@ describe('TableColumnDateText', () => {
});
});
- it('changing fieldName updates display', async () => {
+ // WebKit skipped, see https://github.com/ni/nimble/issues/1940
+ it('changing fieldName updates display #SkipWebkit', async () => {
await table.setData([
{
field: new Date('Dec 10, 2012, 10:35:05 PM').valueOf(),
@@ -136,7 +138,8 @@ describe('TableColumnDateText', () => {
);
});
- it('changing data from value to null displays blank', async () => {
+ // WebKit skipped, see https://github.com/ni/nimble/issues/1940
+ it('changing data from value to null displays blank #SkipWebkit', async () => {
await table.setData([
{ field: new Date('Dec 10, 2012, 10:35:05 PM').valueOf() }
]);
@@ -153,7 +156,8 @@ describe('TableColumnDateText', () => {
expect(pageObject.getRenderedCellContent(0, 0)).toEqual('');
});
- it('changing data from null to value displays value', async () => {
+ // WebKit skipped, see https://github.com/ni/nimble/issues/1940
+ it('changing data from null to value displays value #SkipWebkit', async () => {
await table.setData([{ field: null }]);
await waitForUpdatesAsync();
expect(pageObject.getRenderedCellContent(0, 0)).toEqual('');
@@ -178,7 +182,8 @@ describe('TableColumnDateText', () => {
expect(pageObject.getRenderedCellContent(0, 0)).toEqual('');
});
- it('sets title when cell text is ellipsized', async () => {
+ // WebKit skipped, see https://github.com/ni/nimble/issues/1940
+ it('sets title when cell text is ellipsized #SkipWebkit', async () => {
table.style.width = '200px';
await table.setData([
{ field: new Date('Dec 10, 2012, 10:35:05 PM').valueOf() }
@@ -230,7 +235,8 @@ describe('TableColumnDateText', () => {
expect(pageObject.getCellTitle(0, 0)).toEqual('');
});
- it('sets group header text to rendered date value', async () => {
+ // WebKit skipped, see https://github.com/ni/nimble/issues/1940
+ it('sets group header text to rendered date value #SkipWebkit', async () => {
await table.setData([
{ field: new Date('Dec 10, 2012, 10:35:05 PM').valueOf() }
]);
@@ -250,7 +256,8 @@ describe('TableColumnDateText', () => {
expect(pageObject.getRenderedCellContent(0, 0)).toBe('12/10/2012');
});
- it('updates displayed date when lang token changes', async () => {
+ // WebKit skipped, see https://github.com/ni/nimble/issues/1940
+ it('updates displayed date when lang token changes #SkipWebkit', async () => {
await table.setData([
{ field: new Date('Dec 10, 2012, 10:35:05 PM').valueOf() }
]);
@@ -509,6 +516,90 @@ describe('TableColumnDateText', () => {
await waitForUpdatesAsync();
expect(column.validity.invalidCustomOptionsCombination).toBeFalse();
});
+
+ describe('placeholder', () => {
+ const testCases = [
+ {
+ name: 'value is not specified',
+ data: [{}],
+ groupValue: 'No value'
+ },
+ {
+ name: 'value is undefined',
+ data: [{ field: undefined }],
+ groupValue: 'No value'
+ },
+ {
+ name: 'value is null',
+ data: [{ field: null }],
+ groupValue: 'No value'
+ },
+ {
+ name: 'value is Number.NaN',
+ data: [{ field: Number.NaN }],
+ groupValue: ''
+ },
+ {
+ name: 'value is valid and non-zero',
+ data: [{ field: 1708984169258 }],
+ groupValue: '2/26/2024'
+ },
+ {
+ name: 'value is incorrect type',
+ data: [{ field: 'not a number' as unknown as number }],
+ groupValue: ''
+ },
+ {
+ name: 'value is specified and falsey',
+ data: [{ field: 0 }],
+ groupValue: '1/1/1970'
+ },
+ {
+ name: 'value is Inf',
+ data: [{ field: Number.POSITIVE_INFINITY }],
+ groupValue: ''
+ },
+ {
+ name: 'value is -Inf',
+ data: [{ field: Number.NEGATIVE_INFINITY }],
+ groupValue: ''
+ },
+ {
+ name: 'value is MAX_VALUE',
+ data: [{ field: Number.MAX_VALUE }],
+ groupValue: ''
+ },
+ {
+ name: 'value is too large for Date',
+ data: [{ field: 8640000000000000 + 1 }],
+ groupValue: ''
+ },
+ {
+ name: 'value is too small for Date',
+ data: [{ field: -8640000000000000 - 1 }],
+ groupValue: ''
+ }
+ ];
+
+ parameterizeSpec(testCases, (spec, name, value) => {
+ spec(
+ `group row renders expected value when ${name}`,
+ async () => {
+ // Set a custom time zone so that the behavior of the test does not
+ // depend on the configuration of the computer running the tests.
+ column.format = DateTextFormat.custom;
+ column.customTimeZone = 'UTC';
+ await table.setData(value.data);
+ await connect();
+ await waitForUpdatesAsync();
+
+ expect(
+ pageObject.getRenderedGroupHeaderContent(0)
+ ).toBe(value.groupValue);
+ }
+ );
+ });
+ });
});
describe('with static config', () => {
diff --git a/packages/nimble-components/src/table-column/duration-text/group-header-view/index.ts b/packages/nimble-components/src/table-column/duration-text/group-header-view/index.ts
index 08d900d3c0..d3d678ee89 100644
--- a/packages/nimble-components/src/table-column/duration-text/group-header-view/index.ts
+++ b/packages/nimble-components/src/table-column/duration-text/group-header-view/index.ts
@@ -17,15 +17,7 @@ export class TableColumnDurationTextGroupHeaderView extends TableColumnTextGroup
TableNumberFieldValue,
TableColumnDurationTextColumnConfig
> {
- private columnConfigChanged(): void {
- this.updateText();
- }
-
- private groupHeaderValueChanged(): void {
- this.updateText();
- }
-
- private updateText(): void {
+ protected updateText(): void {
if (this.columnConfig) {
this.text = this.columnConfig.formatter.format(
this.groupHeaderValue
diff --git a/packages/nimble-components/src/table-column/duration-text/tests/table-column-duration-text.spec.ts b/packages/nimble-components/src/table-column/duration-text/tests/table-column-duration-text.spec.ts
index 6191ea3041..419d200602 100644
--- a/packages/nimble-components/src/table-column/duration-text/tests/table-column-duration-text.spec.ts
+++ b/packages/nimble-components/src/table-column/duration-text/tests/table-column-duration-text.spec.ts
@@ -1,4 +1,5 @@
import { html, ref } from '@microsoft/fast-element';
+import { parameterizeSpec } from '@ni/jasmine-parameterized';
import { tableTag, type Table } from '../../../table';
import { TableColumnDurationText, tableColumnDurationTextTag } from '..';
import { waitForUpdatesAsync } from '../../../testing/async-helpers';
@@ -178,4 +179,66 @@ describe('TableColumnDurationText', () => {
'99 j, 14 h, 50 min, 22 s'
);
});
+
+ describe('placeholder', () => {
+ const testCases = [
+ {
+ name: 'value is not specified',
+ data: [{}],
+ groupValue: 'No value'
+ },
+ {
+ name: 'value is undefined',
+ data: [{ field: undefined }],
+ groupValue: 'No value'
+ },
+ {
+ name: 'value is null',
+ data: [{ field: null }],
+ groupValue: 'No value'
+ },
+ {
+ name: 'value is Number.NaN',
+ data: [{ field: Number.NaN }],
+ groupValue: ''
+ },
+ {
+ name: 'value is valid and non-zero',
+ data: [{ field: 20000 }],
+ groupValue: '20 sec'
+ },
+ {
+ name: 'value is incorrect type',
+ data: [{ field: 'not a number' as unknown as number }],
+ groupValue: ''
+ },
+ {
+ name: 'value is specified and falsey',
+ data: [{ field: 0 }],
+ groupValue: '0 sec'
+ },
+ {
+ name: 'value is Inf',
+ data: [{ field: Number.POSITIVE_INFINITY }],
+ groupValue: ''
+ },
+ {
+ name: 'value is negative',
+ data: [{ field: -5 }],
+ groupValue: ''
+ }
+ ];
+
+ parameterizeSpec(testCases, (spec, name, value) => {
+ spec(`group row renders expected value when ${name}`, async () => {
+ await table.setData(value.data);
+ await connect();
+ await waitForUpdatesAsync();
+
+ expect(pageObject.getRenderedGroupHeaderContent(0)).toBe(
+ value.groupValue
+ );
+ });
+ });
+ });
});
diff --git a/packages/nimble-components/src/table-column/enum-text/group-header-view/index.ts b/packages/nimble-components/src/table-column/enum-text/group-header-view/index.ts
index b7c0f2ccc1..e4605b6549 100644
--- a/packages/nimble-components/src/table-column/enum-text/group-header-view/index.ts
+++ b/packages/nimble-components/src/table-column/enum-text/group-header-view/index.ts
@@ -19,22 +19,10 @@ export class TableColumnEnumTextGroupHeaderView extends TableColumnTextGroupHead
TableFieldValue,
TableColumnEnumColumnConfig
> {
- private columnConfigChanged(): void {
- this.updateText();
- }
-
- private groupHeaderValueChanged(): void {
- this.updateText();
- }
-
- private updateText(): void {
- const value = this.groupHeaderValue;
- if (value === undefined || value === null) {
- this.text = '';
- return;
- }
-
- const config = this.columnConfig?.mappingConfigs.get(value);
+ protected updateText(): void {
+ const config = this.columnConfig?.mappingConfigs.get(
+ this.groupHeaderValue!
+ );
this.text = config instanceof MappingTextConfig && config.text
? config.text
: '';
diff --git a/packages/nimble-components/src/table-column/enum-text/tests/table-column-enum-text.spec.ts b/packages/nimble-components/src/table-column/enum-text/tests/table-column-enum-text.spec.ts
index f638c6e553..dbb773ebf4 100644
--- a/packages/nimble-components/src/table-column/enum-text/tests/table-column-enum-text.spec.ts
+++ b/packages/nimble-components/src/table-column/enum-text/tests/table-column-enum-text.spec.ts
@@ -12,10 +12,11 @@ import { MappingText, mappingTextTag } from '../../../mapping/text';
import { mappingSpinnerTag } from '../../../mapping/spinner';
import { mappingIconTag } from '../../../mapping/icon';
import type { MappingKey } from '../../../mapping/base/types';
+import { themeProviderTag } from '../../../theme-provider';
interface SimpleTableRecord extends TableRecord {
- field1?: MappingKey | undefined;
- field2?: MappingKey | undefined;
+ field1?: MappingKey | null;
+ field2?: MappingKey | null;
}
interface BasicTextMapping {
@@ -24,6 +25,7 @@ interface BasicTextMapping {
}
class Model {
+ public table!: Table;
public col1!: TableColumnEnumText;
public col2!: TableColumnEnumText;
}
@@ -32,7 +34,6 @@ interface ModelFixture extends Fixture {
}
describe('TableColumnEnumText', () => {
- let element: Table;
let connect: () => Promise;
let disconnect: () => Promise;
let pageObject: TablePageObject;
@@ -42,20 +43,22 @@ describe('TableColumnEnumText', () => {
async function setup(mappings: BasicTextMapping[], keyType = 'string'): Promise>> {
const source = new Model();
const result = await fixture>(html`
- <${tableTag} style="width: 700px">
- <${tableColumnEnumTextTag} ${ref('col1')} field-name="field1" key-type="${keyType}">
- Column 1
- ${repeat(() => mappings, html`
- <${mappingTextTag}
- key="${x => x.key}"
- text="${x => x.text}">
- ${mappingTextTag}>
- `)}
- ${tableColumnEnumTextTag}>
- <${tableColumnEnumTextTag} ${ref('col2')}>
- Column 2
- ${tableColumnEnumTextTag}>
- ${tableTag}>`, { source });
+ <${themeProviderTag} lang="en-US">
+ <${tableTag} ${ref('table')} style="width: 700px">
+ <${tableColumnEnumTextTag} ${ref('col1')} field-name="field1" key-type="${keyType}">
+ Column 1
+ ${repeat(() => mappings, html`
+ <${mappingTextTag}
+ key="${x => x.key}"
+ text="${x => x.text}">
+ ${mappingTextTag}>
+ `)}
+ ${tableColumnEnumTextTag}>
+ <${tableColumnEnumTextTag} ${ref('col2')}>
+ Column 2
+ ${tableColumnEnumTextTag}>
+ ${tableTag}>
+ <${themeProviderTag}>`, { source });
return {
...result,
model: source
@@ -86,12 +89,14 @@ describe('TableColumnEnumText', () => {
] as const;
parameterizeSpec(dataTypeTests, (spec, name, value) => {
spec(`displays text mapped from ${name}`, async () => {
- ({ element, connect, disconnect, model } = await setup(
+ ({ connect, disconnect, model } = await setup(
[{ key: value.key, text: 'alpha' }],
value.name
));
- pageObject = new TablePageObject(element);
- await element.setData([{ field1: value.key }]);
+ pageObject = new TablePageObject(
+ model.table
+ );
+ await model.table.setData([{ field1: value.key }]);
await connect();
await waitForUpdatesAsync();
@@ -103,11 +108,11 @@ describe('TableColumnEnumText', () => {
});
it('displays blank when no matches', async () => {
- ({ element, connect, disconnect, model } = await setup([
+ ({ connect, disconnect, model } = await setup([
{ key: 'a', text: 'alpha' }
]));
- pageObject = new TablePageObject(element);
- await element.setData([{ field1: 'no match' }]);
+ pageObject = new TablePageObject(model.table);
+ await model.table.setData([{ field1: 'no match' }]);
await connect();
await waitForUpdatesAsync();
@@ -115,12 +120,12 @@ describe('TableColumnEnumText', () => {
});
it('changing fieldName updates display', async () => {
- ({ element, connect, disconnect, model } = await setup([
+ ({ connect, disconnect, model } = await setup([
{ key: 'a', text: 'alpha' },
{ key: 'b', text: 'bravo' }
]));
- pageObject = new TablePageObject(element);
- await element.setData([{ field1: 'a', field2: 'b' }]);
+ pageObject = new TablePageObject(model.table);
+ await model.table.setData([{ field1: 'a', field2: 'b' }]);
await connect();
await waitForUpdatesAsync();
@@ -132,11 +137,11 @@ describe('TableColumnEnumText', () => {
});
it('changing mapping text updates display', async () => {
- ({ element, connect, disconnect, model } = await setup([
+ ({ connect, disconnect, model } = await setup([
{ key: 'a', text: 'alpha' }
]));
- pageObject = new TablePageObject(element);
- await element.setData([{ field1: 'a' }]);
+ pageObject = new TablePageObject(model.table);
+ await model.table.setData([{ field1: 'a' }]);
await connect();
await waitForUpdatesAsync();
@@ -148,11 +153,11 @@ describe('TableColumnEnumText', () => {
});
it('changing mapping key updates display', async () => {
- ({ element, connect, disconnect, model } = await setup([
+ ({ connect, disconnect, model } = await setup([
{ key: 'a', text: 'alpha' }
]));
- pageObject = new TablePageObject(element);
- await element.setData([{ field1: 'b' }]);
+ pageObject = new TablePageObject(model.table);
+ await model.table.setData([{ field1: 'b' }]);
await connect();
await waitForUpdatesAsync();
@@ -166,11 +171,13 @@ describe('TableColumnEnumText', () => {
describe('various string values render as expected', () => {
parameterizeSpec(wackyStrings, (spec, name) => {
spec(`data "${name}" renders as "${name}"`, async () => {
- ({ element, connect, disconnect, model } = await setup([
+ ({ connect, disconnect, model } = await setup([
{ key: 'a', text: name }
]));
- pageObject = new TablePageObject(element);
- await element.setData([{ field1: 'a' }]);
+ pageObject = new TablePageObject(
+ model.table
+ );
+ await model.table.setData([{ field1: 'a' }]);
await connect();
await waitForUpdatesAsync();
@@ -182,11 +189,13 @@ describe('TableColumnEnumText', () => {
describe('various string values render in group header as expected', () => {
parameterizeSpec(wackyStrings, (spec, name) => {
spec(`data "${name}" renders as "${name}"`, async () => {
- ({ element, connect, disconnect, model } = await setup([
+ ({ connect, disconnect, model } = await setup([
{ key: 'a', text: name }
]));
- pageObject = new TablePageObject(element);
- await element.setData([{ field1: 'a' }]);
+ pageObject = new TablePageObject(
+ model.table
+ );
+ await model.table.setData([{ field1: 'a' }]);
await connect();
await waitForUpdatesAsync();
model.col1.groupIndex = 0;
@@ -200,11 +209,11 @@ describe('TableColumnEnumText', () => {
});
it('sets group header text to blank when unmatched', async () => {
- ({ element, connect, disconnect, model } = await setup([
+ ({ connect, disconnect, model } = await setup([
{ key: 'b', text: 'bravo' }
]));
- pageObject = new TablePageObject(element);
- await element.setData([{ field1: 'unmatched' }]);
+ pageObject = new TablePageObject(model.table);
+ await model.table.setData([{ field1: 'unmatched' }]);
await connect();
await waitForUpdatesAsync();
model.col1.groupIndex = 0;
@@ -215,12 +224,12 @@ describe('TableColumnEnumText', () => {
it('sets title when group header text is ellipsized', async () => {
const cellContents = 'a very long value that should get ellipsized due to not fitting within the default cell width';
- ({ element, connect, disconnect, model } = await setup([
+ ({ connect, disconnect, model } = await setup([
{ key: 'a', text: cellContents }
]));
- pageObject = new TablePageObject(element);
- await element.setData([{ field1: 'a' }]);
- element.style.width = '200px';
+ pageObject = new TablePageObject(model.table);
+ await model.table.setData([{ field1: 'a' }]);
+ model.table.style.width = '200px';
await connect();
await waitForUpdatesAsync();
model.col1.groupIndex = 0;
@@ -231,11 +240,11 @@ describe('TableColumnEnumText', () => {
});
it('does not set title when group header text is fully visible', async () => {
- ({ element, connect, disconnect, model } = await setup([
+ ({ connect, disconnect, model } = await setup([
{ key: 'a', text: 'alpha' }
]));
- pageObject = new TablePageObject(element);
- await element.setData([{ field1: 'a' }]);
+ pageObject = new TablePageObject(model.table);
+ await model.table.setData([{ field1: 'a' }]);
await connect();
await waitForUpdatesAsync();
model.col1.groupIndex = 0;
@@ -247,12 +256,12 @@ describe('TableColumnEnumText', () => {
it('removes title on mouseout of group header', async () => {
const cellContents = 'a very long value that should get ellipsized due to not fitting within the default cell width';
- ({ element, connect, disconnect, model } = await setup([
+ ({ connect, disconnect, model } = await setup([
{ key: 'a', text: cellContents }
]));
- pageObject = new TablePageObject(element);
- await element.setData([{ field1: 'a' }]);
- element.style.width = '200px';
+ pageObject = new TablePageObject(model.table);
+ await model.table.setData([{ field1: 'a' }]);
+ model.table.style.width = '200px';
await connect();
await waitForUpdatesAsync();
model.col1.groupIndex = 0;
@@ -266,10 +275,7 @@ describe('TableColumnEnumText', () => {
describe('validation', () => {
it('is valid with no mappings', async () => {
- ({ element, connect, disconnect, model } = await setup(
- [],
- 'number'
- ));
+ ({ connect, disconnect, model } = await setup([], 'number'));
await connect();
await waitForUpdatesAsync();
const column = model.col1;
@@ -281,7 +287,7 @@ describe('TableColumnEnumText', () => {
});
it('is valid with valid numeric key values', async () => {
- ({ element, connect, disconnect, model } = await setup(
+ ({ connect, disconnect, model } = await setup(
[
{ key: '0', text: 'alpha' },
{ key: '1', text: 'alpha' },
@@ -299,7 +305,7 @@ describe('TableColumnEnumText', () => {
});
it('is invalid with invalid numeric key values', async () => {
- ({ element, connect, disconnect, model } = await setup(
+ ({ connect, disconnect, model } = await setup(
[{ key: 'a', text: 'alpha' }],
'number'
));
@@ -311,7 +317,7 @@ describe('TableColumnEnumText', () => {
});
it('is valid with valid boolean key values', async () => {
- ({ element, connect, disconnect, model } = await setup(
+ ({ connect, disconnect, model } = await setup(
[
{ key: true, text: 'alpha' },
{ key: false, text: 'alpha' }
@@ -333,7 +339,7 @@ describe('TableColumnEnumText', () => {
] as const;
parameterizeSpec(dataTypeTests, (spec, name, value) => {
spec(name, async () => {
- ({ element, connect, disconnect, model } = await setup(
+ ({ connect, disconnect, model } = await setup(
[{ key: value.key, text: 'alpha' }],
'boolean'
));
@@ -348,18 +354,11 @@ describe('TableColumnEnumText', () => {
});
});
- class ModelInvalidMappings {
- public col1!: TableColumnEnumText;
- public col2!: TableColumnEnumText;
- }
- interface ModelInvalidMappingsFixture extends Fixture {
- model: ModelInvalidMappings;
- }
// prettier-ignore
- async function setupInvalidMappings(): Promise>> {
- const source = new ModelInvalidMappings();
- const result = await fixture>(html`
- <${tableTag} style="width: 700px">
+ async function setupInvalidMappings(): Promise>> {
+ const source = new Model();
+ const result = await fixture>(html`
+ <${tableTag} ${ref('table')} style="width: 700px">
<${tableColumnEnumTextTag} ${ref('col1')} field-name="field1">
Column 1
<${mappingTextTag} key="foo" label="foo">${mappingTextTag}>
@@ -378,7 +377,7 @@ describe('TableColumnEnumText', () => {
};
}
it('is invalid with icon or spinner mappings', async () => {
- ({ element, connect, disconnect, model } = await setupInvalidMappings());
+ ({ connect, disconnect, model } = await setupInvalidMappings());
await connect();
await waitForUpdatesAsync();
const column1 = model.col1;
@@ -390,7 +389,7 @@ describe('TableColumnEnumText', () => {
});
it('is invalid with duplicate key values', async () => {
- ({ element, connect, disconnect, model } = await setup([
+ ({ connect, disconnect, model } = await setup([
{ key: 'a', text: 'alpha' },
{ key: 'a', text: 'alpha' }
]));
@@ -402,7 +401,7 @@ describe('TableColumnEnumText', () => {
});
it('is invalid with equivalent numeric key values', async () => {
- ({ element, connect, disconnect, model } = await setup(
+ ({ connect, disconnect, model } = await setup(
[
{ key: '0', text: 'alpha' },
{ key: '0.0', text: 'alpha' }
@@ -417,9 +416,7 @@ describe('TableColumnEnumText', () => {
});
it('is invalid with missing key value', async () => {
- ({ element, connect, disconnect, model } = await setup([
- { text: 'alpha' }
- ]));
+ ({ connect, disconnect, model } = await setup([{ text: 'alpha' }]));
await connect();
await waitForUpdatesAsync();
const column = model.col1;
@@ -428,9 +425,7 @@ describe('TableColumnEnumText', () => {
});
it('is invalid with missing text', async () => {
- ({ element, connect, disconnect, model } = await setup([
- { key: 'a' }
- ]));
+ ({ connect, disconnect, model } = await setup([{ key: 'a' }]));
await connect();
await waitForUpdatesAsync();
const column = model.col1;
@@ -438,4 +433,48 @@ describe('TableColumnEnumText', () => {
expect(column.validity.missingTextValue).toBeTrue();
});
});
+
+ describe('placeholder', () => {
+ const testCases = [
+ {
+ name: 'value is not specified',
+ data: [{}],
+ groupValue: 'No value'
+ },
+ {
+ name: 'value is undefined',
+ data: [{ field1: undefined }],
+ groupValue: 'No value'
+ },
+ {
+ name: 'value is null',
+ data: [{ field1: null }],
+ groupValue: 'No value'
+ },
+ {
+ name: 'value is unmapped value',
+ data: [{ field1: 'no match' }],
+ groupValue: ''
+ }
+ ];
+
+ parameterizeSpec(testCases, (spec, name, value) => {
+ spec(`group row renders expected value when ${name}`, async () => {
+ ({ connect, disconnect, model } = await setup([
+ { key: 'a', text: 'alpha' }
+ ]));
+ pageObject = new TablePageObject(
+ model.table
+ );
+ model.col1.groupIndex = 0;
+ await model.table.setData(value.data);
+ await connect();
+ await waitForUpdatesAsync();
+
+ expect(pageObject.getRenderedGroupHeaderTextContent(0)).toBe(
+ value.groupValue
+ );
+ });
+ });
+ });
});
diff --git a/packages/nimble-components/src/table-column/icon/group-header-view/index.ts b/packages/nimble-components/src/table-column/icon/group-header-view/index.ts
index aefbcaae8e..a2a13efc77 100644
--- a/packages/nimble-components/src/table-column/icon/group-header-view/index.ts
+++ b/packages/nimble-components/src/table-column/icon/group-header-view/index.ts
@@ -36,24 +36,14 @@ export class TableColumnIconGroupHeaderView
@observable
public visual?: 'spinner' | 'icon';
- private columnConfigChanged(): void {
- this.updateState();
- }
-
- private groupHeaderValueChanged(): void {
- this.updateState();
- }
-
- private updateState(): void {
+ protected updateText(): void {
this.visual = undefined;
if (!this.columnConfig) {
+ this.text = '';
return;
}
const value = this.groupHeaderValue;
- if (value === undefined || value === null) {
- return;
- }
- const mappingConfig = this.columnConfig.mappingConfigs.get(value);
+ const mappingConfig = this.columnConfig.mappingConfigs.get(value!);
if (mappingConfig instanceof MappingIconConfig) {
this.visual = 'icon';
this.severity = mappingConfig.severity;
diff --git a/packages/nimble-components/src/table-column/icon/group-header-view/template.ts b/packages/nimble-components/src/table-column/icon/group-header-view/template.ts
index de5a04f639..9e23ef2cf8 100644
--- a/packages/nimble-components/src/table-column/icon/group-header-view/template.ts
+++ b/packages/nimble-components/src/table-column/icon/group-header-view/template.ts
@@ -29,4 +29,13 @@ export const template = html`
>${x => x.text}
`
)}
+ ${when(
+ x => x.visual === undefined,
+ html`
+ ${x => x.text}
+ `
+ )}
`;
diff --git a/packages/nimble-components/src/table-column/icon/tests/table-column-icon.spec.ts b/packages/nimble-components/src/table-column/icon/tests/table-column-icon.spec.ts
index daabf451aa..3c2e597b3a 100644
--- a/packages/nimble-components/src/table-column/icon/tests/table-column-icon.spec.ts
+++ b/packages/nimble-components/src/table-column/icon/tests/table-column-icon.spec.ts
@@ -16,10 +16,11 @@ import { IconSeverity } from '../../../icon-base/types';
import { MappingKeyType } from '../../enum-base/types';
import { mappingSpinnerTag } from '../../../mapping/spinner';
import { spinnerTag } from '../../../spinner';
+import { themeProviderTag } from '../../../theme-provider';
interface SimpleTableRecord extends TableRecord {
- field1?: MappingKey | undefined;
- field2?: MappingKey | undefined;
+ field1?: MappingKey | null;
+ field2?: MappingKey | null;
}
interface BasicIconMapping {
@@ -34,6 +35,7 @@ interface BasicSpinnerMapping {
}
class Model {
+ public table!: Table;
public col1!: TableColumnIcon;
}
interface ModelFixture extends Fixture {
@@ -41,7 +43,6 @@ interface ModelFixture extends Fixture {
}
describe('TableColumnIcon', () => {
- let element: Table;
let connect: () => Promise;
let disconnect: () => Promise;
let pageObject: TablePageObject;
@@ -55,24 +56,26 @@ describe('TableColumnIcon', () => {
}): Promise>> {
const source = new Model();
const result = await fixture>(html`
- <${tableTag} style="width: 700px">
- <${tableColumnIconTag} ${ref('col1')} field-name="field1" key-type="${options.keyType}">
- Column 1
- ${repeat(() => options.iconMappings, html`
- <${mappingIconTag}
+ <${themeProviderTag} lang="en-US">
+ <${tableTag} ${ref('table')} style="width: 700px">
+ <${tableColumnIconTag} ${ref('col1')} field-name="field1" key-type="${options.keyType}">
+ Column 1
+ ${repeat(() => options.iconMappings, html`
+ <${mappingIconTag}
+ key="${x => x.key}"
+ text="${x => x.text}"
+ icon="${x => x.icon}">
+ ${mappingIconTag}>
+ `)}
+ ${repeat(() => options.spinnerMappings, html`
+ <${mappingSpinnerTag}
key="${x => x.key}"
text="${x => x.text}"
- icon="${x => x.icon}">
- ${mappingIconTag}>
- `)}
- ${repeat(() => options.spinnerMappings, html`
- <${mappingSpinnerTag}
- key="${x => x.key}"
- text="${x => x.text}"
- ${mappingSpinnerTag}>
- `)}
- ${tableColumnIconTag}>
- ${tableTag}>`, { source });
+ ${mappingSpinnerTag}>
+ `)}
+ ${tableColumnIconTag}>
+ ${tableTag}>
+ <${themeProviderTag}>`, { source });
return {
...result,
model: source
@@ -103,15 +106,17 @@ describe('TableColumnIcon', () => {
] as const;
parameterizeSpec(dataTypeTests, (spec, name, value) => {
spec(`displays icon mapped from ${name}`, async () => {
- ({ element, connect, disconnect, model } = await setup({
+ ({ connect, disconnect, model } = await setup({
keyType: value.name,
iconMappings: [
{ key: value.key, text: 'alpha', icon: iconXmarkTag }
],
spinnerMappings: []
}));
- pageObject = new TablePageObject(element);
- await element.setData([{ field1: value.key }]);
+ pageObject = new TablePageObject(
+ model.table
+ );
+ await model.table.setData([{ field1: value.key }]);
await connect();
await waitForUpdatesAsync();
@@ -123,13 +128,15 @@ describe('TableColumnIcon', () => {
parameterizeSpec(dataTypeTests, (spec, name, value) => {
spec(`displays spinner mapped from ${name}`, async () => {
- ({ element, connect, disconnect, model } = await setup({
+ ({ connect, disconnect, model } = await setup({
keyType: value.name,
iconMappings: [],
spinnerMappings: [{ key: value.key, text: 'alpha' }]
}));
- pageObject = new TablePageObject(element);
- await element.setData([{ field1: value.key }]);
+ pageObject = new TablePageObject(
+ model.table
+ );
+ await model.table.setData([{ field1: value.key }]);
await connect();
await waitForUpdatesAsync();
@@ -141,13 +148,13 @@ describe('TableColumnIcon', () => {
});
it('displays blank when no matches', async () => {
- ({ element, connect, disconnect, model } = await setup({
+ ({ connect, disconnect, model } = await setup({
keyType: MappingKeyType.string,
iconMappings: [{ key: 'a', text: 'alpha', icon: iconXmarkTag }],
spinnerMappings: []
}));
- pageObject = new TablePageObject(element);
- await element.setData([{ field1: 'no match' }]);
+ pageObject = new TablePageObject(model.table);
+ await model.table.setData([{ field1: 'no match' }]);
await connect();
await waitForUpdatesAsync();
@@ -155,13 +162,13 @@ describe('TableColumnIcon', () => {
});
it('displays blank when no icon specified for mapping', async () => {
- ({ element, connect, disconnect, model } = await setup({
+ ({ connect, disconnect, model } = await setup({
keyType: MappingKeyType.string,
iconMappings: [{ key: 'a', text: 'alpha', icon: undefined }],
spinnerMappings: []
}));
- pageObject = new TablePageObject(element);
- await element.setData([{ field1: 'a' }]);
+ pageObject = new TablePageObject(model.table);
+ await model.table.setData([{ field1: 'a' }]);
await connect();
await waitForUpdatesAsync();
@@ -169,7 +176,7 @@ describe('TableColumnIcon', () => {
});
it('changing fieldName updates display', async () => {
- ({ element, connect, disconnect, model } = await setup({
+ ({ connect, disconnect, model } = await setup({
keyType: MappingKeyType.string,
iconMappings: [
{ key: 'a', text: 'alpha', icon: iconXmarkTag },
@@ -177,8 +184,8 @@ describe('TableColumnIcon', () => {
],
spinnerMappings: []
}));
- pageObject = new TablePageObject(element);
- await element.setData([{ field1: 'a', field2: 'b' }]);
+ pageObject = new TablePageObject(model.table);
+ await model.table.setData([{ field1: 'a', field2: 'b' }]);
await connect();
await waitForUpdatesAsync();
@@ -191,13 +198,13 @@ describe('TableColumnIcon', () => {
});
it('changing mapping icon updates display', async () => {
- ({ element, connect, disconnect, model } = await setup({
+ ({ connect, disconnect, model } = await setup({
keyType: MappingKeyType.string,
iconMappings: [{ key: 'a', text: 'alpha', icon: iconXmarkTag }],
spinnerMappings: []
}));
- pageObject = new TablePageObject(element);
- await element.setData([{ field1: 'a' }]);
+ pageObject = new TablePageObject(model.table);
+ await model.table.setData([{ field1: 'a' }]);
await connect();
await waitForUpdatesAsync();
@@ -211,13 +218,13 @@ describe('TableColumnIcon', () => {
});
it('changing mapping severity updates display', async () => {
- ({ element, connect, disconnect, model } = await setup({
+ ({ connect, disconnect, model } = await setup({
keyType: MappingKeyType.string,
iconMappings: [{ key: 'a', text: 'alpha', icon: iconXmarkTag }],
spinnerMappings: []
}));
- pageObject = new TablePageObject(element);
- await element.setData([{ field1: 'a' }]);
+ pageObject = new TablePageObject(model.table);
+ await model.table.setData([{ field1: 'a' }]);
await connect();
await waitForUpdatesAsync();
@@ -231,13 +238,13 @@ describe('TableColumnIcon', () => {
});
it('changing mapping key updates display', async () => {
- ({ element, connect, disconnect, model } = await setup({
+ ({ connect, disconnect, model } = await setup({
keyType: MappingKeyType.string,
iconMappings: [{ key: 'a', text: 'alpha', icon: iconXmarkTag }],
spinnerMappings: []
}));
- pageObject = new TablePageObject(element);
- await element.setData([{ field1: 'b' }]);
+ pageObject = new TablePageObject(model.table);
+ await model.table.setData([{ field1: 'b' }]);
await connect();
await waitForUpdatesAsync();
@@ -251,26 +258,26 @@ describe('TableColumnIcon', () => {
});
it('sets label as title of icon', async () => {
- ({ element, connect, disconnect, model } = await setup({
+ ({ connect, disconnect, model } = await setup({
keyType: MappingKeyType.string,
iconMappings: [{ key: 'a', text: 'alpha', icon: iconXmarkTag }],
spinnerMappings: []
}));
- pageObject = new TablePageObject(element);
- await element.setData([{ field1: 'a' }]);
+ pageObject = new TablePageObject(model.table);
+ await model.table.setData([{ field1: 'a' }]);
await connect();
await waitForUpdatesAsync();
expect(pageObject.getCellTitle(0, 0)).toBe('alpha');
});
it('sets label as aria-label of icon', async () => {
- ({ element, connect, disconnect, model } = await setup({
+ ({ connect, disconnect, model } = await setup({
keyType: MappingKeyType.string,
iconMappings: [{ key: 'a', text: 'alpha', icon: iconXmarkTag }],
spinnerMappings: []
}));
- pageObject = new TablePageObject(element);
- await element.setData([{ field1: 'a' }]);
+ pageObject = new TablePageObject(model.table);
+ await model.table.setData([{ field1: 'a' }]);
await connect();
await waitForUpdatesAsync();
expect(pageObject.getRenderedIconColumnCellIconAriaLabel(0, 0)).toBe(
@@ -281,7 +288,7 @@ describe('TableColumnIcon', () => {
describe('various string values render in group header as expected', () => {
parameterizeSpec(wackyStrings, (spec, name) => {
spec(`data "${name}" renders as "${name}"`, async () => {
- ({ element, connect, disconnect, model } = await setup({
+ ({ connect, disconnect, model } = await setup({
keyType: MappingKeyType.string,
iconMappings: [
{
@@ -292,8 +299,10 @@ describe('TableColumnIcon', () => {
],
spinnerMappings: []
}));
- pageObject = new TablePageObject(element);
- await element.setData([{ field1: 'a' }]);
+ pageObject = new TablePageObject(
+ model.table
+ );
+ await model.table.setData([{ field1: 'a' }]);
await connect();
await waitForUpdatesAsync();
model.col1.groupIndex = 0;
@@ -306,30 +315,14 @@ describe('TableColumnIcon', () => {
});
});
- it('sets group header text to blank when unmatched', async () => {
- ({ element, connect, disconnect, model } = await setup({
- keyType: MappingKeyType.string,
- iconMappings: [{ key: 'b', text: 'bravo', icon: iconXmarkTag }],
- spinnerMappings: []
- }));
- pageObject = new TablePageObject(element);
- await element.setData([{ field1: 'unmatched' }]);
- await connect();
- await waitForUpdatesAsync();
- model.col1.groupIndex = 0;
- await waitForUpdatesAsync();
-
- expect(pageObject.getRenderedGroupHeaderTextContent(0)).toBe('');
- });
-
it('sets group header text label and no icon when icon is undefined', async () => {
- ({ element, connect, disconnect, model } = await setup({
+ ({ connect, disconnect, model } = await setup({
keyType: MappingKeyType.string,
iconMappings: [{ key: 'b', text: 'bravo', icon: undefined }],
spinnerMappings: []
}));
- pageObject = new TablePageObject(element);
- await element.setData([{ field1: 'b' }]);
+ pageObject = new TablePageObject(model.table);
+ await model.table.setData([{ field1: 'b' }]);
await connect();
await waitForUpdatesAsync();
model.col1.groupIndex = 0;
@@ -340,13 +333,13 @@ describe('TableColumnIcon', () => {
});
it('clears cell when mappings removed', async () => {
- ({ element, connect, disconnect, model } = await setup({
+ ({ connect, disconnect, model } = await setup({
keyType: MappingKeyType.string,
iconMappings: [{ key: 'a', text: 'alpha', icon: iconXmarkTag }],
spinnerMappings: []
}));
- pageObject = new TablePageObject(element);
- await element.setData([{ field1: 'a' }]);
+ pageObject = new TablePageObject(model.table);
+ await model.table.setData([{ field1: 'a' }]);
await connect();
await waitForUpdatesAsync();
expect(pageObject.getRenderedIconColumnCellIconTagName(0, 0)).toBe(
@@ -359,13 +352,13 @@ describe('TableColumnIcon', () => {
});
it('clears group header when mappings removed', async () => {
- ({ element, connect, disconnect, model } = await setup({
+ ({ connect, disconnect, model } = await setup({
keyType: MappingKeyType.string,
iconMappings: [{ key: 'a', text: 'alpha', icon: iconXmarkTag }],
spinnerMappings: []
}));
- pageObject = new TablePageObject(element);
- await element.setData([{ field1: 'a' }]);
+ pageObject = new TablePageObject(model.table);
+ await model.table.setData([{ field1: 'a' }]);
model.col1.groupIndex = 0;
await connect();
await waitForUpdatesAsync();
@@ -380,7 +373,7 @@ describe('TableColumnIcon', () => {
describe('validation', () => {
it('is valid with no mappings', async () => {
- ({ element, connect, disconnect, model } = await setup({
+ ({ connect, disconnect, model } = await setup({
keyType: MappingKeyType.number,
iconMappings: [],
spinnerMappings: []
@@ -397,7 +390,7 @@ describe('TableColumnIcon', () => {
});
it('is valid with valid numeric key values', async () => {
- ({ element, connect, disconnect, model } = await setup({
+ ({ connect, disconnect, model } = await setup({
keyType: MappingKeyType.number,
iconMappings: [
{ key: '0', text: 'alpha', icon: iconXmarkTag },
@@ -424,7 +417,7 @@ describe('TableColumnIcon', () => {
] as const;
parameterizeSpec(dataTypeTests, (spec, name, value) => {
spec(name, async () => {
- ({ element, connect, disconnect, model } = await setup({
+ ({ connect, disconnect, model } = await setup({
keyType: MappingKeyType.boolean,
iconMappings: [
{
@@ -446,7 +439,7 @@ describe('TableColumnIcon', () => {
});
it('is invalid with invalid numeric key values', async () => {
- ({ element, connect, disconnect, model } = await setup({
+ ({ connect, disconnect, model } = await setup({
keyType: MappingKeyType.number,
iconMappings: [{ key: 'a', text: 'alpha', icon: iconXmarkTag }],
spinnerMappings: []
@@ -472,6 +465,7 @@ describe('TableColumnIcon', () => {
);
}
it('is invalid with text mapping', async () => {
+ let element: Table;
({ element, connect, disconnect } = await setupInvalidMappings());
await connect();
await waitForUpdatesAsync();
@@ -481,7 +475,7 @@ describe('TableColumnIcon', () => {
});
it('is invalid with duplicate key values', async () => {
- ({ element, connect, disconnect, model } = await setup({
+ ({ connect, disconnect, model } = await setup({
keyType: MappingKeyType.string,
iconMappings: [
{ key: 'a', text: 'alpha', icon: iconXmarkTag },
@@ -496,7 +490,7 @@ describe('TableColumnIcon', () => {
});
it('is invalid with equivalent numeric key values', async () => {
- ({ element, connect, disconnect, model } = await setup({
+ ({ connect, disconnect, model } = await setup({
keyType: MappingKeyType.number,
iconMappings: [
{ key: '0', text: 'alpha', icon: iconXmarkTag },
@@ -511,7 +505,7 @@ describe('TableColumnIcon', () => {
});
it('is invalid with missing key value', async () => {
- ({ element, connect, disconnect, model } = await setup({
+ ({ connect, disconnect, model } = await setup({
keyType: MappingKeyType.string,
iconMappings: [{ text: 'alpha', icon: iconXmarkTag }],
spinnerMappings: []
@@ -523,7 +517,7 @@ describe('TableColumnIcon', () => {
});
it('is invalid with missing icon text value', async () => {
- ({ element, connect, disconnect, model } = await setup({
+ ({ connect, disconnect, model } = await setup({
keyType: MappingKeyType.string,
iconMappings: [{ key: 'a', icon: iconXmarkTag }],
spinnerMappings: []
@@ -535,7 +529,7 @@ describe('TableColumnIcon', () => {
});
it('is invalid with non-icon icon value', async () => {
- ({ element, connect, disconnect, model } = await setup({
+ ({ connect, disconnect, model } = await setup({
keyType: MappingKeyType.string,
iconMappings: [{ key: 'a', text: 'alpha', icon: 'div' }],
spinnerMappings: []
@@ -547,7 +541,7 @@ describe('TableColumnIcon', () => {
});
it('is invalid with completely made up icon value', async () => {
- ({ element, connect, disconnect, model } = await setup({
+ ({ connect, disconnect, model } = await setup({
keyType: MappingKeyType.string,
iconMappings: [{ key: 'a', text: 'alpha', icon: 'foo' }],
spinnerMappings: []
@@ -559,7 +553,7 @@ describe('TableColumnIcon', () => {
});
it('is invalid with missing spinner text value', async () => {
- ({ element, connect, disconnect, model } = await setup({
+ ({ connect, disconnect, model } = await setup({
keyType: MappingKeyType.string,
iconMappings: [],
spinnerMappings: [{ key: 'a' }]
@@ -570,4 +564,50 @@ describe('TableColumnIcon', () => {
expect(model.col1.validity.missingTextValue).toBeTrue();
});
});
+
+ describe('placeholder', () => {
+ const testCases = [
+ {
+ name: 'value is not specified',
+ data: [{}],
+ groupValue: 'No value'
+ },
+ {
+ name: 'value is undefined',
+ data: [{ field1: undefined }],
+ groupValue: 'No value'
+ },
+ {
+ name: 'value is null',
+ data: [{ field1: null }],
+ groupValue: 'No value'
+ },
+ {
+ name: 'value is unmapped value',
+ data: [{ field1: 'no match' }],
+ groupValue: ''
+ }
+ ];
+
+ parameterizeSpec(testCases, (spec, name, value) => {
+ spec(`group row renders expected value when ${name}`, async () => {
+ ({ connect, disconnect, model } = await setup({
+ keyType: MappingKeyType.string,
+ iconMappings: [],
+ spinnerMappings: [{ key: 'a', text: 'a' }]
+ }));
+ pageObject = new TablePageObject(
+ model.table
+ );
+ model.col1.groupIndex = 0;
+ await model.table.setData(value.data);
+ await connect();
+ await waitForUpdatesAsync();
+
+ expect(pageObject.getRenderedGroupHeaderTextContent(0)).toBe(
+ value.groupValue
+ );
+ });
+ });
+ });
});
diff --git a/packages/nimble-components/src/table-column/number-text/group-header-view/index.ts b/packages/nimble-components/src/table-column/number-text/group-header-view/index.ts
index 17666de1aa..1fb5389b9e 100644
--- a/packages/nimble-components/src/table-column/number-text/group-header-view/index.ts
+++ b/packages/nimble-components/src/table-column/number-text/group-header-view/index.ts
@@ -17,15 +17,7 @@ export class TableColumnNumberTextGroupHeaderView extends TableColumnTextGroupHe
TableNumberFieldValue,
TableColumnNumberTextColumnConfig
> {
- private columnConfigChanged(): void {
- this.updateText();
- }
-
- private groupHeaderValueChanged(): void {
- this.updateText();
- }
-
- private updateText(): void {
+ protected updateText(): void {
this.text = this.columnConfig?.formatter?.format(this.groupHeaderValue) ?? '';
}
}
diff --git a/packages/nimble-components/src/table-column/number-text/tests/table-column-number-text.spec.ts b/packages/nimble-components/src/table-column/number-text/tests/table-column-number-text.spec.ts
index 75c2b9af7b..cc1163995f 100644
--- a/packages/nimble-components/src/table-column/number-text/tests/table-column-number-text.spec.ts
+++ b/packages/nimble-components/src/table-column/number-text/tests/table-column-number-text.spec.ts
@@ -170,7 +170,9 @@ describe('TableColumnNumberText', () => {
await waitForUpdatesAsync();
expect(pageObject.getRenderedCellTextContent(0, 0)).toBe('');
- expect(pageObject.getRenderedGroupHeaderTextContent(0)).toBe('');
+ expect(pageObject.getRenderedGroupHeaderTextContent(0)).toBe(
+ 'No value'
+ );
});
it('changing data from null to value displays value', async () => {
@@ -178,7 +180,9 @@ describe('TableColumnNumberText', () => {
await connect();
await waitForUpdatesAsync();
expect(pageObject.getRenderedCellTextContent(0, 0)).toBe('');
- expect(pageObject.getRenderedGroupHeaderTextContent(0)).toBe('');
+ expect(pageObject.getRenderedGroupHeaderTextContent(0)).toBe(
+ 'No value'
+ );
await table.setData([{ number1: -16 }]);
await waitForUpdatesAsync();
@@ -196,7 +200,9 @@ describe('TableColumnNumberText', () => {
await waitForUpdatesAsync();
expect(pageObject.getRenderedCellTextContent(0, 0)).toBe('');
- expect(pageObject.getRenderedGroupHeaderTextContent(0)).toBe('');
+ expect(pageObject.getRenderedGroupHeaderTextContent(0)).toBe(
+ 'No value'
+ );
});
describe('displays title when appropriate', () => {
@@ -727,4 +733,56 @@ describe('TableColumnNumberText', () => {
expect(cellView.alignment).toEqual(TextCellViewBaseAlignment.left);
});
});
+
+ describe('placeholder', () => {
+ const testCases = [
+ {
+ name: 'value is not specified',
+ data: [{}],
+ groupValue: 'No value'
+ },
+ {
+ name: 'value is undefined',
+ data: [{ number1: undefined }],
+ groupValue: 'No value'
+ },
+ {
+ name: 'value is null',
+ data: [{ number1: null }],
+ groupValue: 'No value'
+ },
+ {
+ name: 'value is Number.NaN',
+ data: [{ number1: Number.NaN }],
+ groupValue: 'NaN'
+ },
+ {
+ name: 'value is valid and non-zero',
+ data: [{ number1: 100 }],
+ groupValue: '100'
+ },
+ {
+ name: 'value is incorrect type',
+ data: [{ number1: 'not a number' as unknown as number }],
+ groupValue: ''
+ },
+ {
+ name: 'value is specified and falsey',
+ data: [{ number1: 0 }],
+ groupValue: '0'
+ }
+ ];
+
+ parameterizeSpec(testCases, (spec, name, value) => {
+ spec(`group row renders expected value when ${name}`, async () => {
+ await table.setData(value.data);
+ await connect();
+ await waitForUpdatesAsync();
+
+ expect(pageObject.getRenderedGroupHeaderTextContent(0)).toBe(
+ value.groupValue
+ );
+ });
+ });
+ });
});
diff --git a/packages/nimble-components/src/table-column/text-base/cell-view/tests/table-column-text-base-group-header-view.spec.ts b/packages/nimble-components/src/table-column/text-base/cell-view/tests/table-column-text-base-group-header-view.spec.ts
new file mode 100644
index 0000000000..4875bed262
--- /dev/null
+++ b/packages/nimble-components/src/table-column/text-base/cell-view/tests/table-column-text-base-group-header-view.spec.ts
@@ -0,0 +1,141 @@
+// eslint-disable-next-line max-classes-per-file
+import { customElement, html, ref } from '@microsoft/fast-element';
+import { parameterizeSpec } from '@ni/jasmine-parameterized';
+import {
+ uniqueElementName,
+ type Fixture,
+ fixture
+} from '../../../../utilities/tests/fixture';
+import { waitForUpdatesAsync } from '../../../../testing/async-helpers';
+import { template as textBaseGroupHeaderViewTemplate } from '../template';
+import { styles as textBaseGroupHeaderViewStyles } from '../styles';
+import { TableColumnTextGroupHeaderViewBase } from '../../group-header-view';
+import { ThemeProvider, themeProviderTag } from '../../../../theme-provider';
+import {
+ LabelProviderTable,
+ labelProviderTableTag
+} from '../../../../label-provider/table';
+
+describe('TableColumnTextBaseGroupHeaderView', () => {
+ let labelProvider: LabelProviderTable;
+ let groupHeaderView: TableColumnTextGroupHeaderViewBase;
+ let connect: () => Promise;
+ let disconnect: () => Promise;
+
+ const testTextBaseGroupHeaderViewTag = uniqueElementName();
+ /**
+ * Simple concrete class extending TableColumnTextCellViewBase to use for testing
+ */
+ @customElement({
+ name: testTextBaseGroupHeaderViewTag,
+ template: textBaseGroupHeaderViewTemplate,
+ styles: textBaseGroupHeaderViewStyles
+ })
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ class TestTextBaseGroupHeaderView extends TableColumnTextGroupHeaderViewBase {
+ protected override updateText(): void {
+ this.text = this.groupHeaderValue as string;
+ }
+ }
+
+ class ElementReferences {
+ public labelProvider!: LabelProviderTable;
+ public groupView!: TableColumnTextGroupHeaderViewBase;
+ }
+
+ async function setup(
+ source: ElementReferences
+ ): Promise> {
+ return fixture(
+ html`<${themeProviderTag} lang="en-US">
+ <${labelProviderTableTag} ${ref('labelProvider')}>${labelProviderTableTag}>
+ <${testTextBaseGroupHeaderViewTag} ${ref('groupView')}>${testTextBaseGroupHeaderViewTag}>
+ ${themeProviderTag}>`,
+ { source }
+ );
+ }
+
+ function getRenderedText(): string {
+ return groupHeaderView
+ .shadowRoot!.querySelector('span')!
+ .innerText.trim();
+ }
+
+ beforeEach(async () => {
+ const source = new ElementReferences();
+ ({ connect, disconnect } = await setup(source));
+ labelProvider = source.labelProvider;
+ groupHeaderView = source.groupView;
+ await connect();
+ });
+
+ afterEach(async () => {
+ await disconnect();
+ });
+
+ const testCases = [
+ {
+ name: 'empty string',
+ value: '',
+ renderedText: 'Empty',
+ labelProviderProperty: 'groupRowPlaceholderEmpty'
+ },
+ {
+ name: 'null',
+ value: null,
+ renderedText: 'No value',
+ labelProviderProperty: 'groupRowPlaceholderNoValue'
+ },
+ {
+ name: 'undefined',
+ value: undefined,
+ renderedText: 'No value',
+ labelProviderProperty: 'groupRowPlaceholderNoValue'
+ }
+ ] as const;
+
+ parameterizeSpec(testCases, (spec, name, value) => {
+ spec(
+ `uses default label provider string when the value is ${name}`,
+ async () => {
+ groupHeaderView.groupHeaderValue = value.value;
+ await waitForUpdatesAsync();
+ expect(getRenderedText()).toBe(value.renderedText);
+ }
+ );
+ });
+
+ parameterizeSpec(testCases, (spec, name, value) => {
+ spec(
+ `updates group row with modified label provider string when the value is ${name}`,
+ async () => {
+ const customLabelProviderValue = 'Custom label provider value';
+ groupHeaderView.groupHeaderValue = value.value;
+ await waitForUpdatesAsync();
+ labelProvider[value.labelProviderProperty] = customLabelProviderValue;
+ await waitForUpdatesAsync();
+
+ expect(getRenderedText()).toBe(customLabelProviderValue);
+ }
+ );
+ });
+
+ parameterizeSpec(testCases, (spec, name, value) => {
+ spec(
+ `uses label provider value that was modified while the element was disconnected when the value is ${name}`,
+ async () => {
+ groupHeaderView.groupHeaderValue = value.value;
+ await waitForUpdatesAsync();
+ await disconnect();
+
+ const customLabelProviderValue = 'Custom label provider value';
+ labelProvider[value.labelProviderProperty] = customLabelProviderValue;
+
+ await connect();
+ await waitForUpdatesAsync();
+
+ expect(getRenderedText()).toBe(customLabelProviderValue);
+ }
+ );
+ });
+});
diff --git a/packages/nimble-components/src/table-column/text-base/group-header-view/index.ts b/packages/nimble-components/src/table-column/text-base/group-header-view/index.ts
index 299dc0008d..7da6d8df76 100644
--- a/packages/nimble-components/src/table-column/text-base/group-header-view/index.ts
+++ b/packages/nimble-components/src/table-column/text-base/group-header-view/index.ts
@@ -1,6 +1,11 @@
+import type { DesignTokenSubscriber } from '@microsoft/fast-foundation';
import { observable } from '@microsoft/fast-element';
import { TableGroupHeaderView } from '../../base/group-header-view';
import type { TableFieldValue } from '../../../table/types';
+import {
+ tableGroupRowPlaceholderEmptyLabel,
+ tableGroupRowPlaceholderNoValueLabel
+} from '../../../label-provider/table/label-tokens';
/**
* The group header view base class for displaying fields of any type as text.
@@ -15,7 +20,83 @@ export abstract class TableColumnTextGroupHeaderViewBase<
/**
* Text to render in the cell.
+ *
+ * The value is initialized to `tableGroupRowPlaceholderNoValue` because if the group
+ * row never has a value defined on it, the change handlers may never get called but
+ * the text needs to be correct.
*/
@observable
- public text = '';
+ public text = tableGroupRowPlaceholderNoValueLabel.getValueFor(this);
+
+ private readonly noValuePlaceholderLabelSubscriber: DesignTokenSubscriber<
+ typeof tableGroupRowPlaceholderNoValueLabel
+ > = {
+ handleChange: () => {
+ this.applyPlaceholderTextIfNeeded();
+ }
+ };
+
+ private readonly emptyPlaceholderLabelSubscriber: DesignTokenSubscriber<
+ typeof tableGroupRowPlaceholderEmptyLabel
+ > = {
+ handleChange: () => {
+ this.applyPlaceholderTextIfNeeded();
+ }
+ };
+
+ public override connectedCallback(): void {
+ super.connectedCallback();
+ tableGroupRowPlaceholderNoValueLabel.subscribe(
+ this.noValuePlaceholderLabelSubscriber,
+ this
+ );
+ tableGroupRowPlaceholderEmptyLabel.subscribe(
+ this.emptyPlaceholderLabelSubscriber,
+ this
+ );
+ this.applyPlaceholderTextIfNeeded();
+ }
+
+ public override disconnectedCallback(): void {
+ super.disconnectedCallback();
+ tableGroupRowPlaceholderNoValueLabel.unsubscribe(
+ this.noValuePlaceholderLabelSubscriber
+ );
+ tableGroupRowPlaceholderEmptyLabel.unsubscribe(
+ this.emptyPlaceholderLabelSubscriber
+ );
+ }
+
+ protected abstract updateText(): void;
+
+ private columnConfigChanged(): void {
+ if (!this.applyPlaceholderTextIfNeeded()) {
+ this.updateText();
+ }
+ }
+
+ private groupHeaderValueChanged(): void {
+ if (!this.applyPlaceholderTextIfNeeded()) {
+ this.updateText();
+ }
+ }
+
+ /**
+ * Sets `this.text` to the appropriate placeholder if `groupHeaderValue` warrants it.
+ * @returns `true` if `this.text` was set to a placeholder, `false` otherwise.
+ */
+ private applyPlaceholderTextIfNeeded(): boolean {
+ if (
+ this.groupHeaderValue === null
+ || this.groupHeaderValue === undefined
+ ) {
+ this.text = tableGroupRowPlaceholderNoValueLabel.getValueFor(this);
+ return true;
+ }
+ if (this.groupHeaderValue === '') {
+ this.text = tableGroupRowPlaceholderEmptyLabel.getValueFor(this);
+ return true;
+ }
+ return false;
+ }
}
diff --git a/packages/nimble-components/src/table-column/text/group-header-view/index.ts b/packages/nimble-components/src/table-column/text/group-header-view/index.ts
index 7ce723aeba..44f67102a2 100644
--- a/packages/nimble-components/src/table-column/text/group-header-view/index.ts
+++ b/packages/nimble-components/src/table-column/text/group-header-view/index.ts
@@ -17,7 +17,7 @@ export class TableColumnTextGroupHeaderView extends TableColumnTextGroupHeaderVi
TableStringFieldValue,
TableColumnTextColumnConfig
> {
- private groupHeaderValueChanged(): void {
+ protected updateText(): void {
this.text = typeof this.groupHeaderValue === 'string'
? this.groupHeaderValue
: '';
diff --git a/packages/nimble-components/src/table-column/text/tests/table-column-text-matrix.stories.ts b/packages/nimble-components/src/table-column/text/tests/table-column-text-matrix.stories.ts
new file mode 100644
index 0000000000..30e96422bf
--- /dev/null
+++ b/packages/nimble-components/src/table-column/text/tests/table-column-text-matrix.stories.ts
@@ -0,0 +1,68 @@
+import type { StoryFn, Meta } from '@storybook/html';
+import { html, ViewTemplate } from '@microsoft/fast-element';
+import {
+ createMatrixThemeStory,
+ createMatrix,
+ sharedMatrixParameters
+} from '../../../utilities/tests/matrix';
+import { Table, tableTag } from '../../../table';
+import { tableColumnTextTag } from '..';
+
+const metadata: Meta = {
+ title: 'Tests/Table Column: Text',
+ parameters: {
+ ...sharedMatrixParameters()
+ }
+};
+
+export default metadata;
+
+const data = [
+ {
+ id: '0',
+ firstName: 'Ralph'
+ },
+ {
+ id: '1',
+ firstName: 'Milhouse'
+ },
+ {
+ id: '2',
+ firstName: null
+ },
+ {
+ id: '3',
+ firstName: ''
+ }
+] as const;
+
+// prettier-ignore
+const component = (): ViewTemplate => html`
+ <${tableTag} id-field-name="id" style="height: 320px">
+ <${tableColumnTextTag}
+ field-name="id"
+ >
+ ID
+ ${tableColumnTextTag}>
+ <${tableColumnTextTag}
+ field-name="firstName"
+ group-index="0"
+ >
+ First name
+ ${tableColumnTextTag}>
+ ${tableTag}>
+`;
+
+export const tableColumnTextThemeMatrix: StoryFn = createMatrixThemeStory(
+ createMatrix(component)
+);
+
+tableColumnTextThemeMatrix.play = async (): Promise => {
+ await Promise.all(
+ Array.from(document.querySelectorAll('nimble-table')).map(
+ async table => {
+ await table.setData(data);
+ }
+ )
+ );
+};
diff --git a/packages/nimble-components/src/table-column/text/tests/table-column-text.spec.ts b/packages/nimble-components/src/table-column/text/tests/table-column-text.spec.ts
index 46b243c8d9..0061dcc639 100644
--- a/packages/nimble-components/src/table-column/text/tests/table-column-text.spec.ts
+++ b/packages/nimble-components/src/table-column/text/tests/table-column-text.spec.ts
@@ -1,41 +1,54 @@
-import { html } from '@microsoft/fast-element';
+import { html, ref } from '@microsoft/fast-element';
import { parameterizeSpec } from '@ni/jasmine-parameterized';
-import type { Table } from '../../../table';
+import { tableTag, type Table } from '../../../table';
import { TableColumnText, tableColumnTextTag } from '..';
import { waitForUpdatesAsync } from '../../../testing/async-helpers';
import { type Fixture, fixture } from '../../../utilities/tests/fixture';
import type { TableRecord } from '../../../table/types';
import { TablePageObject } from '../../../table/testing/table.pageobject';
import { wackyStrings } from '../../../utilities/tests/wacky-strings';
+import { themeProviderTag } from '../../../theme-provider';
interface SimpleTableRecord extends TableRecord {
field?: string | null;
anotherField?: string | null;
}
+class ElementReferences {
+ public table!: Table;
+ public column!: TableColumnText;
+}
+
// prettier-ignore
-async function setup(): Promise>> {
+async function setup(source: ElementReferences): Promise>> {
return fixture>(
- html`
- <${tableColumnTextTag} field-name="field" group-index="0">
- Column 1
- ${tableColumnTextTag}>
- <${tableColumnTextTag} field-name="anotherField">
- Squeeze Column 1
- ${tableColumnTextTag}>
- `
+ html`<${themeProviderTag} lang="en-US">
+ <${tableTag} ${ref('table')} style="width: 700px">
+ <${tableColumnTextTag} ${ref('column')} field-name="field" group-index="0">
+ Column 1
+ ${tableColumnTextTag}>
+ <${tableColumnTextTag} field-name="anotherField">
+ Squeeze Column 1
+ ${tableColumnTextTag}>
+ ${tableTag}>
+ ${themeProviderTag}>`,
+ { source }
);
}
describe('TableColumnText', () => {
- let element: Table;
+ let table: Table;
+ let column: TableColumnText;
let connect: () => Promise;
let disconnect: () => Promise;
let pageObject: TablePageObject;
beforeEach(async () => {
- ({ element, connect, disconnect } = await setup());
- pageObject = new TablePageObject(element);
+ const elementReferences = new ElementReferences();
+ ({ connect, disconnect } = await setup(elementReferences));
+ table = elementReferences.table;
+ column = elementReferences.column;
+ pageObject = new TablePageObject(table);
});
afterEach(async () => {
@@ -56,9 +69,7 @@ describe('TableColumnText', () => {
await connect();
await waitForUpdatesAsync();
- const firstColumn = element.columns[0] as TableColumnText;
-
- expect(firstColumn.checkValidity()).toBeTrue();
+ expect(column.checkValidity()).toBeTrue();
});
const noValueData = [
@@ -72,7 +83,7 @@ describe('TableColumnText', () => {
] as const;
parameterizeSpec(noValueData, (spec, name, value) => {
spec(`displays empty string when ${name}`, async () => {
- await element.setData(value.data);
+ await table.setData(value.data);
await connect();
await waitForUpdatesAsync();
@@ -81,38 +92,37 @@ describe('TableColumnText', () => {
});
it('changing fieldName updates display', async () => {
- await element.setData([{ field: 'foo', anotherField: 'bar' }]);
+ await table.setData([{ field: 'foo', anotherField: 'bar' }]);
await connect();
await waitForUpdatesAsync();
- const firstColumn = element.columns[0] as TableColumnText;
- firstColumn.fieldName = 'anotherField';
+ column.fieldName = 'anotherField';
await waitForUpdatesAsync();
expect(pageObject.getRenderedCellTextContent(0, 0)).toBe('bar');
});
it('changing data from value to null displays blank', async () => {
- await element.setData([{ field: 'foo' }]);
+ await table.setData([{ field: 'foo' }]);
await connect();
await waitForUpdatesAsync();
expect(pageObject.getRenderedCellTextContent(0, 0)).toBe('foo');
const updatedValue = { field: null };
const updatedData = [updatedValue];
- await element.setData(updatedData);
+ await table.setData(updatedData);
await waitForUpdatesAsync();
expect(pageObject.getRenderedCellTextContent(0, 0)).toBe('');
});
it('changing data from null to value displays value', async () => {
- await element.setData([{ field: null }]);
+ await table.setData([{ field: null }]);
await connect();
await waitForUpdatesAsync();
expect(pageObject.getRenderedCellTextContent(0, 0)).toBe('');
- await element.setData([{ field: 'foo' }]);
+ await table.setData([{ field: 'foo' }]);
await waitForUpdatesAsync();
expect(pageObject.getRenderedCellTextContent(0, 0)).toBe('foo');
@@ -122,9 +132,8 @@ describe('TableColumnText', () => {
await connect();
await waitForUpdatesAsync();
- const firstColumn = element.columns[0] as TableColumnText;
- firstColumn.fieldName = undefined;
- await element.setData([{ field: 'foo' }]);
+ column.fieldName = undefined;
+ await table.setData([{ field: 'foo' }]);
await waitForUpdatesAsync();
expect(pageObject.getRenderedCellTextContent(0, 0)).toBe('');
@@ -132,7 +141,7 @@ describe('TableColumnText', () => {
it('sets title when cell text is ellipsized', async () => {
const cellContents = 'a very long value that should get ellipsized due to not fitting within the default cell width';
- await element.setData([{ field: cellContents }]);
+ await table.setData([{ field: cellContents }]);
await connect();
await waitForUpdatesAsync();
pageObject.dispatchEventToCell(0, 0, new MouseEvent('mouseover'));
@@ -142,7 +151,7 @@ describe('TableColumnText', () => {
it('does not set title when cell text is fully visible', async () => {
const cellContents = 'short value';
- await element.setData([{ field: cellContents }]);
+ await table.setData([{ field: cellContents }]);
await connect();
await waitForUpdatesAsync();
pageObject.dispatchEventToCell(0, 0, new MouseEvent('mouseover'));
@@ -152,7 +161,7 @@ describe('TableColumnText', () => {
it('removes title on mouseout of cell', async () => {
const cellContents = 'a very long value that should get ellipsized due to not fitting within the default cell width';
- await element.setData([{ field: cellContents }]);
+ await table.setData([{ field: cellContents }]);
await connect();
await waitForUpdatesAsync();
pageObject.dispatchEventToCell(0, 0, new MouseEvent('mouseover'));
@@ -164,8 +173,8 @@ describe('TableColumnText', () => {
it('sets title when group header text is ellipsized', async () => {
const cellContents = 'a very long value that should get ellipsized due to not fitting within the default cell width';
- await element.setData([{ field: cellContents }]);
- element.style.width = '200px';
+ await table.setData([{ field: cellContents }]);
+ table.style.width = '200px';
await connect();
await waitForUpdatesAsync();
pageObject.dispatchEventToGroupHeader(0, new MouseEvent('mouseover'));
@@ -175,7 +184,7 @@ describe('TableColumnText', () => {
it('does not set title when group header text is fully visible', async () => {
const cellContents = 'foo';
- await element.setData([{ field: cellContents }]);
+ await table.setData([{ field: cellContents }]);
await connect();
await waitForUpdatesAsync();
pageObject.dispatchEventToGroupHeader(0, new MouseEvent('mouseover'));
@@ -185,7 +194,7 @@ describe('TableColumnText', () => {
it('removes title on mouseout of group header', async () => {
const cellContents = 'a very long value that should get ellipsized due to not fitting within the default cell width';
- await element.setData([{ field: cellContents }]);
+ await table.setData([{ field: cellContents }]);
await connect();
await waitForUpdatesAsync();
pageObject.dispatchEventToGroupHeader(0, new MouseEvent('mouseover'));
@@ -200,7 +209,7 @@ describe('TableColumnText', () => {
spec(`data "${name}" renders as "${name}"`, async () => {
await connect();
- await element.setData([{ field: name }]);
+ await table.setData([{ field: name }]);
await waitForUpdatesAsync();
expect(pageObject.getRenderedCellTextContent(0, 0)).toBe(name);
@@ -213,7 +222,7 @@ describe('TableColumnText', () => {
spec(`data "${name}" renders as "${name}"`, async () => {
await connect();
- await element.setData([{ field: name }]);
+ await table.setData([{ field: name }]);
await waitForUpdatesAsync();
expect(
@@ -222,4 +231,46 @@ describe('TableColumnText', () => {
});
});
});
+
+ describe('placeholder', () => {
+ const testCases = [
+ {
+ name: 'value is not specified',
+ data: [{}],
+ groupValue: 'No value'
+ },
+ {
+ name: 'value is undefined',
+ data: [{ field: undefined }],
+ groupValue: 'No value'
+ },
+ {
+ name: 'value is null',
+ data: [{ field: null }],
+ groupValue: 'No value'
+ },
+ {
+ name: 'value is incorrect type',
+ data: [{ field: 100 as unknown as string }],
+ groupValue: ''
+ },
+ {
+ name: 'value is an empty string',
+ data: [{ field: '' }],
+ groupValue: 'Empty'
+ }
+ ];
+
+ parameterizeSpec(testCases, (spec, name, value) => {
+ spec(`group row renders expected value when ${name}`, async () => {
+ await table.setData(value.data);
+ await connect();
+ await waitForUpdatesAsync();
+
+ expect(pageObject.getRenderedGroupHeaderTextContent(0)).toBe(
+ value.groupValue
+ );
+ });
+ });
+ });
});
diff --git a/packages/nimble-components/src/table/components/header/tests/table-header.spec.ts b/packages/nimble-components/src/table/components/header/tests/table-header.spec.ts
index 73afb916b5..fd72467373 100644
--- a/packages/nimble-components/src/table/components/header/tests/table-header.spec.ts
+++ b/packages/nimble-components/src/table/components/header/tests/table-header.spec.ts
@@ -46,8 +46,7 @@ describe('TableHeader', () => {
expect(pageObject.isSortDescendingIconVisible()).toBeFalse();
});
- // Firefox skipped, see: https://github.com/ni/nimble/issues/1075
- it('has correct state when sorted ascending #SkipFirefox', async () => {
+ it('has correct state when sorted ascending', async () => {
element.sortDirection = TableColumnSortDirection.ascending;
element.firstSortedColumn = true;
await waitForUpdatesAsync();
@@ -57,8 +56,7 @@ describe('TableHeader', () => {
expect(pageObject.isSortDescendingIconVisible()).toBeFalse();
});
- // Firefox skipped, see: https://github.com/ni/nimble/issues/1075
- it('has correct state when sorted descending #SkipFirefox', async () => {
+ it('has correct state when sorted descending', async () => {
element.sortDirection = TableColumnSortDirection.descending;
element.firstSortedColumn = true;
await waitForUpdatesAsync();
diff --git a/packages/nimble-components/src/table/specs/spec-images/PlacholderText.png b/packages/nimble-components/src/table/specs/spec-images/PlacholderText.png
new file mode 100644
index 0000000000..8b124f440d
Binary files /dev/null and b/packages/nimble-components/src/table/specs/spec-images/PlacholderText.png differ
diff --git a/packages/nimble-components/src/table/specs/table-column-placeholder-hld.md b/packages/nimble-components/src/table/specs/table-column-placeholder-hld.md
new file mode 100644
index 0000000000..39f93b9dc8
--- /dev/null
+++ b/packages/nimble-components/src/table/specs/table-column-placeholder-hld.md
@@ -0,0 +1,205 @@
+# Placeholders for table columns HLD
+
+## Problem Statement
+
+In some cases, an application may want to display a placeholder value in the table when a record does not have a value for a given field. The application should be able to customize that placeholder based on their needs.
+
+## Links To Relevant Work Items and Reference Material
+
+- [Nimble issue 1538](https://github.com/ni/nimble/issues/1538)
+- [Nimble issue 1511](https://github.com/ni/nimble/issues/1511)
+
+## Implementation / Design
+
+### High-level behavior
+
+There are two places where a value is displayed in the table that placeholders need to be considered: (1) in a cell and (2) in a group row. The cell placeholder will be configurable on each column that supports having a placeholder, and it will default to an empty string. The group row placeholder will come from the table's localization provider, and it will not be configurable through a column's API.
+
+Placeholders within a cell will be rendered with nimble's placeholder font, which is currently 60% opacity of nimble's body font.
+
+Placeholders within a group row will have no special visual treatment.
+
+Placeholder values will behave consistently with other strings rendered in the table in that they will truncate with an ellipsis and title if they are longer than the available space. They will be sorted based on the record value rather than the placeholder string.
+
+Below is an example of what placeholders will look like in the table. In this example, the table is grouped by the "Quote" column, which has been configured to have a placeholder value of "None".
+
+![Placeholder text example](./spec-images/PlacholderText.png)
+
+### Column-specific decisions
+
+The exact behavior of placeholders in each existing table column is described below.
+
+#### Text column
+
+| Special-cased field values | Cell display | Group row display |
+| -------------------------- | ------------------------------------------------------------ | ----------------- |
+| `undefined` | column placeholder, or empty if no placeholder is configured | `"No value"` |
+| `null` | column placeholder, or empty if no placeholder is configured | `"No value"` |
+| `''`\* | \ | `"Empty"` |
+
+\*Only empty string (`''`) is treated as a special case for group row placeholders. Other whitespace values will be rendered as-is and should be pre-processed as appropriate by the application.
+
+Column best practices:
+
+- Avoid mixing `undefined` and `null` as values for the same field. When grouping this will lead to two groups (one for `null` values and one for `undefined` values) that both have the text `"No value"`.
+- Avoid mixing empty string with `undefined`/`null`. The distinction when grouping between `"No value"` and `"Empty"` is not likely meaningful to a user.
+- Avoid displaying whitespace values that are not empty string (`''`) as these values will be rendered as-is in group rows.
+
+#### Anchor column
+
+| Special-cased field values | Cell display | Group row display |
+| --------------------------------------------------- | ------------------------------------------------------------------ | ----------------- |
+| Both label and href are `undefined` or `null` | column placeholder, or empty if no placeholder is configured | `"No value"` |
+| Label is `undefined` or `null` with defined href | href value is used as the link's href and the link's display value | `"No value"` |
+| Label is defined with href of `undefined` or `null` | label as a plain string with no link | The label |
+| Label is `''` with any href\* | \ | `"Empty"` |
+
+\*Only empty string (`''`) is treated as a special case for group row placeholders. Other whitespace values will be rendered as-is and should be pre-processed as appropriate by the application.
+
+Column best practices:
+
+- Provide useful labels for well known urls. While an absent label will show the full URL for accessibility, it is useful to instead provide a clear and unique label to improve grouping.
+ - For example, a column of links to notebooks where a notebook may no longer exist, and thus a label is not available, could pre-process the notebook urls and create the label `Missing Notebook (UNIQUE_NOTEBOOK_ID)`. This allows multiple rows referencing the same missing notebook to be grouped together.
+ - Alternatively if the urls are not well-known structures, the application should explicitly provide the href as the label to keep unique labels and preserve grouping as opposed to using `null` / `undefined` labels.
+- Applications should avoid having duplicate labels to different hrefs as those are inaccessible to screen readers (and sighted users). See [high-level discussion](https://fae.disability.illinois.edu/rulesets/LINK_2/) of [aria SC 2.4.4](https://www.w3.org/TR/WCAG22/#link-purpose-in-context).
+ - For example, applications should avoid having `undefined` / `null` as the label as that causes multiple unrelated URLs to be grouped together under the group label "No value". Accessibility is okay as the full url will be shown but the value of grouping is limited.
+ - For example, if a label is missing, an application should avoid generating a non-unique label for multiple URLs (i.e. `Missing Notebook`) as that harms accessibility and limits the value of grouping.
+- Avoid using empty string or other whitespace-only labels with defined hrefs. This will cause the rendered anchor to have no text associated with it, and it will be difficult for a user to see that the anchor exists.
+- Applications may leave the href as `null` / `undefined` to have the anchor column behave effectively like a string column
+- Avoid mixing `undefined` and `null` as values for the label field. When grouping this will lead to two groups (one for `null` values and one for `undefined` values) that both have the text `"No value"`.
+ - As explained above, it is not recommended to use `undefined` or `null` labels when the data has defined hrefs.
+- Avoid mixing empty string with `undefined`/`null` as values for the label field. The distinction when grouping between `"No value"` and `"Empty"` is not likely meaningful to a user.
+ - As explained above, it is not recommended to use empty string, `undefined`, or `null` labels when the data has defined hrefs.
+
+#### Number column
+
+| Special-cased field values | Cell display | Group row display |
+| -------------------------- | ------------------------------------------------------------ | ----------------- |
+| `undefined` | column placeholder, or empty if no placeholder is configured | `"No value"` |
+| `null` | column placeholder, or empty if no placeholder is configured | `"No value"` |
+
+The alignment of the placeholder in the cell will match the alignment of the number in the column.
+
+Column best practices:
+
+- Avoid mixing `undefined` and `null` as values for the same field. When grouping this will lead to two groups (one for `null` values and one for `undefined` values) that both have the text `"No value"`.
+- If relevant to your data source, make sure to consider the IEEE 754 special cases of `-Inf`, `+Inf`, `-0`, `+0`, and `NaN`.
+
+#### Date column
+
+| Special-cased field values | Cell display | Group row display |
+| ----------------------------------- | ------------------------------------------------------------ | ------------------ |
+| `undefined` | column placeholder, or empty if no placeholder is configured | `"No value"` |
+| `null` | column placeholder, or empty if no placeholder is configured | `"No value"` |
+| Invalid value (e.g. `Number.NaN`)\* | \ | \ |
+
+\*This is considered invalid data from the table's perspective and should be fixed within the client application.
+
+Column best practices:
+
+- Avoid mixing `undefined` and `null` as values for the same field. When grouping this will lead to two groups (one for `null` values and one for `undefined` values) that both have the text `"No value"`.
+
+#### Duration column
+
+| Special-cased field values | Cell display | Group row display |
+| ----------------------------------- | ------------------------------------------------------------ | ------------------ |
+| `undefined` | column placeholder, or empty if no placeholder is configured | `"No value"` |
+| `null` | column placeholder, or empty if no placeholder is configured | `"No value"` |
+| Invalid value (e.g. `Number.NaN`)\* | \ | \ |
+
+\*This is considered invalid data from the table's perspective and should be fixed within the client application.
+
+Column best practices:
+
+- Avoid mixing `undefined` and `null` as values for the same field. When grouping this will lead to two groups (one for `null` values and one for `undefined` values) that both have the text `"No value"`.
+
+#### Icon mapping column
+
+The icon mapping column will not have a configuration for a placeholder.
+
+| Special-cased field values | Cell display | Group row display |
+| -------------------------- | ------------- | ------------------ |
+| `undefined` | \ | `"No value"` |
+| `null` | \ | `"No value"` |
+| Non-mapped value\* | \ | \ |
+
+\*This is considered invalid data from the table's perspective and should be fixed within the client application.
+
+Column best practices:
+
+- Avoid mixing `undefined` and `null` as values for the same field. When grouping this will lead to two groups (one for `null` values and one for `undefined` values) that both have the text `"No value"`.
+- Avoid using values that do not correspond to a mapping for the column.
+- To display an empty cell but have a non-blank group row, create a mapping of the record value to an `undefined` icon.
+
+#### Text mapping column
+
+The text mapping column will not have a configuration for a placeholder.
+
+| Special-cased field values | Cell display | Group row display |
+| -------------------------- | ------------- | ------------------ |
+| `undefined` | \ | `"No value"` |
+| `null` | \ | `"No value"` |
+| Non-mapped value\* | \ | \ |
+
+\*This is considered invalid data from the table's perspective and should be fixed within the client application.
+
+Column best practices:
+
+- Avoid mixing `undefined` and `null` as values for the same field. When grouping this will lead to two groups (one for `null` values and one for `undefined` values) that both have the text `"No value"`.
+- Avoid using values that do not correspond to a mapping for the column.
+
+### Implementation plan
+
+A column's placeholder will be stored as part of that column's `columnConfig` object. There will be no changes to the `TableColumn`, `ColumnInternals`, or `ColumnInternalsOptions` classes. The placeholder from the `columnConfig` object will be used by the cell views when rendering the cell.
+
+We will create a placeholder mixin that adds the following to columns that chose to use it:
+
+- `placeholder` string property
+- `placeholder` attribute
+- abstract `placeholderChanged` function that will force columns using the mixin to implement `placeholderChanged` to update their column configuration
+
+The columns that will be updated to use this mixin are:
+
+- TableColumnText
+- TableColumnAnchor
+- TableColumnNumberText
+- TableColumnDateText
+- TableColumnDurationText
+
+### Localization
+
+All group row placeholder strings will be localized through the table's localization provider. Those strings are:
+
+- No value
+- Empty
+
+If an application is localized, it can set a column's `placeholder` to a localized value.
+
+## Alternative Implementations / Designs
+
+### Configurable placeholders in group rows
+
+In addition to extending column APIs to have a `placeholder` property, they could also be extended to have a `group-placeholder` property. However, this level of configuration is not required by any client applications today. This feature is also likely to introduce inconsistency throughout our applications.
+
+If an application wants to modify the value of a group row placeholder without this feature, they can do so through the localization provider.
+
+### Allow different placeholders to be configured for each cell
+
+We could add the ability to have different placeholders configured for each cell, but this poses a few different problems:
+
+1. This would likely need to be a drastically different API, such as having the placeholder specified in the record. That would lead to quite a bit of duplicate information being set on the table, particularly when there are no use cases for this right now.
+1. This would lead to a confusing state for the user because the placholder would be different for various rows, but all those rows would be in a single group.
+
+A use case for different information being presented to the user for each cell will likely be solved by [a different feature to show cell-specific state](https://github.com/ni/nimble/issues/1776).
+
+## Future Work
+
+Future columns should consider adding a placeholder as part of their API. The general guidance for placeholders is:
+
+- Group rows should always have a non-blank value to display, assuming the data provided to the table is valid.
+- A column's API should support an application being able to render placeholder text in cell when the record value is `undefined` or `null`.
+ - There are exceptions to this, such as the enum text column because the client application is expected to provide only known values that correspond to specified mappings.
+
+## Open Issues
+
+_None_
diff --git a/packages/nimble-components/src/table/tests/table-column-sizing.spec.ts b/packages/nimble-components/src/table/tests/table-column-sizing.spec.ts
index bbdc7e8086..3b32cfa666 100644
--- a/packages/nimble-components/src/table/tests/table-column-sizing.spec.ts
+++ b/packages/nimble-components/src/table/tests/table-column-sizing.spec.ts
@@ -496,14 +496,17 @@ describe('Table Interactive Column Sizing', () => {
});
it('sizing table with a horizontal scrollbar does not change column widths until sized beyond current column pixel widths', async () => {
- // create horizontal scrollbar with total column width of 450
- pageObject.dragSizeColumnByRightDivider(2, [100]);
+ // Create a horizontal scrollbar with a total column width of 500. This updates the columns'
+ // current fractional widths to 0.8, 0.8, 2, and 0.4, which keeps the columns widths as
+ // integers when the table is resized later in the test. Otherwise, different browsers
+ // may have slightly different rounding behaviors.
+ pageObject.dragSizeColumnByRightDivider(2, [150]);
// size table below threshhold of total column widths
await pageObject.sizeTableToGivenRowWidth(425, element);
- expect(pageObject.getTotalCellRenderedWidth()).toBe(450);
- // size table 50 pixels beyond total column widths
- await pageObject.sizeTableToGivenRowWidth(500, element);
expect(pageObject.getTotalCellRenderedWidth()).toBe(500);
+ // size table 100 pixels beyond total column widths
+ await pageObject.sizeTableToGivenRowWidth(600, element);
+ expect(pageObject.getTotalCellRenderedWidth()).toBe(600);
expect(pageObject.isHorizontalScrollbarVisible()).toBeFalse();
});
diff --git a/packages/nimble-components/src/table/tests/table.spec.ts b/packages/nimble-components/src/table/tests/table.spec.ts
index 8d0fad78c7..0eaaa3ee11 100644
--- a/packages/nimble-components/src/table/tests/table.spec.ts
+++ b/packages/nimble-components/src/table/tests/table.spec.ts
@@ -677,7 +677,8 @@ describe('Table', () => {
verifyRenderedData(dataSubsetAtEnd);
});
- it('and calls focusedRecycleCallback on focused cell views when a scroll happens', async () => {
+ // WebKit skipped, see https://github.com/ni/nimble/issues/1942
+ it('and calls focusedRecycleCallback on focused cell views when a scroll happens #SkipWebkit', async () => {
const focusableColumn = document.createElement(
focusableColumnName
) as TestFocusableTableColumn;
diff --git a/packages/nimble-components/src/tests/component-status.stories.ts b/packages/nimble-components/src/tests/component-status.stories.ts
index 37ec7819a1..ba39a01fd9 100644
--- a/packages/nimble-components/src/tests/component-status.stories.ts
+++ b/packages/nimble-components/src/tests/component-status.stories.ts
@@ -557,7 +557,7 @@ const metadata: Meta = {
${ref('tableRef')}
data-unused="${x => x.updateData(x)}"
${/* Make the table big enough to remove vertical scrollbar */ ''}
- style="height: ${x => (x.status === 'active' ? '1200px' : '600px')};"
+ style="height: calc((34px * var(--data-length)) + 32px);"
>
<${tableColumnAnchorTag} target="_top"
column-id="component-name-column"
@@ -644,6 +644,10 @@ const metadata: Meta = {
const data = components.filter(component => (x.status === 'future'
? isFuture(component)
: !isFuture(component)));
+ x.tableRef.style.setProperty(
+ '--data-length',
+ data.length.toString()
+ );
await x.tableRef.setData(data);
})();
},
diff --git a/packages/nimble-components/src/tests/nimble-intro.stories.mdx b/packages/nimble-components/src/tests/nimble-intro.mdx
similarity index 100%
rename from packages/nimble-components/src/tests/nimble-intro.stories.mdx
rename to packages/nimble-components/src/tests/nimble-intro.mdx
diff --git a/packages/nimble-components/src/tests/patterns.mdx b/packages/nimble-components/src/tests/patterns.mdx
index cae683ef0b..55fa0d3bfc 100644
--- a/packages/nimble-components/src/tests/patterns.mdx
+++ b/packages/nimble-components/src/tests/patterns.mdx
@@ -1,6 +1,6 @@
import { Meta } from '@storybook/addon-docs';
-
+
# Patterns
diff --git a/packages/nimble-components/src/theme-provider/design-token-comments.ts b/packages/nimble-components/src/theme-provider/design-token-comments.ts
index 7e38139d14..986dfb39f3 100644
--- a/packages/nimble-components/src/theme-provider/design-token-comments.ts
+++ b/packages/nimble-components/src/theme-provider/design-token-comments.ts
@@ -13,8 +13,6 @@ export const comments: { readonly [key in TokenName]: string | null } = {
'Control fill color for "primary" appearance-variant buttons',
buttonPrimaryFontColor:
'Font color for "primary" appearance-variant buttons',
- buttonFillActivePrimaryColor:
- 'Active fill color for "primary" appearance-variant buttons',
buttonFillAccentColor:
'Control fill color for "accent" appearance-variant buttons',
buttonAccentBlockFontColor:
@@ -23,8 +21,6 @@ export const comments: { readonly [key in TokenName]: string | null } = {
'Font color for "accent" appearance-variant outline buttons',
buttonBorderAccentOutlineColor:
'Border color for "accent" appearance-variant outline buttons',
- buttonFillAccentActiveColor:
- 'Active fill color for "accent" appearance-variant buttons',
fillSelectedColor: 'Control fill color when a control is selected',
fillSelectedRgbPartialColor:
'DEPRECATED: *-partial tokens are used with rgba() to set color transparency in component stylesheets',
@@ -210,6 +206,14 @@ export const comments: { readonly [key in TokenName]: string | null } = {
'Font line height for the "Placeholder" base token',
placeholderFallbackFontFamily:
'Fallback font family for the "Placeholder" base token',
+ bodyFont: 'Font shorthand for the "Body" base token',
+ bodyFontColor: 'Font color for the "Body" base token',
+ bodyDisabledFontColor: 'Disabled font color for the "Body" base token',
+ bodyFontFamily: 'Font family for the "Body" base token',
+ bodyFontSize: 'Font size for the "Body" base token',
+ bodyFontWeight: 'Font weight for the "Body" base token',
+ bodyFontLineHeight: 'Font line height for the "Body" base token',
+ bodyFallbackFontFamily: 'Fallback font family for the "Body" base token',
bodyEmphasizedFont: 'Font shorthand for the "BodyEmphasized" base token',
bodyEmphasizedFontColor: 'Font color for the "BodyEmphasized" base token',
bodyEmphasizedDisabledFontColor:
@@ -221,30 +225,32 @@ export const comments: { readonly [key in TokenName]: string | null } = {
'Font line height for the "BodyEmphasized" base token',
bodyEmphasizedFallbackFontFamily:
'Fallback font family for the "BodyEmphasized" base token',
- bodyEmphasizedPlus1Font:
+ bodyPlus1Font: 'Font shorthand for the "Body_2" base token',
+ bodyPlus1FontColor: 'Font color for the "Body_2" base token',
+ bodyPlus1DisabledFontColor:
+ 'Disabled font color for the "Body_2" base token',
+ bodyPlus1FontFamily: 'Font family for the "Body_2" base token',
+ bodyPlus1FontSize: 'Font size for the "Body_2" base token',
+ bodyPlus1FontWeight: 'Font weight for the "Body_2" base token',
+ bodyPlus1FontLineHeight: 'Font line height for the "Body_2" base token',
+ bodyPlus1FallbackFontFamily:
+ 'Fallback font family for the "Body_2" base token',
+ bodyPlus1EmphasizedFont:
'Font shorthand for the "BodyEmphasized_2" base token',
- bodyEmphasizedPlus1FontColor:
+ bodyPlus1EmphasizedFontColor:
'Font color for the "BodyEmphasized_2" base token',
- bodyEmphasizedPlus1DisabledFontColor:
+ bodyPlus1EmphasizedDisabledFontColor:
'Disabled font color for the "BodyEmphasized_2" base token',
- bodyEmphasizedPlus1FontFamily:
+ bodyPlus1EmphasizedFontFamily:
'Font family for the "BodyEmphasized_2" base token',
- bodyEmphasizedPlus1FontSize:
+ bodyPlus1EmphasizedFontSize:
'Font size for the "BodyEmphasized_2" base token',
- bodyEmphasizedPlus1FontWeight:
+ bodyPlus1EmphasizedFontWeight:
'Font weight for the "BodyEmphasized_2" base token',
- bodyEmphasizedPlus1FontLineHeight:
+ bodyPlus1EmphasizedFontLineHeight:
'Font line height for the "BodyEmphasized_2" base token',
- bodyEmphasizedPlus1FallbackFontFamily:
+ bodyPlus1EmphasizedFallbackFontFamily:
'Fallback font family for the "BodyEmphasized_2" base token',
- bodyFont: 'Font shorthand for the "Body" base token',
- bodyFontColor: 'Font color for the "Body" base token',
- bodyDisabledFontColor: 'Disabled font color for the "Body" base token',
- bodyFontFamily: 'Font family for the "Body" base token',
- bodyFontSize: 'Font size for the "Body" base token',
- bodyFontWeight: 'Font weight for the "Body" base token',
- bodyFontLineHeight: 'Font line height for the "Body" base token',
- bodyFallbackFontFamily: 'Fallback font family for the "Body" base token',
groupHeaderFont: 'Font shorthand for the "Group_Header_1" base token',
groupHeaderFontColor: 'Font color for the "Group_Header_1" base token',
groupHeaderDisabledFontColor:
diff --git a/packages/nimble-components/src/theme-provider/design-token-names.ts b/packages/nimble-components/src/theme-provider/design-token-names.ts
index 3725fadbba..6ba34439e4 100644
--- a/packages/nimble-components/src/theme-provider/design-token-names.ts
+++ b/packages/nimble-components/src/theme-provider/design-token-names.ts
@@ -15,12 +15,10 @@ export const tokenNames: { readonly [key in TokenName]: string } = {
sectionBackgroundColor: 'section-background-color',
buttonFillPrimaryColor: 'button-fill-primary-color',
buttonPrimaryFontColor: 'button-primary-font-color',
- buttonFillActivePrimaryColor: 'button-fill-active-primary-color',
buttonFillAccentColor: 'button-fill-accent-color',
buttonAccentBlockFontColor: 'button-accent-block-font-color',
buttonAccentOutlineFontColor: 'button-accent-outline-font-color',
buttonBorderAccentOutlineColor: 'button-border-accent-outline-color',
- buttonFillAccentActiveColor: 'button-fill-accent-active-color',
fillSelectedColor: 'fill-selected-color',
fillSelectedRgbPartialColor: 'fill-selected-rgb-partial-color',
fillHoverSelectedColor: 'fill-hover-selected-color',
@@ -169,25 +167,6 @@ export const tokenNames: { readonly [key in TokenName]: string } = {
placeholderFontWeight: 'placeholder-font-weight',
placeholderFontLineHeight: 'placeholder-font-line-height',
placeholderFallbackFontFamily: 'placeholder-fallback-font-family',
- bodyEmphasizedFont: 'body-emphasized-font',
- bodyEmphasizedFontColor: 'body-emphasized-font-color',
- bodyEmphasizedDisabledFontColor: 'body-emphasized-disabled-font-color',
- bodyEmphasizedFontFamily: 'body-emphasized-font-family',
- bodyEmphasizedFontSize: 'body-emphasized-font-size',
- bodyEmphasizedFontWeight: 'body-emphasized-font-weight',
- bodyEmphasizedFontLineHeight: 'body-emphasized-font-line-height',
- bodyEmphasizedFallbackFontFamily: 'body-emphasized-fallback-font-family',
- bodyEmphasizedPlus1Font: 'body-emphasized-plus-1-font',
- bodyEmphasizedPlus1FontColor: 'body-emphasized-plus-1-font-color',
- bodyEmphasizedPlus1DisabledFontColor:
- 'body-emphasized-plus-1-disabled-font-color',
- bodyEmphasizedPlus1FontFamily: 'body-emphasized-plus-1-font-family',
- bodyEmphasizedPlus1FontSize: 'body-emphasized-plus-1-font-size',
- bodyEmphasizedPlus1FontWeight: 'body-emphasized-plus-1-font-weight',
- bodyEmphasizedPlus1FontLineHeight:
- 'body-emphasized-plus-1-font-line-height',
- bodyEmphasizedPlus1FallbackFontFamily:
- 'body-emphasized-plus-1-fallback-font-family',
bodyFont: 'body-font',
bodyFontColor: 'body-font-color',
bodyDisabledFontColor: 'body-disabled-font-color',
@@ -196,6 +175,33 @@ export const tokenNames: { readonly [key in TokenName]: string } = {
bodyFontWeight: 'body-font-weight',
bodyFontLineHeight: 'body-font-line-height',
bodyFallbackFontFamily: 'body-fallback-font-family',
+ bodyEmphasizedFont: 'body-emphasized-font',
+ bodyEmphasizedFontColor: 'body-emphasized-font-color',
+ bodyEmphasizedDisabledFontColor: 'body-emphasized-disabled-font-color',
+ bodyEmphasizedFontFamily: 'body-emphasized-font-family',
+ bodyEmphasizedFontSize: 'body-emphasized-font-size',
+ bodyEmphasizedFontWeight: 'body-emphasized-font-weight',
+ bodyEmphasizedFontLineHeight: 'body-emphasized-font-line-height',
+ bodyEmphasizedFallbackFontFamily: 'body-emphasized-fallback-font-family',
+ bodyPlus1Font: 'body-plus-1-font',
+ bodyPlus1FontColor: 'body-plus-1-font-color',
+ bodyPlus1DisabledFontColor: 'body-plus-1-disabled-font-color',
+ bodyPlus1FontFamily: 'body-plus-1-font-family',
+ bodyPlus1FontSize: 'body-plus-1-font-size',
+ bodyPlus1FontWeight: 'body-plus-1-font-weight',
+ bodyPlus1FontLineHeight: 'body-plus-1-font-line-height',
+ bodyPlus1FallbackFontFamily: 'body-plus-1-fallback-font-family',
+ bodyPlus1EmphasizedFont: 'body-plus-1-emphasized-font',
+ bodyPlus1EmphasizedFontColor: 'body-plus-1-emphasized-font-color',
+ bodyPlus1EmphasizedDisabledFontColor:
+ 'body-plus-1-emphasized-disabled-font-color',
+ bodyPlus1EmphasizedFontFamily: 'body-plus-1-emphasized-font-family',
+ bodyPlus1EmphasizedFontSize: 'body-plus-1-emphasized-font-size',
+ bodyPlus1EmphasizedFontWeight: 'body-plus-1-emphasized-font-weight',
+ bodyPlus1EmphasizedFontLineHeight:
+ 'body-plus-1-emphasized-font-line-height',
+ bodyPlus1EmphasizedFallbackFontFamily:
+ 'body-plus-1-emphasized-fallback-font-family',
groupHeaderFont: 'group-header-font',
groupHeaderFontColor: 'group-header-font-color',
groupHeaderDisabledFontColor: 'group-header-disabled-font-color',
@@ -270,6 +276,7 @@ export const scssInternalPropertySetterMarkdown = (
// Order of suffixes in the array matter, as we want single word suffixes after the multi-word ones
const tokenSuffixes = [
'RgbPartialColor',
+ 'DisabledFontColor',
'FontColor',
'FontLineHeight',
'FontWeight',
diff --git a/packages/nimble-components/src/theme-provider/design-tokens.ts b/packages/nimble-components/src/theme-provider/design-tokens.ts
index 0261fe694d..dcb6b0fb05 100644
--- a/packages/nimble-components/src/theme-provider/design-tokens.ts
+++ b/packages/nimble-components/src/theme-provider/design-tokens.ts
@@ -23,6 +23,9 @@ import {
BodyFamily,
BodySize,
BodyWeight,
+ Body2Family,
+ Body2Size,
+ Body2Weight,
ControlLabel1Family,
ControlLabel1Size,
ControlLabel1Weight,
@@ -83,6 +86,7 @@ import {
BodyEmphasizedLineHeight,
BodyEmphasized2LineHeight,
BodyLineHeight,
+ Body2LineHeight,
GroupLabel1LineHeight,
ControlLabel1LineHeight,
ButtonLabel1LineHeight,
@@ -94,8 +98,7 @@ import {
GridHeaderFamily,
GridHeaderWeight,
GridHeaderSize,
- DigitalGreenDark105,
- DigitalGreenDark110
+ DigitalGreenDark105
} from '@ni/nimble-tokens/dist/styledictionary/js/tokens';
import {
modalBackdropColorThemeColorStatic,
@@ -118,6 +121,7 @@ const Subtitle2FallbackFontFamily = 'Source Sans Pro Fallback';
const LinkFallbackFontFamily = 'Source Sans Pro Fallback';
const PlaceholderFallbackFontFamily = 'Source Sans Pro Fallback';
const BodyFallbackFontFamily = 'Source Sans Pro Fallback';
+const Body2FallbackFontFamily = 'Source Sans Pro Fallback';
const BodyEmphasizedFallbackFontFamily = 'Source Sans Pro Fallback';
const BodyEmphasized2FallbackFontFamily = 'Source Sans Pro Fallback';
const GroupLabel1FallbackFontFamily = 'Source Sans Pro Fallback';
@@ -243,15 +247,6 @@ export const buttonPrimaryFontColor = DesignToken.create(
styleNameFromTokenName(tokenNames.buttonPrimaryFontColor)
).withDefault((element: HTMLElement) => getColorForTheme(element, Black15, Black15, White));
-export const buttonFillActivePrimaryColor = DesignToken.create(
- styleNameFromTokenName(tokenNames.buttonFillActivePrimaryColor)
-).withDefault((element: HTMLElement) => getColorForTheme(
- element,
- hexToRgbaCssColor(Black91, 0.85),
- hexToRgbaCssColor(Black15, 0.2),
- hexToRgbaCssColor(White, 0.2)
-));
-
export const buttonFillAccentColor = DesignToken.create(
styleNameFromTokenName(tokenNames.buttonFillAccentColor)
).withDefault((element: HTMLElement) => getColorForTheme(
@@ -278,15 +273,6 @@ export const buttonBorderAccentOutlineColor = DesignToken.create(
hexToRgbaCssColor(White, 0.3)
));
-export const buttonFillAccentActiveColor = DesignToken.create(
- styleNameFromTokenName(tokenNames.buttonFillAccentActiveColor)
-).withDefault((element: HTMLElement) => getColorForTheme(
- element,
- DigitalGreenDark110,
- DigitalGreenDark,
- hexToRgbaCssColor(White, 0.2)
-));
-
// Component Sizing Tokens
export const controlHeight = DesignToken.create(
styleNameFromTokenName(tokenNames.controlHeight)
@@ -555,8 +541,8 @@ export const [
tokenNames.linkActiveFont,
(element: HTMLElement) => getColorForTheme(
element,
- DigitalGreenDark,
- PowerGreen,
+ DigitalGreenLight,
+ DigitalGreenLight,
hexToRgbaCssColor(White, 0.6)
),
(element: HTMLElement) => hexToRgbaCssColor(getDefaultFontColorForTheme(element), 0.3),
@@ -578,7 +564,7 @@ export const [
linkProminentFallbackFontFamily
] = createFontTokens(
tokenNames.linkProminentFont,
- (element: HTMLElement) => getColorForTheme(element, DigitalGreenDark, PowerGreen, PowerGreen),
+ (element: HTMLElement) => getColorForTheme(element, DigitalGreenDark105, PowerGreen, PowerGreen),
(element: HTMLElement) => hexToRgbaCssColor(getDefaultFontColorForTheme(element), 0.3),
LinkLightUiFamily,
LinkLightUiWeight,
@@ -598,7 +584,12 @@ export const [
linkActiveProminentFallbackFontFamily
] = createFontTokens(
tokenNames.linkActiveProminentFont,
- (element: HTMLElement) => getDefaultFontColorForTheme(element),
+ (element: HTMLElement) => getColorForTheme(
+ element,
+ DigitalGreenLight,
+ DigitalGreenLight,
+ PowerGreen
+ ),
(element: HTMLElement) => hexToRgbaCssColor(getDefaultFontColorForTheme(element), 0.3),
LinkLightUiFamily,
LinkLightUiWeight,
@@ -627,6 +618,26 @@ export const [
PlaceholderFallbackFontFamily
);
+export const [
+ bodyFont,
+ bodyFontColor,
+ bodyDisabledFontColor,
+ bodyFontFamily,
+ bodyFontWeight,
+ bodyFontSize,
+ bodyFontLineHeight,
+ bodyFallbackFontFamily
+] = createFontTokens(
+ tokenNames.bodyFont,
+ (element: HTMLElement) => getDefaultFontColorForTheme(element),
+ (element: HTMLElement) => hexToRgbaCssColor(getDefaultFontColorForTheme(element), 0.3),
+ BodyFamily,
+ BodyWeight,
+ BodySize,
+ BodyLineHeight,
+ BodyFallbackFontFamily
+);
+
export const [
bodyEmphasizedFont,
bodyEmphasizedFontColor,
@@ -648,43 +659,43 @@ export const [
);
export const [
- bodyEmphasizedPlus1Font,
- bodyEmphasizedPlus1FontColor,
- bodyEmphasizedPlus1DisabledFontColor,
- bodyEmphasizedPlus1FontFamily,
- bodyEmphasizedPlus1FontWeight,
- bodyEmphasizedPlus1FontSize,
- bodyEmphasizedPlus1FontLineHeight,
- bodyEmphasizedPlus1FallbackFontFamily
+ bodyPlus1Font,
+ bodyPlus1FontColor,
+ bodyPlus1DisabledFontColor,
+ bodyPlus1FontFamily,
+ bodyPlus1FontWeight,
+ bodyPlus1FontSize,
+ bodyPlus1FontLineHeight,
+ bodyPlus1FallbackFontFamily
] = createFontTokens(
- tokenNames.bodyEmphasizedPlus1Font,
+ tokenNames.bodyPlus1Font,
(element: HTMLElement) => getDefaultFontColorForTheme(element),
(element: HTMLElement) => hexToRgbaCssColor(getDefaultFontColorForTheme(element), 0.3),
- BodyEmphasized2Family,
- BodyEmphasized2Weight,
- BodyEmphasized2Size,
- BodyEmphasized2LineHeight,
- BodyEmphasized2FallbackFontFamily
+ Body2Family,
+ Body2Weight,
+ Body2Size,
+ Body2LineHeight,
+ Body2FallbackFontFamily
);
export const [
- bodyFont,
- bodyFontColor,
- bodyDisabledFontColor,
- bodyFontFamily,
- bodyFontWeight,
- bodyFontSize,
- bodyFontLineHeight,
- bodyFallbackFontFamily
+ bodyPlus1EmphasizedFont,
+ bodyPlus1EmphasizedFontColor,
+ bodyPlus1EmphasizedDisabledFontColor,
+ bodyPlus1EmphasizedFontFamily,
+ bodyPlus1EmphasizedFontWeight,
+ bodyPlus1EmphasizedFontSize,
+ bodyPlus1EmphasizedFontLineHeight,
+ bodyPlus1EmphasizedFallbackFontFamily
] = createFontTokens(
- tokenNames.bodyFont,
+ tokenNames.bodyPlus1EmphasizedFont,
(element: HTMLElement) => getDefaultFontColorForTheme(element),
(element: HTMLElement) => hexToRgbaCssColor(getDefaultFontColorForTheme(element), 0.3),
- BodyFamily,
- BodyWeight,
- BodySize,
- BodyLineHeight,
- BodyFallbackFontFamily
+ BodyEmphasized2Family,
+ BodyEmphasized2Weight,
+ BodyEmphasized2Size,
+ BodyEmphasized2LineHeight,
+ BodyEmphasized2FallbackFontFamily
);
export const [
diff --git a/packages/nimble-components/src/theme-provider/tests/tokens.stories.ts b/packages/nimble-components/src/theme-provider/tests/tokens.stories.ts
index 595f922d0f..5c84b9fb61 100644
--- a/packages/nimble-components/src/theme-provider/tests/tokens.stories.ts
+++ b/packages/nimble-components/src/theme-provider/tests/tokens.stories.ts
@@ -96,6 +96,7 @@ const tokenTemplates: {
} = {
Color: colorTemplate,
RgbPartialColor: rgbColorTemplate,
+ DisabledFontColor: colorTemplate,
FontColor: colorTemplate,
FontLineHeight: stringValueTemplate,
FontWeight: stringValueTemplate,
diff --git a/packages/nimble-components/src/toggle-button/index.ts b/packages/nimble-components/src/toggle-button/index.ts
index dc876ed997..d1ae498136 100644
--- a/packages/nimble-components/src/toggle-button/index.ts
+++ b/packages/nimble-components/src/toggle-button/index.ts
@@ -10,7 +10,7 @@ import {
import { styles } from './styles';
import { template } from './template';
import type { ButtonPattern } from '../patterns/button/types';
-import { ButtonAppearance } from './types';
+import { ButtonAppearance, ButtonAppearanceVariant } from './types';
declare global {
interface HTMLElementTagNameMap {
@@ -30,6 +30,14 @@ export class ToggleButton extends FoundationSwitch implements ButtonPattern {
@attr
public appearance: ButtonAppearance = ButtonAppearance.outline;
+ /**
+ * @public
+ * @remarks
+ * HTML Attribute: appearance-variant
+ */
+ @attr({ attribute: 'appearance-variant' })
+ public appearanceVariant: ButtonAppearanceVariant;
+
/**
* @public
* @remarks
diff --git a/packages/nimble-components/src/toggle-button/styles.ts b/packages/nimble-components/src/toggle-button/styles.ts
index ad1edbb3b8..090746dc19 100644
--- a/packages/nimble-components/src/toggle-button/styles.ts
+++ b/packages/nimble-components/src/toggle-button/styles.ts
@@ -1,70 +1,37 @@
import { css } from '@microsoft/fast-element';
-import { focusVisible } from '../utilities/style/focus';
import {
- borderHoverColor,
- borderWidth,
+ buttonAccentOutlineFontColor,
+ buttonLabelFontColor,
fillSelectedColor,
- fillSelectedRgbPartialColor
+ fillSelectedRgbPartialColor,
+ iconColor
} from '../theme-provider/design-tokens';
-import { styles as buttonStyles } from '../patterns/button/styles';
+import {
+ buttonAppearanceVariantStyles,
+ styles as buttonStyles
+} from '../patterns/button/styles';
+import { appearanceBehavior } from '../utilities/style/appearance';
+import { ButtonAppearance } from './types';
export const styles = css`
${buttonStyles}
+ ${buttonAppearanceVariantStyles}
- @layer base {
+ @layer checked {
.control[aria-pressed='true'] {
background-color: transparent;
+ color: ${buttonLabelFontColor};
background-image: linear-gradient(
${fillSelectedColor},
${fillSelectedColor}
);
border-color: rgba(${fillSelectedRgbPartialColor}, 0.3);
}
- }
-
- @layer hover {
- .control[aria-pressed='true']:hover {
- border-color: ${borderHoverColor};
- box-shadow: 0px 0px 0px ${borderWidth} ${borderHoverColor} inset;
- background-image: linear-gradient(
- ${fillSelectedColor},
- ${fillSelectedColor}
- );
- background-size: calc(100% - 4px) calc(100% - 4px);
- }
- }
-
- @layer focusVisible {
- .control[aria-pressed='true']${focusVisible} {
- border-color: ${borderHoverColor};
- box-shadow: 0px 0px 0px ${borderWidth} ${borderHoverColor} inset;
- background-image: linear-gradient(
- ${fillSelectedColor},
- ${fillSelectedColor}
- );
- background-size: calc(100% - 4px) calc(100% - 4px);
- }
-
- .control[aria-pressed='true']${focusVisible}::before {
- outline: ${borderWidth} solid ${borderHoverColor};
- outline-offset: -3px;
- color: transparent;
- }
- }
-
- @layer active {
- .control[aria-pressed='true']:active {
- box-shadow: none;
- background-image: linear-gradient(
- ${fillSelectedColor},
- ${fillSelectedColor}
- );
- background-size: calc(100% - 2px) calc(100% - 2px);
- }
- .control[aria-pressed='true']:active::before {
- outline: none;
+ .control[aria-pressed='true'] [part='start'],
+ .control[aria-pressed='true'] [part='end'] {
+ ${iconColor.cssCustomProperty}: ${buttonLabelFontColor};
}
}
@@ -76,15 +43,26 @@ export const styles = css`
);
border-color: rgba(${fillSelectedRgbPartialColor}, 0.3);
}
-
- :host([disabled]) .control[aria-pressed='true']:hover {
- background-image: linear-gradient(
- ${fillSelectedColor},
- ${fillSelectedColor}
- );
- background-size: 100% 100%;
- border-color: rgba(${fillSelectedRgbPartialColor}, 0.3);
- box-shadow: none;
- }
}
-`;
+`.withBehaviors(
+ appearanceBehavior(
+ ButtonAppearance.outline,
+ css`
+ @layer checked {
+ :host([appearance-variant='accent'])
+ .control[aria-pressed='true'] {
+ color: ${buttonAccentOutlineFontColor};
+ }
+
+ :host([appearance-variant='accent'])
+ .control[aria-pressed='true']
+ [part='start'],
+ :host([appearance-variant='accent'])
+ .control[aria-pressed='true']
+ [part='end'] {
+ ${iconColor.cssCustomProperty}: ${buttonAccentOutlineFontColor};
+ }
+ }
+ `
+ )
+);
diff --git a/packages/nimble-components/src/toggle-button/tests/toggle-button-matrix.stories.ts b/packages/nimble-components/src/toggle-button/tests/toggle-button-matrix.stories.ts
index a2772921b3..7e6fc7e002 100644
--- a/packages/nimble-components/src/toggle-button/tests/toggle-button-matrix.stories.ts
+++ b/packages/nimble-components/src/toggle-button/tests/toggle-button-matrix.stories.ts
@@ -1,7 +1,5 @@
import type { StoryFn, Meta } from '@storybook/html';
import { html, ViewTemplate, when } from '@microsoft/fast-element';
-import { pascalCase } from '@microsoft/fast-web-utilities';
-import { ButtonAppearance } from '../types';
import {
createMatrix,
sharedMatrixParameters,
@@ -14,6 +12,14 @@ import { textCustomizationWrapper } from '../../utilities/tests/text-customizati
import { toggleButtonTag } from '..';
import { iconArrowExpanderDownTag } from '../../icons/arrow-expander-down';
import { iconKeyTag } from '../../icons/key';
+import {
+ appearanceStates,
+ type AppearanceState,
+ type AppearanceVariantState,
+ type PartVisibilityState,
+ appearanceVariantStates,
+ partVisibilityStates
+} from '../../patterns/button/tests/states';
const metadata: Meta = {
title: 'Tests/Toggle Button',
@@ -24,21 +30,6 @@ const metadata: Meta = {
export default metadata;
-/* array of iconVisible, labelVisible, endIconVisible */
-const partVisibilityStates = [
- [true, true, false],
- [true, false, false],
- [false, true, false],
- [true, true, true],
- [false, true, true]
-] as const;
-type PartVisibilityState = (typeof partVisibilityStates)[number];
-
-const appearanceStates: [string, string | undefined][] = Object.entries(
- ButtonAppearance
-).map(([key, value]) => [pascalCase(key), value]);
-type AppearanceState = (typeof appearanceStates)[number];
-
const checkedStates = [
['Checked', true],
['Unchecked', false]
@@ -50,16 +41,18 @@ const component = (
[iconVisible, labelVisible, endIconVisible]: PartVisibilityState,
[checkedName, checked]: CheckedState,
[disabledName, disabled]: DisabledState,
- [appearanceName, appearance]: AppearanceState
+ [appearanceName, appearance]: AppearanceState,
+ [appearanceVariantName, appearanceVariant]: AppearanceVariantState
): ViewTemplate => html`
<${toggleButtonTag}
appearance="${() => appearance}"
+ appearance-variant="${() => appearanceVariant}"
?disabled=${() => disabled}
?content-hidden=${() => !labelVisible}
?checked=${() => checked}
style="margin-right: 8px; margin-bottom: 8px;">
${when(() => iconVisible, html`<${iconKeyTag} slot="start">${iconKeyTag}>`)}
- ${() => `${checkedName} ${appearanceName} Toggle Button ${disabledName}`}
+ ${() => `${checkedName} ${appearanceVariantName} ${appearanceName} Toggle Button ${disabledName}`}
${when(() => endIconVisible, html`<${iconArrowExpanderDownTag} slot="end">${iconArrowExpanderDownTag}>`)}
${toggleButtonTag}>
`;
@@ -69,7 +62,8 @@ export const toggleButtonThemeMatrix: StoryFn = createMatrixThemeStory(
partVisibilityStates,
checkedStates,
disabledStates,
- appearanceStates
+ appearanceStates,
+ appearanceVariantStates
])
);
diff --git a/packages/nimble-components/src/toggle-button/tests/toggle-button.mdx b/packages/nimble-components/src/toggle-button/tests/toggle-button.mdx
index cc2ba73349..1c505a644b 100644
--- a/packages/nimble-components/src/toggle-button/tests/toggle-button.mdx
+++ b/packages/nimble-components/src/toggle-button/tests/toggle-button.mdx
@@ -1,5 +1,7 @@
import { Controls, Canvas, Meta, Title } from '@storybook/blocks';
import ContentHiddenDocs from '../../patterns/button/tests/content-hidden-docs.mdx';
+import StylingDocs from '../../patterns/button/tests/styling-docs.mdx';
+import { toggleButtonTag } from '..';
import * as toggleButtonStories from './toggle-button.stories';
@@ -14,9 +16,7 @@ screen reader would say something like "Mute toggle button pressed".
-{/* ## Appearances */}
-
-{/* ## Appearance Variants */}
+
{/* ## Usage */}
diff --git a/packages/nimble-components/src/toggle-button/tests/toggle-button.stories.ts b/packages/nimble-components/src/toggle-button/tests/toggle-button.stories.ts
index 960d0cfad1..6e3be5500e 100644
--- a/packages/nimble-components/src/toggle-button/tests/toggle-button.stories.ts
+++ b/packages/nimble-components/src/toggle-button/tests/toggle-button.stories.ts
@@ -2,8 +2,10 @@ import { html, when } from '@microsoft/fast-element';
import { withActions } from '@storybook/addon-actions/decorator';
import type { HtmlRenderer, Meta, StoryObj } from '@storybook/html';
import { createUserSelectedThemeStory } from '../../utilities/tests/storybook';
-import { ButtonAppearance } from '../types';
+import { ButtonAppearance, ButtonAppearanceVariant } from '../types';
import {
+ appearanceDescription,
+ appearanceVariantDescription,
contentHiddenDescription,
endIconDescription,
iconDescription
@@ -14,7 +16,8 @@ import { iconKeyTag } from '../../icons/key';
interface ToggleButtonArgs {
label: string;
- appearance: string;
+ appearance: keyof typeof ButtonAppearance;
+ appearanceVariant: keyof typeof ButtonAppearanceVariant;
checked: boolean;
disabled: boolean;
icon: boolean;
@@ -33,7 +36,14 @@ const metadata: Meta = {
argTypes: {
appearance: {
options: Object.values(ButtonAppearance),
- control: { type: 'radio' }
+ control: { type: 'radio' },
+ description: appearanceDescription
+ },
+ appearanceVariant: {
+ name: 'appearance-variant',
+ options: Object.keys(ButtonAppearanceVariant),
+ control: { type: 'radio' },
+ description: appearanceVariantDescription
},
contentHidden: {
description: contentHiddenDescription
@@ -51,7 +61,8 @@ const metadata: Meta = {
?checked="${x => x.checked}"
?disabled="${x => x.disabled}"
?content-hidden="${x => x.contentHidden}"
- appearance="${x => x.appearance}"
+ appearance="${x => ButtonAppearance[x.appearance]}"
+ appearance-variant="${x => ButtonAppearanceVariant[x.appearanceVariant]}"
>
${when(x => x.icon, html`<${iconKeyTag} slot="start">${iconKeyTag}>`)}
${x => x.label}
@@ -59,8 +70,9 @@ const metadata: Meta = {
${toggleButtonTag}>
`),
args: {
- label: 'Ghost Toggle Button',
- appearance: 'ghost',
+ label: 'Toggle Button',
+ appearance: 'outline',
+ appearanceVariant: 'default',
checked: true,
icon: false,
endIcon: false,
diff --git a/packages/nimble-components/src/toggle-button/types.ts b/packages/nimble-components/src/toggle-button/types.ts
index eaf843c952..f38e4ea704 100644
--- a/packages/nimble-components/src/toggle-button/types.ts
+++ b/packages/nimble-components/src/toggle-button/types.ts
@@ -2,4 +2,7 @@
* Types of toggle button appearance.
* @public
*/
-export { ButtonAppearance } from '../patterns/button/types';
+export {
+ ButtonAppearance,
+ ButtonAppearanceVariant
+} from '../patterns/button/types';
diff --git a/packages/nimble-components/src/wafer-map/index.ts b/packages/nimble-components/src/wafer-map/index.ts
index a37ef562cd..4598885dbf 100644
--- a/packages/nimble-components/src/wafer-map/index.ts
+++ b/packages/nimble-components/src/wafer-map/index.ts
@@ -5,22 +5,29 @@ import {
} from '@microsoft/fast-element';
import { DesignSystem, FoundationElement } from '@microsoft/fast-foundation';
import { zoomIdentity, ZoomTransform } from 'd3-zoom';
+import type { Table } from 'apache-arrow';
import { template } from './template';
import { styles } from './styles';
import { DataManager } from './modules/data-manager';
+import { DataManager as ExperimentalDataManager } from './modules/experimental/data-manager';
import { RenderingModule } from './modules/rendering';
-import { EventCoordinator } from './modules/event-coordinator';
import {
+ HoverDie,
HoverDieOpacity,
WaferMapColorScale,
WaferMapColorScaleMode,
WaferMapDie,
WaferMapOrientation,
WaferMapOriginLocation,
- WaferMapValidity
+ WaferMapValidity,
+ type WaferRequiredFields
} from './types';
import { WaferMapUpdateTracker } from './modules/wafer-map-update-tracker';
import { WaferMapValidator } from './modules/wafer-map-validator';
+import { WorkerRenderer } from './modules/experimental/worker-renderer';
+import { HoverHandler } from './modules/hover-handler';
+import { HoverHandler as ExperimentalHoverHandler } from './modules/experimental/hover-handler';
+import { ZoomHandler } from './modules/zoom-handler';
declare global {
interface HTMLElementTagNameMap {
@@ -31,12 +38,14 @@ declare global {
/**
* A nimble-styled WaferMap
*/
-export class WaferMap extends FoundationElement {
+export class WaferMap<
+ T extends WaferRequiredFields = WaferRequiredFields
+> extends FoundationElement {
/**
* @internal
* needs to be initialized before the properties trigger changes
*/
- public readonly waferMapUpdateTracker = new WaferMapUpdateTracker(this);
+ public readonly waferMapUpdateTracker: WaferMapUpdateTracker = new WaferMapUpdateTracker(this.asRequiredFieldsWaferMap);
@attr({ attribute: 'origin-location' })
public originLocation: WaferMapOriginLocation = WaferMapOriginLocation.bottomLeft;
@@ -86,11 +95,33 @@ export class WaferMap extends FoundationElement {
/**
* @internal
*/
- public readonly dataManager = new DataManager(this);
+ public readonly stableDataManager: DataManager = new DataManager(
+ this.asRequiredFieldsWaferMap
+ );
+
+ /**
+ * @internal
+ */
+ public readonly experimentalDataManager: ExperimentalDataManager = new ExperimentalDataManager(this.asRequiredFieldsWaferMap);
+
+ public dataManager: DataManager | ExperimentalDataManager = this.stableDataManager;
+
/**
* @internal
*/
- public readonly renderer = new RenderingModule(this);
+ public readonly mainRenderer = new RenderingModule(
+ this.asRequiredFieldsWaferMap
+ );
+
+ /**
+ * @internal
+ */
+ public readonly workerRenderer = new WorkerRenderer(
+ this.asRequiredFieldsWaferMap
+ );
+
+ @observable
+ public renderer: RenderingModule | WorkerRenderer = this.mainRenderer;
/**
* @internal
@@ -135,18 +166,29 @@ export class WaferMap extends FoundationElement {
/**
* @internal
*/
- @observable public hoverDie: WaferMapDie | undefined;
+ @observable public hoverDie: WaferMapDie | HoverDie | undefined;
@observable public highlightedTags: string[] = [];
@observable public dies: WaferMapDie[] = [];
+ @observable public diesTable: Table | undefined;
+
@observable public colorScale: WaferMapColorScale = {
colors: [],
values: []
};
- private readonly eventCoordinator = new EventCoordinator(this);
+ private readonly hoverHandler: HoverHandler = new HoverHandler(
+ this.asRequiredFieldsWaferMap
+ );
+
+ private readonly experimentalHoverHandler: ExperimentalHoverHandler = new ExperimentalHoverHandler(this.asRequiredFieldsWaferMap);
+
+ private readonly zoomHandler: ZoomHandler = new ZoomHandler(
+ this.asRequiredFieldsWaferMap
+ );
+
private readonly resizeObserver = this.createResizeObserver();
- private readonly waferMapValidator = new WaferMapValidator(this);
+ private readonly waferMapValidator: WaferMapValidator = new WaferMapValidator(this.asRequiredFieldsWaferMap);
public get validity(): WaferMapValidity {
return this.waferMapValidator.getValidity();
@@ -157,12 +199,18 @@ export class WaferMap extends FoundationElement {
this.canvasContext = this.canvas.getContext('2d', {
willReadFrequently: true
})!;
+ this.hoverHandler.connect();
+ this.experimentalHoverHandler.connect();
+ this.zoomHandler.connect();
this.resizeObserver.observe(this);
this.waferMapUpdateTracker.trackAll();
}
public override disconnectedCallback(): void {
super.disconnectedCallback();
+ this.hoverHandler.disconnect();
+ this.experimentalHoverHandler.disconnect();
+ this.zoomHandler.disconnect();
this.resizeObserver.unobserve(this);
}
@@ -175,9 +223,19 @@ export class WaferMap extends FoundationElement {
* The hover does not require an event update, but it's also the last update in the sequence.
*/
public update(): void {
+ this.validate();
+ if (this.validity.invalidDiesTableSchema) {
+ return;
+ }
+ this.renderer = this.isExperimentalRenderer()
+ ? this.workerRenderer
+ : this.mainRenderer;
if (this.waferMapUpdateTracker.requiresEventsUpdate) {
- this.eventCoordinator.detachEvents();
- this.waferMapValidator.validateGridDimensions();
+ // zoom translateExtent needs to be recalculated when canvas size changes
+ this.zoomHandler.disconnect();
+ this.dataManager = this.isExperimentalRenderer()
+ ? this.experimentalDataManager
+ : this.stableDataManager;
if (this.waferMapUpdateTracker.requiresContainerDimensionsUpdate) {
this.dataManager.updateContainerDimensions();
this.renderer.updateSortedDiesAndDrawWafer();
@@ -197,12 +255,24 @@ export class WaferMap extends FoundationElement {
} else if (this.waferMapUpdateTracker.requiresDrawnWaferUpdate) {
this.renderer.drawWafer();
}
- this.eventCoordinator.attachEvents();
+ this.zoomHandler.connect();
} else if (this.waferMapUpdateTracker.requiresRenderHoverUpdate) {
this.renderer.renderHover();
}
}
+ /**
+ * @internal
+ */
+ public isExperimentalRenderer(): boolean {
+ return this.diesTable !== undefined;
+ }
+
+ private validate(): void {
+ this.waferMapValidator.validateGridDimensions();
+ this.waferMapValidator.validateDiesTableSchema();
+ }
+
private createResizeObserver(): ResizeObserver {
const resizeObserver = new ResizeObserver(entries => {
const entry = entries[0];
@@ -275,6 +345,11 @@ export class WaferMap extends FoundationElement {
this.waferMapUpdateTracker.queueUpdate();
}
+ private diesTableChanged(): void {
+ this.waferMapUpdateTracker.track('dies');
+ this.waferMapUpdateTracker.queueUpdate();
+ }
+
private colorScaleChanged(): void {
this.waferMapUpdateTracker.track('colorScale');
this.waferMapUpdateTracker.queueUpdate();
@@ -300,6 +375,10 @@ export class WaferMap extends FoundationElement {
this.waferMapUpdateTracker.track('hoverDie');
this.waferMapUpdateTracker.queueUpdate();
}
+
+ private get asRequiredFieldsWaferMap(): WaferMap {
+ return this as WaferMap;
+ }
}
const nimbleWaferMap = WaferMap.compose({
diff --git a/packages/nimble-components/src/wafer-map/modules/data-manager.ts b/packages/nimble-components/src/wafer-map/modules/data-manager.ts
index fb9fdbc3e9..b098683bfd 100644
--- a/packages/nimble-components/src/wafer-map/modules/data-manager.ts
+++ b/packages/nimble-components/src/wafer-map/modules/data-manager.ts
@@ -58,13 +58,13 @@ export class DataManager {
return this.dataMap;
}
- private readonly computations;
- private readonly prerendering;
+ private readonly computations: Computations;
+ private readonly prerendering: Prerendering;
private dataMap!: Map;
public constructor(private readonly wafermap: WaferMap) {
this.computations = new Computations(wafermap);
- this.prerendering = new Prerendering(wafermap, this);
+ this.prerendering = new Prerendering(wafermap);
}
public updateContainerDimensions(): void {
diff --git a/packages/nimble-components/src/wafer-map/modules/event-coordinator.ts b/packages/nimble-components/src/wafer-map/modules/event-coordinator.ts
deleted file mode 100644
index 420d401be5..0000000000
--- a/packages/nimble-components/src/wafer-map/modules/event-coordinator.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import type { WaferMapDie } from '../types';
-import { ZoomHandler } from './zoom-handler';
-import type { WaferMap } from '..';
-import { HoverHandler } from './hover-handler';
-
-export interface EventCoordinatorCallbacks {
- dieSelected: (die: WaferMapDie) => void;
-}
-
-/**
- * EventCoordinator deals with user interactions and events
- */
-export class EventCoordinator {
- private readonly zoomHandler;
- private readonly hoverHandler;
- public constructor(private readonly wafermap: WaferMap) {
- this.zoomHandler = new ZoomHandler(wafermap);
- this.hoverHandler = new HoverHandler(wafermap);
- }
-
- public attachEvents(): void {
- this.zoomHandler.createZoomBehavior();
- this.wafermap.addEventListener('mousemove', this.onMouseMove);
- this.wafermap.addEventListener('mouseout', this.onMouseOut);
- this.wafermap.canvas.addEventListener('wheel', this.onWheelMove, {
- passive: false
- });
- }
-
- public detachEvents(): void {
- this.wafermap.removeEventListener('mousemove', this.onMouseMove);
- this.wafermap.removeEventListener('mouseout', this.onMouseOut);
- this.wafermap.canvas.removeEventListener('wheel', this.onWheelMove);
- }
-
- private readonly onWheelMove = (event: Event): void => {
- event.preventDefault();
- };
-
- private readonly onMouseMove = (event: MouseEvent): void => {
- this.hoverHandler.mousemove(event);
- };
-
- private readonly onMouseOut = (): void => {
- this.hoverHandler.mouseout();
- };
-}
diff --git a/packages/nimble-components/src/wafer-map/modules/experimental/computations.ts b/packages/nimble-components/src/wafer-map/modules/experimental/computations.ts
new file mode 100644
index 0000000000..340c41da86
--- /dev/null
+++ b/packages/nimble-components/src/wafer-map/modules/experimental/computations.ts
@@ -0,0 +1,231 @@
+import { scaleLinear, ScaleLinear } from 'd3-scale';
+import type { WaferMap } from '../..';
+import { Dimensions, Margin, WaferMapOriginLocation } from '../../types';
+
+interface GridDimensions {
+ origin: {
+ x: number,
+ y: number
+ };
+ rows: number;
+ cols: number;
+}
+
+/**
+ * Computations calculates and stores different measures which are used in the Wafermap
+ */
+export class Computations {
+ public get containerDimensions(): Dimensions {
+ return this._containerDimensions;
+ }
+
+ public get dieDimensions(): Dimensions {
+ return this._dieDimensions;
+ }
+
+ public get margin(): Margin {
+ return this._margin;
+ }
+
+ public get horizontalScale(): ScaleLinear {
+ return this._horizontalScale;
+ }
+
+ public get verticalScale(): ScaleLinear {
+ return this._verticalScale;
+ }
+
+ private _containerDimensions!: Dimensions;
+ private _dieDimensions!: Dimensions;
+ private _margin!: Margin;
+ private _horizontalScale!: ScaleLinear;
+ private _verticalScale!: ScaleLinear;
+ private readonly defaultPadding = 0;
+ private readonly baseMarginPercentage = 0.04;
+
+ public constructor(private readonly wafermap: WaferMap) {}
+
+ public updateContainerDimensions(): void {
+ const canvasDimensions = {
+ width: this.wafermap.canvasWidth,
+ height: this.wafermap.canvasHeight
+ };
+ const canvasDiameter = Math.min(
+ canvasDimensions.width,
+ canvasDimensions.height
+ );
+ const canvasMargin = {
+ top: (canvasDimensions.height - canvasDiameter) / 2,
+ right: (canvasDimensions.width - canvasDiameter) / 2,
+ bottom: (canvasDimensions.height - canvasDiameter) / 2,
+ left: (canvasDimensions.width - canvasDiameter) / 2
+ };
+ const baseMargin = {
+ top: canvasDiameter * this.baseMarginPercentage,
+ right: canvasDiameter * this.baseMarginPercentage,
+ bottom: canvasDiameter * this.baseMarginPercentage,
+ left: canvasDiameter * this.baseMarginPercentage
+ };
+ this._margin = this.calculateMarginAddition(baseMargin, canvasMargin);
+ this._containerDimensions = this.calculateContainerDimensions(
+ canvasDimensions,
+ this._margin
+ );
+ this.updateScales();
+ }
+
+ public updateScales(): void {
+ const containerDiameter = Math.min(
+ this._containerDimensions.width,
+ this._containerDimensions.height
+ );
+ const gridDimensions = this.gridDimensionsValidAndDefined()
+ ? this.calculateGridDimensionsFromBoundingBox()
+ : this.calculateGridDimensionsFromDies();
+ // this scale is used for positioning the dies on the canvas
+ const originLocation = this.wafermap.originLocation;
+ this._horizontalScale = this.createHorizontalScale(
+ originLocation,
+ gridDimensions,
+ containerDiameter
+ );
+ // this scale is used for positioning the dies on the canvas
+ this._verticalScale = this.createVerticalScale(
+ originLocation,
+ gridDimensions,
+ containerDiameter
+ );
+ this._dieDimensions = {
+ width: Math.abs(
+ this._horizontalScale(0) - this._horizontalScale(1)
+ ),
+ height: Math.abs(this._verticalScale(0) - this._verticalScale(1))
+ };
+ }
+
+ private gridDimensionsValidAndDefined(): boolean {
+ return (
+ !this.wafermap.validity.invalidGridDimensions
+ && typeof this.wafermap.gridMinX === 'number'
+ && typeof this.wafermap.gridMinY === 'number'
+ && typeof this.wafermap.gridMaxX === 'number'
+ && typeof this.wafermap.gridMinX === 'number'
+ );
+ }
+
+ private calculateGridDimensionsFromBoundingBox(): GridDimensions {
+ const gridDimensions = { origin: { x: 0, y: 0 }, rows: 0, cols: 0 };
+ if (
+ typeof this.wafermap.gridMaxY === 'number'
+ && typeof this.wafermap.gridMinY === 'number'
+ && typeof this.wafermap.gridMaxX === 'number'
+ && typeof this.wafermap.gridMinX === 'number'
+ ) {
+ gridDimensions.origin.x = this.wafermap.gridMinX;
+ gridDimensions.origin.y = this.wafermap.gridMinY;
+ gridDimensions.rows = this.wafermap.gridMaxY - this.wafermap.gridMinY + 1;
+ gridDimensions.cols = this.wafermap.gridMaxX - this.wafermap.gridMinX + 1;
+ }
+ return gridDimensions;
+ }
+
+ private calculateGridDimensionsFromDies(): GridDimensions {
+ if (this.wafermap.diesTable === undefined) {
+ return { origin: { x: 0, y: 0 }, rows: 0, cols: 0 };
+ }
+
+ const colIndex = this.wafermap.diesTable
+ .getChild('colIndex')!
+ .toArray();
+ const rowIndex = this.wafermap.diesTable
+ .getChild('rowIndex')!
+ .toArray();
+
+ const minPoint = { x: colIndex[0]!, y: rowIndex[0]! };
+ const maxPoint = { x: colIndex[0]!, y: rowIndex[0]! };
+
+ // will replace iterating with arquero after fixing issues: https://github.com/uwdata/arquero/pull/346
+ for (let i = 0; i < colIndex.length; i++) {
+ if (colIndex[i]! < minPoint.x) {
+ minPoint.x = colIndex[i]!;
+ }
+ if (colIndex[i]! > maxPoint.x) {
+ maxPoint.x = colIndex[i]!;
+ }
+ if (rowIndex[i]! < minPoint.y) {
+ minPoint.y = rowIndex[i]!;
+ }
+ if (rowIndex[i]! > maxPoint.y) {
+ maxPoint.y = rowIndex[i]!;
+ }
+ }
+
+ return {
+ origin: minPoint,
+ rows: maxPoint.y - minPoint.y + 1,
+ cols: maxPoint.x - minPoint.x + 1
+ };
+ }
+
+ private calculateContainerDimensions(
+ canvasDimensions: Dimensions,
+ margin: Margin
+ ): Dimensions {
+ return {
+ width: canvasDimensions.width - margin.left - margin.right,
+ height: canvasDimensions.height - margin.top - margin.bottom
+ };
+ }
+
+ private createHorizontalScale(
+ originLocation: WaferMapOriginLocation,
+ grid: GridDimensions,
+ containerWidth: number
+ ): ScaleLinear {
+ const scale = scaleLinear();
+ if (
+ originLocation === WaferMapOriginLocation.bottomLeft
+ || originLocation === WaferMapOriginLocation.topLeft
+ ) {
+ return scale
+ .domain([grid.origin.x, grid.origin.x + grid.cols])
+ .range([0, containerWidth]);
+ }
+ return scale
+ .domain([grid.origin.x - 1, grid.origin.x + grid.cols - 1])
+ .range([containerWidth, 0]);
+ }
+
+ private createVerticalScale(
+ originLocation: WaferMapOriginLocation,
+ grid: GridDimensions,
+ containerHeight: number
+ ): ScaleLinear {
+ const scale = scaleLinear();
+ // html canvas has top-left origin https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Drawing_shapes#the_grid
+ // we need to flip the vertical scale
+ if (
+ originLocation === WaferMapOriginLocation.bottomLeft
+ || originLocation === WaferMapOriginLocation.bottomRight
+ ) {
+ return scale
+ .domain([grid.origin.y - 1, grid.origin.y + grid.rows - 1])
+ .range([containerHeight, 0]);
+ }
+ return scale
+ .domain([grid.origin.y, grid.origin.y + grid.rows])
+ .range([0, containerHeight]);
+ }
+
+ private calculateMarginAddition(
+ baseMargin: Margin,
+ addedMargin: Margin
+ ): Margin {
+ return {
+ top: baseMargin.top + addedMargin.top,
+ right: baseMargin.right + addedMargin.right,
+ bottom: baseMargin.bottom + addedMargin.bottom,
+ left: baseMargin.left + addedMargin.left
+ };
+ }
+}
diff --git a/packages/nimble-components/src/wafer-map/modules/experimental/data-manager.ts b/packages/nimble-components/src/wafer-map/modules/experimental/data-manager.ts
new file mode 100644
index 0000000000..b9ec12f93c
--- /dev/null
+++ b/packages/nimble-components/src/wafer-map/modules/experimental/data-manager.ts
@@ -0,0 +1,87 @@
+import type { ScaleLinear } from 'd3-scale';
+import { Computations } from './computations';
+import { Prerendering } from '../prerendering';
+import type { WaferMap } from '../..';
+import type {
+ Dimensions,
+ Margin,
+ DieRenderInfo,
+ WaferMapDie,
+ PointCoordinates
+} from '../../types';
+
+/**
+ * Data Manager uses Computations and Prerendering modules in order and exposes the results
+ */
+export class DataManager {
+ public get containerDimensions(): Dimensions {
+ return this.computations.containerDimensions;
+ }
+
+ public get dieDimensions(): Dimensions {
+ return this.computations.dieDimensions;
+ }
+
+ public get margin(): Margin {
+ return this.computations.margin;
+ }
+
+ public get horizontalScale(): ScaleLinear {
+ return this.computations.horizontalScale;
+ }
+
+ public get verticalScale(): ScaleLinear {
+ return this.computations.verticalScale;
+ }
+
+ public get labelsFontSize(): number {
+ return this.prerendering.labelsFontSize;
+ }
+
+ public get diesRenderInfo(): DieRenderInfo[] {
+ return this.prerendering.diesRenderInfo;
+ }
+
+ public get data(): Map {
+ return this.dataMap;
+ }
+
+ private readonly computations: Computations;
+ private readonly prerendering: Prerendering;
+ private dataMap!: Map;
+
+ public constructor(private readonly wafermap: WaferMap) {
+ this.computations = new Computations(wafermap);
+ this.prerendering = new Prerendering(wafermap);
+ }
+
+ public updateContainerDimensions(): void {
+ this.computations.updateContainerDimensions();
+ this.updateDataMap();
+ this.updateLabelsFontSize();
+ }
+
+ public updateScales(): void {
+ this.computations.updateScales();
+ this.updateDataMap();
+ this.updateLabelsFontSize();
+ }
+
+ public updateLabelsFontSize(): void {
+ this.prerendering.updateLabelsFontSize();
+ }
+
+ public updateDiesRenderInfo(): void {
+ this.prerendering.updateDiesRenderInfo();
+ }
+
+ public getWaferMapDie(point: PointCoordinates): WaferMapDie | undefined {
+ return this.dataMap.get(`${point.x}_${point.y}`);
+ }
+
+ private updateDataMap(): void {
+ this.dataMap = new Map(
+ this.wafermap.dies.map(die => [`${die.x}_${die.y}`, die])
+ );
+ }
+}
diff --git a/packages/nimble-components/src/wafer-map/modules/experimental/hover-handler.ts b/packages/nimble-components/src/wafer-map/modules/experimental/hover-handler.ts
new file mode 100644
index 0000000000..b3da77e01f
--- /dev/null
+++ b/packages/nimble-components/src/wafer-map/modules/experimental/hover-handler.ts
@@ -0,0 +1,107 @@
+import type { WaferMap } from '../..';
+import { PointCoordinates, WaferMapOriginLocation } from '../../types';
+import { DataManager } from './data-manager';
+
+/**
+ * HoverHandler deals with user interactions and events like hovering
+ */
+export class HoverHandler {
+ public constructor(private readonly wafermap: WaferMap) {}
+
+ /**
+ * @internal
+ */
+ public connect(): void {
+ this.wafermap.addEventListener('mousemove', this.onMouseMove);
+ this.wafermap.addEventListener('mouseout', this.onMouseOut);
+ }
+
+ /**
+ * @internal
+ */
+ public disconnect(): void {
+ this.wafermap.removeEventListener('mousemove', this.onMouseMove);
+ this.wafermap.removeEventListener('mouseout', this.onMouseOut);
+ }
+
+ /**
+ * @internal
+ * keep public for testing until data manager refactor
+ */
+ public readonly onMouseMove = (event: MouseEvent): void => {
+ if (!this.wafermap.isExperimentalRenderer()) {
+ return;
+ }
+ // get original mouse position in case we are in zoom.
+ const invertedPoint = this.wafermap.transform.invert([
+ event.offsetX,
+ event.offsetY
+ ]);
+ const dieCoordinates = this.calculateDieCoordinates({
+ x: invertedPoint[0],
+ y: invertedPoint[1]
+ });
+ if (dieCoordinates === undefined) {
+ this.wafermap.hoverDie = undefined;
+ return;
+ }
+ const colIndex = this.wafermap
+ .diesTable!.getChild('colIndex')!
+ .toArray();
+ const rowIndex = this.wafermap
+ .diesTable!.getChild('rowIndex')!
+ .toArray();
+
+ // will replace iterating with arquero filtering after fixing errors
+ for (let i = 0; i < colIndex.length; i++) {
+ if (
+ colIndex[i] === dieCoordinates.x
+ && rowIndex[i] === dieCoordinates.y
+ ) {
+ this.wafermap.hoverDie = {
+ index: i,
+ x: dieCoordinates.x,
+ y: dieCoordinates.y
+ };
+ return;
+ }
+ }
+ this.wafermap.hoverDie = undefined;
+ };
+
+ private readonly onMouseOut = (_event: MouseEvent): void => {
+ this.wafermap.hoverDie = undefined;
+ };
+
+ private calculateDieCoordinates(
+ mousePosition: PointCoordinates
+ ): PointCoordinates | undefined {
+ if (
+ this.wafermap.isExperimentalRenderer()
+ && this.wafermap.dataManager instanceof DataManager
+ ) {
+ const originLocation = this.wafermap.originLocation;
+ const xRoundFunction = originLocation === WaferMapOriginLocation.bottomLeft
+ || originLocation === WaferMapOriginLocation.topLeft
+ ? Math.floor
+ : Math.ceil;
+ const yRoundFunction = originLocation === WaferMapOriginLocation.bottomLeft
+ || originLocation === WaferMapOriginLocation.bottomRight
+ ? Math.ceil
+ : Math.floor;
+ // go to x and y scale to get the x,y values of the die.
+ const x = xRoundFunction(
+ this.wafermap.dataManager.horizontalScale.invert(
+ mousePosition.x - this.wafermap.dataManager.margin.left
+ )
+ );
+ const y = yRoundFunction(
+ this.wafermap.dataManager.verticalScale.invert(
+ mousePosition.y - this.wafermap.dataManager.margin.top
+ )
+ );
+ return { x, y };
+ }
+ return undefined;
+ }
+}
diff --git a/packages/nimble-components/src/wafer-map/modules/experimental/worker-renderer.ts b/packages/nimble-components/src/wafer-map/modules/experimental/worker-renderer.ts
new file mode 100644
index 0000000000..5556c7bfe2
--- /dev/null
+++ b/packages/nimble-components/src/wafer-map/modules/experimental/worker-renderer.ts
@@ -0,0 +1,53 @@
+import type { WaferMap } from '../..';
+import { HoverDieOpacity } from '../../types';
+
+/**
+ * Responsible for drawing the dies inside the wafer map, adding dieText and scaling the canvas
+ */
+export class WorkerRenderer {
+ public constructor(private readonly wafermap: WaferMap) {}
+
+ public updateSortedDiesAndDrawWafer(): void {
+ // redundant function for backwards compatibility
+ this.drawWafer();
+ }
+
+ public drawWafer(): void {
+ // rendering will be implemented in a future PR
+ this.renderHover();
+ }
+
+ public renderHover(): void {
+ this.wafermap.hoverWidth = this.wafermap.dataManager.dieDimensions.width
+ * this.wafermap.transform.k;
+ this.wafermap.hoverHeight = this.wafermap.dataManager.dieDimensions.height
+ * this.wafermap.transform.k;
+ this.wafermap.hoverOpacity = this.wafermap.hoverDie === undefined
+ ? HoverDieOpacity.hide
+ : HoverDieOpacity.show;
+ this.wafermap.hoverTransform = this.calculateHoverTransform();
+ }
+
+ private calculateHoverTransform(): string {
+ if (this.wafermap.hoverDie !== undefined) {
+ const scaledX = this.wafermap.dataManager.horizontalScale(
+ this.wafermap.hoverDie.x
+ );
+ if (scaledX === undefined) {
+ return '';
+ }
+ const scaledY = this.wafermap.dataManager.verticalScale(
+ this.wafermap.hoverDie.y
+ );
+ if (scaledY === undefined) {
+ return '';
+ }
+ const transformedPoint = this.wafermap.transform.apply([
+ scaledX + this.wafermap.dataManager.margin.left,
+ scaledY + this.wafermap.dataManager.margin.top
+ ]);
+ return `translate(${transformedPoint[0]}, ${transformedPoint[1]})`;
+ }
+ return '';
+ }
+}
diff --git a/packages/nimble-components/src/wafer-map/modules/hover-handler.ts b/packages/nimble-components/src/wafer-map/modules/hover-handler.ts
index cab13191d6..da3b5dc336 100644
--- a/packages/nimble-components/src/wafer-map/modules/hover-handler.ts
+++ b/packages/nimble-components/src/wafer-map/modules/hover-handler.ts
@@ -1,18 +1,39 @@
import type { WaferMap } from '..';
import { PointCoordinates, WaferMapOriginLocation } from '../types';
+import { DataManager } from './data-manager';
/**
* HoverHandler deals with user interactions and events like hovering
*/
export class HoverHandler {
public constructor(private readonly wafermap: WaferMap) {}
- public mousemove(event: MouseEvent): void {
+
+ /**
+ * @internal
+ */
+ public connect(): void {
+ this.wafermap.addEventListener('mousemove', this.onMouseMove);
+ this.wafermap.addEventListener('mouseout', this.onMouseOut);
+ }
+
+ /**
+ * @internal
+ */
+ public disconnect(): void {
+ this.wafermap.removeEventListener('mousemove', this.onMouseMove);
+ this.wafermap.removeEventListener('mouseout', this.onMouseOut);
+ }
+
+ private readonly onMouseMove = (event: MouseEvent): void => {
+ if (this.wafermap.isExperimentalRenderer()) {
+ return;
+ }
const mousePosition: PointCoordinates = {
x: event.offsetX,
y: event.offsetY
};
- if (!this.hoversOverDie(this.wafermap, mousePosition)) {
+ if (!this.hoversOverDie(mousePosition)) {
this.wafermap.hoverDie = undefined;
return;
}
@@ -22,50 +43,56 @@ export class HoverHandler {
mousePosition.y
]);
- const dieCoordinates = this.calculateDieCoordinates(this.wafermap, {
+ const dieCoordinates = this.calculateDieCoordinates({
x: invertedPoint[0],
y: invertedPoint[1]
});
+ if (dieCoordinates === undefined) {
+ this.wafermap.hoverDie = undefined;
+ return;
+ }
this.wafermap.hoverDie = this.wafermap.dataManager.getWaferMapDie(dieCoordinates);
- }
+ };
- public mouseout(): void {
+ private readonly onMouseOut = (_event: MouseEvent): void => {
this.wafermap.hoverDie = undefined;
- }
+ };
private calculateDieCoordinates(
- wafermap: WaferMap,
mousePosition: PointCoordinates
- ): PointCoordinates {
- const originLocation = wafermap.originLocation;
- const xRoundFunction = originLocation === WaferMapOriginLocation.bottomLeft
- || originLocation === WaferMapOriginLocation.topLeft
- ? Math.floor
- : Math.ceil;
- const yRoundFunction = originLocation === WaferMapOriginLocation.bottomLeft
- || originLocation === WaferMapOriginLocation.bottomRight
- ? Math.floor
- : Math.ceil;
- // go to x and y scale to get the x,y values of the die.
- const x = xRoundFunction(
- wafermap.dataManager.invertedHorizontalScale(
- mousePosition.x - wafermap.dataManager.margin.left
- )
- );
- const y = yRoundFunction(
- wafermap.dataManager.invertedVerticalScale(
- mousePosition.y - wafermap.dataManager.margin.top
- )
- );
- return { x, y };
+ ): PointCoordinates | undefined {
+ if (
+ !this.wafermap.isExperimentalRenderer()
+ && this.wafermap.dataManager instanceof DataManager
+ ) {
+ const originLocation = this.wafermap.originLocation;
+ const xRoundFunction = originLocation === WaferMapOriginLocation.bottomLeft
+ || originLocation === WaferMapOriginLocation.topLeft
+ ? Math.floor
+ : Math.ceil;
+ const yRoundFunction = originLocation === WaferMapOriginLocation.bottomLeft
+ || originLocation === WaferMapOriginLocation.bottomRight
+ ? Math.floor
+ : Math.ceil;
+ // go to x and y scale to get the x,y values of the die.
+ const x = xRoundFunction(
+ this.wafermap.dataManager.invertedHorizontalScale(
+ mousePosition.x - this.wafermap.dataManager.margin.left
+ )
+ );
+ const y = yRoundFunction(
+ this.wafermap.dataManager.invertedVerticalScale(
+ mousePosition.y - this.wafermap.dataManager.margin.top
+ )
+ );
+ return { x, y };
+ }
+ return undefined;
}
- private hoversOverDie(
- wafermap: WaferMap,
- mousePosition: PointCoordinates
- ): boolean {
- const rgba = wafermap.canvasContext.getImageData(
+ private hoversOverDie(mousePosition: PointCoordinates): boolean {
+ const rgba = this.wafermap.canvasContext.getImageData(
mousePosition.x,
mousePosition.y,
1,
diff --git a/packages/nimble-components/src/wafer-map/modules/prerendering.ts b/packages/nimble-components/src/wafer-map/modules/prerendering.ts
index eb3bdab107..e1d34cbae4 100644
--- a/packages/nimble-components/src/wafer-map/modules/prerendering.ts
+++ b/packages/nimble-components/src/wafer-map/modules/prerendering.ts
@@ -8,7 +8,6 @@ import type {
WaferMapDie
} from '../types';
import type { WaferMap } from '..';
-import type { DataManager } from './data-manager';
/**
* Prerendering prepares render-ready dies data to be used by the rendering module
@@ -34,14 +33,11 @@ export class Prerendering {
private readonly emptyDieColor = 'rgba(218,223,236,1)';
private readonly nanDieColor = 'rgba(122,122,122,1)';
- public constructor(
- private readonly wafermap: WaferMap,
- private readonly dataManager: Readonly
- ) {}
+ public constructor(private readonly wafermap: WaferMap) {}
public updateLabelsFontSize(): void {
this._labelsFontSize = this.calculateLabelsFontSize(
- this.dataManager.dieDimensions,
+ this.wafermap.dataManager.dieDimensions,
this.wafermap.maxCharacters
);
this.updateDiesRenderInfo();
@@ -52,16 +48,19 @@ export class Prerendering {
this.wafermap.colorScale,
this.wafermap.colorScaleMode
);
+ const isDieRenderInfo = (
+ info: DieRenderInfo | null
+ ): info is DieRenderInfo => info !== null;
this._diesRenderInfo = this.wafermap.dies
.map(die => this.computeDieRenderInfo(die))
- .filter(info => info !== null) as DieRenderInfo[];
+ .filter(isDieRenderInfo);
}
private computeDieRenderInfo(die: WaferMapDie): DieRenderInfo | null {
- const margin = this.dataManager.margin;
+ const margin = this.wafermap.dataManager.margin;
- const scaledX = this.dataManager.horizontalScale(die.x);
- const scaledY = this.dataManager.verticalScale(die.y);
+ const scaledX = this.wafermap.dataManager.horizontalScale(die.x);
+ const scaledY = this.wafermap.dataManager.verticalScale(die.y);
if (scaledX === undefined || scaledY === undefined) {
return null;
@@ -124,7 +123,7 @@ export class Prerendering {
return '';
}
const label = `${value}${dieLabelsSuffix}`;
- if (label.length > maxCharacters) {
+ if (label.length >= maxCharacters) {
return `${label.substring(0, maxCharacters)}…`;
}
return label;
diff --git a/packages/nimble-components/src/wafer-map/modules/wafer-map-validator.ts b/packages/nimble-components/src/wafer-map/modules/wafer-map-validator.ts
index 06aa8fb2ad..6788f50149 100644
--- a/packages/nimble-components/src/wafer-map/modules/wafer-map-validator.ts
+++ b/packages/nimble-components/src/wafer-map/modules/wafer-map-validator.ts
@@ -1,3 +1,4 @@
+import { DataType, Precision } from 'apache-arrow';
import type { WaferMap } from '..';
import type { WaferMapValidity } from '../types';
@@ -7,11 +8,13 @@ import type { WaferMapValidity } from '../types';
*/
export class WaferMapValidator {
private invalidGridDimensions = false;
+ private invalidDiesTableSchema = false;
public constructor(private readonly wafermap: WaferMap) {}
public getValidity(): WaferMapValidity {
return {
- invalidGridDimensions: this.invalidGridDimensions
+ invalidGridDimensions: this.invalidGridDimensions,
+ invalidDiesTableSchema: this.invalidDiesTableSchema
};
}
@@ -22,10 +25,10 @@ export class WaferMapValidator {
public validateGridDimensions(): boolean {
this.invalidGridDimensions = false;
if (
- typeof this.wafermap.gridMinX === 'undefined'
- && typeof this.wafermap.gridMaxX === 'undefined'
- && typeof this.wafermap.gridMinY === 'undefined'
- && typeof this.wafermap.gridMaxY === 'undefined'
+ this.wafermap.gridMinX === undefined
+ && this.wafermap.gridMaxX === undefined
+ && this.wafermap.gridMinY === undefined
+ && this.wafermap.gridMaxY === undefined
) {
this.invalidGridDimensions = false;
} else if (
@@ -40,4 +43,30 @@ export class WaferMapValidator {
}
return !this.invalidGridDimensions;
}
+
+ public validateDiesTableSchema(): boolean {
+ this.invalidDiesTableSchema = false;
+ if (this.wafermap.diesTable === undefined) {
+ this.invalidDiesTableSchema = false;
+ } else {
+ const fields = this.wafermap.diesTable.schema.fields;
+ const colField = fields.find(field => field.name === 'colIndex');
+ const rowField = fields.find(field => field.name === 'rowIndex');
+ const valueField = fields.find(field => field.name === 'value');
+ if (
+ !colField
+ || !rowField
+ || !valueField
+ || !DataType.isInt(colField.type)
+ || colField.type.bitWidth !== 32
+ || !DataType.isInt(rowField.type)
+ || rowField.type.bitWidth !== 32
+ || !DataType.isFloat(valueField.type)
+ || valueField.type.precision !== Precision.DOUBLE
+ ) {
+ this.invalidDiesTableSchema = true;
+ }
+ }
+ return !this.invalidDiesTableSchema;
+ }
}
diff --git a/packages/nimble-components/src/wafer-map/modules/zoom-handler.ts b/packages/nimble-components/src/wafer-map/modules/zoom-handler.ts
index 38a82d89b1..79635b403c 100644
--- a/packages/nimble-components/src/wafer-map/modules/zoom-handler.ts
+++ b/packages/nimble-components/src/wafer-map/modules/zoom-handler.ts
@@ -1,11 +1,5 @@
import { select } from 'd3-selection';
-import {
- zoom,
- ZoomBehavior,
- zoomIdentity,
- ZoomTransform,
- zoomTransform
-} from 'd3-zoom';
+import { zoom, ZoomTransform } from 'd3-zoom';
import type { WaferMap } from '..';
interface ZoomEvent {
@@ -16,61 +10,44 @@ interface ZoomEvent {
* ZoomHandler deals with user interactions and events like zooming
*/
export class ZoomHandler {
- private zoomTransform: ZoomTransform = zoomIdentity;
- private readonly minScale = 1.1;
- private readonly minExtentPoint: [number, number] = [-100, -100];
- private readonly extentPadding = 100;
- private zoomBehavior!: ZoomBehavior;
+ private readonly scaleExtent: [number, number] = [1, 100];
+ private readonly minExtentPoint: [number, number] = [0, 0];
public constructor(private readonly wafermap: WaferMap) {}
- public createZoomBehavior(): void {
- this.zoomBehavior = zoom()
- .scaleExtent([
- 1.1,
- this.getZoomMax(
- this.wafermap.canvasWidth * this.wafermap.canvasHeight,
- this.wafermap.dataManager.containerDimensions.width
- * this.wafermap.dataManager.containerDimensions.height
- )
- ])
+ /**
+ * @internal
+ */
+ public connect(): void {
+ this.createZoomBehavior();
+ this.wafermap.addEventListener('wheel', this.onWheelMove, {
+ passive: false
+ });
+ }
+
+ /**
+ * @internal
+ */
+ public disconnect(): void {
+ zoom().on('zoom', null)(select(this.wafermap as Element));
+ this.wafermap.removeEventListener('wheel', this.onWheelMove);
+ }
+
+ private createZoomBehavior(): void {
+ zoom()
+ .scaleExtent(this.scaleExtent)
.translateExtent([
this.minExtentPoint,
- [
- this.wafermap.canvasWidth + this.extentPadding,
- this.wafermap.canvasHeight + this.extentPadding
- ]
+ [this.wafermap.canvasWidth, this.wafermap.canvasHeight]
])
- .filter((event: Event) => {
- const transform = zoomTransform(this.wafermap.canvas);
- const filterEval = transform.k >= this.minScale || event.type === 'wheel';
- return filterEval;
- })
.on('zoom', (event: ZoomEvent) => {
// D3 will automatically remove existing handlers when adding new ones
// See: https://github.com/d3/d3-zoom/blob/v3.0.0/README.md#zoom_on
- this.rescale(event);
- });
-
- this.zoomBehavior(select(this.wafermap.canvas as Element));
+ this.wafermap.transform = event.transform;
+ })(select(this.wafermap as Element));
}
- private rescale(event: ZoomEvent): void {
- const transform = event.transform;
- if (transform.k === this.minScale) {
- this.zoomTransform = zoomIdentity;
- this.zoomBehavior.transform(
- select(this.wafermap.canvas as Element),
- zoomIdentity
- );
- } else {
- this.zoomTransform = transform;
- }
-
- this.wafermap.transform = this.zoomTransform;
- }
-
- private getZoomMax(canvasArea: number, dataArea: number): number {
- return Math.ceil((dataArea / canvasArea) * 100);
- }
+ private readonly onWheelMove = (event: Event): void => {
+ event.preventDefault();
+ };
}
diff --git a/packages/nimble-components/src/wafer-map/tests/computations.spec.ts b/packages/nimble-components/src/wafer-map/tests/computations.spec.ts
index deb673b81c..0f0ead89ef 100644
--- a/packages/nimble-components/src/wafer-map/tests/computations.spec.ts
+++ b/packages/nimble-components/src/wafer-map/tests/computations.spec.ts
@@ -1,4 +1,3 @@
-import type { WaferMap } from '..';
import { Computations } from '../modules/computations';
import { Margin, WaferMapOriginLocation } from '../types';
import { getWaferMapMockComputations, getWaferMapDies } from './utilities';
@@ -20,7 +19,7 @@ describe('Wafermap Computations module', () => {
100,
100
);
- computationsModule = new Computations(waferMock as WaferMap);
+ computationsModule = new Computations(waferMock);
computationsModule.updateContainerDimensions();
});
@@ -70,7 +69,7 @@ describe('Wafermap Computations module', () => {
200,
100
);
- computationsModule = new Computations(waferMock as WaferMap);
+ computationsModule = new Computations(waferMock);
computationsModule.updateContainerDimensions();
});
@@ -114,7 +113,7 @@ describe('Wafermap Computations module', () => {
100,
100
);
- computationsModule = new Computations(waferMock as WaferMap);
+ computationsModule = new Computations(waferMock);
computationsModule.updateContainerDimensions();
});
@@ -136,7 +135,7 @@ describe('Wafermap Computations module', () => {
100,
100
);
- computationsModule = new Computations(waferMock as WaferMap);
+ computationsModule = new Computations(waferMock);
computationsModule.updateContainerDimensions();
});
@@ -158,7 +157,7 @@ describe('Wafermap Computations module', () => {
100,
100
);
- computationsModule = new Computations(waferMock as WaferMap);
+ computationsModule = new Computations(waferMock);
computationsModule.updateContainerDimensions();
});
@@ -180,7 +179,7 @@ describe('Wafermap Computations module', () => {
100,
100
);
- computationsModule = new Computations(waferMock as WaferMap);
+ computationsModule = new Computations(waferMock);
computationsModule.updateContainerDimensions();
});
diff --git a/packages/nimble-components/src/wafer-map/tests/data-generator.ts b/packages/nimble-components/src/wafer-map/tests/data-generator.ts
index e89fc9cbc8..77cc8efa71 100644
--- a/packages/nimble-components/src/wafer-map/tests/data-generator.ts
+++ b/packages/nimble-components/src/wafer-map/tests/data-generator.ts
@@ -1,3 +1,4 @@
+import { Table, tableFromArrays } from 'apache-arrow';
import type { WaferMapDie } from '../types';
import type { IValueGenerator } from './value-generator';
@@ -19,6 +20,20 @@ const generateStringValue = (
return valueToString(value);
};
+const generateFloatValue = (
+ x: number,
+ y: number,
+ valueGenerator: IValueGenerator
+): number => {
+ let value: number;
+ if (valueGenerator !== undefined) {
+ value = valueGenerator(x, y);
+ } else {
+ value = Math.random() * 100;
+ }
+ return value;
+};
+
const generateTagValue = (valueGenerator: IValueGenerator): string => {
let value: string;
if (valueGenerator !== undefined) {
@@ -91,3 +106,54 @@ export const generateWaferData = (
}
return diesSet;
};
+
+export const generateWaferTableData = (
+ numDies: number,
+ valueGenerator: IValueGenerator
+): Table => {
+ const colIndex = [];
+ const rowIndex = [];
+ const value = [];
+
+ if (numDies > 0) {
+ // calculate the equivalent radius of a circle that would contain the <<<>>> number of dies
+ const radius = Math.ceil(Math.sqrt(numDies / Math.PI));
+ const centerX = radius;
+ const centerY = radius;
+
+ // Generate dies values - start from the bottom and go up
+ for (let i = centerY - radius; i <= centerY + radius; i++) {
+ let stringValue: number;
+
+ // generate points left of centerX
+ for (
+ let j = centerX;
+ (j - centerX) * (j - centerX) + (i - centerY) * (i - centerY)
+ <= radius * radius;
+ j--
+ ) {
+ stringValue = generateFloatValue(i, j, valueGenerator);
+ colIndex.push(j);
+ rowIndex.push(i);
+ value.push(stringValue);
+ }
+ // generate points right of centerX
+ for (
+ let j = centerX + 1;
+ (j - centerX) * (j - centerX) + (i - centerY) * (i - centerY)
+ <= radius * radius;
+ j++
+ ) {
+ stringValue = generateFloatValue(i, j, valueGenerator);
+ colIndex.push(j);
+ rowIndex.push(i);
+ value.push(stringValue);
+ }
+ }
+ }
+ return tableFromArrays({
+ colIndex: Int32Array.from(colIndex),
+ rowIndex: Int32Array.from(rowIndex),
+ value: Float64Array.from(value)
+ });
+};
diff --git a/packages/nimble-components/src/wafer-map/tests/data-manager.spec.ts b/packages/nimble-components/src/wafer-map/tests/data-manager.spec.ts
index 454c0f4a79..d28a4a951e 100644
--- a/packages/nimble-components/src/wafer-map/tests/data-manager.spec.ts
+++ b/packages/nimble-components/src/wafer-map/tests/data-manager.spec.ts
@@ -56,7 +56,7 @@ describe('Wafermap Data Manager', () => {
processUpdates();
- dataManagerModule = element.dataManager;
+ dataManagerModule = element.stableDataManager;
});
afterEach(async () => {
diff --git a/packages/nimble-components/src/wafer-map/tests/experimental-computations.spec.ts b/packages/nimble-components/src/wafer-map/tests/experimental-computations.spec.ts
new file mode 100644
index 0000000000..c4e1aa1556
--- /dev/null
+++ b/packages/nimble-components/src/wafer-map/tests/experimental-computations.spec.ts
@@ -0,0 +1,144 @@
+import { parameterizeSpec } from '@ni/jasmine-parameterized';
+import { Computations } from '../modules/experimental/computations';
+import { Margin, WaferMapOriginLocation } from '../types';
+import {
+ getWaferMapMockComputationsExperimental,
+ getWaferMapDiesTable
+} from './utilities';
+
+describe('Wafermap Experimental Computations module', () => {
+ let computationsModule: Computations;
+
+ describe('with 100 square canvas', () => {
+ const expectedMargin: Margin = {
+ top: 4,
+ right: 4,
+ bottom: 4,
+ left: 4
+ };
+ beforeEach(() => {
+ const waferMock = getWaferMapMockComputationsExperimental(
+ getWaferMapDiesTable(),
+ WaferMapOriginLocation.topLeft,
+ 100,
+ 100
+ );
+ computationsModule = new Computations(waferMock);
+ computationsModule.updateContainerDimensions();
+ });
+
+ it('should have expected square container', () => {
+ expect(computationsModule.containerDimensions).toEqual({
+ width: 92,
+ height: 92
+ });
+ });
+
+ it('should have expected die size', () => {
+ const computedDimensions = {
+ width: Math.ceil(computationsModule.dieDimensions.width),
+ height: Math.ceil(computationsModule.dieDimensions.height)
+ };
+ expect(computedDimensions).toEqual({
+ width: 19,
+ height: 16
+ });
+ });
+
+ it('should have expected margin', () => {
+ expect(computationsModule.margin).toEqual(expectedMargin);
+ });
+
+ it('should have horizontal domain containing min and max column indexes', () => {
+ expect(computationsModule.horizontalScale.domain()).toEqual([2, 7]);
+ });
+ it('should have vertical domain containing min and max row indexes, ', () => {
+ expect(computationsModule.verticalScale.domain()).toEqual([1, 7]);
+ });
+ });
+
+ describe('with rectangular canvas', () => {
+ beforeEach(() => {
+ const waferMock = getWaferMapMockComputationsExperimental(
+ getWaferMapDiesTable(),
+ WaferMapOriginLocation.topLeft,
+ 200,
+ 100
+ );
+ computationsModule = new Computations(waferMock);
+ computationsModule.updateContainerDimensions();
+ });
+
+ it('should have adjusted square container', () => {
+ expect(computationsModule.containerDimensions).toEqual({
+ width: 92,
+ height: 92
+ });
+ });
+
+ it('should have adjusted die size', () => {
+ const computedDimensions = {
+ width: Math.ceil(computationsModule.dieDimensions.width),
+ height: Math.ceil(computationsModule.dieDimensions.height)
+ };
+ expect(computedDimensions).toEqual({
+ width: 19,
+ height: 16
+ });
+ });
+
+ it('should have adjusted margin', () => {
+ expect(computationsModule.margin).toEqual({
+ top: 4,
+ right: 54,
+ bottom: 4,
+ left: 54
+ });
+ });
+ });
+
+ const testCases = [
+ {
+ name: WaferMapOriginLocation.topLeft,
+ horizontalRange: [0, 92],
+ verticalRange: [0, 92]
+ },
+ {
+ name: WaferMapOriginLocation.topRight,
+ horizontalRange: [92, 0],
+ verticalRange: [0, 92]
+ },
+ {
+ name: WaferMapOriginLocation.bottomLeft,
+ horizontalRange: [0, 92],
+ verticalRange: [92, 0]
+ },
+ {
+ name: WaferMapOriginLocation.bottomRight,
+ horizontalRange: [92, 0],
+ verticalRange: [92, 0]
+ }
+ ] as const;
+
+ parameterizeSpec(testCases, (spec, name, value) => {
+ spec(
+ `with ${name} originLocation should have expected horizontal range and vertical range`,
+ () => {
+ const waferMock = getWaferMapMockComputationsExperimental(
+ getWaferMapDiesTable(),
+ value.name,
+ 100,
+ 100
+ );
+ computationsModule = new Computations(waferMock);
+ computationsModule.updateContainerDimensions();
+ expect(computationsModule.horizontalScale.range()).toEqual(
+ value.horizontalRange
+ );
+ expect(computationsModule.verticalScale.range()).toEqual(
+ value.verticalRange
+ );
+ }
+ );
+ });
+});
diff --git a/packages/nimble-components/src/wafer-map/tests/experimental-data-manager.spec.ts b/packages/nimble-components/src/wafer-map/tests/experimental-data-manager.spec.ts
new file mode 100644
index 0000000000..d50c4d4ce8
--- /dev/null
+++ b/packages/nimble-components/src/wafer-map/tests/experimental-data-manager.spec.ts
@@ -0,0 +1,177 @@
+import { html } from '@microsoft/fast-element';
+
+import { fixture, Fixture } from '../../utilities/tests/fixture';
+import { processUpdates } from '../../testing/async-helpers';
+import type { DataManager } from '../modules/experimental/data-manager';
+import type { WaferMap } from '..';
+import {
+ Dimensions,
+ Margin,
+ WaferMapColorScaleMode,
+ WaferMapOriginLocation
+} from '../types';
+import {
+ getColorScale,
+ getHighlightedTags,
+ getWaferMapDies,
+ getWaferMapDiesTable
+} from './utilities';
+
+async function setup(): Promise> {
+ return fixture(html` `);
+}
+
+describe('Wafermap Experimental Data Manager', () => {
+ let dataManagerModule: DataManager;
+ const dieLabelsSuffix = '%';
+ const canvasWidth = 200;
+ const canvasHeight = 100;
+ const canvasDimensions: Dimensions = {
+ width: canvasWidth,
+ height: canvasHeight
+ };
+ const expectedMargin: Margin = {
+ top: 4,
+ right: 54,
+ bottom: 4,
+ left: 54
+ };
+
+ let element: WaferMap;
+ let connect: () => Promise;
+ let disconnect: () => Promise;
+
+ beforeEach(async () => {
+ ({ element, connect, disconnect } = await setup());
+ await connect();
+ element.diesTable = getWaferMapDiesTable();
+ element.colorScale = getColorScale();
+ element.originLocation = WaferMapOriginLocation.bottomLeft;
+ element.dieLabelsSuffix = dieLabelsSuffix;
+ element.dieLabelsHidden = false;
+ element.maxCharacters = 3;
+ element.colorScaleMode = WaferMapColorScaleMode.ordinal;
+ element.canvasWidth = canvasWidth;
+ element.canvasHeight = canvasHeight;
+
+ processUpdates();
+
+ dataManagerModule = element.experimentalDataManager;
+ });
+
+ afterEach(async () => {
+ await disconnect();
+ });
+
+ it('computes the correct containerDimensions', () => {
+ expect(dataManagerModule.containerDimensions).toEqual({
+ width: 92,
+ height: 92
+ });
+ });
+
+ it('computes the correct dieDimensions', () => {
+ const computedDimensions = {
+ width: Math.ceil(dataManagerModule.dieDimensions.width),
+ height: Math.ceil(dataManagerModule.dieDimensions.height)
+ };
+ expect(computedDimensions).toEqual({
+ width: 19,
+ height: 16
+ });
+ });
+
+ it('should have expected margin', () => {
+ expect(dataManagerModule.margin).toEqual(expectedMargin);
+ });
+
+ it('should have increasing horizontal range', () => {
+ expect(dataManagerModule.horizontalScale.range()).toEqual([0, 92]);
+ });
+
+ it('should have decreasing vertical range', () => {
+ // because the canvas has top-left origin location we need to flip the vertical scale
+ expect(dataManagerModule.verticalScale.range()).toEqual([92, 0]);
+ });
+
+ it('should not have labelsFontSize larger than the die height', () => {
+ expect(dataManagerModule.labelsFontSize).toBeLessThanOrEqual(
+ dataManagerModule.dieDimensions.height
+ );
+ });
+
+ it('should not have labelsFontSize larger than the die width', () => {
+ expect(dataManagerModule.labelsFontSize).toBeLessThanOrEqual(
+ dataManagerModule.dieDimensions.width
+ );
+ });
+
+ // skipped until prerendering is refactored
+ xit('should have as many dies as provided', () => {
+ expect(dataManagerModule.diesRenderInfo.length).toEqual(
+ getWaferMapDies().length
+ );
+ });
+
+ // skipped until prerendering is refactored
+ xit('should have label with suffix for each die', () => {
+ const actualValues = dataManagerModule.diesRenderInfo.map(
+ dieRenderInfo => dieRenderInfo.text
+ );
+ expect(actualValues).not.toHaveSize(0);
+ for (const value of actualValues) {
+ expect(value).toContain(dieLabelsSuffix);
+ }
+ });
+
+ // skipped until prerendering is refactored
+ xit('should have all dies with full opacity from the highlighted list', () => {
+ const highlightedTags = getHighlightedTags();
+ const dies = getWaferMapDies().filter(die => die.tags?.some(dieTag => highlightedTags.some(
+ highlightedTag => dieTag === highlightedTag
+ )));
+ const diesWithFullOpacity = dataManagerModule.diesRenderInfo.filter(x => x.fillStyle.endsWith(',1)'));
+ expect(dies.length).toEqual(diesWithFullOpacity.length);
+ });
+
+ // skipped until prerendering is refactored
+ xit('should not have any dies with partial opacity from the highlighted list', () => {
+ const highlightedTags = getHighlightedTags();
+ const dies = getWaferMapDies().filter(
+ die => !die.tags?.some(dieTag => highlightedTags.some(
+ highlightedTag => dieTag === highlightedTag
+ ))
+ );
+ const diesWithPartialOpacity = dataManagerModule.diesRenderInfo.filter(
+ x => !x.fillStyle.endsWith(',1)')
+ );
+ expect(dies.length).toEqual(diesWithPartialOpacity.length);
+ });
+
+ // skipped until prerendering is refactored
+ xit('should have all dies inside the canvas with margins', () => {
+ const actualValues = dataManagerModule.diesRenderInfo.map(
+ dieRenderInfo => {
+ return {
+ x: dieRenderInfo.x,
+ y: dieRenderInfo.y
+ };
+ }
+ );
+ expect(actualValues).not.toHaveSize(0);
+ for (const value of actualValues) {
+ expect(value.x).toBeGreaterThanOrEqual(0);
+ expect(value.y).toBeGreaterThanOrEqual(0);
+ expect(value.x).toBeLessThanOrEqual(
+ canvasDimensions.width
+ - dataManagerModule.dieDimensions.width
+ - expectedMargin.left
+ );
+ expect(value.y).toBeLessThanOrEqual(
+ canvasDimensions.height
+ - dataManagerModule.dieDimensions.height
+ - expectedMargin.bottom
+ );
+ }
+ });
+});
diff --git a/packages/nimble-components/src/wafer-map/tests/hover-handler.spec.ts b/packages/nimble-components/src/wafer-map/tests/hover-handler.spec.ts
new file mode 100644
index 0000000000..978d4fd705
--- /dev/null
+++ b/packages/nimble-components/src/wafer-map/tests/hover-handler.spec.ts
@@ -0,0 +1,93 @@
+import { html } from '@microsoft/fast-element';
+import { parameterizeSpec } from '@ni/jasmine-parameterized';
+import { WaferMapOriginLocation } from '../types';
+import { getWaferMapDiesTable } from './utilities';
+import type { WaferMap } from '..';
+import { processUpdates } from '../../testing/async-helpers';
+import { Fixture, fixture } from '../../utilities/tests/fixture';
+
+async function setup(): Promise> {
+ return fixture(html` `);
+}
+
+describe('HoverHandler', () => {
+ let element: WaferMap;
+ let connect: () => Promise;
+ const canvasWidth = 100;
+ const canvasHeight = 100;
+ let disconnect: () => Promise;
+
+ beforeEach(async () => {
+ ({ element, connect, disconnect } = await setup());
+ await connect();
+ element.diesTable = getWaferMapDiesTable();
+ element.originLocation = WaferMapOriginLocation.bottomLeft;
+ element.canvasWidth = canvasWidth;
+ element.canvasHeight = canvasHeight;
+
+ processUpdates();
+ });
+
+ afterEach(async () => {
+ await disconnect();
+ });
+
+ const testCases = [
+ {
+ name: WaferMapOriginLocation.bottomLeft,
+ expectedDie: { index: 5, x: 3, y: 5 }
+ },
+ {
+ name: WaferMapOriginLocation.topLeft,
+ expectedDie: { index: 4, x: 3, y: 4 }
+ },
+ {
+ name: WaferMapOriginLocation.bottomRight,
+ expectedDie: { index: 10, x: 4, y: 5 }
+ },
+ {
+ name: WaferMapOriginLocation.topRight,
+ expectedDie: { index: 9, x: 4, y: 4 }
+ }
+ ] as const;
+
+ parameterizeSpec(testCases, (spec, name, value) => {
+ spec(
+ `will return the expected index when mouse moved in range from ${name}`,
+ () => {
+ element.originLocation = value.name;
+ element.dispatchEvent(
+ new MouseEvent('mousemove', {
+ clientX: 30,
+ clientY: 30
+ })
+ );
+ processUpdates();
+ expect(element.hoverDie).toEqual(value.expectedDie);
+ }
+ );
+ });
+
+ const undefinedTestCases = [
+ { name: WaferMapOriginLocation.bottomLeft, expectedDie: undefined },
+ { name: WaferMapOriginLocation.topLeft, expectedDie: undefined },
+ { name: WaferMapOriginLocation.bottomRight, expectedDie: undefined },
+ { name: WaferMapOriginLocation.topRight, expectedDie: undefined }
+ ] as const;
+ parameterizeSpec(undefinedTestCases, (spec, name, value) => {
+ spec(
+ `will return undefined when mouse moved out of range from ${name}`,
+ () => {
+ element.originLocation = value.name;
+ element.dispatchEvent(
+ new MouseEvent('mousemove', {
+ clientX: 101,
+ clientY: 101
+ })
+ );
+ processUpdates();
+ expect(element.hoverDie).toEqual(value.expectedDie);
+ }
+ );
+ });
+});
diff --git a/packages/nimble-components/src/wafer-map/tests/prerendering.coloring.spec.ts b/packages/nimble-components/src/wafer-map/tests/prerendering.coloring.spec.ts
index 5cfb169a65..2b93f87e82 100644
--- a/packages/nimble-components/src/wafer-map/tests/prerendering.coloring.spec.ts
+++ b/packages/nimble-components/src/wafer-map/tests/prerendering.coloring.spec.ts
@@ -1,5 +1,3 @@
-import type { WaferMap } from '..';
-import type { DataManager } from '../modules/data-manager';
import { Prerendering } from '../modules/prerendering';
import { WaferMapColorScaleMode } from '../types';
import {
@@ -28,6 +26,12 @@ describe('Wafermap Prerendering module', () => {
const margin = { top: 0, right: 0, bottom: 0, left: 0 };
beforeEach(() => {
+ const dataManagerMock = getDataManagerMock(
+ dieDimensions,
+ margin,
+ defaultHorizontalScale,
+ defaultVerticalScale
+ );
const waferMock = getWaferMapMockPrerendering(
getWaferMapDies(),
{ colors: ['red'], values: ['1'] },
@@ -35,18 +39,10 @@ describe('Wafermap Prerendering module', () => {
colorScaleMode,
dieLabelsHidden,
dieLabelsSuffix,
- maxCharacters
- );
- const dataManagerMock = getDataManagerMock(
- dieDimensions,
- margin,
- defaultHorizontalScale,
- defaultVerticalScale
- );
- prerenderingModule = new Prerendering(
- waferMock as WaferMap,
- dataManagerMock as DataManager
+ maxCharacters,
+ dataManagerMock
);
+ prerenderingModule = new Prerendering(waferMock);
prerenderingModule.updateLabelsFontSize();
});
@@ -70,6 +66,12 @@ describe('Wafermap Prerendering module', () => {
const margin = { top: 0, right: 0, bottom: 0, left: 0 };
beforeEach(() => {
+ const dataManagerMock = getDataManagerMock(
+ dieDimensions,
+ margin,
+ defaultHorizontalScale,
+ defaultVerticalScale
+ );
const waferMock = getWaferMapMockPrerendering(
getWaferMapDies(),
{
@@ -80,18 +82,10 @@ describe('Wafermap Prerendering module', () => {
colorScaleMode,
dieLabelsHidden,
dieLabelsSuffix,
- maxCharacters
- );
- const dataManagerMock = getDataManagerMock(
- dieDimensions,
- margin,
- defaultHorizontalScale,
- defaultVerticalScale
- );
- prerenderingModule = new Prerendering(
- waferMock as WaferMap,
- dataManagerMock as DataManager
+ maxCharacters,
+ dataManagerMock
);
+ prerenderingModule = new Prerendering(waferMock);
prerenderingModule.updateLabelsFontSize();
});
@@ -115,6 +109,12 @@ describe('Wafermap Prerendering module', () => {
const margin = { top: 0, right: 0, bottom: 0, left: 0 };
beforeEach(() => {
+ const dataManagerMock = getDataManagerMock(
+ dieDimensions,
+ margin,
+ defaultHorizontalScale,
+ defaultVerticalScale
+ );
const waferMock = getWaferMapMockPrerendering(
getWaferMapDies(),
{
@@ -125,18 +125,10 @@ describe('Wafermap Prerendering module', () => {
colorScaleMode,
dieLabelsHidden,
dieLabelsSuffix,
- maxCharacters
- );
- const dataManagerMock = getDataManagerMock(
- dieDimensions,
- margin,
- defaultHorizontalScale,
- defaultVerticalScale
- );
- prerenderingModule = new Prerendering(
- waferMock as WaferMap,
- dataManagerMock as DataManager
+ maxCharacters,
+ dataManagerMock
);
+ prerenderingModule = new Prerendering(waferMock);
prerenderingModule.updateLabelsFontSize();
});
@@ -167,6 +159,12 @@ describe('Wafermap Prerendering module', () => {
const margin = { top: 0, right: 0, bottom: 0, left: 0 };
beforeEach(() => {
+ const dataManagerMock = getDataManagerMock(
+ dieDimensions,
+ margin,
+ defaultHorizontalScale,
+ defaultVerticalScale
+ );
const waferMock = getWaferMapMockPrerendering(
getWaferMapDies(),
{ colors: ['red'], values: ['1'] },
@@ -174,18 +172,10 @@ describe('Wafermap Prerendering module', () => {
colorScaleMode,
dieLabelsHidden,
dieLabelsSuffix,
- maxCharacters
- );
- const dataManagerMock = getDataManagerMock(
- dieDimensions,
- margin,
- defaultHorizontalScale,
- defaultVerticalScale
- );
- prerenderingModule = new Prerendering(
- waferMock as WaferMap,
- dataManagerMock as DataManager
+ maxCharacters,
+ dataManagerMock
);
+ prerenderingModule = new Prerendering(waferMock);
prerenderingModule.updateLabelsFontSize();
});
@@ -209,6 +199,12 @@ describe('Wafermap Prerendering module', () => {
const margin = { top: 0, right: 0, bottom: 0, left: 0 };
beforeEach(() => {
+ const dataManagerMock = getDataManagerMock(
+ dieDimensions,
+ margin,
+ defaultHorizontalScale,
+ defaultVerticalScale
+ );
const waferMock = getWaferMapMockPrerendering(
getWaferMapDies(),
{
@@ -219,18 +215,10 @@ describe('Wafermap Prerendering module', () => {
colorScaleMode,
dieLabelsHidden,
dieLabelsSuffix,
- maxCharacters
- );
- const dataManagerMock = getDataManagerMock(
- dieDimensions,
- margin,
- defaultHorizontalScale,
- defaultVerticalScale
- );
- prerenderingModule = new Prerendering(
- waferMock as WaferMap,
- dataManagerMock as DataManager
+ maxCharacters,
+ dataManagerMock
);
+ prerenderingModule = new Prerendering(waferMock);
prerenderingModule.updateLabelsFontSize();
});
@@ -260,6 +248,12 @@ describe('Wafermap Prerendering module', () => {
const margin = { top: 0, right: 0, bottom: 0, left: 0 };
beforeEach(() => {
+ const dataManagerMock = getDataManagerMock(
+ dieDimensions,
+ margin,
+ getScaleBand([0, 1], [0, 100]),
+ getScaleBand([0, 1], [0, 100])
+ );
const waferMock = getWaferMapMockPrerendering(
[
{
@@ -273,18 +267,10 @@ describe('Wafermap Prerendering module', () => {
WaferMapColorScaleMode.linear,
dieLabelsHidden,
dieLabelsSuffix,
- maxCharacters
- );
- const dataManagerMock = getDataManagerMock(
- dieDimensions,
- margin,
- getScaleBand([0, 1], [0, 100]),
- getScaleBand([0, 1], [0, 100])
- );
- prerenderingModule = new Prerendering(
- waferMock as WaferMap,
- dataManagerMock as DataManager
+ maxCharacters,
+ dataManagerMock
);
+ prerenderingModule = new Prerendering(waferMock);
prerenderingModule.updateLabelsFontSize();
});
@@ -308,6 +294,12 @@ describe('Wafermap Prerendering module', () => {
const margin = { top: 0, right: 0, bottom: 0, left: 0 };
beforeEach(() => {
+ const dataManagerMock = getDataManagerMock(
+ dieDimensions,
+ margin,
+ getScaleBand([0, 1], [0, 100]),
+ getScaleBand([0, 1], [0, 100])
+ );
const waferMock = getWaferMapMockPrerendering(
[
{
@@ -321,18 +313,10 @@ describe('Wafermap Prerendering module', () => {
WaferMapColorScaleMode.linear,
dieLabelsHidden,
dieLabelsSuffix,
- maxCharacters
- );
- const dataManagerMock = getDataManagerMock(
- dieDimensions,
- margin,
- getScaleBand([0, 1], [0, 100]),
- getScaleBand([0, 1], [0, 100])
- );
- prerenderingModule = new Prerendering(
- waferMock as WaferMap,
- dataManagerMock as DataManager
+ maxCharacters,
+ dataManagerMock
);
+ prerenderingModule = new Prerendering(waferMock);
prerenderingModule.updateLabelsFontSize();
});
@@ -356,6 +340,12 @@ describe('Wafermap Prerendering module', () => {
const margin = { top: 0, right: 0, bottom: 0, left: 0 };
beforeEach(() => {
+ const dataManagerMock = getDataManagerMock(
+ dieDimensions,
+ margin,
+ defaultHorizontalScale,
+ defaultVerticalScale
+ );
const waferMock = getWaferMapMockPrerendering(
getWaferMapDies(),
{ colors: ['red'], values: [] },
@@ -363,18 +353,10 @@ describe('Wafermap Prerendering module', () => {
WaferMapColorScaleMode.ordinal,
dieLabelsHidden,
dieLabelsSuffix,
- maxCharacters
- );
- const dataManagerMock = getDataManagerMock(
- dieDimensions,
- margin,
- defaultHorizontalScale,
- defaultVerticalScale
- );
- prerenderingModule = new Prerendering(
- waferMock as WaferMap,
- dataManagerMock as DataManager
+ maxCharacters,
+ dataManagerMock
);
+ prerenderingModule = new Prerendering(waferMock);
prerenderingModule.updateLabelsFontSize();
});
@@ -405,6 +387,12 @@ describe('Wafermap Prerendering module', () => {
const margin = { top: 0, right: 0, bottom: 0, left: 0 };
beforeEach(() => {
+ const dataManagerMock = getDataManagerMock(
+ dieDimensions,
+ margin,
+ defaultHorizontalScale,
+ defaultVerticalScale
+ );
const waferMock = getWaferMapMockPrerendering(
getWaferMapDies(),
{ colors: ['red'], values: [] },
@@ -412,18 +400,10 @@ describe('Wafermap Prerendering module', () => {
WaferMapColorScaleMode.ordinal,
dieLabelsHidden,
dieLabelsSuffix,
- maxCharacters
- );
- const dataManagerMock = getDataManagerMock(
- dieDimensions,
- margin,
- defaultHorizontalScale,
- defaultVerticalScale
- );
- prerenderingModule = new Prerendering(
- waferMock as WaferMap,
- dataManagerMock as DataManager
+ maxCharacters,
+ dataManagerMock
);
+ prerenderingModule = new Prerendering(waferMock);
prerenderingModule.updateLabelsFontSize();
});
diff --git a/packages/nimble-components/src/wafer-map/tests/prerendering.labeling.spec.ts b/packages/nimble-components/src/wafer-map/tests/prerendering.labeling.spec.ts
index 60c98db5b8..dead70f68d 100644
--- a/packages/nimble-components/src/wafer-map/tests/prerendering.labeling.spec.ts
+++ b/packages/nimble-components/src/wafer-map/tests/prerendering.labeling.spec.ts
@@ -1,5 +1,3 @@
-import type { WaferMap } from '..';
-import type { DataManager } from '../modules/data-manager';
import { Prerendering } from '../modules/prerendering';
import { WaferMapColorScaleMode } from '../types';
import {
@@ -24,6 +22,12 @@ describe('Wafermap Prerendering module', () => {
const margin = { top: 0, right: 0, bottom: 0, left: 0 };
beforeEach(() => {
+ const dataManagerMock = getDataManagerMock(
+ dieDimensions,
+ margin,
+ defaultHorizontalScale,
+ defaultVerticalScale
+ );
const waferMock = getWaferMapMockPrerendering(
getWaferMapDies(),
{ colors: [], values: [] },
@@ -31,18 +35,10 @@ describe('Wafermap Prerendering module', () => {
WaferMapColorScaleMode.linear,
dieLabelsHidden,
dieLabelsSuffix,
- maxCharacters
- );
- const dataManagerMock = getDataManagerMock(
- dieDimensions,
- margin,
- defaultHorizontalScale,
- defaultVerticalScale
- );
- prerenderingModule = new Prerendering(
- waferMock as WaferMap,
- dataManagerMock as DataManager
+ maxCharacters,
+ dataManagerMock
);
+ prerenderingModule = new Prerendering(waferMock);
prerenderingModule.updateLabelsFontSize();
});
@@ -68,6 +64,12 @@ describe('Wafermap Prerendering module', () => {
const margin = { top: 0, right: 0, bottom: 0, left: 0 };
beforeEach(() => {
+ const dataManagerMock = getDataManagerMock(
+ dieDimensions,
+ margin,
+ defaultHorizontalScale,
+ defaultVerticalScale
+ );
const waferMock = getWaferMapMockPrerendering(
getWaferMapDies(),
{ colors: [], values: [] },
@@ -75,18 +77,10 @@ describe('Wafermap Prerendering module', () => {
WaferMapColorScaleMode.linear,
dieLabelsHidden,
dieLabelsSuffix,
- maxCharacters
- );
- const dataManagerMock = getDataManagerMock(
- dieDimensions,
- margin,
- defaultHorizontalScale,
- defaultVerticalScale
- );
- prerenderingModule = new Prerendering(
- waferMock as WaferMap,
- dataManagerMock as DataManager
+ maxCharacters,
+ dataManagerMock
);
+ prerenderingModule = new Prerendering(waferMock);
prerenderingModule.updateLabelsFontSize();
});
@@ -106,6 +100,12 @@ describe('Wafermap Prerendering module', () => {
const margin = { top: 0, right: 0, bottom: 0, left: 0 };
beforeEach(() => {
+ const dataManagerMock = getDataManagerMock(
+ dieDimensions,
+ margin,
+ defaultHorizontalScale,
+ defaultVerticalScale
+ );
const waferMock = getWaferMapMockPrerendering(
getWaferMapDies(),
{ colors: [], values: [] },
@@ -113,18 +113,10 @@ describe('Wafermap Prerendering module', () => {
WaferMapColorScaleMode.linear,
dieLabelsHidden,
dieLabelsSuffix,
- maxCharacters
- );
- const dataManagerMock = getDataManagerMock(
- dieDimensions,
- margin,
- defaultHorizontalScale,
- defaultVerticalScale
- );
- prerenderingModule = new Prerendering(
- waferMock as WaferMap,
- dataManagerMock as DataManager
+ maxCharacters,
+ dataManagerMock
);
+ prerenderingModule = new Prerendering(waferMock);
prerenderingModule.updateLabelsFontSize();
});
@@ -148,6 +140,12 @@ describe('Wafermap Prerendering module', () => {
const margin = { top: 0, right: 0, bottom: 0, left: 0 };
beforeEach(() => {
+ const dataManagerMock = getDataManagerMock(
+ dieDimensions,
+ margin,
+ defaultHorizontalScale,
+ defaultVerticalScale
+ );
const waferMock = getWaferMapMockPrerendering(
getWaferMapDies(),
{ colors: [], values: [] },
@@ -155,18 +153,10 @@ describe('Wafermap Prerendering module', () => {
WaferMapColorScaleMode.linear,
dieLabelsHidden,
dieLabelsSuffix,
- maxCharacters
- );
- const dataManagerMock = getDataManagerMock(
- dieDimensions,
- margin,
- defaultHorizontalScale,
- defaultVerticalScale
- );
- prerenderingModule = new Prerendering(
- waferMock as WaferMap,
- dataManagerMock as DataManager
+ maxCharacters,
+ dataManagerMock
);
+ prerenderingModule = new Prerendering(waferMock);
prerenderingModule.updateLabelsFontSize();
});
@@ -186,11 +176,17 @@ describe('Wafermap Prerendering module', () => {
const dieDimensions = { width: 10, height: 10 };
const dieLabelsSuffix = '';
const dieLabelsHidden = false;
- const maxCharacters = 2;
+ const maxCharacters = 3;
const highlightedTags: string[] = [];
const margin = { top: 0, right: 0, bottom: 0, left: 0 };
beforeEach(() => {
+ const dataManagerMock = getDataManagerMock(
+ dieDimensions,
+ margin,
+ defaultHorizontalScale,
+ defaultVerticalScale
+ );
const waferMock = getWaferMapMockPrerendering(
getWaferMapDies(),
{ colors: [], values: [] },
@@ -198,18 +194,10 @@ describe('Wafermap Prerendering module', () => {
WaferMapColorScaleMode.linear,
dieLabelsHidden,
dieLabelsSuffix,
- maxCharacters
- );
- const dataManagerMock = getDataManagerMock(
- dieDimensions,
- margin,
- defaultHorizontalScale,
- defaultVerticalScale
- );
- prerenderingModule = new Prerendering(
- waferMock as WaferMap,
- dataManagerMock as DataManager
+ maxCharacters,
+ dataManagerMock
);
+ prerenderingModule = new Prerendering(waferMock);
prerenderingModule.updateLabelsFontSize();
});
@@ -231,11 +219,17 @@ describe('Wafermap Prerendering module', () => {
const dieDimensions = { width: 10, height: 10 };
const dieLabelsSuffix = '';
const dieLabelsHidden = false;
- const maxCharacters = 3;
+ const maxCharacters = 4;
const highlightedTags: string[] = [];
const margin = { top: 0, right: 0, bottom: 0, left: 0 };
beforeEach(() => {
+ const dataManagerMock = getDataManagerMock(
+ dieDimensions,
+ margin,
+ defaultHorizontalScale,
+ defaultVerticalScale
+ );
const waferMock = getWaferMapMockPrerendering(
getWaferMapDiesAsNaN(),
{ colors: [], values: [] },
@@ -243,18 +237,10 @@ describe('Wafermap Prerendering module', () => {
WaferMapColorScaleMode.linear,
dieLabelsHidden,
dieLabelsSuffix,
- maxCharacters
- );
- const dataManagerMock = getDataManagerMock(
- dieDimensions,
- margin,
- defaultHorizontalScale,
- defaultVerticalScale
- );
- prerenderingModule = new Prerendering(
- waferMock as WaferMap,
- dataManagerMock as DataManager
+ maxCharacters,
+ dataManagerMock
);
+ prerenderingModule = new Prerendering(waferMock);
prerenderingModule.updateLabelsFontSize();
});
@@ -281,6 +267,12 @@ describe('Wafermap Prerendering module', () => {
const margin = { top: 0, right: 0, bottom: 0, left: 0 };
beforeEach(() => {
+ const dataManagerMock = getDataManagerMock(
+ dieDimensions,
+ margin,
+ defaultHorizontalScale,
+ defaultVerticalScale
+ );
const waferMock = getWaferMapMockPrerendering(
getWaferMapDiesAsFloats(),
{ colors: [], values: [] },
@@ -288,18 +280,10 @@ describe('Wafermap Prerendering module', () => {
WaferMapColorScaleMode.linear,
dieLabelsHidden,
dieLabelsSuffix,
- maxCharacters
- );
- const dataManagerMock = getDataManagerMock(
- dieDimensions,
- margin,
- defaultHorizontalScale,
- defaultVerticalScale
- );
- prerenderingModule = new Prerendering(
- waferMock as WaferMap,
- dataManagerMock as DataManager
+ maxCharacters,
+ dataManagerMock
);
+ prerenderingModule = new Prerendering(waferMock);
prerenderingModule.updateLabelsFontSize();
});
@@ -330,6 +314,12 @@ describe('Wafermap Prerendering module', () => {
const margin = { top: 0, right: 0, bottom: 0, left: 0 };
beforeEach(() => {
+ const dataManagerMock = getDataManagerMock(
+ dieDimensions,
+ margin,
+ defaultHorizontalScale,
+ defaultVerticalScale
+ );
const waferMock = getWaferMapMockPrerendering(
getWaferMapDies(),
{ colors: [], values: [] },
@@ -337,18 +327,10 @@ describe('Wafermap Prerendering module', () => {
WaferMapColorScaleMode.linear,
dieLabelsHidden,
dieLabelsSuffix,
- maxCharacters
- );
- const dataManagerMock = getDataManagerMock(
- dieDimensions,
- margin,
- defaultHorizontalScale,
- defaultVerticalScale
- );
- prerenderingModule = new Prerendering(
- waferMock as WaferMap,
- dataManagerMock as DataManager
+ maxCharacters,
+ dataManagerMock
);
+ prerenderingModule = new Prerendering(waferMock);
prerenderingModule.updateLabelsFontSize();
});
diff --git a/packages/nimble-components/src/wafer-map/tests/prerendering.positioning.spec.ts b/packages/nimble-components/src/wafer-map/tests/prerendering.positioning.spec.ts
index a54606c526..8b65558c50 100644
--- a/packages/nimble-components/src/wafer-map/tests/prerendering.positioning.spec.ts
+++ b/packages/nimble-components/src/wafer-map/tests/prerendering.positioning.spec.ts
@@ -1,5 +1,3 @@
-import type { WaferMap } from '..';
-import type { DataManager } from '../modules/data-manager';
import { Prerendering } from '../modules/prerendering';
import { WaferMapColorScaleMode } from '../types';
import {
@@ -23,6 +21,12 @@ describe('Wafermap Prerendering module', () => {
const margin = { top: 20, right: 10, bottom: 0, left: 0 };
beforeEach(() => {
+ const dataManagerMock = getDataManagerMock(
+ dieDimensions,
+ margin,
+ defaultHorizontalScale,
+ defaultVerticalScale
+ );
const waferMock = getWaferMapMockPrerendering(
getWaferMapDies(),
{ colors: [], values: [] },
@@ -30,18 +34,10 @@ describe('Wafermap Prerendering module', () => {
WaferMapColorScaleMode.linear,
dieLabelsHidden,
dieLabelsSuffix,
- maxCharacters
- );
- const dataManagerMock = getDataManagerMock(
- dieDimensions,
- margin,
- defaultHorizontalScale,
- defaultVerticalScale
- );
- prerenderingModule = new Prerendering(
- waferMock as WaferMap,
- dataManagerMock as DataManager
+ maxCharacters,
+ dataManagerMock
);
+ prerenderingModule = new Prerendering(waferMock);
prerenderingModule.updateLabelsFontSize();
});
@@ -83,6 +79,12 @@ describe('Wafermap Prerendering module', () => {
const highlightedTags: string[] = [];
beforeEach(() => {
+ const dataManagerMock = getDataManagerMock(
+ dieDimensions,
+ margin,
+ horizontalScale,
+ defaultVerticalScale
+ );
const waferMock = getWaferMapMockPrerendering(
getWaferMapDies(),
{ colors: [], values: [] },
@@ -90,18 +92,10 @@ describe('Wafermap Prerendering module', () => {
WaferMapColorScaleMode.linear,
dieLabelsHidden,
dieLabelsSuffix,
- maxCharacters
- );
- const dataManagerMock = getDataManagerMock(
- dieDimensions,
- margin,
- horizontalScale,
- defaultVerticalScale
- );
- prerenderingModule = new Prerendering(
- waferMock as WaferMap,
- dataManagerMock as DataManager
+ maxCharacters,
+ dataManagerMock
);
+ prerenderingModule = new Prerendering(waferMock);
prerenderingModule.updateLabelsFontSize();
});
@@ -132,6 +126,12 @@ describe('Wafermap Prerendering module', () => {
const highlightedTags: string[] = [];
beforeEach(() => {
+ const dataManagerMock = getDataManagerMock(
+ dieDimensions,
+ margin,
+ defaultHorizontalScale,
+ verticalScale
+ );
const waferMock = getWaferMapMockPrerendering(
getWaferMapDies(),
{ colors: [], values: [] },
@@ -139,18 +139,10 @@ describe('Wafermap Prerendering module', () => {
WaferMapColorScaleMode.linear,
dieLabelsHidden,
dieLabelsSuffix,
- maxCharacters
- );
- const dataManagerMock = getDataManagerMock(
- dieDimensions,
- margin,
- defaultHorizontalScale,
- verticalScale
- );
- prerenderingModule = new Prerendering(
- waferMock as WaferMap,
- dataManagerMock as DataManager
+ maxCharacters,
+ dataManagerMock
);
+ prerenderingModule = new Prerendering(waferMock);
prerenderingModule.updateLabelsFontSize();
});
diff --git a/packages/nimble-components/src/wafer-map/tests/sets.ts b/packages/nimble-components/src/wafer-map/tests/sets.ts
index 96045d371d..80089b35ee 100644
--- a/packages/nimble-components/src/wafer-map/tests/sets.ts
+++ b/packages/nimble-components/src/wafer-map/tests/sets.ts
@@ -1,3 +1,4 @@
+import { Table, tableFromArrays } from 'apache-arrow';
import type { WaferMapDie, WaferMapColorScale } from '../types';
export const highlightedTagsSets: string[][] = [
@@ -100,6 +101,62 @@ export const wafermapDieSets: WaferMapDie[][] = [
]
];
+export const wafermapDiesTableSets: Table[] = [
+ tableFromArrays({
+ colIndex: Int32Array.from([0, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 4]),
+ rowIndex: Int32Array.from([2, 2, 1, 3, 2, 1, 0, 3, 4, 2, 1, 3, 2]),
+ value: Float64Array.from([
+ 14.24, 76.43, 44.63, 67.93, 72.71, 79.04, 26.49, 37.79, 59.82, 52.9,
+ 98.5, 20.83, 62.8
+ ]),
+ firstTag: [
+ 'a',
+ 'b',
+ 'g',
+ 'a',
+ 'h',
+ 'b',
+ 'c',
+ null,
+ null,
+ null,
+ 'g',
+ 'c',
+ 'g'
+ ],
+ secondTag: [
+ 'b',
+ 'c',
+ null,
+ null,
+ 'e',
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ ],
+ metadata: [
+ 'metadata02',
+ 'metadata12',
+ 'metadata11',
+ 'metadata13',
+ 'metadata22',
+ 'metadata21',
+ 'metadata20',
+ 'metadata23',
+ 'metadata24',
+ 'metadata32',
+ 'metadata31',
+ 'metadata33',
+ 'metadata42'
+ ]
+ })
+];
+
export const waferMapColorScaleSets: WaferMapColorScale[] = [
{
colors: ['red', 'orange', 'green'],
diff --git a/packages/nimble-components/src/wafer-map/tests/utilities.ts b/packages/nimble-components/src/wafer-map/tests/utilities.ts
index f99e96a02e..30f100c3ff 100644
--- a/packages/nimble-components/src/wafer-map/tests/utilities.ts
+++ b/packages/nimble-components/src/wafer-map/tests/utilities.ts
@@ -1,12 +1,16 @@
-import { ScaleBand, scaleBand } from 'd3-scale';
+import { ScaleBand, ScaleQuantile, scaleBand, scaleQuantile } from 'd3-scale';
+import { type Table, tableFromArrays } from 'apache-arrow';
+import type { ZoomTransform } from 'd3-zoom';
import {
Dimensions,
+ HoverDie,
Margin,
WaferMapColorScale,
WaferMapColorScaleMode,
WaferMapDie,
WaferMapOriginLocation,
- WaferMapValidity
+ WaferMapValidity,
+ WaferRequiredFields
} from '../types';
import type { DataManager } from '../modules/data-manager';
import type { WaferMap } from '..';
@@ -33,6 +37,19 @@ export function getWaferMapDies(): WaferMapDie[] {
{ value: '18', x: 6, y: 4 }
];
}
+export function getWaferMapDiesTable(): Table {
+ return tableFromArrays({
+ colIndex: new Int32Array([
+ 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6
+ ]),
+ rowIndex: new Int32Array([
+ 3, 4, 2, 3, 4, 5, 1, 2, 3, 4, 5, 6, 2, 3, 4, 5, 3, 4
+ ]),
+ value: new Float64Array([
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18
+ ])
+ });
+}
export function getWaferMapDiesAsFloats(): WaferMapDie[] {
return getWaferMapDies().map(die => {
@@ -63,6 +80,12 @@ export function getScaleBand(
return scaleBand().domain(domain).range(range);
}
+export function getScaleQuantile(
+ domain: number[] = [],
+ range: number[] = []
+): ScaleQuantile {
+ return scaleQuantile().domain(domain).range(range);
+}
export const defaultHorizontalScale = scaleBand()
.domain([2, 3, 4, 5, 6])
.range([2, 7]);
@@ -76,16 +99,39 @@ export function getDataManagerMock(
margin: Margin,
horizontalScale: ScaleBand = getScaleBand([], []),
verticalScale: ScaleBand = getScaleBand([], [])
-): Pick<
+): DataManager {
+ const dataManagerMock: Pick<
DataManager,
'horizontalScale' | 'verticalScale' | 'dieDimensions' | 'margin'
- > {
- return {
+ > = {
horizontalScale,
verticalScale,
dieDimensions,
margin
};
+ return dataManagerMock as DataManager;
+}
+
+export function getDataManagerMockForHover(
+ margin: Margin,
+ invertedHorizontalScale: ScaleQuantile = getScaleQuantile(
+ [],
+ []
+ ),
+ invertedVerticalScale: ScaleQuantile = getScaleQuantile(
+ [],
+ []
+ )
+): DataManager {
+ const dataManagerMock: Pick<
+ DataManager,
+ 'invertedHorizontalScale' | 'invertedVerticalScale' | 'margin'
+ > = {
+ invertedHorizontalScale,
+ invertedVerticalScale,
+ margin
+ };
+ return dataManagerMock as DataManager;
}
export function getWaferMapMockPrerendering(
@@ -95,8 +141,10 @@ export function getWaferMapMockPrerendering(
colorScaleMode: WaferMapColorScaleMode = WaferMapColorScaleMode.linear,
dieLabelsHidden = true,
dieLabelsSuffix = '',
- maxCharacters = 4
-): Pick<
+ maxCharacters = 4,
+ dataManager = {} as DataManager
+): WaferMap {
+ const waferMapMock: Pick<
WaferMap,
| 'dies'
| 'colorScale'
@@ -105,47 +153,111 @@ export function getWaferMapMockPrerendering(
| 'dieLabelsHidden'
| 'dieLabelsSuffix'
| 'maxCharacters'
- > {
- return {
+ | 'dataManager'
+ > = {
dies,
colorScale,
highlightedTags,
colorScaleMode,
dieLabelsHidden,
dieLabelsSuffix,
- maxCharacters
+ maxCharacters,
+ dataManager
};
+ return waferMapMock as WaferMap;
}
+export function getWaferMapMockHover(
+ diesTable: Table,
+ transform: ZoomTransform,
+ originLocation: WaferMapOriginLocation,
+ hoverDie: HoverDie | undefined,
+ dataManager: DataManager,
+ isExperimentalRenderer: boolean
+): WaferMap {
+ const waferMapMock: Pick<
+ WaferMap,
+ | 'diesTable'
+ | 'transform'
+ | 'originLocation'
+ | 'hoverDie'
+ | 'dataManager'
+ | 'isExperimentalRenderer'
+ > = {
+ diesTable,
+ transform,
+ originLocation,
+ hoverDie,
+ dataManager,
+ isExperimentalRenderer: () => isExperimentalRenderer
+ };
+ return waferMapMock as WaferMap;
+}
export function getWaferMapMockComputations(
dies: WaferMapDie[] = getWaferMapDies(),
originLocation: WaferMapOriginLocation,
canvasWidth: number,
canvasHeight: number,
- validity: WaferMapValidity = { invalidGridDimensions: false }
-): Pick<
+ validity: WaferMapValidity = {
+ invalidGridDimensions: false,
+ invalidDiesTableSchema: false
+ }
+): WaferMap {
+ const waferMapMock: Pick<
WaferMap,
'dies' | 'originLocation' | 'canvasWidth' | 'canvasHeight' | 'validity'
- > {
- return {
+ > = {
dies,
originLocation,
canvasWidth,
canvasHeight,
validity
};
+ return waferMapMock as WaferMap;
+}
+export function getWaferMapMockComputationsExperimental(
+ diesTable: Table = getWaferMapDiesTable(),
+ originLocation: WaferMapOriginLocation,
+ canvasWidth: number,
+ canvasHeight: number,
+ validity: WaferMapValidity = {
+ invalidGridDimensions: false,
+ invalidDiesTableSchema: false
+ }
+): WaferMap {
+ const waferMapMock: Pick<
+ WaferMap,
+ | 'diesTable'
+ | 'originLocation'
+ | 'canvasWidth'
+ | 'canvasHeight'
+ | 'validity'
+ > = {
+ diesTable,
+ originLocation,
+ canvasWidth,
+ canvasHeight,
+ validity
+ };
+ return waferMapMock as WaferMap;
}
export function getWaferMapMockValidator(
gridMinX: number | undefined,
gridMaxX: number | undefined,
gridMinY: number | undefined,
- gridMaxY: number | undefined
-): Pick {
- return {
+ gridMaxY: number | undefined,
+ diesTable: Table | undefined = undefined
+): WaferMap {
+ const waferMapMock: Pick<
+ WaferMap,
+ 'gridMinX' | 'gridMaxX' | 'gridMinY' | 'gridMaxY' | 'diesTable'
+ > = {
gridMinX,
gridMaxX,
gridMinY,
- gridMaxY
+ gridMaxY,
+ diesTable
};
+ return waferMapMock as WaferMap;
}
diff --git a/packages/nimble-components/src/wafer-map/tests/wafer-map-validator.spec.ts b/packages/nimble-components/src/wafer-map/tests/wafer-map-validator.spec.ts
index 7325e432ce..659fbee23c 100644
--- a/packages/nimble-components/src/wafer-map/tests/wafer-map-validator.spec.ts
+++ b/packages/nimble-components/src/wafer-map/tests/wafer-map-validator.spec.ts
@@ -1,84 +1,179 @@
-import type { WaferMap } from '..';
+import { Table, tableFromArrays } from 'apache-arrow';
import { WaferMapValidator } from '../modules/wafer-map-validator';
import { getWaferMapMockValidator } from './utilities';
describe('Wafermap Validator module', () => {
let waferMapValidator: WaferMapValidator;
- describe('with undefined grid dimensions', () => {
- beforeEach(() => {
- const waferMock = getWaferMapMockValidator(
- undefined,
- undefined,
- undefined,
- undefined
- );
- waferMapValidator = new WaferMapValidator(waferMock as WaferMap);
- waferMapValidator.validateGridDimensions();
- });
+ it('with undefined grid dimensions should be valid', () => {
+ const waferMock = getWaferMapMockValidator(
+ undefined,
+ undefined,
+ undefined,
+ undefined
+ );
+ waferMapValidator = new WaferMapValidator(waferMock);
+ waferMapValidator.validateGridDimensions();
- it('should be valid', () => {
- expect(waferMapValidator.isValid()).toBeTrue();
- });
+ expect(waferMapValidator.isValid()).toBeTrue();
});
- describe('with equal grid dimensions', () => {
- beforeEach(() => {
- const waferMock = getWaferMapMockValidator(0, 0, 0, 0);
- waferMapValidator = new WaferMapValidator(waferMock as WaferMap);
- waferMapValidator.validateGridDimensions();
- });
+ it('with equal grid dimensions should be valid', () => {
+ const waferMock = getWaferMapMockValidator(0, 0, 0, 0);
+ waferMapValidator = new WaferMapValidator(waferMock);
+ waferMapValidator.validateGridDimensions();
- it('should be valid', () => {
- expect(waferMapValidator.isValid()).toBeTrue();
- });
+ expect(waferMapValidator.isValid()).toBeTrue();
});
- describe('with positive grid dimensions', () => {
- beforeEach(() => {
- const waferMock = getWaferMapMockValidator(1, 2, 1, 2);
- waferMapValidator = new WaferMapValidator(waferMock as WaferMap);
- waferMapValidator.validateGridDimensions();
- });
+ it('with positive grid dimensions should be valid', () => {
+ const waferMock = getWaferMapMockValidator(1, 2, 1, 2);
+ waferMapValidator = new WaferMapValidator(waferMock);
+ waferMapValidator.validateGridDimensions();
- it('should be valid', () => {
- expect(waferMapValidator.isValid()).toBeTrue();
- });
+ expect(waferMapValidator.isValid()).toBeTrue();
});
- describe('with negative grid dimensions', () => {
- beforeEach(() => {
- const waferMock = getWaferMapMockValidator(-2, -1, -2, -1);
- waferMapValidator = new WaferMapValidator(waferMock as WaferMap);
- waferMapValidator.validateGridDimensions();
- });
+ it('with negative grid dimensions should be valid', () => {
+ const waferMock = getWaferMapMockValidator(-2, -1, -2, -1);
+ waferMapValidator = new WaferMapValidator(waferMock);
+ waferMapValidator.validateGridDimensions();
- it('should be valid', () => {
- expect(waferMapValidator.isValid()).toBeTrue();
- });
+ expect(waferMapValidator.isValid()).toBeTrue();
});
- describe('with one undefined grid dimension', () => {
- beforeEach(() => {
- const waferMock = getWaferMapMockValidator(0, 0, 0, undefined);
- waferMapValidator = new WaferMapValidator(waferMock as WaferMap);
- waferMapValidator.validateGridDimensions();
- });
+ it('with one undefined grid dimension should not be valid', () => {
+ const waferMock = getWaferMapMockValidator(0, 0, 0, undefined);
+ waferMapValidator = new WaferMapValidator(waferMock);
+ waferMapValidator.validateGridDimensions();
- it('should not be valid', () => {
- expect(waferMapValidator.isValid()).toBeFalse();
- });
+ expect(waferMapValidator.isValid()).toBeFalse();
});
- describe('with impossible grid dimension', () => {
- beforeEach(() => {
- const waferMock = getWaferMapMockValidator(1, -1, 1, -1);
- waferMapValidator = new WaferMapValidator(waferMock as WaferMap);
- waferMapValidator.validateGridDimensions();
- });
+ it('with impossible grid dimension should not be valid', () => {
+ const waferMock = getWaferMapMockValidator(1, -1, 1, -1);
+ waferMapValidator = new WaferMapValidator(waferMock);
+ waferMapValidator.validateGridDimensions();
- it('should not be valid', () => {
- expect(waferMapValidator.isValid()).toBeFalse();
+ expect(waferMapValidator.getValidity()).toEqual({
+ invalidGridDimensions: true,
+ invalidDiesTableSchema: false
});
+ expect(waferMapValidator.isValid()).toBeFalse();
+ });
+
+ it('with undefined dies table should be valid', () => {
+ const waferMock = getWaferMapMockValidator(
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined
+ );
+ waferMapValidator = new WaferMapValidator(waferMock);
+ waferMapValidator.validateDiesTableSchema();
+ expect(waferMapValidator.isValid()).toBeTrue();
+ });
+
+ it('with colIndex, rowIndex and value column as Int32, Int32 and Float64 dies table should be valid', () => {
+ const waferMock = getWaferMapMockValidator(
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ tableFromArrays({
+ colIndex: Int32Array.from([]),
+ rowIndex: Int32Array.from([]),
+ value: Float64Array.from([])
+ })
+ );
+ waferMapValidator = new WaferMapValidator(waferMock);
+ waferMapValidator.validateDiesTableSchema();
+
+ expect(waferMapValidator.isValid()).toBeTrue();
+ });
+
+ it('with colIndex, rowIndex and value column as Int32, Int32 and Float32 dies table should be invalid', () => {
+ const waferMock = getWaferMapMockValidator(
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ tableFromArrays({
+ colIndex: Int32Array.from([]),
+ rowIndex: Int32Array.from([]),
+ value: Float32Array.from([])
+ })
+ );
+ waferMapValidator = new WaferMapValidator(waferMock);
+ waferMapValidator.validateDiesTableSchema();
+
+ expect(waferMapValidator.isValid()).toBeFalse();
+ });
+
+ it('with colIndex, rowIndex and value column as Int8, Int32 and Float64 dies table should be invalid', () => {
+ const waferMock = getWaferMapMockValidator(
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ tableFromArrays({
+ colIndex: Int8Array.from([]),
+ rowIndex: Int32Array.from([]),
+ value: Float64Array.from([])
+ })
+ );
+ waferMapValidator = new WaferMapValidator(waferMock);
+ waferMapValidator.validateDiesTableSchema();
+
+ expect(waferMapValidator.isValid()).toBeFalse();
+ });
+
+ it('with colIndex, rowIndex and value column as Int32 dies table should be invalid', () => {
+ const waferMock = getWaferMapMockValidator(
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ tableFromArrays({
+ colIndex: Int32Array.from([]),
+ rowIndex: Int32Array.from([]),
+ value: Int32Array.from([])
+ })
+ );
+ waferMapValidator = new WaferMapValidator(waferMock);
+ waferMapValidator.validateDiesTableSchema();
+
+ expect(waferMapValidator.isValid()).toBeFalse();
+ });
+
+ it('with no column dies table should be invalid', () => {
+ const waferMock = getWaferMapMockValidator(
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ new Table()
+ );
+ waferMapValidator = new WaferMapValidator(waferMock);
+ waferMapValidator.validateDiesTableSchema();
+
+ expect(waferMapValidator.isValid()).toBeFalse();
+ });
+ it('with just colIndex and rowIndex column dies table should be invalid', () => {
+ const waferMock = getWaferMapMockValidator(
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ tableFromArrays({
+ colIndex: Int32Array.from([]),
+ rowIndex: Int32Array.from([])
+ })
+ );
+ waferMapValidator = new WaferMapValidator(waferMock);
+ waferMapValidator.validateDiesTableSchema();
+
+ expect(waferMapValidator.isValid()).toBeFalse();
});
});
diff --git a/packages/nimble-components/src/wafer-map/tests/wafer-map.spec.ts b/packages/nimble-components/src/wafer-map/tests/wafer-map.spec.ts
index 82326e07db..dbb9cb7e0f 100644
--- a/packages/nimble-components/src/wafer-map/tests/wafer-map.spec.ts
+++ b/packages/nimble-components/src/wafer-map/tests/wafer-map.spec.ts
@@ -1,4 +1,5 @@
import { html } from '@microsoft/fast-element';
+import { Table, tableFromArrays } from 'apache-arrow';
import { WaferMap } from '..';
import { processUpdates } from '../../testing/async-helpers';
import { type Fixture, fixture } from '../../utilities/tests/fixture';
@@ -7,6 +8,8 @@ import {
WaferMapOrientation,
WaferMapOriginLocation
} from '../types';
+import { RenderingModule } from '../modules/rendering';
+import { WorkerRenderer } from '../modules/experimental/worker-renderer';
async function setup(): Promise> {
return fixture(html` `);
@@ -85,6 +88,12 @@ describe('WaferMap', () => {
expect(spy).toHaveBeenCalledTimes(1);
});
+ it('will update once after diesTable change', () => {
+ element.diesTable = new Table();
+ processUpdates();
+ expect(spy).toHaveBeenCalledTimes(1);
+ });
+
it('will update once after colorScale changes', () => {
element.colorScale = { colors: ['red', 'red'], values: ['1', '1'] };
processUpdates();
@@ -106,6 +115,79 @@ describe('WaferMap', () => {
});
});
+ describe('worker renderer draw flow', () => {
+ let drawWaferSpy: jasmine.Spy;
+ beforeEach(() => {
+ drawWaferSpy = spyOn(element.workerRenderer, 'drawWafer');
+ });
+
+ // skipped until prerendering is refactored
+ xit('will call drawWafer after supported diesTable change', () => {
+ element.diesTable = tableFromArrays({
+ colIndex: Int32Array.from([]),
+ rowIndex: Int32Array.from([]),
+ value: Float64Array.from([])
+ });
+ processUpdates();
+ expect(element.validity.invalidDiesTableSchema).toBeFalse();
+ expect(drawWaferSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it('will not call drawWafer after unsupported diesTable change', () => {
+ element.diesTable = new Table();
+ processUpdates();
+ expect(element.validity.invalidDiesTableSchema).toBeTrue();
+ expect(drawWaferSpy).toHaveBeenCalledTimes(0);
+ });
+ });
+
+ describe('worker renderer flow', () => {
+ let renderHoverSpy: jasmine.Spy;
+ beforeEach(() => {
+ renderHoverSpy = spyOn(element.workerRenderer, 'renderHover');
+ });
+
+ it('will use RenderingModule after dies change', () => {
+ element.dies = [{ x: 1, y: 1, value: '1' }];
+ processUpdates();
+ expect(element.renderer instanceof RenderingModule).toBeTrue();
+ });
+
+ it('will use WorkerRenderer after supported diesTable change', () => {
+ element.diesTable = tableFromArrays({
+ colIndex: Int32Array.from([]),
+ rowIndex: Int32Array.from([]),
+ value: Float64Array.from([])
+ });
+ processUpdates();
+ expect(element.renderer instanceof WorkerRenderer).toBeTrue();
+ });
+
+ it('will use RenderingModule after unsupported diesTable change', () => {
+ element.diesTable = new Table();
+ processUpdates();
+ expect(element.renderer instanceof RenderingModule).toBeTrue();
+ });
+
+ it('will call renderHover after supported diesTable change', () => {
+ element.diesTable = tableFromArrays({
+ colIndex: Int32Array.from([]),
+ rowIndex: Int32Array.from([]),
+ value: Float64Array.from([])
+ });
+ processUpdates();
+ expect(element.validity.invalidDiesTableSchema).toBeFalse();
+ expect(renderHoverSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it('will not call renderHover after unsupported diesTable change', () => {
+ element.diesTable = new Table();
+ processUpdates();
+ expect(element.validity.invalidDiesTableSchema).toBeTrue();
+ expect(renderHoverSpy).toHaveBeenCalledTimes(0);
+ });
+ });
+
describe('zoom flow', () => {
let initialValue: string | undefined;
@@ -120,7 +202,7 @@ describe('WaferMap', () => {
});
it('will zoom in the wafer-map', () => {
- element.canvas.dispatchEvent(
+ element.dispatchEvent(
new WheelEvent('wheel', { deltaY: -2, deltaMode: -1 })
);
processUpdates();
@@ -129,7 +211,7 @@ describe('WaferMap', () => {
});
it('will zoom out to identity', () => {
- element.canvas.dispatchEvent(
+ element.dispatchEvent(
new WheelEvent('wheel', { deltaY: -2, deltaMode: -1 })
);
@@ -137,7 +219,7 @@ describe('WaferMap', () => {
const zoomedValue = getTransform();
expect(zoomedValue).not.toEqual(initialValue);
- element.canvas.dispatchEvent(
+ element.dispatchEvent(
new WheelEvent('wheel', { deltaY: 2, deltaMode: -1 })
);
@@ -147,7 +229,7 @@ describe('WaferMap', () => {
});
it('will not zoom out when at identity', () => {
- element.canvas.dispatchEvent(
+ element.dispatchEvent(
new WheelEvent('wheel', { deltaY: 2, deltaMode: -1 })
);
processUpdates();
@@ -187,7 +269,7 @@ describe('WaferMap', () => {
expect(initialHeight).toBe(460);
expect(initialWidth).toBe(460);
- element.canvas.dispatchEvent(
+ element.dispatchEvent(
new WheelEvent('wheel', { deltaY: -2, deltaMode: -1 })
);
processUpdates();
@@ -206,7 +288,7 @@ describe('WaferMap', () => {
processUpdates();
const initialTransform = element.hoverTransform;
expect(initialTransform).not.toEqual('');
- element.canvas.dispatchEvent(
+ element.dispatchEvent(
new WheelEvent('wheel', { deltaY: -2, deltaMode: -1 })
);
processUpdates();
diff --git a/packages/nimble-components/src/wafer-map/tests/wafer-map.stories.ts b/packages/nimble-components/src/wafer-map/tests/wafer-map.stories.ts
index 2d4a35ac5a..c0b82ce309 100644
--- a/packages/nimble-components/src/wafer-map/tests/wafer-map.stories.ts
+++ b/packages/nimble-components/src/wafer-map/tests/wafer-map.stories.ts
@@ -1,10 +1,11 @@
import { html } from '@microsoft/fast-element';
import type { Meta, StoryObj } from '@storybook/html';
+import type { Table } from 'apache-arrow';
import {
createUserSelectedThemeStory,
incubatingWarning
} from '../../utilities/tests/storybook';
-import { generateWaferData } from './data-generator';
+import { generateWaferData, generateWaferTableData } from './data-generator';
import {
goodValueGenerator,
badValueGenerator,
@@ -23,7 +24,8 @@ import {
import {
highlightedTagsSets,
wafermapDieSets,
- waferMapColorScaleSets
+ waferMapColorScaleSets,
+ wafermapDiesTableSets
} from './sets';
import { waferMapTag } from '..';
@@ -32,7 +34,10 @@ interface WaferMapArgs {
colorScaleMode: WaferMapColorScaleMode;
dieLabelsHidden: boolean;
dieLabelsSuffix: string;
+ apiVersion: 'stable' | 'experimental';
dies: string;
+ highlightedTags: string;
+ diesTable: string;
maxCharacters: number;
orientation: WaferMapOrientation;
originLocation: WaferMapOriginLocation;
@@ -42,7 +47,6 @@ interface WaferMapArgs {
gridMaxY: number | undefined;
dieHover: unknown;
validity: WaferMapValidity;
- highlightedTags: string;
}
const getDiesSet = (
@@ -77,7 +81,38 @@ const getDiesSet = (
)!;
break;
default:
- returnedValue = [] as WaferMapDie[];
+ returnedValue = [];
+ }
+ return returnedValue;
+};
+
+const getDiesTableSet = (setName: string, sets: Table[]): Table | undefined => {
+ const seed = 0.5;
+ let returnedValue: Table | undefined;
+ switch (setName) {
+ case 'fixedDies10':
+ returnedValue = sets[0]!;
+ break;
+ case 'goodDies100':
+ returnedValue = generateWaferTableData(
+ 100,
+ goodValueGenerator(seed)
+ );
+ break;
+ case 'goodDies1000':
+ returnedValue = generateWaferTableData(
+ 1000,
+ goodValueGenerator(seed)
+ )!;
+ break;
+ case 'badDies10000':
+ returnedValue = generateWaferTableData(
+ 10000,
+ badValueGenerator(seed)
+ )!;
+ break;
+ default:
+ returnedValue = undefined;
}
return returnedValue;
};
@@ -98,7 +133,7 @@ const getHighlightedTags = (setName: string, sets: string[][]): string[] => {
returnedValue = sets[3]!;
break;
default:
- returnedValue = [] as string[];
+ returnedValue = [];
break;
}
return returnedValue;
@@ -107,6 +142,7 @@ const getHighlightedTags = (setName: string, sets: string[][]): string[] => {
const metadata: Meta = {
title: 'Incubating/Wafer Map',
parameters: {
+ viewMode: 'docs',
actions: {
handles: ['click', 'die-hover']
}
@@ -126,6 +162,7 @@ const metadata: Meta = {
:colorScale="${x => x.colorScale}"
:dies="${x => getDiesSet(x.dies, wafermapDieSets)}"
:highlightedTags="${x => getHighlightedTags(x.highlightedTags, highlightedTagsSets)}"
+ :diesTable="${x => getDiesTableSet(x.diesTable, wafermapDiesTableSets)}"
>
${waferMapTag}>
`),
args: {
+ apiVersion: 'stable',
colorScale: waferMapColorScaleSets[0],
colorScaleMode: WaferMapColorScaleMode.linear,
dies: 'fixedDies10',
+ diesTable: undefined,
+ highlightedTags: 'set1',
dieLabelsHidden: false,
dieLabelsSuffix: '',
- highlightedTags: 'set1',
maxCharacters: 4,
orientation: WaferMapOrientation.left,
originLocation: WaferMapOriginLocation.bottomLeft,
@@ -151,6 +190,20 @@ const metadata: Meta = {
gridMaxY: undefined
},
argTypes: {
+ apiVersion: {
+ name: 'API Version',
+ description:
+ 'Select the API version of the component. The stable version is the one that is recommended for production use, while the experimental version is the one that is still under development and is not recommended for production use. The default value is `stable`. To enable the Experimental API in code, the `diesTable` should be used in place of the `dies`.',
+ options: ['stable', 'experimental'],
+ control: {
+ type: 'inline-radio',
+ labels: {
+ stable: 'Stable',
+ experimental: 'Experimental'
+ }
+ },
+ defaultValue: 'stable'
+ },
colorScale: {
description: `Represents the color spectrum which shows the status of the dies on the wafer.
@@ -185,7 +238,7 @@ const metadata: Meta = {
}
},
dies: {
- description: `Represents the input data, an array of \`WaferMapDie\`, which will be rendered by the wafer map
+ description: `Represents the input data, an array of \`WaferMapDie\`, which will be rendered by the wafer map. Part of the Stable API.
Usage details
@@ -207,19 +260,34 @@ const metadata: Meta = {
badDies10000: 'Very large dies set of mostly bad values'
}
},
- defaultValue: 'set1'
- },
- dieLabelsHidden: {
- name: 'die-labels-hidden',
- description:
- 'Boolean value that determines if the dies labels in the wafer map view are shown or not. Default value is false.',
- control: { type: 'boolean' }
+ defaultValue: 'fixedDies10',
+ if: { arg: 'apiVersion', eq: 'stable' }
},
- dieLabelsSuffix: {
- name: 'die-labels-suffix',
- description:
- 'String that can be added as a label at the end of each wafer map die value',
- control: { type: 'text' }
+ diesTable: {
+ description: `Represents the input data, an apache-arrow \`Table\`, which will be rendered by the wafer map. Part of the Experimental API.
+
+
+ Usage details
+ The \`diesTable\` element is a public property. As such, it is not available as an attribute, however it can be read or set on the corresponding \`WaferMap\` DOM element.
+
+ `,
+ options: [
+ 'fixedDies10',
+ 'goodDies100',
+ 'goodDies1000',
+ 'badDies10000'
+ ],
+ control: {
+ type: 'radio',
+ labels: {
+ fixedDies10: 'Small dies set of fixed values',
+ goodDies100: 'Medium dies set of mostly good values',
+ goodDies1000: 'Large dies set of mostly good values',
+ badDies10000: 'Very large dies set of mostly bad values'
+ }
+ },
+ defaultValue: 'fixedDies10',
+ if: { arg: 'apiVersion', eq: 'experimental' }
},
highlightedTags: {
description: `Represent a list of strings that will be highlighted in the wafer map view. Each die has a tags?: string[] property, if at least one element of highlightedTags equals at least one element of die.tags the die will be highlighted.
@@ -241,6 +309,18 @@ const metadata: Meta = {
},
defaultValue: 'set1'
},
+ dieLabelsHidden: {
+ name: 'die-labels-hidden',
+ description:
+ 'Boolean value that determines if the dies labels in the wafer map view are shown or not. Default value is false.',
+ control: { type: 'boolean' }
+ },
+ dieLabelsSuffix: {
+ name: 'die-labels-suffix',
+ description:
+ 'String that can be added as a label at the end of each wafer map die value',
+ control: { type: 'text' }
+ },
maxCharacters: {
name: 'max-characters',
description:
@@ -308,7 +388,9 @@ const metadata: Meta = {
description: `Readonly object of boolean values that represents the validity states that the wafer map's configuration can be in.
The object's type is \`WaferMapValidity\`, and it contains the following boolean properties:
-- \`invalidGridDimensions \`: \`true\` when some of the \`gridMinX\`, \`gridMinY\`, \`gridMaxX\` or \`gridMaxY\` are \`undefined\`, but \`false\` when all of them are provided or all of them are \`undefined\``,
+- \`invalidGridDimensions \`: \`true\` when some of the \`gridMinX\`, \`gridMinY\`, \`gridMaxX\` or \`gridMaxY\` are \`undefined\`, but \`false\` when all of them are provided or all of them are \`undefined\`
+
+- \`invalidDiesTableSchema \`: \`true\` when the \`diesTable\` does not have all of the three expected columns: \`colIndex\`, \`rowIndex\` and \`value\`, but \`false\` when all of them are provided or the \`diesTable\` is \`undefined\``,
control: false
}
}
diff --git a/packages/nimble-components/src/wafer-map/types.ts b/packages/nimble-components/src/wafer-map/types.ts
index c8af44ad91..7193593c4f 100644
--- a/packages/nimble-components/src/wafer-map/types.ts
+++ b/packages/nimble-components/src/wafer-map/types.ts
@@ -1,4 +1,4 @@
-import type { DataManager } from './modules/data-manager';
+import type { Float64, Int32 } from 'apache-arrow';
export const WaferMapOriginLocation = {
bottomLeft: 'bottom-left',
@@ -45,18 +45,17 @@ export interface WaferMapDie {
tags?: string[];
}
+export interface HoverDie {
+ index: number;
+ x: number;
+ y: number;
+}
+
export interface WaferMapColorScale {
colors: string[];
values: string[];
}
-export interface HoverHandlerData {
- canvas: HTMLCanvasElement;
- rect: HTMLElement;
- dataManager: DataManager;
- originLocation: WaferMapOriginLocation;
-}
-
export interface Dimensions {
readonly width: number;
readonly height: number;
@@ -86,4 +85,18 @@ export interface ValidityObject {
}
export interface WaferMapValidity extends ValidityObject {
readonly invalidGridDimensions: boolean;
+ readonly invalidDiesTableSchema: boolean;
}
+
+// Apache arrow probably should not be using a Record and index types on TypeMap
+// because in strict checking they end up required.
+// See: https://github.com/apache/arrow/issues/12663#issuecomment-1088244575
+// We can work around that issue by using a type alias instead of an interface
+// Where index signatures are looser.
+// See: https://github.com/microsoft/TypeScript/issues/15300#issuecomment-1317901527
+// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
+export type WaferRequiredFields = {
+ colIndex: Int32,
+ rowIndex: Int32,
+ value: Float64
+};
diff --git a/packages/nimble-components/tsconfig.json b/packages/nimble-components/tsconfig.json
index a0fb1d14bd..4d2592292d 100644
--- a/packages/nimble-components/tsconfig.json
+++ b/packages/nimble-components/tsconfig.json
@@ -21,7 +21,8 @@
"importsNotUsedAsValues": "error",
"noImplicitOverride": true,
"noUncheckedIndexedAccess": true,
- "noPropertyAccessFromIndexSignature": false
+ "noPropertyAccessFromIndexSignature": false,
+ "jsx": "preserve"
},
"include": ["src"]
}
diff --git a/packages/nimble-tokens/CHANGELOG.json b/packages/nimble-tokens/CHANGELOG.json
index b9caa9a88d..040f937e44 100644
--- a/packages/nimble-tokens/CHANGELOG.json
+++ b/packages/nimble-tokens/CHANGELOG.json
@@ -1,6 +1,51 @@
{
"name": "@ni/nimble-tokens",
"entries": [
+ {
+ "date": "Tue, 26 Mar 2024 20:45:41 GMT",
+ "version": "6.13.1",
+ "tag": "@ni/nimble-tokens_v6.13.1",
+ "comments": {
+ "patch": [
+ {
+ "author": "7282195+m-akinc@users.noreply.github.com",
+ "package": "@ni/nimble-tokens",
+ "commit": "594ac221124cc1b52cd28113714bb8ff71e5820b",
+ "comment": "Fix value of DigitalGreenDark"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Thu, 21 Mar 2024 17:13:38 GMT",
+ "version": "6.13.0",
+ "tag": "@ni/nimble-tokens_v6.13.0",
+ "comments": {
+ "minor": [
+ {
+ "author": "1458528+fredvisser@users.noreply.github.com",
+ "package": "@ni/nimble-tokens",
+ "commit": "b93385a27672ebc03e18a864e8fa268727595c66",
+ "comment": "Add Body_2 base font token"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Tue, 12 Mar 2024 21:01:54 GMT",
+ "version": "6.12.1",
+ "tag": "@ni/nimble-tokens_v6.12.1",
+ "comments": {
+ "patch": [
+ {
+ "author": "7282195+m-akinc@users.noreply.github.com",
+ "package": "@ni/nimble-tokens",
+ "commit": "bc825e1b057eafd8bc005d11e9a224aa9aee9619",
+ "comment": "Update typescript to 4.9.5"
+ }
+ ]
+ }
+ },
{
"date": "Wed, 06 Mar 2024 17:56:10 GMT",
"version": "6.12.0",
diff --git a/packages/nimble-tokens/CHANGELOG.md b/packages/nimble-tokens/CHANGELOG.md
index fbb4e1919e..3eb9526e4c 100644
--- a/packages/nimble-tokens/CHANGELOG.md
+++ b/packages/nimble-tokens/CHANGELOG.md
@@ -1,9 +1,33 @@
# Change Log - @ni/nimble-tokens
-This log was last generated on Wed, 28 Feb 2024 19:35:04 GMT and should not be manually modified.
+This log was last generated on Tue, 26 Mar 2024 20:45:41 GMT and should not be manually modified.
+## 6.13.1
+
+Tue, 26 Mar 2024 20:45:41 GMT
+
+### Patches
+
+- Fix value of DigitalGreenDark ([ni/nimble@594ac22](https://github.com/ni/nimble/commit/594ac221124cc1b52cd28113714bb8ff71e5820b))
+
+## 6.13.0
+
+Thu, 21 Mar 2024 17:13:38 GMT
+
+### Minor changes
+
+- Add Body_2 base font token ([ni/nimble@b93385a](https://github.com/ni/nimble/commit/b93385a27672ebc03e18a864e8fa268727595c66))
+
+## 6.12.1
+
+Tue, 12 Mar 2024 21:01:54 GMT
+
+### Patches
+
+- Update typescript to 4.9.5 ([ni/nimble@bc825e1](https://github.com/ni/nimble/commit/bc825e1b057eafd8bc005d11e9a224aa9aee9619))
+
## 6.12.0
Wed, 28 Feb 2024 19:35:04 GMT
diff --git a/packages/nimble-tokens/package.json b/packages/nimble-tokens/package.json
index 7d6d61c119..04efb09bb2 100644
--- a/packages/nimble-tokens/package.json
+++ b/packages/nimble-tokens/package.json
@@ -1,6 +1,6 @@
{
"name": "@ni/nimble-tokens",
- "version": "6.12.0",
+ "version": "6.13.1",
"description": "Design tokens for the NI Nimble Design System",
"scripts": {
"build": "npm run build:svg-to-ts && npm run build:ts && npm run build:svg-to-ico && npm run build:generate-font-scss && npm run build:style-dictionary",
@@ -46,7 +46,7 @@
"style-dictionary": "^3.9.2",
"svg-to-ts": "^12.0.0",
"to-ico": "^1.1.5",
- "typescript": "~4.8.2"
+ "typescript": "~4.9.5"
},
"files": [
"dist/styledictionary/css/**",
diff --git a/packages/nimble-tokens/source/styledictionary/properties/colors.json b/packages/nimble-tokens/source/styledictionary/properties/colors.json
index 812df2c5ad..245808c7d9 100644
--- a/packages/nimble-tokens/source/styledictionary/properties/colors.json
+++ b/packages/nimble-tokens/source/styledictionary/properties/colors.json
@@ -91,7 +91,7 @@
"value": "#AEB0B3ff"
},
"DigitalGreenDark": {
- "value": "#006B46ff"
+ "value": "#008557ff"
},
"DigitalGreenDark105": {
"value": "#00734Bff"
diff --git a/packages/nimble-tokens/source/styledictionary/properties/fonts.json b/packages/nimble-tokens/source/styledictionary/properties/fonts.json
index 4745cd3608..9e43932adf 100644
--- a/packages/nimble-tokens/source/styledictionary/properties/fonts.json
+++ b/packages/nimble-tokens/source/styledictionary/properties/fonts.json
@@ -24,6 +24,14 @@
"value": "Regular"
}
},
+ "Body_2": {
+ "family": {
+ "value": "Source Sans Pro"
+ },
+ "weight": {
+ "value": "Regular"
+ }
+ },
"Header0": {
"family": {
"value": "Noto Serif"
@@ -260,6 +268,9 @@
"Body": {
"value": "14"
},
+ "Body_2": {
+ "value": "16"
+ },
"Header0": {
"value": "36"
},
diff --git a/packages/nimble-tokens/source/styledictionary/properties/sizes.json b/packages/nimble-tokens/source/styledictionary/properties/sizes.json
index 97d1a1fa25..0b6de7129d 100644
--- a/packages/nimble-tokens/source/styledictionary/properties/sizes.json
+++ b/packages/nimble-tokens/source/styledictionary/properties/sizes.json
@@ -3,6 +3,9 @@
"Body-line-height": {
"value": "18"
},
+ "Body_2-line-height": {
+ "value": "16"
+ },
"ControlLabel_1-line-height": {
"value": "16"
},
diff --git a/packages/performance/.eslintrc.js b/packages/performance/.eslintrc.cjs
similarity index 75%
rename from packages/performance/.eslintrc.js
rename to packages/performance/.eslintrc.cjs
index 3e1be2eb59..f7f5374ae5 100644
--- a/packages/performance/.eslintrc.js
+++ b/packages/performance/.eslintrc.cjs
@@ -27,5 +27,11 @@ module.exports = {
// Rules enabled due to strictNullChecks
'@typescript-eslint/no-non-null-assertion': 'off',
}
+ }, {
+ files: ['vite.config.js'],
+ rules: {
+ // Configuration scripts will not be in published package and are allowed to use devDependencies
+ 'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
+ }
}]
};
diff --git a/packages/performance/lighthouserc.js b/packages/performance/lighthouserc.cjs
similarity index 100%
rename from packages/performance/lighthouserc.js
rename to packages/performance/lighthouserc.cjs
diff --git a/packages/performance/package.json b/packages/performance/package.json
index 56c255a00b..a2b5b6af64 100644
--- a/packages/performance/package.json
+++ b/packages/performance/package.json
@@ -2,6 +2,7 @@
"name": "@ni-private/performance",
"version": "1.0.0",
"private": true,
+ "type": "module",
"description": "A package to measure the performance of the Nimble Components",
"scripts": {
"build": "tsc && vite build src",
@@ -15,7 +16,7 @@
},
"devDependencies": {
"@lhci/cli": "^0.13.0",
- "typescript": "~4.8.2",
+ "typescript": "~4.9.5",
"vite": "^5.1.5"
},
"files": [
diff --git a/packages/performance/src/vite.config.js b/packages/performance/src/vite.config.js
index 2a8b74e794..8f5bec56a9 100644
--- a/packages/performance/src/vite.config.js
+++ b/packages/performance/src/vite.config.js
@@ -1,4 +1,3 @@
-// eslint-disable-next-line import/no-extraneous-dependencies
import { defineConfig } from 'vite';
import { resolve } from 'path';
diff --git a/packages/site/package.json b/packages/site/package.json
index 8788e7c94f..9f443eac51 100644
--- a/packages/site/package.json
+++ b/packages/site/package.json
@@ -17,7 +17,7 @@
},
"devDependencies": {
"@11ty/eleventy": "^2.0.1",
- "typescript": "~4.8.2",
+ "typescript": "~4.9.5",
"vite": "^5.1.5"
},
"files": [
diff --git a/packages/xliff-to-json-converter/CHANGELOG.json b/packages/xliff-to-json-converter/CHANGELOG.json
index 7f91727566..005266fb38 100644
--- a/packages/xliff-to-json-converter/CHANGELOG.json
+++ b/packages/xliff-to-json-converter/CHANGELOG.json
@@ -1,6 +1,21 @@
{
"name": "@ni/xliff-to-json-converter",
"entries": [
+ {
+ "date": "Tue, 12 Mar 2024 21:01:54 GMT",
+ "version": "1.1.5",
+ "tag": "@ni/xliff-to-json-converter_v1.1.5",
+ "comments": {
+ "patch": [
+ {
+ "author": "7282195+m-akinc@users.noreply.github.com",
+ "package": "@ni/xliff-to-json-converter",
+ "commit": "bc825e1b057eafd8bc005d11e9a224aa9aee9619",
+ "comment": "Update typescript to 4.9.5"
+ }
+ ]
+ }
+ },
{
"date": "Wed, 06 Mar 2024 17:56:10 GMT",
"version": "1.1.4",
diff --git a/packages/xliff-to-json-converter/CHANGELOG.md b/packages/xliff-to-json-converter/CHANGELOG.md
index cbf7b1de84..6d24ded3ae 100644
--- a/packages/xliff-to-json-converter/CHANGELOG.md
+++ b/packages/xliff-to-json-converter/CHANGELOG.md
@@ -1,9 +1,17 @@
# Change Log - @ni/xliff-to-json-converter
-This log was last generated on Thu, 22 Feb 2024 13:39:47 GMT and should not be manually modified.
+This log was last generated on Tue, 12 Mar 2024 21:01:54 GMT and should not be manually modified.
+## 1.1.5
+
+Tue, 12 Mar 2024 21:01:54 GMT
+
+### Patches
+
+- Update typescript to 4.9.5 ([ni/nimble@bc825e1](https://github.com/ni/nimble/commit/bc825e1b057eafd8bc005d11e9a224aa9aee9619))
+
## 1.1.4
Thu, 22 Feb 2024 13:39:47 GMT
diff --git a/packages/xliff-to-json-converter/package.json b/packages/xliff-to-json-converter/package.json
index 2213e87371..565897f7a8 100644
--- a/packages/xliff-to-json-converter/package.json
+++ b/packages/xliff-to-json-converter/package.json
@@ -1,6 +1,6 @@
{
"name": "@ni/xliff-to-json-converter",
- "version": "1.1.4",
+ "version": "1.1.5",
"description": "A utility to convert translation files from XLIFF to JSON for Angular localization",
"main": "dist/commonjs/cli.js",
"bin": {
@@ -34,7 +34,7 @@
"@types/yargs": "^17.0.10",
"jasmine": "^5.1.0",
"jasmine-core": "^5.1.2",
- "typescript": "~4.8.2"
+ "typescript": "~4.9.5"
},
"dependencies": {
"xliff": "^6.1.0",
diff --git a/specs/templates/custom-component.md b/specs/templates/custom-component.md
index b4655429d8..dee58d9827 100644
--- a/specs/templates/custom-component.md
+++ b/specs/templates/custom-component.md
@@ -109,6 +109,7 @@
- *Components which delegate focus require all global ARIA attributes to be enumerated*
- *Components should either follow an existing [ARIA Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/) or provide thorough research indicating why a new pattern is appropriate. Research should include sources like [Open UI Community Group](https://github.com/openui/open-ui) and other popular design systems.*
- *Behavior with browser configurations like "Prefers reduced motion"*
+- *Support for standard link behaviors if the component is an anchor or contains an anchor. These behaviors are enumerated in the [anchor-patterns story](/packages/nimble-components/src/patterns/anchor/tests/anchor-patterns.mdx). The story should be updated to include the new component.*
### Mobile
diff --git a/specs/templates/fast-based-component.md b/specs/templates/fast-based-component.md
index e1acfeba43..3bd79913da 100644
--- a/specs/templates/fast-based-component.md
+++ b/specs/templates/fast-based-component.md
@@ -57,6 +57,7 @@
- *Documentation: Any requirements besides standard Storybook docs and updating the Example Client App demo?*
- *Tooling: Any new tools, updates to tools, code generation, etc?*
- *Accessibility: keyboard navigation/focus, form input, use with assistive technology, etc.*
+ - *Support for standard link behaviors if the component is an anchor or contains an anchor. These behaviors are enumerated in the [anchor-patterns story](/packages/nimble-components/src/patterns/anchor/tests/anchor-patterns.mdx). The story should be updated to include the new component.*
- *Mobile: small screens, touch interactions, mobile-specific integrations*
- *Globalization: special RTL handling, swapping of icons/visuals, localization, etc.*
- *Performance: does the FAST component meet Nimble's performance requirements?*