diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 5f0e72697b..e5b3210cfd 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -6,6 +6,10 @@ on:
branches:
- '**'
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
env:
GITHUB_SERVICE_USER: "rajsite"
GITHUB_SERVICE_EMAIL: "rajsite@users.noreply.github.com"
diff --git a/.vscode/settings.json b/.vscode/settings.json
index f045c80f8c..8b67d4f680 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -6,7 +6,6 @@
"mode": "auto"
}
],
- "xd.globalEditor": true,
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint",
"omnisharp.useModernNet": true
}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 930ecf0da3..b6570fceeb 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -25,7 +25,7 @@ This repository uses the following tooling. See below for more info.
First step in development is to build the monorepo which requires the following to be installed:
- Node.js version 20+ (run `node --version`) and npm version 10+ (run `npm --version`) which can be downloaded from https://nodejs.org/en/download/
-- .NET 6 SDK (`6.0.202 <= version < 7`) which can be downloaded from https://dotnet.microsoft.com/en-us/download
+- .NET 6 SDK (`6.0.418 <= version < 7`) which can be downloaded from https://dotnet.microsoft.com/en-us/download
- Run `dotnet --info` to verify the required version of the SDK is installed. A `v6` install is required, but it's fine if later versions are installed too.
From the `nimble` directory:
diff --git a/angular-workspace/lighthouserc.js b/angular-workspace/lighthouserc.js
new file mode 100644
index 0000000000..a4f441de3f
--- /dev/null
+++ b/angular-workspace/lighthouserc.js
@@ -0,0 +1,27 @@
+module.exports = {
+ ci: {
+ collect: {
+ // Serve from dist folder so Angular project name is part of url path
+ // and shows up in GitHub status checks
+ staticDistDir: './dist/',
+ url: [
+ 'http://localhost/example-client-app/#/customapp'
+ ],
+ numberOfRuns: 3,
+ settings: {
+ preset: 'desktop',
+ // Omit the pwa category
+ onlyCategories: ['accessibility', 'best-practices', 'performance', 'seo']
+ }
+ },
+ assert: {
+ assertions: {
+ 'categories:performance': ['error', { minScore: 0.9 }],
+ 'categories:accessibility': ['error', { minScore: 0.8 }]
+ }
+ },
+ upload: {
+ target: 'temporary-public-storage',
+ },
+ },
+};
diff --git a/angular-workspace/package.json b/angular-workspace/package.json
index efb8dbdcfd..5b7b55a82a 100644
--- a/angular-workspace/package.json
+++ b/angular-workspace/package.json
@@ -15,6 +15,7 @@
"pack": "npm run pack:library && npm run pack:application",
"pack:library": "cd dist/ni/nimble-angular && npm pack",
"pack:application": "cd dist/example-client-app && npm pack",
+ "performance": "lhci autorun",
"watch": "ng build --watch --configuration development",
"test": "ng test --watch=false",
"lint": "ng lint",
@@ -39,6 +40,7 @@
"@angular/cli": "^15.2.10",
"@angular/compiler-cli": "^15.2.10",
"@angular/localize": "^15.2.10",
+ "@lhci/cli": "^0.13.0",
"@microsoft/fast-web-utilities": "^6.0.0",
"@ni/eslint-config-angular": "^6.0.0",
"@ni/eslint-config-javascript": "^4.2.0",
diff --git a/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.html b/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.html
index a5efee841e..6d2778ae65 100644
--- a/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.html
+++ b/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.html
@@ -42,6 +42,7 @@
Block Anchor Button
Ghost Anchor Button
+
Card Button
@@ -150,7 +152,7 @@
Select
-
+
Option 1
Option 2
Option 3
@@ -288,6 +290,28 @@
Add rows
+
+
Table with delayed hierarchy
+
+
+ First name
+
+
+ Last name
+
+
+ Age
+
+
+
Tabs
diff --git a/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.ts b/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.ts
index 1c90271b58..ce06158d7d 100644
--- a/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.ts
+++ b/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.ts
@@ -1,8 +1,8 @@
/* eslint-disable no-alert */
-import { Component, Inject, ViewChild } from '@angular/core';
+import { AfterViewInit, Component, Inject, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { DrawerLocation, MenuItem, NimbleDialogDirective, NimbleDrawerDirective, OptionNotFound, OPTION_NOT_FOUND, UserDismissed } from '@ni/nimble-angular';
-import type { TableRecord } from '@ni/nimble-angular/table';
+import { NimbleTableDirective, TableRecordDelayedHierarchyState, TableRecord, TableRowExpansionToggleEventDetail, TableSetRecordHierarchyOptions } from '@ni/nimble-angular/table';
import { NimbleRichTextEditorDirective } from '@ni/nimble-angular/rich-text/editor';
import { BehaviorSubject, Observable } from 'rxjs';
@@ -25,12 +25,21 @@ interface SimpleTableRecord extends TableRecord {
duration: number;
}
+interface PersonTableRecord extends TableRecord {
+ id: string;
+ parentId?: string;
+ firstName: string;
+ lastName: string;
+ age: number;
+ hasChildren?: boolean;
+}
+
@Component({
selector: 'example-customapp',
templateUrl: './customapp.component.html',
styleUrls: ['./customapp.component.scss']
})
-export class CustomAppComponent {
+export class CustomAppComponent implements AfterViewInit {
public bannerOpen = false;
public dialogCloseReason: string;
public drawerCloseReason: string;
@@ -63,10 +72,40 @@ export class CustomAppComponent {
public readonly tableData$: Observable;
private readonly tableDataSubject = new BehaviorSubject([]);
+ private delayedHierarchyTableData: PersonTableRecord[] = [
+ {
+ firstName: 'Jacqueline',
+ lastName: 'Bouvier',
+ age: 80,
+ id: 'jacqueline-bouvier',
+ parentId: undefined,
+ hasChildren: true
+ },
+ {
+ firstName: 'Mona',
+ lastName: 'Simpson',
+ age: 77,
+ id: 'mona-simpson',
+ parentId: undefined,
+ hasChildren: true
+ },
+ {
+ firstName: 'Agnes',
+ lastName: 'Skinner',
+ age: 88,
+ id: 'agnes-skinner',
+ parentId: undefined,
+ hasChildren: true
+ },
+ ];
+
+ private readonly recordsLoadingChildren = new Set();
+ private readonly recordsWithLoadedChildren = new Set();
@ViewChild('dialog', { read: NimbleDialogDirective }) private readonly dialog: NimbleDialogDirective;
@ViewChild('drawer', { read: NimbleDrawerDirective }) private readonly drawer: NimbleDrawerDirective;
@ViewChild('editor', { read: NimbleRichTextEditorDirective }) private readonly editor: NimbleRichTextEditorDirective;
+ @ViewChild('delayedHierarchyTable', { read: NimbleTableDirective }) private readonly delayedHierarchyTable: NimbleTableDirective;
public constructor(@Inject(ActivatedRoute) public readonly route: ActivatedRoute) {
this.tableData$ = this.tableDataSubject.asObservable();
@@ -81,6 +120,10 @@ export class CustomAppComponent {
}
}
+ public ngAfterViewInit(): void {
+ void this.updateDelayedHierarchyTable();
+ }
+
public onMenuButtonMenuChange(event: Event): void {
const menuItemText = (event.target as MenuItem).innerText;
alert(`${menuItemText} selected`);
@@ -139,4 +182,110 @@ export class CustomAppComponent {
public loadRichTextEditorContent(): void {
this.editor.setMarkdown(this.markdownString);
}
+
+ public onRowExpandToggle(e: Event): void {
+ const event = e as CustomEvent;
+ const recordId = event.detail.recordId;
+ if (event.detail.newState && !this.recordsLoadingChildren.has(recordId) && !this.recordsWithLoadedChildren.has(recordId)) {
+ const record = this.delayedHierarchyTableData.find(x => x.id === recordId && x.hasChildren);
+ if (!record) {
+ return;
+ }
+
+ this.recordsLoadingChildren.add(recordId);
+ void this.updateDelayedHierarchyTable(false);
+
+ window.setTimeout(() => {
+ this.delayedHierarchyTableData = [
+ ...this.delayedHierarchyTableData,
+ ...this.getChildren(recordId)
+ ];
+ this.recordsLoadingChildren.delete(recordId);
+ this.recordsWithLoadedChildren.add(recordId);
+ void this.updateDelayedHierarchyTable();
+ }, 1500);
+ }
+ }
+
+ private async updateDelayedHierarchyTable(setData = true): Promise {
+ if (setData) {
+ await this.delayedHierarchyTable.setData(this.delayedHierarchyTableData);
+ }
+ const hierarchyOptions = this.delayedHierarchyTableData.filter(person => {
+ return person.hasChildren && !this.recordsWithLoadedChildren.has(person.id);
+ }).map(person => {
+ const state = this.recordsLoadingChildren.has(person.id)
+ ? TableRecordDelayedHierarchyState.loadingChildren
+ : TableRecordDelayedHierarchyState.canLoadChildren;
+ return {
+ recordId: person.id,
+ options: { delayedHierarchyState: state }
+ };
+ });
+ await this.delayedHierarchyTable.setRecordHierarchyOptions(hierarchyOptions);
+ }
+
+ private getChildren(id: string): PersonTableRecord[] {
+ switch (id) {
+ case 'jacqueline-bouvier':
+ return [{
+ firstName: 'Marge',
+ lastName: 'Simpson',
+ age: 35,
+ id: 'marge-simpson',
+ parentId: id,
+ hasChildren: true
+ }, {
+ firstName: 'Selma',
+ lastName: 'Bouvier',
+ age: 45,
+ id: 'selma-bouvier',
+ parentId: id
+ }, {
+ firstName: 'Patty',
+ lastName: 'Bouvier',
+ age: 45,
+ id: 'patty-bouvier',
+ parentId: id
+ }];
+ case 'marge-simpson':
+ return [{
+ firstName: 'Bart',
+ lastName: 'Simpson',
+ age: 12,
+ id: 'bart-simpson',
+ parentId: id
+ }, {
+ firstName: 'Lisa',
+ lastName: 'Simpson',
+ age: 10,
+ id: 'lisa-simpson',
+ parentId: id
+ }, {
+ firstName: 'Maggie',
+ lastName: 'Simpson',
+ age: 1,
+ id: 'maggie-simpson',
+ parentId: id
+ }];
+ case 'mona-simpson':
+ return [{
+ firstName: 'Homer',
+ lastName: 'Simpson',
+ age: 35,
+ id: 'homer-simpson',
+ parentId: id
+ }];
+ case 'agnes-skinner':
+ return [{
+ firstName: 'Seymour',
+ lastName: 'Skinner',
+ age: 42,
+ id: 'seymour-skinner',
+ parentId: id
+ }];
+ default:
+ return [];
+ }
+ }
}
diff --git a/angular-workspace/projects/example-client-app/src/index.html b/angular-workspace/projects/example-client-app/src/index.html
index e0ef279eaf..ee4a6df4e0 100644
--- a/angular-workspace/projects/example-client-app/src/index.html
+++ b/angular-workspace/projects/example-client-app/src/index.html
@@ -4,6 +4,7 @@
Angular All Components Demo - Nimble Design System - NI
+
diff --git a/angular-workspace/projects/ni/nimble-angular/CHANGELOG.json b/angular-workspace/projects/ni/nimble-angular/CHANGELOG.json
index 05e1b00f57..05fae94cea 100644
--- a/angular-workspace/projects/ni/nimble-angular/CHANGELOG.json
+++ b/angular-workspace/projects/ni/nimble-angular/CHANGELOG.json
@@ -1,6 +1,312 @@
{
"name": "@ni/nimble-angular",
"entries": [
+ {
+ "date": "Thu, 22 Feb 2024 19:42:35 GMT",
+ "version": "20.2.6",
+ "tag": "@ni/nimble-angular_v20.2.6",
+ "comments": {
+ "patch": [
+ {
+ "author": "beachball",
+ "package": "@ni/nimble-angular",
+ "comment": "Bump @ni/nimble-components to v21.6.3",
+ "commit": "not available"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Thu, 22 Feb 2024 01:13:03 GMT",
+ "version": "20.2.5",
+ "tag": "@ni/nimble-angular_v20.2.5",
+ "comments": {
+ "patch": [
+ {
+ "author": "beachball",
+ "package": "@ni/nimble-angular",
+ "comment": "Bump @ni/nimble-components to v21.6.2",
+ "commit": "not available"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Wed, 21 Feb 2024 23:32:45 GMT",
+ "version": "20.2.4",
+ "tag": "@ni/nimble-angular_v20.2.4",
+ "comments": {
+ "patch": [
+ {
+ "author": "beachball",
+ "package": "@ni/nimble-angular",
+ "comment": "Bump @ni/nimble-components to v21.6.1",
+ "commit": "not available"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Wed, 21 Feb 2024 21:18:20 GMT",
+ "version": "20.2.3",
+ "tag": "@ni/nimble-angular_v20.2.3",
+ "comments": {
+ "patch": [
+ {
+ "author": "beachball",
+ "package": "@ni/nimble-angular",
+ "comment": "Bump @ni/nimble-components to v21.6.0",
+ "commit": "not available"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Wed, 21 Feb 2024 19:48:20 GMT",
+ "version": "20.2.2",
+ "tag": "@ni/nimble-angular_v20.2.2",
+ "comments": {
+ "patch": [
+ {
+ "author": "beachball",
+ "package": "@ni/nimble-angular",
+ "comment": "Bump @ni/nimble-components to v21.5.5",
+ "commit": "not available"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Tue, 20 Feb 2024 17:54:12 GMT",
+ "version": "20.2.1",
+ "tag": "@ni/nimble-angular_v20.2.1",
+ "comments": {
+ "patch": [
+ {
+ "author": "beachball",
+ "package": "@ni/nimble-angular",
+ "comment": "Bump @ni/nimble-components to v21.5.4",
+ "commit": "not available"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Mon, 19 Feb 2024 23:03:42 GMT",
+ "version": "20.2.0",
+ "tag": "@ni/nimble-angular_v20.2.0",
+ "comments": {
+ "minor": [
+ {
+ "author": "20542556+mollykreis@users.noreply.github.com",
+ "package": "@ni/nimble-angular",
+ "commit": "8fc186e3c759eac9d69ba01802b07dfb9e51efbd",
+ "comment": "Add support for delayed hierarchy in the table"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Mon, 19 Feb 2024 22:25:59 GMT",
+ "version": "20.1.2",
+ "tag": "@ni/nimble-angular_v20.1.2",
+ "comments": {
+ "patch": [
+ {
+ "author": "beachball",
+ "package": "@ni/nimble-angular",
+ "comment": "Bump @ni/nimble-components to v21.5.3",
+ "commit": "not available"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Mon, 19 Feb 2024 19:14:52 GMT",
+ "version": "20.1.1",
+ "tag": "@ni/nimble-angular_v20.1.1",
+ "comments": {
+ "patch": [
+ {
+ "author": "beachball",
+ "package": "@ni/nimble-angular",
+ "comment": "Bump @ni/nimble-components to v21.5.2",
+ "commit": "not available"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Mon, 19 Feb 2024 16:59:58 GMT",
+ "version": "20.1.0",
+ "tag": "@ni/nimble-angular_v20.1.0",
+ "comments": {
+ "minor": [
+ {
+ "author": "26874831+atmgrifter00@users.noreply.github.com",
+ "package": "@ni/nimble-angular",
+ "commit": "315917c5cf3768d0dcaf3901eef0087c5299832e",
+ "comment": "Angular filterable Select"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Fri, 16 Feb 2024 18:10:59 GMT",
+ "version": "20.0.10",
+ "tag": "@ni/nimble-angular_v20.0.10",
+ "comments": {
+ "patch": [
+ {
+ "author": "7282195+m-akinc@users.noreply.github.com",
+ "package": "@ni/nimble-angular",
+ "commit": "2b6c327fd3bc4f49178ac370f695a8b941278c46",
+ "comment": "Inline sources into map files"
+ },
+ {
+ "author": "beachball",
+ "package": "@ni/nimble-angular",
+ "comment": "Bump @ni/nimble-components to v21.5.1",
+ "commit": "not available"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Thu, 15 Feb 2024 20:48:52 GMT",
+ "version": "20.0.9",
+ "tag": "@ni/nimble-angular_v20.0.9",
+ "comments": {
+ "patch": [
+ {
+ "author": "beachball",
+ "package": "@ni/nimble-angular",
+ "comment": "Bump @ni/nimble-components to v21.5.0",
+ "commit": "not available"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Thu, 15 Feb 2024 01:01:57 GMT",
+ "version": "20.0.8",
+ "tag": "@ni/nimble-angular_v20.0.8",
+ "comments": {
+ "none": [
+ {
+ "author": "7282195+m-akinc@users.noreply.github.com",
+ "package": "@ni/nimble-angular",
+ "commit": "19c97b51b82b32b95517a914d43a028860f1279a",
+ "comment": "Add publishConfig key to package files"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Wed, 14 Feb 2024 21:03:17 GMT",
+ "version": "20.0.8",
+ "tag": "@ni/nimble-angular_v20.0.8",
+ "comments": {
+ "patch": [
+ {
+ "author": "beachball",
+ "package": "@ni/nimble-angular",
+ "comment": "Bump @ni/nimble-components to v21.4.0",
+ "commit": "not available"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Tue, 13 Feb 2024 21:22:58 GMT",
+ "version": "20.0.7",
+ "tag": "@ni/nimble-angular_v20.0.7",
+ "comments": {
+ "patch": [
+ {
+ "author": "beachball",
+ "package": "@ni/nimble-angular",
+ "comment": "Bump @ni/nimble-components to v21.3.3",
+ "commit": "not available"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Tue, 13 Feb 2024 17:44:00 GMT",
+ "version": "20.0.6",
+ "tag": "@ni/nimble-angular_v20.0.6",
+ "comments": {
+ "patch": [
+ {
+ "author": "beachball",
+ "package": "@ni/nimble-angular",
+ "comment": "Bump @ni/nimble-components to v21.3.2",
+ "commit": "not available"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Fri, 09 Feb 2024 02:02:53 GMT",
+ "version": "20.0.4",
+ "tag": "@ni/nimble-angular_v20.0.4",
+ "comments": {
+ "patch": [
+ {
+ "author": "beachball",
+ "package": "@ni/nimble-angular",
+ "comment": "Bump @ni/nimble-components to v21.3.1",
+ "commit": "not available"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Wed, 07 Feb 2024 21:48:45 GMT",
+ "version": "20.0.3",
+ "tag": "@ni/nimble-angular_v20.0.3",
+ "comments": {
+ "patch": [
+ {
+ "author": "beachball",
+ "package": "@ni/nimble-angular",
+ "comment": "Bump @ni/nimble-components to v21.3.0",
+ "commit": "not available"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Wed, 07 Feb 2024 19:06:22 GMT",
+ "version": "20.0.2",
+ "tag": "@ni/nimble-angular_v20.0.2",
+ "comments": {
+ "patch": [
+ {
+ "author": "beachball",
+ "package": "@ni/nimble-angular",
+ "comment": "Bump @ni/nimble-components to v21.2.1",
+ "commit": "not available"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Fri, 02 Feb 2024 21:51:03 GMT",
+ "version": "20.0.1",
+ "tag": "@ni/nimble-angular_v20.0.1",
+ "comments": {
+ "patch": [
+ {
+ "author": "beachball",
+ "package": "@ni/nimble-angular",
+ "comment": "Bump @ni/nimble-components to v21.2.0",
+ "commit": "not available"
+ }
+ ]
+ }
+ },
{
"date": "Wed, 31 Jan 2024 20:22:58 GMT",
"version": "20.0.0",
diff --git a/angular-workspace/projects/ni/nimble-angular/CHANGELOG.md b/angular-workspace/projects/ni/nimble-angular/CHANGELOG.md
index faa304e685..8b56060dc1 100644
--- a/angular-workspace/projects/ni/nimble-angular/CHANGELOG.md
+++ b/angular-workspace/projects/ni/nimble-angular/CHANGELOG.md
@@ -1,9 +1,162 @@
# Change Log - @ni/nimble-angular
-This log was last generated on Wed, 31 Jan 2024 20:22:58 GMT and should not be manually modified.
+This log was last generated on Thu, 22 Feb 2024 19:42:35 GMT and should not be manually modified.
+## 20.2.6
+
+Thu, 22 Feb 2024 19:42:35 GMT
+
+### Patches
+
+- Bump @ni/nimble-components to v21.6.3
+
+## 20.2.5
+
+Thu, 22 Feb 2024 01:13:03 GMT
+
+### Patches
+
+- Bump @ni/nimble-components to v21.6.2
+
+## 20.2.4
+
+Wed, 21 Feb 2024 23:32:45 GMT
+
+### Patches
+
+- Bump @ni/nimble-components to v21.6.1
+
+## 20.2.3
+
+Wed, 21 Feb 2024 21:18:20 GMT
+
+### Patches
+
+- Bump @ni/nimble-components to v21.6.0
+
+## 20.2.2
+
+Wed, 21 Feb 2024 19:48:20 GMT
+
+### Patches
+
+- Bump @ni/nimble-components to v21.5.5
+
+## 20.2.1
+
+Tue, 20 Feb 2024 17:54:12 GMT
+
+### Patches
+
+- Bump @ni/nimble-components to v21.5.4
+
+## 20.2.0
+
+Mon, 19 Feb 2024 23:03:42 GMT
+
+### Minor changes
+
+- Add support for delayed hierarchy in the table ([ni/nimble@8fc186e](https://github.com/ni/nimble/commit/8fc186e3c759eac9d69ba01802b07dfb9e51efbd))
+
+## 20.1.2
+
+Mon, 19 Feb 2024 22:25:59 GMT
+
+### Patches
+
+- Bump @ni/nimble-components to v21.5.3
+
+## 20.1.1
+
+Mon, 19 Feb 2024 19:14:52 GMT
+
+### Patches
+
+- Bump @ni/nimble-components to v21.5.2
+
+## 20.1.0
+
+Mon, 19 Feb 2024 16:59:58 GMT
+
+### Minor changes
+
+- Angular filterable Select ([ni/nimble@315917c](https://github.com/ni/nimble/commit/315917c5cf3768d0dcaf3901eef0087c5299832e))
+
+## 20.0.10
+
+Fri, 16 Feb 2024 18:10:59 GMT
+
+### Patches
+
+- Inline sources into map files ([ni/nimble@2b6c327](https://github.com/ni/nimble/commit/2b6c327fd3bc4f49178ac370f695a8b941278c46))
+- Bump @ni/nimble-components to v21.5.1
+
+## 20.0.9
+
+Thu, 15 Feb 2024 20:48:52 GMT
+
+### Patches
+
+- Bump @ni/nimble-components to v21.5.0
+
+## 20.0.8
+
+Wed, 14 Feb 2024 21:03:17 GMT
+
+### Patches
+
+- Bump @ni/nimble-components to v21.4.0
+
+## 20.0.7
+
+Tue, 13 Feb 2024 21:22:58 GMT
+
+### Patches
+
+- Bump @ni/nimble-components to v21.3.3
+
+## 20.0.6
+
+Tue, 13 Feb 2024 17:44:00 GMT
+
+### Patches
+
+- Bump @ni/nimble-components to v21.3.2
+
+## 20.0.4
+
+Fri, 09 Feb 2024 02:02:53 GMT
+
+### Patches
+
+- Bump @ni/nimble-components to v21.3.1
+
+## 20.0.3
+
+Wed, 07 Feb 2024 21:48:45 GMT
+
+### Patches
+
+- Bump @ni/nimble-components to v21.3.0
+
+## 20.0.2
+
+Wed, 07 Feb 2024 19:06:22 GMT
+
+### Patches
+
+- Bump @ni/nimble-components to v21.2.1
+
+## 20.0.1
+
+Fri, 02 Feb 2024 21:51:03 GMT
+
+### Patches
+
+- Bump @ni/nimble-components to v21.2.0
+
## 20.0.0
Wed, 31 Jan 2024 20:22:58 GMT
diff --git a/angular-workspace/projects/ni/nimble-angular/label-provider/table/nimble-label-provider-table-with-defaults.directive.ts b/angular-workspace/projects/ni/nimble-angular/label-provider/table/nimble-label-provider-table-with-defaults.directive.ts
index d50ff13964..cbee2fcd95 100644
--- a/angular-workspace/projects/ni/nimble-angular/label-provider/table/nimble-label-provider-table-with-defaults.directive.ts
+++ b/angular-workspace/projects/ni/nimble-angular/label-provider/table/nimble-label-provider-table-with-defaults.directive.ts
@@ -23,5 +23,6 @@ export class NimbleLabelProviderTableWithDefaultsDirective {
this.elementRef.nativeElement.groupSelectAll = $localize`:Nimble table - select all rows in group|:Select all rows in group`;
this.elementRef.nativeElement.rowSelect = $localize`:Nimble table - select row|:Select row`;
this.elementRef.nativeElement.rowOperationColumn = $localize`:Nimble table - row operation column|:Row operations`;
+ this.elementRef.nativeElement.rowLoading = $localize`:Nimble table - row loading|:Loading`;
}
}
\ No newline at end of file
diff --git a/angular-workspace/projects/ni/nimble-angular/label-provider/table/nimble-label-provider-table.directive.ts b/angular-workspace/projects/ni/nimble-angular/label-provider/table/nimble-label-provider-table.directive.ts
index 58ca689455..7f68fe9cb5 100644
--- a/angular-workspace/projects/ni/nimble-angular/label-provider/table/nimble-label-provider-table.directive.ts
+++ b/angular-workspace/projects/ni/nimble-angular/label-provider/table/nimble-label-provider-table.directive.ts
@@ -103,4 +103,12 @@ export class NimbleLabelProviderTableDirective {
@Input('row-operation-column') public set rowOperationColumn(value: string | undefined) {
this.renderer.setProperty(this.elementRef.nativeElement, 'rowOperationColumn', value);
}
+
+ public get rowLoading(): string | undefined {
+ return this.elementRef.nativeElement.rowLoading;
+ }
+
+ @Input('row-loading') public set rowLoading(value: string | undefined) {
+ this.renderer.setProperty(this.elementRef.nativeElement, 'rowLoading', value);
+ }
}
\ No newline at end of file
diff --git a/angular-workspace/projects/ni/nimble-angular/label-provider/table/tests/nimble-label-provider-table-with-defaults.directive.spec.ts b/angular-workspace/projects/ni/nimble-angular/label-provider/table/tests/nimble-label-provider-table-with-defaults.directive.spec.ts
index 20ccfb23fc..6f62cb10b9 100644
--- a/angular-workspace/projects/ni/nimble-angular/label-provider/table/tests/nimble-label-provider-table-with-defaults.directive.spec.ts
+++ b/angular-workspace/projects/ni/nimble-angular/label-provider/table/tests/nimble-label-provider-table-with-defaults.directive.spec.ts
@@ -39,6 +39,7 @@ describe('Nimble LabelProviderTable withDefaults directive', () => {
[computeMsgId('Select all rows in group', 'Nimble table - select all rows in group')]: 'Translated select all rows in group',
[computeMsgId('Select row', 'Nimble table - select row')]: 'Translated select row',
[computeMsgId('Row operations', 'Nimble table - row operation column')]: 'Translated row operations',
+ [computeMsgId('Loading', 'Nimble table - row loading')]: 'Translated loading',
});
const fixture = TestBed.createComponent(TestHostComponent);
const testHostComponent = fixture.componentInstance;
@@ -58,5 +59,6 @@ describe('Nimble LabelProviderTable withDefaults directive', () => {
expect(labelProvider.groupSelectAll).toBe('Translated select all rows in group');
expect(labelProvider.rowSelect).toBe('Translated select row');
expect(labelProvider.rowOperationColumn).toBe('Translated row operations');
+ expect(labelProvider.rowLoading).toBe('Translated loading');
});
});
diff --git a/angular-workspace/projects/ni/nimble-angular/label-provider/table/tests/nimble-label-provider-table.directive.spec.ts b/angular-workspace/projects/ni/nimble-angular/label-provider/table/tests/nimble-label-provider-table.directive.spec.ts
index b68edb8944..87f25ad84c 100644
--- a/angular-workspace/projects/ni/nimble-angular/label-provider/table/tests/nimble-label-provider-table.directive.spec.ts
+++ b/angular-workspace/projects/ni/nimble-angular/label-provider/table/tests/nimble-label-provider-table.directive.spec.ts
@@ -15,6 +15,7 @@ describe('Nimble Label Provider Table', () => {
const label9 = 'String 9';
const label10 = 'String 10';
const label11 = 'String 11';
+ const label12 = 'String 12';
beforeEach(() => {
TestBed.configureTestingModule({
@@ -106,6 +107,11 @@ describe('Nimble Label Provider Table', () => {
expect(directive.rowOperationColumn).toBeUndefined();
expect(nativeElement.rowOperationColumn).toBeUndefined();
});
+
+ it('has expected defaults for rowLoading', () => {
+ expect(directive.rowLoading).toBeUndefined();
+ expect(nativeElement.rowLoading).toBeUndefined();
+ });
});
describe('with template string values', () => {
@@ -123,6 +129,7 @@ describe('Nimble Label Provider Table', () => {
group-select-all="${label9}"
row-select="${label10}"
row-operation-column="${label11}"
+ row-loading="${label12}"
>
`
@@ -201,6 +208,11 @@ describe('Nimble Label Provider Table', () => {
expect(directive.rowOperationColumn).toBe(label11);
expect(nativeElement.rowOperationColumn).toBe(label11);
});
+
+ it('will use template string values for rowLoading', () => {
+ expect(directive.rowLoading).toBe(label12);
+ expect(nativeElement.rowLoading).toBe(label12);
+ });
});
describe('with property bound values', () => {
@@ -218,6 +230,7 @@ describe('Nimble Label Provider Table', () => {
[groupSelectAll]="groupSelectAll"
[rowSelect]="rowSelect"
[rowOperationColumn]="rowOperationColumn"
+ [rowLoading]="rowLoading"
>
`
@@ -236,6 +249,7 @@ describe('Nimble Label Provider Table', () => {
public groupSelectAll = label1;
public rowSelect = label1;
public rowOperationColumn = label1;
+ public rowLoading = label1;
}
let fixture: ComponentFixture;
@@ -373,6 +387,17 @@ describe('Nimble Label Provider Table', () => {
expect(directive.rowOperationColumn).toBe(label2);
expect(nativeElement.rowOperationColumn).toBe(label2);
});
+
+ it('can be configured with property binding for rowLoading', () => {
+ expect(directive.rowLoading).toBe(label1);
+ expect(nativeElement.rowLoading).toBe(label1);
+
+ fixture.componentInstance.rowLoading = label2;
+ fixture.detectChanges();
+
+ expect(directive.rowLoading).toBe(label2);
+ expect(nativeElement.rowLoading).toBe(label2);
+ });
});
describe('with attribute bound values', () => {
@@ -390,6 +415,7 @@ describe('Nimble Label Provider Table', () => {
[attr.group-select-all]="groupSelectAll"
[attr.row-select]="rowSelect"
[attr.row-operation-column]="rowOperationColumn"
+ [attr.row-loading]="rowLoading"
>
`
@@ -408,6 +434,7 @@ describe('Nimble Label Provider Table', () => {
public groupSelectAll = label1;
public rowSelect = label1;
public rowOperationColumn = label1;
+ public rowLoading = label1;
}
let fixture: ComponentFixture;
@@ -545,5 +572,16 @@ describe('Nimble Label Provider Table', () => {
expect(directive.rowOperationColumn).toBe(label2);
expect(nativeElement.rowOperationColumn).toBe(label2);
});
+
+ it('can be configured with attribute binding for rowLoading', () => {
+ expect(directive.rowLoading).toBe(label1);
+ expect(nativeElement.rowLoading).toBe(label1);
+
+ fixture.componentInstance.rowLoading = label2;
+ fixture.detectChanges();
+
+ expect(directive.rowLoading).toBe(label2);
+ expect(nativeElement.rowLoading).toBe(label2);
+ });
});
});
diff --git a/angular-workspace/projects/ni/nimble-angular/package.json b/angular-workspace/projects/ni/nimble-angular/package.json
index 94337ebc36..8c3ccce4a9 100644
--- a/angular-workspace/projects/ni/nimble-angular/package.json
+++ b/angular-workspace/projects/ni/nimble-angular/package.json
@@ -1,6 +1,6 @@
{
"name": "@ni/nimble-angular",
- "version": "20.0.0",
+ "version": "20.2.6",
"description": "Angular components for the NI Nimble Design System",
"scripts": {
"invoke-publish": "cd ../../../ && npm run build:library && cd dist/ni/nimble-angular && npm publish"
@@ -9,6 +9,9 @@
"type": "git",
"url": "git+https://github.com/ni/nimble.git"
},
+ "publishConfig": {
+ "access": "public"
+ },
"author": {
"name": "National Instruments"
},
@@ -28,7 +31,7 @@
"@angular/forms": "^15.2.10",
"@angular/localize": "^15.2.10",
"@angular/router": "^15.2.10",
- "@ni/nimble-components": "^21.1.0"
+ "@ni/nimble-components": "^21.6.3"
},
"dependencies": {
"tslib": "^2.2.0"
diff --git a/angular-workspace/projects/ni/nimble-angular/src/directives/select/nimble-select.directive.ts b/angular-workspace/projects/ni/nimble-angular/src/directives/select/nimble-select.directive.ts
index 44b0f8452f..ef01f25446 100644
--- a/angular-workspace/projects/ni/nimble-angular/src/directives/select/nimble-select.directive.ts
+++ b/angular-workspace/projects/ni/nimble-angular/src/directives/select/nimble-select.directive.ts
@@ -1,9 +1,11 @@
import { Directive, ElementRef, Input, Renderer2 } from '@angular/core';
import { type Select, selectTag } from '@ni/nimble-components/dist/esm/select';
+import { FilterMode } from '@ni/nimble-components/dist/esm/select/types';
import type { DropdownAppearance } from '@ni/nimble-components/dist/esm/patterns/dropdown/types';
import { BooleanValueOrAttribute, toBooleanProperty } from '@ni/nimble-angular/internal-utilities';
export type { Select };
+export { FilterMode };
export { selectTag };
/**
@@ -21,6 +23,14 @@ export class NimbleSelectDirective {
this.renderer.setProperty(this.elementRef.nativeElement, 'appearance', value);
}
+ public get filterMode(): FilterMode {
+ return this.elementRef.nativeElement.filterMode;
+ }
+
+ @Input('filter-mode') public set filterMode(value: FilterMode) {
+ this.renderer.setProperty(this.elementRef.nativeElement, 'filterMode', value);
+ }
+
public get disabled(): boolean {
return this.elementRef.nativeElement.disabled;
}
diff --git a/angular-workspace/projects/ni/nimble-angular/src/directives/select/tests/nimble-select.directive.spec.ts b/angular-workspace/projects/ni/nimble-angular/src/directives/select/tests/nimble-select.directive.spec.ts
index 3411303d8b..ab439f1d9e 100644
--- a/angular-workspace/projects/ni/nimble-angular/src/directives/select/tests/nimble-select.directive.spec.ts
+++ b/angular-workspace/projects/ni/nimble-angular/src/directives/select/tests/nimble-select.directive.spec.ts
@@ -2,7 +2,7 @@ import { Component, ElementRef, ViewChild } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import type { BooleanValueOrAttribute } from '@ni/nimble-angular/internal-utilities';
import { DropdownAppearance } from '../../../public-api';
-import { NimbleSelectDirective, Select } from '../nimble-select.directive';
+import { FilterMode, NimbleSelectDirective, Select } from '../nimble-select.directive';
import { NimbleSelectModule } from '../nimble-select.module';
describe('Nimble select', () => {
@@ -52,6 +52,11 @@ describe('Nimble select', () => {
expect(nativeElement.appearance).toBe(DropdownAppearance.underline);
});
+ it('has expected defaults for filterMode', () => {
+ expect(directive.filterMode).toBe(FilterMode.none);
+ expect(nativeElement.filterMode).toBe(FilterMode.none);
+ });
+
it('has expected defaults for errorText', () => {
expect(directive.errorText).toBeUndefined();
expect(nativeElement.errorText).toBeUndefined();
@@ -80,6 +85,7 @@ describe('Nimble select', () => {
@@ -116,6 +122,11 @@ describe('Nimble select', () => {
expect(nativeElement.appearance).toBe(DropdownAppearance.block);
});
+ it('will use template string values for filterMode', () => {
+ expect(directive.filterMode).toBe(FilterMode.standard);
+ expect(nativeElement.filterMode).toBe(FilterMode.standard);
+ });
+
it('will use template string values for errorText', () => {
expect(directive.errorText).toBe('error text');
expect(nativeElement.errorText).toBe('error text');
@@ -133,6 +144,7 @@ describe('Nimble select', () => {
@@ -144,6 +156,7 @@ describe('Nimble select', () => {
@ViewChild('select', { read: ElementRef }) public elementRef: ElementRef
+
Card Button
@@ -170,7 +172,7 @@
Select
-
+
Option 1
Option 2
Option 3
@@ -219,7 +221,7 @@
Table
-
+
AddTableRows(10))>Add rows
+
+
Table with delayed hierarchy
+
+ First name
+ Last name
+ Age
+
+
Tabs
@@ -358,5 +368,21 @@
+
+
Wafer Map
+
+
+
AddDiesToRadius(10))>Increase Die Count
+
RemoveDiesFromRadius(10))>Decrease Die Count
+
diff --git a/packages/nimble-blazor/Examples/Demo.Shared/Pages/ComponentsDemo.razor.cs b/packages/nimble-blazor/Examples/Demo.Shared/Pages/ComponentsDemo.razor.cs
index 35346469fc..3645f45127 100644
--- a/packages/nimble-blazor/Examples/Demo.Shared/Pages/ComponentsDemo.razor.cs
+++ b/packages/nimble-blazor/Examples/Demo.Shared/Pages/ComponentsDemo.razor.cs
@@ -15,16 +15,124 @@ public partial class ComponentsDemo
private NimbleDialog? _dialog;
private string? DialogClosedReason { get; set; }
private NimbleDrawer? _drawer;
+ private NimbleTable? _table;
+ private NimbleTable? _delayedHierarchyTable;
private string? DrawerClosedReason { get; set; }
private string? SelectedRadio { get; set; } = "2";
private bool BannerOpen { get; set; }
+ private List _delayedHierarchyTableData = new List()
+ {
+ new PersonTableRecord("jacqueline-bouvier", null, "Jacqueline", "Bouvier", 80, true),
+ new PersonTableRecord("mona-simpson", null, "Mona", "Simpson", 77, true),
+ new PersonTableRecord("agnes-skinner", null, "Agnes", "Skinner", 88, true)
+ };
+ private HashSet _recordsLoadingChildren = new HashSet();
+ private HashSet _recordsWithLoadedChildren = new HashSet();
[NotNull]
public IEnumerable TableData { get; set; } = Enumerable.Empty();
+ [NotNull]
+ public IEnumerable Dies { get; set; } = Enumerable.Empty();
+ [NotNull]
+ public IEnumerable HighlightedTags { get; set; } = Enumerable.Empty();
+ [NotNull]
+ public WaferMapColorScale ColorScale { get; set; } = new WaferMapColorScale(new List { "red", "green" }, new List { "0", "100" });
public ComponentsDemo()
{
AddTableRows(10);
+ UpdateDies(5);
+ }
+
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ await _table!.SetDataAsync(TableData);
+ await UpdateDelayedHierarchyTableAsync();
+ await base.OnAfterRenderAsync(firstRender);
+ }
+
+ private async Task OnRowExpandToggleAsync(TableRowExpandToggleEventArgs e)
+ {
+ var recordId = e.RecordId;
+ if (e.NewState && !_recordsLoadingChildren.Contains(recordId) && !_recordsWithLoadedChildren.Contains(recordId))
+ {
+ var record = _delayedHierarchyTableData.Find(person => person.Id == recordId);
+ if (record == null)
+ {
+ return;
+ }
+
+ _recordsLoadingChildren.Add(recordId);
+ await UpdateDelayedHierarchyTableAsync(false);
+
+ await Task.Delay(1500);
+ _recordsLoadingChildren.Remove(recordId);
+ _recordsWithLoadedChildren.Add(recordId);
+ var childrenToAdd = GetChildren(recordId);
+ childrenToAdd.ForEach(child => _delayedHierarchyTableData.Add(child));
+ await UpdateDelayedHierarchyTableAsync();
+ }
+ }
+
+ private List GetChildren(string recordId)
+ {
+ switch (recordId)
+ {
+ case "jacqueline-bouvier":
+ return new List()
+ {
+ new PersonTableRecord("marge-simpson", recordId, "Marge", "Simpson", 35, true),
+ new PersonTableRecord("selma-bouvier", recordId, "Selma", "Bouvier", 45, false),
+ new PersonTableRecord("patty-bouvier", recordId, "Patty", "Bouvier", 45, false)
+ };
+ case "marge-simpson":
+ return new List()
+ {
+ new PersonTableRecord("bart-simpson", recordId, "Bart", "Simpson", 12, false),
+ new PersonTableRecord("lisa-bouvier", recordId, "Lisa", "Simpson", 10, false),
+ new PersonTableRecord("maggie-bouvier", recordId, "Maggie", "Simpson", 1, false)
+ };
+ case "mona-simpson":
+ return new List()
+ {
+ new PersonTableRecord("homer-simpson", recordId, "Homer", "Simpson", 35, false)
+ };
+ case "agnes-skinner":
+ return new List()
+ {
+ new PersonTableRecord("seymour-skinner", recordId, "Seymour", "Skinner", 42, false)
+ };
+ default:
+ return new List();
+ }
+ }
+
+ private async Task UpdateDelayedHierarchyTableAsync(bool setData = true)
+ {
+ if (setData)
+ {
+ await _delayedHierarchyTable!.SetDataAsync(_delayedHierarchyTableData);
+ }
+
+ List options = new List();
+ _delayedHierarchyTableData.ForEach(person =>
+ {
+ if (_recordsLoadingChildren.Contains(person.Id))
+ {
+ options.Add(
+ new TableSetRecordHierarchyOptions(
+ person.Id,
+ new TableRecordHierarchyOptions(TableRecordDelayedHierarchyState.LoadingChildren)));
+ }
+ else if (person.HasChildren && !_recordsWithLoadedChildren.Contains(person.Id))
+ {
+ options.Add(
+ new TableSetRecordHierarchyOptions(
+ person.Id,
+ new TableRecordHierarchyOptions(TableRecordDelayedHierarchyState.CanLoadChildren)));
+ }
+ });
+ await _delayedHierarchyTable!.SetRecordHierarchyOptionsAsync(options);
}
private string DrawerLocationAsString
@@ -82,6 +190,50 @@ public void AddTableRows(int numberOfRowsToAdd)
TableData = tableData;
}
+
+ public void UpdateDies(int numberOfDies)
+ {
+ if (numberOfDies < 0)
+ {
+ return;
+ }
+ var dies = new List();
+ int radius = (int)Math.Ceiling(Math.Sqrt(numberOfDies / Math.PI));
+ var centerX = radius;
+ var centerY = radius;
+
+ for (var i = centerY - radius; i <= centerY + radius; i++)
+ {
+ for (
+ var j = centerX;
+ (j - centerX) * (j - centerX) + (i - centerY) * (i - centerY)
+ <= radius * radius;
+ j--)
+ {
+ var value = (i + j) % 100;
+ dies.Add(new WaferMapDie(i, j, value.ToString(CultureInfo.CurrentCulture)));
+ }
+ // generate points right of centerX
+ for (
+ var j = centerX + 1;
+ (j - centerX) * (j - centerX) + (i - centerY) * (i - centerY)
+ <= radius * radius;
+ j++)
+ {
+ var value = (i + j) % 100;
+ dies.Add(new WaferMapDie(i, j, value.ToString(CultureInfo.CurrentCulture)));
+ }
+ }
+ Dies = dies;
+ }
+ public void AddDiesToRadius(int numberOfDies)
+ {
+ UpdateDies(Dies.Count() + (int)(numberOfDies * numberOfDies * Math.PI));
+ }
+ public void RemoveDiesFromRadius(int numberOfDies)
+ {
+ UpdateDies(Dies.Count() - (int)(numberOfDies * numberOfDies * Math.PI));
+ }
}
public class SimpleTableRecord
@@ -125,6 +277,26 @@ public SimpleTableRecord(
public double Duration { get; }
}
+ public class PersonTableRecord
+ {
+ public PersonTableRecord(string id, string? parentId, string firstName, string lastName, int age, bool hasChildren)
+ {
+ Id = id;
+ ParentId = parentId;
+ FirstName = firstName;
+ LastName = lastName;
+ Age = age;
+ HasChildren = hasChildren;
+ }
+
+ public string Id { get; }
+ public string? ParentId { get; }
+ public string FirstName { get; }
+ public string LastName { get; }
+ public int Age { get; }
+ public bool HasChildren { get; }
+ }
+
public enum DialogResult
{
OK,
diff --git a/packages/nimble-blazor/NimbleBlazor/Components/NimbleLabelProviderTable.razor b/packages/nimble-blazor/NimbleBlazor/Components/NimbleLabelProviderTable.razor
index 4aa34f6fce..50351dc8bc 100644
--- a/packages/nimble-blazor/NimbleBlazor/Components/NimbleLabelProviderTable.razor
+++ b/packages/nimble-blazor/NimbleBlazor/Components/NimbleLabelProviderTable.razor
@@ -11,5 +11,6 @@
group-select-all="@GroupSelectAll"
row-select="@RowSelect"
row-operation-column="@RowOperationColumn"
+ row-loading="@RowLoading"
@attributes="AdditionalAttributes">
diff --git a/packages/nimble-blazor/NimbleBlazor/Components/NimbleLabelProviderTable.razor.cs b/packages/nimble-blazor/NimbleBlazor/Components/NimbleLabelProviderTable.razor.cs
index 5c70363e40..60ab8da142 100644
--- a/packages/nimble-blazor/NimbleBlazor/Components/NimbleLabelProviderTable.razor.cs
+++ b/packages/nimble-blazor/NimbleBlazor/Components/NimbleLabelProviderTable.razor.cs
@@ -40,6 +40,9 @@ public partial class NimbleLabelProviderTable : ComponentBase
[Parameter]
public string? RowOperationColumn { get; set; }
+ [Parameter]
+ public string? RowLoading { get; set; }
+
///
/// Gets or sets a collection of additional attributes that will be applied to the created element.
///
diff --git a/packages/nimble-blazor/NimbleBlazor/Components/NimbleSelect.razor b/packages/nimble-blazor/NimbleBlazor/Components/NimbleSelect.razor
index 1bf2ff2dbf..5e18f48922 100644
--- a/packages/nimble-blazor/NimbleBlazor/Components/NimbleSelect.razor
+++ b/packages/nimble-blazor/NimbleBlazor/Components/NimbleSelect.razor
@@ -8,6 +8,7 @@
name="@Name"
position="@Position.ToAttributeValue()"
appearance="@Appearance.ToAttributeValue()"
+ filter-mode="@FilterMode.ToAttributeValue()"
error-text="@ErrorText"
error-visible="@ErrorVisible"
@attributes="AdditionalAttributes">
diff --git a/packages/nimble-blazor/NimbleBlazor/Components/NimbleSelect.razor.cs b/packages/nimble-blazor/NimbleBlazor/Components/NimbleSelect.razor.cs
index 44705d9f72..6c07e85686 100644
--- a/packages/nimble-blazor/NimbleBlazor/Components/NimbleSelect.razor.cs
+++ b/packages/nimble-blazor/NimbleBlazor/Components/NimbleSelect.razor.cs
@@ -14,15 +14,30 @@ public partial class NimbleSelect : NimbleInputBase
[Parameter]
public string? Name { get; set; }
+ ///
+ /// Gets or sets the disabled state of the select.
+ ///
[Parameter]
public bool? Disabled { get; set; }
+ ///
+ /// Gets or sets the select dropdown position.
+ ///
[Parameter]
public Position? Position { get; set; }
+ ///
+ /// Gets or sets the select appearance .
+ ///
[Parameter]
public DropdownAppearance? Appearance { get; set; }
+ ///
+ /// Gets or sets the select filter mode.
+ ///
+ [Parameter]
+ public FilterMode? FilterMode { get; set; }
+
///
/// Gets or sets the select error text
///
diff --git a/packages/nimble-blazor/NimbleBlazor/Components/NimbleTable.razor b/packages/nimble-blazor/NimbleBlazor/Components/NimbleTable.razor
index 5933f24f78..6c09c39dad 100644
--- a/packages/nimble-blazor/NimbleBlazor/Components/NimbleTable.razor
+++ b/packages/nimble-blazor/NimbleBlazor/Components/NimbleTable.razor
@@ -10,6 +10,7 @@
@onnimbleactionmenutoggle="(__value) => HandleActionMenuToggle(__value)"
@onnimbletablerowselectionchange="(__value) => HandleSelectionChange(__value)"
@onnimbletablecolumnconfigurationchange="(__value) => HandleColumnConfigurationChange(__value)"
+ @onnimbletablerowexpandtoggle="(__value) => HandleRowExpandToggle(__value)"
>
@ChildContent
\ No newline at end of file
diff --git a/packages/nimble-blazor/NimbleBlazor/Components/NimbleTable.razor.cs b/packages/nimble-blazor/NimbleBlazor/Components/NimbleTable.razor.cs
index 82e1d2be08..1a21f1e3cc 100644
--- a/packages/nimble-blazor/NimbleBlazor/Components/NimbleTable.razor.cs
+++ b/packages/nimble-blazor/NimbleBlazor/Components/NimbleTable.razor.cs
@@ -1,5 +1,6 @@
using System.Text.Json;
using Microsoft.AspNetCore.Components;
+using Microsoft.Extensions.Options;
using Microsoft.JSInterop;
namespace NimbleBlazor;
@@ -13,13 +14,12 @@ namespace NimbleBlazor;
public partial class NimbleTable : ComponentBase
{
private ElementReference _table;
- private bool _dataUpdated = false;
- private IEnumerable _data = Enumerable.Empty();
internal static string SetTableDataMethodName = "NimbleBlazor.Table.setData";
internal static string GetSelectedRecordIdsMethodName = "NimbleBlazor.Table.getSelectedRecordIds";
internal static string SetSelectedRecordIdsMethodName = "NimbleBlazor.Table.setSelectedRecordIds";
internal static string CheckTableValidityMethodName = "NimbleBlazor.Table.checkValidity";
internal static string GetTableValidityMethodName = "NimbleBlazor.Table.getValidity";
+ internal static string SetRecordHierarchyOptionsMethodName = "NimbleBlazor.Table.setRecordHierarchyOptions";
[Inject]
private IJSRuntime? JSRuntime { get; set; }
@@ -36,32 +36,19 @@ public partial class NimbleTable : ComponentBase
[Parameter]
public RenderFragment? ChildContent { get; set; }
+ [Parameter(CaptureUnmatchedValues = true)]
+ public IDictionary? AdditionalAttributes { get; set; }
+
///
- /// Gets or sets the data for the table.
+ /// Sets the data in the table.
///
- [Parameter]
- public IEnumerable Data
+ /// The data to set in the table
+ public async Task SetDataAsync(IEnumerable data)
{
- get
- {
- return _data;
- }
- set
- {
- _data = value;
- _dataUpdated = true;
- }
+ var options = new JsonSerializerOptions { MaxDepth = 3 };
+ await JSRuntime!.InvokeVoidAsync(SetTableDataMethodName, _table, JsonSerializer.Serialize(data, options));
}
- ///
- /// Gets or sets a callback that's invoked when the data changes
- ///
- [Parameter]
- public EventCallback> DataChanged { get; set; }
-
- [Parameter(CaptureUnmatchedValues = true)]
- public IDictionary? AdditionalAttributes { get; set; }
-
///
/// Returns the set of selected record IDs.
///
@@ -79,6 +66,15 @@ public async Task SetSelectedRecordIdsAsync(IEnumerable recordIds)
await JSRuntime!.InvokeAsync(SetSelectedRecordIdsMethodName, _table, recordIds);
}
+ ///
+ /// Sets the hierarchy options for each record in the table.
+ ///
+ /// The hierarchy options
+ public async Task SetRecordHierarchyOptionsAsync(IEnumerable options)
+ {
+ await JSRuntime!.InvokeVoidAsync(SetRecordHierarchyOptionsMethodName, _table, options);
+ }
+
///
/// Returns whether or not the table is valid.
///
@@ -119,6 +115,12 @@ public async Task GetValidityAsync()
[Parameter]
public EventCallback ColumnConfigurationChange { get; set; }
+ ///
+ /// Gets or sets a callback that's invoked when a column's configuration is changed.
+ ///
+ [Parameter]
+ public EventCallback RowExpandToggle { get; set; }
+
///
/// Called when 'action-menu-toggle' changes on the web component.
///
@@ -155,15 +157,12 @@ protected async void HandleColumnConfigurationChange(TableColumnConfigurationEve
await ColumnConfigurationChange.InvokeAsync(eventArgs);
}
- ///
- ///
- protected override async Task OnAfterRenderAsync(bool firstRender)
+ ///
+ /// Called when the 'row-expand-toggle' event is fired on the web component.
+ ///
+ /// The toggle state of a table row
+ protected async void HandleRowExpandToggle(TableRowExpandToggleEventArgs eventArgs)
{
- var options = new JsonSerializerOptions { MaxDepth = 3 };
- if (_dataUpdated)
- {
- await JSRuntime!.InvokeVoidAsync(SetTableDataMethodName, _table, JsonSerializer.Serialize(Data, options));
- }
- _dataUpdated = false;
+ await RowExpandToggle.InvokeAsync(eventArgs);
}
}
diff --git a/packages/nimble-blazor/NimbleBlazor/Components/NimbleWaferMap.razor b/packages/nimble-blazor/NimbleBlazor/Components/NimbleWaferMap.razor
new file mode 100644
index 0000000000..ad36903e82
--- /dev/null
+++ b/packages/nimble-blazor/NimbleBlazor/Components/NimbleWaferMap.razor
@@ -0,0 +1,16 @@
+@namespace NimbleBlazor
+ HandleDieHoverChange(__value)">
+
\ No newline at end of file
diff --git a/packages/nimble-blazor/NimbleBlazor/Components/NimbleWaferMap.razor.cs b/packages/nimble-blazor/NimbleBlazor/Components/NimbleWaferMap.razor.cs
new file mode 100644
index 0000000000..85e16b37bf
--- /dev/null
+++ b/packages/nimble-blazor/NimbleBlazor/Components/NimbleWaferMap.razor.cs
@@ -0,0 +1,188 @@
+using System.Text.Json;
+using Microsoft.AspNetCore.Components;
+using Microsoft.JSInterop;
+
+namespace NimbleBlazor;
+
+///
+/// A visualization widget which displays the location and color code of integrated circuits on a silicon wafer.
+///
+public partial class NimbleWaferMap : ComponentBase
+{
+ private ElementReference _waferMap;
+ private bool _diesUpdated = false;
+ private IEnumerable? _dies = Enumerable.Empty();
+ private bool _colorScaleUpdated = false;
+ private WaferMapColorScale? _colorScale;
+ private bool _highlightedTagsUpdated = false;
+ private IEnumerable? _highlightedTags = Enumerable.Empty();
+ internal static string GetWaferMapValidityMethodName = "NimbleBlazor.WaferMap.getValidity";
+ internal static string SetWaferMapDiesMethodName = "NimbleBlazor.WaferMap.setDies";
+ internal static string SetWaferMapColorScaleMethodName = "NimbleBlazor.WaferMap.setColorScale";
+ internal static string SetWaferMapHighlightedTagsMethodName = "NimbleBlazor.WaferMap.setHighlightedTags";
+
+ [Inject]
+ private IJSRuntime? JSRuntime { get; set; }
+
+ ///
+ /// Represents the starting point and the direction of the two axes, X and Y, which are used for displaying the die grid on the wafer map canvas.
+ ///
+ [Parameter]
+ public WaferMapOriginLocation? OriginLocation { get; set; }
+
+ ///
+ /// Represents the X coordinate of the minimum corner of the the grid bounding box for rendering the wafer map.
+ ///
+ [Parameter]
+ public double? GridMinX { get; set; }
+
+ ///
+ /// Represents the X coordinate of the maximum corner of the the grid bounding box for rendering the wafer map.
+ ///
+ [Parameter]
+ public double? GridMaxX { get; set; }
+
+ ///
+ /// Represents the Y coordinate of the minimum corner of the the grid bounding box for rendering the wafer map.
+ ///
+ [Parameter]
+ public double? GridMinY { get; set; }
+
+ ///
+ /// Represents the Y coordinate of the maximum corner of the the grid bounding box for rendering the wafer map.
+ ///
+ [Parameter]
+ public double? GridMaxY { get; set; }
+
+ ///
+ /// Represents the orientation of the notch on the wafer map outline.
+ ///
+ [Parameter]
+ public WaferMapOrientation? Orientation { get; set; }
+
+ ///
+ /// Represents the number of characters allowed to be displayed within a single die, including the label suffix.
+ ///
+ [Parameter]
+ public double? MaxCharacters { get; set; }
+
+ ///
+ /// Represents a boolean value that determines if the die labels in the wafer map view are shown or not. Default value is false.
+ ///
+ [Parameter]
+ public bool? DieLabelsHidden { get; set; }
+
+ ///
+ /// Represents a string that can be added as a label in the end of the each data information in the wafer map dies value.
+ ///
+ [Parameter]
+ public string? DieLabelsSuffix { get; set; }
+
+ ///
+ /// Represents an Enum value that determent if the colorScale is represent a continues gradient values (linear), or is set categorically (ordinal).
+ ///
+ [Parameter]
+ public WaferMapColorScaleMode? ColorScaleMode { get; set; }
+
+ ///
+ /// Represents a list of strings of dies that will be highlighted in the wafer map view.
+ ///
+ [Parameter]
+ public IEnumerable? HighlightedTags
+ {
+ get
+ {
+ return _highlightedTags;
+ }
+ set
+ {
+ _highlightedTags = value;
+ _highlightedTagsUpdated = true;
+ }
+ }
+
+ ///
+ /// Represents the input data, an array of `WaferMapDie`, which fills the wafer map with content
+ ///
+ [Parameter]
+ public IEnumerable? Dies
+ {
+ get
+ {
+ return _dies;
+ }
+ set
+ {
+ _dies = value;
+ _diesUpdated = true;
+ }
+ }
+
+ ///
+ /// Represents the color spectrum which shows the status of the dies on the wafer.
+ ///
+ [Parameter]
+ public WaferMapColorScale? ColorScale
+ {
+ get
+ {
+ return _colorScale;
+ }
+ set
+ {
+ _colorScale = value;
+ _colorScaleUpdated = true;
+ }
+ }
+
+ ///
+ /// Will be triggered to inform the user that the state of hovering over a die has changed.
+ ///
+ [Parameter]
+ public EventCallback HoverDieChanged { get; set; }
+
+ ///
+ /// Called when the 'die-hover' event is fired on the web component.
+ ///
+ /// The configuration of the columns
+ protected async void HandleDieHoverChange(WaferMapHoverDieChangedEventArgs eventArgs)
+ {
+ await HoverDieChanged.InvokeAsync(eventArgs);
+ }
+
+ ///
+ /// Returns an object of boolean values that represents the validity states that the wafer map's configuration can be in.
+ ///
+ public async Task GetValidityAsync()
+ {
+ return await JSRuntime!.InvokeAsync(GetWaferMapValidityMethodName, _waferMap);
+ }
+
+ ///
+ /// Any additional attributes that did not match known properties.
+ ///
+ [Parameter(CaptureUnmatchedValues = true)]
+ public IDictionary? AdditionalAttributes { get; set; }
+
+ ///
+ ///
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ var options = new JsonSerializerOptions { MaxDepth = 3 };
+ if (_diesUpdated)
+ {
+ await JSRuntime!.InvokeVoidAsync(SetWaferMapDiesMethodName, _waferMap, JsonSerializer.Serialize(_dies, options));
+ }
+ _diesUpdated = false;
+ if (_colorScaleUpdated)
+ {
+ await JSRuntime!.InvokeVoidAsync(SetWaferMapColorScaleMethodName, _waferMap, JsonSerializer.Serialize(_colorScale, options));
+ }
+ _colorScaleUpdated = false;
+ if (_highlightedTagsUpdated)
+ {
+ await JSRuntime!.InvokeVoidAsync(SetWaferMapHighlightedTagsMethodName, _waferMap, JsonSerializer.Serialize(_highlightedTags, options));
+ }
+ _highlightedTagsUpdated = false;
+ }
+}
diff --git a/packages/nimble-blazor/NimbleBlazor/EventHandlers.cs b/packages/nimble-blazor/NimbleBlazor/EventHandlers.cs
index ed4fefb661..9f8818d93c 100644
--- a/packages/nimble-blazor/NimbleBlazor/EventHandlers.cs
+++ b/packages/nimble-blazor/NimbleBlazor/EventHandlers.cs
@@ -62,6 +62,18 @@ public class TableColumnConfiguration
public double? PixelWidth { get; set; }
}
+public class TableRowExpandToggleEventArgs : EventArgs
+{
+ public string RecordId { get; set; } = string.Empty;
+ public bool NewState { get; set; }
+ public bool OldState { get; set; }
+}
+
+public class WaferMapHoverDieChangedEventArgs : EventArgs
+{
+ public WaferMapDie? CurrentDie { get; set; }
+}
+
[EventHandler("onnimbletabsactiveidchange", typeof(TabsChangeEventArgs), enableStopPropagation: true, enablePreventDefault: true)]
[EventHandler("onnimblecheckedchange", typeof(CheckboxChangeEventArgs), enableStopPropagation: true, enablePreventDefault: true)]
[EventHandler("onnimblemenubuttontoggle", typeof(MenuButtonToggleEventArgs), enableStopPropagation: true, enablePreventDefault: false)]
@@ -71,6 +83,8 @@ public class TableColumnConfiguration
[EventHandler("onnimbleactionmenubeforetoggle", typeof(TableActionMenuToggleEventArgs), enableStopPropagation: true, enablePreventDefault: false)]
[EventHandler("onnimbletablerowselectionchange", typeof(TableRowSelectionEventArgs), enableStopPropagation: true, enablePreventDefault: false)]
[EventHandler("onnimbletablecolumnconfigurationchange", typeof(TableColumnConfigurationEventArgs), enableStopPropagation: true, enablePreventDefault: false)]
+[EventHandler("onnimbletablerowexpandtoggle", typeof(TableRowExpandToggleEventArgs), enableStopPropagation: true, enablePreventDefault: false)]
+[EventHandler("onnimblewafermapdiehoverchange", typeof(WaferMapHoverDieChangedEventArgs), enableStopPropagation: true, enablePreventDefault: false)]
public static class EventHandlers
{
}
diff --git a/packages/nimble-blazor/NimbleBlazor/FilterMode.cs b/packages/nimble-blazor/NimbleBlazor/FilterMode.cs
new file mode 100644
index 0000000000..6289426e8e
--- /dev/null
+++ b/packages/nimble-blazor/NimbleBlazor/FilterMode.cs
@@ -0,0 +1,14 @@
+namespace NimbleBlazor;
+
+public enum FilterMode
+{
+ None,
+ Standard
+}
+
+internal static class FilterModeExtensions
+{
+ private static readonly Dictionary _enumValues = AttributeHelpers.GetEnumNamesAsKebabCaseValues();
+
+ public static string? ToAttributeValue(this FilterMode? value) => (value == null || value == FilterMode.None) ? null : _enumValues[value.Value];
+}
diff --git a/packages/nimble-blazor/NimbleBlazor/TableHierarchyOptions.cs b/packages/nimble-blazor/NimbleBlazor/TableHierarchyOptions.cs
new file mode 100644
index 0000000000..a9be899842
--- /dev/null
+++ b/packages/nimble-blazor/NimbleBlazor/TableHierarchyOptions.cs
@@ -0,0 +1,14 @@
+using System.Text.Json.Serialization;
+
+namespace NimbleBlazor;
+
+public class TableRecordHierarchyOptions
+{
+ public TableRecordHierarchyOptions(TableRecordDelayedHierarchyState delayedHierarchyState)
+ {
+ DelayedHierarchyState = delayedHierarchyState;
+ }
+
+ [JsonConverter(typeof(TableRecordDelayedHierarchyStateConverter))]
+ public TableRecordDelayedHierarchyState DelayedHierarchyState { get; set; }
+}
diff --git a/packages/nimble-blazor/NimbleBlazor/TableRecordDelayedHierarchyState.cs b/packages/nimble-blazor/NimbleBlazor/TableRecordDelayedHierarchyState.cs
new file mode 100644
index 0000000000..cc84348879
--- /dev/null
+++ b/packages/nimble-blazor/NimbleBlazor/TableRecordDelayedHierarchyState.cs
@@ -0,0 +1,15 @@
+namespace NimbleBlazor;
+
+public enum TableRecordDelayedHierarchyState
+{
+ None,
+ CanLoadChildren,
+ LoadingChildren
+}
+
+internal static class TableRecordDelayedHierarchyStateExtensions
+{
+ internal static readonly Dictionary EnumValues = AttributeHelpers.GetEnumNamesAsKebabCaseValues();
+
+ public static string? ToAttributeValue(this TableRecordDelayedHierarchyState? value) => (value == null || value == TableRecordDelayedHierarchyState.None) ? null : EnumValues[value.Value];
+}
diff --git a/packages/nimble-blazor/NimbleBlazor/TableRecordDelayedHierarchyStateConverter.cs b/packages/nimble-blazor/NimbleBlazor/TableRecordDelayedHierarchyStateConverter.cs
new file mode 100644
index 0000000000..988c098621
--- /dev/null
+++ b/packages/nimble-blazor/NimbleBlazor/TableRecordDelayedHierarchyStateConverter.cs
@@ -0,0 +1,19 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace NimbleBlazor;
+
+internal class TableRecordDelayedHierarchyStateConverter : JsonConverter
+{
+ public override TableRecordDelayedHierarchyState Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var value = reader.GetString();
+ return TableRecordDelayedHierarchyStateExtensions.EnumValues.FirstOrDefault(x => x.Value == value).Key;
+ }
+
+ public override void Write(Utf8JsonWriter writer, TableRecordDelayedHierarchyState value, JsonSerializerOptions options)
+ {
+ var convertedValue = (value as TableRecordDelayedHierarchyState?).ToAttributeValue();
+ writer.WriteStringValue(convertedValue);
+ }
+}
diff --git a/packages/nimble-blazor/NimbleBlazor/TableSetRecordHierarchyOptions.cs b/packages/nimble-blazor/NimbleBlazor/TableSetRecordHierarchyOptions.cs
new file mode 100644
index 0000000000..9aeaa0a18e
--- /dev/null
+++ b/packages/nimble-blazor/NimbleBlazor/TableSetRecordHierarchyOptions.cs
@@ -0,0 +1,14 @@
+namespace NimbleBlazor;
+
+public class TableSetRecordHierarchyOptions
+{
+ public TableSetRecordHierarchyOptions(string recordId, TableRecordHierarchyOptions options)
+ {
+ RecordId = recordId;
+ Options = options;
+ }
+
+ public string RecordId { get; set; }
+
+ public TableRecordHierarchyOptions Options { get; set; }
+}
diff --git a/packages/nimble-blazor/NimbleBlazor/WaferMapColorScale.cs b/packages/nimble-blazor/NimbleBlazor/WaferMapColorScale.cs
new file mode 100644
index 0000000000..e8b724905c
--- /dev/null
+++ b/packages/nimble-blazor/NimbleBlazor/WaferMapColorScale.cs
@@ -0,0 +1,17 @@
+using System.Text.Json.Serialization;
+
+namespace NimbleBlazor;
+
+public class WaferMapColorScale
+{
+ [JsonPropertyName("colors")]
+ public IEnumerable Colors { get; set; }
+ [JsonPropertyName("values")]
+ public IEnumerable Values { get; set; }
+
+ public WaferMapColorScale(IEnumerable colors, IEnumerable values)
+ {
+ Colors = colors;
+ Values = values;
+ }
+}
diff --git a/packages/nimble-blazor/NimbleBlazor/WaferMapColorScaleMode.cs b/packages/nimble-blazor/NimbleBlazor/WaferMapColorScaleMode.cs
new file mode 100644
index 0000000000..1b13316014
--- /dev/null
+++ b/packages/nimble-blazor/NimbleBlazor/WaferMapColorScaleMode.cs
@@ -0,0 +1,14 @@
+namespace NimbleBlazor;
+
+public enum WaferMapColorScaleMode
+{
+ Linear,
+ Ordinal
+}
+
+internal static class WaferMapColorScaleModeExtensions
+{
+ private static readonly Dictionary _enumValues = AttributeHelpers.GetEnumNamesAsKebabCaseValues();
+
+ public static string? ToAttributeValue(this WaferMapColorScaleMode? value) => (value == null || value == WaferMapColorScaleMode.Linear) ? null : _enumValues[value.Value];
+}
\ No newline at end of file
diff --git a/packages/nimble-blazor/NimbleBlazor/WaferMapDie.cs b/packages/nimble-blazor/NimbleBlazor/WaferMapDie.cs
new file mode 100644
index 0000000000..0ea7e4712b
--- /dev/null
+++ b/packages/nimble-blazor/NimbleBlazor/WaferMapDie.cs
@@ -0,0 +1,26 @@
+using System.Text.Json.Serialization;
+
+namespace NimbleBlazor;
+
+public class WaferMapDie
+{
+ [JsonPropertyName("value")]
+ public string Value { get; set; }
+ [JsonPropertyName("x")]
+ public double X { get; set; }
+ [JsonPropertyName("y")]
+ public double Y { get; set; }
+ [JsonPropertyName("metadata")]
+ public string? Metadata { get; set; }
+ [JsonPropertyName("tags")]
+ public IEnumerable? Tags { get; set; }
+
+ public WaferMapDie(double x, double y, string value, string? metadata = null, IEnumerable? tags = null)
+ {
+ X = x;
+ Y = y;
+ Value = value;
+ Metadata = metadata;
+ Tags = tags;
+ }
+}
diff --git a/packages/nimble-blazor/NimbleBlazor/WaferMapOrientation.cs b/packages/nimble-blazor/NimbleBlazor/WaferMapOrientation.cs
new file mode 100644
index 0000000000..4f2ae87708
--- /dev/null
+++ b/packages/nimble-blazor/NimbleBlazor/WaferMapOrientation.cs
@@ -0,0 +1,16 @@
+namespace NimbleBlazor;
+
+public enum WaferMapOrientation
+{
+ Top,
+ Bottom,
+ Left,
+ Right
+}
+
+internal static class WaferMapOrientationExtensions
+{
+ private static readonly Dictionary _enumValues = AttributeHelpers.GetEnumNamesAsKebabCaseValues();
+
+ public static string? ToAttributeValue(this WaferMapOrientation? value) => (value == null || value == WaferMapOrientation.Top) ? null : _enumValues[value.Value];
+}
diff --git a/packages/nimble-blazor/NimbleBlazor/WaferMapOriginLocation.cs b/packages/nimble-blazor/NimbleBlazor/WaferMapOriginLocation.cs
new file mode 100644
index 0000000000..bc849aee51
--- /dev/null
+++ b/packages/nimble-blazor/NimbleBlazor/WaferMapOriginLocation.cs
@@ -0,0 +1,16 @@
+namespace NimbleBlazor;
+
+public enum WaferMapOriginLocation
+{
+ BottomLeft,
+ BottomRight,
+ TopLeft,
+ TopRight
+}
+
+internal static class WaferMapOriginLocationExtensions
+{
+ private static readonly Dictionary _enumValues = AttributeHelpers.GetEnumNamesAsKebabCaseValues();
+
+ public static string? ToAttributeValue(this WaferMapOriginLocation? value) => (value == null || value == WaferMapOriginLocation.BottomLeft) ? null : _enumValues[value.Value];
+}
diff --git a/packages/nimble-blazor/NimbleBlazor/WaferMapValidity.cs b/packages/nimble-blazor/NimbleBlazor/WaferMapValidity.cs
new file mode 100644
index 0000000000..110aa76337
--- /dev/null
+++ b/packages/nimble-blazor/NimbleBlazor/WaferMapValidity.cs
@@ -0,0 +1,14 @@
+using System.Text.Json.Serialization;
+
+namespace NimbleBlazor;
+
+public interface IWaferMapValidity
+{
+ public bool InvalidGridDimensions { get; }
+}
+
+internal class WaferMapValidity : IWaferMapValidity
+{
+ [JsonPropertyName("invalidGridDimensions")]
+ public bool InvalidGridDimensions { get; set; }
+}
\ No newline at end of file
diff --git a/packages/nimble-blazor/NimbleBlazor/wwwroot/NimbleBlazor.lib.module.js b/packages/nimble-blazor/NimbleBlazor/wwwroot/NimbleBlazor.lib.module.js
index eac35b9554..6f0ada1426 100644
--- a/packages/nimble-blazor/NimbleBlazor/wwwroot/NimbleBlazor.lib.module.js
+++ b/packages/nimble-blazor/NimbleBlazor/wwwroot/NimbleBlazor.lib.module.js
@@ -118,6 +118,26 @@ export function afterStarted(Blazor) {
};
}
});
+ // Used by NimbleTable.razor
+ Blazor.registerCustomEventType('nimbletablerowexpandtoggle', {
+ browserEventName: 'row-expand-toggle',
+ createEventArgs: event => {
+ return {
+ recordId: event.detail.recordId,
+ newState: event.detail.newState,
+ oldState: event.detail.oldState
+ };
+ }
+ });
+ // Used by NimbleWaferMap.razor
+ Blazor.registerCustomEventType('nimblewafermapdiehoverchange', {
+ browserEventName: 'die-hover',
+ createEventArgs: event => {
+ return {
+ currentDie: event.detail.currentDie
+ };
+ }
+ });
}
if (window.NimbleBlazor) {
@@ -149,6 +169,17 @@ window.NimbleBlazor = window.NimbleBlazor ?? {
const dataObject = JSON.parse(data);
await tableReference.setData(dataObject);
},
+ setRecordHierarchyOptions: async function (tableReference, options) {
+ // Blazor converts the 'None' delayed hierarchy state to null,
+ // but nimble-components expects 'None' to be passed as undefined.
+ // Therefore, change any null values to undefined.
+ for (const option of options) {
+ if (option.options.delayedHierarchyState === null) {
+ option.options.delayedHierarchyState = undefined;
+ }
+ }
+ await tableReference.setRecordHierarchyOptions(options);
+ },
getSelectedRecordIds: async function (tableReference) {
return tableReference.getSelectedRecordIds();
},
@@ -169,5 +200,22 @@ window.NimbleBlazor = window.NimbleBlazor ?? {
getValidity: function (themeProviderReference) {
return themeProviderReference.validity;
}
+ },
+ WaferMap: {
+ getValidity: function (waferMapReference) {
+ return waferMapReference.validity;
+ },
+ setDies: function (waferMapReference, data) {
+ const diesObject = JSON.parse(data);
+ waferMapReference.dies = diesObject;
+ },
+ setColorScale: function (waferMapReference, data) {
+ const colorScaleObject = JSON.parse(data);
+ waferMapReference.colorScale = colorScaleObject;
+ },
+ setHighlightedTags: function (waferMapReference, data) {
+ const highlightedTagsObject = JSON.parse(data);
+ waferMapReference.highlightedTags = highlightedTagsObject;
+ }
}
};
diff --git a/packages/nimble-blazor/README.md b/packages/nimble-blazor/README.md
index 1d70e2077c..ad63665c2a 100644
--- a/packages/nimble-blazor/README.md
+++ b/packages/nimble-blazor/README.md
@@ -78,6 +78,32 @@ To test out your changes, do "Debug" >> "Start without Debugging" in Visual Stud
More complete examples can be found in the Demo.Client/Server example projects.
+#### NimbleTable usage
+
+The `NimbleTable` requires that its data be set via the `SetDataAsync` method. The appropriate place to call this method is either in the `OnAfterRenderAsync` override of the hosting component or after that method has been called for the first time.
+
+As the `NimbleTable` is generic a client must supply its generic type in the markup using the `TData` property syntax. The following code represents a typical usage of the `NimbleTable`:
+```html
+
+@code {
+ private NimbleTable? _table;
+ private IEnumerable TableData { get; set; } = Enumerable.Empty();
+ ...
+ public override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ await base.OnAfterRenderAsync(firstRender);
+ await _table.SetDataAsync(TableData); // populate TableData before here
+ }
+
+ public class MyRecordType
+ {
+ ...
+ }
+}
+```
+
+For more information regarding the Blazor component lifecycle mechanisms (such as `OnAfterRenderAsync`), please consult the [Microsoft Blazor docs](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/lifecycle).
+
### Theming and Design Tokens
To use Nimble's theme-aware design tokens in a Blazor app, you should have a `` element as an ancestor to all of the Nimble components you use. The app's default layout (`MainLayout.razor` in the examples) is a good place to put the theme provider (as the root content of the page).
diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/NimbleBlazor.Tests.Acceptance.csproj b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/NimbleBlazor.Tests.Acceptance.csproj
index e054aa559d..08520ca39f 100644
--- a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/NimbleBlazor.Tests.Acceptance.csproj
+++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/NimbleBlazor.Tests.Acceptance.csproj
@@ -23,7 +23,7 @@
-
+
diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/TableColumnEnumTextIntKey.razor b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/TableColumnEnumTextIntKey.razor
index 0d9c3c2adf..f82c929458 100644
--- a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/TableColumnEnumTextIntKey.razor
+++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/TableColumnEnumTextIntKey.razor
@@ -3,7 +3,7 @@
@using System.Diagnostics.CodeAnalysis;
@inherits LayoutComponentBase
-
+
@@ -15,6 +15,7 @@
@code {
[NotNull]
public IEnumerable TableData { get; set; } = Enumerable.Empty();
+ private NimbleTable? _table;
public TableColumnEnumTextIntKey()
{
@@ -32,6 +33,12 @@
TableData = tableData;
}
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ await _table!.SetDataAsync(TableData);
+ await base.OnAfterRenderAsync(firstRender);
+ }
+
public class RowData
{
public RowData(int id)
diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/TableColumnIconBoolKey.razor b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/TableColumnIconBoolKey.razor
index 91868e8a2c..4cfb01f13e 100644
--- a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/TableColumnIconBoolKey.razor
+++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/TableColumnIconBoolKey.razor
@@ -3,7 +3,7 @@
@using System.Diagnostics.CodeAnalysis;
@inherits LayoutComponentBase
-
+
@@ -14,6 +14,7 @@
@code {
[NotNull]
public IEnumerable TableData { get; set; } = Enumerable.Empty();
+ private NimbleTable? _table;
public TableColumnIconBoolKey()
{
@@ -31,6 +32,12 @@
TableData = tableData;
}
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ await _table!.SetDataAsync(TableData);
+ await base.OnAfterRenderAsync(firstRender);
+ }
+
public class RowData
{
public RowData(int id)
diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/TableColumnNumberText.razor b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/TableColumnNumberText.razor
index 313080ede7..bec5d6c263 100644
--- a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/TableColumnNumberText.razor
+++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/TableColumnNumberText.razor
@@ -3,7 +3,7 @@
@using System.Diagnostics.CodeAnalysis;
@inherits LayoutComponentBase
-
+
Column 1
@@ -14,6 +14,8 @@
[NotNull]
public IEnumerable TableData { get; set; } = Enumerable.Empty();
+ private NimbleTable? _table;
+
public TableColumnNumberText()
{
UpdateTableData(2);
@@ -30,6 +32,12 @@
TableData = tableData;
}
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ await _table!.SetDataAsync(TableData);
+ await base.OnAfterRenderAsync(firstRender);
+ }
+
public class RowData
{
public RowData(int id)
diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/TableBindToData.razor b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/TableSetData.razor
similarity index 73%
rename from packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/TableBindToData.razor
rename to packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/TableSetData.razor
index 24670dbd10..eb505779d6 100644
--- a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/TableBindToData.razor
+++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/TableSetData.razor
@@ -1,17 +1,18 @@
-@page "/TableBindToData"
+@page "/TableSetData"
@namespace NimbleBlazor.Tests.Acceptance.Pages
@using System.Diagnostics.CodeAnalysis;
@inherits LayoutComponentBase
-
+
Column 1
@code {
[NotNull]
public IEnumerable TableData { get; set; } = Enumerable.Empty();
+ private NimbleTable? _table;
- public TableBindToData()
+ public TableSetData()
{
UpdateTableData(5);
}
@@ -29,6 +30,12 @@
TableData = tableData;
}
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ await _table!.SetDataAsync(TableData);
+ await base.OnAfterRenderAsync(firstRender);
+ }
+
public class RowData
{
public RowData(string id, string field1)
diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/TableSetRecordHierarchyOptionsTest.razor b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/TableSetRecordHierarchyOptionsTest.razor
new file mode 100644
index 0000000000..3c5b87e630
--- /dev/null
+++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/TableSetRecordHierarchyOptionsTest.razor
@@ -0,0 +1,68 @@
+@page "/TableSetRecordHierarchyOptionsTest"
+@namespace NimbleBlazor.Tests.Acceptance.Pages
+@using System.Diagnostics.CodeAnalysis;
+@inherits LayoutComponentBase
+
+
+ Column 1
+
+
+
+@code {
+ [NotNull]
+ public IEnumerable TableData { get; set; } = Enumerable.Empty();
+ [NotNull]
+ public IEnumerable HiearchyOptions { get; set; } = Enumerable.Empty();
+ public string TextFieldText = string.Empty;
+ private NimbleTable? _table;
+
+ public TableSetRecordHierarchyOptionsTest()
+ {
+ UpdateTableData(4);
+ }
+
+ public void UpdateTableData(int numberOfRows)
+ {
+ var tableData = new RowData[numberOfRows];
+ for (int i = 0; i < numberOfRows; i++)
+ {
+ tableData[i] = new RowData(
+ i.ToString(null, null),
+ $"A{i}");
+ }
+
+ TableData = tableData;
+
+ var hierarchyOptions = new List()
+ {
+ new TableSetRecordHierarchyOptions("0", new TableRecordHierarchyOptions(TableRecordDelayedHierarchyState.CanLoadChildren)),
+ new TableSetRecordHierarchyOptions("1", new TableRecordHierarchyOptions(TableRecordDelayedHierarchyState.None)),
+ new TableSetRecordHierarchyOptions("2", new TableRecordHierarchyOptions(TableRecordDelayedHierarchyState.LoadingChildren))
+ };
+ HiearchyOptions = hierarchyOptions;
+ }
+
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ await _table!.SetDataAsync(TableData);
+ await _table!.SetRecordHierarchyOptionsAsync(HiearchyOptions);
+ await base.OnAfterRenderAsync(firstRender);
+ }
+
+ private void OnRowExpandToggle(TableRowExpandToggleEventArgs e)
+ {
+ TextFieldText = $"RecordId: {e.RecordId}, OldState: {e.OldState}, NewState: {e.NewState}";
+ }
+
+ public class RowData
+ {
+ public RowData(string id, string field1)
+ {
+ Id = id;
+ Field1 = field1;
+ }
+
+ public string Id { get; }
+ public string Field1 { get; }
+ }
+}
diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/WaferMapRenderTest.razor b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/WaferMapRenderTest.razor
new file mode 100644
index 0000000000..f69718ce04
--- /dev/null
+++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Pages/WaferMapRenderTest.razor
@@ -0,0 +1,97 @@
+@page "/WaferMapRenderTest"
+@namespace NimbleBlazor.Tests.Acceptance.Pages
+@using System.Diagnostics.CodeAnalysis;
+@inherits LayoutComponentBase
+
+
+
+Get Validity Async
+
+@code {
+ public NimbleWaferMap? _waferMap;
+ private string? TextFieldText { get; set; }
+ [NotNull]
+ public IEnumerable Dies { get; set; } = Enumerable.Empty();
+ [NotNull]
+ public IEnumerable HighlightedTags { get; set; } = Enumerable.Empty();
+ [NotNull]
+ public WaferMapColorScale ColorScale { get; set; } = new WaferMapColorScale(new List { "red", "green" }, new List { "0", "6" });
+
+ public WaferMapRenderTest()
+ {
+ UpdateDies(5);
+ }
+
+ public void UpdateDies(int numDies)
+ {
+
+ var dies = new List();
+ int radius = (int)Math.Ceiling(Math.Sqrt(numDies / Math.PI));
+ var centerX = radius;
+ var centerY = radius;
+
+ for (var i = centerY - radius; i <= centerY + radius; i++)
+ {
+ // generate points left of centerX
+ for (
+ var j = centerX;
+ (j - centerX) * (j - centerX) + (i - centerY) * (i - centerY)
+ <= radius * radius;
+ j--)
+ {
+ var value = (i + j) % 100;
+ dies.Add(new WaferMapDie(i, j, value.ToString()));
+ }
+ // generate points right of centerX
+ for (
+ var j = centerX + 1;
+ (j - centerX) * (j - centerX) + (i - centerY) * (i - centerY)
+ <= radius * radius;
+ j++)
+ {
+ var value = (i + j) % 100;
+ dies.Add(new WaferMapDie(i, j, value.ToString()));
+ }
+ }
+ Dies = dies;
+ }
+
+ public async Task GetValidityAsync()
+ {
+ if (_waferMap == null)
+ {
+ return;
+ }
+
+ var validity = await _waferMap.GetValidityAsync();
+
+ TextFieldText = validity.InvalidGridDimensions.ToString();
+
+ }
+
+ private void ShowCurrentDie(WaferMapHoverDieChangedEventArgs e)
+ {
+ if (_waferMap == null)
+ {
+ return;
+ }
+
+ var currentDie = e.CurrentDie;
+
+ TextFieldText = currentDie?.Value;
+
+ }
+}
diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Tests/TableTests.cs b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Tests/TableTests.cs
index d7b3a59ac3..2ef27ad634 100644
--- a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Tests/TableTests.cs
+++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Tests/TableTests.cs
@@ -13,7 +13,7 @@ public TableTests(PlaywrightFixture playwrightFixture, BlazorServerWebHostFixtur
[Fact]
public async Task Table_RendersBoundDataAsync()
{
- await using (var pageWrapper = await NewPageForRouteAsync("TableBindToData"))
+ await using (var pageWrapper = await NewPageForRouteAsync("TableSetData"))
{
var page = pageWrapper.Page;
var table = page.Locator("nimble-table");
@@ -24,5 +24,67 @@ public async Task Table_RendersBoundDataAsync()
await Assertions.Expect(rows).ToContainTextAsync(new string[] { "A0", "A1", "A2", "A3", "A4" });
}
}
+
+ [Theory]
+ [InlineData(0, TableRecordDelayedHierarchyState.CanLoadChildren)]
+ [InlineData(1, TableRecordDelayedHierarchyState.None)]
+ [InlineData(2, TableRecordDelayedHierarchyState.LoadingChildren)]
+ [InlineData(3, TableRecordDelayedHierarchyState.None)]
+ public async Task Table_RendersHierarchyOptionsAsync(int rowIndex, TableRecordDelayedHierarchyState expectedHierarchyState)
+ {
+ await using (var pageWrapper = await NewPageForRouteAsync("TableSetRecordHierarchyOptionsTest"))
+ {
+ var page = pageWrapper.Page;
+ var table = page.Locator("nimble-table");
+ await Assertions.Expect(table).ToBeVisibleAsync();
+
+ var rows = table.Locator("nimble-table-row");
+ await Assertions.Expect(rows).ToHaveCountAsync(4);
+
+ var row = rows.Nth(rowIndex);
+ var rowExpandCollapseButton = row.Locator("nimble-button");
+ var rowSpinner = row.Locator("nimble-spinner");
+
+ if (expectedHierarchyState == TableRecordDelayedHierarchyState.CanLoadChildren)
+ {
+ await Assertions.Expect(rowExpandCollapseButton).ToBeVisibleAsync();
+ }
+ else
+ {
+ await Assertions.Expect(rowExpandCollapseButton).Not.ToBeVisibleAsync();
+ }
+
+ if (expectedHierarchyState == TableRecordDelayedHierarchyState.LoadingChildren)
+ {
+ await Assertions.Expect(rowSpinner).ToBeVisibleAsync();
+ }
+ else
+ {
+ await Assertions.Expect(rowSpinner).Not.ToBeVisibleAsync();
+ }
+ }
+ }
+
+ [Fact]
+ public async Task Table_TriggersRowExpandToggleEventAsync()
+ {
+ await using (var pageWrapper = await NewPageForRouteAsync("TableSetRecordHierarchyOptionsTest"))
+ {
+ var page = pageWrapper.Page;
+ var table = page.Locator("nimble-table");
+ await Assertions.Expect(table).ToBeVisibleAsync();
+ var textField = page.Locator("nimble-text-field");
+
+ var rows = table.Locator("nimble-table-row");
+ var expandableRow = rows.Nth(0);
+ var rowExpandCollapseButton = expandableRow.Locator("nimble-button");
+
+ await rowExpandCollapseButton.ClickAsync();
+ await Assertions.Expect(textField).ToHaveAttributeAsync("current-value", "RecordId: 0, OldState: False, NewState: True");
+
+ await rowExpandCollapseButton.ClickAsync();
+ await Assertions.Expect(textField).ToHaveAttributeAsync("current-value", "RecordId: 0, OldState: True, NewState: False");
+ }
+ }
}
}
diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Tests/WaferMapTests.cs b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Tests/WaferMapTests.cs
new file mode 100644
index 0000000000..ed8f68aa22
--- /dev/null
+++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests.Acceptance/Tests/WaferMapTests.cs
@@ -0,0 +1,59 @@
+using Microsoft.Playwright;
+using Xunit;
+
+namespace NimbleBlazor.Tests.Acceptance
+{
+ public class WaferMapTests : AcceptanceTestsBase
+ {
+ private const int RenderingTimeout = 200;
+ public WaferMapTests(PlaywrightFixture playwrightFixture, BlazorServerWebHostFixture blazorServerClassFixture)
+ : base(playwrightFixture, blazorServerClassFixture)
+ { }
+
+ [Fact]
+ public async Task WaferMap_WithDiesAndColorScale_RendersColorsAsync()
+ {
+ await using var pageWrapper = await NewPageForRouteAsync("WaferMapRenderTest");
+ var page = pageWrapper.Page;
+ var canvas = page.Locator("canvas");
+
+ await Assertions.Expect(canvas).ToBeVisibleAsync();
+ await Task.Delay(RenderingTimeout);
+ var color = await page.EvaluateAsync(
+ @"document.getElementsByTagName('nimble-wafer-map')[0].canvas.getContext('2d').getImageData(249, 249, 1, 1).data.toString()");
+
+ Assert.Equal(@"85,85,0,255", color);
+ }
+
+ [Fact]
+ public async Task WaferMap_WithGridDimensions_IsValidAsync()
+ {
+ await using var pageWrapper = await NewPageForRouteAsync("WaferMapRenderTest");
+ var page = pageWrapper.Page;
+ var canvas = page.Locator("canvas");
+ var validButton = page.Locator("nimble-button");
+ var textField = page.Locator("nimble-text-field");
+
+ await Assertions.Expect(canvas).ToBeVisibleAsync();
+ await Task.Delay(RenderingTimeout);
+ await validButton.ClickAsync();
+
+ await Assertions.Expect(textField).ToHaveAttributeAsync("current-value", "False");
+ }
+
+ [Fact]
+ public async Task WaferMap_WithHoverEvent_TriggersDieChangeEventAsync()
+ {
+ await using var pageWrapper = await NewPageForRouteAsync("WaferMapRenderTest");
+ var page = pageWrapper.Page;
+ var canvas = page.Locator("canvas");
+ var textField = page.Locator("nimble-text-field");
+
+ await Assertions.Expect(canvas).ToBeVisibleAsync();
+ await Task.Delay(RenderingTimeout);
+ await canvas.HoverAsync();
+
+ await Assertions.Expect(textField).ToHaveAttributeAsync("current-value", "4");
+ }
+ }
+}
diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests/NimbleBlazor.Tests.csproj b/packages/nimble-blazor/Tests/NimbleBlazor.Tests/NimbleBlazor.Tests.csproj
index 2d7450ff85..3f6da0db3b 100644
--- a/packages/nimble-blazor/Tests/NimbleBlazor.Tests/NimbleBlazor.Tests.csproj
+++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests/NimbleBlazor.Tests.csproj
@@ -26,11 +26,11 @@
-
-
-
+
+
+
-
+
diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests/Unit/Components/NimbleLabelProviderTableTests.cs b/packages/nimble-blazor/Tests/NimbleBlazor.Tests/Unit/Components/NimbleLabelProviderTableTests.cs
index a4e8fa4da2..0e607ee84d 100644
--- a/packages/nimble-blazor/Tests/NimbleBlazor.Tests/Unit/Components/NimbleLabelProviderTableTests.cs
+++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests/Unit/Components/NimbleLabelProviderTableTests.cs
@@ -41,6 +41,7 @@ public void NimbleLabelProviderTable_SupportsAdditionalAttributes()
[InlineData(nameof(NimbleLabelProviderTable.GroupSelectAll))]
[InlineData(nameof(NimbleLabelProviderTable.RowSelect))]
[InlineData(nameof(NimbleLabelProviderTable.RowOperationColumn))]
+ [InlineData(nameof(NimbleLabelProviderTable.RowLoading))]
public void NimbleLabelProviderTable_LabelIsSet(string propertyName)
{
var labelValue = propertyName + "UpdatedValue";
diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests/Unit/Components/NimbleSelectTests.cs b/packages/nimble-blazor/Tests/NimbleBlazor.Tests/Unit/Components/NimbleSelectTests.cs
index e5c38d15c5..03403fef01 100644
--- a/packages/nimble-blazor/Tests/NimbleBlazor.Tests/Unit/Components/NimbleSelectTests.cs
+++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests/Unit/Components/NimbleSelectTests.cs
@@ -77,6 +77,23 @@ public void DropdownAppearance_AttributeIsSet(DropdownAppearance value, string e
Assert.Contains(expectedAttribute, select.Markup);
}
+ [Fact]
+ public void Select_FilterModeStandardIsSet()
+ {
+ var expectedContents = "filter-mode=\"standard\"";
+ var select = RenderWithPropertySet(x => x.FilterMode, FilterMode.Standard);
+
+ Assert.Contains(expectedContents, select.Markup);
+ }
+
+ [Fact]
+ public void Select_FilterModeNoneIsSet()
+ {
+ var select = RenderWithPropertySet(x => x.FilterMode, FilterMode.None);
+
+ Assert.DoesNotContain("filter-mode", select.Markup);
+ }
+
private IRenderedComponent RenderWithPropertySet(Expression> propertyGetter, TProperty propertyValue)
{
var context = new TestContext();
diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests/Unit/Components/NimbleTableTests.cs b/packages/nimble-blazor/Tests/NimbleBlazor.Tests/Unit/Components/NimbleTableTests.cs
index 9dd9ba72a4..661c1581fc 100644
--- a/packages/nimble-blazor/Tests/NimbleBlazor.Tests/Unit/Components/NimbleTableTests.cs
+++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests/Unit/Components/NimbleTableTests.cs
@@ -3,6 +3,7 @@
using System.Linq.Expressions;
using System.Security.Principal;
using System.Text.Json;
+using System.Threading.Tasks;
using Bunit;
using Xunit;
#nullable enable
@@ -106,27 +107,29 @@ public void NimbleTable_WithClassAttribute_HasTableMarkup()
}
[Fact]
- public void NimbleTable_GivenSupportedData_ThrowsNoException()
+ public async Task NimbleTable_GivenSupportedData_ThrowsNoExceptionAsync()
{
var parentRowData = new TableRowData("Bob", "Smith");
var childRowData = new TableRowData("Sally", "Smith");
var tableData = new TableRowData[] { parentRowData, childRowData };
+ var table = Render();
- var ex = Record.Exception(() => { RenderWithPropertySet, TableRowData>(x => x.Data, tableData); });
+ var ex = Record.ExceptionAsync(async () => { await table.Instance.SetDataAsync(tableData); });
- Assert.Null(ex);
+ Assert.Null(ex.Result);
}
[Fact]
- public void NimbleTable_GivenUnsupportedData_ThrowsJsonException()
+ public async Task NimbleTable_GivenUnsupportedData_ThrowsJsonExceptionAsync()
{
var parentRowData = new BadTableRowData("Bob", "Smith");
var childRowData = new BadTableRowData("Sally", "Smith", parentRowData);
var tableData = new BadTableRowData[] { parentRowData, childRowData };
+ var table = Render();
- var ex = Record.Exception(() => { RenderWithPropertySet, BadTableRowData>(x => x.Data, tableData); });
+ var ex = Record.ExceptionAsync(async () => { await table.Instance.SetDataAsync(tableData); });
- Assert.IsType(ex);
+ Assert.IsType(ex.Result);
}
private IRenderedComponent> RenderWithPropertySet(Expression, TProperty>> propertyGetter, TProperty propertyValue)
@@ -135,4 +138,11 @@ private IRenderedComponent> RenderWithPropertySet>(p => p.Add(propertyGetter, propertyValue));
}
+
+ private IRenderedComponent> Render()
+ {
+ var context = new TestContext();
+ context.JSInterop.Mode = JSRuntimeMode.Loose;
+ return context.RenderComponent>();
+ }
}
diff --git a/packages/nimble-blazor/Tests/NimbleBlazor.Tests/Unit/Components/NimbleWaferMapTests.cs b/packages/nimble-blazor/Tests/NimbleBlazor.Tests/Unit/Components/NimbleWaferMapTests.cs
new file mode 100644
index 0000000000..78435da920
--- /dev/null
+++ b/packages/nimble-blazor/Tests/NimbleBlazor.Tests/Unit/Components/NimbleWaferMapTests.cs
@@ -0,0 +1,113 @@
+using System;
+using System.Linq.Expressions;
+using Bunit;
+using Xunit;
+
+namespace NimbleBlazor.Tests.Unit.Components;
+
+///
+/// Tests for .
+///
+public class NimbleWaferMapTests
+{
+ [Fact]
+ public void NimbleWaferMap_WithGridMinXAttribute_HasMarkup()
+ {
+ var waferMap = RenderWithPropertySet(x => x.GridMinX!, 3);
+
+ var expectedMarkup = "grid-min-x=\"3\"";
+ Assert.Contains(expectedMarkup, waferMap.Markup);
+ }
+
+ [Fact]
+ public void NimbleWaferMap_WithGridMaxXAttribute_HasMarkup()
+ {
+ var waferMap = RenderWithPropertySet(x => x.GridMaxX!, 3);
+
+ var expectedMarkup = "grid-max-x=\"3\"";
+ Assert.Contains(expectedMarkup, waferMap.Markup);
+ }
+
+ [Fact]
+ public void NimbleWaferMap_WithGridMinYAttribute_HasMarkup()
+ {
+ var waferMap = RenderWithPropertySet(x => x.GridMinY!, 3);
+
+ var expectedMarkup = "grid-min-y=\"3\"";
+ Assert.Contains(expectedMarkup, waferMap.Markup);
+ }
+
+ [Fact]
+ public void NimbleWaferMap_WithGridMaxYAttribute_HasMarkup()
+ {
+ var waferMap = RenderWithPropertySet(x => x.GridMaxY!, 3);
+
+ var expectedMarkup = "grid-max-y=\"3\"";
+ Assert.Contains(expectedMarkup, waferMap.Markup);
+ }
+
+ [Fact]
+ public void NimbleWaferMap_WithDieLabelsHiddenAttribute_HasMarkup()
+ {
+ var waferMap = RenderWithPropertySet(x => x.DieLabelsHidden!, true);
+
+ var expectedMarkup = "die-labels-hidden";
+ Assert.Contains(expectedMarkup, waferMap.Markup);
+ }
+
+ [Fact]
+ public void NimbleWaferMap_WithDieLabelsSuffixAttribute_HasMarkup()
+ {
+ var waferMap = RenderWithPropertySet(x => x.DieLabelsSuffix!, "a");
+
+ var expectedMarkup = "die-labels-suffix=\"a\"";
+ Assert.Contains(expectedMarkup, waferMap.Markup);
+ }
+
+ [Fact]
+ public void NimbleWaferMap_WithMaxCharactersAttribute_HasMarkup()
+ {
+ var waferMap = RenderWithPropertySet(x => x.MaxCharacters!, 3);
+
+ var expectedMarkup = "max-characters=\"3\"";
+ Assert.Contains(expectedMarkup, waferMap.Markup);
+ }
+
+ [Fact]
+ public void NimbleWaferMap_WithColorScaleModeAttribute_HasMarkup()
+ {
+ var waferMap = RenderWithPropertySet(x => x.ColorScaleMode!, WaferMapColorScaleMode.Ordinal);
+
+ var expectedMarkup = "ordinal";
+ Assert.Contains(expectedMarkup, waferMap.Markup);
+ }
+
+ [Theory]
+ [InlineData(WaferMapOriginLocation.TopRight, "top-right")]
+ [InlineData(WaferMapOriginLocation.TopLeft, "top-left")]
+ [InlineData(WaferMapOriginLocation.BottomRight, "bottom-right")]
+ public void NimbleWaferMap_WaferMapOriginLocation_AttributeIsSet(WaferMapOriginLocation value, string expectedAttribute)
+ {
+ var waferMap = RenderWithPropertySet(x => x.OriginLocation!, value);
+
+ Assert.Contains(expectedAttribute, waferMap.Markup);
+ }
+
+ [Theory]
+ [InlineData(WaferMapOrientation.Bottom, "bottom")]
+ [InlineData(WaferMapOrientation.Left, "left")]
+ [InlineData(WaferMapOrientation.Right, "right")]
+ public void NimbleWaferMap_WaferMapOrientation_AttributeIsSet(WaferMapOrientation value, string expectedAttribute)
+ {
+ var waferMap = RenderWithPropertySet(x => x.Orientation!, value);
+
+ Assert.Contains(expectedAttribute, waferMap.Markup);
+ }
+
+ private IRenderedComponent RenderWithPropertySet(Expression> propertyGetter, TProperty propertyValue)
+ {
+ var context = new TestContext();
+ context.JSInterop.Mode = JSRuntimeMode.Loose;
+ return context.RenderComponent(p => p.Add(propertyGetter, propertyValue));
+ }
+}
diff --git a/packages/nimble-blazor/package.json b/packages/nimble-blazor/package.json
index fd6b70cfd3..dcb5bf529b 100644
--- a/packages/nimble-blazor/package.json
+++ b/packages/nimble-blazor/package.json
@@ -1,6 +1,6 @@
{
"name": "@ni/nimble-blazor",
- "version": "13.2.2",
+ "version": "14.3.2",
"description": "Blazor components for the NI Nimble Design System",
"scripts": {
"postinstall": "node build/generate-playwright-version-properties/source/index.js",
@@ -19,7 +19,8 @@
"format:js": "eslint . --fix",
"test": "dotnet test -c Release",
"pack": "cross-env-shell dotnet pack NimbleBlazor -c Release -p:PackageVersion=$npm_package_version --output dist",
- "invoke-publish": "npm run invoke-publish:nuget && npm run invoke-publish:npm -- ",
+ "invoke-publish": "npm run invoke-publish:clean-nuget && npm run pack && npm run invoke-publish:nuget && npm run invoke-publish:npm -- ",
+ "invoke-publish:clean-nuget": "rimraf --glob \"dist/*.nupkg\"",
"invoke-publish:nuget": "cross-env-shell dotnet nuget push \"dist/*.nupkg\" -k $NUGET_SECRET_TOKEN -s \"https://api.nuget.org/v3/index.json\"",
"invoke-publish:npm": "echo \"noop command to swallow npm args\"",
"copy-resources": "node build/copyNimbleResources.js"
@@ -44,10 +45,11 @@
"@ni/eslint-config-javascript": "^4.2.0",
"@ni/nimble-components": "*",
"@ni/nimble-tokens": "*",
- "playwright": "1.40.0",
"@rollup/plugin-node-resolve": "^15.0.1",
"cross-env": "^7.0.3",
"glob": "^8.1.0",
+ "playwright": "1.40.0",
+ "rimraf": "^5.0.5",
"rollup": "^3.10.1"
}
}
diff --git a/packages/nimble-components/.storybook/blocks/README.md b/packages/nimble-components/.storybook/blocks/README.md
index b6ef589631..38f32aa404 100644
--- a/packages/nimble-components/.storybook/blocks/README.md
+++ b/packages/nimble-components/.storybook/blocks/README.md
@@ -36,6 +36,18 @@ This folder contains reusable React components used in Nimble component MDX docu
- Renders a frame to match visual design of existing Storybook Doc blocks.
+### Tag
+
+- Renders a tag string in code blocks. Optionally surrounding it with angle brackets to show as HTML elements.
+
+ | Property | Example |
+ | --------- | --------------------------------------- |
+ | _default_ | `nimble-component` |
+ | open | `` |
+ | openClose | `` |
+ | close | `` |
+ | selfClose | `` |
+
## Usage
Compose the components to create clear and attractive usage documentation.
diff --git a/packages/nimble-components/.storybook/blocks/StoryLayout.tsx b/packages/nimble-components/.storybook/blocks/StoryLayout.tsx
index 6da477c176..a6f78c709f 100644
--- a/packages/nimble-components/.storybook/blocks/StoryLayout.tsx
+++ b/packages/nimble-components/.storybook/blocks/StoryLayout.tsx
@@ -61,3 +61,20 @@ export const Dont = ({ children }) => {
);
};
+
+/**
+ * Renders a "Tag" component for Storybook documentation.
+ */
+export const Tag = ({ name, open, openClose, close, selfClose }) => {
+ if (open) {
+ return (<{name}>
);
+ } else if (close) {
+ return (</{name}>
);
+ } else if (openClose) {
+ return (<{name}></{name}>
);
+ } else if (selfClose) {
+ return (<{name}/>
);
+ } else {
+ return ({name}
);
+ }
+};
\ No newline at end of file
diff --git a/packages/nimble-components/.storybook/blocks/story-layout.css b/packages/nimble-components/.storybook/blocks/story-layout.css
index 9389961d10..a0c4ca4ade 100644
--- a/packages/nimble-components/.storybook/blocks/story-layout.css
+++ b/packages/nimble-components/.storybook/blocks/story-layout.css
@@ -15,6 +15,7 @@
}
.story-layout-frame {
+ margin-bottom: 32px !important;
border: 1px solid rgba(128, 128, 128, 0.2);
border-radius: 4px;
box-shadow: 0px 1px 4px rgba(128, 128, 128, 0.22);
diff --git a/packages/nimble-components/.storybook/main.js b/packages/nimble-components/.storybook/main.js
index 5948ba3db1..b342bf962e 100644
--- a/packages/nimble-components/.storybook/main.js
+++ b/packages/nimble-components/.storybook/main.js
@@ -8,7 +8,8 @@ export const addons = [
{
name: '@storybook/addon-essentials',
options: {
- outline: false
+ outline: false,
+ docs: false
}
},
{
diff --git a/packages/nimble-components/.storybook/preview.js b/packages/nimble-components/.storybook/preview.js
index cd096d1919..35547f1fbe 100644
--- a/packages/nimble-components/.storybook/preview.js
+++ b/packages/nimble-components/.storybook/preview.js
@@ -12,7 +12,8 @@ import {
Do,
Dont,
Frame,
- Divider
+ Divider,
+ Tag
} from './blocks/StoryLayout.tsx';
export const parameters = {
@@ -54,7 +55,8 @@ export const parameters = {
Do,
Dont,
Frame,
- Divider
+ Divider,
+ Tag
}
}
};
diff --git a/packages/nimble-components/CHANGELOG.json b/packages/nimble-components/CHANGELOG.json
index e5fa24b222..4bec04eb90 100644
--- a/packages/nimble-components/CHANGELOG.json
+++ b/packages/nimble-components/CHANGELOG.json
@@ -1,6 +1,429 @@
{
"name": "@ni/nimble-components",
"entries": [
+ {
+ "date": "Thu, 22 Feb 2024 21:27:19 GMT",
+ "version": "21.6.3",
+ "tag": "@ni/nimble-components_v21.6.3",
+ "comments": {
+ "none": [
+ {
+ "author": "rajsite@users.noreply.github.com",
+ "package": "@ni/nimble-components",
+ "commit": "84480ae94997da7240feba4e19d73e42cadcc9f6",
+ "comment": "Remove code element from stories used for formatting"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Thu, 22 Feb 2024 19:42:35 GMT",
+ "version": "21.6.3",
+ "tag": "@ni/nimble-components_v21.6.3",
+ "comments": {
+ "patch": [
+ {
+ "author": "7282195+m-akinc@users.noreply.github.com",
+ "package": "@ni/nimble-components",
+ "commit": "70e9234b636b0a2e67a658ecef06008ac8e96fa2",
+ "comment": "Clean up dialog/drawer properly if cancel event skipped"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Thu, 22 Feb 2024 01:13:03 GMT",
+ "version": "21.6.2",
+ "tag": "@ni/nimble-components_v21.6.2",
+ "comments": {
+ "patch": [
+ {
+ "author": "beachball",
+ "package": "@ni/nimble-components",
+ "comment": "Bump @ni/nimble-tokens to v6.11.1",
+ "commit": "not available"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Wed, 21 Feb 2024 23:32:45 GMT",
+ "version": "21.6.1",
+ "tag": "@ni/nimble-components_v21.6.1",
+ "comments": {
+ "patch": [
+ {
+ "author": "20542556+mollykreis@users.noreply.github.com",
+ "package": "@ni/nimble-components",
+ "commit": "ff99d91bbe3dd65e8595770c9ac05cbb8ad5282d",
+ "comment": "Update `TableRecordDelayedHierarchyState` enum values to follow kebab casing convention"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Wed, 21 Feb 2024 21:18:20 GMT",
+ "version": "21.6.0",
+ "tag": "@ni/nimble-components_v21.6.0",
+ "comments": {
+ "minor": [
+ {
+ "author": "1458528+fredvisser@users.noreply.github.com",
+ "package": "@ni/nimble-components",
+ "commit": "4037b7c13da51926e6fbfd774a4da19cd75cb98e",
+ "comment": "Add bodyEmphasizedPlus1Font tokens"
+ },
+ {
+ "author": "beachball",
+ "package": "@ni/nimble-components",
+ "comment": "Bump @ni/nimble-tokens to v6.11.0",
+ "commit": "not available"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Wed, 21 Feb 2024 19:48:20 GMT",
+ "version": "21.5.5",
+ "tag": "@ni/nimble-components_v21.5.5",
+ "comments": {
+ "patch": [
+ {
+ "author": "beachball",
+ "package": "@ni/nimble-components",
+ "comment": "Bump @ni/jasmine-parameterized to v0.2.2",
+ "commit": "not available"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Tue, 20 Feb 2024 17:54:12 GMT",
+ "version": "21.5.4",
+ "tag": "@ni/nimble-components_v21.5.4",
+ "comments": {
+ "patch": [
+ {
+ "author": "20542556+mollykreis@users.noreply.github.com",
+ "package": "@ni/nimble-components",
+ "commit": "c6c16de04b8387dbb999c62fbc3f35a2b3643427",
+ "comment": "Fix bug where nesting level of rows sometimes gets out of sync with data"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Mon, 19 Feb 2024 22:25:59 GMT",
+ "version": "21.5.3",
+ "tag": "@ni/nimble-components_v21.5.3",
+ "comments": {
+ "patch": [
+ {
+ "author": "20542556+mollykreis@users.noreply.github.com",
+ "package": "@ni/nimble-components",
+ "commit": "28825d16c421175868f88992be64874e0d74b655",
+ "comment": "Fix bug where setting underline-hidden=false on the anchor column would sometimes still hide the underline"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Mon, 19 Feb 2024 19:14:51 GMT",
+ "version": "21.5.2",
+ "tag": "@ni/nimble-components_v21.5.2",
+ "comments": {
+ "patch": [
+ {
+ "author": "20542556+mollykreis@users.noreply.github.com",
+ "package": "@ni/nimble-components",
+ "commit": "15a30c5a9c04fae9c3f6d2eadb0b31056c87db56",
+ "comment": "Allow a row with a state of `canLoadChildren` to be expanded when it does not have children"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Fri, 16 Feb 2024 18:10:59 GMT",
+ "version": "21.5.1",
+ "tag": "@ni/nimble-components_v21.5.1",
+ "comments": {
+ "patch": [
+ {
+ "author": "7282195+m-akinc@users.noreply.github.com",
+ "package": "@ni/nimble-components",
+ "commit": "2b6c327fd3bc4f49178ac370f695a8b941278c46",
+ "comment": "Inline sources into map files"
+ },
+ {
+ "author": "beachball",
+ "package": "@ni/nimble-components",
+ "comment": "Bump @ni/nimble-tokens to v6.10.1",
+ "commit": "not available"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Thu, 15 Feb 2024 20:48:52 GMT",
+ "version": "21.5.0",
+ "tag": "@ni/nimble-components_v21.5.0",
+ "comments": {
+ "minor": [
+ {
+ "author": "26874831+atmgrifter00@users.noreply.github.com",
+ "package": "@ni/nimble-components",
+ "commit": "f2ecb4e69a8c9569d9e19cacfa0533b8b170b3f9",
+ "comment": "Adding filter input to Select"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Thu, 15 Feb 2024 01:01:57 GMT",
+ "version": "21.4.0",
+ "tag": "@ni/nimble-components_v21.4.0",
+ "comments": {
+ "none": [
+ {
+ "author": "7282195+m-akinc@users.noreply.github.com",
+ "package": "@ni/nimble-components",
+ "commit": "19c97b51b82b32b95517a914d43a028860f1279a",
+ "comment": "Add publishConfig key to package files"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Wed, 14 Feb 2024 21:03:17 GMT",
+ "version": "21.4.0",
+ "tag": "@ni/nimble-components_v21.4.0",
+ "comments": {
+ "minor": [
+ {
+ "author": "20542556+mollykreis@users.noreply.github.com",
+ "package": "@ni/nimble-components",
+ "commit": "b3fc70ec6857d24fcc257eeab7697ac89c663a55",
+ "comment": "Add `loadingChildren` delayed hierarchy state"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Tue, 13 Feb 2024 23:45:09 GMT",
+ "version": "21.3.3",
+ "tag": "@ni/nimble-components_v21.3.3",
+ "comments": {
+ "none": [
+ {
+ "author": "rajsite@users.noreply.github.com",
+ "package": "@ni/nimble-components",
+ "commit": "d9255477187a8fc95fff5fea7f1418b2b76cf93d",
+ "comment": "Pin react wrapper version"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Tue, 13 Feb 2024 21:22:58 GMT",
+ "version": "21.3.3",
+ "tag": "@ni/nimble-components_v21.3.3",
+ "comments": {
+ "patch": [
+ {
+ "author": "7282195+m-akinc@users.noreply.github.com",
+ "package": "@ni/nimble-components",
+ "commit": "0180d30c8833d50eb529794c362cc476f305f88d",
+ "comment": "Fix performance issue for table with many grouped columns"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Tue, 13 Feb 2024 17:44:00 GMT",
+ "version": "21.3.2",
+ "tag": "@ni/nimble-components_v21.3.2",
+ "comments": {
+ "patch": [
+ {
+ "author": "123377523+vivinkrishna-ni@users.noreply.github.com",
+ "package": "@ni/nimble-components",
+ "commit": "34efe55087a58bb99e4aa42873512946164b47d2",
+ "comment": "Update dependency on Tiptap extensions to fix mention issue in rich text editor"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Mon, 12 Feb 2024 13:48:12 GMT",
+ "version": "21.3.1",
+ "tag": "@ni/nimble-components_v21.3.1",
+ "comments": {
+ "none": [
+ {
+ "author": "33986780+munteannatan@users.noreply.github.com",
+ "package": "@ni/nimble-components",
+ "commit": "fd66105bfc34419b0ccab692d3d21669970eebfc",
+ "comment": "New rendering HLD for the Wafer Map Component"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Fri, 09 Feb 2024 22:59:31 GMT",
+ "version": "21.3.1",
+ "tag": "@ni/nimble-components_v21.3.1",
+ "comments": {
+ "none": [
+ {
+ "author": "rajsite@users.noreply.github.com",
+ "package": "@ni/nimble-components",
+ "commit": "8032637eb7eb28f455a712ed74c9119b21f55fc9",
+ "comment": "Create fastParameters for icons and status table"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Fri, 09 Feb 2024 21:58:13 GMT",
+ "version": "21.3.1",
+ "tag": "@ni/nimble-components_v21.3.1",
+ "comments": {
+ "none": [
+ {
+ "author": "1458528+fredvisser@users.noreply.github.com",
+ "package": "@ni/nimble-components",
+ "commit": "77d91358a4e7b3d6581bcecb7a301b023a3face1",
+ "comment": "Update import of component to match other layout components"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Fri, 09 Feb 2024 02:02:53 GMT",
+ "version": "21.3.1",
+ "tag": "@ni/nimble-components_v21.3.1",
+ "comments": {
+ "patch": [
+ {
+ "author": "7282195+m-akinc@users.noreply.github.com",
+ "package": "@ni/nimble-components",
+ "commit": "e68df6d348dcaa664e87db35725f9d30f2d2dca5",
+ "comment": "Update Storybook to 7.6.13"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Thu, 08 Feb 2024 19:42:05 GMT",
+ "version": "21.3.0",
+ "tag": "@ni/nimble-components_v21.3.0",
+ "comments": {
+ "none": [
+ {
+ "author": "33986780+munteannatan@users.noreply.github.com",
+ "package": "@ni/nimble-components",
+ "commit": "5c8cf7be5e962dee7518e9e27ccf2a14315d84a7",
+ "comment": "Updated Wafer Map Blazor Component Status"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Wed, 07 Feb 2024 21:48:45 GMT",
+ "version": "21.3.0",
+ "tag": "@ni/nimble-components_v21.3.0",
+ "comments": {
+ "minor": [
+ {
+ "author": "20542556+mollykreis@users.noreply.github.com",
+ "package": "@ni/nimble-components",
+ "commit": "8d58b0180a3fb2c5a837718a6da3489b67d821bc",
+ "comment": "Extend nimble-table API to allow specifying that a record has children that can be loaded when the row is expanded"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Wed, 07 Feb 2024 19:06:22 GMT",
+ "version": "21.2.1",
+ "tag": "@ni/nimble-components_v21.2.1",
+ "comments": {
+ "patch": [
+ {
+ "author": "33986780+munteannatan@users.noreply.github.com",
+ "package": "@ni/nimble-components",
+ "commit": "9fdd20db5366e435fc4576d5a4881959fa143e30",
+ "comment": "fixed wafer map component test warnings"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Wed, 07 Feb 2024 10:17:13 GMT",
+ "version": "21.2.0",
+ "tag": "@ni/nimble-components_v21.2.0",
+ "comments": {
+ "none": [
+ {
+ "author": "110180309+Razvan1928@users.noreply.github.com",
+ "package": "@ni/nimble-components",
+ "commit": "712034c7f2fb25979165f022149c4e31e9b787bd",
+ "comment": "Add storybook links to nimble-components intro page"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Mon, 05 Feb 2024 18:16:28 GMT",
+ "version": "21.2.0",
+ "tag": "@ni/nimble-components_v21.2.0",
+ "comments": {
+ "none": [
+ {
+ "author": "1458528+fredvisser@users.noreply.github.com",
+ "package": "@ni/nimble-components",
+ "commit": "5c681c75a8c40ea060d961da29df59e0f0a550fc",
+ "comment": "Migrate Storybook docs to MDX format"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Sun, 04 Feb 2024 04:31:52 GMT",
+ "version": "21.2.0",
+ "tag": "@ni/nimble-components_v21.2.0",
+ "comments": {
+ "none": [
+ {
+ "author": "123377523+vivinkrishna-ni@users.noreply.github.com",
+ "package": "@ni/nimble-components",
+ "commit": "c36b37f3ab227bc7422a059e332cca42e63ea5ea",
+ "comment": "Re-enable intermittent test in rich text editor for mention support"
+ }
+ ]
+ }
+ },
+ {
+ "date": "Fri, 02 Feb 2024 21:51:03 GMT",
+ "version": "21.2.0",
+ "tag": "@ni/nimble-components_v21.2.0",
+ "comments": {
+ "minor": [
+ {
+ "author": "1458528+fredvisser@users.noreply.github.com",
+ "package": "@ni/nimble-components",
+ "commit": "8ba4f05ec0bd828265d2f74b16a278adf46db268",
+ "comment": "New pause, play, stop, and external link icons. Updated running-arrow icon."
+ },
+ {
+ "author": "beachball",
+ "package": "@ni/nimble-components",
+ "comment": "Bump @ni/nimble-tokens to v6.10.0",
+ "commit": "not available"
+ }
+ ]
+ }
+ },
{
"date": "Wed, 31 Jan 2024 20:22:58 GMT",
"version": "21.1.0",
diff --git a/packages/nimble-components/CHANGELOG.md b/packages/nimble-components/CHANGELOG.md
index f0c6536617..b397462582 100644
--- a/packages/nimble-components/CHANGELOG.md
+++ b/packages/nimble-components/CHANGELOG.md
@@ -1,9 +1,148 @@
# Change Log - @ni/nimble-components
-This log was last generated on Wed, 31 Jan 2024 20:22:58 GMT and should not be manually modified.
+This log was last generated on Thu, 22 Feb 2024 19:42:35 GMT and should not be manually modified.
+## 21.6.3
+
+Thu, 22 Feb 2024 19:42:35 GMT
+
+### Patches
+
+- Clean up dialog/drawer properly if cancel event skipped ([ni/nimble@70e9234](https://github.com/ni/nimble/commit/70e9234b636b0a2e67a658ecef06008ac8e96fa2))
+
+## 21.6.2
+
+Thu, 22 Feb 2024 01:13:03 GMT
+
+### Patches
+
+- Bump @ni/nimble-tokens to v6.11.1
+
+## 21.6.1
+
+Wed, 21 Feb 2024 23:32:45 GMT
+
+### Patches
+
+- Update `TableRecordDelayedHierarchyState` enum values to follow kebab casing convention ([ni/nimble@ff99d91](https://github.com/ni/nimble/commit/ff99d91bbe3dd65e8595770c9ac05cbb8ad5282d))
+
+## 21.6.0
+
+Wed, 21 Feb 2024 21:18:20 GMT
+
+### Minor changes
+
+- Add bodyEmphasizedPlus1Font tokens ([ni/nimble@4037b7c](https://github.com/ni/nimble/commit/4037b7c13da51926e6fbfd774a4da19cd75cb98e))
+- Bump @ni/nimble-tokens to v6.11.0
+
+## 21.5.5
+
+Wed, 21 Feb 2024 19:48:20 GMT
+
+### Patches
+
+- Bump @ni/jasmine-parameterized to v0.2.2
+
+## 21.5.4
+
+Tue, 20 Feb 2024 17:54:12 GMT
+
+### Patches
+
+- Fix bug where nesting level of rows sometimes gets out of sync with data ([ni/nimble@c6c16de](https://github.com/ni/nimble/commit/c6c16de04b8387dbb999c62fbc3f35a2b3643427))
+
+## 21.5.3
+
+Mon, 19 Feb 2024 22:25:59 GMT
+
+### Patches
+
+- Fix bug where setting underline-hidden=false on the anchor column would sometimes still hide the underline ([ni/nimble@28825d1](https://github.com/ni/nimble/commit/28825d16c421175868f88992be64874e0d74b655))
+
+## 21.5.2
+
+Mon, 19 Feb 2024 19:14:51 GMT
+
+### Patches
+
+- Allow a row with a state of `canLoadChildren` to be expanded when it does not have children ([ni/nimble@15a30c5](https://github.com/ni/nimble/commit/15a30c5a9c04fae9c3f6d2eadb0b31056c87db56))
+
+## 21.5.1
+
+Fri, 16 Feb 2024 18:10:59 GMT
+
+### Patches
+
+- Inline sources into map files ([ni/nimble@2b6c327](https://github.com/ni/nimble/commit/2b6c327fd3bc4f49178ac370f695a8b941278c46))
+- Bump @ni/nimble-tokens to v6.10.1
+
+## 21.5.0
+
+Thu, 15 Feb 2024 20:48:52 GMT
+
+### Minor changes
+
+- Adding filter input to Select ([ni/nimble@f2ecb4e](https://github.com/ni/nimble/commit/f2ecb4e69a8c9569d9e19cacfa0533b8b170b3f9))
+
+## 21.4.0
+
+Wed, 14 Feb 2024 21:03:17 GMT
+
+### Minor changes
+
+- Add `loadingChildren` delayed hierarchy state ([ni/nimble@b3fc70e](https://github.com/ni/nimble/commit/b3fc70ec6857d24fcc257eeab7697ac89c663a55))
+
+## 21.3.3
+
+Tue, 13 Feb 2024 21:22:58 GMT
+
+### Patches
+
+- Fix performance issue for table with many grouped columns ([ni/nimble@0180d30](https://github.com/ni/nimble/commit/0180d30c8833d50eb529794c362cc476f305f88d))
+
+## 21.3.2
+
+Tue, 13 Feb 2024 17:44:00 GMT
+
+### Patches
+
+- Update dependency on Tiptap extensions to fix mention issue in rich text editor ([ni/nimble@34efe55](https://github.com/ni/nimble/commit/34efe55087a58bb99e4aa42873512946164b47d2))
+
+## 21.3.1
+
+Fri, 09 Feb 2024 02:02:53 GMT
+
+### Patches
+
+- Update Storybook to 7.6.13 ([ni/nimble@e68df6d](https://github.com/ni/nimble/commit/e68df6d348dcaa664e87db35725f9d30f2d2dca5))
+
+## 21.3.0
+
+Wed, 07 Feb 2024 21:48:45 GMT
+
+### Minor changes
+
+- Extend nimble-table API to allow specifying that a record has children that can be loaded when the row is expanded ([ni/nimble@8d58b01](https://github.com/ni/nimble/commit/8d58b0180a3fb2c5a837718a6da3489b67d821bc))
+
+## 21.2.1
+
+Wed, 07 Feb 2024 19:06:22 GMT
+
+### Patches
+
+- fixed wafer map component test warnings ([ni/nimble@9fdd20d](https://github.com/ni/nimble/commit/9fdd20db5366e435fc4576d5a4881959fa143e30))
+
+## 21.2.0
+
+Fri, 02 Feb 2024 21:51:03 GMT
+
+### Minor changes
+
+- New pause, play, stop, and external link icons. Updated running-arrow icon. ([ni/nimble@8ba4f05](https://github.com/ni/nimble/commit/8ba4f05ec0bd828265d2f74b16a278adf46db268))
+- Bump @ni/nimble-tokens to v6.10.0
+
## 21.1.0
Wed, 31 Jan 2024 20:22:58 GMT
diff --git a/packages/nimble-components/CONTRIBUTING.md b/packages/nimble-components/CONTRIBUTING.md
index 5df982ed6c..607712adff 100644
--- a/packages/nimble-components/CONTRIBUTING.md
+++ b/packages/nimble-components/CONTRIBUTING.md
@@ -35,7 +35,7 @@ From the `nimble` directory:
Before building a new component, 3 specification documents need to be created:
1. An interaction design (IxD) spec to get agreement on the component's behavior and other core requirements. The spec process is described in the [`/specs` folder](/specs/README.md).
-2. A visual design (ViD) spec to get agreement on the component's appearance, spacing, icons, and tokens. The visual design spec can be created in Adobe XD or Figma, and linked to the component work item and Storybook documentation. See [Tips for using Adobe XD to inspect component designs](/packages/nimble-components/docs/xd-tips.md) to learn more about how to navigate these specs.
+2. A visual design (ViD) spec to get agreement on the component's appearance, spacing, icons, and tokens. The visual design spec should be created in Figma and linked to the component work item and Storybook [Component Status](https://nimble.ni.dev/storybook/?path=/docs/component-status--docs) page.
3. A technical design spec to get agreement on the component's behavior, API, and high-level implementation. The spec process is described in the [`/specs` folder](/specs/README.md).
## Development workflow
@@ -122,7 +122,7 @@ Create a new folder named after your component with some core files:
| tests/component-name.spec.ts | Unit tests for this component. Covers behaviors added to components on top of existing Foundation behaviors or behavior of new components. |
| tests/component-name.stories.ts | Contains the component hosted in Storybook. This provides a live component view for development and testing. In the future, this will also provide API documentation. |
| tests/component-name-matrix.stories.ts | Contains a story that shows all component states for all themes hosted in Storybook. This is used by Chromatic visual tests to verify styling changes across all themes and states. |
-| tests/component-name-docs.stories.ts | Contains the Storybook documentation for this component. This should provide design guidance and usage information. See [Creating Storybook Component Documentation](/packages/nimble-components/docs/creating-storybook-component-documentation.md) for more information. |
+| tests/component-name.mdx | Contains the Storybook documentation for this component. This should provide design guidance and usage information. See [Creating Storybook Component Documentation](/packages/nimble-components/docs/creating-storybook-component-documentation.md) for more information. |
| tests/component-name.react.tsx | Simple React wrapper for the component to be used in Storybook MDX documentation |
### Add to component bundle
@@ -476,7 +476,17 @@ Component custom element names are specified in `index.ts` when registering the
3. **variant** can be used to distinguish alternate configurations of one presentation. For example, `anchor-`, `card-`, `menu-`, and `toggle-` are all variants of the `button` presentation. The primary configuration can omit the `variant` segment (e.g. `nimble-button`).
4. **presentation** describes the visual presentation of the component. For example, `button`, `tab`, or `text-field`.
-## Token naming
+## Theme-aware tokens
+
+Nimble maps [base tokens](/packages/nimble-tokens/CONTRIBUTING.md#editing-base-tokens) to theme-aware tokens which are then used to style components. These tokens automatically adjust to the theme set by the `theme-provider` and relate to specific contexts or components.
+
+To modify the generated tokens, complete these steps:
+
+1. Edit the `design-tokens*` typescript files in `src/theme-provider/`.
+2. Rebuild the generated token files by running the repository's build command, `npm run build`.
+3. Test your changes locally and create a PR using the normal process.
+
+### Naming
Public names for theme-aware tokens are specified in `src/theme-provider/design-token-names.ts`. Use the following structure when creating new tokens.
@@ -486,3 +496,15 @@ Public names for theme-aware tokens are specified in `src/theme-provider/design-
2. Where **part** is the specific part of the element to which the token applies (e.g. 'border', 'background', or shadow).
3. Where **state** is the more specific state descriptor (e.g. 'selected' or 'disabled'). Multiple states should be sorted alphabetically.
4. Where **token_type** is the token category (e.g. 'color', 'font', 'font-color', 'height', 'width', or 'size').
+
+### Size ramp
+
+For tokens with multiple sizes, use the following structure for **element** names. E.g. for `title`:
+
+| Element name |
+| ------------- |
+| title-plus-2 |
+| title-plus-1 |
+| title |
+| title-minus-1 |
+| title-minus-2 |
diff --git a/packages/nimble-components/docs/creating-storybook-component-documentation.md b/packages/nimble-components/docs/creating-storybook-component-documentation.md
index 58ef547bfa..6b08eeac53 100644
--- a/packages/nimble-components/docs/creating-storybook-component-documentation.md
+++ b/packages/nimble-components/docs/creating-storybook-component-documentation.md
@@ -9,20 +9,38 @@ From the `nimble` directory:
3. To view the component documentation in Storybook: `npm run storybook -w @ni/nimble-components`
## Documentation Workflow
-Add a `docs.description.component` string to the component `parameters` object. E.g.
-
-```ts
-const metadata: Meta = {
- title: 'SomeComponent',
- parameters: {
- docs: {
- description: {
- component: '**Some component description**'
- }
- },
- ...
+
+Add `component-name.mdx` file in component `test` directory with the following template:
+
+```jsx
+import { Canvas, Meta, Controls, Title } from '@storybook/blocks';
+import { NimbleComponentName } from './component-name.react';
+import * as componentNameStories from './component-name.stories';
+
+
+
+
+*Component description*
+
+
+
+
+## Appearances
+
+## Appearance Variants
+
+## Usage
+
+## Examples
+
+## Accessibility
+
+## Resources
+
```
+Fill out the template with all available information, and comment out any empty sections. E.g. `{/* ## Examples */}`
+
If the component has a [W3C ARIA description](https://www.w3.org/WAI/ARIA/apg/patterns/), consider using that to describe the component purpose.
### Markdown
@@ -44,7 +62,4 @@ All other Markdown formatting is supported. See any [Markdown Cheatsheet](https:
### Testing
-When you run Storybook (See **Getting Started** above), you should see the component description within the **Docs** tab. E.g.
-
-![Storybook DocsPage overview](/packages/nimble-components/docs/images/docs-page-overview.png)
-
+When you run Storybook (See **Getting Started** above), you should see the `component-name.mdx` file rendered as the component **Docs** page.
\ No newline at end of file
diff --git a/packages/nimble-components/docs/images/docs-page-overview.png b/packages/nimble-components/docs/images/docs-page-overview.png
deleted file mode 100644
index 05e315f7ae..0000000000
Binary files a/packages/nimble-components/docs/images/docs-page-overview.png and /dev/null differ
diff --git a/packages/nimble-components/docs/images/xd-appearance.png b/packages/nimble-components/docs/images/xd-appearance.png
deleted file mode 100644
index 7d4705c85a..0000000000
Binary files a/packages/nimble-components/docs/images/xd-appearance.png and /dev/null differ
diff --git a/packages/nimble-components/docs/images/xd-components.png b/packages/nimble-components/docs/images/xd-components.png
deleted file mode 100644
index 4c4d3061fd..0000000000
Binary files a/packages/nimble-components/docs/images/xd-components.png and /dev/null differ
diff --git a/packages/nimble-components/docs/images/xd-home-and-pages.png b/packages/nimble-components/docs/images/xd-home-and-pages.png
deleted file mode 100644
index 7bdbf8d87a..0000000000
Binary files a/packages/nimble-components/docs/images/xd-home-and-pages.png and /dev/null differ
diff --git a/packages/nimble-components/docs/images/xd-parts-menu.png b/packages/nimble-components/docs/images/xd-parts-menu.png
deleted file mode 100644
index b9c666c931..0000000000
Binary files a/packages/nimble-components/docs/images/xd-parts-menu.png and /dev/null differ
diff --git a/packages/nimble-components/docs/images/xd-right-bar.png b/packages/nimble-components/docs/images/xd-right-bar.png
deleted file mode 100644
index 881d899966..0000000000
Binary files a/packages/nimble-components/docs/images/xd-right-bar.png and /dev/null differ
diff --git a/packages/nimble-components/docs/images/xd-states.png b/packages/nimble-components/docs/images/xd-states.png
deleted file mode 100644
index 443b47fdea..0000000000
Binary files a/packages/nimble-components/docs/images/xd-states.png and /dev/null differ
diff --git a/packages/nimble-components/docs/images/xd-zoom.png b/packages/nimble-components/docs/images/xd-zoom.png
deleted file mode 100644
index 5f2290feec..0000000000
Binary files a/packages/nimble-components/docs/images/xd-zoom.png and /dev/null differ
diff --git a/packages/nimble-components/docs/xd-tips.md b/packages/nimble-components/docs/xd-tips.md
deleted file mode 100644
index 3e8d939e53..0000000000
--- a/packages/nimble-components/docs/xd-tips.md
+++ /dev/null
@@ -1,50 +0,0 @@
-# Tips for using Adobe XD to inspect component designs
-
-This describes tips to inspect the visual design for a component in Adobe XD and extract the information needed to develop it in code.
-
-## Finding the design spec
-
-The visual design specs are stored in the [Nimble_Components Adobe XD Document](https://xd.adobe.com/view/33ffad4a-eb2c-4241-b8c5-ebfff1faf6f6-66ac/) (viewable in a web browser or desktop application; these instructions cover the web browser). This is maintained by the NI Visual Design team.
-
-1. To navigate the document, ensure you're in "Comments" mode by clicking the "speech bubble" icon in the right bar.
-
-![Right bar](/packages/nimble-components/docs/images/xd-right-bar.png)
-
-2. To go to the table of contents, click the "home" icon at the bottom of the document.
-
-![Home](/packages/nimble-components/docs/images/xd-home-and-pages.png)
-
-3. From the table of contents, click on the button for a component to navigate to its design specs.
-
-![Components](/packages/nimble-components/docs/images/xd-components.png)
-
-4. Each component is rendered in different themes with different backgrounds. Use the page buttons to navigate between them.
-
-![Pages](/packages/nimble-components/docs/images/xd-home-and-pages.png)
-
-
-
-## Inspecting component parts
-
-1. To inspect component parts, ensure you're in "Specs" mode by clicking the "HTML tag" icon in the right bar.
-
-![Right bar](/packages/nimble-components/docs/images/xd-right-bar.png)
-
-2. Zoom in to see component parts at higher detail.
-
-![Zoom](/packages/nimble-components/docs/images/xd-zoom.png)
-
-3. Select a specific part by right-clicking the part and choosing the layer of interest.
-
-![Parts](/packages/nimble-components/docs/images/xd-parts-menu.png)
-
-4. View the tokens used to construct that part in the "Appearance" section of the right configuration pane. Colors generally map to base token names in `nimble-tokens`. Don't forget to check the opacity value; they often vary from 100%.
-
-![Parts](/packages/nimble-components/docs/images/xd-appearance.png)
-
-5. To view the component in a different state or theme, you can either:
- 1. navigate to that preconfigured copy of the component on one of the theme pages
- 2. configure the component you've already selected to use that state or theme by selecting it from the "Component: Default State" section of the right configuration pane
-
-![States](/packages/nimble-components/docs/images/xd-states.png)
-
diff --git a/packages/nimble-components/package.json b/packages/nimble-components/package.json
index 6a2b3b747a..bc4ceda4b8 100644
--- a/packages/nimble-components/package.json
+++ b/packages/nimble-components/package.json
@@ -1,6 +1,6 @@
{
"name": "@ni/nimble-components",
- "version": "21.1.0",
+ "version": "21.6.3",
"description": "Styled web components for the NI Nimble Design System",
"scripts": {
"build": "npm run generate-icons && npm run build-components && npm run bundle-components && npm run generate-scss && npm run build-storybook",
@@ -48,6 +48,9 @@
"type": "git",
"url": "git+https://github.com/ni/nimble.git"
},
+ "publishConfig": {
+ "access": "public"
+ },
"author": {
"name": "National Instruments"
},
@@ -61,23 +64,23 @@
"@microsoft/fast-element": "^1.12.0",
"@microsoft/fast-foundation": "2.49.4",
"@microsoft/fast-web-utilities": "^6.0.0",
- "@ni/nimble-tokens": "^6.9.1",
+ "@ni/nimble-tokens": "^6.11.1",
"@tanstack/table-core": "^8.10.7",
"@tanstack/virtual-core": "^3.0.0-beta.68",
- "@tiptap/core": "^2.1.6",
- "@tiptap/extension-bold": "^2.1.6",
- "@tiptap/extension-bullet-list": "^2.1.6",
- "@tiptap/extension-document": "^2.1.6",
- "@tiptap/extension-history": "^2.1.6",
- "@tiptap/extension-italic": "^2.1.6",
- "@tiptap/extension-link": "^2.1.6",
- "@tiptap/extension-list-item": "^2.1.6",
- "@tiptap/extension-mention": "^2.1.6",
- "@tiptap/extension-ordered-list": "^2.1.6",
- "@tiptap/extension-paragraph": "^2.1.6",
- "@tiptap/extension-placeholder": "^2.1.6",
- "@tiptap/extension-text": "^2.1.6",
- "@tiptap/extension-hard-break": "^2.1.6",
+ "@tiptap/core": "^2.2.2",
+ "@tiptap/extension-bold": "^2.2.2",
+ "@tiptap/extension-bullet-list": "^2.2.2",
+ "@tiptap/extension-document": "^2.2.2",
+ "@tiptap/extension-history": "^2.2.2",
+ "@tiptap/extension-italic": "^2.2.2",
+ "@tiptap/extension-link": "^2.2.2",
+ "@tiptap/extension-list-item": "^2.2.2",
+ "@tiptap/extension-mention": "^2.2.2",
+ "@tiptap/extension-ordered-list": "^2.2.2",
+ "@tiptap/extension-paragraph": "^2.2.2",
+ "@tiptap/extension-placeholder": "^2.2.2",
+ "@tiptap/extension-text": "^2.2.2",
+ "@tiptap/extension-hard-break": "^2.2.2",
"@types/d3-array": "^3.0.4",
"@types/d3-random": "^3.0.1",
"@types/d3-scale": "^4.0.2",
@@ -97,27 +100,27 @@
"devDependencies": {
"@babel/cli": "^7.13.16",
"@babel/core": "^7.20.12",
- "@microsoft/fast-react-wrapper": "^0.3.20",
+ "@microsoft/fast-react-wrapper": "0.3.22",
"@ni/eslint-config-javascript": "^4.2.0",
"@ni/eslint-config-typescript": "^4.2.0",
- "@ni/jasmine-parameterized": "^0.2.1",
+ "@ni/jasmine-parameterized": "^0.2.2",
"@rollup/plugin-commonjs": "^24.0.1",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-replace": "^5.0.1",
"@rollup/plugin-terser": "^0.4.0",
- "@storybook/addon-a11y": "^7.4.0",
- "@storybook/addon-actions": "^7.4.0",
- "@storybook/addon-docs": "^7.0.4",
- "@storybook/addon-essentials": "^7.4.0",
- "@storybook/addon-interactions": "^7.4.0",
- "@storybook/addon-links": "^7.4.0",
- "@storybook/addons": "^7.4.0",
- "@storybook/cli": "^7.4.0",
- "@storybook/csf": "0.1.1",
- "@storybook/html": "^7.4.0",
- "@storybook/html-webpack5": "^7.4.0",
- "@storybook/theming": "^7.4.0",
+ "@storybook/addon-a11y": "^7.6.13",
+ "@storybook/addon-actions": "^7.6.13",
+ "@storybook/addon-docs": "^7.6.13",
+ "@storybook/addon-essentials": "^7.6.13",
+ "@storybook/addon-interactions": "^7.6.13",
+ "@storybook/addon-links": "^7.6.13",
+ "@storybook/addons": "^7.6.13",
+ "@storybook/cli": "^7.6.13",
+ "@storybook/csf": "0.1.2",
+ "@storybook/html": "^7.6.13",
+ "@storybook/html-webpack5": "^7.6.13",
+ "@storybook/theming": "^7.6.13",
"@types/jasmine": "^4.3.1",
"@types/webpack-env": "^1.15.2",
"babel-loader": "^9.1.2",
@@ -148,7 +151,7 @@
"rollup-plugin-polyfill-node": "^0.12.0",
"rollup-plugin-sourcemaps": "^0.6.3",
"source-map-loader": "^4.0.1",
- "storybook": "^7.4.0",
+ "storybook": "^7.6.13",
"ts-loader": "^9.2.5",
"typescript": "~4.8.2",
"webpack": "^5.75.0",
diff --git a/packages/nimble-components/src/anchor-button/tests/anchor-button-matrix.stories.ts b/packages/nimble-components/src/anchor-button/tests/anchor-button-matrix.stories.ts
index 9484143e7d..8042bcfc73 100644
--- a/packages/nimble-components/src/anchor-button/tests/anchor-button-matrix.stories.ts
+++ b/packages/nimble-components/src/anchor-button/tests/anchor-button-matrix.stories.ts
@@ -7,13 +7,11 @@ import {
} from '../../patterns/button/types';
import {
createMatrix,
- sharedMatrixParameters
+ sharedMatrixParameters,
+ createMatrixThemeStory
} from '../../utilities/tests/matrix';
import { disabledStates, DisabledState } from '../../utilities/tests/states';
-import {
- createMatrixThemeStory,
- createStory
-} from '../../utilities/tests/storybook';
+import { createStory } from '../../utilities/tests/storybook';
import { hiddenWrapper } from '../../utilities/tests/hidden';
import { textCustomizationWrapper } from '../../utilities/tests/text-customization';
import { anchorButtonTag } from '..';
diff --git a/packages/nimble-components/src/anchor-button/tests/anchor-button.mdx b/packages/nimble-components/src/anchor-button/tests/anchor-button.mdx
index c50d500466..f989f03654 100644
--- a/packages/nimble-components/src/anchor-button/tests/anchor-button.mdx
+++ b/packages/nimble-components/src/anchor-button/tests/anchor-button.mdx
@@ -1,6 +1,6 @@
import {
Controls,
- DocsStory,
+ Canvas,
Meta,
Stories,
Title,
@@ -11,14 +11,20 @@ import ContentHiddenDocs from '../../patterns/button/tests/content-hidden-docs.m
import * as anchorButtonStories from './anchor-button.stories';
-
-
-
+An anchor button is a component with the visual appearance of a button, but it navigates like an anchor/link when pressed.
+
+
-# Usage Docs
+{/* ## Appearances */}
+
+{/* ## Appearance Variants */}
+
+{/* ## Usage */}
+
+{/* ## Examples */}
## Target Configuration
@@ -27,3 +33,5 @@ import * as anchorButtonStories from './anchor-button.stories';
## Accessibility
+
+{/* ## Resources */}
diff --git a/packages/nimble-components/src/anchor-button/tests/anchor-button.stories.ts b/packages/nimble-components/src/anchor-button/tests/anchor-button.stories.ts
index 7300a0a96f..d67736d017 100644
--- a/packages/nimble-components/src/anchor-button/tests/anchor-button.stories.ts
+++ b/packages/nimble-components/src/anchor-button/tests/anchor-button.stories.ts
@@ -33,12 +33,6 @@ interface AnchorButtonArgs {
const metadata: Meta = {
title: 'Components/Anchor Button',
parameters: {
- docs: {
- description: {
- component:
- 'An anchor button is a component with the visual appearance of a button, but it navigates like an anchor/link when pressed.'
- }
- },
actions: {}
},
// prettier-ignore
diff --git a/packages/nimble-components/src/anchor-tabs/tests/anchor-tabs-matrix.stories.ts b/packages/nimble-components/src/anchor-tabs/tests/anchor-tabs-matrix.stories.ts
index e312743e1b..7822d04ed5 100644
--- a/packages/nimble-components/src/anchor-tabs/tests/anchor-tabs-matrix.stories.ts
+++ b/packages/nimble-components/src/anchor-tabs/tests/anchor-tabs-matrix.stories.ts
@@ -1,12 +1,10 @@
import type { Meta, Story } from '@storybook/html';
import { html, ViewTemplate, when } from '@microsoft/fast-element';
-import {
- createMatrixThemeStory,
- createStory
-} from '../../utilities/tests/storybook';
+import { createStory } from '../../utilities/tests/storybook';
import {
createMatrix,
- sharedMatrixParameters
+ sharedMatrixParameters,
+ createMatrixThemeStory
} from '../../utilities/tests/matrix';
import { DisabledState, disabledStates } from '../../utilities/tests/states';
import { hiddenWrapper } from '../../utilities/tests/hidden';
diff --git a/packages/nimble-components/src/anchor-tabs/tests/anchor-tabs.mdx b/packages/nimble-components/src/anchor-tabs/tests/anchor-tabs.mdx
index be7cb10ba9..f176c47ade 100644
--- a/packages/nimble-components/src/anchor-tabs/tests/anchor-tabs.mdx
+++ b/packages/nimble-components/src/anchor-tabs/tests/anchor-tabs.mdx
@@ -1,23 +1,26 @@
-import {
- Controls,
- DocsStory,
- Meta,
- Stories,
- Title,
- Description
-} from '@storybook/blocks';
+import { Controls, Canvas, Meta, Title } from '@storybook/blocks';
import TargetDocs from '../../patterns/anchor/tests/target-docs.mdx';
+import { anchorTabTag } from '../../anchor-tab';
import * as anchorTabsStories from './anchor-tabs.stories';
-
-
-
+Anchor tabs are a sequence of links that are styled to look like tab elements, where one link can
+be distinguished as the currently active item. Use this component instead of the standard tabs component when each tab
+represents a different URL to navigate to. Use the standard tabs component when the tabs should switch between different
+tab panels hosted on the same page.
+
+
-# Usage Docs
+{/* ## Appearances */}
+
+{/* ## Appearance Variants */}
+
+{/* ## Usage */}
+
+{/* ## Examples */}
## Target Configuration
@@ -26,5 +29,9 @@ import * as anchorTabsStories from './anchor-tabs.stories';
## Angular Usage
In an Angular application, it is common to integrate with the router by setting `nimbleRouterLink` (rather than `href`)
-on each `nimble-anchor-tab` element. In those cases, it is recommended to also set `replaceUrl="true"` so that switching
+on each element. In those cases, it is recommended to also set `replaceUrl="true"` so that switching
between tabs does not add to the browser history.
+
+{/* ## Accessibility */}
+
+{/* ## Resources */}
diff --git a/packages/nimble-components/src/anchor-tabs/tests/anchor-tabs.stories.ts b/packages/nimble-components/src/anchor-tabs/tests/anchor-tabs.stories.ts
index a74edaaec6..9b2b5f8d60 100644
--- a/packages/nimble-components/src/anchor-tabs/tests/anchor-tabs.stories.ts
+++ b/packages/nimble-components/src/anchor-tabs/tests/anchor-tabs.stories.ts
@@ -13,20 +13,9 @@ interface TabsArgs {
tabDisabled: boolean;
}
-const overviewText = `Anchor tabs are a sequence of links that are styled to look like tab elements, where one link can
-be distinguished as the currently active item. Use this component instead of the standard tabs component when each tab
-represents a different URL to navigate to. Use the standard tabs component when the tabs should switch between different
-tab panels hosted on the same page.`;
-
const metadata: Meta = {
title: 'Components/Anchor Tabs',
- parameters: {
- docs: {
- description: {
- component: overviewText
- }
- }
- },
+ parameters: {},
// prettier-ignore
render: createUserSelectedThemeStory(html`
<${anchorTabsTag} activeid="${x => x.activeId}">
diff --git a/packages/nimble-components/src/anchor/tests/anchor-matrix.stories.ts b/packages/nimble-components/src/anchor/tests/anchor-matrix.stories.ts
index f3d8f634cc..255bc21f20 100644
--- a/packages/nimble-components/src/anchor/tests/anchor-matrix.stories.ts
+++ b/packages/nimble-components/src/anchor/tests/anchor-matrix.stories.ts
@@ -3,12 +3,10 @@ import { html, ViewTemplate } from '@microsoft/fast-element';
import { pascalCase } from '@microsoft/fast-web-utilities';
import {
createMatrix,
- sharedMatrixParameters
+ sharedMatrixParameters,
+ createMatrixThemeStory
} from '../../utilities/tests/matrix';
-import {
- createMatrixThemeStory,
- createStory
-} from '../../utilities/tests/storybook';
+import { createStory } from '../../utilities/tests/storybook';
import { hiddenWrapper } from '../../utilities/tests/hidden';
import { textCustomizationWrapper } from '../../utilities/tests/text-customization';
import { AnchorAppearance } from '../types';
diff --git a/packages/nimble-components/src/anchor/tests/anchor.mdx b/packages/nimble-components/src/anchor/tests/anchor.mdx
index 13466cfa13..c0f9be60c5 100644
--- a/packages/nimble-components/src/anchor/tests/anchor.mdx
+++ b/packages/nimble-components/src/anchor/tests/anchor.mdx
@@ -1,23 +1,26 @@
-import {
- Controls,
- DocsStory,
- Meta,
- Stories,
- Title,
- Description
-} from '@storybook/blocks';
+import { Controls, Canvas, Meta, Title } from '@storybook/blocks';
import TargetDocs from '../../patterns/anchor/tests/target-docs.mdx';
import * as anchorStories from './anchor.stories';
-
-
-
+Per [W3C](https://www.w3.org/WAI/ARIA/apg/patterns/link/), an anchor/link widget provides an interactive reference to a resource. The target resource can be either external or local, i.e., either outside or within the current page or application.
+
+
-# Usage Docs
+{/* ## Appearances */}
+
+{/* ## Appearance Variants */}
+
+{/* ## Usage */}
+
+{/* ## Examples */}
+
+{/* ## Accessibility */}
+
+{/* ## Resources */}
## Target Configuration
diff --git a/packages/nimble-components/src/anchor/tests/anchor.stories.ts b/packages/nimble-components/src/anchor/tests/anchor.stories.ts
index 538fa67512..df80cf379b 100644
--- a/packages/nimble-components/src/anchor/tests/anchor.stories.ts
+++ b/packages/nimble-components/src/anchor/tests/anchor.stories.ts
@@ -22,12 +22,6 @@ interface AnchorArgs {
const metadata: Meta = {
title: 'Components/Anchor',
parameters: {
- docs: {
- description: {
- component:
- 'Per [W3C](https://www.w3.org/WAI/ARIA/apg/patterns/link/), an anchor/link widget provides an interactive reference to a resource. The target resource can be either external or local, i.e., either outside or within the current page or application.'
- }
- },
actions: {}
},
// prettier-ignore
diff --git a/packages/nimble-components/src/anchored-region/styles.ts b/packages/nimble-components/src/anchored-region/styles.ts
index af79d69069..13e03f8203 100644
--- a/packages/nimble-components/src/anchored-region/styles.ts
+++ b/packages/nimble-components/src/anchored-region/styles.ts
@@ -3,8 +3,13 @@ import { ZIndexLevels } from '../utilities/style/types';
export const styles = css`
:host {
- contain: layout;
+ /* Avoid using the 'display' helper to customize hidden behavior */
display: block;
+ contain: layout;
z-index: ${ZIndexLevels.zIndex1000};
}
+
+ :host([hidden]) {
+ visibility: hidden;
+ }
`;
diff --git a/packages/nimble-components/src/anchored-region/tests/anchored-region.mdx b/packages/nimble-components/src/anchored-region/tests/anchored-region.mdx
new file mode 100644
index 0000000000..f83c1237d8
--- /dev/null
+++ b/packages/nimble-components/src/anchored-region/tests/anchored-region.mdx
@@ -0,0 +1,30 @@
+import { Canvas, Meta, Controls, Title } from '@storybook/blocks';
+import { NimbleAnchoredRegion } from './anchored-region.react';
+import * as anchoredRegionStories from './anchored-region.stories';
+
+
+
+
+The anchored region should not generally be used directly by nimble clients. Instead, it is intended to be used within other nimble
+components where one part of the component needs to be dynamically positioned based on another element within the component. For example, the popup menu
+within a menu button or a tooltip.
+
+An anchored region is a container component which enables authors to create layouts where the contents of the anchored region can be positioned relative
+to another "anchor" element. Additionally, the anchored region can react to the available space between the anchor and a parent
+["viewport"](https://developer.mozilla.org/en-US/docs/Glossary/viewport) element such that the region is placed on the side of the anchor with the most
+available space, or even resize itself based on that space.
+
+
+
+
+{/* ## Appearances */}
+
+{/* ## Appearance Variants */}
+
+{/* ## Usage */}
+
+{/* ## Examples */}
+
+{/* ## Accessibility */}
+
+{/* ## Resources */}
diff --git a/packages/nimble-components/src/anchored-region/tests/anchored-region.stories.ts b/packages/nimble-components/src/anchored-region/tests/anchored-region.stories.ts
index d66fe60f53..0f58990bc4 100644
--- a/packages/nimble-components/src/anchored-region/tests/anchored-region.stories.ts
+++ b/packages/nimble-components/src/anchored-region/tests/anchored-region.stories.ts
@@ -14,24 +14,9 @@ interface AnchoredRegionArgs {
verticalPosition: string;
}
-const overviewText = `The anchored region should not generally be used directly by nimble clients. Instead, it is intended to be used within other nimble
-components where one part of the component needs to be dynamically positioned based on another element within the component. For example, the popup menu
-within a menu button or a tooltip.
-
-An anchored region is a container component which enables authors to create layouts where the contents of the anchored region can be positioned relative
-to another "anchor" element. Additionally, the anchored region can react to the available space between the anchor and a parent
-["viewport"](https://developer.mozilla.org/en-US/docs/Glossary/viewport) element such that the region is placed on the side of the anchor with the most
-available space, or even resize itself based on that space.`;
-
const metadata: Meta = {
title: 'Tests/Anchored Region',
- parameters: {
- docs: {
- description: {
- component: overviewText
- }
- }
- },
+ parameters: {},
// prettier-ignore
render: createUserSelectedThemeStory(html`
${x => createTemplate(x.labelProviderTag)}
diff --git a/packages/nimble-components/src/label-provider/core/index.ts b/packages/nimble-components/src/label-provider/core/index.ts
index 4df2722039..f7c901dc12 100644
--- a/packages/nimble-components/src/label-provider/core/index.ts
+++ b/packages/nimble-components/src/label-provider/core/index.ts
@@ -7,7 +7,9 @@ import {
numericIncrementLabel,
errorIconLabel,
warningIconLabel,
- informationIconLabel
+ informationIconLabel,
+ filterSearchLabel,
+ filterNoResultsLabel
} from './label-tokens';
declare global {
@@ -22,7 +24,9 @@ const supportedLabels = {
numericIncrement: numericIncrementLabel,
errorIcon: errorIconLabel,
warningIcon: warningIconLabel,
- informationIcon: informationIconLabel
+ informationIcon: informationIconLabel,
+ filterSearch: filterSearchLabel,
+ filterNoResults: filterNoResultsLabel
} as const;
/**
@@ -49,6 +53,12 @@ export class LabelProviderCore
@attr({ attribute: 'information-icon' })
public informationIcon: string | undefined;
+ @attr({ attribute: 'filter-search' })
+ public filterSearch: string | undefined;
+
+ @attr({ attribute: 'filter-no-results' })
+ public filterNoResults: string | undefined;
+
protected override readonly supportedLabels = supportedLabels;
}
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 09744178cf..91ed6e5993 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
@@ -8,5 +8,7 @@ export const coreLabelDefaults: { readonly [key in TokenName]: string } = {
numericDecrementLabel: 'Decrement',
errorIconLabel: 'Error',
warningIconLabel: 'Warning',
- informationIconLabel: 'Information'
+ informationIconLabel: '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 958075d8ed..2cfe2debce 100644
--- a/packages/nimble-components/src/label-provider/core/label-tokens.ts
+++ b/packages/nimble-components/src/label-provider/core/label-tokens.ts
@@ -30,3 +30,13 @@ export const informationIconLabel = DesignToken.create({
name: 'information-icon-label',
cssCustomPropertyName: null
}).withDefault(coreLabelDefaults.informationIconLabel);
+
+export const filterSearchLabel = DesignToken.create({
+ name: 'filter-search-label',
+ cssCustomPropertyName: null
+}).withDefault(coreLabelDefaults.filterSearchLabel);
+
+export const filterNoResultsLabel = DesignToken.create({
+ name: 'filter-no-results-label',
+ cssCustomPropertyName: null
+}).withDefault(coreLabelDefaults.filterNoResultsLabel);
diff --git a/packages/nimble-components/src/label-provider/table/index.ts b/packages/nimble-components/src/label-provider/table/index.ts
index 840c509148..8081845910 100644
--- a/packages/nimble-components/src/label-provider/table/index.ts
+++ b/packages/nimble-components/src/label-provider/table/index.ts
@@ -14,7 +14,8 @@ import {
tableRowExpandLabel,
tableRowOperationColumnLabel,
tableRowSelectLabel,
- tableSelectAllLabel
+ tableSelectAllLabel,
+ tableRowLoadingLabel
} from './label-tokens';
declare global {
@@ -36,7 +37,8 @@ const supportedLabels = {
selectAll: tableSelectAllLabel,
groupSelectAll: tableGroupSelectAllLabel,
rowSelect: tableRowSelectLabel,
- rowOperationColumn: tableRowOperationColumnLabel
+ rowOperationColumn: tableRowOperationColumnLabel,
+ rowLoading: tableRowLoadingLabel
} as const;
/**
@@ -84,6 +86,9 @@ export class LabelProviderTable
@attr({ attribute: 'row-operation-column' })
public rowOperationColumn: string | undefined;
+ @attr({ attribute: 'row-loading' })
+ public rowLoading: 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 e80029b2c9..79b553c018 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
@@ -15,5 +15,6 @@ export const tableLabelDefaults: { readonly [key in TokenName]: string } = {
tableSelectAllLabel: 'Select all rows',
tableGroupSelectAllLabel: 'Select all rows in group',
tableRowSelectLabel: 'Select row',
- tableRowOperationColumnLabel: 'Row operations'
+ tableRowOperationColumnLabel: 'Row operations',
+ tableRowLoadingLabel: 'Loading'
};
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 4fc928ec3c..26edb26e30 100644
--- a/packages/nimble-components/src/label-provider/table/label-tokens.ts
+++ b/packages/nimble-components/src/label-provider/table/label-tokens.ts
@@ -67,3 +67,8 @@ export const tableRowOperationColumnLabel = DesignToken.create({
name: 'table-row-operation-column-label',
cssCustomPropertyName: null
}).withDefault(tableLabelDefaults.tableRowOperationColumnLabel);
+
+export const tableRowLoadingLabel = DesignToken.create({
+ name: 'table-row-loading-label',
+ cssCustomPropertyName: null
+}).withDefault(tableLabelDefaults.tableRowLoadingLabel);
diff --git a/packages/nimble-components/src/listbox/tests/listbox-matrix.stories.ts b/packages/nimble-components/src/listbox/tests/listbox-matrix.stories.ts
index 9e6e574924..135e106931 100644
--- a/packages/nimble-components/src/listbox/tests/listbox-matrix.stories.ts
+++ b/packages/nimble-components/src/listbox/tests/listbox-matrix.stories.ts
@@ -2,12 +2,10 @@ import { ViewTemplate, html } from '@microsoft/fast-element';
import type { Meta, StoryFn } from '@storybook/html';
import {
createMatrix,
- sharedMatrixParameters
+ sharedMatrixParameters,
+ createMatrixThemeStory
} from '../../utilities/tests/matrix';
-import {
- createMatrixThemeStory,
- createStory
-} from '../../utilities/tests/storybook';
+import { createStory } from '../../utilities/tests/storybook';
import { listboxTag } from '..';
import { listOptionTag } from '../../list-option';
import { hiddenWrapper } from '../../utilities/tests/hidden';
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 4965aa314a..3b9de718a7 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
@@ -4,13 +4,11 @@ import { pascalCase } from '@microsoft/fast-web-utilities';
import { ButtonAppearance } from '../types';
import {
createMatrix,
- sharedMatrixParameters
+ sharedMatrixParameters,
+ createMatrixThemeStory
} from '../../utilities/tests/matrix';
import { disabledStates, DisabledState } from '../../utilities/tests/states';
-import {
- createMatrixThemeStory,
- createStory
-} from '../../utilities/tests/storybook';
+import { createStory } from '../../utilities/tests/storybook';
import { hiddenWrapper } from '../../utilities/tests/hidden';
import { iconArrowExpanderDownTag } from '../../icons/arrow-expander-down';
import { iconKeyTag } from '../../icons/key';
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 ee94424be1..7c39ab4534 100644
--- a/packages/nimble-components/src/menu-button/tests/menu-button.mdx
+++ b/packages/nimble-components/src/menu-button/tests/menu-button.mdx
@@ -1,23 +1,26 @@
-import {
- Controls,
- DocsStory,
- Meta,
- Stories,
- Title,
- Description
-} from '@storybook/blocks';
+import { Controls, Canvas, Meta, Title } from '@storybook/blocks';
import ContentHiddenDocs from '../../patterns/button/tests/content-hidden-docs.mdx';
import * as menuButtonStories from './menu-button.stories';
-
-
+Per [W3C](https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/), a menu button is a button that opens a menu. It is
+often styled as a typical push button with a downward pointing arrow or triangle to hint that activating the button will display a menu.
+
+
-# Usage Docs
+{/* ## Appearances */}
+
+{/* ## Appearance Variants */}
+
+{/* ## Usage */}
+
+{/* ## Examples */}
## Accessibility
+
+{/* ## Resources */}
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 7d7d294e7a..8a8e6bfcd3 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
@@ -1,6 +1,6 @@
import { html, when } from '@microsoft/fast-element';
import { withActions } from '@storybook/addon-actions/decorator';
-import type { Meta, StoryObj } from '@storybook/html';
+import type { HtmlRenderer, Meta, StoryObj } from '@storybook/html';
import {
createUserSelectedThemeStory,
disableStorybookZoomTransform
@@ -23,9 +23,6 @@ interface MenuButtonArgs {
menuPosition: string;
}
-const overviewText = `Per [W3C](https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/), a menu button is a button that opens a menu. It is
-often styled as a typical push button with a downward pointing arrow or triangle to hint that activating the button will display a menu.`;
-
const endIconDescription = `When including an icon after the text content, set \`slot="end"\` on the icon to ensure proper styling.
This icon will be hidden when \`contentHidden\` is set to \`true\`
@@ -33,13 +30,8 @@ This icon will be hidden when \`contentHidden\` is set to \`true\`
const metadata: Meta = {
title: 'Components/Menu Button',
- decorators: [withActions],
+ decorators: [withActions],
parameters: {
- docs: {
- description: {
- component: overviewText
- }
- },
actions: {
handles: ['toggle', 'beforetoggle']
},
diff --git a/packages/nimble-components/src/menu/tests/menu-matrix.stories.ts b/packages/nimble-components/src/menu/tests/menu-matrix.stories.ts
index 8113073a2f..54d0ff8382 100644
--- a/packages/nimble-components/src/menu/tests/menu-matrix.stories.ts
+++ b/packages/nimble-components/src/menu/tests/menu-matrix.stories.ts
@@ -1,10 +1,8 @@
import { html, ViewTemplate, when } from '@microsoft/fast-element';
import type { StoryFn, Meta } from '@storybook/html';
+import { createStory } from '../../utilities/tests/storybook';
import {
createMatrixThemeStory,
- createStory
-} from '../../utilities/tests/storybook';
-import {
createMatrix,
sharedMatrixParameters
} from '../../utilities/tests/matrix';
diff --git a/packages/nimble-components/src/menu/tests/menu.mdx b/packages/nimble-components/src/menu/tests/menu.mdx
index 99933269d3..4229b15346 100644
--- a/packages/nimble-components/src/menu/tests/menu.mdx
+++ b/packages/nimble-components/src/menu/tests/menu.mdx
@@ -1,28 +1,31 @@
-import {
- Controls,
- DocsStory,
- Meta,
- Stories,
- Title,
- Description
-} from '@storybook/blocks';
+import { Controls, Canvas, Meta, Title } from '@storybook/blocks';
import TargetDocs from '../../patterns/anchor/tests/target-docs.mdx';
import * as menuStories from './menu.stories';
+import { menuTag } from '..';
+import { menuItemTag } from '../../menu-item';
+import { anchorMenuItemTag } from '../../anchor-menu-item';
-
-
-
-
+Per [W3C](https://www.w3.org/WAI/ARIA/apg/patterns/menubar/) - A menu is a widget that offers a list of choices to the user,
+such as a set of actions or functions. Menu widgets behave like native operating system menus, such as the menus that pull down from the
+menubars commonly found at the top of many desktop application windows. A menu is usually opened, or made visible, by activating a menu button,
+choosing an item in a menu that opens a sub menu, or by invoking a command, such as Shift + F10 in Windows, that opens a context specific menu.
+When a user activates a choice in a menu, the menu usually closes unless the choice opened a submenu.
-# Usage Docs
+The supports several child elements including ``, `
`, and .
+
+
+
## Child Elements
-- `nimble-menu-item` - Use this child element to execute a function when the item is activated or to include a sub-menu. To include a sub-menu, place a `nimble-menu` as a child of a `nimble-menu-item`.
-- `nimble-anchor-menu-item` - Use this child element to navigate to a URL when the item is activated.
+- - Use this child element to execute a function when
+ the item is activated or to include a sub-menu. To include a sub-menu, place
+ a as a child of a .
+- - Use this child element to navigate to a URL
+ when the item is activated.
### Anchor Menu Item Target Configuration
@@ -34,4 +37,4 @@ The menu can be configured to display a custom header with arbitrary content. Th
For example:
-
+
diff --git a/packages/nimble-components/src/menu/tests/menu.stories.ts b/packages/nimble-components/src/menu/tests/menu.stories.ts
index d573bfe3f6..28ece26e25 100644
--- a/packages/nimble-components/src/menu/tests/menu.stories.ts
+++ b/packages/nimble-components/src/menu/tests/menu.stories.ts
@@ -1,6 +1,6 @@
import { html, repeat, when } from '@microsoft/fast-element';
import { withActions } from '@storybook/addon-actions/decorator';
-import type { Meta, StoryObj } from '@storybook/html';
+import type { HtmlRenderer, Meta, StoryObj } from '@storybook/html';
import { createUserSelectedThemeStory } from '../../utilities/tests/storybook';
import { menuTag } from '..';
import { iconArrowLeftFromLineTag } from '../../icons/arrow-left-from-line';
@@ -36,23 +36,10 @@ interface ItemArgs extends MenuItemArgs {
type: 'nimble-menu-item' | 'header' | 'hr';
}
-const overviewText = `Per [W3C](https://www.w3.org/WAI/ARIA/apg/patterns/menubar/) - A menu is a widget that offers a list of choices to the user,
-such as a set of actions or functions. Menu widgets behave like native operating system menus, such as the menus that pull down from the
-menubars commonly found at the top of many desktop application windows. A menu is usually opened, or made visible, by activating a menu button,
-choosing an item in a menu that opens a sub menu, or by invoking a command, such as Shift + F10 in Windows, that opens a context specific menu.
-When a user activates a choice in a menu, the menu usually closes unless the choice opened a submenu.
-
-The \`nimble-menu\` supports several child elements including \`\`, \`
\`, \`\` and \`\`.`;
-
const metadata: Meta = {
title: 'Components/Menu',
- decorators: [withActions],
+ decorators: [withActions],
parameters: {
- docs: {
- description: {
- component: overviewText
- }
- },
actions: {
handles: ['change']
}
@@ -62,13 +49,6 @@ const metadata: Meta = {
export default metadata;
export const menu: StoryObj = {
- parameters: {
- docs: {
- description: {
- story: overviewText
- }
- }
- },
// prettier-ignore
render: createUserSelectedThemeStory(html`
<${menuTag}>
diff --git a/packages/nimble-components/src/modal/index.ts b/packages/nimble-components/src/modal/index.ts
index e3ad8c4fe7..8d096f8d62 100644
--- a/packages/nimble-components/src/modal/index.ts
+++ b/packages/nimble-components/src/modal/index.ts
@@ -95,6 +95,19 @@ export abstract class Modal extends FoundationElement {
return true;
}
+ /**
+ * @internal
+ */
+ public closeHandler(): void {
+ if (this.resolveShow) {
+ // If
+ // - the browser implements dialogs with the CloseWatcher API, and
+ // - the user presses ESC without first interacting with the dialog (e.g. clicking, scrolling),
+ // the cancel event is not fired, but the close event still is, and the dialog just closes.
+ this.doResolveShow(UserDismissed);
+ }
+ }
+
// Derived classes can override this, but should not call it directly (except from the override).
protected startOpening(): void {
this.finishOpening();
diff --git a/packages/nimble-components/src/number-field/tests/number-field-matrix.stories.ts b/packages/nimble-components/src/number-field/tests/number-field-matrix.stories.ts
index 397d5cdd7f..a6f3f35220 100644
--- a/packages/nimble-components/src/number-field/tests/number-field-matrix.stories.ts
+++ b/packages/nimble-components/src/number-field/tests/number-field-matrix.stories.ts
@@ -1,11 +1,9 @@
import type { StoryFn, Meta } from '@storybook/html';
import { html, ViewTemplate } from '@microsoft/fast-element';
import { pascalCase } from '@microsoft/fast-web-utilities';
+import { createStory } from '../../utilities/tests/storybook';
import {
createMatrixThemeStory,
- createStory
-} from '../../utilities/tests/storybook';
-import {
createMatrix,
sharedMatrixParameters
} from '../../utilities/tests/matrix';
diff --git a/packages/nimble-components/src/number-field/tests/number-field.mdx b/packages/nimble-components/src/number-field/tests/number-field.mdx
new file mode 100644
index 0000000000..4090916277
--- /dev/null
+++ b/packages/nimble-components/src/number-field/tests/number-field.mdx
@@ -0,0 +1,23 @@
+import { Canvas, Meta, Controls, Title } from '@storybook/blocks';
+import { NimbleNumberField } from './number-field.react';
+import * as numberFieldStories from './number-field.stories';
+
+
+
+
+Similar to a single line text box but only used for numeric data. The controls allow the user to increment and decrement the value.
+
+
+
+
+{/* ## Appearances */}
+
+{/* ## Appearance Variants */}
+
+{/* ## Usage */}
+
+{/* ## Examples */}
+
+{/* ## Accessibility */}
+
+{/* ## Resources */}
diff --git a/packages/nimble-components/src/number-field/tests/number-field.stories.ts b/packages/nimble-components/src/number-field/tests/number-field.stories.ts
index 3126b6de7b..a029ef3d3f 100644
--- a/packages/nimble-components/src/number-field/tests/number-field.stories.ts
+++ b/packages/nimble-components/src/number-field/tests/number-field.stories.ts
@@ -1,6 +1,6 @@
import { html } from '@microsoft/fast-element';
import { withActions } from '@storybook/addon-actions/decorator';
-import type { Meta, StoryObj } from '@storybook/html';
+import type { HtmlRenderer, Meta, StoryObj } from '@storybook/html';
import { createUserSelectedThemeStory } from '../../utilities/tests/storybook';
import { NumberFieldAppearance } from '../types';
import { numberFieldTag } from '..';
@@ -29,15 +29,8 @@ interface NumberFieldArgs extends LabelUserArgs {
const metadata: Meta = {
title: 'Components/Number Field',
- tags: ['autodocs'],
- decorators: [withActions],
+ decorators: [withActions],
parameters: {
- docs: {
- description: {
- component:
- 'Similar to a single line text box but only used for numeric data. The controls allow the user to increment and decrement the value.'
- }
- },
actions: {
handles: ['change', 'input']
}
diff --git a/packages/nimble-components/src/patterns/dropdown/styles.ts b/packages/nimble-components/src/patterns/dropdown/styles.ts
index 06e003e3e2..194bb55ad3 100644
--- a/packages/nimble-components/src/patterns/dropdown/styles.ts
+++ b/packages/nimble-components/src/patterns/dropdown/styles.ts
@@ -134,10 +134,6 @@ export const styles = css`
border-bottom-color: ${failColor};
}
- .anchored-region[hidden] {
- visibility: hidden;
- }
-
.listbox {
box-sizing: border-box;
display: inline-flex;
diff --git a/packages/nimble-components/src/radio-group/tests/radio-group-matrix.stories.ts b/packages/nimble-components/src/radio-group/tests/radio-group-matrix.stories.ts
index 079a3e8c11..7e696e713d 100644
--- a/packages/nimble-components/src/radio-group/tests/radio-group-matrix.stories.ts
+++ b/packages/nimble-components/src/radio-group/tests/radio-group-matrix.stories.ts
@@ -3,13 +3,11 @@ import { html, ViewTemplate } from '@microsoft/fast-element';
import { Orientation } from '@microsoft/fast-web-utilities';
import {
createMatrix,
- sharedMatrixParameters
+ sharedMatrixParameters,
+ createMatrixThemeStory
} from '../../utilities/tests/matrix';
import { disabledStates, DisabledState } from '../../utilities/tests/states';
-import {
- createMatrixThemeStory,
- createStory
-} from '../../utilities/tests/storybook';
+import { createStory } from '../../utilities/tests/storybook';
import { hiddenWrapper } from '../../utilities/tests/hidden';
import { textCustomizationWrapper } from '../../utilities/tests/text-customization';
import { radioGroupTag } from '..';
diff --git a/packages/nimble-components/src/radio-group/tests/radio-group.mdx b/packages/nimble-components/src/radio-group/tests/radio-group.mdx
index e6e2f67dcf..31341c0112 100644
--- a/packages/nimble-components/src/radio-group/tests/radio-group.mdx
+++ b/packages/nimble-components/src/radio-group/tests/radio-group.mdx
@@ -1,24 +1,27 @@
-import {
- Controls,
- DocsStory,
- Meta,
- Stories,
- Title,
- Description
-} from '@storybook/blocks';
+import { Controls, Canvas, Meta, Title } from '@storybook/blocks';
import * as radioGroupStories from './radio-group.stories';
-
-
-
+Per [W3C](https://www.w3.org/WAI/ARIA/apg/patterns/radio/) - A radio group is a set of checkable buttons, known as radio buttons, where no more than one of the buttons can be checked at a time. Some implementations may initialize the set with all buttons in the unchecked state in order to force the user to check one of the buttons before moving past a certain point in the workflow.
+
+
-# Usage Docs
+{/* ## Appearances */}
+
+{/* ## Appearance Variants */}
+
+{/* ## Usage */}
-## Angular Usage
+{/* ## Examples */}
+
+### Angular Usage
The Angular CVA for the radio button group ignores the value of `callSetDisabledState` configured on the form module.
Instead, it always uses the default value of `'always'`.
+
+{/* ## Accessibility */}
+
+{/* ## Resources */}
diff --git a/packages/nimble-components/src/radio-group/tests/radio-group.stories.ts b/packages/nimble-components/src/radio-group/tests/radio-group.stories.ts
index b0a35c8fcc..4b0b69d116 100644
--- a/packages/nimble-components/src/radio-group/tests/radio-group.stories.ts
+++ b/packages/nimble-components/src/radio-group/tests/radio-group.stories.ts
@@ -1,7 +1,7 @@
import { html } from '@microsoft/fast-element';
import { Orientation } from '@microsoft/fast-web-utilities';
import { withActions } from '@storybook/addon-actions/decorator';
-import type { Meta, StoryObj } from '@storybook/html';
+import type { HtmlRenderer, Meta, StoryObj } from '@storybook/html';
import { createUserSelectedThemeStory } from '../../utilities/tests/storybook';
import { radioGroupTag } from '..';
import { radioTag } from '../../radio';
@@ -16,14 +16,8 @@ interface RadioGroupArgs {
const metadata: Meta = {
title: 'Components/Radio Group',
- decorators: [withActions],
+ decorators: [withActions],
parameters: {
- docs: {
- description: {
- component:
- 'Per [W3C](https://www.w3.org/WAI/ARIA/apg/patterns/radio/) – A radio group is a set of checkable buttons, known as radio buttons, where no more than one of the buttons can be checked at a time. Some implementations may initialize the set with all buttons in the unchecked state in order to force the user to check one of the buttons before moving past a certain point in the workflow.'
- }
- },
actions: {
handles: ['change']
}
diff --git a/packages/nimble-components/src/rich-text-mention/users/view/tests/rich-text-mention-users-view-matrix.stories.ts b/packages/nimble-components/src/rich-text-mention/users/view/tests/rich-text-mention-users-view-matrix.stories.ts
index a3997e50af..5d5f8803fb 100644
--- a/packages/nimble-components/src/rich-text-mention/users/view/tests/rich-text-mention-users-view-matrix.stories.ts
+++ b/packages/nimble-components/src/rich-text-mention/users/view/tests/rich-text-mention-users-view-matrix.stories.ts
@@ -2,9 +2,9 @@ import { ViewTemplate, html } from '@microsoft/fast-element';
import type { Meta, StoryFn } from '@storybook/html';
import {
createMatrix,
- sharedMatrixParameters
+ sharedMatrixParameters,
+ createMatrixThemeStory
} from '../../../../utilities/tests/matrix';
-import { createMatrixThemeStory } from '../../../../utilities/tests/storybook';
import { richTextMentionUsersViewTag } from '..';
import {
bodyFont,
diff --git a/packages/nimble-components/src/rich-text/editor/models/create-tiptap-editor.ts b/packages/nimble-components/src/rich-text/editor/models/create-tiptap-editor.ts
index fb83eec703..7fcaf49915 100644
--- a/packages/nimble-components/src/rich-text/editor/models/create-tiptap-editor.ts
+++ b/packages/nimble-components/src/rich-text/editor/models/create-tiptap-editor.ts
@@ -210,7 +210,7 @@ function createCustomMentionExtension(
return [
config.viewElement,
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
- this.options.renderLabel({
+ this.options.renderText({
options: this.options,
node
})
diff --git a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-matrix.stories.ts b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-matrix.stories.ts
index f8fbcd32aa..2171a08fba 100644
--- a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-matrix.stories.ts
+++ b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-matrix.stories.ts
@@ -1,10 +1,8 @@
import type { Meta, StoryFn } from '@storybook/html';
import { html, ViewTemplate } from '@microsoft/fast-element';
+import { createStory } from '../../../utilities/tests/storybook';
import {
createMatrixThemeStory,
- createStory
-} from '../../../utilities/tests/storybook';
-import {
createMatrix,
sharedMatrixParameters
} from '../../../utilities/tests/matrix';
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 81997fee8c..e4aed8d1f7 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
@@ -1205,8 +1205,7 @@ describe('RichTextEditorMentionListbox', () => {
expect(pageObject.isMentionListboxOpened()).toBeFalse();
});
- // Disabled due to intermittency. See: https://ni.visualstudio.com/DevCentral/_workitems/edit/2632606
- xit('should commit mention into the editor on Enter', async () => {
+ it('should commit mention into the editor on Enter', async () => {
await appendUserMentionConfiguration(element, [
{ key: 'user:1', displayName: 'username1' }
]);
diff --git a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.mdx b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.mdx
index 417b978626..f18d35c7de 100644
--- a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.mdx
+++ b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.mdx
@@ -1,28 +1,22 @@
-import {
- Controls,
- DocsStory,
- Meta,
- Stories,
- Title,
- Description
-} from '@storybook/blocks';
+import { Controls, Canvas, Meta, Title, Description } from '@storybook/blocks';
import * as editorStories from './rich-text-editor.stories';
import * as mentionUserStories from '../../../rich-text-mention/users/tests/rich-text-mention-users.stories';
import * as userMappingStories from '../../../mapping/user/tests/mapping-user.stories';
+import { richTextEditorTag } from '..';
-
-
-
-
+The rich text editor component allows users to add/edit text formatted with various styling options including bold, italics, numbered lists, and bulleted lists. The editor generates markdown output and takes markdown as input. The markdown flavor used is [CommonMark](https://spec.commonmark.org/0.30/).
-# Usage Docs
+See the [rich text viewer](?path=/docs/incubating-rich-text-viewer--docs) component to render markdown without allowing editing.
+
+
+
## User Mention Usage
-To enable support for `@mention` in the `nimble-rich-text-editor` component, include the specified configuration and mapping components as children. These components facilitate the parsing of input markdown into a mention node, displaying a user's name and shows list of user names in the mapping dropdown. Additionally, ensure that each user mentioned in the markdown input has a corresponding user mapping.
+To enable support for `@mention` in the component, include the specified configuration and mapping components as children. These components facilitate the parsing of input markdown into a mention node, displaying a user's name and shows list of user names in the mapping dropdown. Additionally, ensure that each user mentioned in the markdown input has a corresponding user mapping.
It is also highly recommended to limit the number of users displayed (maximum 50 users at a time) in the mention popup when triggered in the editor and based on filtering, update the mention elements dynamically by listening to the `mention-update` event. This enhances the performance and overall user experience when interacting with the mention dropdown. For more details, see [Client Usage Guidance on Filtered Users](https://github.com/ni/nimble/blob/main/packages/nimble-components/src/rich-text/specs/mention-hld.md#client-usage-guidance-on-filtered-users).
diff --git a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.stories.ts b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.stories.ts
index 04826a4710..30a8beb9ac 100644
--- a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.stories.ts
+++ b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.stories.ts
@@ -1,5 +1,5 @@
import { html, ref, when } from '@microsoft/fast-element';
-import type { Meta, StoryObj } from '@storybook/html';
+import type { HtmlRenderer, Meta, StoryObj } from '@storybook/html';
import { withActions } from '@storybook/addon-actions/decorator';
import {
createUserSelectedThemeStory,
@@ -66,7 +66,6 @@ const mentionDataSets = {
}
} as const;
-const richTextEditorDescription = 'The rich text editor component allows users to add/edit text formatted with various styling options including bold, italics, numbered lists, and bulleted lists. The editor generates markdown output and takes markdown as input. The markdown flavor used is [CommonMark](https://spec.commonmark.org/0.30/).\n\n See the [rich text viewer](?path=/docs/incubating-rich-text-viewer--docs) component to render markdown without allowing editing.';
const setMarkdownDescription = 'A function that sets content in the editor with the provided markdown string.';
const getMarkdownDescription = 'A function that serializes the current data in the editor and returns the markdown string.';
const footerActionButtonDescription = `You can place a button or anchor button at the far-right of the footer section, set \`slot="footer-actions"\`.
@@ -86,13 +85,8 @@ The object's type is \`RichTextValidity\`, and it contains the following boolean
const metadata: Meta = {
title: 'Incubating/Rich Text Editor',
- decorators: [withActions],
+ decorators: [withActions],
parameters: {
- docs: {
- description: {
- component: richTextEditorDescription
- }
- },
actions: {
handles: ['input', 'mention-update']
}
diff --git a/packages/nimble-components/src/rich-text/mention-listbox/tests/mention-listbox-matrix.stories.ts b/packages/nimble-components/src/rich-text/mention-listbox/tests/mention-listbox-matrix.stories.ts
index a3ff942fb9..e9b9819758 100644
--- a/packages/nimble-components/src/rich-text/mention-listbox/tests/mention-listbox-matrix.stories.ts
+++ b/packages/nimble-components/src/rich-text/mention-listbox/tests/mention-listbox-matrix.stories.ts
@@ -6,12 +6,10 @@ import { hiddenWrapper } from '../../../utilities/tests/hidden';
import { loremIpsum } from '../../../utilities/tests/lorem-ipsum';
import {
createMatrix,
- sharedMatrixParameters
+ sharedMatrixParameters,
+ createMatrixThemeStory
} from '../../../utilities/tests/matrix';
-import {
- createMatrixThemeStory,
- createStory
-} from '../../../utilities/tests/storybook';
+import { createStory } from '../../../utilities/tests/storybook';
const metadata: Meta = {
title: 'Tests/Rich Text Mention Listbox',
diff --git a/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer-matrix.stories.ts b/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer-matrix.stories.ts
index 98d854b301..f4b0542afa 100644
--- a/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer-matrix.stories.ts
+++ b/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer-matrix.stories.ts
@@ -1,10 +1,8 @@
import type { Meta, StoryFn } from '@storybook/html';
import { html, ViewTemplate } from '@microsoft/fast-element';
+import { createStory } from '../../../utilities/tests/storybook';
import {
createMatrixThemeStory,
- createStory
-} from '../../../utilities/tests/storybook';
-import {
createMatrix,
sharedMatrixParameters
} from '../../../utilities/tests/matrix';
diff --git a/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer.mdx b/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer.mdx
index 348bad73d6..5a3885eea9 100644
--- a/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer.mdx
+++ b/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer.mdx
@@ -1,28 +1,22 @@
-import {
- Controls,
- DocsStory,
- Meta,
- Stories,
- Title,
- Description
-} from '@storybook/blocks';
+import { Controls, Canvas, Meta, Title, Description } from '@storybook/blocks';
import * as viewerStories from './rich-text-viewer.stories';
import * as mentionUserStories from '../../../rich-text-mention/users/tests/rich-text-mention-users.stories';
import * as userMappingStories from '../../../mapping/user/tests/mapping-user.stories';
+import { richTextViewerTag } from '..';
-
-
-
-
+The rich text viewer component allows users to view text formatted with various styling options including bold, italics, numbered lists, and bulleted lists. The rich text to render is provided as a markdown string.
-# Usage Docs
+See the [rich text editor](?path=/docs/incubating-rich-text-editor--docs) component to enable users to modify the markdown contents.
+
+
+
## User Mention Usage
-To enable support for `@mention` in the `nimble-rich-text-viewer` component, include the specified configuration and mapping components as children. These components facilitate the conversion of input markdown into a mention node, displaying a user's name. Additionally, ensure that each user mentioned in the markdown input has a corresponding user mapping. For more details, see [Client Usage Guidance on Filtered Users](https://github.com/ni/nimble/blob/main/packages/nimble-components/src/rich-text/specs/mention-hld.md#client-usage-guidance-on-filtered-users-1).
+To enable support for `@mention` in the component, include the specified configuration and mapping components as children. These components facilitate the conversion of input markdown into a mention node, displaying a user's name. Additionally, ensure that each user mentioned in the markdown input has a corresponding user mapping. For more details, see [Client Usage Guidance on Filtered Users](https://github.com/ni/nimble/blob/main/packages/nimble-components/src/rich-text/specs/mention-hld.md#client-usage-guidance-on-filtered-users-1).
## User Configuration
diff --git a/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer.stories.ts b/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer.stories.ts
index 50ac3bfbd1..9292f87003 100644
--- a/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer.stories.ts
+++ b/packages/nimble-components/src/rich-text/viewer/tests/rich-text-viewer.stories.ts
@@ -32,8 +32,6 @@ const dataSets = {
}
} as const;
-const richTextViewerDescription = 'The rich text viewer component allows users to view text formatted with various styling options including bold, italics, numbered lists, and bulleted lists. The rich text to render is provided as a markdown string.\n\n See the [rich text editor](?path=/docs/incubating-rich-text-editor--docs) component to enable users to modify the markdown contents.';
-
const validityDescription = `Readonly object of boolean values that represents the validity states that the viewer's configuration can be in.
The object's type is \`RichTextValidity\`, and it contains the following boolean properties:
- \`invalidMentionConfiguration\`: \`true\` when a mention configuration is invalid. Call \`checkValidity()\` on each mention component to see which configuration is invalid, and read the \`validity\` property of that mention for details about why it's invalid.
@@ -42,13 +40,7 @@ The object's type is \`RichTextValidity\`, and it contains the following boolean
const metadata: Meta = {
title: 'Incubating/Rich Text Viewer',
- parameters: {
- docs: {
- description: {
- component: richTextViewerDescription
- }
- }
- },
+ parameters: {},
// prettier-ignore
render: createUserSelectedThemeStory(html`
${incubatingWarning({
diff --git a/packages/nimble-components/src/select/index.ts b/packages/nimble-components/src/select/index.ts
index be85b9d9be..15ee96da7a 100644
--- a/packages/nimble-components/src/select/index.ts
+++ b/packages/nimble-components/src/select/index.ts
@@ -1,10 +1,34 @@
-import { attr, html, observable } from '@microsoft/fast-element';
+// Based on: https://github.com/microsoft/fast/blob/%40microsoft/fast-foundation_v2.49.5/packages/web-components/fast-foundation/src/select/select.ts
+import {
+ attr,
+ html,
+ observable,
+ Observable,
+ volatile
+} from '@microsoft/fast-element';
import {
AnchoredRegion,
DesignSystem,
Select as FoundationSelect,
- SelectOptions
+ ListboxOption,
+ SelectOptions,
+ SelectPosition,
+ applyMixins,
+ StartEnd,
+ DelegatesARIASelect,
+ Listbox
} from '@microsoft/fast-foundation';
+import {
+ keyArrowDown,
+ keyArrowUp,
+ keyEnd,
+ keyEnter,
+ keyEscape,
+ keyHome,
+ keySpace,
+ keyTab,
+ uniqueId
+} from '@microsoft/fast-web-utilities';
import { arrowExpanderDown16X16 } from '@ni/nimble-tokens/dist/icons/js';
import { styles } from './styles';
import { DropdownAppearance } from '../patterns/dropdown/types';
@@ -12,6 +36,10 @@ import { errorTextTemplate } from '../patterns/error/template';
import type { ErrorPattern } from '../patterns/error/types';
import { iconExclamationMarkTag } from '../icons/exclamation-mark';
import { template } from './template';
+import type { ListOption } from '../list-option';
+import { FilterMode } from './types';
+import { diacriticInsensitiveStringNormalizer } from '../utilities/models/string-normalizers';
+import { FormAssociatedSelect } from './models/select-form-associated';
declare global {
interface HTMLElementTagNameMap {
@@ -19,13 +47,25 @@ declare global {
}
}
+// Used in overrides of base class methods
+// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
+type BooleanOrVoid = boolean | void;
+
/**
- * A nimble-styled HTML select
+ * A nimble-styled HTML select.
*/
-export class Select extends FoundationSelect implements ErrorPattern {
+export class Select extends FormAssociatedSelect implements ErrorPattern {
@attr
public appearance: DropdownAppearance = DropdownAppearance.underline;
+ /**
+ * Reflects the placement for the listbox when the select is open.
+ *
+ * @public
+ */
+ @attr({ attribute: 'position' })
+ public positionAttribute?: SelectPosition;
+
/**
* A message explaining why the value is invalid.
*
@@ -39,61 +79,797 @@ export class Select extends FoundationSelect implements ErrorPattern {
@attr({ attribute: 'error-visible', mode: 'boolean' })
public errorVisible = false;
+ @attr({ attribute: 'filter-mode' })
+ public filterMode: FilterMode = FilterMode.none;
+
+ /**
+ * @internal
+ */
+ @attr({ attribute: 'open', mode: 'boolean' })
+ public open = false;
+
+ /**
+ * Holds the current state for the calculated position of the listbox.
+ *
+ * @public
+ */
+ @observable
+ public position?: SelectPosition;
+
+ /**
+ * The ref to the internal `.control` element.
+ *
+ * @internal
+ */
+ @observable
+ public control!: HTMLElement;
+
+ /**
+ * Reference to the internal listbox element.
+ *
+ * @internal
+ */
+ public listbox!: HTMLDivElement;
+
+ /**
+ * The unique id for the internal listbox element.
+ *
+ * @internal
+ */
+ public listboxId: string = uniqueId('listbox-');
+
+ /**
+ * @internal
+ */
+ @observable
+ public scrollableRegion!: HTMLElement;
+
+ /**
+ * @internal
+ */
+ @observable
+ public filterInput?: HTMLInputElement;
+
/**
* @internal
*/
@observable
- public region?: AnchoredRegion;
+ public anchoredRegion!: AnchoredRegion;
/** @internal */
@observable
public hasOverflow = false;
- // Workaround for https://github.com/microsoft/fast/issues/5123
- public override setPositioning(): void {
- if (!this.$fastController.isConnected) {
- // Don't call setPositioning() until we're connected,
- // since this.forcedPosition isn't initialized yet.
- return;
+ /**
+ * @internal
+ */
+ @observable
+ public filteredOptions: ListboxOption[] = [];
+
+ /**
+ * @internal
+ */
+ @observable
+ public filter = '';
+
+ /**
+ * @internal
+ */
+ @observable
+ public committedSelectedOption: ListboxOption | undefined = undefined;
+
+ /**
+ * The max height for the listbox when opened.
+ *
+ * @internal
+ */
+ @observable
+ public maxHeight = 0;
+
+ /**
+ * The component is collapsible when in single-selection mode with no size attribute.
+ *
+ * @internal
+ */
+ @volatile
+ public get collapsible(): boolean {
+ return !(this.multiple || typeof this.size === 'number');
+ }
+
+ private _value = '';
+ private forcedPosition = false;
+ private indexWhenOpened?: number;
+
+ /**
+ * @internal
+ */
+ public override connectedCallback(): void {
+ super.connectedCallback();
+ this.forcedPosition = !!this.positionAttribute;
+ this.initializeOpenState();
+ }
+
+ /**
+ * The list of options. This mirrors FAST's override implementation for this
+ * member for the Combobox to support a filtered list in the dropdown.
+ *
+ * @public
+ * @remarks
+ * Overrides `Listbox.options`.
+ */
+ public override get options(): ListboxOption[] {
+ Observable.track(this, 'options');
+ return this.filteredOptions?.length
+ ? this.filteredOptions
+ : (this._options as ListOption[]);
+ }
+
+ public override set options(value: ListboxOption[]) {
+ this._options = value;
+ Observable.notify(this, 'options');
+ }
+
+ public override get value(): string {
+ Observable.track(this, 'value');
+ return this._value;
+ }
+
+ public override set value(next: string) {
+ const prev = this._value;
+ let newValue = next;
+
+ // use 'options' here instead of '_options' as 'selectedIndex' may be relative
+ // to filtered set
+ if (this.options?.length) {
+ const newValueIndex = this.options.findIndex(
+ el => el.value === newValue
+ );
+ const prevSelectedValue = this.options[this.selectedIndex]?.value ?? null;
+ const nextSelectedValue = this.options[newValueIndex]?.value ?? null;
+
+ if (
+ newValueIndex === -1
+ || prevSelectedValue !== nextSelectedValue
+ ) {
+ newValue = '';
+ this.selectedIndex = newValueIndex;
+ }
+
+ newValue = this.firstSelectedOption?.value ?? newValue;
+ }
+
+ if (prev !== newValue && !(this.open && this.selectedIndex < 0)) {
+ this._value = newValue;
+ super.valueChanged(prev, newValue);
+ if (!this.open) {
+ this.committedSelectedOption = this._options.find(
+ o => o.value === newValue
+ );
+ }
+ Observable.notify(this, 'value');
+ if (this.collapsible) {
+ Observable.notify(this, 'displayValue');
+ }
+ }
+ }
+
+ /**
+ * @internal
+ */
+ public get displayValue(): string {
+ Observable.track(this, 'displayValue');
+ return this.committedSelectedOption?.text ?? '';
+ }
+
+ /**
+ * @internal
+ */
+ public anchoredRegionChanged(
+ _prev: AnchoredRegion | undefined,
+ _next: AnchoredRegion | undefined
+ ): void {
+ if (this.anchoredRegion && this.control) {
+ this.anchoredRegion.anchorElement = this.control;
+ }
+ }
+
+ /**
+ * @internal
+ */
+ public controlChanged(
+ _prev: HTMLElement | undefined,
+ _next: HTMLElement | undefined
+ ): void {
+ if (this.anchoredRegion && this.control) {
+ this.anchoredRegion.anchorElement = this.control;
}
- super.setPositioning();
- this.updateListboxMaxHeightCssVariable();
}
- // Workaround for https://github.com/microsoft/fast/issues/5773
+ /**
+ * @internal
+ */
public override slottedOptionsChanged(
prev: Element[],
next: Element[]
): void {
const value = this.value;
+ this._options.forEach(o => {
+ const notifier = Observable.getNotifier(o);
+ notifier.unsubscribe(this, 'value');
+ });
+
super.slottedOptionsChanged(prev, next);
+
+ this._options.forEach(o => {
+ const notifier = Observable.getNotifier(o);
+ notifier.subscribe(this, 'value');
+ });
+ this.setProxyOptions();
+ this.updateValue();
+ // We need to force an update to the filteredOptions observable
+ // (by calling 'filterOptions()) so that the template correctly updates.
+ this.filterOptions();
if (value) {
this.value = value;
}
+ this.committedSelectedOption = this.options[this.selectedIndex];
}
- private regionChanged(
- _prev: AnchoredRegion | undefined,
- _next: AnchoredRegion | undefined
+ /**
+ * @internal
+ */
+ public override clickHandler(e: MouseEvent): BooleanOrVoid {
+ // do nothing if the select is disabled
+ if (this.disabled) {
+ return;
+ }
+
+ if (this.open) {
+ const captured = (e.target as HTMLElement).closest(
+ 'option,[role=option]'
+ );
+
+ if (!captured?.disabled) {
+ this.updateSelectedIndexFromFilteredSet();
+ }
+
+ if (captured?.disabled) {
+ return;
+ }
+ }
+
+ super.clickHandler(e);
+
+ this.open = this.collapsible && !this.open;
+
+ if (!this.open && this.indexWhenOpened !== this.selectedIndex) {
+ this.updateValue(true);
+ }
+ }
+
+ /**
+ * Updates the value when an option's value changes.
+ *
+ * @param source - the source object
+ * @param propertyName - the property to evaluate
+ *
+ * @internal
+ * @override
+ */
+ public override handleChange(source: unknown, propertyName: string): void {
+ super.handleChange(source, propertyName);
+ if (propertyName === 'value') {
+ this.updateValue();
+ }
+ }
+
+ /**
+ * Prevents focus when size is set and a scrollbar is clicked.
+ *
+ * @param e - the mouse event object
+ *
+ * @override
+ * @internal
+ */
+ public override mousedownHandler(e: MouseEvent): BooleanOrVoid {
+ if (e.offsetX >= 0 && e.offsetX <= this.listbox?.scrollWidth) {
+ return super.mousedownHandler(e);
+ }
+
+ return this.collapsible;
+ }
+
+ /**
+ * @internal
+ */
+ public regionLoadedHandler(): void {
+ this.focusAndScrollOptionIntoView();
+ }
+
+ /**
+ * Sets the multiple property on the proxy element.
+ *
+ * @param prev - the previous multiple value
+ * @param next - the current multiple value
+ */
+ public override multipleChanged(
+ prev: boolean | undefined,
+ next: boolean
): void {
- if (this.region && this.control) {
- this.region.anchorElement = this.control;
+ super.multipleChanged(prev, next);
+
+ if (this.proxy) {
+ this.proxy.multiple = next;
}
}
- private controlChanged(
- _prev: HTMLElement | undefined,
- _next: HTMLElement | undefined
+ /**
+ * @internal
+ */
+ public inputClickHandler(e: MouseEvent): void {
+ e.stopPropagation(); // clicking in filter input shouldn't close dropdown
+ }
+
+ /**
+ * @internal
+ */
+ public changeValueHandler(): void {
+ this.committedSelectedOption = this.options.find(
+ option => option.selected
+ );
+ }
+
+ /**
+ * @internal
+ */
+ public updateDisplayValue(): void {
+ if (this.collapsible) {
+ Observable.notify(this, 'displayValue');
+ }
+ }
+
+ /**
+ * Handle content changes on the control input.
+ *
+ * @param e - the input event
+ * @internal
+ */
+ public inputHandler(e: InputEvent): boolean {
+ this.filter = this.filterInput?.value ?? '';
+ if (!this.committedSelectedOption) {
+ this.committedSelectedOption = this._options.find(
+ option => option.selected
+ );
+ }
+ this.clearSelection();
+ this.filterOptions();
+
+ if (
+ this.filteredOptions.length > 0
+ && this.committedSelectedOption
+ && !this.filteredOptions.includes(this.committedSelectedOption)
+ ) {
+ const enabledOptions = this.filteredOptions.filter(
+ o => !o.disabled
+ );
+ if (enabledOptions.length > 0) {
+ enabledOptions[0]!.selected = true;
+ } else {
+ // only filtered option is disabled
+ this.selectedOptions = [];
+ this.selectedIndex = -1;
+ }
+ } else if (this.committedSelectedOption) {
+ this.committedSelectedOption.selected = true;
+ }
+
+ if (e.inputType.includes('deleteContent') || !this.filter.length) {
+ return true;
+ }
+
+ e.stopPropagation();
+ return true;
+ }
+
+ /**
+ * @internal
+ */
+ public override focusoutHandler(e: FocusEvent): BooleanOrVoid {
+ this.updateSelectedIndexFromFilteredSet();
+ super.focusoutHandler(e);
+ if (!this.open) {
+ return true;
+ }
+
+ const focusTarget = e.relatedTarget as HTMLElement;
+ if (this.isSameNode(focusTarget)) {
+ this.focus();
+ return true;
+ }
+
+ if (!this.options?.includes(focusTarget as ListboxOption)) {
+ this.open = false;
+ if (this.indexWhenOpened !== this.selectedIndex) {
+ this.updateValue(true);
+ }
+ }
+ return true;
+ }
+
+ /**
+ * @internal
+ */
+ public override keydownHandler(e: KeyboardEvent): BooleanOrVoid {
+ super.keydownHandler(e);
+ const key = e.key;
+ if (e.ctrlKey || e.shiftKey) {
+ return true;
+ }
+
+ switch (key) {
+ case keySpace: {
+ // when dropdown is open allow user to enter a space for filter text
+ if (this.open && this.filterMode !== FilterMode.none) {
+ break;
+ }
+
+ e.preventDefault();
+ if (this.collapsible && this.typeAheadExpired) {
+ this.open = !this.open;
+ }
+ if (!this.open) {
+ this.focus();
+ }
+ break;
+ }
+ case keyHome:
+ case keyEnd: {
+ e.preventDefault();
+ break;
+ }
+ case keyEnter: {
+ e.preventDefault();
+ if (
+ this.filteredOptions.length === 0
+ || this.filteredOptions.every(o => o.disabled)
+ ) {
+ return false;
+ }
+ this.updateSelectedIndexFromFilteredSet();
+ this.open = !this.open;
+ if (!this.open) {
+ this.focus();
+ }
+ break;
+ }
+ case keyEscape: {
+ // clear filter as update to "selectedIndex" will result in processing
+ // "options" and not "_options"
+ this.filter = '';
+ if (this.committedSelectedOption) {
+ this.clearSelection();
+ this.selectedIndex = this._options.indexOf(
+ this.committedSelectedOption
+ );
+ }
+ if (this.collapsible && this.open) {
+ e.preventDefault();
+ this.open = false;
+ }
+ // reset 'selected' state otherwise the selected state doesn't stick.
+ const selectedOption = this._options[this.selectedIndex];
+ if (selectedOption) {
+ selectedOption.selected = true;
+ }
+ this.focus();
+ break;
+ }
+ case keyTab: {
+ if (this.collapsible && this.open) {
+ e.preventDefault();
+ this.open = false;
+ }
+
+ return true;
+ }
+
+ default: {
+ break;
+ }
+ }
+
+ if (!this.open && this.indexWhenOpened !== this.selectedIndex) {
+ this.updateValue(true);
+ this.indexWhenOpened = this.selectedIndex;
+ }
+
+ return !(key === keyArrowDown || key === keyArrowUp);
+ }
+
+ /**
+ * Updates the proxy value when the selected index changes.
+ *
+ * @param prev - the previous selected index
+ * @param next - the next selected index
+ *
+ * @internal
+ */
+ public override selectedIndexChanged(
+ prev: number | undefined,
+ next: number
+ ): void {
+ super.selectedIndexChanged(prev, next);
+ this.updateValue();
+ }
+
+ /**
+ * Synchronize the `aria-disabled` property when the `disabled` property changes.
+ *
+ * @param prev - The previous disabled value
+ * @param next - The next disabled value
+ *
+ * @internal
+ */
+ public override disabledChanged(prev: boolean, next: boolean): void {
+ if (super.disabledChanged) {
+ super.disabledChanged(prev, next);
+ }
+ this.ariaDisabled = this.disabled ? 'true' : 'false';
+ }
+
+ /**
+ * Reset the element to its first selectable option when its parent form is reset.
+ *
+ * @internal
+ */
+ public override formResetCallback(): void {
+ this.setProxyOptions();
+ // Call the base class's implementation setDefaultSelectedOption instead of the select's
+ // override, in order to reset the selectedIndex without using the value property.
+ super.setDefaultSelectedOption();
+ if (this.selectedIndex === -1) {
+ this.selectedIndex = 0;
+ }
+ }
+
+ // Prevents parent classes from resetting selectedIndex to a positive
+ // value while filtering, which can result in a disabled option being
+ // selected.
+ protected override setSelectedOptions(): void {
+ if (this.open && this.selectedIndex === -1) {
+ return;
+ }
+
+ super.setSelectedOptions();
+ }
+
+ protected override focusAndScrollOptionIntoView(): void {
+ super.focusAndScrollOptionIntoView();
+ if (this.open) {
+ window.requestAnimationFrame(() => {
+ this.filterInput?.focus();
+ });
+ }
+ }
+
+ protected positionChanged(
+ _: SelectPosition | undefined,
+ next: SelectPosition | undefined
+ ): void {
+ this.positionAttribute = next;
+ this.setPositioning();
+ }
+
+ /**
+ * Updates the proxy's size property when the size attribute changes.
+ *
+ * @param prev - the previous size
+ * @param next - the current size
+ *
+ * @override
+ * @internal
+ */
+ protected override sizeChanged(
+ prev: number | undefined,
+ next: number
+ ): void {
+ super.sizeChanged(prev, next);
+
+ if (this.proxy) {
+ this.proxy.size = next;
+ }
+ }
+
+ protected openChanged(): void {
+ if (!this.collapsible) {
+ return;
+ }
+
+ if (this.open) {
+ this.initializeOpenState();
+ this.indexWhenOpened = this.selectedIndex;
+
+ return;
+ }
+
+ this.filter = '';
+ if (this.filterInput) {
+ this.filterInput.value = '';
+ }
+
+ this.ariaControls = '';
+ this.ariaExpanded = 'false';
+ }
+
+ /**
+ * Updates the selectedness of each option when the list of selected options changes.
+ *
+ * @param prev - the previous list of selected options
+ * @param next - the current list of selected options
+ *
+ * @override
+ * @internal
+ */
+ protected override selectedOptionsChanged(
+ prev: ListboxOption[] | undefined,
+ next: ListboxOption[]
): void {
- if (this.region && this.control) {
- this.region.anchorElement = this.control;
+ super.selectedOptionsChanged(prev, next);
+ this.options?.forEach((o, i) => {
+ const proxyOption = this.proxy?.options.item(i);
+ if (proxyOption) {
+ proxyOption.selected = o.selected;
+ }
+ });
+ }
+
+ /**
+ * Sets the selected index to match the first option with the selected attribute, or
+ * the first selectable option.
+ *
+ * @override
+ * @internal
+ */
+ protected override setDefaultSelectedOption(): void {
+ const options: ListboxOption[] = this.options
+ ?? Array.from(this.children).filter(o => Listbox.slottedOptionFilter(o as HTMLElement));
+
+ const selectedIndex = options?.findIndex(
+ el => el.hasAttribute('selected')
+ || el.selected
+ || el.value === this.value
+ );
+
+ if (selectedIndex !== -1) {
+ this.selectedIndex = selectedIndex;
+ return;
+ }
+
+ this.selectedIndex = 0;
+ }
+
+ private setPositioning(): void {
+ if (!this.$fastController.isConnected) {
+ // Don't call setPositioning() until we're connected,
+ // since this.forcedPosition isn't initialized yet.
+ return;
+ }
+ const currentBox = this.getBoundingClientRect();
+ const viewportHeight = window.innerHeight;
+ const availableBottom = viewportHeight - currentBox.bottom;
+
+ if (this.forcedPosition) {
+ this.position = this.positionAttribute;
+ } else if (currentBox.top > availableBottom) {
+ this.position = SelectPosition.above;
+ } else {
+ this.position = SelectPosition.below;
+ }
+
+ this.positionAttribute = this.forcedPosition
+ ? this.positionAttribute
+ : this.position;
+
+ this.maxHeight = this.position === SelectPosition.above
+ ? Math.trunc(currentBox.top)
+ : Math.trunc(availableBottom);
+ this.updateListboxMaxHeightCssVariable();
+ }
+
+ /**
+ * Filter available options by text value.
+ *
+ * @public
+ */
+ private filterOptions(): void {
+ const filter = this.filter.toLowerCase();
+
+ if (filter) {
+ this.filteredOptions = this._options.filter(option => {
+ return diacriticInsensitiveStringNormalizer(
+ option.text
+ ).includes(diacriticInsensitiveStringNormalizer(filter));
+ });
+ } else {
+ this.filteredOptions = this._options;
+ }
+
+ this._options.forEach(o => {
+ o.hidden = !this.filteredOptions.includes(o);
+ });
+ }
+
+ /**
+ * Sets the value and display value to match the first selected option.
+ *
+ * @param shouldEmit - if true, the input and change events will be emitted
+ *
+ * @internal
+ */
+ private updateValue(shouldEmit?: boolean): void {
+ if (this.$fastController.isConnected) {
+ this.value = this.firstSelectedOption?.value ?? '';
+ }
+
+ if (shouldEmit) {
+ this.$emit('input');
+ this.$emit('change', this, {
+ bubbles: true,
+ composed: undefined
+ });
+ }
+ }
+
+ /**
+ * Resets and fills the proxy to match the component's options.
+ *
+ * @internal
+ */
+ private setProxyOptions(): void {
+ if (this.proxy instanceof HTMLSelectElement && this.options) {
+ this.proxy.options.length = 0;
+ this.options.forEach(option => {
+ const proxyOption = option.proxy
+ || (option instanceof HTMLOptionElement
+ ? option.cloneNode()
+ : null);
+
+ if (proxyOption) {
+ this.proxy.options.add(proxyOption);
+ }
+ });
}
}
+ private clearSelection(): void {
+ this.options.forEach(option => {
+ option.selected = false;
+ });
+ }
+
+ private filterChanged(): void {
+ this.filterOptions();
+ }
+
private maxHeightChanged(): void {
this.updateListboxMaxHeightCssVariable();
}
+ private initializeOpenState(): void {
+ if (!this.open) {
+ this.ariaExpanded = 'false';
+ this.ariaControls = '';
+ return;
+ }
+
+ this.committedSelectedOption = this._options[this.selectedIndex];
+ this.ariaControls = this.listboxId;
+ this.ariaExpanded = 'true';
+
+ this.setPositioning();
+ this.focusAndScrollOptionIntoView();
+ }
+
private updateListboxMaxHeightCssVariable(): void {
if (this.listbox) {
this.listbox.style.setProperty(
@@ -102,6 +878,26 @@ export class Select extends FoundationSelect implements ErrorPattern {
);
}
}
+
+ private updateSelectedIndexFromFilteredSet(): void {
+ const selectedItem = this.filteredOptions.length > 0
+ ? this.options[this.selectedIndex]
+ ?? this.committedSelectedOption
+ : this.committedSelectedOption;
+
+ if (!selectedItem) {
+ return;
+ }
+ // Clear filter so any logic resolving against 'this.options' resolves against all options,
+ // since selectedIndex should be relative to entire set.
+ this.filter = '';
+ // translate selectedIndex for filtered list to selectedIndex for all items
+ this.selectedIndex = this._options.indexOf(selectedItem);
+ // force selected to true again if the selection hasn't actually changed
+ if (selectedItem === this.committedSelectedOption) {
+ selectedItem.selected = true;
+ }
+ }
}
const nimbleSelect = Select.compose({
@@ -119,5 +915,6 @@ const nimbleSelect = Select.compose({
`
});
+applyMixins(Select, StartEnd, DelegatesARIASelect);
DesignSystem.getOrCreate().withPrefix('nimble').register(nimbleSelect());
export const selectTag = 'nimble-select';
diff --git a/packages/nimble-components/src/select/models/select-form-associated.ts b/packages/nimble-components/src/select/models/select-form-associated.ts
new file mode 100644
index 0000000000..4d52dab411
--- /dev/null
+++ b/packages/nimble-components/src/select/models/select-form-associated.ts
@@ -0,0 +1,18 @@
+// Based on: https://github.com/microsoft/fast/blob/%40microsoft/fast-foundation_v2.49.5/packages/web-components/fast-foundation/src/select/select.form-associated.ts
+/* eslint-disable max-classes-per-file */
+import { FormAssociated, ListboxElement } from '@microsoft/fast-foundation';
+
+// eslint-disable-next-line jsdoc/require-jsdoc
+class Select extends ListboxElement {}
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+interface Select extends FormAssociated {}
+
+/**
+ * A form-associated base class for the Select component. This was copied from the
+ * FAST FormAssociatedSelect (which is not exported by fast-foundation)
+ *
+ * @internal
+ */
+export class FormAssociatedSelect extends FormAssociated(Select) {
+ public proxy = document.createElement('select');
+}
diff --git a/packages/nimble-components/src/select/styles.ts b/packages/nimble-components/src/select/styles.ts
index 195c066073..c6dabeac0e 100644
--- a/packages/nimble-components/src/select/styles.ts
+++ b/packages/nimble-components/src/select/styles.ts
@@ -1,9 +1,21 @@
import { css } from '@microsoft/fast-element';
+import { White } from '@ni/nimble-tokens/dist/styledictionary/js/tokens';
import { styles as dropdownStyles } from '../patterns/dropdown/styles';
import { styles as errorStyles } from '../patterns/error/styles';
-import { borderWidth } from '../theme-provider/design-tokens';
+import {
+ borderRgbPartialColor,
+ borderWidth,
+ controlHeight,
+ mediumPadding,
+ placeholderFontColor,
+ smallPadding
+} from '../theme-provider/design-tokens';
import { appearanceBehavior } from '../utilities/style/appearance';
import { DropdownAppearance } from './types';
+import { focusVisible } from '../utilities/style/focus';
+import { themeBehavior } from '../utilities/style/theme';
+import { Theme } from '../theme-provider/types';
+import { hexToRgbaCssColor } from '../utilities/style/colors';
export const styles = css`
${dropdownStyles}
@@ -28,6 +40,84 @@ export const styles = css`
[part='end'] {
display: contents;
}
+
+ .listbox {
+ overflow-x: clip;
+ }
+
+ .listbox.empty slot {
+ display: none;
+ }
+
+ .listbox.above {
+ flex-flow: column-reverse;
+ }
+
+ .filter-field {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ height: ${controlHeight};
+ background: transparent;
+ }
+
+ .filter-field::before {
+ content: '';
+ position: absolute;
+ height: 0px;
+ border-bottom: rgba(${borderRgbPartialColor}, 0.1) 2px solid;
+ bottom: calc(${controlHeight} + ${smallPadding} - ${borderWidth});
+ }
+
+ .filter-field.above::before {
+ width: calc(100% - (2 * ${borderWidth}));
+ }
+
+ .filter-field::after {
+ content: '';
+ position: absolute;
+ height: 0px;
+ border-bottom: rgba(${borderRgbPartialColor}, 0.1) 2px solid;
+ top: calc(${controlHeight} + ${smallPadding} - ${borderWidth});
+ }
+
+ .filter-field:not(.above)::after {
+ width: calc(100% - (2 * ${borderWidth}));
+ }
+
+ .filter-icon {
+ padding-left: ${smallPadding};
+ }
+
+ .filter-input {
+ background: transparent;
+ border: none;
+ color: inherit;
+ font: inherit;
+ height: var(--ni-nimble-control-height);
+ padding: 0 ${smallPadding} 0 ${mediumPadding};
+ width: 100%;
+ }
+
+ .filter-input::placeholder {
+ color: ${placeholderFontColor};
+ }
+
+ .filter-input${focusVisible} {
+ outline: 0px;
+ }
+
+ .scrollable-region {
+ overflow: auto;
+ }
+
+ .no-results-label {
+ color: ${placeholderFontColor};
+ height: ${controlHeight};
+ display: inline-flex;
+ align-items: center;
+ padding: ${smallPadding} ${mediumPadding};
+ }
`.withBehaviors(
appearanceBehavior(
DropdownAppearance.block,
@@ -37,5 +127,14 @@ export const styles = css`
padding-bottom: 0;
}
`
+ ),
+ themeBehavior(
+ Theme.color,
+ css`
+ .filter-field,
+ .no-results-label {
+ background: ${hexToRgbaCssColor(White, 0.15)};
+ }
+ `
)
);
diff --git a/packages/nimble-components/src/select/template.ts b/packages/nimble-components/src/select/template.ts
index ecdb1c1b23..e5c2cdeb4e 100644
--- a/packages/nimble-components/src/select/template.ts
+++ b/packages/nimble-components/src/select/template.ts
@@ -16,7 +16,14 @@ import type { Select } from '.';
import { anchoredRegionTag } from '../anchored-region';
import { DropdownPosition } from '../patterns/dropdown/types';
import { overflow } from '../utilities/directive/overflow';
+import { iconMagnifyingGlassTag } from '../icons/magnifying-glass';
+import {
+ filterNoResultsLabel,
+ filterSearchLabel
+} from '../label-provider/core/label-tokens';
+import { FilterMode } from './types';
+/* eslint-disable @typescript-eslint/indent */
// prettier-ignore
export const template: FoundationElementTemplate<
ViewTemplate,
@@ -24,15 +31,13 @@ SelectOptions
> = (context, definition) => html`
x.clickHandler(c.event as MouseEvent)}"
+ @change="${x => x.changeValueHandler()}"
+ @contentchange="${x => x.updateDisplayValue()}"
@focusin="${(x, c) => x.focusinHandler(c.event as FocusEvent)}"
@focusout="${(x, c) => x.focusoutHandler(c.event as FocusEvent)}"
@keydown="${(x, c) => x.keydownHandler(c.event as KeyboardEvent)}"
@mousedown="${(x, c) => x.mousedownHandler(c.event as MouseEvent)}"
>
- ${when(
- x => x.collapsible,
- html`
- x.disabled}"
- ${ref('control')}
- >
- ${startSlotTemplate(context, definition)}
-
- (x.hasOverflow && x.displayValue ? x.displayValue : null)}>
- ${x => x.displayValue}
-
-
-
- ${definition.indicator || ''}
-
-
-
- ${endSlotTemplate(context, definition)}
-
- `
- )}
+ ${when(x => x.collapsible, html`
+ x.disabled}"
+ ${ref('control')}
+ >
+ ${startSlotTemplate(context, definition)}
+
+ (x.hasOverflow && x.displayValue ? x.displayValue : null)}>
+ ${x => x.displayValue}
+
+
+
+ ${definition.indicator || ''}
+
+
+
+ ${endSlotTemplate(context, definition)}
+
+ `)
+ }
<${anchoredRegionTag}
- ${ref('region')}
+ ${ref('anchoredRegion')}
class="anchored-region"
fixed-placement
auto-update-mode="auto"
@@ -80,22 +85,52 @@ SelectOptions
horizontal-default-position="center"
horizontal-positioning-mode="locktodefault"
horizontal-scaling="anchor"
+ @loaded="${x => x.regionLoadedHandler()}"
?hidden="${x => (x.collapsible ? !x.open : false)}">
- x.disabled}"
- ${ref('listbox')}
- >
-
n instanceof HTMLElement && Listbox.slottedOptionFilter(n),
- flatten: true,
- property: 'slottedOptions',
- })}
- >
+
+
x.disabled}"
+ ${ref('listbox')}
+ >
+ ${when(x => x.filterMode !== FilterMode.none, html
`
+
+ <${iconMagnifyingGlassTag} class="filter-icon">${iconMagnifyingGlassTag}>
+ x.inputHandler(c.event as InputEvent)}"
+ @click="${(x, c) => x.inputClickHandler(c.event as MouseEvent)}"
+ placeholder="${x => filterSearchLabel.getValueFor(x)}"
+ value="${x => x.filter}"
+ />
+
+ `)}
+
+ n instanceof HTMLElement && Listbox.slottedOptionFilter(n),
+ flatten: true,
+ property: 'slottedOptions',
+ })}
+ >
+
+ ${when(x => (x.filterMode !== FilterMode.none && x.filteredOptions.length === 0), html`
+
+ ${x => filterNoResultsLabel.getValueFor(x)}
+
+ `)}
+
${anchoredRegionTag}>
diff --git a/packages/nimble-components/src/select/testing/select.pageobject.ts b/packages/nimble-components/src/select/testing/select.pageobject.ts
new file mode 100644
index 0000000000..a4d0df9c90
--- /dev/null
+++ b/packages/nimble-components/src/select/testing/select.pageobject.ts
@@ -0,0 +1,186 @@
+import {
+ keyEnter,
+ keyEscape,
+ keyArrowDown,
+ keySpace
+} from '@microsoft/fast-web-utilities';
+import type { Select } from '..';
+import type { ListOption } from '../../list-option';
+import { waitForUpdatesAsync } from '../../testing/async-helpers';
+import { FilterMode } from '../types';
+
+/**
+ * Page object for the `nimble-select` component to provide consistent ways
+ * of querying and interacting with the component during tests.
+ */
+export class SelectPageObject {
+ public constructor(private readonly selectElement: Select) {}
+
+ public async openAndSetFilterText(filterText: string): Promise
{
+ if (this.selectElement.filterMode === FilterMode.none) {
+ throw new Error(
+ 'Can not set filter text with filterMode set to "none".'
+ );
+ }
+ await this.clickSelect();
+ const filterInput = this.getFilterInput();
+ if (filterInput) {
+ filterInput.value = filterText;
+ }
+
+ await waitForUpdatesAsync();
+ const inputEvent = new InputEvent('input');
+ filterInput?.dispatchEvent(inputEvent);
+ await waitForUpdatesAsync();
+ }
+
+ public async closeDropdown(): Promise {
+ this.selectElement.open = false;
+ await waitForUpdatesAsync();
+ }
+
+ public async setOptions(options: ListOption[]): Promise {
+ options.forEach(option => {
+ option.setAttribute('role', 'option');
+ });
+ this.selectElement.slottedOptions = options;
+ await waitForUpdatesAsync();
+ }
+
+ public getFilteredOptions(): ListOption[] {
+ return this.selectElement.filteredOptions as ListOption[];
+ }
+
+ public getSelectedOption(): ListOption | null {
+ return (this.selectElement.selectedOptions[0] as ListOption) ?? null;
+ }
+
+ /**
+ * Either opens or closes the dropdown depending on its current state
+ */
+ public async clickSelect(): Promise {
+ this.selectElement.dispatchEvent(new Event('click'));
+ await waitForUpdatesAsync();
+ }
+
+ public clickSelectedItem(): void {
+ if (!this.selectElement.open) {
+ throw new Error('Select must be open to click selectedItem');
+ }
+
+ this.clickOption(this.selectElement.selectedIndex);
+ }
+
+ public async clickFilterInput(): Promise {
+ if (!this.selectElement.filterInput) {
+ throw new Error('Filter input is not available.');
+ }
+ this.selectElement.filterInput.click();
+ await waitForUpdatesAsync();
+ }
+
+ public clickOption(index: number): void {
+ if (!this.selectElement.open) {
+ throw new Error('Select must be open to click selectedItem');
+ }
+
+ if (index >= this.selectElement.options.length) {
+ throw new Error(
+ '"index" greater than number of current displayed options'
+ );
+ }
+
+ const option = this.selectElement.options[index]!;
+ option.scrollIntoView();
+ const optionRect = option.getClientRects()[0]!;
+ const clickEvent = new MouseEvent('click', {
+ clientY: optionRect.y + optionRect.height / 2,
+ clientX: optionRect.width / 2,
+ bubbles: true
+ });
+ option.dispatchEvent(clickEvent);
+ }
+
+ public async clickAway(): Promise {
+ this.selectElement.dispatchEvent(new Event('focusout'));
+ await waitForUpdatesAsync();
+ }
+
+ public pressEnterKey(): void {
+ this.selectElement.dispatchEvent(
+ new KeyboardEvent('keydown', { key: keyEnter })
+ );
+ }
+
+ public pressEscapeKey(): void {
+ this.selectElement.dispatchEvent(
+ new KeyboardEvent('keydown', { key: keyEscape })
+ );
+ }
+
+ public pressArrowDownKey(): void {
+ this.selectElement.dispatchEvent(
+ new KeyboardEvent('keydown', { key: keyArrowDown })
+ );
+ }
+
+ public async pressSpaceKey(): Promise {
+ const alreadyOpen = this.selectElement.open;
+ this.selectElement.dispatchEvent(
+ new KeyboardEvent('keydown', { key: keySpace, bubbles: true })
+ );
+ await waitForUpdatesAsync();
+ if (
+ this.selectElement.filterMode === FilterMode.standard
+ && alreadyOpen
+ ) {
+ // add space to end of current filter
+ const filterValue = `${
+ this.selectElement.filterInput?.value ?? ''
+ } `;
+ if (this.selectElement.filterInput) {
+ this.selectElement.filterInput.value = filterValue;
+ }
+ this.selectElement.filterInput?.dispatchEvent(
+ new InputEvent('input', { inputType: 'insertText' })
+ );
+ }
+ await waitForUpdatesAsync();
+ }
+
+ public isDropdownVisible(): boolean {
+ return (
+ this.selectElement.shadowRoot?.querySelector('.listbox') !== null
+ );
+ }
+
+ public isFilterInputVisible(): boolean {
+ return (
+ this.selectElement.shadowRoot?.querySelector('.filter-field')
+ !== null
+ );
+ }
+
+ public isNoResultsLabelVisible(): boolean {
+ return (
+ this.selectElement.shadowRoot?.querySelector(
+ '.no-results-label'
+ ) !== null
+ );
+ }
+
+ public getFilterInputText(): string {
+ return this.getFilterInput()?.value ?? '';
+ }
+
+ private getFilterInput(): HTMLInputElement | null | undefined {
+ if (this.selectElement.filterMode === FilterMode.none) {
+ throw new Error(
+ 'Select has filterMode of "none" so there is no filter input'
+ );
+ }
+ return this.selectElement.shadowRoot?.querySelector(
+ '.filter-input'
+ );
+ }
+}
diff --git a/packages/nimble-components/src/select/tests/select-matrix.stories.ts b/packages/nimble-components/src/select/tests/select-matrix.stories.ts
index 05591ea89d..4b80bb4bb7 100644
--- a/packages/nimble-components/src/select/tests/select-matrix.stories.ts
+++ b/packages/nimble-components/src/select/tests/select-matrix.stories.ts
@@ -1,11 +1,9 @@
import type { StoryFn, Meta } from '@storybook/html';
import { html, ViewTemplate } from '@microsoft/fast-element';
import { pascalCase } from '@microsoft/fast-web-utilities';
+import { createStory } from '../../utilities/tests/storybook';
import {
createMatrixThemeStory,
- createStory
-} from '../../utilities/tests/storybook';
-import {
createMatrix,
sharedMatrixParameters
} from '../../utilities/tests/matrix';
diff --git a/packages/nimble-components/src/select/tests/select-opened-matrix.stories.ts b/packages/nimble-components/src/select/tests/select-opened-matrix.stories.ts
index c3a9394633..dd5661b6c4 100644
--- a/packages/nimble-components/src/select/tests/select-opened-matrix.stories.ts
+++ b/packages/nimble-components/src/select/tests/select-opened-matrix.stories.ts
@@ -3,8 +3,9 @@ import { html, ViewTemplate } from '@microsoft/fast-element';
import { createFixedThemeStory } from '../../utilities/tests/storybook';
import { sharedMatrixParameters } from '../../utilities/tests/matrix';
import { backgroundStates } from '../../utilities/tests/states';
-import { selectTag } from '..';
+import { Select, selectTag } from '..';
import { listOptionTag } from '../../list-option';
+import { FilterMode } from '../types';
const metadata: Meta = {
title: 'Tests/Select',
@@ -17,16 +18,19 @@ export default metadata;
const positionStates = [
['below', 'margin-bottom: 120px;'],
- ['above', 'margin-top: 120px;']
+ ['above', 'margin-top: 180px;']
] as const;
type PositionState = (typeof positionStates)[number];
+const filterModeStates = Object.values(FilterMode);
+type FilterModeState = (typeof filterModeStates)[number];
+
// prettier-ignore
-const component = ([
- position,
- positionStyle
-]: PositionState): ViewTemplate => html`
- <${selectTag} open position="${() => position}" style="${() => positionStyle}">
+const component = (
+ [position, positionStyle]: PositionState,
+ filterMode: FilterModeState
+): ViewTemplate => html`
+ <${selectTag} open position="${() => position}" style="${() => positionStyle}" filter-mode="${() => filterMode}">
<${listOptionTag} value="1">Option 1${listOptionTag}>
<${listOptionTag} value="2" disabled>Option 2${listOptionTag}>
<${listOptionTag} value="3">Option 3${listOptionTag}>
@@ -45,29 +49,109 @@ if (remaining.length > 0) {
throw new Error('New backgrounds need to be supported');
}
-export const selectBelowOpenLightThemeWhiteBackground: StoryFn = createFixedThemeStory(
- component(positionStates[0]),
+export const selectBelowOpenNoFilterLightThemeWhiteBackground: StoryFn = createFixedThemeStory(
+ component(positionStates[0], FilterMode.none),
+ lightThemeWhiteBackground
+);
+
+export const selectBelowOpenStandardFilterLightThemeWhiteBackground: StoryFn = createFixedThemeStory(
+ component(positionStates[0], FilterMode.standard),
+ lightThemeWhiteBackground
+);
+
+export const selectAboveOpenNoFilterLightThemeWhiteBackground: StoryFn = createFixedThemeStory(
+ component(positionStates[1], FilterMode.none),
lightThemeWhiteBackground
);
-export const selectAboveOpenLightThemeWhiteBackground: StoryFn = createFixedThemeStory(
- component(positionStates[1]),
+
+export const selectAboveOpenStandardFilterLightThemeWhiteBackground: StoryFn = createFixedThemeStory(
+ component(positionStates[1], FilterMode.standard),
lightThemeWhiteBackground
);
-export const selectBelowOpenColorThemeDarkGreenBackground: StoryFn = createFixedThemeStory(
- component(positionStates[0]),
+export const selectBelowOpenColorNoFilterThemeDarkGreenBackground: StoryFn = createFixedThemeStory(
+ component(positionStates[0], FilterMode.none),
+ colorThemeDarkGreenBackground
+);
+
+export const selectBelowOpenColorStandardFilterThemeDarkGreenBackground: StoryFn = createFixedThemeStory(
+ component(positionStates[0], FilterMode.standard),
+ colorThemeDarkGreenBackground
+);
+
+export const selectAboveOpenNoFilterColorThemeDarkGreenBackground: StoryFn = createFixedThemeStory(
+ component(positionStates[1], FilterMode.none),
colorThemeDarkGreenBackground
);
-export const selectAboveOpenColorThemeDarkGreenBackground: StoryFn = createFixedThemeStory(
- component(positionStates[1]),
+
+export const selectAboveOpenStandardFilterColorThemeDarkGreenBackground: StoryFn = createFixedThemeStory(
+ component(positionStates[1], FilterMode.standard),
colorThemeDarkGreenBackground
);
-export const selectBelowOpenDarkThemeBlackBackground: StoryFn = createFixedThemeStory(
- component(positionStates[0]),
+export const selectBelowOpenNoFilterDarkThemeBlackBackground: StoryFn = createFixedThemeStory(
+ component(positionStates[0], FilterMode.none),
+ darkThemeBlackBackground
+);
+
+export const selectBelowOpenStandardFilterDarkThemeBlackBackground: StoryFn = createFixedThemeStory(
+ component(positionStates[0], FilterMode.standard),
+ darkThemeBlackBackground
+);
+
+export const selectAboveOpenNoFilterDarkThemeBlackBackground: StoryFn = createFixedThemeStory(
+ component(positionStates[1], FilterMode.none),
+ darkThemeBlackBackground
+);
+
+export const selectAboveOpenStandardFilterDarkThemeBlackBackground: StoryFn = createFixedThemeStory(
+ component(positionStates[1], FilterMode.standard),
darkThemeBlackBackground
);
-export const selectAboveOpenDarkThemeBlackBackground: StoryFn = createFixedThemeStory(
- component(positionStates[1]),
+
+const noMatchesFilterPlayFunction = (): void => {
+ const select = document.querySelector('nimble-select');
+ select!.filter = 'abc';
+};
+
+export const selectAboveOpenFilterNoMatchDarkThemeBlackBackground: StoryFn = createFixedThemeStory(
+ component(positionStates[1], FilterMode.standard),
+ darkThemeBlackBackground
+);
+
+selectAboveOpenFilterNoMatchDarkThemeBlackBackground.play = noMatchesFilterPlayFunction;
+
+export const selectAboveOpenFilterNoMatchLightThemeWhiteBackground: StoryFn = createFixedThemeStory(
+ component(positionStates[1], FilterMode.standard),
+ lightThemeWhiteBackground
+);
+
+selectAboveOpenFilterNoMatchLightThemeWhiteBackground.play = noMatchesFilterPlayFunction;
+
+export const selectAboveOpenFilterNoMatchColorThemeGreenBackground: StoryFn = createFixedThemeStory(
+ component(positionStates[1], FilterMode.standard),
+ colorThemeDarkGreenBackground
+);
+
+selectAboveOpenFilterNoMatchColorThemeGreenBackground.play = noMatchesFilterPlayFunction;
+
+export const selectBelowOpenFilterNoMatchDarkThemeBlackBackground: StoryFn = createFixedThemeStory(
+ component(positionStates[0], FilterMode.standard),
darkThemeBlackBackground
);
+
+selectBelowOpenFilterNoMatchDarkThemeBlackBackground.play = noMatchesFilterPlayFunction;
+
+export const selectBelowOpenFilterNoMatchLightThemeWhiteBackground: StoryFn = createFixedThemeStory(
+ component(positionStates[0], FilterMode.standard),
+ lightThemeWhiteBackground
+);
+
+selectBelowOpenFilterNoMatchLightThemeWhiteBackground.play = noMatchesFilterPlayFunction;
+
+export const selectBelowOpenFilterNoMatchColorThemeGreenBackground: StoryFn = createFixedThemeStory(
+ component(positionStates[0], FilterMode.standard),
+ colorThemeDarkGreenBackground
+);
+
+selectBelowOpenFilterNoMatchColorThemeGreenBackground.play = noMatchesFilterPlayFunction;
diff --git a/packages/nimble-components/src/select/tests/select.mdx b/packages/nimble-components/src/select/tests/select.mdx
new file mode 100644
index 0000000000..89cfa0fccb
--- /dev/null
+++ b/packages/nimble-components/src/select/tests/select.mdx
@@ -0,0 +1,24 @@
+import { Canvas, Meta, Controls, Title } from '@storybook/blocks';
+import { NimbleSelect } from './select.react';
+import * as selectStories from './select.stories';
+import { listOptionTag } from '../../list-option';
+
+
+
+
+Select is a control for selecting amongst a set of options. Its value comes from the `value` of the currently selected , or, if no value exists for that option, the option's content. Upon clicking on the element, the other options are visible. The user cannot manually enter values, and thus the list cannot be filtered.
+
+
+
+
+{/* ## Appearances */}
+
+{/* ## Appearance Variants */}
+
+{/* ## Usage */}
+
+{/* ## Examples */}
+
+{/* ## Accessibility */}
+
+{/* ## Resources */}
diff --git a/packages/nimble-components/src/select/tests/select.spec.ts b/packages/nimble-components/src/select/tests/select.spec.ts
index fb7a8c8513..d801913fac 100644
--- a/packages/nimble-components/src/select/tests/select.spec.ts
+++ b/packages/nimble-components/src/select/tests/select.spec.ts
@@ -1,10 +1,13 @@
import { html, repeat } from '@microsoft/fast-element';
import { fixture, Fixture } from '../../utilities/tests/fixture';
import { Select, selectTag } from '..';
-import { listOptionTag } from '../../list-option';
+import { ListOption, listOptionTag } from '../../list-option';
import { waitForUpdatesAsync } from '../../testing/async-helpers';
-import { createEventListener } from '../../utilities/tests/component';
import { checkFullyInViewport } from '../../utilities/tests/intersection-observer';
+import { FilterMode } from '../types';
+import { SelectPageObject } from '../testing/select.pageobject';
+import { createEventListener } from '../../utilities/tests/component';
+import { filterSearchLabel } from '../../label-provider/core/label-tokens';
async function setup(
position?: string,
@@ -18,6 +21,11 @@ async function setup(
One
Two
Three
+ T Disabled
+ Zürich
+ Has Space
`;
return fixture(viewTemplate);
@@ -133,6 +141,58 @@ describe('Select', () => {
await disconnect();
});
+ it('filter input is not visible with default filterMode', async () => {
+ const { element, connect, disconnect } = await setup();
+ await connect();
+ const pageObject = new SelectPageObject(element);
+ await pageObject.clickSelect();
+ expect(pageObject.isDropdownVisible()).toBeTrue();
+ expect(pageObject.isFilterInputVisible()).toBeFalse();
+
+ await disconnect();
+ });
+
+ it('pressing Esc after navigating to new option in dropdown maintains original selected option', async () => {
+ const { element, connect, disconnect } = await setup();
+ await connect();
+ const pageObject = new SelectPageObject(element);
+ await pageObject.clickSelect();
+ pageObject.pressArrowDownKey();
+ await waitForUpdatesAsync();
+ expect(element.selectedIndex).toBe(1);
+
+ pageObject.pressEscapeKey();
+ await waitForUpdatesAsync();
+
+ expect(element.value).toBe('one');
+ await disconnect();
+ });
+
+ it('navigating between options in dropdown does not update display value', async () => {
+ const { element, connect, disconnect } = await setup();
+ await connect();
+ const pageObject = new SelectPageObject(element);
+ await pageObject.clickSelect();
+ pageObject.pressArrowDownKey();
+ await waitForUpdatesAsync();
+
+ expect(element.displayValue).toBe('One');
+ await disconnect();
+ });
+
+ it('pressing will select a new value and close dropdown', async () => {
+ const { element, connect, disconnect } = await setup();
+ await connect();
+ const pageObject = new SelectPageObject(element);
+ await pageObject.clickSelect();
+ pageObject.pressArrowDownKey();
+ await pageObject.pressSpaceKey();
+ expect(element.value).toBe('two');
+ expect(element.open).toBeFalse();
+
+ await disconnect();
+ });
+
describe('with 500 options', () => {
async function setup500Options(): Promise> {
// prettier-ignore
@@ -151,7 +211,7 @@ describe('Select', () => {
await clickAndWaitForOpen(element);
const fullyVisible = await checkFullyInViewport(element.listbox);
- expect(element.listbox.scrollHeight).toBeGreaterThan(
+ expect(element.scrollableRegion.scrollHeight).toBeGreaterThan(
window.innerHeight
);
expect(fullyVisible).toBe(true);
@@ -258,4 +318,320 @@ describe('Select', () => {
expect(getSelectedValueTitle()).toBe('');
});
});
+
+ describe('opening and closing dropdown', () => {
+ let element: Select;
+ let connect: () => Promise;
+ let disconnect: () => Promise;
+ let pageObject: SelectPageObject;
+
+ beforeEach(async () => {
+ ({ element, connect, disconnect } = await setup());
+ element.style.width = '200px';
+ element.filterMode = FilterMode.standard;
+ await connect();
+ pageObject = new SelectPageObject(element);
+ });
+
+ afterEach(async () => {
+ await disconnect();
+ });
+
+ const filterModeTestData = [
+ {
+ filter: FilterMode.none,
+ name: 'none'
+ },
+ {
+ filter: FilterMode.standard,
+ name: 'standard'
+ }
+ ];
+ filterModeTestData.forEach(testData => {
+ describe(`with filterMode = ${testData.name}`, () => {
+ it('pressing opens dropdown', () => {
+ element.filterMode = testData.filter;
+ pageObject.pressEnterKey();
+ expect(element.open).toBeTrue();
+ });
+
+ it('pressing opens dropdown', async () => {
+ element.filterMode = testData.filter;
+ await pageObject.pressSpaceKey();
+ expect(element.open).toBeTrue();
+ });
+
+ it('after pressing to close dropdown, will re-open dropdown', async () => {
+ element.filterMode = testData.filter;
+ await 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 () => {
+ element.filterMode = testData.filter;
+ await pageObject.clickSelect();
+ pageObject.pressEscapeKey();
+ expect(document.activeElement).toBe(element);
+ });
+
+ it('after closing dropdown by committing a value with , activeElement is Select element', async () => {
+ element.filterMode = testData.filter;
+ await pageObject.clickSelect();
+ pageObject.pressArrowDownKey();
+ pageObject.pressEnterKey();
+ expect(document.activeElement).toBe(element);
+ });
+ });
+ });
+ });
+
+ describe('filtering', () => {
+ let element: Select;
+ let connect: () => Promise;
+ let disconnect: () => Promise;
+ let pageObject: SelectPageObject;
+
+ beforeEach(async () => {
+ ({ element, connect, disconnect } = await setup());
+ element.style.width = '200px';
+ element.filterMode = FilterMode.standard;
+ await connect();
+ pageObject = new SelectPageObject(element);
+ });
+
+ afterEach(async () => {
+ await disconnect();
+ });
+
+ it('matches any character in option strings', async () => {
+ await pageObject.openAndSetFilterText('o'); // Matches 'One' and 'Two'
+ const filteredOptions = pageObject
+ .getFilteredOptions()
+ .map(option => option.text);
+ await pageObject.closeDropdown();
+ expect(filteredOptions).toContain('One');
+ expect(filteredOptions).toContain('Two');
+ expect(filteredOptions.length).toBe(2);
+ });
+
+ it('matches diacritic characters in option strings', async () => {
+ await pageObject.openAndSetFilterText('u'); // Matches 'Zürich'
+ const filteredOptions = pageObject
+ .getFilteredOptions()
+ .map(option => option.text);
+ await pageObject.closeDropdown();
+ expect(filteredOptions).toContain('Zürich');
+ expect(filteredOptions.length).toBe(1);
+ });
+
+ it('filtering out current selected item changes selected item but not value', async () => {
+ let currentSelection = pageObject.getSelectedOption();
+ expect(currentSelection?.text).toBe('One');
+ expect(element.value).toBe('one');
+
+ await pageObject.openAndSetFilterText('T'); // Matches 'Two' and 'Three'
+ currentSelection = pageObject.getSelectedOption();
+ expect(currentSelection?.text).toBe('Two');
+ expect(element.value).toBe('one');
+ });
+
+ it('filtering out current selected item and then pressing does not change value, reverts selected item and closes popup', async () => {
+ let currentSelection = pageObject.getSelectedOption();
+ expect(currentSelection?.text).toBe('One');
+ expect(element.value).toBe('one');
+
+ await pageObject.openAndSetFilterText('T'); // Matches 'Two' and 'Three'
+ pageObject.pressEscapeKey();
+ currentSelection = pageObject.getSelectedOption();
+ expect(currentSelection?.text).toBe('One');
+ expect(element.value).toBe('one');
+ expect(element.open).toBeFalse();
+ });
+
+ it('opening popup shows correct selected element after cancelling previous selection', async () => {
+ let currentSelection = pageObject.getSelectedOption();
+ expect(currentSelection?.text).toBe('One');
+ expect(element.value).toBe('one');
+
+ await pageObject.openAndSetFilterText('T'); // Matches 'Two' and 'Three'
+ pageObject.pressEscapeKey();
+
+ await pageObject.clickSelect();
+ currentSelection = pageObject.getSelectedOption();
+ expect(currentSelection?.selected).toBeTrue();
+ });
+
+ it('opening popup shows correct selected element after filtering and committing but not changing selected option', async () => {
+ let currentSelection = pageObject.getSelectedOption();
+ expect(currentSelection?.text).toBe('One');
+ expect(element.value).toBe('one');
+
+ await pageObject.openAndSetFilterText('One'); // Matches 'One'
+ pageObject.pressEnterKey();
+
+ await pageObject.clickSelect();
+ currentSelection = pageObject.getSelectedOption();
+ expect(currentSelection?.selected).toBeTrue();
+ });
+
+ it('filtering out current selected item and then pressing changes value and closes popup', async () => {
+ const currentSelection = pageObject.getSelectedOption();
+ expect(currentSelection?.text).toBe('One');
+ expect(element.value).toBe('one');
+
+ await pageObject.openAndSetFilterText('T'); // Matches 'Two' and 'Three'
+ pageObject.pressEnterKey();
+ expect(element.value).toBe('two'); // 'Two' is first option in list so it should be selected now
+ expect(element.open).toBeFalse();
+ });
+
+ it('filtering out current selected item and then clicking selected option changes value and closes popup', async () => {
+ const currentSelection = pageObject.getSelectedOption();
+ expect(currentSelection?.text).toBe('One');
+ expect(element.value).toBe('one');
+
+ await pageObject.openAndSetFilterText('T'); // Matches 'Two' and 'Three'
+ pageObject.clickSelectedItem();
+ expect(element.value).toBe('two'); // 'Two' is first option in list so it should be selected now
+ expect(element.open).toBeFalse();
+ });
+
+ it('filtering out current selected item and then clicking non-selected option changes value and closes popup', async () => {
+ const currentSelection = pageObject.getSelectedOption();
+ expect(currentSelection?.text).toBe('One');
+ expect(element.value).toBe('one');
+
+ await pageObject.openAndSetFilterText('T'); // Matches 'Two' and 'Three'
+ pageObject.clickOption(1); // index 1 matches option with 'Three' text
+ expect(element.value).toBe('three');
+ expect(element.open).toBeFalse();
+ });
+
+ it('filtering out current selected item and then losing focus changes value and closes popup', async () => {
+ const currentSelection = pageObject.getSelectedOption();
+ expect(currentSelection?.text).toBe('One');
+ expect(element.value).toBe('one');
+
+ await pageObject.openAndSetFilterText('T'); // Matches 'Two' and 'Three'
+ await pageObject.clickAway();
+ expect(element.value).toBe('two'); // 'Two' is first option in list so it should be selected now
+ expect(element.open).toBeFalse();
+ });
+
+ it('allows to be used as part of filter text', async () => {
+ await pageObject.openAndSetFilterText(' '); // Matches 'Has Space'
+ const currentSelection = pageObject.getSelectedOption();
+ expect(currentSelection?.text).toBe('Has Space');
+ expect(element.open).toBeTrue();
+ });
+
+ it('pressing after dropdown is open will enter " " as filter text and keep dropdown open', async () => {
+ await pageObject.clickSelect();
+ await waitForUpdatesAsync();
+ await pageObject.pressSpaceKey();
+ await waitForUpdatesAsync();
+ expect(element.open).toBeTrue();
+ expect(pageObject.getFilterInputText()).toBe(' ');
+ });
+
+ 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();
+
+ expect(pageObject.getFilterInputText()).toBe('');
+ expect(pageObject.getFilteredOptions().length).toBe(6);
+ });
+
+ it('entering filter text with no match results in "no items found" element', async () => {
+ await pageObject.openAndSetFilterText('abc'); // Matches nothing
+ expect(pageObject.isNoResultsLabelVisible()).toBeTrue();
+ });
+
+ it('entering filter text with matches does not display "not items found" element', async () => {
+ await pageObject.openAndSetFilterText('T');
+ expect(pageObject.isNoResultsLabelVisible()).toBeFalse();
+ });
+
+ it('opening dropdown with no filter does not display "not items found" element', async () => {
+ await pageObject.clickSelect();
+ expect(pageObject.isNoResultsLabelVisible()).toBeFalse();
+ });
+
+ it('clicking disabled option does not cause select to change state', async () => {
+ await pageObject.openAndSetFilterText('T');
+ const currentFilteredOptions = pageObject.getFilteredOptions();
+ pageObject.clickOption(2); // click disabled option
+
+ expect(pageObject.getFilteredOptions()).toEqual(
+ currentFilteredOptions
+ );
+ expect(element.open).toBeTrue();
+ expect(pageObject.getSelectedOption()?.text).toBe('Two');
+ });
+
+ it('filtering to only disabled item, then pressing does not close popup or change value', async () => {
+ await pageObject.openAndSetFilterText('Disabled');
+ pageObject.pressEnterKey();
+ expect(element.open).toBeTrue();
+ expect(element.value).toBe('one');
+ });
+
+ it('filtering to no available options, then pressing does not close popup or change value', async () => {
+ await pageObject.openAndSetFilterText('abc');
+ pageObject.pressEnterKey();
+ expect(element.open).toBeTrue();
+ expect(element.value).toBe('one');
+ });
+
+ it('filtering to only disabled item, then clicking away does not change value', async () => {
+ await pageObject.openAndSetFilterText('Disabled');
+ await pageObject.clickAway();
+ const currentSelection = pageObject.getSelectedOption();
+ expect(currentSelection?.value).toBe('one');
+ });
+
+ it('filtering to only disabled item does not select item', async () => {
+ await pageObject.openAndSetFilterText('Disabled');
+ expect(pageObject.getSelectedOption()).toBeNull();
+ });
+
+ it('updating slottedOptions while open applies filter to new options', async () => {
+ const newOptions = [
+ new ListOption('Ten', 'ten'),
+ new ListOption('Twenty', 'twenty')
+ ];
+ await pageObject.openAndSetFilterText('tw');
+ expect(pageObject.getFilteredOptions()[0]?.value).toBe('two');
+ await pageObject.setOptions(newOptions);
+ expect(pageObject.getFilteredOptions()[0]?.value).toBe('twenty');
+ });
+
+ it('clicking in filter input after dropdown is open, does not close dropdown', async () => {
+ await pageObject.clickSelect();
+ await pageObject.clickFilterInput();
+ expect(element.open).toBeTrue();
+ });
+
+ it('filter input placeholder gets text from design token', async () => {
+ filterSearchLabel.setValueFor(element, 'foo');
+ await waitForUpdatesAsync();
+ const filterInput = element.shadowRoot?.querySelector('.filter-input');
+ expect(filterInput?.getAttribute('placeholder')).toBe('foo');
+ });
+
+ it('filter input "aria-controls" and "aria-activedescendant" attributes are set to element state', async () => {
+ await pageObject.clickSelect();
+ const filterInput = element.shadowRoot?.querySelector('.filter-input');
+ expect(filterInput?.getAttribute('aria-controls')).toBe(
+ element.ariaControls
+ );
+ expect(filterInput?.getAttribute('aria-activedescendant')).toBe(
+ element.ariaActiveDescendant
+ );
+ });
+ });
});
diff --git a/packages/nimble-components/src/select/tests/select.stories.ts b/packages/nimble-components/src/select/tests/select.stories.ts
index b744acbefa..e6225882d0 100644
--- a/packages/nimble-components/src/select/tests/select.stories.ts
+++ b/packages/nimble-components/src/select/tests/select.stories.ts
@@ -1,6 +1,6 @@
import { html, repeat } from '@microsoft/fast-element';
import { withActions } from '@storybook/addon-actions/decorator';
-import type { Meta, StoryObj } from '@storybook/html';
+import type { HtmlRenderer, Meta, StoryObj } from '@storybook/html';
import {
createUserSelectedThemeStory,
disableStorybookZoomTransform
@@ -10,6 +10,7 @@ import { selectTag } from '..';
import { listOptionTag } from '../../list-option';
import { ExampleOptionsType } from './types';
import { menuMinWidth } from '../../theme-provider/design-tokens';
+import { FilterMode } from '../types';
interface SelectArgs {
disabled: boolean;
@@ -18,6 +19,7 @@ interface SelectArgs {
dropDownPosition: string;
optionsType: ExampleOptionsType;
appearance: string;
+ filterMode: keyof typeof FilterMode;
}
interface OptionArgs {
@@ -30,7 +32,8 @@ const simpleOptions: readonly OptionArgs[] = [
{ label: 'Option 1', value: '1', disabled: false },
{ label: 'Option 2', value: '2', disabled: true },
{ label: 'Option 3', value: '3', disabled: false },
- { label: 'Option 4', value: '4', disabled: false }
+ { label: 'Option 4', value: '4', disabled: false },
+ { label: 'Zürich', value: '5', disabled: false }
] as const;
const wideOptions: readonly OptionArgs[] = [
@@ -62,17 +65,18 @@ const optionSets = {
[ExampleOptionsType.manyOptions]: manyOptions
} as const;
+const filterModeDescription = `
+This attribute controls the filtering behavior of the \`Select\`. The default of \`none\` results in a dropdown with no input for filtering. A non-'none' setting results in a search input placed at the top or the bottom of the dropdown when opened (depending on where the popup is shown relative to the component). The \`standard\` setting will perform a case-insensitive and diacritic-insensitive filtering of the available options anywhere within the text of each option.
+
+The act of filtering will use the \`hidden\` attribute on the options to remove and re-add them to the visible set. Thus, any client-provided \`hidden\` settings of the options will be overridden.
+
+It is recommended that if the \`Select\` has 15 or fewer options that you use the \`none\` setting for the \`filter-mode\`.
+`;
+
const metadata: Meta = {
title: 'Components/Select',
- tags: ['autodocs'],
- decorators: [withActions],
+ decorators: [withActions],
parameters: {
- docs: {
- description: {
- component:
- "Select is a control for selecting amongst a set of options. Its value comes from the `value` of the currently selected `nimble-list-option`, or, if no value exists for that option, the option's content. Upon clicking on the element, the other options are visible. The user cannot manually enter values, and thus the list cannot be filtered."
- }
- },
actions: {
handles: ['change']
},
@@ -89,6 +93,7 @@ const metadata: Meta = {
?disabled="${x => x.disabled}"
position="${x => x.dropDownPosition}"
appearance="${x => x.appearance}"
+ filter-mode="${x => (x.filterMode === 'none' ? undefined : x.filterMode)}"
style="width: var(${menuMinWidth.cssCustomProperty});"
>
${repeat(x => optionSets[x.optionsType], html`
@@ -110,6 +115,11 @@ const metadata: Meta = {
options: Object.values(DropdownAppearance),
control: { type: 'radio' }
},
+ filterMode: {
+ options: Object.keys(FilterMode),
+ control: { type: 'radio' },
+ description: filterModeDescription
+ },
errorText: {
name: 'error-text'
},
@@ -133,6 +143,7 @@ const metadata: Meta = {
disabled: false,
errorVisible: false,
errorText: 'Value is invalid',
+ filterMode: 'none',
dropDownPosition: 'below',
appearance: DropdownAppearance.underline,
optionsType: ExampleOptionsType.simpleOptions
diff --git a/packages/nimble-components/src/select/tests/types.spec.ts b/packages/nimble-components/src/select/tests/types.spec.ts
new file mode 100644
index 0000000000..5d9f846959
--- /dev/null
+++ b/packages/nimble-components/src/select/tests/types.spec.ts
@@ -0,0 +1,9 @@
+import type { FilterMode } from '../types';
+
+describe('Select type', () => {
+ it('FilterMode fails compile if assigning arbitrary string values', () => {
+ // @ts-expect-error This expect will fail if the enum-like type is missing "as const"
+ const filterMode: FilterMode = 'hello';
+ expect(filterMode!).toEqual('hello');
+ });
+});
diff --git a/packages/nimble-components/src/select/types.ts b/packages/nimble-components/src/select/types.ts
index ecbe350457..2dd87b7f12 100644
--- a/packages/nimble-components/src/select/types.ts
+++ b/packages/nimble-components/src/select/types.ts
@@ -4,3 +4,13 @@
*/
export { DropdownAppearance } from '../patterns/dropdown/types';
+
+/**
+ * Types of select filter mode.
+ * @public
+ */
+export const FilterMode = {
+ none: undefined,
+ standard: 'standard'
+} as const;
+export type FilterMode = (typeof FilterMode)[keyof typeof FilterMode];
diff --git a/packages/nimble-components/src/spinner/tests/spinner-matrix.stories.ts b/packages/nimble-components/src/spinner/tests/spinner-matrix.stories.ts
index d922f499c6..85699292f9 100644
--- a/packages/nimble-components/src/spinner/tests/spinner-matrix.stories.ts
+++ b/packages/nimble-components/src/spinner/tests/spinner-matrix.stories.ts
@@ -5,12 +5,10 @@ import { isChromatic } from '../../utilities/tests/isChromatic';
import {
createMatrix,
- sharedMatrixParameters
+ sharedMatrixParameters,
+ createMatrixThemeStory
} from '../../utilities/tests/matrix';
-import {
- createMatrixThemeStory,
- createStory
-} from '../../utilities/tests/storybook';
+import { createStory } from '../../utilities/tests/storybook';
import {
bodyFontColor,
spinnerLargeHeight,
diff --git a/packages/nimble-components/src/spinner/tests/spinner.mdx b/packages/nimble-components/src/spinner/tests/spinner.mdx
index 3ad1d7b8a6..ca16347852 100644
--- a/packages/nimble-components/src/spinner/tests/spinner.mdx
+++ b/packages/nimble-components/src/spinner/tests/spinner.mdx
@@ -1,24 +1,19 @@
-import {
- DocsStory,
- Meta,
- Controls,
- Stories,
- Title,
- Description
-} from '@storybook/blocks';
+import { Canvas, Meta, Controls, Title } from '@storybook/blocks';
import * as spinnerStories from './spinner.stories';
+import { spinnerTag } from '..';
-
-
-
-
+ The is an animating indicator that can be placed in a
+ particular region of a page to represent loading progress, or an
+ ongoing operation, of an indeterminate / unknown duration.
-# Usage Docs
+ It has 3 sizes (64px, 32px, and 16px) and 2 appearance types (default and
+ accent).
-
+
+
## Sizing
diff --git a/packages/nimble-components/src/spinner/tests/spinner.stories.ts b/packages/nimble-components/src/spinner/tests/spinner.stories.ts
index 6ec7b61f2f..a5b142abdd 100644
--- a/packages/nimble-components/src/spinner/tests/spinner.stories.ts
+++ b/packages/nimble-components/src/spinner/tests/spinner.stories.ts
@@ -26,18 +26,9 @@ interface SpinnerArgs {
appearance: keyof typeof SpinnerAppearance;
}
-const overviewText = 'The `nimble-spinner` is an animating indicator that can be placed in a particular region of a page to represent '
- + 'loading progress, or an ongoing operation, of an indeterminate / unknown duration.
'
- + 'It has 3 sizes (64px, 32px, and 16px) and 2 appearance types (default and accent).
';
const metadata: Meta = {
title: 'Components/Spinner',
- parameters: {
- docs: {
- description: {
- component: overviewText
- }
- }
- },
+ parameters: {},
argTypes: {
size: {
description:
diff --git a/packages/nimble-components/src/switch/tests/switch-matrix.stories.ts b/packages/nimble-components/src/switch/tests/switch-matrix.stories.ts
index c3f4469d33..241865ef49 100644
--- a/packages/nimble-components/src/switch/tests/switch-matrix.stories.ts
+++ b/packages/nimble-components/src/switch/tests/switch-matrix.stories.ts
@@ -2,13 +2,11 @@ import type { StoryFn, Meta } from '@storybook/html';
import { html, ViewTemplate, when } from '@microsoft/fast-element';
import {
createMatrix,
- sharedMatrixParameters
+ sharedMatrixParameters,
+ createMatrixThemeStory
} from '../../utilities/tests/matrix';
import { disabledStates, DisabledState } from '../../utilities/tests/states';
-import {
- createMatrixThemeStory,
- createStory
-} from '../../utilities/tests/storybook';
+import { createStory } from '../../utilities/tests/storybook';
import { hiddenWrapper } from '../../utilities/tests/hidden';
import { switchTag } from '..';
diff --git a/packages/nimble-components/src/switch/tests/switch.mdx b/packages/nimble-components/src/switch/tests/switch.mdx
new file mode 100644
index 0000000000..6af17a85a0
--- /dev/null
+++ b/packages/nimble-components/src/switch/tests/switch.mdx
@@ -0,0 +1,28 @@
+import { Canvas, Meta, Controls, Title } from '@storybook/blocks';
+import { NimbleSwitch } from './switch.react';
+import * as switchStories from './switch.stories';
+
+
+
+
+Per [W3C](https://www.w3.org/WAI/ARIA/apg/patterns/switch/) - A switch is an input widget that
+allows users to choose one of two values: on or off. Switches are similar to checkboxes and toggle buttons, which
+can also serve as binary inputs. One difference, however, is that switches can only be used for binary input while
+checkboxes and toggle buttons allow implementations the option of supporting a third middle state. Checkboxes can
+be checked or not checked and can optionally also allow for a partially checked state. Toggle buttons can be
+pressed or not pressed and can optionally allow for a partially pressed state.
+
+
+
+
+{/* ## Appearances */}
+
+{/* ## Appearance Variants */}
+
+{/* ## Usage */}
+
+{/* ## Examples */}
+
+{/* ## Accessibility */}
+
+{/* ## Resources */}
diff --git a/packages/nimble-components/src/switch/tests/switch.stories.ts b/packages/nimble-components/src/switch/tests/switch.stories.ts
index dd31b349b1..9f455a3d54 100644
--- a/packages/nimble-components/src/switch/tests/switch.stories.ts
+++ b/packages/nimble-components/src/switch/tests/switch.stories.ts
@@ -1,6 +1,6 @@
import { html, when } from '@microsoft/fast-element';
import { withActions } from '@storybook/addon-actions/decorator';
-import type { Meta, StoryObj } from '@storybook/html';
+import type { HtmlRenderer, Meta, StoryObj } from '@storybook/html';
import { createUserSelectedThemeStory } from '../../utilities/tests/storybook';
import { switchTag } from '..';
@@ -12,23 +12,10 @@ interface SwitchArgs {
uncheckedMessage: string;
}
-const overviewText = `Per [W3C](https://www.w3.org/WAI/ARIA/apg/patterns/switch/) - A switch is an input widget that
-allows users to choose one of two values: on or off. Switches are similar to checkboxes and toggle buttons, which
-can also serve as binary inputs. One difference, however, is that switches can only be used for binary input while
-checkboxes and toggle buttons allow implementations the option of supporting a third middle state. Checkboxes can
-be checked or not checked and can optionally also allow for a partially checked state. Toggle buttons can be
-pressed or not pressed and can optionally allow for a partially pressed state.`;
-
const metadata: Meta = {
title: 'Components/Switch',
- tags: ['autodocs'],
- decorators: [withActions],
+ decorators: [withActions],
parameters: {
- docs: {
- description: {
- component: overviewText
- }
- },
actions: {
handles: ['change']
}
diff --git a/packages/nimble-components/src/table-column/anchor/cell-view/template.ts b/packages/nimble-components/src/table-column/anchor/cell-view/template.ts
index 78859b7400..d65f7f03bd 100644
--- a/packages/nimble-components/src/table-column/anchor/cell-view/template.ts
+++ b/packages/nimble-components/src/table-column/anchor/cell-view/template.ts
@@ -26,7 +26,7 @@ export const template = html`
target="${x => x.columnConfig?.target}"
type="${x => x.columnConfig?.type}"
download="${x => x.columnConfig?.download}"
- underline-hidden="${x => x.columnConfig?.underlineHidden}"
+ ?underline-hidden="${x => x.columnConfig?.underlineHidden}"
appearance="${x => x.columnConfig?.appearance}"
title=${x => (x.hasOverflow ? x.text : null)}
>
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 d0fa97738e..84e03ecd55 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
@@ -1,8 +1,8 @@
import type { StoryFn, Meta } from '@storybook/html';
import { html, ViewTemplate } from '@microsoft/fast-element';
import { pascalCase } from '@microsoft/fast-web-utilities';
-import { createMatrixThemeStory } from '../../../utilities/tests/storybook';
import {
+ createMatrixThemeStory,
createMatrix,
sharedMatrixParameters
} from '../../../utilities/tests/matrix';
@@ -45,9 +45,9 @@ const appearanceStates: [string, string | undefined][] = Object.entries(
).map(([key, value]) => [pascalCase(key), value]);
type AppearanceState = (typeof appearanceStates)[number];
-const underlineHiddenStates: [string, boolean | undefined][] = [
+const underlineHiddenStates: [string, boolean][] = [
['Underline Hidden', true],
- ['', undefined]
+ ['', false]
];
type UnderlineHiddenState = (typeof underlineHiddenStates)[number];
@@ -63,7 +63,7 @@ const component = (
href-field-name="link"
group-index="0"
appearance="${() => appearance}"
- underline-hidden="${() => underlineHidden}"
+ ?underline-hidden="${() => underlineHidden}"
>
<${iconUserTag}>${iconUserTag}>
${tableColumnAnchorTag}>
diff --git a/packages/nimble-components/src/table-column/anchor/tests/table-column-anchor.mdx b/packages/nimble-components/src/table-column/anchor/tests/table-column-anchor.mdx
index 4cf777e031..683b4e2126 100644
--- a/packages/nimble-components/src/table-column/anchor/tests/table-column-anchor.mdx
+++ b/packages/nimble-components/src/table-column/anchor/tests/table-column-anchor.mdx
@@ -1,23 +1,16 @@
-import {
- Controls,
- DocsStory,
- Meta,
- Stories,
- Title,
- Description
-} from '@storybook/blocks';
+import { Controls, Canvas, Meta, Title } from '@storybook/blocks';
import TargetDocs from '../../../patterns/anchor/tests/target-docs.mdx';
import * as tableColumnAnchorStories from './table-column-anchor.stories';
+import { tableColumnAnchorTag } from '..';
+import { tableTag } from '../../../table';
-
-
-
-
+The column is used to display string fields as links or text in the . If a row provides an href for a link, that cell will display a link, otherwise it will display plain text.
-# Usage Docs
+
+
## Target Configuration
@@ -27,7 +20,7 @@ import * as tableColumnAnchorStories from './table-column-anchor.stories';
In an Angular app, you can configure a callback to intercept clicks so that you may invoke the router to perform the navigation instead of the default handler:
-```
+```html
Link
diff --git a/packages/nimble-components/src/table-column/anchor/tests/table-column-anchor.spec.ts b/packages/nimble-components/src/table-column/anchor/tests/table-column-anchor.spec.ts
index 7ab0bbe890..89a8ad203c 100644
--- a/packages/nimble-components/src/table-column/anchor/tests/table-column-anchor.spec.ts
+++ b/packages/nimble-components/src/table-column/anchor/tests/table-column-anchor.spec.ts
@@ -239,6 +239,29 @@ describe('TableColumnAnchor', () => {
);
});
+ it('updating underline-hidden from true to false removes the underline-hidden attribute from the anchor', async () => {
+ await element.setData([{ link: 'foo' }]);
+ await connect();
+ await waitForUpdatesAsync();
+
+ const firstColumn = element.columns[0] as TableColumnAnchor;
+ firstColumn.underlineHidden = true;
+ await waitForUpdatesAsync();
+ expect(
+ pageObject
+ .getRenderedCellAnchor(0, 0)
+ .hasAttribute('underline-hidden')
+ ).toBeTrue();
+
+ firstColumn.underlineHidden = false;
+ await waitForUpdatesAsync();
+ expect(
+ pageObject
+ .getRenderedCellAnchor(0, 0)
+ .hasAttribute('underline-hidden')
+ ).toBeFalse();
+ });
+
const linkOptionData = [
{ name: 'hreflang', accessor: (x: Anchor) => x.hreflang },
{ name: 'ping', accessor: (x: Anchor) => x.ping },
diff --git a/packages/nimble-components/src/table-column/anchor/tests/table-column-anchor.stories.ts b/packages/nimble-components/src/table-column/anchor/tests/table-column-anchor.stories.ts
index fd559e4dc5..4cdca5af2f 100644
--- a/packages/nimble-components/src/table-column/anchor/tests/table-column-anchor.stories.ts
+++ b/packages/nimble-components/src/table-column/anchor/tests/table-column-anchor.stories.ts
@@ -1,5 +1,5 @@
import { html, ref } from '@microsoft/fast-element';
-import type { Meta, StoryObj } from '@storybook/html';
+import type { HtmlRenderer, Meta, StoryObj } from '@storybook/html';
import { withActions } from '@storybook/addon-actions/decorator';
import { createUserSelectedThemeStory } from '../../../utilities/tests/storybook';
import { tableTag } from '../../../table';
@@ -15,7 +15,7 @@ import { AnchorAppearance } from '../../../anchor/types';
const metadata: Meta = {
title: 'Components/Table Column: Anchor',
- decorators: [withActions],
+ decorators: [withActions],
parameters: {
actions: {
handles: sharedTableActions
@@ -71,16 +71,8 @@ interface AnchorColumnTableArgs extends SharedTableArgs {
underlineHidden: boolean;
}
-const anchorColumnDescription = 'The `nimble-table-column-anchor` column is used to display string fields as links or text in the `nimble-table`. If a row provides an href for a link, that cell will display a link, otherwise it will display plain text.';
-
export const anchorColumn: StoryObj = {
- parameters: {
- docs: {
- description: {
- story: anchorColumnDescription
- }
- }
- },
+ parameters: {},
// prettier-ignore
render: createUserSelectedThemeStory(html`
<${tableTag}
diff --git a/packages/nimble-components/src/table-column/base/tests/table-column.mdx b/packages/nimble-components/src/table-column/base/tests/table-column.mdx
index 76c2e8628c..ab236f23fb 100644
--- a/packages/nimble-components/src/table-column/base/tests/table-column.mdx
+++ b/packages/nimble-components/src/table-column/base/tests/table-column.mdx
@@ -1,50 +1,42 @@
-import {
- Controls,
- DocsStory,
- Meta,
- Stories,
- Title,
- Description
-} from '@storybook/blocks';
+import { Controls, Canvas, Meta, Title, Description } from '@storybook/blocks';
import * as tableColumnStories from './table-column.stories';
-
## Adding columns
-
+
## Setting header content
-
+
## Configuring common attributes
-
+
## Sorting
-
+
## Grouping
-
+
## Column Width
-
+
diff --git a/packages/nimble-components/src/table-column/base/tests/table-column.stories.ts b/packages/nimble-components/src/table-column/base/tests/table-column.stories.ts
index 448ef05c4e..4008f32dac 100644
--- a/packages/nimble-components/src/table-column/base/tests/table-column.stories.ts
+++ b/packages/nimble-components/src/table-column/base/tests/table-column.stories.ts
@@ -1,5 +1,5 @@
import { html, ref, when } from '@microsoft/fast-element';
-import type { Meta, StoryObj } from '@storybook/html';
+import type { HtmlRenderer, Meta, StoryObj } from '@storybook/html';
import { withActions } from '@storybook/addon-actions/decorator';
import { createUserSelectedThemeStory } from '../../../utilities/tests/storybook';
import {
@@ -79,7 +79,7 @@ information about specific types of column.`;
const metadata: Meta = {
title: 'Components/Table Column Configuration',
- decorators: [withActions],
+ decorators: [withActions],
parameters: {
docs: {
description: {
diff --git a/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text-matrix.stories.ts b/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text-matrix.stories.ts
index b48248b268..7f4ff268a9 100644
--- a/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text-matrix.stories.ts
+++ b/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text-matrix.stories.ts
@@ -1,7 +1,7 @@
import type { StoryFn, Meta } from '@storybook/html';
import { html, ViewTemplate } from '@microsoft/fast-element';
-import { createMatrixThemeStory } from '../../../utilities/tests/storybook';
import {
+ createMatrixThemeStory,
createMatrix,
sharedMatrixParameters
} from '../../../utilities/tests/matrix';
diff --git a/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.mdx b/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.mdx
new file mode 100644
index 0000000000..28422d38a7
--- /dev/null
+++ b/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.mdx
@@ -0,0 +1,25 @@
+import { Canvas, Meta, Controls, Title } from '@storybook/blocks';
+import { NimbleTableColumnDateText } from './table-column-date-text.react';
+import * as tableColumnDateTextStories from './table-column-date-text.stories';
+import { tableColumnDateTextTag } from '..';
+import { tableTag } from '../../../table';
+
+
+
+
+The column is used to display date-time fields as text in the . The date-time values must be of type `number` and represent the number of milliseconds since January 1, 1970 UTC. This is the representation used by the [JavaScript `Date` type](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date). Dates are formatted in a locale-specific way based on the value of the `lang` token, which can be set via the [nimble-theme-provider](?path=/docs/tokens-theme-provider--docs).
+
+
+
+
+{/* ## Appearances */}
+
+{/* ## Appearance Variants */}
+
+{/* ## Usage */}
+
+{/* ## Examples */}
+
+{/* ## Accessibility */}
+
+{/* ## Resources */}
diff --git a/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.stories.ts b/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.stories.ts
index 05747a9c47..bb6c6796e2 100644
--- a/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.stories.ts
+++ b/packages/nimble-components/src/table-column/date-text/tests/table-column-date-text.stories.ts
@@ -1,5 +1,5 @@
import { html, ref } from '@microsoft/fast-element';
-import type { Meta, StoryObj } from '@storybook/html';
+import type { HtmlRenderer, Meta, StoryObj } from '@storybook/html';
import { withActions } from '@storybook/addon-actions/decorator';
import { createUserSelectedThemeStory } from '../../../utilities/tests/storybook';
import { tableTag } from '../../../table';
@@ -55,8 +55,7 @@ const simpleData = [
const metadata: Meta = {
title: 'Components/Table Column: Date Text',
- decorators: [withActions],
- tags: ['autodocs'],
+ decorators: [withActions],
parameters: {
actions: {
handles: sharedTableActions
@@ -104,21 +103,13 @@ interface TextColumnTableArgs extends SharedTableArgs {
validity: () => void;
}
-const dateTextColumnDescription = 'The `nimble-table-column-date-text` column is used to display date-time fields as text in the `nimble-table`. The date-time values must be of type `number` and represent the number of milliseconds since January 1, 1970 UTC. This is the representation used by the [JavaScript `Date` type](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date). Dates are formatted in a locale-specific way based on the value of the `lang` token, which can be set via the [`nimble-theme-provider`](?path=/docs/tokens-theme-provider--docs).';
-
const validityDescription = `Readonly object of boolean values that represents the validity states that the column's configuration can be in.
The object's type is \`TableColumnValidity\`, and it contains the following boolean properties:
- \`invalidCustomOptionsCombination\`: \`true\` when an invalid combination of formatting options (i.e. \`custom-*\`) have been specified. To determine which specific options are in conflict, you may use [MDN's Try It widget](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#try_it) or a browser console to get a detailed exception message.
`;
export const dateTextColumn: StoryObj = {
- parameters: {
- docs: {
- description: {
- story: dateTextColumnDescription
- }
- }
- },
+ parameters: {},
// prettier-ignore
render: createUserSelectedThemeStory(html`
<${tableTag}
diff --git a/packages/nimble-components/src/table-column/duration-text/tests/table-column-duration-text-matrix.stories.ts b/packages/nimble-components/src/table-column/duration-text/tests/table-column-duration-text-matrix.stories.ts
index 48651d884f..b4f6daf048 100644
--- a/packages/nimble-components/src/table-column/duration-text/tests/table-column-duration-text-matrix.stories.ts
+++ b/packages/nimble-components/src/table-column/duration-text/tests/table-column-duration-text-matrix.stories.ts
@@ -1,7 +1,7 @@
import type { StoryFn, Meta } from '@storybook/html';
import { html, ViewTemplate } from '@microsoft/fast-element';
-import { createMatrixThemeStory } from '../../../utilities/tests/storybook';
import {
+ createMatrixThemeStory,
createMatrix,
sharedMatrixParameters
} from '../../../utilities/tests/matrix';
diff --git a/packages/nimble-components/src/table-column/duration-text/tests/table-column-duration-text.mdx b/packages/nimble-components/src/table-column/duration-text/tests/table-column-duration-text.mdx
index 42ad77dd4a..c5ecbd6153 100644
--- a/packages/nimble-components/src/table-column/duration-text/tests/table-column-duration-text.mdx
+++ b/packages/nimble-components/src/table-column/duration-text/tests/table-column-duration-text.mdx
@@ -1,25 +1,19 @@
-import {
- Controls,
- DocsStory,
- Meta,
- Title,
- Description
-} from '@storybook/blocks';
+import { Controls, Canvas, Meta, Title } from '@storybook/blocks';
import * as tableColumnDurationTextStories from './table-column-duration-text.stories';
+import { tableColumnDurationTextTag } from '..';
+import { tableTag } from '../../../table';
-
-
- column is used to display a period of elapsed time as text in the . The values must be of type `number` and each represent a positive total number of milliseconds. Values that represent more than 999 days or less than a millisecond will be formatted in scientific notation as seconds only. All sub-second values will be represented with up to 3 digits of precision. Values are formatted in a locale-specific way based on the value of the lang token, which can be set via the [nimble-theme-provider](https://60e89457a987cf003efc0a5b-ckpboqfrwq.chromatic.com/iframe.html?path=/docs/tokens-theme-provider--docs).
+
+
-# Usage Docs
-
## Angular Usage
### Duration Pipe
diff --git a/packages/nimble-components/src/table-column/duration-text/tests/table-column-duration-text.stories.ts b/packages/nimble-components/src/table-column/duration-text/tests/table-column-duration-text.stories.ts
index 04df998364..86ea0921cf 100644
--- a/packages/nimble-components/src/table-column/duration-text/tests/table-column-duration-text.stories.ts
+++ b/packages/nimble-components/src/table-column/duration-text/tests/table-column-duration-text.stories.ts
@@ -1,5 +1,5 @@
import { html, ref } from '@microsoft/fast-element';
-import type { Meta, StoryObj } from '@storybook/html';
+import type { HtmlRenderer, Meta, StoryObj } from '@storybook/html';
import { withActions } from '@storybook/addon-actions/decorator';
import { createUserSelectedThemeStory } from '../../../utilities/tests/storybook';
import { tableTag } from '../../../table';
@@ -42,7 +42,7 @@ const simpleData = [
const metadata: Meta = {
title: 'Components/Table Column: Duration Text',
- decorators: [withActions],
+ decorators: [withActions],
parameters: {
actions: {
handles: sharedTableActions
@@ -68,16 +68,8 @@ interface TextColumnTableArgs extends SharedTableArgs {
fieldName: string;
}
-const durationTextColumnDescription = 'The `nimble-table-column-duration-text` column is used to display a period of elapsed time as text in the `nimble-table`. The values must be of type `number` and each represent a positive total number of milliseconds. Values that represent more than 999 days or less than a millisecond will be formatted in scientific notation as seconds only. All sub-second values will be represented with up to 3 digits of precision. Values are formatted in a locale-specific way based on the value of the lang token, which can be set via the [nimble-theme-provider](https://60e89457a987cf003efc0a5b-ckpboqfrwq.chromatic.com/iframe.html?path=/docs/tokens-theme-provider--docs).';
-
export const durationTextColumn: StoryObj = {
- parameters: {
- docs: {
- description: {
- story: durationTextColumnDescription
- }
- }
- },
+ parameters: {},
// prettier-ignore
render: createUserSelectedThemeStory(html`
<${tableTag}
diff --git a/packages/nimble-components/src/table-column/enum-text/tests/table-column-enum-text-matrix.stories.ts b/packages/nimble-components/src/table-column/enum-text/tests/table-column-enum-text-matrix.stories.ts
index ab01ca6d08..4ae26fad00 100644
--- a/packages/nimble-components/src/table-column/enum-text/tests/table-column-enum-text-matrix.stories.ts
+++ b/packages/nimble-components/src/table-column/enum-text/tests/table-column-enum-text-matrix.stories.ts
@@ -1,7 +1,9 @@
import type { StoryFn, Meta } from '@storybook/html';
import { html, ViewTemplate } from '@microsoft/fast-element';
-import { createMatrixThemeStory } from '../../../utilities/tests/storybook';
-import { sharedMatrixParameters } from '../../../utilities/tests/matrix';
+import {
+ createMatrixThemeStory,
+ sharedMatrixParameters
+} from '../../../utilities/tests/matrix';
import { Table, tableTag } from '../../../table';
import { tableColumnEnumTextTag } from '..';
import { mappingTextTag } from '../../../mapping/text';
diff --git a/packages/nimble-components/src/table-column/enum-text/tests/table-column-enum-text.mdx b/packages/nimble-components/src/table-column/enum-text/tests/table-column-enum-text.mdx
index 7c065b5bfb..6e23e98c83 100644
--- a/packages/nimble-components/src/table-column/enum-text/tests/table-column-enum-text.mdx
+++ b/packages/nimble-components/src/table-column/enum-text/tests/table-column-enum-text.mdx
@@ -1,23 +1,19 @@
-import {
- DocsStory,
- Meta,
- Controls,
- Stories,
- Title,
- Description
-} from '@storybook/blocks';
+import { Canvas, Meta, Controls, Title, Description } from '@storybook/blocks';
+import { columnOperationBehavior } from '../../base/tests/table-column-stories-utils';
import * as tableColumnEnumTextStories from './table-column-enum-text.stories';
import * as textMappingStories from '../../../mapping/text/tests/mapping-text.stories';
+import { tableColumnEnumTextTag } from '../';
+import { tableTag } from '../../../table';
-
-
-
-
+The column renders string, number, or boolean values as mapped text in the .
-# Usage Docs
+{columnOperationBehavior}
+
+
+
## Blazor Usage
diff --git a/packages/nimble-components/src/table-column/enum-text/tests/table-column-enum-text.stories.ts b/packages/nimble-components/src/table-column/enum-text/tests/table-column-enum-text.stories.ts
index 27815cd315..d4f8a492e4 100644
--- a/packages/nimble-components/src/table-column/enum-text/tests/table-column-enum-text.stories.ts
+++ b/packages/nimble-components/src/table-column/enum-text/tests/table-column-enum-text.stories.ts
@@ -5,7 +5,6 @@ import { tableTag } from '../../../table';
import { tableColumnEnumTextTag } from '..';
import {
SharedTableArgs,
- columnOperationBehavior,
sharedTableArgTypes,
sharedTableArgs
} from '../../base/tests/table-column-stories-utils';
@@ -36,19 +35,9 @@ const simpleData = [
}
] as const;
-const enumTextColumnDescription = `The \`nimble-table-column-enum-text\` column renders string, number, or boolean values as mapped text in the \`nimble-table\`.
-
-${columnOperationBehavior}`;
-
const metadata: Meta = {
title: 'Components/Table Column: Enum Text',
- parameters: {
- docs: {
- description: {
- component: enumTextColumnDescription
- }
- }
- }
+ parameters: {}
};
export default metadata;
diff --git a/packages/nimble-components/src/table-column/icon/tests/table-column-icon-matrix.stories.ts b/packages/nimble-components/src/table-column/icon/tests/table-column-icon-matrix.stories.ts
index 4933d4aa9d..10c6577db8 100644
--- a/packages/nimble-components/src/table-column/icon/tests/table-column-icon-matrix.stories.ts
+++ b/packages/nimble-components/src/table-column/icon/tests/table-column-icon-matrix.stories.ts
@@ -1,7 +1,9 @@
import type { StoryFn, Meta } from '@storybook/html';
import { html, ViewTemplate } from '@microsoft/fast-element';
-import { createMatrixThemeStory } from '../../../utilities/tests/storybook';
-import { sharedMatrixParameters } from '../../../utilities/tests/matrix';
+import {
+ createMatrixThemeStory,
+ sharedMatrixParameters
+} from '../../../utilities/tests/matrix';
import { Table, tableTag } from '../../../table';
import { tableColumnIconTag } from '..';
import { iconCheckTag } from '../../../icons/check';
diff --git a/packages/nimble-components/src/table-column/icon/tests/table-column-icon.mdx b/packages/nimble-components/src/table-column/icon/tests/table-column-icon.mdx
index cc16d30bf2..0602423db5 100644
--- a/packages/nimble-components/src/table-column/icon/tests/table-column-icon.mdx
+++ b/packages/nimble-components/src/table-column/icon/tests/table-column-icon.mdx
@@ -1,36 +1,33 @@
-import {
- DocsStory,
- Meta,
- Controls,
- Stories,
- Title,
- Description
-} from '@storybook/blocks';
+import { Canvas, Meta, Controls, Title, Description } from '@storybook/blocks';
+import { columnOperationBehavior } from '../../base/tests/table-column-stories-utils';
import * as tableColumnIconStories from './table-column-icon.stories';
import * as iconMappingStories from '../../../mapping/icon/tests/mapping-icon.stories';
import * as spinnerMappingStories from '../../../mapping/spinner/tests/mapping-spinner.stories';
+import { tableColumnIconTag } from '../';
+import { tableTag } from '../../../table';
+import { spinnerTag } from '../../../spinner';
-
-
-
-
+The column renders string, number, or boolean values as a Nimble icon or in the .
-# Usage Docs
+{columnOperationBehavior}
+
+
+
## Blazor Usage
When setting a child mapping element's `Key` value to a string, you must wrap it in `@()` so that the compiler treats it as a string, e.g.
``
-# Icon Mapping
+## Icon Mapping
-# Spinner Mapping
+## Spinner Mapping
diff --git a/packages/nimble-components/src/table-column/icon/tests/table-column-icon.stories.ts b/packages/nimble-components/src/table-column/icon/tests/table-column-icon.stories.ts
index 6752927cf9..542f8f3599 100644
--- a/packages/nimble-components/src/table-column/icon/tests/table-column-icon.stories.ts
+++ b/packages/nimble-components/src/table-column/icon/tests/table-column-icon.stories.ts
@@ -5,7 +5,6 @@ import { tableTag } from '../../../table';
import { tableColumnIconTag } from '..';
import {
SharedTableArgs,
- columnOperationBehavior,
sharedTableArgTypes,
sharedTableArgs
} from '../../base/tests/table-column-stories-utils';
@@ -44,19 +43,9 @@ const simpleData = [
}
] as const;
-const iconColumnDescription = `The \`nimble-table-column-icon\` column renders string, number, or boolean values as a Nimble icon or \`nimble-spinner\` in the \`nimble-table\`.
-
-${columnOperationBehavior}`;
-
const metadata: Meta = {
title: 'Components/Table Column: Icon',
- parameters: {
- docs: {
- description: {
- component: iconColumnDescription
- }
- }
- }
+ parameters: {}
};
export default metadata;
diff --git a/packages/nimble-components/src/table-column/number-text/tests/table-column-number-text-matrix.stories.ts b/packages/nimble-components/src/table-column/number-text/tests/table-column-number-text-matrix.stories.ts
index b949936c14..144f05c096 100644
--- a/packages/nimble-components/src/table-column/number-text/tests/table-column-number-text-matrix.stories.ts
+++ b/packages/nimble-components/src/table-column/number-text/tests/table-column-number-text-matrix.stories.ts
@@ -1,8 +1,8 @@
import type { StoryFn, Meta } from '@storybook/html';
import { html, ViewTemplate } from '@microsoft/fast-element';
import { pascalCase } from '@microsoft/fast-web-utilities';
-import { createMatrixThemeStory } from '../../../utilities/tests/storybook';
import {
+ createMatrixThemeStory,
createMatrix,
sharedMatrixParameters
} from '../../../utilities/tests/matrix';
diff --git a/packages/nimble-components/src/table-column/number-text/tests/table-column-number-text.mdx b/packages/nimble-components/src/table-column/number-text/tests/table-column-number-text.mdx
index 947f910136..7fcfebf185 100644
--- a/packages/nimble-components/src/table-column/number-text/tests/table-column-number-text.mdx
+++ b/packages/nimble-components/src/table-column/number-text/tests/table-column-number-text.mdx
@@ -1,25 +1,26 @@
-import {
- Controls,
- DocsStory,
- Meta,
- Stories,
- Title,
- Description
-} from '@storybook/blocks';
+import { Canvas, Meta, Controls, Title } from '@storybook/blocks';
+import { NimbleTableColumnNumberText } from './table-column-number-text.react';
+import { columnOperationBehavior } from '../../base/tests/table-column-stories-utils';
import * as tableColumnNumberTextStories from './table-column-number-text.stories';
+import { tableColumnNumberTextTag } from '..';
+import { tableTag } from '../../../table';
-
-
-
+The column is used to display number fields as text in the . Numbers are formatted in a locale-specific way
+based on the value of the `lang` token, which can be set via the [nimble-theme-provider](?path=/docs/tokens-theme-provider--docs).
+
+{columnOperationBehavior}
+
+
-# Usage Docs
+{/* ## Appearances */}
+
+{/* ## Appearance Variants */}
+
+{/* ## Usage */}
## Angular Usage
@@ -36,3 +37,9 @@ To use it:
- `decimalDigits` - When `numberTextFormat` is `NumberTextFormat.decimal`, specifies the number of decimal places to show. If neither `decimalDigits` or `decimalMaximumDigits` are set, a default value of 2 is used. `decimalDigits` and `decimalMaximumDigits` cannot both be set at the same time. The value must be in the range 0 - 20 (inclusive).
- `decimalMaximumDigits` - When `numberTextFormat` is `NumberTextFormat.decimal`, specifies the maximum number of decimal places to show. This differs from `decimalDigits` in that trailing zeros are omitted. `decimalDigits` and `decimalMaximumDigits` cannot both be set at the same time. The value must be in the range 0 - 20 (inclusive).
- `unitScale` - A `UnitScale` object indicating units to display. Possible values are `byteUnitScale`, `byte1024UnitScale`, and `voltUnitScale` which are exported from `@ni/nimble-angular/pipes`.
+
+{/* ## Examples */}
+
+{/* ## Accessibility */}
+
+{/* ## Resources */}
diff --git a/packages/nimble-components/src/table-column/number-text/tests/table-column-number-text.stories.ts b/packages/nimble-components/src/table-column/number-text/tests/table-column-number-text.stories.ts
index f2b2cab6bd..431a9393ce 100644
--- a/packages/nimble-components/src/table-column/number-text/tests/table-column-number-text.stories.ts
+++ b/packages/nimble-components/src/table-column/number-text/tests/table-column-number-text.stories.ts
@@ -1,12 +1,11 @@
import { html, ref, when } from '@microsoft/fast-element';
-import type { Meta, StoryObj } from '@storybook/html';
+import type { HtmlRenderer, Meta, StoryObj } from '@storybook/html';
import { withActions } from '@storybook/addon-actions/decorator';
import { createUserSelectedThemeStory } from '../../../utilities/tests/storybook';
import { tableTag } from '../../../table';
import { tableColumnNumberTextTag } from '..';
import {
SharedTableArgs,
- columnOperationBehavior,
sharedTableActions,
sharedTableArgTypes,
sharedTableArgs
@@ -56,7 +55,7 @@ const simpleData = [
const metadata: Meta = {
title: 'Components/Table Column: Number Text',
- decorators: [withActions],
+ decorators: [withActions],
parameters: {
actions: {
handles: sharedTableActions
@@ -89,11 +88,6 @@ interface NumberTextColumnTableArgs extends SharedTableArgs {
validity: () => void;
}
-const numberTextColumnDescription = `The \`nimble-table-column-number-text\` column is used to display number fields as text in the \`nimble-table\`. Numbers are formatted in a locale-specific way
-based on the value of the \`lang\` token, which can be set via the [\`nimble-theme-provider\`](?path=/docs/tokens-theme-provider--docs).
-
-${columnOperationBehavior}`;
-
const formatDescription = `Configures the way that the numeric value is formatted to render within the column.
@@ -151,13 +145,7 @@ const unitDescription = `A unit for the column may be configured by providing a
`;
export const numberTextColumn: StoryObj = {
- parameters: {
- docs: {
- description: {
- story: numberTextColumnDescription
- }
- }
- },
+ parameters: {},
// prettier-ignore
render: createUserSelectedThemeStory(html`
<${tableTag}
diff --git a/packages/nimble-components/src/table-column/text/tests/table-column-text.mdx b/packages/nimble-components/src/table-column/text/tests/table-column-text.mdx
new file mode 100644
index 0000000000..5d15e30550
--- /dev/null
+++ b/packages/nimble-components/src/table-column/text/tests/table-column-text.mdx
@@ -0,0 +1,25 @@
+import { Canvas, Meta, Controls, Title } from '@storybook/blocks';
+import { NimbleTableColumnText } from './table-column-text.react';
+import * as tableColumnTextStories from './table-column-text.stories';
+import { tableColumnTextTag } from '..';
+import { tableTag } from '../../../table';
+
+
+
+
+The column is used to display string fields as text in the .
+
+
+
+
+{/* ## Appearances */}
+
+{/* ## Appearance Variants */}
+
+{/* ## Usage */}
+
+{/* ## Examples */}
+
+{/* ## Accessibility */}
+
+{/* ## Resources */}
diff --git a/packages/nimble-components/src/table-column/text/tests/table-column-text.stories.ts b/packages/nimble-components/src/table-column/text/tests/table-column-text.stories.ts
index 091ac2af23..f5c34d201c 100644
--- a/packages/nimble-components/src/table-column/text/tests/table-column-text.stories.ts
+++ b/packages/nimble-components/src/table-column/text/tests/table-column-text.stories.ts
@@ -1,5 +1,5 @@
import { html, ref } from '@microsoft/fast-element';
-import type { Meta, StoryObj } from '@storybook/html';
+import type { HtmlRenderer, Meta, StoryObj } from '@storybook/html';
import { withActions } from '@storybook/addon-actions/decorator';
import { createUserSelectedThemeStory } from '../../../utilities/tests/storybook';
import { tableTag } from '../../../table';
@@ -39,8 +39,7 @@ const simpleData = [
const metadata: Meta = {
title: 'Components/Table Column: Text',
- decorators: [withActions],
- tags: ['autodocs'],
+ decorators: [withActions],
parameters: {
actions: {
handles: sharedTableActions
@@ -68,16 +67,8 @@ interface TextColumnTableArgs extends SharedTableArgs {
fieldName: TextColumnFieldNameOption;
}
-const textColumnDescription = 'The `nimble-table-column-text` column is used to display string fields as text in the `nimble-table`.';
-
export const textColumn: StoryObj = {
- parameters: {
- docs: {
- description: {
- story: textColumnDescription
- }
- }
- },
+ parameters: {},
// prettier-ignore
render: createUserSelectedThemeStory(html`
<${tableTag}
diff --git a/packages/nimble-components/src/table/components/row/index.ts b/packages/nimble-components/src/table/components/row/index.ts
index 7c745f0dfe..ec4cb5b7c5 100644
--- a/packages/nimble-components/src/table/components/row/index.ts
+++ b/packages/nimble-components/src/table/components/row/index.ts
@@ -86,6 +86,9 @@ export class TableRow<
@attr({ attribute: 'row-operation-grid-cell-hidden', mode: 'boolean' })
public rowOperationGridCellHidden = false;
+ @attr({ mode: 'boolean' })
+ public loading = false;
+
/**
* @internal
* An array that parallels the `columns` array and contains the indent
@@ -125,6 +128,11 @@ export class TableRow<
return this.isParentRow && this.nestingLevel === 0;
}
+ @volatile
+ public get isNestedParent(): boolean {
+ return this.isParentRow && this.nestingLevel > 0;
+ }
+
// Programmatically updating the selection state of a checkbox fires the 'change' event.
// Therefore, selection change events that occur due to programmatically updating
// the selection checkbox 'checked' value should be ingored.
diff --git a/packages/nimble-components/src/table/components/row/styles.ts b/packages/nimble-components/src/table/components/row/styles.ts
index 8db988590e..72ed9e40cf 100644
--- a/packages/nimble-components/src/table/components/row/styles.ts
+++ b/packages/nimble-components/src/table/components/row/styles.ts
@@ -5,6 +5,7 @@ import {
applicationBackgroundColor,
borderWidth,
controlHeight,
+ controlSlimHeight,
fillHoverColor,
fillHoverSelectedColor,
fillSelectedColor,
@@ -53,6 +54,21 @@ export const styles = css`
}
.expand-collapse-button {
+ flex: 0 0 auto;
+ padding-left: calc(
+ ${mediumPadding} + (var(--ni-private-table-row-indent-level) - 1) *
+ ${controlHeight}
+ );
+ }
+
+ .spinner-container {
+ flex: 0 0 auto;
+ width: ${controlSlimHeight};
+ height: ${controlSlimHeight};
+ align-self: center;
+ display: flex;
+ align-items: center;
+ justify-content: center;
padding-left: calc(
${mediumPadding} + (var(--ni-private-table-row-indent-level) - 1) *
${controlHeight}
@@ -60,6 +76,7 @@ export const styles = css`
}
.row-operations-container {
+ flex: 0 0 auto;
display: flex;
}
diff --git a/packages/nimble-components/src/table/components/row/template.ts b/packages/nimble-components/src/table/components/row/template.ts
index 1e4b616509..fe9944ca89 100644
--- a/packages/nimble-components/src/table/components/row/template.ts
+++ b/packages/nimble-components/src/table/components/row/template.ts
@@ -9,11 +9,14 @@ import { checkboxTag } from '../../../checkbox';
import {
tableRowCollapseLabel,
tableRowExpandLabel,
+ tableRowLoadingLabel,
tableRowSelectLabel
} from '../../../label-provider/table/label-tokens';
import type { TableColumn } from '../../../table-column/base';
import { buttonTag } from '../../../button';
import { iconArrowExpanderRightTag } from '../../../icons/arrow-expander-right';
+import { spinnerTag } from '../../../spinner';
+import { SpinnerAppearance } from '../../../spinner/types';
// prettier-ignore
export const template = html`
@@ -40,21 +43,33 @@ export const template = html`
`)}
${when(x => x.isParentRow, html`
- <${buttonTag}
- appearance="${ButtonAppearance.ghost}"
- content-hidden
- class="expand-collapse-button"
- tabindex="-1"
- @click="${(x, c) => x.onRowExpandToggle(c.event)}"
- title="${x => (x.expanded ? tableRowCollapseLabel.getValueFor(x) : tableRowExpandLabel.getValueFor(x))}"
- aria-hidden="true"
- >
- <${iconArrowExpanderRightTag} ${ref('expandIcon')} slot="start" class="expander-icon ${x => x.animationClass}">${iconArrowExpanderRightTag}>
- ${buttonTag}>
+ ${when(x => x.loading, html`
+
+ <${spinnerTag}
+ appearance="${SpinnerAppearance.accent}"
+ aria-label="${x => tableRowLoadingLabel.getValueFor(x)}"
+ title="${x => tableRowLoadingLabel.getValueFor(x)}"
+ >
+ ${spinnerTag}>
+
+ `)}
+ ${when(x => !x.loading, html`
+ <${buttonTag}
+ appearance="${ButtonAppearance.ghost}"
+ content-hidden
+ class="expand-collapse-button"
+ tabindex="-1"
+ @click="${(x, c) => x.onRowExpandToggle(c.event)}"
+ title="${x => (x.expanded ? tableRowCollapseLabel.getValueFor(x) : tableRowExpandLabel.getValueFor(x))}"
+ aria-hidden="true"
+ >
+ <${iconArrowExpanderRightTag} ${ref('expandIcon')} slot="start" class="expander-icon ${x => x.animationClass}">${iconArrowExpanderRightTag}>
+ ${buttonTag}>
+ `)}
`)}
${repeat(x => x.columns, html`
${when(x => !x.columnHidden, html`
diff --git a/packages/nimble-components/src/table/components/row/tests/table-row.pageobject.ts b/packages/nimble-components/src/table/components/row/tests/table-row.pageobject.ts
index 97dc2c91f9..a4852e8d0d 100644
--- a/packages/nimble-components/src/table/components/row/tests/table-row.pageobject.ts
+++ b/packages/nimble-components/src/table/components/row/tests/table-row.pageobject.ts
@@ -1,4 +1,6 @@
import type { TableRow } from '..';
+import type { Button } from '../../../../button';
+import { Spinner, spinnerTag } from '../../../../spinner';
import type { TableRecord } from '../../../types';
import { tableCellTag, TableCell } from '../../cell';
@@ -23,9 +25,13 @@ export class TableRowPageObject {
);
}
- public getExpandCollapseButton(): HTMLElement | null {
- return this.tableRowElement.shadowRoot!.querySelector(
+ public getExpandCollapseButton(): Button | null {
+ return this.tableRowElement.shadowRoot!.querySelector