diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e5b3210cfd5..3638cbafc83 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -87,8 +87,8 @@ jobs: with: path: 'packages/site/dist' - # Publish to Chromatic (from any push) - - name: Publish to Chromatic + # Publish to Chromatic (from any push) - Nimble + - name: Publish to Chromatic (Nimble) if: env.HAS_CHROMATIC_PROJECT_TOKEN == 'true' && github.event_name == 'push' uses: chromaui/action@v1 with: @@ -103,6 +103,20 @@ jobs: exitOnceUploaded: true # Do not wait for test results exitZeroOnChanges: true # Option to prevent the workflow from failing + # Publish to Chromatic (from any push) - Spright + - name: Publish to Chromatic (Spright) + if: env.HAS_CHROMATIC_PROJECT_TOKEN == 'true' && github.event_name == 'push' + uses: chromaui/action@v1 + with: + projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} + onlyChanged: "!startsWith(github.ref, 'refs/heads/main')" # Use TurboSnap for PR builds + workingDir: ./packages/spright-components + externals: | + - '.storybook/public/**' + storybookBuildDir: ../../packages/site/dist/storybook + exitOnceUploaded: true # Do not wait for test results + exitZeroOnChanges: true # Option to prevent the workflow from failing + # Lint - run: npm run lint diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b6570fceeb2..5e9698860f7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -202,3 +202,19 @@ Some ways to make progress on an intermittent test tech debt issue are: 1. In a branch a developer can try and re-enable the test and reproduce the failure by including additional logging, etc. Creating a PR is not necessary to queue a build in nimble; every commit has an associated build and will re-run the tests. 2. If the failure is too intermittent to detect by manually queuing builds in a branch and needs additional logging and executions in main, then modify the test so that it will not fail the test suite and add the additional logging needed to make it run in main. Actively monitor the change and have a pre-defined date to disable the test and re-evaluate how to handle the issue. + +## Contributing to Spright + +The Spright packages, while part of the Nimble monorepo, have some differing contribution policies. + +### Code ownership + +Spright packages generally follow the inner source model. The Nimble team owns shared code and configuration, but the components, their tests, and their documentation are owned by the teams that created them. Bug fixes and new features should be contributed by the team that needs them. + +### Code quality + +Code should adhere to NI and Nimble standards for quality and test coverage. Spright components may choose to compromise on Nimble standards for aspects like visual design or API breadth, but should always have a level of quality suitable for use in production applications. + +### Documentation + +Storybook documentation for components should include a **Usage Guidance** section that explains what the component should and should not be used for. This could include information about feature gaps, guidance about when to use the component rather than a comparable Nimble component, and context about why the component is in Spright rather than Nimble. \ No newline at end of file diff --git a/README.md b/README.md index e617ab70fef..2320496b0df 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,9 @@ [![Nimble Blazor Nuget version and repo link](https://img.shields.io/nuget/v/NimbleBlazor.svg?label=NimbleBlazor)](https://www.nuget.org/packages/NimbleBlazor) [![Nimble Components NPM version and repo link](https://img.shields.io/npm/v/@ni/nimble-components.svg?label=@ni/nimble-components)](https://www.npmjs.com/package/@ni/nimble-components) [![Nimble Tokens NPM version and repo link](https://img.shields.io/npm/v/@ni/nimble-tokens.svg?label=@ni/nimble-tokens)](https://www.npmjs.com/package/@ni/nimble-tokens) +[![Spright Angular NPM version and repo link](https://img.shields.io/npm/v/@ni/spright-angular.svg?label=@ni/spright-angular)](https://www.npmjs.com/package/@ni/spright-angular) +[![Spright Blazor Nuget version and repo link](https://img.shields.io/nuget/v/SprightBlazor.svg?label=SprightBlazor)](https://www.nuget.org/packages/SprightBlazor) +[![Spright Components NPM version and repo link](https://img.shields.io/npm/v/@ni/spright-components.svg?label=@ni/spright-components)](https://www.npmjs.com/package/@ni/spright-components) The NI Nimble Design System: styled UI components for NI applications. @@ -26,6 +29,9 @@ This repository contains the source for the following packages: - **[`@ni/nimble-blazor`](/packages/nimble-blazor/)** - styled Blazor components for use in NI Blazor applications - **[`@ni/nimble-components`](/packages/nimble-components/)** - styled web components for use in other applications (also used by `nimble-angular` and `nimble-blazor`) - **[`@ni/nimble-tokens`](/packages/nimble-tokens/)** - design tokens used by the component packages +- **[`@ni/spright-angular`](/angular-workspace/projects/ni/spright-angular/)** - experimental, composite, or product-specific, styled Angular components for use in NI Angular applications +- **[`@ni/spright-blazor`](/packages/spright-blazor/)** - experimental, composite, or product-specific, styled Blazor components for use in NI Blazor applications +- **[`@ni/spright-components`](/packages/spright-components/)** - experimental, composite, or product-specific, styled web components for use in other applications (also used by `spright-angular` and `spright-blazor`) And some additional utility packages: - [`@ni/jasmine-parameterized`](/packages/jasmine-parameterized/) - a utility for writing [Jasmine](https://jasmine.github.io/) parameterized tests @@ -59,4 +65,25 @@ See `Getting Started` in [`Contributing.md`](/CONTRIBUTING.md#getting-started) t ## Component Status -View status of components that are completed and on the roadmap in the [Component Status](https://ni.github.io/nimble/storybook/?path=/docs/component-status--docs) page. \ No newline at end of file +View status of components that are completed and on the roadmap in the [Component Status](https://ni.github.io/nimble/storybook/?path=/docs/component-status--docs) page. + +## Spright + +`spright-components`, `spright-angular`, and `SprightBlazor` are counterparts of the Nimble packages for components that do not belong in the Nimble Design System. + +### What are the different types of Spright components? + +Examples include: +1. "Molecule" components which combine Nimble "atom" components. For example, a group of card buttons with a specific layout. +2. Product-specific components. For example, a configuration pane that uses product-specific terminology or connects to a product-specific data model. +3. Data-connected components. For example, a table that populates itself by making HTTP requests to a specific service. +4. Experimental components that are trying out new UX patterns to see if they should someday be promoted to Nimble. + +### Why Spright? + +"Spright" is an archaic variant of "sprite" that is the root of "sprightly"; think of it as a rapidly moving peer of Nimble. + +This concept is inspired by the "recipes" concept in [Design System Pace Layers](https://bigmedium.com/ideas/design-system-pace-layers-slow-fast.html). The goals are: + +1. Keep Nimble's core components at a high level of quality and reusability. +2. Allow innovative new contributions to follow Nimble's architecture, leverage its infrastructure, and start on a path to being part of Nimble without being unnecessarily slowed by the rigorous process needed to achieve the first goal. diff --git a/angular-workspace/angular.json b/angular-workspace/angular.json index e764ed7d794..f63804f96de 100644 --- a/angular-workspace/angular.json +++ b/angular-workspace/angular.json @@ -47,6 +47,47 @@ } } }, + "@ni/spright-angular": { + "projectType": "library", + "root": "projects/ni/spright-angular", + "sourceRoot": "projects/ni/spright-angular", + "prefix": "lib", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:ng-packagr", + "options": { + "project": "projects/ni/spright-angular/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "projects/ni/spright-angular/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "projects/ni/spright-angular/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "projects/ni/spright-angular/test.ts", + "tsConfig": "projects/ni/spright-angular/tsconfig.spec.json", + "karmaConfig": "projects/ni/spright-angular/karma.conf.js" + } + }, + "lint": { + "builder": "@angular-eslint/builder:lint", + "options": { + "lintFilePatterns": [ + "projects/ni/spright-angular/**/*.ts", + "projects/ni/spright-angular/**/*.js", + "projects/ni/spright-angular/**/*.html" + ] + } + } + } + }, "example-client-app": { "projectType": "application", "schematics": { diff --git a/angular-workspace/package.json b/angular-workspace/package.json index 5b7b55a82aa..0ab0289af60 100644 --- a/angular-workspace/package.json +++ b/angular-workspace/package.json @@ -5,15 +5,18 @@ "scripts": { "ng": "ng", "start": "ng serve", - "build": "npm run build:library && npm run build:application", + "build": "npm run build:library && npm run build:spright && npm run build:application", "build:library": "npm run generate-icons && ng build @ni/nimble-angular", "watch:library": "npm run generate-icons && ng build @ni/nimble-angular --watch", + "build:spright": "ng build @ni/spright-angular", + "watch:spright": "ng build @ni/spright-angular --watch", "build:application": "ng build example-client-app", "generate-icons": "npm run generate-icons:bundle && npm run generate-icons:run", "generate-icons:bundle": "rollup --bundleConfigAsCjs --config projects/ni/nimble-angular/build/generate-icons/rollup.config.js", "generate-icons:run": "node projects/ni/nimble-angular/build/generate-icons/dist/index.js", - "pack": "npm run pack:library && npm run pack:application", + "pack": "npm run pack:library && npm run pack:spright && npm run pack:application", "pack:library": "cd dist/ni/nimble-angular && npm pack", + "pack:spright": "cd dist/ni/spright-angular && npm pack", "pack:application": "cd dist/example-client-app && npm pack", "performance": "lhci autorun", "watch": "ng build --watch --configuration development", @@ -31,6 +34,7 @@ "@angular/platform-browser-dynamic": "^15.2.10", "@angular/router": "^15.2.10", "@ni/nimble-components": "*", + "@ni/spright-components": "*", "rxjs": "^7.3.0", "tslib": "^2.2.0", "zone.js": "^0.11.4" diff --git a/angular-workspace/projects/ni/spright-angular/.eslintrc.js b/angular-workspace/projects/ni/spright-angular/.eslintrc.js new file mode 100644 index 00000000000..33b1659088c --- /dev/null +++ b/angular-workspace/projects/ni/spright-angular/.eslintrc.js @@ -0,0 +1,15 @@ +module.exports = { + extends: '../../../.eslintrc.js', + overrides: [ + { + files: ['*.ts'], + parserOptions: { + project: [ + './tsconfig.lib.json', + './tsconfig.spec.json' + ], + tsconfigRootDir: __dirname + } + } + ] +}; diff --git a/angular-workspace/projects/ni/spright-angular/CONTRIBUTING.md b/angular-workspace/projects/ni/spright-angular/CONTRIBUTING.md new file mode 100644 index 00000000000..f7863dd48a4 --- /dev/null +++ b/angular-workspace/projects/ni/spright-angular/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# Contributing to Spright Angular + +Contributions should follow the same [guidelines](../nimble-angular/CONTRIBUTING.md) as the Nimble Angular project. \ No newline at end of file diff --git a/angular-workspace/projects/ni/spright-angular/README.md b/angular-workspace/projects/ni/spright-angular/README.md new file mode 100644 index 00000000000..0b27e4b66b1 --- /dev/null +++ b/angular-workspace/projects/ni/spright-angular/README.md @@ -0,0 +1,13 @@ +
+

ni | spright | angular

+
+ +# Spright Angular + +[![NPM Version](https://img.shields.io/npm/v/@ni/spright-angular.svg)](https://www.npmjs.com/package/@ni/spright-angular) + +Spright components for [Angular](https://angular.io) applications + +## Contributing + +Follow the instructions in [CONTRIBUTING.md](CONTRIBUTING.md) to modify this library. diff --git a/angular-workspace/projects/ni/spright-angular/accordion/ng-package.json b/angular-workspace/projects/ni/spright-angular/accordion/ng-package.json new file mode 100644 index 00000000000..90febd7faed --- /dev/null +++ b/angular-workspace/projects/ni/spright-angular/accordion/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + } +} diff --git a/angular-workspace/projects/ni/spright-angular/accordion/public-api.ts b/angular-workspace/projects/ni/spright-angular/accordion/public-api.ts new file mode 100644 index 00000000000..25ed5a2da0c --- /dev/null +++ b/angular-workspace/projects/ni/spright-angular/accordion/public-api.ts @@ -0,0 +1,2 @@ +export * from './spright-accordion.directive'; +export * from './spright-accordion.module'; diff --git a/angular-workspace/projects/ni/spright-angular/accordion/spright-accordion.directive.ts b/angular-workspace/projects/ni/spright-angular/accordion/spright-accordion.directive.ts new file mode 100644 index 00000000000..b2b86e79970 --- /dev/null +++ b/angular-workspace/projects/ni/spright-angular/accordion/spright-accordion.directive.ts @@ -0,0 +1,13 @@ +import { Directive } from '@angular/core'; +import { type Accordion, accordionTag } from '@ni/spright-components/dist/esm/accordion'; + +export type { Accordion }; +export { accordionTag }; + +/** + * Directive to provide Angular integration for the accordion. + */ +@Directive({ + selector: 'spright-accordion' +}) +export class SprightAccordionDirective { } diff --git a/angular-workspace/projects/ni/spright-angular/accordion/spright-accordion.module.ts b/angular-workspace/projects/ni/spright-angular/accordion/spright-accordion.module.ts new file mode 100644 index 00000000000..87e101a58a8 --- /dev/null +++ b/angular-workspace/projects/ni/spright-angular/accordion/spright-accordion.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SprightAccordionDirective } from './spright-accordion.directive'; + +import '@ni/spright-components/dist/esm/accordion'; + +@NgModule({ + declarations: [SprightAccordionDirective], + imports: [CommonModule], + exports: [SprightAccordionDirective] +}) +export class SprightAccordionModule { } diff --git a/angular-workspace/projects/ni/spright-angular/accordion/tests/accordion.directive.spec.ts b/angular-workspace/projects/ni/spright-angular/accordion/tests/accordion.directive.spec.ts new file mode 100644 index 00000000000..557174e567e --- /dev/null +++ b/angular-workspace/projects/ni/spright-angular/accordion/tests/accordion.directive.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; +import { SprightAccordionModule } from '../spright-accordion.module'; + +describe('Spright accordion', () => { + describe('module', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [SprightAccordionModule] + }); + }); + + it('custom element is defined', () => { + expect(customElements.get('spright-accordion')).not.toBeUndefined(); + }); + }); +}); diff --git a/angular-workspace/projects/ni/spright-angular/internal-utilities/ng-package.json b/angular-workspace/projects/ni/spright-angular/internal-utilities/ng-package.json new file mode 100644 index 00000000000..7945e60e703 --- /dev/null +++ b/angular-workspace/projects/ni/spright-angular/internal-utilities/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + } +} \ No newline at end of file diff --git a/angular-workspace/projects/ni/spright-angular/internal-utilities/public-api.ts b/angular-workspace/projects/ni/spright-angular/internal-utilities/public-api.ts new file mode 100644 index 00000000000..f169b0a8e71 --- /dev/null +++ b/angular-workspace/projects/ni/spright-angular/internal-utilities/public-api.ts @@ -0,0 +1 @@ +export * from './template-value-helpers'; \ No newline at end of file diff --git a/angular-workspace/projects/ni/spright-angular/internal-utilities/template-value-helpers.ts b/angular-workspace/projects/ni/spright-angular/internal-utilities/template-value-helpers.ts new file mode 100644 index 00000000000..7f338151d13 --- /dev/null +++ b/angular-workspace/projects/ni/spright-angular/internal-utilities/template-value-helpers.ts @@ -0,0 +1,61 @@ +/** + * Conversion helpers for values coming from template attributes or property bindings + */ + +// Values assigned to directives can come from template attributes, ie +// or from property bindings, ie +// So setters for our directives accept both string values from template attributes and +// the expected property type. This file has helpers for common property types. +// More context: https://v13.angular.io/guide/template-typecheck#input-setter-coercion + +type BooleanAttribute = '' | null; +export type BooleanValueOrAttribute = boolean | BooleanAttribute; +export type NumberValueOrAttribute = number | string; + +/** + * Converts values from templates (empty string or null) or boolean bindings to a boolean property representation + */ +export const toBooleanProperty = (value: BooleanValueOrAttribute): boolean => { + if (value === false || value === null) { + return false; + } + // For boolean attributes the empty string value is true + return true; +}; + +/** + * Converts values from templates (empty string or null) or boolean bindings to an Aria boolean + * attribute representation (the strings "true" or "false") + */ +export const toBooleanAriaAttribute = (value: BooleanValueOrAttribute): 'true' | 'false' => { + if (value === false || value === null) { + return 'false'; + } + // For boolean attributes the empty string value is true + return 'true'; +}; + +/** + * Converts values from templates (number representation as a string) or number bindings to a number property representation + */ +export const toNumberProperty = (value: NumberValueOrAttribute): number => { + // Angular: https://github.com/angular/angular/blob/2664bc2b3ef4ee5fd671f915828cfcc274a36c77/packages/forms/src/directives/number_value_accessor.ts#L67 + // And Fast: https://github.com/microsoft/fast/blob/46bb6d9aab2c37105f4434db3795e176c2354a4f/packages/web-components/fast-element/src/components/attributes.ts#L100 + // Handle numeric conversions from the view differently + // Since Number(val) https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-number-constructor-number-value + // and val * 1 https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-applystringornumericbinaryoperator + // Are identical (use ToNumeric algorithm), went with Number() for clarity + return Number(value); +}; + +/** + * Converts values from templates (number representation as a string) or number bindings to a number property representation. + * The values of `null` and `undefined` are also supported, and they are not converted. + */ +export const toNullableNumberProperty = (value: NumberValueOrAttribute | null | undefined): number | null | undefined => { + if (value === undefined || value === null) { + return value; + } + + return toNumberProperty(value); +}; diff --git a/angular-workspace/projects/ni/spright-angular/karma.conf.js b/angular-workspace/projects/ni/spright-angular/karma.conf.js new file mode 100644 index 00000000000..74d415efe59 --- /dev/null +++ b/angular-workspace/projects/ni/spright-angular/karma.conf.js @@ -0,0 +1,53 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +process.env.CHROME_BIN = require('playwright').chromium.executablePath(); +const karmaJasmine = require('karma-jasmine'); +const karmaChromeLauncher = require('karma-chrome-launcher'); +const karmaJasmineHtmlReporter = require('karma-jasmine-html-reporter'); +const karmaCoverage = require('karma-coverage'); +const karmaAngular = require('@angular-devkit/build-angular/plugins/karma'); +const path = require('path'); + +module.exports = config => { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + karmaJasmine, + karmaChromeLauncher, + karmaJasmineHtmlReporter, + karmaCoverage, + karmaAngular + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + stopSpecOnExpectationFailure: false + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: path.join(__dirname, '../../../coverage/ni/spright-angular-core'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['ChromeHeadless'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/angular-workspace/projects/ni/spright-angular/ng-package.json b/angular-workspace/projects/ni/spright-angular/ng-package.json new file mode 100644 index 00000000000..0402feab59d --- /dev/null +++ b/angular-workspace/projects/ni/spright-angular/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../../dist/ni/spright-angular", + "lib": { + "entryFile": "src/public-api.ts" + } +} diff --git a/angular-workspace/projects/ni/spright-angular/package.json b/angular-workspace/projects/ni/spright-angular/package.json new file mode 100644 index 00000000000..ec192e21a4e --- /dev/null +++ b/angular-workspace/projects/ni/spright-angular/package.json @@ -0,0 +1,36 @@ +{ + "name": "@ni/spright-angular", + "version": "0.0.1", + "description": "Angular APIs for the NI Spright components", + "scripts": { + "invoke-publish": "cd ../../../ && npm run build:spright && cd dist/ni/spright-angular && npm publish" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ni/nimble.git" + }, + "publishConfig": { + "access": "public" + }, + "author": { + "name": "National Instruments" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/ni/nimble/issues" + }, + "homepage": "https://github.com/ni/nimble#readme", + "exports": { + "./styles/*": { + "sass": "./styles/*.scss" + } + }, + "peerDependencies": { + "@angular/common": "^15.2.10", + "@angular/core": "^15.2.10", + "@ni/spright-components": "*" + }, + "dependencies": { + "tslib": "^2.2.0" + } +} diff --git a/angular-workspace/projects/ni/spright-angular/src/public-api.ts b/angular-workspace/projects/ni/spright-angular/src/public-api.ts new file mode 100644 index 00000000000..609557d6484 --- /dev/null +++ b/angular-workspace/projects/ni/spright-angular/src/public-api.ts @@ -0,0 +1,6 @@ +/* + * Public API Surface of spright-angular-core (primary entry point) + */ + +// eslint-disable-next-line import/no-default-export +export default {}; \ No newline at end of file diff --git a/angular-workspace/projects/ni/spright-angular/test.ts b/angular-workspace/projects/ni/spright-angular/test.ts new file mode 100644 index 00000000000..ad9d734f9be --- /dev/null +++ b/angular-workspace/projects/ni/spright-angular/test.ts @@ -0,0 +1,25 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js'; +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting(), + { + errorOnUnknownElements: true, + errorOnUnknownProperties: true + } +); + +// Elevate console errors and warnings to test failures +// eslint-disable-next-line no-console, @typescript-eslint/no-explicit-any +console.error = (data: any): void => fail(data); +// eslint-disable-next-line no-console, @typescript-eslint/no-explicit-any +console.warn = (data: any): void => fail(data); diff --git a/angular-workspace/projects/ni/spright-angular/tsconfig.lib.json b/angular-workspace/projects/ni/spright-angular/tsconfig.lib.json new file mode 100644 index 00000000000..7d74867972a --- /dev/null +++ b/angular-workspace/projects/ni/spright-angular/tsconfig.lib.json @@ -0,0 +1,23 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "../../../out-tsc/lib", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [], + "preserveSymlinks": true, + "lib": [ + "dom", + "es2020" + ] + }, + "exclude": [ + "test.ts", + "**/*.spec.ts" + ], + "angularCompilerOptions": { + "compilationMode": "partial" + } +} diff --git a/angular-workspace/projects/ni/spright-angular/tsconfig.lib.prod.json b/angular-workspace/projects/ni/spright-angular/tsconfig.lib.prod.json new file mode 100644 index 00000000000..fb40dbbb034 --- /dev/null +++ b/angular-workspace/projects/ni/spright-angular/tsconfig.lib.prod.json @@ -0,0 +1,7 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + } +} diff --git a/angular-workspace/projects/ni/spright-angular/tsconfig.spec.json b/angular-workspace/projects/ni/spright-angular/tsconfig.spec.json new file mode 100644 index 00000000000..8f667f2c4ab --- /dev/null +++ b/angular-workspace/projects/ni/spright-angular/tsconfig.spec.json @@ -0,0 +1,17 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "../../../out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "test.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/angular-workspace/tsconfig.json b/angular-workspace/tsconfig.json index a3d635f4fe3..560f975bde6 100644 --- a/angular-workspace/tsconfig.json +++ b/angular-workspace/tsconfig.json @@ -8,6 +8,12 @@ ], "@ni/nimble-angular/*": [ "dist/ni/nimble-angular/*" + ], + "@ni/spright-angular": [ + "dist/ni/spright-angular" + ], + "@ni/spright-angular/*": [ + "dist/ni/spright-angular/*" ] }, "baseUrl": "./", diff --git a/package-lock.json b/package-lock.json index b8191d6030f..47c240825d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,9 +14,12 @@ "packages/xliff-to-json-converter", "packages/nimble-tokens", "packages/nimble-components", + "packages/spright-components", "angular-workspace", "angular-workspace/projects/ni/nimble-angular", + "angular-workspace/projects/ni/spright-angular", "packages/nimble-blazor", + "packages/spright-blazor", "packages/performance", "packages/site" ], @@ -40,6 +43,7 @@ "@angular/platform-browser-dynamic": "^15.2.10", "@angular/router": "^15.2.10", "@ni/nimble-components": "*", + "@ni/spright-components": "*", "rxjs": "^7.3.0", "tslib": "^2.2.0", "zone.js": "^0.11.4" @@ -97,6 +101,19 @@ "@ni/nimble-components": "^21.8.0" } }, + "angular-workspace/projects/ni/spright-angular": { + "name": "@ni/spright-angular", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "tslib": "^2.2.0" + }, + "peerDependencies": { + "@angular/common": "^15.2.10", + "@angular/core": "^15.2.10", + "@ni/spright-components": "*" + } + }, "node_modules/@11ty/dependency-tree": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@11ty/dependency-tree/-/dependency-tree-2.0.1.tgz", @@ -5932,6 +5949,18 @@ "resolved": "packages/nimble-tokens", "link": true }, + "node_modules/@ni/spright-angular": { + "resolved": "angular-workspace/projects/ni/spright-angular", + "link": true + }, + "node_modules/@ni/spright-blazor": { + "resolved": "packages/spright-blazor", + "link": true + }, + "node_modules/@ni/spright-components": { + "resolved": "packages/spright-components", + "link": true + }, "node_modules/@ni/xliff-to-json-converter": { "resolved": "packages/xliff-to-json-converter", "link": true @@ -34283,6 +34312,208 @@ "vite": "^4.0.4" } }, + "packages/spright-blazor": { + "name": "@ni/spright-blazor", + "version": "0.0.1", + "hasInstallScript": true, + "license": "MIT", + "devDependencies": { + "@microsoft/fast-web-utilities": "^6.0.0", + "@ni/eslint-config-javascript": "^4.2.0", + "@ni/nimble-components": "*", + "@ni/nimble-tokens": "*", + "@ni/spright-components": "*", + "@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" + } + }, + "packages/spright-blazor/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "packages/spright-blazor/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "packages/spright-blazor/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "packages/spright-blazor/node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "packages/spright-blazor/node_modules/rimraf": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", + "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", + "dev": true, + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "packages/spright-blazor/node_modules/rimraf/node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "packages/spright-blazor/node_modules/rimraf/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "packages/spright-components": { + "name": "@ni/spright-components", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "@microsoft/fast-element": "*", + "@microsoft/fast-foundation": "*", + "@microsoft/fast-web-utilities": "*", + "@ni/nimble-components": "*", + "@ni/nimble-tokens": "*", + "tslib": "^2.2.0" + }, + "devDependencies": { + "@babel/cli": "^7.13.16", + "@babel/core": "^7.20.12", + "@ni/eslint-config-javascript": "^4.2.0", + "@ni/eslint-config-typescript": "^4.2.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", + "@types/jasmine": "^4.3.1", + "@types/webpack-env": "^1.15.2", + "babel-loader": "^9.1.2", + "circular-dependency-plugin": "^5.2.0", + "css-loader": "^6.7.3", + "dotenv-webpack": "^8.0.1", + "eslint-plugin-jsdoc": "^46.8.2", + "eslint-plugin-storybook": "^0.6.13", + "html-webpack-plugin": "^5.3.1", + "jasmine-core": "^4.5.0", + "karma": "^6.3.0", + "karma-chrome-launcher": "^3.1.0", + "karma-firefox-launcher": "^2.1.0", + "karma-jasmine": "^5.1.0", + "karma-jasmine-html-reporter": "^2.0.0", + "karma-jasmine-spec-tags": "^2.0.0", + "karma-source-map-support": "^1.4.0", + "karma-sourcemap-loader": "^0.3.7", + "karma-spec-reporter": "^0.0.36", + "karma-webkit-launcher": "^2.1.0", + "karma-webpack": "^5.0.0", + "playwright": "^1.30.0", + "prettier": "^2.8.8", + "prettier-eslint": "^15.0.1", + "prettier-eslint-cli": "^7.1.0", + "remark-gfm": "^3.0.1", + "source-map-loader": "^4.0.0", + "storybook": "^7.4.0", + "ts-loader": "^9.2.5", + "typescript": "~4.8.2", + "webpack": "^5.75.0", + "webpack-cli": "^5.0.1", + "webpack-dev-middleware": "^6.0.1" + } + }, + "packages/spright-components/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "packages/xliff-to-json-converter": { "name": "@ni/xliff-to-json-converter", "version": "1.1.4", diff --git a/package.json b/package.json index 0ae1858b76e..2f9443bcf43 100644 --- a/package.json +++ b/package.json @@ -37,9 +37,12 @@ "packages/xliff-to-json-converter", "packages/nimble-tokens", "packages/nimble-components", + "packages/spright-components", "angular-workspace", "angular-workspace/projects/ni/nimble-angular", + "angular-workspace/projects/ni/spright-angular", "packages/nimble-blazor", + "packages/spright-blazor", "packages/performance", "packages/site" ], diff --git a/packages/spright-blazor/.eslintignore b/packages/spright-blazor/.eslintignore new file mode 100644 index 00000000000..168d39dd54e --- /dev/null +++ b/packages/spright-blazor/.eslintignore @@ -0,0 +1,8 @@ +dist +Examples/*/bin +Examples/*/obj +SprightBlazor/bin +SprightBlazor/obj +SprightBlazor/wwwroot/nimble-tokens +SprightBlazor/wwwroot/spright-components +SprightBlazor/wwwroot/SprightBlazor.HybridWorkaround.js \ No newline at end of file diff --git a/packages/spright-blazor/.eslintrc.js b/packages/spright-blazor/.eslintrc.js new file mode 100644 index 00000000000..bbce1773800 --- /dev/null +++ b/packages/spright-blazor/.eslintrc.js @@ -0,0 +1,29 @@ +module.exports = { + root: true, + extends: [ + '@ni/eslint-config-javascript' + ], + parserOptions: { + ecmaVersion: 2020 + }, + overrides: [ + { + files: [ + 'build/**/*.js' + ], + rules: { + // Build scripts will not be in published package and are allowed to use devDependencies + 'import/no-extraneous-dependencies': ['error', { devDependencies: true }], + + // Okay to use console.log in build scripts + 'no-console': 'off', + + // Rollup config files use default exports + 'import/no-default-export': 'off', + + // Enabled to prevent accidental usage of async-await + 'require-await': 'error' + } + } + ] +}; \ No newline at end of file diff --git a/packages/spright-blazor/.gitignore b/packages/spright-blazor/.gitignore new file mode 100644 index 00000000000..3903ef69a23 --- /dev/null +++ b/packages/spright-blazor/.gitignore @@ -0,0 +1,46 @@ +# Folders +SprightBlazor/wwwroot/nimble-tokens/ +SprightBlazor/wwwroot/spright-components/ +SprightBlazor/wwwroot/SprightBlazor.HybridWorkaround.js +build/generate-playwright-version-properties/dist/ +artifacts/ +bin/ +obj/ +.dotnet/ +.nuget/ +.packages/ +.tools/ +.vs/ +.vscode/ +node_modules/ +BenchmarkDotNet.Artifacts/ +modules/ + +# File extensions +*.aps +*.binlog +*.dll +*.DS_Store +*.exe +*.idb +*.lib +*.log +*.pch +*.pdb +*.pidb +*.psess +*.res +*.snk +*.so +*.suo +*.tlog +*.user +*.userprefs +*.vspx + +# Specific files, typically generated by tools +msbuild.ProjectImports.zip +StyleCop.Cache +UpgradeLog.htm +.idea +*.svclog diff --git a/packages/spright-blazor/CONTRIBUTING.md b/packages/spright-blazor/CONTRIBUTING.md new file mode 100644 index 00000000000..e96005c34d8 --- /dev/null +++ b/packages/spright-blazor/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# Contributing to Spright Blazor + +Contributors should follow the same instructions and guidelines as given in the [Nimble Blazor CONTRIBUTING doc](../nimble-blazor/CONTRIBUTING.md). \ No newline at end of file diff --git a/packages/spright-blazor/CodeAnalysisDictionary.xml b/packages/spright-blazor/CodeAnalysisDictionary.xml new file mode 100644 index 00000000000..1c9a08093ed --- /dev/null +++ b/packages/spright-blazor/CodeAnalysisDictionary.xml @@ -0,0 +1,23 @@ + + + + + args + blazor + bool + enum + getter + json + nullable + runtime + spright + tooltip + + + + + + + + + diff --git a/packages/spright-blazor/Examples/Demo.Client/Demo.Client.csproj b/packages/spright-blazor/Examples/Demo.Client/Demo.Client.csproj new file mode 100644 index 00000000000..03dd4b1b942 --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Client/Demo.Client.csproj @@ -0,0 +1,34 @@ + + + + net6.0 + enable + enable + + + + + + + + + True + ;CS0122 + ;NU1701;CA1707;CS0122;CS1573;CS1591,@(RoslynTransition_DisabledRule) + + + + ;CS0122 + ;NU1701;CA1707;CS0122;CS1573;CS1591,@(RoslynTransition_DisabledRule) + + + + + + + + + + + + diff --git a/packages/spright-blazor/Examples/Demo.Client/Program.cs b/packages/spright-blazor/Examples/Demo.Client/Program.cs new file mode 100644 index 00000000000..1499f7e3725 --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Client/Program.cs @@ -0,0 +1,12 @@ +#pragma warning disable CA1812 +using Demo.Shared; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; + +var builder = WebAssemblyHostBuilder.CreateDefault(args); +builder.RootComponents.Add("#app"); +builder.RootComponents.Add("head::after"); + +builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); + +await builder.Build().RunAsync(); diff --git a/packages/spright-blazor/Examples/Demo.Client/Properties/launchSettings.json b/packages/spright-blazor/Examples/Demo.Client/Properties/launchSettings.json new file mode 100644 index 00000000000..8068bf64977 --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Client/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:6621", + "sslPort": 44308 + } + }, + "profiles": { + "Demo.Client": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "https://localhost:7026;http://localhost:5026", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/packages/spright-blazor/Examples/Demo.Client/_Imports.razor b/packages/spright-blazor/Examples/Demo.Client/_Imports.razor new file mode 100644 index 00000000000..b2f2cd310d6 --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Client/_Imports.razor @@ -0,0 +1,10 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.AspNetCore.Components.WebAssembly.Http +@using Microsoft.JSInterop + + diff --git a/packages/spright-blazor/Examples/Demo.Client/wwwroot/css/site.css b/packages/spright-blazor/Examples/Demo.Client/wwwroot/css/site.css new file mode 100644 index 00000000000..d2c2e844e5b --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Client/wwwroot/css/site.css @@ -0,0 +1,37 @@ +body, html, #app { + width: 100%; + height: 100%; +} + +body { + margin: 0; +} + +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + +#blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; +} + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + +.blazor-error-boundary::after { + content: "An error has occurred." +} diff --git a/packages/spright-blazor/Examples/Demo.Client/wwwroot/favicon.ico b/packages/spright-blazor/Examples/Demo.Client/wwwroot/favicon.ico new file mode 100644 index 00000000000..0785a5aefbb Binary files /dev/null and b/packages/spright-blazor/Examples/Demo.Client/wwwroot/favicon.ico differ diff --git a/packages/spright-blazor/Examples/Demo.Client/wwwroot/index.html b/packages/spright-blazor/Examples/Demo.Client/wwwroot/index.html new file mode 100644 index 00000000000..15c1477f95e --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Client/wwwroot/index.html @@ -0,0 +1,27 @@ + + + + + + + Blazor All Components Demo - Spright - NI + + + + + + + + +
Loading...
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
+ + + + + diff --git a/packages/spright-blazor/Examples/Demo.Client/wwwroot/staticwebapp.config.json b/packages/spright-blazor/Examples/Demo.Client/wwwroot/staticwebapp.config.json new file mode 100644 index 00000000000..9d62611db51 --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Client/wwwroot/staticwebapp.config.json @@ -0,0 +1,5 @@ +{ + "navigationFallback": { + "rewrite": "/index.html" + } +} \ No newline at end of file diff --git a/packages/spright-blazor/Examples/Demo.Hybrid/App.xaml b/packages/spright-blazor/Examples/Demo.Hybrid/App.xaml new file mode 100644 index 00000000000..26f4fa02e60 --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Hybrid/App.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/packages/spright-blazor/Examples/Demo.Hybrid/App.xaml.cs b/packages/spright-blazor/Examples/Demo.Hybrid/App.xaml.cs new file mode 100644 index 00000000000..c2773ef7816 --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Hybrid/App.xaml.cs @@ -0,0 +1,11 @@ +namespace Demo.Hybrid +{ + using System.Windows; + + /// + /// Interaction logic for App.xaml. + /// + public partial class App : Application + { + } +} diff --git a/packages/spright-blazor/Examples/Demo.Hybrid/AssemblyInfo.cs b/packages/spright-blazor/Examples/Demo.Hybrid/AssemblyInfo.cs new file mode 100644 index 00000000000..13e5973dbc7 --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Hybrid/AssemblyInfo.cs @@ -0,0 +1,6 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, // where theme specific resource dictionaries are located (used if a resource is not found in the page, or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly) // where the generic resource dictionary is located (used if a resource is not found in the page, app, or any theme specific resource dictionaries) +] diff --git a/packages/spright-blazor/Examples/Demo.Hybrid/Demo.Hybrid.csproj b/packages/spright-blazor/Examples/Demo.Hybrid/Demo.Hybrid.csproj new file mode 100644 index 00000000000..3fd4de95f60 --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Hybrid/Demo.Hybrid.csproj @@ -0,0 +1,36 @@ + + + + WinExe + net6.0-windows + true + enable + true + + + + LRT001;SA1633;NU1701;CA1707;CS0122;CS1573;CS1591,@(RoslynTransition_DisabledRule) + + + + LRT001;SA1633;NU1701;CA1707;CS0122;CS1573;CS1591,@(RoslynTransition_DisabledRule) + + + + + + + + + + + + + + + + + + + + diff --git a/packages/spright-blazor/Examples/Demo.Hybrid/MainWindow.xaml b/packages/spright-blazor/Examples/Demo.Hybrid/MainWindow.xaml new file mode 100644 index 00000000000..7d769b1cc7c --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Hybrid/MainWindow.xaml @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/packages/spright-blazor/Examples/Demo.Hybrid/MainWindow.xaml.cs b/packages/spright-blazor/Examples/Demo.Hybrid/MainWindow.xaml.cs new file mode 100644 index 00000000000..456173a5c09 --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Hybrid/MainWindow.xaml.cs @@ -0,0 +1,41 @@ +namespace Demo.Hybrid +{ + using System.Windows; + using Microsoft.Extensions.DependencyInjection; + + /// + /// Interaction logic for MainWindow.xaml. + /// + public partial class MainWindow : Window + { + /// + /// Initializes a new instance of the class. + /// + public MainWindow() + { + this.InitializeComponent(); +#if RELEASE + this.Loaded += this.MainWindow_Loaded; +#endif + var serviceCollection = new ServiceCollection(); + serviceCollection.AddWpfBlazorWebView(); + serviceCollection.AddBlazorWebViewDeveloperTools(); +#pragma warning disable NI1004 + this.Resources.Add("services", serviceCollection.BuildServiceProvider()); +#pragma warning restore NI1004 + } + +#if RELEASE +#pragma warning disable VSTHRD100 // Avoid async void methods + private async void MainWindow_Loaded(object sender, RoutedEventArgs e) +#pragma warning restore VSTHRD100 // Avoid async void methods + { + // We recommend for Release versions to turn off browser accelerator keys + // for applications that are meant to operate more like a native desktop app. + await this.blazorWebView.WebView.EnsureCoreWebView2Async().ConfigureAwait(true); + var settings = this.blazorWebView.WebView.CoreWebView2.Settings; + settings.AreBrowserAcceleratorKeysEnabled = false; + } +#endif + } +} diff --git a/packages/spright-blazor/Examples/Demo.Hybrid/_Imports.razor b/packages/spright-blazor/Examples/Demo.Hybrid/_Imports.razor new file mode 100644 index 00000000000..ffd1385a17b --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Hybrid/_Imports.razor @@ -0,0 +1,2 @@ +@using Microsoft.AspNetCore.Components.Web +@using SprightBlazor \ No newline at end of file diff --git a/packages/spright-blazor/Examples/Demo.Hybrid/wwwroot/css/app.css b/packages/spright-blazor/Examples/Demo.Hybrid/wwwroot/css/app.css new file mode 100644 index 00000000000..66202171bb8 --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Hybrid/wwwroot/css/app.css @@ -0,0 +1,22 @@ +.validation-message { + color: red; +} + +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + +#blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; +} \ No newline at end of file diff --git a/packages/spright-blazor/Examples/Demo.Hybrid/wwwroot/index.html b/packages/spright-blazor/Examples/Demo.Hybrid/wwwroot/index.html new file mode 100644 index 00000000000..46e167b1fe8 --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Hybrid/wwwroot/index.html @@ -0,0 +1,29 @@ + + + + + + + Demo.Hybrid + + + + + + + +
Loading...
+ +
+ An unhandled error has occurred. + Reload + 🗙 +
+ + + + + + + diff --git a/packages/spright-blazor/Examples/Demo.Server/Demo.Server.csproj b/packages/spright-blazor/Examples/Demo.Server/Demo.Server.csproj new file mode 100644 index 00000000000..9c55b97c80e --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Server/Demo.Server.csproj @@ -0,0 +1,29 @@ + + + + net6.0 + enable + enable + + + + True + ;CS0122 + LRT001;NU1701;CA1707;CS0122;CS1573;CS1591,@(RoslynTransition_DisabledRule) + + + + ;CS0122 + LRT001;NU1701;CA1707;CS0122;CS1573;CS1591,@(RoslynTransition_DisabledRule) + + + + + + + + + + + + diff --git a/packages/spright-blazor/Examples/Demo.Server/Pages/Error.cshtml b/packages/spright-blazor/Examples/Demo.Server/Pages/Error.cshtml new file mode 100644 index 00000000000..1e496e1f698 --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Server/Pages/Error.cshtml @@ -0,0 +1,41 @@ +@page +@model Demo.Server.Pages.ErrorModel + + + + + + + + Error + + + + +
+
+

Error.

+

An error occurred while processing your request.

+ + @if (Model.ShowRequestId) + { +

+ Request ID: @Model.RequestId +

+ } + +

Development Mode

+

+ Swapping to the Development environment displays detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+
+
+ + + diff --git a/packages/spright-blazor/Examples/Demo.Server/Pages/Error.cshtml.cs b/packages/spright-blazor/Examples/Demo.Server/Pages/Error.cshtml.cs new file mode 100644 index 00000000000..3c3b7b78948 --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Server/Pages/Error.cshtml.cs @@ -0,0 +1,27 @@ +using System.Diagnostics; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Demo.Server.Pages +{ + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + [IgnoreAntiforgeryToken] + public class ErrorModel : PageModel + { + private readonly ILogger _logger; + + public string? RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + public ErrorModel(ILogger logger) + { + _logger = logger; + } + + public void OnGet() + { + RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; + } + } +} \ No newline at end of file diff --git a/packages/spright-blazor/Examples/Demo.Server/Pages/_Host.cshtml b/packages/spright-blazor/Examples/Demo.Server/Pages/_Host.cshtml new file mode 100644 index 00000000000..00360761428 --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Server/Pages/_Host.cshtml @@ -0,0 +1,8 @@ +@page "/" +@namespace Demo.Server.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = "_Layout"; +} + + diff --git a/packages/spright-blazor/Examples/Demo.Server/Pages/_Layout.cshtml b/packages/spright-blazor/Examples/Demo.Server/Pages/_Layout.cshtml new file mode 100644 index 00000000000..35df3fe6088 --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Server/Pages/_Layout.cshtml @@ -0,0 +1,33 @@ +@using Microsoft.AspNetCore.Components.Web +@namespace Demo.Server.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers + + + + + + + + + + + + + + @RenderBody() + +
+ + An error has occurred. This application may no longer respond until reloaded. + + + An unhandled exception has occurred. See browser dev tools for details. + + Reload + 🗙 +
+ + + + + \ No newline at end of file diff --git a/packages/spright-blazor/Examples/Demo.Server/Program.cs b/packages/spright-blazor/Examples/Demo.Server/Program.cs new file mode 100644 index 00000000000..233f20cf9ea --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Server/Program.cs @@ -0,0 +1,29 @@ +#pragma warning disable CA1812 + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddRazorPages(); +builder.Services.AddServerSideBlazor(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error"); + + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} + +app.UseHttpsRedirection(); + +app.UseStaticFiles(); + +app.UseRouting(); + +app.MapBlazorHub(); +app.MapFallbackToPage("/_Host"); + +app.Run(); diff --git a/packages/spright-blazor/Examples/Demo.Server/Properties/launchSettings.json b/packages/spright-blazor/Examples/Demo.Server/Properties/launchSettings.json new file mode 100644 index 00000000000..8093e49a40e --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Server/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:14509", + "sslPort": 44320 + } + }, + "profiles": { + "Demo.Server": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7062;http://localhost:5062", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/packages/spright-blazor/Examples/Demo.Server/_Imports.razor b/packages/spright-blazor/Examples/Demo.Server/_Imports.razor new file mode 100644 index 00000000000..58199838bdf --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Server/_Imports.razor @@ -0,0 +1,10 @@ +@using System.Net.Http +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using Demo.Server + diff --git a/packages/spright-blazor/Examples/Demo.Server/appsettings.Development.json b/packages/spright-blazor/Examples/Demo.Server/appsettings.Development.json new file mode 100644 index 00000000000..770d3e93146 --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Server/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/packages/spright-blazor/Examples/Demo.Server/appsettings.json b/packages/spright-blazor/Examples/Demo.Server/appsettings.json new file mode 100644 index 00000000000..10f68b8c8b4 --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Server/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/packages/spright-blazor/Examples/Demo.Server/wwwroot/css/site.css b/packages/spright-blazor/Examples/Demo.Server/wwwroot/css/site.css new file mode 100644 index 00000000000..8024c8b7fb5 --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Server/wwwroot/css/site.css @@ -0,0 +1,37 @@ +body, html { + width: 100%; + height: 100%; +} + +body { + margin: 0; +} + +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + +#blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; +} + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + +.blazor-error-boundary::after { + content: "An error has occurred." +} diff --git a/packages/spright-blazor/Examples/Demo.Server/wwwroot/favicon.ico b/packages/spright-blazor/Examples/Demo.Server/wwwroot/favicon.ico new file mode 100644 index 00000000000..0785a5aefbb Binary files /dev/null and b/packages/spright-blazor/Examples/Demo.Server/wwwroot/favicon.ico differ diff --git a/packages/spright-blazor/Examples/Demo.Shared/App.razor b/packages/spright-blazor/Examples/Demo.Shared/App.razor new file mode 100644 index 00000000000..843f201ab32 --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Shared/App.razor @@ -0,0 +1,11 @@ + + + + + + Not found + +

Sorry, there's nothing at this address.

+
+
+
\ No newline at end of file diff --git a/packages/spright-blazor/Examples/Demo.Shared/Demo.Shared.csproj b/packages/spright-blazor/Examples/Demo.Shared/Demo.Shared.csproj new file mode 100644 index 00000000000..fae1a8a4603 --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Shared/Demo.Shared.csproj @@ -0,0 +1,39 @@ + + + + net6.0 + enable + enable + false + + + + True + ;CS0122 + CA1716;LRT001;NU1701;CA1707;CS0122;CS1573;CS1591,@(RoslynTransition_DisabledRule) + + + + ;CS0122 + CA1716;LRT001;NU1701;CA1707;CS0122;CS1573;CS1591,@(RoslynTransition_DisabledRule) + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/spright-blazor/Examples/Demo.Shared/Pages/ComponentsDemo.razor b/packages/spright-blazor/Examples/Demo.Shared/Pages/ComponentsDemo.razor new file mode 100644 index 00000000000..26d61dfad12 --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Shared/Pages/ComponentsDemo.razor @@ -0,0 +1,18 @@ +@page "/" +@namespace Demo.Shared.Pages +@inherits LayoutComponentBase + +
+

+ Explore the components below to see the Spright components in action. +

+
+
+
Accordion Component
+ + Accordion + Numeric field + +
+
+
diff --git a/packages/spright-blazor/Examples/Demo.Shared/Pages/ComponentsDemo.razor.cs b/packages/spright-blazor/Examples/Demo.Shared/Pages/ComponentsDemo.razor.cs new file mode 100644 index 00000000000..4b0f1d7d402 --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Shared/Pages/ComponentsDemo.razor.cs @@ -0,0 +1,13 @@ +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using SprightBlazor; + +namespace Demo.Shared.Pages +{ + /// + /// The components demo page + /// + public partial class ComponentsDemo + { + } +} diff --git a/packages/spright-blazor/Examples/Demo.Shared/Pages/ComponentsDemo.razor.css b/packages/spright-blazor/Examples/Demo.Shared/Pages/ComponentsDemo.razor.css new file mode 100644 index 00000000000..0fb6f33ba76 --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Shared/Pages/ComponentsDemo.razor.css @@ -0,0 +1,40 @@ +.root { + background-color: var(--ni-nimble-application-background-color); +} + +.content { + font: var(--ni-nimble-body-font); + overflow-y: auto; +} + +.container-label { + font: var(--ni-nimble-group-header-font); + color: var(--ni-nimble-group-header-font-color); + padding-bottom: var(--ni-nimble-standard-padding); +} + +p, a { + color: var(--ni-nimble-control-label-font-color); +} + +.container { + background-color: var(--ni-nimble-application-background-color); + display: inline-flex; + justify-content: left; + flex-direction: column; + padding: 0px 2px 0px 10px; +} + +.sub-container { + margin: var(--ni-nimble-standard-padding); +} + +.sub-container > * { + padding-right: var(--ni-nimble-standard-padding); +} + +.theme-select { + display: block; + width: 150px; + min-width: unset; +} \ No newline at end of file diff --git a/packages/spright-blazor/Examples/Demo.Shared/Shared/ExampleHeader.razor b/packages/spright-blazor/Examples/Demo.Shared/Shared/ExampleHeader.razor new file mode 100644 index 00000000000..d6dfd460864 --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Shared/Shared/ExampleHeader.razor @@ -0,0 +1,19 @@ +@using NimbleBlazor +@namespace Demo.Shared + +
+ + + +
+ User Settings +
+
+ diff --git a/packages/spright-blazor/Examples/Demo.Shared/Shared/ExampleHeader.razor.cs b/packages/spright-blazor/Examples/Demo.Shared/Shared/ExampleHeader.razor.cs new file mode 100644 index 00000000000..69bc3722aa1 --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Shared/Shared/ExampleHeader.razor.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.Components; +using NimbleBlazor; + +namespace Demo.Shared +{ + /// + /// The ExampleHeader Component. + /// + public partial class ExampleHeader + { + private NimbleDrawer? _drawerReference; + + [Parameter] + public Theme Theme { get; set; } + + [Parameter] + public EventCallback ThemeChanged { get; set; } + + private string ThemeAsString + { + get => Theme.ToString(); + set => Theme = (Theme)Enum.Parse(typeof(Theme), value); + } + + private async void OnUserThemeChange(string newTheme) + { + ThemeAsString = newTheme; + await ThemeChanged.InvokeAsync(Theme); + } + + private async void OnUserSettingsSelected() + { + await _drawerReference!.ShowAsync(); + } + + private async void OnCloseButtonClicked() + { + await _drawerReference!.CloseAsync(); + } + } +} \ No newline at end of file diff --git a/packages/spright-blazor/Examples/Demo.Shared/Shared/ExampleHeader.razor.css b/packages/spright-blazor/Examples/Demo.Shared/Shared/ExampleHeader.razor.css new file mode 100644 index 00000000000..d8365bd37b4 --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Shared/Shared/ExampleHeader.razor.css @@ -0,0 +1,28 @@ +.header { + --example-client-header-height: 48px; + background-color: var(--ni-nimble-header-background-color); + display: flex; + justify-content: flex-end; + height: var(--example-client-header-height); + flex-shrink: 0; +} + +::deep .user-settings-drawer { + padding: var(--ni-nimble-standard-padding); + display: flex; + flex-direction: column; +} + +::deep .theme-select { + width: 150px; + min-width: unset; +} + +::deep .close-button { + align-self: flex-end; +} + +.button-container { + display: flex; + align-items: center; +} diff --git a/packages/spright-blazor/Examples/Demo.Shared/Shared/MainLayout.razor b/packages/spright-blazor/Examples/Demo.Shared/Shared/MainLayout.razor new file mode 100644 index 00000000000..cc8d0015cfc --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Shared/Shared/MainLayout.razor @@ -0,0 +1,20 @@ +@using Microsoft.AspNetCore.Components +@using NimbleBlazor +@namespace Demo.Shared +@inherits LayoutComponentBase + +Blazor All Components Demo - Spright - NI + + +
+ + + + @Body + + +

@ex.Message

+
+
+
+
diff --git a/packages/spright-blazor/Examples/Demo.Shared/Shared/MainLayout.razor.cs b/packages/spright-blazor/Examples/Demo.Shared/Shared/MainLayout.razor.cs new file mode 100644 index 00000000000..fdf5ea884a2 --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Shared/Shared/MainLayout.razor.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.JSInterop; +using NimbleBlazor; + +namespace Demo.Shared +{ + /// + /// The MainLayout Component. + /// + public partial class MainLayout + { + private Theme Theme { get; set; } = Theme.Light; + + public ErrorBoundary? ErrorBoundary { get; set; } + + [Inject] + public IJSRuntime? JSRuntime { get; set; } + + protected override void OnParametersSet() + { + ErrorBoundary?.Recover(); + } + } +} \ No newline at end of file diff --git a/packages/spright-blazor/Examples/Demo.Shared/Shared/MainLayout.razor.css b/packages/spright-blazor/Examples/Demo.Shared/Shared/MainLayout.razor.css new file mode 100644 index 00000000000..9c95a98781c --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Shared/Shared/MainLayout.razor.css @@ -0,0 +1,6 @@ +.root { + display: flex; + flex-direction: column; + height: 100%; + background-color: var(--ni-nimble-application-background-color); +} diff --git a/packages/spright-blazor/Examples/Demo.Shared/_Imports.razor b/packages/spright-blazor/Examples/Demo.Shared/_Imports.razor new file mode 100644 index 00000000000..c3bfbdfcf6d --- /dev/null +++ b/packages/spright-blazor/Examples/Demo.Shared/_Imports.razor @@ -0,0 +1,8 @@ +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using NimbleBlazor +@using SprightBlazor +@using Demo.Shared.Pages \ No newline at end of file diff --git a/packages/spright-blazor/README.md b/packages/spright-blazor/README.md new file mode 100644 index 00000000000..51b9fee45b8 --- /dev/null +++ b/packages/spright-blazor/README.md @@ -0,0 +1,15 @@ +
+

ni | spright | blazor

+
+ +# Spright Blazor + +[![Nuget Version](https://img.shields.io/nuget/v/SprightBlazor.svg)](https://www.nuget.org/packages/SprightBlazor) + +Experimental, high-level, or product-specific, NI-styled UI components for Blazor applications + +This package follows the same structure and patterns as the Nimble Blazor package, so refer to the [Nimble Blazor README](https://github.com/ni/nimble/blob/main/packages/nimble-blazor/README.md) for guidance. + +## Contributing + +Follow the instructions in [CONTRIBUTING.md](CONTRIBUTING.md) to modify this library. diff --git a/packages/spright-blazor/SprightBlazor.sln b/packages/spright-blazor/SprightBlazor.sln new file mode 100644 index 00000000000..0e0e885de1a --- /dev/null +++ b/packages/spright-blazor/SprightBlazor.sln @@ -0,0 +1,73 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.32112.339 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SprightBlazor", "SprightBlazor\SprightBlazor.csproj", "{FD4F8A39-17CC-4118-A192-3E392FA728CA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SprightBlazor.Tests", "Tests\SprightBlazor.Tests\SprightBlazor.Tests.csproj", "{1E11DA86-D43D-4CF7-94F5-B4565450BF4C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{638B1C16-782F-4C91-A09C-3569957356DF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Demo.Client", "Examples\Demo.Client\Demo.Client.csproj", "{A8C1D36B-77FA-4D9B-893E-226FA2786D31}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Demo.Server", "Examples\Demo.Server\Demo.Server.csproj", "{6A1D0B77-BBF2-415E-B3A8-FAB00879F07C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Demo.Shared", "Examples\Demo.Shared\Demo.Shared.csproj", "{8B6E367C-E472-4E68-98D2-968CFCF6939D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{E5C31FAF-7DEF-494F-A0D2-C9A4875F2132}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Demo.Hybrid", "Examples\Demo.Hybrid\Demo.Hybrid.csproj", "{EAC50129-EF2E-4E7B-98D0-64502E97ED8B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SprightBlazor.Tests.Acceptance", "Tests\SprightBlazor.Tests.Acceptance\SprightBlazor.Tests.Acceptance.csproj", "{7C65AEA1-8CA2-48DC-81FE-CE39295BDD4B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FD4F8A39-17CC-4118-A192-3E392FA728CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FD4F8A39-17CC-4118-A192-3E392FA728CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FD4F8A39-17CC-4118-A192-3E392FA728CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FD4F8A39-17CC-4118-A192-3E392FA728CA}.Release|Any CPU.Build.0 = Release|Any CPU + {1E11DA86-D43D-4CF7-94F5-B4565450BF4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1E11DA86-D43D-4CF7-94F5-B4565450BF4C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1E11DA86-D43D-4CF7-94F5-B4565450BF4C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1E11DA86-D43D-4CF7-94F5-B4565450BF4C}.Release|Any CPU.Build.0 = Release|Any CPU + {A8C1D36B-77FA-4D9B-893E-226FA2786D31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A8C1D36B-77FA-4D9B-893E-226FA2786D31}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A8C1D36B-77FA-4D9B-893E-226FA2786D31}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A8C1D36B-77FA-4D9B-893E-226FA2786D31}.Release|Any CPU.Build.0 = Release|Any CPU + {6A1D0B77-BBF2-415E-B3A8-FAB00879F07C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6A1D0B77-BBF2-415E-B3A8-FAB00879F07C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6A1D0B77-BBF2-415E-B3A8-FAB00879F07C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6A1D0B77-BBF2-415E-B3A8-FAB00879F07C}.Release|Any CPU.Build.0 = Release|Any CPU + {8B6E367C-E472-4E68-98D2-968CFCF6939D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8B6E367C-E472-4E68-98D2-968CFCF6939D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8B6E367C-E472-4E68-98D2-968CFCF6939D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8B6E367C-E472-4E68-98D2-968CFCF6939D}.Release|Any CPU.Build.0 = Release|Any CPU + {EAC50129-EF2E-4E7B-98D0-64502E97ED8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EAC50129-EF2E-4E7B-98D0-64502E97ED8B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EAC50129-EF2E-4E7B-98D0-64502E97ED8B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EAC50129-EF2E-4E7B-98D0-64502E97ED8B}.Release|Any CPU.Build.0 = Release|Any CPU + {7C65AEA1-8CA2-48DC-81FE-CE39295BDD4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C65AEA1-8CA2-48DC-81FE-CE39295BDD4B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C65AEA1-8CA2-48DC-81FE-CE39295BDD4B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C65AEA1-8CA2-48DC-81FE-CE39295BDD4B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {1E11DA86-D43D-4CF7-94F5-B4565450BF4C} = {E5C31FAF-7DEF-494F-A0D2-C9A4875F2132} + {A8C1D36B-77FA-4D9B-893E-226FA2786D31} = {638B1C16-782F-4C91-A09C-3569957356DF} + {6A1D0B77-BBF2-415E-B3A8-FAB00879F07C} = {638B1C16-782F-4C91-A09C-3569957356DF} + {8B6E367C-E472-4E68-98D2-968CFCF6939D} = {638B1C16-782F-4C91-A09C-3569957356DF} + {EAC50129-EF2E-4E7B-98D0-64502E97ED8B} = {638B1C16-782F-4C91-A09C-3569957356DF} + {7C65AEA1-8CA2-48DC-81FE-CE39295BDD4B} = {E5C31FAF-7DEF-494F-A0D2-C9A4875F2132} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {38E2A588-0714-41E7-9BA3-D89622560FF9} + EndGlobalSection +EndGlobal diff --git a/packages/spright-blazor/SprightBlazor/Components/SprightAccordion.razor b/packages/spright-blazor/SprightBlazor/Components/SprightAccordion.razor new file mode 100644 index 00000000000..2f22596beff --- /dev/null +++ b/packages/spright-blazor/SprightBlazor/Components/SprightAccordion.razor @@ -0,0 +1,5 @@ +@namespace SprightBlazor + + @ChildContent + diff --git a/packages/spright-blazor/SprightBlazor/Components/SprightAccordion.razor.cs b/packages/spright-blazor/SprightBlazor/Components/SprightAccordion.razor.cs new file mode 100644 index 00000000000..e5de766e7f9 --- /dev/null +++ b/packages/spright-blazor/SprightBlazor/Components/SprightAccordion.razor.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Components; + +namespace SprightBlazor; + +public partial class SprightAccordion : ComponentBase +{ + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter(CaptureUnmatchedValues = true)] + public IDictionary? AdditionalAttributes { get; set; } +} diff --git a/packages/spright-blazor/SprightBlazor/Properties/AssemblyInfo.cs b/packages/spright-blazor/SprightBlazor/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..9b5149cfdcf --- /dev/null +++ b/packages/spright-blazor/SprightBlazor/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("SprightBlazor.Tests")] diff --git a/packages/spright-blazor/SprightBlazor/SprightBlazor.csproj b/packages/spright-blazor/SprightBlazor/SprightBlazor.csproj new file mode 100644 index 00000000000..4cb179e2cf7 --- /dev/null +++ b/packages/spright-blazor/SprightBlazor/SprightBlazor.csproj @@ -0,0 +1,49 @@ + + + + net6.0 + enable + enable + embedded + NI + https://github.com/ni/nimble + https://github.com/ni/nimble + git + Web Components, .NET, Spright, Nimble, FAST, Blazor, .NET 6.0 + en + MIT + + + + 1701;1702,8669,1591 + + + + CS8669 + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/spright-blazor/SprightBlazor/_Imports.razor b/packages/spright-blazor/SprightBlazor/_Imports.razor new file mode 100644 index 00000000000..ff72d4aa88a --- /dev/null +++ b/packages/spright-blazor/SprightBlazor/_Imports.razor @@ -0,0 +1,4 @@ +@namespace SprightBlazor +@using System +@using System.Diagnostics.CodeAnalysis +@using Microsoft.AspNetCore.Components.Web diff --git a/packages/spright-blazor/SprightBlazor/wwwroot/SprightBlazor.lib.module.js b/packages/spright-blazor/SprightBlazor/wwwroot/SprightBlazor.lib.module.js new file mode 100644 index 00000000000..b5d3af37e11 --- /dev/null +++ b/packages/spright-blazor/SprightBlazor/wwwroot/SprightBlazor.lib.module.js @@ -0,0 +1,42 @@ +/* eslint-disable func-names */ +/* eslint-disable no-undef */ + +/** + * Register the custom event types used by Spright components. + * + * JavaScript initializer for SprightBlazor project, see + * https://docs.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/?view=aspnetcore-6.0#javascript-initializers + */ + +export function afterStarted(Blazor) { + if (window.SprightBlazor.calledAfterStarted) { + console.warn('Attempted to initialize Spright Blazor multiple times!'); // eslint-disable-line + return; + } + + if (!Blazor) { + throw new Error('Blazor not ready to initialize Spright!'); + } + + window.SprightBlazor.calledAfterStarted = true; + + /* Register any custom events here + Blazor.registerCustomEventType('sprighteventname', { + browserEventName: 'foo', + createEventArgs: event => { + return { + newState: event.detail.newState, + oldState: event.detail.oldState + }; + } + }); + */ +} + +if (window.SprightBlazor) { + console.warn('Attempting to initialize SprightBlazor multiple times!'); // eslint-disable-line +} + +window.SprightBlazor = window.SprightBlazor ?? { + calledAfterStarted: false +}; diff --git a/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/App.razor b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/App.razor new file mode 100644 index 00000000000..6fd3ed1b5a3 --- /dev/null +++ b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/App.razor @@ -0,0 +1,12 @@ + + + + + + + Not found + +

Sorry, there's nothing at this address.

+
+
+
diff --git a/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/BlazorServerWebHostFixture.cs b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/BlazorServerWebHostFixture.cs new file mode 100644 index 00000000000..21d5a311279 --- /dev/null +++ b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/BlazorServerWebHostFixture.cs @@ -0,0 +1,18 @@ +namespace SprightBlazor.Tests.Acceptance; + +/// +/// Test fixture which starts up a Blazor Server web server +/// +public class BlazorServerWebHostFixture : WebHostServerFixture +{ + protected override IHost CreateWebHost() + { + return new HostBuilder() + .ConfigureWebHost(webHostBuilder => webHostBuilder + .UseKestrel() + .UseStartup() + .UseStaticWebAssets() + .UseUrls("http://127.0.0.1:0")) // Pick a port dynamically + .Build(); + } +} diff --git a/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Pages/AccordionClick.razor b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Pages/AccordionClick.razor new file mode 100644 index 00000000000..4081c3f5b74 --- /dev/null +++ b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Pages/AccordionClick.razor @@ -0,0 +1,14 @@ +@page "/AccordionClick" +@namespace SprightBlazor.Tests.Acceptance.Pages +@inherits LayoutComponentBase + +Foo + +@code { + private SprightAccordion? _accordion; + + public async Task ClickHandler() + { + // do something + } +} diff --git a/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Pages/_Host.cshtml b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Pages/_Host.cshtml new file mode 100644 index 00000000000..a0e7413dd31 --- /dev/null +++ b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Pages/_Host.cshtml @@ -0,0 +1,8 @@ +@page "/" +@namespace SprightBlazor.Tests.Acceptance.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = "_Layout"; +} + + diff --git a/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Pages/_Layout.cshtml b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Pages/_Layout.cshtml new file mode 100644 index 00000000000..22285b58761 --- /dev/null +++ b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Pages/_Layout.cshtml @@ -0,0 +1,33 @@ +@using Microsoft.AspNetCore.Components.Web +@namespace SprightBlazor.Tests.Acceptance.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers + + + + + + + + + + + + + + + @RenderBody() + +
+ + An error has occurred. This application may no longer respond until reloaded. + + + An unhandled exception has occurred. See browser dev tools for details. + + Reload + 🗙 +
+ + + + diff --git a/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/PlaywrightFixture.cs b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/PlaywrightFixture.cs new file mode 100644 index 00000000000..a7e4f2150a3 --- /dev/null +++ b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/PlaywrightFixture.cs @@ -0,0 +1,45 @@ +using Microsoft.Playwright; +using Xunit; +using PlaywrightProgram = Microsoft.Playwright.Program; + +namespace SprightBlazor.Tests.Acceptance; + +/// +/// Fixture to handle Playwright initialization for acceptance tests. +/// +public class PlaywrightFixture : IAsyncLifetime +{ + private IBrowser? _browser; + private IPlaywright? _playwright; + public IBrowserContext? BrowserContext { get; private set; } + + public async Task InitializeAsync() + { + _playwright = await Playwright.CreateAsync(); + _browser = await _playwright.Chromium.LaunchAsync( + new BrowserTypeLaunchOptions() + { +#if DEBUG + Headless = false, + SlowMo = 1000 +#endif + }); + BrowserContext = await _browser.NewContextAsync(new BrowserNewContextOptions { IgnoreHTTPSErrors = true }); +#if DEBUG + BrowserContext.SetDefaultTimeout(30000); +#endif + } + + public async Task DisposeAsync() + { + if (BrowserContext != null) + { + await BrowserContext.DisposeAsync(); + } + if (_browser != null) + { + await _browser.DisposeAsync(); + } + _playwright?.Dispose(); + } +} \ No newline at end of file diff --git a/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Program.cs b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Program.cs new file mode 100644 index 00000000000..b505500b6f9 --- /dev/null +++ b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Program.cs @@ -0,0 +1,21 @@ +namespace SprightBlazor.Tests.Acceptance +{ + /// + /// Main entry point which spins up the web server and allows loading the Razor fixtures/pages in a browser + /// without running a specific test. + /// + public static class Program + { + public static void Main(string[] arguments) + { + var builder = WebApplication.CreateBuilder(arguments); + + var startup = new Startup(builder.Configuration); + startup.ConfigureServices(builder.Services); + var app = builder.Build(); + startup.Configure(app); + + app.Run(); + } + } +} diff --git a/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Properties/launchSettings.json b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Properties/launchSettings.json new file mode 100644 index 00000000000..71fb0ae4d6e --- /dev/null +++ b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:44651" + } + }, + "profiles": { + "SprightBlazor.Acceptance.Tests": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5202", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Shared/MainLayout.razor b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Shared/MainLayout.razor new file mode 100644 index 00000000000..b9118a8dba0 --- /dev/null +++ b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Shared/MainLayout.razor @@ -0,0 +1,20 @@ +@using Microsoft.AspNetCore.Components +@using NimbleBlazor +@namespace SprightBlazor.Tests.Acceptance.Shared +@inherits LayoutComponentBase + +Spright Blazor tests + +
+ + +
+ @Body +
+
+ +

@ex.Message

+
+
+
+
diff --git a/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Shared/MainLayout.razor.cs b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Shared/MainLayout.razor.cs new file mode 100644 index 00000000000..7318bc5d8bf --- /dev/null +++ b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Shared/MainLayout.razor.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.JSInterop; +using NimbleBlazor; + +namespace SprightBlazor.Tests.Acceptance.Shared +{ + /// + /// The MainLayout Component. + /// + public partial class MainLayout + { + private Theme Theme { get; set; } = Theme.Light; + + public ErrorBoundary? ErrorBoundary { get; set; } + + [Inject] + public IJSRuntime? JSRuntime { get; set; } + + protected override void OnParametersSet() + { + ErrorBoundary?.Recover(); + } + } +} \ No newline at end of file diff --git a/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Shared/MainLayout.razor.css b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Shared/MainLayout.razor.css new file mode 100644 index 00000000000..9c95a98781c --- /dev/null +++ b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Shared/MainLayout.razor.css @@ -0,0 +1,6 @@ +.root { + display: flex; + flex-direction: column; + height: 100%; + background-color: var(--ni-nimble-application-background-color); +} diff --git a/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/SharedPlaywrightCollectionDefinition.cs b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/SharedPlaywrightCollectionDefinition.cs new file mode 100644 index 00000000000..28f4a0bddeb --- /dev/null +++ b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/SharedPlaywrightCollectionDefinition.cs @@ -0,0 +1,12 @@ +using Xunit; + +namespace SprightBlazor.Tests.Acceptance +{ + [CollectionDefinition(nameof(PlaywrightFixture))] + public class SharedPlaywrightCollectionDefinition : ICollectionFixture + { + // This class has no code, and is never created. Its purpose is simply + // to be the place to apply [CollectionDefinition] and all the + // ICollectionFixture<> interfaces. + } +} diff --git a/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/SprightBlazor.Tests.Acceptance.csproj b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/SprightBlazor.Tests.Acceptance.csproj new file mode 100644 index 00000000000..14c660ed6a3 --- /dev/null +++ b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/SprightBlazor.Tests.Acceptance.csproj @@ -0,0 +1,43 @@ + + + + + net6.0 + enable + enable + false + + + + CA1716;LRT001;NU1701;CA1707;CS0122;CS1573;CS1591,@(RoslynTransition_DisabledRule) + + + + CA1716;LRT001;NU1701;CA1707;CS0122;CS1573;CS1591,@(RoslynTransition_DisabledRule) + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Startup.cs b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Startup.cs new file mode 100644 index 00000000000..856e6472fbf --- /dev/null +++ b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Startup.cs @@ -0,0 +1,32 @@ +namespace SprightBlazor.Tests.Acceptance; + +/// +/// Web server initialization for Blazor Server +/// +public sealed class Startup +{ + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + services.AddRazorPages(); + services.AddServerSideBlazor(); + } + + public void Configure(IApplicationBuilder app) + { + app.UseDeveloperExceptionPage(); + app.UseStaticFiles(); + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapBlazorHub(); + endpoints.MapFallbackToPage("/_Host"); + }); + } +} \ No newline at end of file diff --git a/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Tests/AcceptanceTestsBase.cs b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Tests/AcceptanceTestsBase.cs new file mode 100644 index 00000000000..4b55cad21fd --- /dev/null +++ b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Tests/AcceptanceTestsBase.cs @@ -0,0 +1,62 @@ +using Microsoft.Playwright; +using Xunit; + +namespace SprightBlazor.Tests.Acceptance +{ + [Collection(nameof(PlaywrightFixture))] + public abstract class AcceptanceTestsBase : IClassFixture + { + private PlaywrightFixture _playwrightFixture; + private readonly BlazorServerWebHostFixture _blazorServerClassFixture; + + protected AcceptanceTestsBase( + PlaywrightFixture playwrightFixture, + BlazorServerWebHostFixture blazorServerClassFixture) + { + _playwrightFixture = playwrightFixture; + _blazorServerClassFixture = blazorServerClassFixture; + } + + private IBrowserContext BrowserContext + { + get + { + return _playwrightFixture.BrowserContext!; + } + } + + protected async Task NewPageForRouteAsync(string route) + { + var page = await BrowserContext.NewPageAsync(); + await NavigateToPageAsync(page, route); + await WaitForSprightBlazorInitializationAsync(page); + return new AsyncDisposablePage(page); + } + + private async Task NavigateToPageAsync(IPage page, string route) + { + var address = new Uri(_blazorServerClassFixture.ServerAddress!, route).AbsoluteUri; + await page.GotoAsync(address); + } + + private async Task WaitForSprightBlazorInitializationAsync(IPage page) + { + await page.WaitForFunctionAsync("window.SprightBlazor && window.SprightBlazor.calledAfterStarted === true"); + } + + protected sealed class AsyncDisposablePage : IAsyncDisposable + { + public IPage Page { get; private set; } + + public AsyncDisposablePage(IPage page) + { + Page = page; + } + + public async ValueTask DisposeAsync() + { + await Page.CloseAsync(); + } + } + } +} diff --git a/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Tests/AccordionTests.cs b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Tests/AccordionTests.cs new file mode 100644 index 00000000000..8496c2d7c77 --- /dev/null +++ b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/Tests/AccordionTests.cs @@ -0,0 +1,24 @@ +using Microsoft.Playwright; +using Xunit; + +namespace SprightBlazor.Tests.Acceptance +{ + public class AccordionTests : AcceptanceTestsBase + { + public AccordionTests(PlaywrightFixture playwrightFixture, BlazorServerWebHostFixture blazorServerClassFixture) + : base(playwrightFixture, blazorServerClassFixture) + { + } + + [Fact] + public async Task Accordion_DoesSomethingAsync() + { + await using (var pageWrapper = await NewPageForRouteAsync("AccordionClick")) + { + var page = pageWrapper.Page; + var accordionElement = page.Locator("spright-accordion", new PageLocatorOptions() { HasText = "Foo" }); + await accordionElement.ClickAsync(); + } + } + } +} diff --git a/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/WebHostServerFixture.cs b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/WebHostServerFixture.cs new file mode 100644 index 00000000000..1e6bb3f2903 --- /dev/null +++ b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/WebHostServerFixture.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Xunit; + +namespace SprightBlazor.Tests.Acceptance; + +public abstract class WebHostServerFixture : IAsyncLifetime, IDisposable +{ + private IHost? _host; + + public Uri? ServerAddress { get; set; } + + public async Task InitializeAsync() + { + _host = CreateWebHost(); + await _host.StartAsync(); + + var server = _host.Services.GetRequiredService(); + var addressFeature = server.Features.Get(); + ServerAddress = new Uri(addressFeature!.Addresses.First()); + } + + public async Task DisposeAsync() + { + if (_host != null) + { + await _host.StopAsync(); + _host.Dispose(); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _host?.Dispose(); + } + } + + protected abstract IHost CreateWebHost(); +} diff --git a/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/_Imports.razor b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/_Imports.razor new file mode 100644 index 00000000000..4983bda4941 --- /dev/null +++ b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/_Imports.razor @@ -0,0 +1,12 @@ +@using System.Net.Http +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using NimbleBlazor +@using SprightBlazor.Tests.Acceptance +@using SprightBlazor.Tests.Acceptance.Shared +@using SprightBlazor.Tests.Acceptance.Pages diff --git a/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/appsettings.Development.json b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/appsettings.Development.json new file mode 100644 index 00000000000..770d3e93146 --- /dev/null +++ b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/appsettings.json b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/appsettings.json new file mode 100644 index 00000000000..10f68b8c8b4 --- /dev/null +++ b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/wwwroot/css/site.css b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/wwwroot/css/site.css new file mode 100644 index 00000000000..3afadc202ee --- /dev/null +++ b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/wwwroot/css/site.css @@ -0,0 +1,28 @@ +#blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } diff --git a/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/wwwroot/favicon.ico b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/wwwroot/favicon.ico new file mode 100644 index 00000000000..63e859b476e Binary files /dev/null and b/packages/spright-blazor/Tests/SprightBlazor.Tests.Acceptance/wwwroot/favicon.ico differ diff --git a/packages/spright-blazor/Tests/SprightBlazor.Tests/Properties/AssemblyInfo.cs b/packages/spright-blazor/Tests/SprightBlazor.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..376679ee9e0 --- /dev/null +++ b/packages/spright-blazor/Tests/SprightBlazor.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,12 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyDescription("")] +[assembly: AssemblyCopyright("Copyright © 2024")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +[assembly: ComVisible(false)] + +[assembly: Guid("f5016674-c408-4918-8367-dd5a524ca55f")] +[assembly: AssemblyMetadata("Localize", "false")] diff --git a/packages/spright-blazor/Tests/SprightBlazor.Tests/SprightBlazor.Tests.csproj b/packages/spright-blazor/Tests/SprightBlazor.Tests/SprightBlazor.Tests.csproj new file mode 100644 index 00000000000..70f7a83cfe5 --- /dev/null +++ b/packages/spright-blazor/Tests/SprightBlazor.Tests/SprightBlazor.Tests.csproj @@ -0,0 +1,47 @@ + + + + net6.0 + + false + + SprightBlazor.Tests + + SprightBlazor.Tests + + + + True + ;CS0122 + ;NU1701;CA1707;CS0122;CS1573;CS1591,@(RoslynTransition_DisabledRule) + + + + ;CS0122 + ;NU1701;CA1707;CS0122;CS1573;CS1591,@(RoslynTransition_DisabledRule) + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/packages/spright-blazor/Tests/SprightBlazor.Tests/Unit/Components/SprightAccordionTests.cs b/packages/spright-blazor/Tests/SprightBlazor.Tests/Unit/Components/SprightAccordionTests.cs new file mode 100644 index 00000000000..cd9bb64d23f --- /dev/null +++ b/packages/spright-blazor/Tests/SprightBlazor.Tests/Unit/Components/SprightAccordionTests.cs @@ -0,0 +1,33 @@ +using System; +using System.Linq.Expressions; +using Bunit; +using Xunit; + +namespace SprightBlazor.Tests.Unit.Components; + +/// +/// Tests for . +/// +public class SprightAccordionTests +{ + [Fact] + public void SprightAccordion_Render_HasCorrectMarkup() + { + var context = new TestContext(); + context.JSInterop.Mode = JSRuntimeMode.Loose; + var expectedMarkup = "spright-accordion"; + + var component = context.RenderComponent(); + + Assert.Contains(expectedMarkup, component.Markup); + } + + [Fact] + public void SprightAccordion_SupportsAdditionalAttributes() + { + var context = new TestContext(); + context.JSInterop.Mode = JSRuntimeMode.Loose; + var exception = Record.Exception(() => context.RenderComponent(ComponentParameter.CreateParameter("class", "foo"))); + Assert.Null(exception); + } +} diff --git a/packages/spright-blazor/build/copyResources.js b/packages/spright-blazor/build/copyResources.js new file mode 100644 index 00000000000..58c0521348c --- /dev/null +++ b/packages/spright-blazor/build/copyResources.js @@ -0,0 +1,60 @@ +const path = require('path'); +const glob = require('glob'); +const fs = require('fs'); + +const destinationDirectory = path.resolve(__dirname, '../SprightBlazor/wwwroot'); +console.log(`Destination directory for blazor assets: "${destinationDirectory}"`); + +const sprightComponentsPath = resolvePackagePath('@ni/spright-components'); +const nimbleTokensPath = resolvePackagePath('@ni/nimble-tokens'); + +const componentsBasePath = path.resolve(sprightComponentsPath, 'dist'); +const componentsSrc = [ + { src: 'all-components-bundle.min.*' } +]; + +const tokensBasePath = path.resolve(nimbleTokensPath, 'dist/fonts'); +const tokensSrc = [ + { src: '**' } +]; + +prepareDestinationDirectory('spright-components'); +copyFiles(componentsSrc, componentsBasePath, 'spright-components'); +prepareDestinationDirectory('nimble-tokens'); +copyFiles(tokensSrc, tokensBasePath, 'nimble-tokens'); + +function resolvePackagePath(packageName) { + return path.dirname(require.resolve(`${packageName}/package.json`)); +} + +function copyFiles(srcPatterns, srcPath, destRelativeDirectory) { + for (const pattern of srcPatterns) { + const sourcePaths = glob.sync(pattern.src, { + // glob paths should only have forward slashes + // so run glob in resolved path (which has backslashes on windows) + cwd: srcPath, + absolute: true, + nodir: true + }); + if (sourcePaths.length <= 0) { + throw new Error(`No files found at path ${pattern.src}`); + } + for (const currentSrcPath of sourcePaths) { + const destRelativePath = pattern.dest ? pattern.dest : path.relative(srcPath, currentSrcPath); + const destAbsolutePath = path.resolve(destinationDirectory, destRelativeDirectory, destRelativePath); + const destAbsoluteDir = path.dirname(destAbsolutePath); + if (!fs.existsSync(destAbsoluteDir)) { + fs.mkdirSync(destAbsoluteDir, { recursive: true }); + } + fs.copyFileSync(currentSrcPath, destAbsolutePath); + } + } +} + +function prepareDestinationDirectory(destRelativeDirectory) { + const destDirectory = path.resolve(destinationDirectory, destRelativeDirectory); + if (fs.existsSync(destDirectory)) { + fs.rmSync(destDirectory, { recursive: true }); + } + fs.mkdirSync(destDirectory, { recursive: true }); +} diff --git a/packages/spright-blazor/build/generate-hybrid/.eslintrc.js b/packages/spright-blazor/build/generate-hybrid/.eslintrc.js new file mode 100644 index 00000000000..f0b0317bc42 --- /dev/null +++ b/packages/spright-blazor/build/generate-hybrid/.eslintrc.js @@ -0,0 +1,10 @@ +module.exports = { + overrides: [ + { + files: ['source/*.js'], + env: { + browser: true + }, + } + ] +}; \ No newline at end of file diff --git a/packages/spright-blazor/build/generate-hybrid/rollup.config.js b/packages/spright-blazor/build/generate-hybrid/rollup.config.js new file mode 100644 index 00000000000..23548cae241 --- /dev/null +++ b/packages/spright-blazor/build/generate-hybrid/rollup.config.js @@ -0,0 +1,12 @@ +import { nodeResolve } from '@rollup/plugin-node-resolve'; + +const path = require('path'); + +export default { + input: path.resolve(__dirname, 'source/SprightBlazor.HybridWorkaround.js'), + output: { + file: path.resolve(__dirname, '../../SprightBlazor/wwwroot/SprightBlazor.HybridWorkaround.js'), + format: 'iife' + }, + plugins: [nodeResolve()] +}; \ No newline at end of file diff --git a/packages/spright-blazor/build/generate-hybrid/source/SprightBlazor.HybridWorkaround.js b/packages/spright-blazor/build/generate-hybrid/source/SprightBlazor.HybridWorkaround.js new file mode 100644 index 00000000000..19c55f20756 --- /dev/null +++ b/packages/spright-blazor/build/generate-hybrid/source/SprightBlazor.HybridWorkaround.js @@ -0,0 +1,3 @@ +import { afterStarted } from '../../../SprightBlazor/wwwroot/SprightBlazor.lib.module'; + +afterStarted(window.Blazor); \ No newline at end of file diff --git a/packages/spright-blazor/build/generate-playwright-version-properties/source/Playwright.PackageVersion.template b/packages/spright-blazor/build/generate-playwright-version-properties/source/Playwright.PackageVersion.template new file mode 100644 index 00000000000..b09d0ce6090 --- /dev/null +++ b/packages/spright-blazor/build/generate-playwright-version-properties/source/Playwright.PackageVersion.template @@ -0,0 +1,5 @@ + + + PLAYWRIGHT_VERSION_PLACEHOLDER + + diff --git a/packages/spright-blazor/build/generate-playwright-version-properties/source/index.js b/packages/spright-blazor/build/generate-playwright-version-properties/source/index.js new file mode 100644 index 00000000000..8d68d37b27e --- /dev/null +++ b/packages/spright-blazor/build/generate-playwright-version-properties/source/index.js @@ -0,0 +1,16 @@ +const fs = require('fs'); +const path = require('path'); + +const resolvedPlaywrightPackageJsonPath = require.resolve('playwright/package.json'); +const playwrightVersion = JSON.parse(fs.readFileSync(resolvedPlaywrightPackageJsonPath, 'utf8')).version; + +const templatePath = path.resolve(__dirname, 'Playwright.PackageVersion.template'); +const templateContents = fs.readFileSync(templatePath, 'utf8'); +const propsFileContent = templateContents.replace(/PLAYWRIGHT_VERSION_PLACEHOLDER/g, playwrightVersion); + +const destAbsoluteDir = path.resolve(__dirname, '../dist/'); +if (!fs.existsSync(destAbsoluteDir)) { + fs.mkdirSync(destAbsoluteDir, { recursive: true }); +} +const destPath = path.resolve(destAbsoluteDir, 'Playwright.PackageVersion.props'); +fs.writeFileSync(destPath, propsFileContent, 'utf8'); diff --git a/packages/spright-blazor/global.json b/packages/spright-blazor/global.json new file mode 100644 index 00000000000..3735867d984 --- /dev/null +++ b/packages/spright-blazor/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "6.0.418", + "allowPrelease": "false", + "rollForward": "latestMinor" + } +} diff --git a/packages/spright-blazor/package.json b/packages/spright-blazor/package.json new file mode 100644 index 00000000000..251a507d580 --- /dev/null +++ b/packages/spright-blazor/package.json @@ -0,0 +1,53 @@ +{ + "name": "@ni/spright-blazor", + "version": "0.0.1", + "description": "Spright Blazor components", + "scripts": { + "postinstall": "node build/generate-playwright-version-properties/source/index.js", + "build": "npm run generate-hybrid && npm run build:release && npm run build:client", + "build:release": "dotnet build -c Release /p:TreatWarningsAsErrors=true /warnaserror", + "build:client": "dotnet publish -p:BlazorEnableCompression=false -c Release Examples/Demo.Client --output dist/blazor-client-app", + "generate-hybrid": "rollup --bundleConfigAsCjs --config build/generate-hybrid/rollup.config.js", + "lint": "npm run lint:cs && npm run lint:js", + "lint:cs": "dotnet format --verify-no-changes", + "lint:js": "eslint .", + "format": "npm run format:cs && npm run format:js", + "format:cs": "dotnet format", + "format:js": "eslint . --fix", + "test": "dotnet test -c Release", + "pack": "cross-env-shell dotnet pack SprightBlazor -c Release -p:PackageVersion=$npm_package_version --output dist", + "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/copyResources.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ni/nimble.git" + }, + "author": { + "name": "National Instruments" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/ni/nimble/issues" + }, + "homepage": "https://github.com/ni/nimble#readme", + "files": [ + "!*" + ], + "devDependencies": { + "@microsoft/fast-web-utilities": "^6.0.0", + "@ni/eslint-config-javascript": "^4.2.0", + "@ni/nimble-components": "*", + "@ni/nimble-tokens": "*", + "@ni/spright-components": "*", + "@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/spright-components/.eslintrc.js b/packages/spright-components/.eslintrc.js new file mode 100644 index 00000000000..c6fad828994 --- /dev/null +++ b/packages/spright-components/.eslintrc.js @@ -0,0 +1,33 @@ +module.exports = { + root: true, + extends: '@ni/eslint-config-javascript', + parserOptions: { + ecmaVersion: 2020 + }, + ignorePatterns: [ + // Force inclusion of storybook dot file hidden folder + '!/.storybook', + 'node_modules', + 'dist' + ], + rules: { + // Enabled to prevent accidental usage of async-await + 'require-await': 'error' + }, + overrides: [ + { + files: ['.storybook/**'], + env: { + browser: true + }, + rules: { + // Storybook files will not be in published package and are allowed to use devDependencies + 'import/no-extraneous-dependencies': [ + 'error', + { devDependencies: true } + ], + 'import/no-default-export': 'off' + } + } + ] +}; diff --git a/packages/spright-components/.npmignore b/packages/spright-components/.npmignore new file mode 100644 index 00000000000..b758c14e332 --- /dev/null +++ b/packages/spright-components/.npmignore @@ -0,0 +1,9 @@ +* +!/dist/*.* +!/dist/esm/** +/dist/esm/**/tests + +# The storybook build ends up making a duplicate +# typescript build of spright-components inside the dist folder +# See https://github.com/ni/nimble/issues/568 +/dist/esm/spright-components diff --git a/packages/spright-components/.prettierignore b/packages/spright-components/.prettierignore new file mode 100644 index 00000000000..5694e1a195b --- /dev/null +++ b/packages/spright-components/.prettierignore @@ -0,0 +1,29 @@ +# Visit and ignore all objects in the root. +# Then explicitly allow all TypeScript, JavaScript, Markdown, and JSON files in the root, src, and .storybook folders. +/* +!*.ts +!*.js +!*.md +!*.mdx +!*.json +!src/ +!/.storybook/ + +/src/**/*.* +!src/**/*.ts +!src/**/*.js +!src/**/*.md +!src/**/*.mdx +!src/**/*.json + +/.storybook/*.* +!/.storybook/*.js + +# Explicitly exclude the below files and folders +dist +node_modules +package.json +CHANGELOG.* +src/utilities/tests/tests/fixture.spec.ts +src/utilities/tests/fixture.ts +src/utilities/style/direction.ts diff --git a/packages/spright-components/.prettierrc.json b/packages/spright-components/.prettierrc.json new file mode 100644 index 00000000000..d29ec5d8dc1 --- /dev/null +++ b/packages/spright-components/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "tabWidth": 4, + "useTabs": false, + "trailingComma": "none", + "singleQuote": true, + "endOfLine": "auto" +} diff --git a/packages/spright-components/.storybook/main.js b/packages/spright-components/.storybook/main.js new file mode 100644 index 00000000000..36bd1209aa4 --- /dev/null +++ b/packages/spright-components/.storybook/main.js @@ -0,0 +1,52 @@ +import remarkGfm from 'remark-gfm'; +import CircularDependencyPlugin from 'circular-dependency-plugin'; + +// All files participating in storybook should be in src +// so that TypeScript and linters can track them correctly +export const stories = ['../src/**/*.mdx', '../src/**/*.stories.ts']; +export const addons = [ + { + name: '@storybook/addon-essentials', + options: { + outline: false, + docs: false + } + }, + { + name: '@storybook/addon-docs', + options: { + mdxPluginOptions: { + mdxCompileOptions: { + remarkPlugins: [remarkGfm] + } + } + } + }, + '@storybook/addon-a11y', + '@storybook/addon-interactions' +]; +export function webpackFinal(config) { + config.module.rules.push({ + test: /\.ts$/, + use: [ + { + loader: require.resolve('ts-loader') + } + ] + }); + config.plugins.push( + new CircularDependencyPlugin({ + exclude: /node_modules/, + failOnError: process.env.NODE_ENV === 'production' + }) + ); + config.performance = { + hints: false + }; + return config; +} +export const staticDirs = ['public']; +export const framework = { + name: '@storybook/html-webpack5', + options: {} +}; diff --git a/packages/spright-components/.storybook/manager-head.html b/packages/spright-components/.storybook/manager-head.html new file mode 100644 index 00000000000..58f6f2f2f74 --- /dev/null +++ b/packages/spright-components/.storybook/manager-head.html @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/packages/spright-components/.storybook/manager.js b/packages/spright-components/.storybook/manager.js new file mode 100644 index 00000000000..e3348a08d2e --- /dev/null +++ b/packages/spright-components/.storybook/manager.js @@ -0,0 +1,19 @@ +import { addons } from '@storybook/addons'; +import theme from './theme'; + +addons.setConfig({ + enableShortcuts: false, + sidebar: { + filters: { + patterns: item => { + const isPublicSite = window.location.hostname === 'nimble.ni.dev'; + const isItemInternal = item.title.startsWith('Tests/') + || item.title.startsWith('Internal/') + || item.title.startsWith('patterns/'); + const shouldHideInSidebar = isPublicSite && isItemInternal; + return !shouldHideInSidebar; + } + } + }, + theme +}); diff --git a/packages/spright-components/.storybook/preview.css b/packages/spright-components/.storybook/preview.css new file mode 100644 index 00000000000..4947349198e --- /dev/null +++ b/packages/spright-components/.storybook/preview.css @@ -0,0 +1,5 @@ +/* The code-hide-top-container class should only be used +by the storybook.ts createStory helpers */ +.code-hide-top-container { + display: contents; +} diff --git a/packages/spright-components/.storybook/preview.js b/packages/spright-components/.storybook/preview.js new file mode 100644 index 00000000000..f281a3a20ae --- /dev/null +++ b/packages/spright-components/.storybook/preview.js @@ -0,0 +1,50 @@ +import { configureActions } from '@storybook/addon-actions'; +import '@ni/nimble-tokens/dist/fonts/css/fonts.css'; +import './preview.css'; +import { transformSource } from './transformSource'; +import { + backgroundStates, + defaultBackgroundState +} from '../dist/esm/utilities/tests/states'; + +export const parameters = { + backgrounds: { + default: defaultBackgroundState.name, + values: backgroundStates.map(({ name, value }) => ({ name, value })) + }, + options: { + storySort: { + method: 'alphabetical', + // Items within arrays show the sort order for children of the category above + // https://storybook.js.org/docs/react/writing-stories/naming-components-and-hierarchy#sorting-stories + order: [ + 'Getting Started', + 'Components', + ['Status Table'], + 'Incubating', + ['Docs'], + 'Tokens', + ['Docs'], + 'Tests', + ['Docs'], + 'Internal', + ['Docs'] + ] + } + }, + controls: { + expanded: true + }, + docs: { + source: { + transform: transformSource + } + } +}; + +// Storybook's default serialization of events includes the serialized event target. This can +// be quite large, such as in a table with a lot of records. Therefore, the serialization depth +// should be limited to avoid poor performance. +configureActions({ + depth: 1 +}); diff --git a/packages/spright-components/.storybook/public/favicon.ico b/packages/spright-components/.storybook/public/favicon.ico new file mode 100644 index 00000000000..0785a5aefbb Binary files /dev/null and b/packages/spright-components/.storybook/public/favicon.ico differ diff --git a/packages/spright-components/.storybook/spright-ui-logo.svg b/packages/spright-components/.storybook/spright-ui-logo.svg new file mode 100644 index 00000000000..ca3a42f997d --- /dev/null +++ b/packages/spright-components/.storybook/spright-ui-logo.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/packages/spright-components/.storybook/theme.js b/packages/spright-components/.storybook/theme.js new file mode 100644 index 00000000000..d762b40d62a --- /dev/null +++ b/packages/spright-components/.storybook/theme.js @@ -0,0 +1,45 @@ +import { create } from '@storybook/theming/create'; +import { + Black91, + Brand100, + Brand85, + Black75, + White +} from '@ni/nimble-tokens/dist/styledictionary/js/tokens'; +import logo from './spright-ui-logo.svg'; + +export default create({ + base: 'light', + + colorPrimary: Brand100, + colorSecondary: Black75, + + // UI + appBg: 'white', + appContentBg: '#F4F4F4', + appBorderColor: 'grey', + appBorderRadius: 4, + + // Typography + fontBase: '"Open Sans", sans-serif', + fontCode: 'monospace', + + // Text colors + textColor: Black91, + textInverseColor: 'rgba(255,255,255,0.9)', + + // Toolbar default and active colors + barTextColor: White, + barSelectedColor: White, + barBg: Brand85, + + // Form colors + inputBg: 'white', + inputBorder: 'silver', + inputTextColor: 'black', + inputBorderRadius: 4, + + brandTitle: 'Spright components', + brandUrl: 'https://github.com/ni/nimble', + brandImage: logo +}); diff --git a/packages/spright-components/.storybook/transformSource.js b/packages/spright-components/.storybook/transformSource.js new file mode 100644 index 00000000000..3672e109939 --- /dev/null +++ b/packages/spright-components/.storybook/transformSource.js @@ -0,0 +1,90 @@ +import prettier from 'prettier/esm/standalone'; +import parserHTML from 'prettier/esm/parser-html'; + +const createFragmentFromHTML = html => { + const template = document.createElement('template'); + template.innerHTML = html; + const fragment = template.content; + return fragment; +}; + +const createHTMLFromFragment = fragment => { + const dummyElement = document.createElement('div'); + dummyElement.append(fragment); + const html = dummyElement.innerHTML; + return html; +}; + +const removeCodeHideTopContainerNode = node => { + const topContainer = node.firstElementChild; + if (topContainer?.classList.contains('code-hide-top-container')) { + node.removeChild(topContainer); + const children = Array.from(topContainer.children); + children.forEach(child => node.append(child)); + } +}; + +const removeCodeHideNodes = node => { + if (node.hasChildNodes()) { + const nodes = Array.from(node.childNodes); + nodes.forEach(child => removeCodeHideNodes(child)); + } + if (node instanceof HTMLElement && node.classList.contains('code-hide')) { + node.parentNode?.removeChild(node); + } +}; + +const removeCommentNodes = node => { + if (node.hasChildNodes()) { + const nodes = Array.from(node.childNodes); + nodes.forEach(child => removeCommentNodes(child)); + } + if (node.nodeType === Node.COMMENT_NODE) { + node.parentNode?.removeChild(node); + } +}; + +const removeClassAttributes = node => { + if (node.hasChildNodes()) { + const nodes = Array.from(node.childNodes); + nodes.forEach(child => removeClassAttributes(child)); + } + // Assume that all class attributes added to spright elements were added by FAST + // and are not part of the control api + if ( + node instanceof HTMLElement + && node.tagName.toLowerCase().startsWith('spright-') + ) { + node.removeAttribute('class'); + } +}; + +const removeBlankLines = html => html + .split('\n') + .filter(line => line.trim() !== '') + .join('\n'); + +const removeEmptyAttributes = html => html.replaceAll('=""', ''); + +// A custom source transformer. See: +// https://github.com/storybookjs/storybook/blob/next/addons/docs/docs/recipes.md#customizing-source-snippets +export const transformSource = source => { + const fragment = createFragmentFromHTML(source); + // FAST inserts HTML comments for binding insertion points which look + // like that we remove + removeCommentNodes(fragment); + removeCodeHideNodes(fragment); + removeCodeHideTopContainerNode(fragment); + removeClassAttributes(fragment); + const html = createHTMLFromFragment(fragment); + + const trimmedHTML = removeBlankLines(html); + const emptyAttributesRemovedHTML = removeEmptyAttributes(trimmedHTML); + const formmattedHTML = prettier.format(emptyAttributesRemovedHTML, { + parser: 'html', + plugins: [parserHTML], + htmlWhitespaceSensitivity: 'ignore', + tabWidth: 4 + }); + return formmattedHTML; +}; diff --git a/packages/spright-components/CONTRIBUTING.md b/packages/spright-components/CONTRIBUTING.md new file mode 100644 index 00000000000..1aa53b5d992 --- /dev/null +++ b/packages/spright-components/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# Contributing to Spright Components + +Spright components are not subject to the same rules and rigor as Nimble components, but when in doubt, refer to the [`nimble-components` contributing doc](../nimble-components/CONTRIBUTING.md) for guidance. diff --git a/packages/spright-components/README.md b/packages/spright-components/README.md new file mode 100644 index 00000000000..135e174c9a0 --- /dev/null +++ b/packages/spright-components/README.md @@ -0,0 +1,9 @@ +
+

ni | spright

+
+ +# Spright Components + +[![NPM Version](https://img.shields.io/npm/v/@ni/spright-components.svg)](https://www.npmjs.com/package/@ni/spright-components) + +Experimental, composite, or product-specific, NI-styled web components for web applications. diff --git a/packages/spright-components/karma.conf.js b/packages/spright-components/karma.conf.js new file mode 100644 index 00000000000..2c3a56dcbdb --- /dev/null +++ b/packages/spright-components/karma.conf.js @@ -0,0 +1,151 @@ +// Based on fast-components configuration: +// https://github.com/microsoft/fast/blob/6549309c1ed2dea838561d23fea6337ef16d7908/packages/web-components/fast-components/karma.conf.js +// Coverage from the fast configuration removed due to lack of Webpack 5 support: +// https://github.com/webpack-contrib/istanbul-instrumenter-loader/issues/110 + +const playwright = require('playwright'); + +process.env.WEBKIT_HEADLESS_BIN = playwright.webkit.executablePath(); +process.env.WEBKIT_BIN = playwright.webkit.executablePath(); +process.env.FIREFOX_BIN = playwright.firefox.executablePath(); +process.env.CHROME_BIN = playwright.chromium.executablePath(); + +const path = require('path'); +const webpack = require('webpack'); + +const basePath = path.resolve(__dirname); +const commonChromeFlags = [ + '--no-default-browser-check', + '--no-first-run', + '--no-sandbox', + '--no-managed-user-acknowledgment-check', + '--disable-background-timer-throttling', + '--disable-backing-store-limit', + '--disable-boot-animation', + '--disable-cloud-import', + '--disable-contextual-search', + '--disable-default-apps', + '--disable-extensions', + '--disable-infobars', + '--disable-translate', + '--force-prefers-reduced-motion', + '--lang=en-US' +]; + +// Create a webpack environment plugin to use while running tests so that +// functionality that accesses the environment, such as the TanStack table +// within the nimble-table, work correctly. +// Note: Unless we run the tests twice, we have to choose to either run them +// against the 'production' configuration or the 'development' configuration. +// Because we expect shipping apps to use the 'production' configuration, we +// have chosen to run tests aginst that configuration. +const webpackEnvironmentPlugin = new webpack.EnvironmentPlugin({ + NODE_ENV: 'production' +}); + +module.exports = config => { + const options = { + basePath, + browserDisconnectTimeout: 10000, + processKillTimeout: 10000, + frameworks: [ + 'source-map-support', + 'jasmine', + 'webpack', + 'jasmine-spec-tags' + ], + plugins: [ + 'karma-jasmine', + 'karma-jasmine-html-reporter', + 'karma-jasmine-spec-tags', + 'karma-webpack', + 'karma-source-map-support', + 'karma-sourcemap-loader', + 'karma-chrome-launcher', + 'karma-firefox-launcher', + 'karma-webkit-launcher' + ], + files: ['dist/esm/utilities/tests/setup.js'], + preprocessors: { + 'dist/esm/utilities/tests/setup.js': ['webpack', 'sourcemap'] + }, + webpackMiddleware: { + // webpack-dev-middleware configuration + // i. e. + stats: 'errors-only' + }, + webpack: { + mode: 'none', + resolve: { + extensions: ['.js'], + modules: ['dist', 'node_modules'], + mainFields: ['module', 'main'] + }, + devtool: 'inline-source-map', + performance: { + hints: false + }, + optimization: { + nodeEnv: false, + usedExports: true, + flagIncludedChunks: false, + sideEffects: true, + concatenateModules: true, + splitChunks: { + name: false + }, + runtimeChunk: false, + checkWasmTypes: false, + minimize: false + }, + module: { + rules: [ + { + test: /\.js\.map$/, + use: ['ignore-loader'] + }, + { + test: /\.js$/, + enforce: 'pre', + use: [ + { + loader: 'source-map-loader' + } + ] + } + ] + }, + plugins: [webpackEnvironmentPlugin] + }, + mime: { + 'text/x-typescript': ['ts'] + }, + reporters: ['kjhtml'], + browsers: ['ChromeHeadlessOpt'], + customLaunchers: { + ChromeDebugging: { + base: 'Chrome', + flags: [...commonChromeFlags, '--remote-debugging-port=9333'], + debug: true + }, + ChromeHeadlessOpt: { + base: 'ChromeHeadless', + flags: [...commonChromeFlags] + }, + FirefoxDebugging: { + base: 'Firefox', + debug: true + }, + WebkitDebugging: { + base: 'Webkit', + debug: true + } + }, + client: { + captureConsole: true + }, + logLevel: config.LOG_ERROR // to disable the WARN 404 for image requests + }; + + config.set(options); +}; diff --git a/packages/spright-components/karma.conf.verbose.js b/packages/spright-components/karma.conf.verbose.js new file mode 100644 index 00000000000..c147a108bd6 --- /dev/null +++ b/packages/spright-components/karma.conf.verbose.js @@ -0,0 +1,15 @@ +/** + * A karma config file that adds verbose reporting to the base configuration + */ + +const originalConfigFunction = require('./karma.conf'); + +module.exports = config => { + originalConfigFunction(config); + const options = { + plugins: [...config.plugins, 'karma-spec-reporter'], + reporters: [...config.reporters, 'spec'] + }; + + config.set(options); +}; diff --git a/packages/spright-components/package.json b/packages/spright-components/package.json new file mode 100644 index 00000000000..cf5daff780b --- /dev/null +++ b/packages/spright-components/package.json @@ -0,0 +1,115 @@ +{ + "name": "@ni/spright-components", + "version": "0.0.1", + "description": "NI Spright Components", + "scripts": { + "build": "npm run build-components && npm run bundle-components && npm run build-storybook", + "lint": "npm run eslint && npm run prettier", + "format": "npm run eslint-fix && npm run prettier-fix", + "eslint": "eslint .", + "eslint-fix": "eslint src --fix", + "prettier": "prettier-eslint \"**/*.*\" --list-different --prettier-ignore", + "prettier-fix": "prettier-eslint \"**/*.*\" --write --prettier-ignore", + "pack": "npm pack", + "invoke-publish": "npm publish", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build -o dist/storybook --webpack-stats-json", + "storybook-open-webkit": "playwright wk http://localhost:6006", + "build-components": "tsc -p ./tsconfig.json", + "build-components:watch": "tsc -p ./tsconfig.json -w", + "bundle-components": "rollup --bundleConfigAsCjs --config", + "tdd": "npm run build-components && npm run test-chrome", + "tdd:watch": "npm run build-components:watch & npm run test-chrome:watch", + "tdd-firefox": "npm run build-components && npm run test-firefox", + "tdd-firefox:watch": "npm run build-components:watch & npm run test-firefox:watch", + "tdd-webkit": "npm run build-components && npm run test-webkit", + "tdd-webkit:watch": "npm run build-components:watch & npm run test-webkit:watch", + "test-chrome:debugger": "karma start karma.conf.js --browsers=ChromeDebugging --skip-tags SkipChrome", + "test-chrome:verbose": "karma start karma.conf.verbose.js --browsers=ChromeHeadlessOpt --single-run --skip-tags SkipChrome", + "test-chrome:watch": "karma start karma.conf.js --browsers=ChromeHeadlessOpt --skip-tags SkipChrome --watch-extensions js", + "test-chrome": "karma start karma.conf.js --browsers=ChromeHeadlessOpt --single-run --skip-tags SkipChrome", + "test-firefox:debugger": "karma start karma.conf.js --browsers=FirefoxDebugging --skip-tags SkipFirefox", + "test-firefox:verbose": "karma start karma.conf.verbose.js --browsers=FirefoxHeadless --single-run --skip-tags SkipFirefox", + "test-firefox:watch": "karma start karma.conf.js --browsers=FirefoxHeadless --skip-tags SkipFirefox --watch-extensions js", + "test-firefox": "karma start karma.conf.js --browsers=FirefoxHeadless --single-run --skip-tags SkipFirefox", + "test-webkit:debugger": "karma start karma.conf.js --browsers=WebkitDebugging --skip-tags SkipWebkit", + "test-webkit:verbose": "karma start karma.conf.verbose.js --browsers=WebkitHeadless --single-run --skip-tags SkipWebkit", + "test-webkit:watch": "karma start karma.conf.js --browsers=WebkitHeadless --skip-tags SkipWebkit --watch-extensions js", + "test-webkit": "karma start karma.conf.js --browsers=WebkitHeadless --single-run --skip-tags SkipWebkit", + "test": "npm run test-chrome:verbose && npm run test-firefox:verbose" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ni/nimble.git" + }, + "publishConfig": { + "access": "public" + }, + "author": { + "name": "National Instruments" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/ni/nimble/issues" + }, + "homepage": "https://github.com/ni/nimble#readme", + "dependencies": { + "@microsoft/fast-element": "*", + "@microsoft/fast-foundation": "*", + "@microsoft/fast-web-utilities": "*", + "@ni/nimble-components": "*", + "@ni/nimble-tokens": "*", + "tslib": "^2.2.0" + }, + "devDependencies": { + "@babel/cli": "^7.13.16", + "@babel/core": "^7.20.12", + "@ni/eslint-config-javascript": "^4.2.0", + "@ni/eslint-config-typescript": "^4.2.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", + "@types/jasmine": "^4.3.1", + "@types/webpack-env": "^1.15.2", + "babel-loader": "^9.1.2", + "circular-dependency-plugin": "^5.2.0", + "css-loader": "^6.7.3", + "dotenv-webpack": "^8.0.1", + "eslint-plugin-jsdoc": "^46.8.2", + "eslint-plugin-storybook": "^0.6.13", + "html-webpack-plugin": "^5.3.1", + "jasmine-core": "^4.5.0", + "karma": "^6.3.0", + "karma-chrome-launcher": "^3.1.0", + "karma-firefox-launcher": "^2.1.0", + "karma-jasmine": "^5.1.0", + "karma-jasmine-html-reporter": "^2.0.0", + "karma-jasmine-spec-tags": "^2.0.0", + "karma-source-map-support": "^1.4.0", + "karma-sourcemap-loader": "^0.3.7", + "karma-spec-reporter": "^0.0.36", + "karma-webkit-launcher": "^2.1.0", + "karma-webpack": "^5.0.0", + "playwright": "^1.30.0", + "prettier": "^2.8.8", + "prettier-eslint": "^15.0.1", + "prettier-eslint-cli": "^7.1.0", + "remark-gfm": "^3.0.1", + "source-map-loader": "^4.0.0", + "storybook": "^7.4.0", + "ts-loader": "^9.2.5", + "typescript": "~4.8.2", + "webpack": "^5.75.0", + "webpack-cli": "^5.0.1", + "webpack-dev-middleware": "^6.0.1" + } +} diff --git a/packages/spright-components/rollup.config.js b/packages/spright-components/rollup.config.js new file mode 100644 index 00000000000..787b48dd43c --- /dev/null +++ b/packages/spright-components/rollup.config.js @@ -0,0 +1,85 @@ +import commonJS from '@rollup/plugin-commonjs'; +import resolve from '@rollup/plugin-node-resolve'; +import sourcemaps from 'rollup-plugin-sourcemaps'; +import terser from '@rollup/plugin-terser'; +import replace from '@rollup/plugin-replace'; +import json from '@rollup/plugin-json'; +import nodePolyfills from 'rollup-plugin-polyfill-node'; + +const umdDevelopmentPlugin = () => replace({ + 'process.env.NODE_ENV': JSON.stringify('development'), + preventAssignment: true +}); + +const umdProductionPlugin = () => replace({ + 'process.env.NODE_ENV': JSON.stringify('production'), + preventAssignment: true +}); + +// d3 has circular dependencies it won't remove: +// https://github.com/d3/d3-selection/issues/168 +// So ignore just d3's circular dependencies following the pattern from: +// https://github.com/rollup/rollup/issues/1089#issuecomment-635564942 +// Updated to use current onwarn api: +// https://rollupjs.org/configuration-options/#onwarn +const onwarn = (warning, defaultHandler) => { + const ignoredWarnings = [ + { + code: 'CIRCULAR_DEPENDENCY', + file: 'node_modules/d3-' + } + ]; + + if ( + !ignoredWarnings.some( + ({ code, file }) => warning.code === code && warning.message.includes(file) + ) + ) { + defaultHandler(warning); + } +}; + +// eslint-disable-next-line import/no-default-export +export default [ + { + input: 'dist/esm/all-components.js', + output: { + file: 'dist/all-components-bundle.js', + format: 'iife', + sourcemap: true + }, + plugins: [ + umdDevelopmentPlugin(), + nodePolyfills(), + sourcemaps(), + resolve(), + commonJS(), + json() + ], + onwarn + }, + { + input: 'dist/esm/all-components.js', + output: { + file: 'dist/all-components-bundle.min.js', + format: 'iife', + sourcemap: true, + plugins: [ + terser({ + output: { + semicolons: false + } + }) + ] + }, + plugins: [ + umdProductionPlugin(), + nodePolyfills(), + sourcemaps(), + resolve(), + commonJS(), + json() + ], + onwarn + } +]; diff --git a/packages/spright-components/src/.eslintrc.js b/packages/spright-components/src/.eslintrc.js new file mode 100644 index 00000000000..e7be5485734 --- /dev/null +++ b/packages/spright-components/src/.eslintrc.js @@ -0,0 +1,145 @@ +const restrictedImportsPaths = () => ([ + { + name: '@microsoft/fast-foundation', + importNames: ['focusVisible'], + message: + 'Please use focusVisible from src/utilities/style/focus instead.' + }, + { + name: '@microsoft/fast-components', + message: + 'It is not expected to leverage @microsoft/fast-components directly as they are coupled to FAST design tokens.' + } +]); + +module.exports = { + root: true, + plugins: ['jsdoc'], + extends: [ + '@ni/eslint-config-typescript', + '@ni/eslint-config-typescript/requiring-type-checking' + ], + parserOptions: { + project: '../tsconfig.json', + tsconfigRootDir: __dirname + }, + rules: { + // Require non-empty JSDoc comment on class declarations + 'jsdoc/require-jsdoc': [ + 'error', + { + publicOnly: false, + require: { + ClassDeclaration: true, + FunctionDeclaration: false + } + } + ], + 'jsdoc/require-description': [ + 'error', + { contexts: ['ClassDeclaration'] } + ], + + 'no-restricted-syntax': ['error', { + selector: 'TSEnumDeclaration', + message: 'Use a const object instead of an enum. See other types.ts files for examples.' + }], + + // Rules enabled due to strictNullChecks + '@typescript-eslint/no-non-null-assertion': 'off', + + // Improves readability of templates to avoid return types in template expressions + '@typescript-eslint/explicit-function-return-type': [ + 'error', + { allowExpressions: true } + ], + + 'no-restricted-imports': [ + 'error', + { + paths: restrictedImportsPaths() + } + ], + + // Enabled to prevent accidental usage of async-await + '@typescript-eslint/require-await': 'error' + }, + ignorePatterns: ['.eslintrc.js'], + overrides: [ + { + files: ['*.stories.ts'], + extends: ['plugin:storybook/recommended'], + rules: { + // Storybook files will not be in published package and are allowed to use devDependencies + 'import/no-extraneous-dependencies': [ + 'error', + { devDependencies: true } + ], + 'import/no-default-export': 'off', + 'storybook/prefer-pascal-case': 'off' + } + }, + { + files: ['*.spec.ts'], + env: { + jasmine: true + }, + rules: { + // Classes defined in test code aren't part of the public API so don't need docs + 'jsdoc/require-jsdoc': 'off', + 'jsdoc/require-description': 'off', + 'no-restricted-imports': [ + 'error', + { + paths: [ + ...restrictedImportsPaths(), + { + name: '@microsoft/fast-element', + importNames: ['DOM'], + message: 'For tests, please use functions from src/testing/async-helpers instead.' + } + ] + } + ], + } + }, + { + files: ['styles.ts'], + rules: { + // Prettier and eslint conflict in how they format CSS in styles files and we prefer prettier's output + '@typescript-eslint/indent': 'off' + } + }, + { + // Instead of enums, this repo uses const objects and type unions which should live in types.ts + files: ['types.ts'], + rules: { + // The const object and type union should have the same name + '@typescript-eslint/no-redeclare': 'off', + // Enum-like variables should use PascalCase and values should use camelCase + // Also allow camelCase variables for things like attribute strings + '@typescript-eslint/naming-convention': [ + 'error', + { + selector: 'objectLiteralProperty', + format: ['camelCase'], + }, + { + selector: 'variable', + format: ['camelCase', 'PascalCase'], + }, + { + selector: 'typeLike', + format: ['PascalCase'], + }, + { + selector: 'default', + format: ['camelCase'], + leadingUnderscore: 'allow', + trailingUnderscore: 'allow', + }, + ], + } + } + ] +}; diff --git a/packages/spright-components/src/accordion/index.ts b/packages/spright-components/src/accordion/index.ts new file mode 100644 index 00000000000..a328f8dd889 --- /dev/null +++ b/packages/spright-components/src/accordion/index.ts @@ -0,0 +1,27 @@ +import { + DesignSystem, + Accordion as FoundationAccordion +} from '@microsoft/fast-foundation'; +import { styles } from './styles'; +import { template } from './template'; + +declare global { + interface HTMLElementTagNameMap { + 'spright-accordion': Accordion; + } +} + +/** + * A nimble-styled accordion + */ +export class Accordion extends FoundationAccordion {} + +const sprightAccordion = Accordion.compose({ + baseName: 'accordion', + baseClass: FoundationAccordion, + template, + styles +}); + +DesignSystem.getOrCreate().withPrefix('spright').register(sprightAccordion()); +export const accordionTag = 'spright-accordion'; diff --git a/packages/spright-components/src/accordion/styles.ts b/packages/spright-components/src/accordion/styles.ts new file mode 100644 index 00000000000..4d9b01d4267 --- /dev/null +++ b/packages/spright-components/src/accordion/styles.ts @@ -0,0 +1,35 @@ +import { css } from '@microsoft/fast-element'; +import { display } from '@microsoft/fast-foundation'; +import { + bodyFont, + bodyFontColor, + borderColor, + borderWidth, + sectionBackgroundColor, + standardPadding, + titleFont, + titleFontColor +} from '@ni/nimble-components/dist/esm/theme-provider/design-tokens'; + +export const styles = css` + ${display('flex')} + + :host { + flex-direction: column; + gap: ${standardPadding}; + padding: ${standardPadding}; + border: ${borderWidth} solid ${borderColor}; + font: ${bodyFont}; + color: ${bodyFontColor}; + background-color: ${sectionBackgroundColor}; + } + + section { + display: contents; + } + + slot[name='title'] { + font: ${titleFont}; + color: ${titleFontColor}; + } +`; diff --git a/packages/spright-components/src/accordion/template.ts b/packages/spright-components/src/accordion/template.ts new file mode 100644 index 00000000000..373ebc2b8bc --- /dev/null +++ b/packages/spright-components/src/accordion/template.ts @@ -0,0 +1,12 @@ +import { html } from '@microsoft/fast-element'; +import type { Accordion } from '.'; + +export const template = html` + ${ + '' /* Explicitly set role to work around Lighthouse error. See https://github.com/ni/nimble/issues/1650. */ +} +
+ + +
+`; diff --git a/packages/spright-components/src/accordion/tests/accordion-matrix.stories.ts b/packages/spright-components/src/accordion/tests/accordion-matrix.stories.ts new file mode 100644 index 00000000000..e6b29379164 --- /dev/null +++ b/packages/spright-components/src/accordion/tests/accordion-matrix.stories.ts @@ -0,0 +1,47 @@ +import { ViewTemplate, html } from '@microsoft/fast-element'; +import type { Meta, StoryFn } from '@storybook/html'; +import { listOptionTag } from '@ni/nimble-components/dist/esm/list-option'; +import { numberFieldTag } from '@ni/nimble-components/dist/esm/number-field'; +import { selectTag } from '@ni/nimble-components/dist/esm/select'; +import { + createMatrix, + sharedMatrixParameters +} from '../../utilities/tests/matrix'; +import { + createMatrixThemeStory, + createStory +} from '../../utilities/tests/storybook'; +import { hiddenWrapper } from '../../utilities/tests/hidden'; +import { accordionTag } from '..'; + +const metadata: Meta = { + title: 'Tests/Accordion', + parameters: { + ...sharedMatrixParameters() + } +}; + +export default metadata; + +const component = (): ViewTemplate => html` + <${accordionTag}> + Title + <${numberFieldTag}>Numeric field 1 + <${numberFieldTag}>Numeric field 2 + <${selectTag}> + <${listOptionTag} value="1">Option 1 + <${listOptionTag} value="2">Option 2 + <${listOptionTag} value="3">Option 3 + + +`; + +export const accordionThemeMatrix: StoryFn = createMatrixThemeStory( + createMatrix(component) +); + +export const hiddenAccordion: StoryFn = createStory( + hiddenWrapper( + html`<${accordionTag} hidden>Hidden Accordion` + ) +); diff --git a/packages/spright-components/src/accordion/tests/accordion.spec.ts b/packages/spright-components/src/accordion/tests/accordion.spec.ts new file mode 100644 index 00000000000..fd00f5475b0 --- /dev/null +++ b/packages/spright-components/src/accordion/tests/accordion.spec.ts @@ -0,0 +1,13 @@ +import { Accordion, accordionTag } from '..'; + +describe('Accordion', () => { + it('should export its tag', () => { + expect(accordionTag).toBe('spright-accordion'); + }); + + it('can construct an element instance', () => { + expect(document.createElement('spright-accordion')).toBeInstanceOf( + Accordion + ); + }); +}); diff --git a/packages/spright-components/src/accordion/tests/accordion.stories.ts b/packages/spright-components/src/accordion/tests/accordion.stories.ts new file mode 100644 index 00000000000..e5aac3189f4 --- /dev/null +++ b/packages/spright-components/src/accordion/tests/accordion.stories.ts @@ -0,0 +1,55 @@ +import { html } from '@microsoft/fast-element'; +import type { Meta, StoryObj } from '@storybook/html'; +import { listOptionTag } from '@ni/nimble-components/dist/esm/list-option'; +import { numberFieldTag } from '@ni/nimble-components/dist/esm//number-field'; +import { selectTag } from '@ni/nimble-components/dist/esm//select'; +import { + createUserSelectedThemeStory, + disableStorybookZoomTransform +} from '../../utilities/tests/storybook'; +import { accordionTag } from '..'; + +interface AccordionArgs { + title: string; +} + +const overviewText = 'The `spright-accordion` is a collapsible container which groups and contains arbitrary content or controls.'; + +const metadata: Meta = { + title: 'Components/Accordion', + tags: ['autodocs'], + parameters: { + docs: { + description: { + component: overviewText + } + }, + actions: {} + }, + render: createUserSelectedThemeStory(html` + ${disableStorybookZoomTransform} + <${accordionTag}> + ${x => x.title} + <${numberFieldTag}>Numeric field 1 + <${numberFieldTag}>Numeric field 2 + <${selectTag}> + <${listOptionTag} value="1">Option 1 + <${listOptionTag} value="2">Option 2 + <${listOptionTag} value="3">Option 3 + + + `), + argTypes: { + title: { + description: + 'Text displayed as a title inside the accordion. Accordions should **always include a title**. The title is used to provide an accessible name to assistive technologies.

Provide the title in an element targeted to the `title` slot.' + } + }, + args: { + title: 'Title text' + } +}; + +export default metadata; + +export const accordion: StoryObj = {}; diff --git a/packages/spright-components/src/all-components.ts b/packages/spright-components/src/all-components.ts new file mode 100644 index 00000000000..be6c673d32e --- /dev/null +++ b/packages/spright-components/src/all-components.ts @@ -0,0 +1,9 @@ +/** + * Import of all the web components available in Spright. + * Production applications are encouraged to import only components + * that are required instead of leveraging this file. + */ + +import './accordion'; + +import '@ni/nimble-components/dist/esm/all-components'; diff --git a/packages/spright-components/src/utilities/tests/hidden.ts b/packages/spright-components/src/utilities/tests/hidden.ts new file mode 100644 index 00000000000..1e6b5aa58d1 --- /dev/null +++ b/packages/spright-components/src/utilities/tests/hidden.ts @@ -0,0 +1,12 @@ +import { html, ViewTemplate } from '@microsoft/fast-element'; + +/** + * Wraps a given component template with a border and a message indicating + * that the component is expected to be hidden. + */ +export const hiddenWrapper = (template: ViewTemplate): ViewTemplate => { + return html` + Intentionally blank + ${template} + `; +}; diff --git a/packages/spright-components/src/utilities/tests/matrix.ts b/packages/spright-components/src/utilities/tests/matrix.ts new file mode 100644 index 00000000000..639fcef25d7 --- /dev/null +++ b/packages/spright-components/src/utilities/tests/matrix.ts @@ -0,0 +1,225 @@ +import { html, repeat, ViewTemplate } from '@microsoft/fast-element'; + +export const sharedMatrixParameters = () => ({ + controls: { + hideNoControlsWarning: true + }, + a11y: { disabled: true }, + docs: { + source: { + code: null + }, + transformSource: (source: string): string => source + }, + backgrounds: { + disable: true, + grid: { + disable: true + } + }, + viewMode: 'story', + previewTabs: { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'storybook/docs/panel': { + hidden: true + } + } +} as const); + +/** + * Takes an array of state values that can be used with the template to match the permutations of the provided states. + */ +export function createMatrix(component: () => ViewTemplate): ViewTemplate; + +export function createMatrix( + component: (state1: State1) => ViewTemplate, + dimensions: readonly [readonly State1[]] +): ViewTemplate; + +export function createMatrix( + component: (state1: State1, state2: State2) => ViewTemplate, + dimensions: readonly [readonly State1[], readonly State2[]] +): ViewTemplate; + +export function createMatrix( + component: (state1: State1, state2: State2, state3: State3) => ViewTemplate, + dimensions: readonly [ + readonly State1[], + readonly State2[], + readonly State3[] + ] +): ViewTemplate; + +export function createMatrix( + component: ( + state1: State1, + state2: State2, + state3: State3, + state4: State4 + ) => ViewTemplate, + dimensions: readonly [ + readonly State1[], + readonly State2[], + readonly State3[], + readonly State4[] + ] +): ViewTemplate; + +export function createMatrix( + component: ( + state1: State1, + state2: State2, + state3: State3, + state4: State4, + state5: State5 + ) => ViewTemplate, + dimensions: readonly [ + readonly State1[], + readonly State2[], + readonly State3[], + readonly State4[], + readonly State5[] + ] +): ViewTemplate; + +export function createMatrix( + component: ( + state1: State1, + state2: State2, + state3: State3, + state4: State4, + state5: State5, + state6: State6 + ) => ViewTemplate, + dimensions: readonly [ + readonly State1[], + readonly State2[], + readonly State3[], + readonly State4[], + readonly State5[], + readonly State6[] + ] +): ViewTemplate; + +export function createMatrix< + State1, + State2, + State3, + State4, + State5, + State6, + State7 +>( + component: ( + state1: State1, + state2: State2, + state3: State3, + state4: State4, + state5: State5, + state6: State6, + state7: State7 + ) => ViewTemplate, + dimensions: readonly [ + readonly State1[], + readonly State2[], + readonly State3[], + readonly State4[], + readonly State5[], + readonly State6[], + readonly State7[] + ] +): ViewTemplate; + +export function createMatrix< + State1, + State2, + State3, + State4, + State5, + State6, + State7, + State8 +>( + component: ( + state1: State1, + state2: State2, + state3: State3, + state4: State4, + state5: State5, + state6: State6, + state7: State7, + state8: State8 + ) => ViewTemplate, + dimensions: readonly [ + readonly State1[], + readonly State2[], + readonly State3[], + readonly State4[], + readonly State5[], + readonly State6[], + readonly State7[], + readonly State8[] + ] +): ViewTemplate; + +export function createMatrix< + State1, + State2, + State3, + State4, + State5, + State6, + State7, + State8, + State9 +>( + component: ( + state1: State1, + state2: State2, + state3: State3, + state4: State4, + state5: State5, + state6: State6, + state7: State7, + state8: State8, + state9: State9 + ) => ViewTemplate, + dimensions: readonly [ + readonly State1[], + readonly State2[], + readonly State3[], + readonly State4[], + readonly State5[], + readonly State6[], + readonly State7[], + readonly State8[], + readonly State9[] + ] +): ViewTemplate; + +export function createMatrix( + component: (...states: readonly unknown[]) => ViewTemplate, + dimensions?: readonly (readonly unknown[])[] +): ViewTemplate { + const matrix: ViewTemplate[] = []; + const recurseDimensions = ( + currentDimensions?: readonly (readonly unknown[])[], + ...states: readonly unknown[] + ): void => { + if (currentDimensions && currentDimensions.length >= 1) { + const [currentDimension, ...remainingDimensions] = currentDimensions; + for (const currentState of currentDimension!) { + recurseDimensions(remainingDimensions, ...states, currentState); + } + } else { + matrix.push(component(...states)); + } + }; + recurseDimensions(dimensions); + // prettier-ignore + return html` + ${repeat(() => matrix, html` + ${(x: ViewTemplate): ViewTemplate => x} + `)} + `; +} diff --git a/packages/spright-components/src/utilities/tests/setup.ts b/packages/spright-components/src/utilities/tests/setup.ts new file mode 100644 index 00000000000..048884826fe --- /dev/null +++ b/packages/spright-components/src/utilities/tests/setup.ts @@ -0,0 +1,8 @@ +// Import all the test files to run in browser + +function importAll(r: __WebpackModuleApi.RequireContext): void { + r.keys().forEach(r); +} + +// Explicitly add to browser test +importAll(require.context('../../', true, /\.spec\.js$/)); diff --git a/packages/spright-components/src/utilities/tests/states.ts b/packages/spright-components/src/utilities/tests/states.ts new file mode 100644 index 00000000000..3bd9997635a --- /dev/null +++ b/packages/spright-components/src/utilities/tests/states.ts @@ -0,0 +1,43 @@ +import { Theme } from '@ni/nimble-components/dist/esm//theme-provider/types'; + +export const backgroundStates = [ + { + name: `"${Theme.light}" theme on white`, + value: '#F4F4F4', + theme: Theme.light + }, + { + name: `"${Theme.color}" theme on dark green`, + value: '#044123', + theme: Theme.color + }, + { + name: `"${Theme.dark}" theme on black`, + value: '#252526', + theme: Theme.dark + } +] as const; +export const [defaultBackgroundState] = backgroundStates; +export type BackgroundState = (typeof backgroundStates)[number]; + +export const disabledStates = [ + ['', false], + ['Disabled', true] +] as const; +export type DisabledState = (typeof disabledStates)[number]; + +export const errorStates = [ + ['', false, ''], + ['Error Message', true, 'This is not valid.'], + ['Error No Message', true, ''] +] as const; +export type ErrorState = (typeof errorStates)[number]; + +export const readOnlyStates = [ + ['', false], + ['Read-Only', true] +] as const; +export type ReadOnlyState = (typeof readOnlyStates)[number]; + +export const iconVisibleStates = [false, true] as const; +export type IconVisibleState = (typeof iconVisibleStates)[number]; diff --git a/packages/spright-components/src/utilities/tests/storybook.ts b/packages/spright-components/src/utilities/tests/storybook.ts new file mode 100644 index 00000000000..4b28e73e93f --- /dev/null +++ b/packages/spright-components/src/utilities/tests/storybook.ts @@ -0,0 +1,180 @@ +import { html, ViewTemplate } from '@microsoft/fast-element'; +import { themeProviderTag } from '@ni/nimble-components/dist/esm/theme-provider'; +import { bodyFont } from '@ni/nimble-components/dist/esm/theme-provider/design-tokens'; +import type { Theme } from '@ni/nimble-components/dist/esm/theme-provider/types'; +import { createMatrix } from './matrix'; +import { + BackgroundState, + backgroundStates, + defaultBackgroundState +} from './states'; + +/** + * Renders a ViewTemplate as elements in a DocumentFragment. + * Bindings, such as event binding, will be active. + */ +const renderViewTemplate = ( + viewTemplate: ViewTemplate, + source: TSource +): DocumentFragment => { + const template = document.createElement('template'); + const fragment = template.content; + viewTemplate.render(source, fragment); + return fragment; +}; + +/** + * Renders a FAST `html` template as a story. + */ +export const createStory = ( + viewTemplate: ViewTemplate +): ((source: TSource) => Element) => { + return (source: TSource): Element => { + const wrappedViewTemplate = html` +
${viewTemplate}
+ `; + const fragment = renderViewTemplate(wrappedViewTemplate, source); + const content = fragment.firstElementChild!; + return content; + }; +}; + +const getGlobalTheme = (context: unknown): Theme => { + type GlobalValue = string | undefined; + // @ts-expect-error Accessing the global background defined in preview.js + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const globalValue = context?.globals?.backgrounds?.value as GlobalValue; + const background = backgroundStates.find(({ value }) => value === globalValue) + ?? defaultBackgroundState; + return background.theme; +}; + +/** + * Renders a FAST `html` template as a story that responds to the + * background theme selection in Storybook. + */ +export const createUserSelectedThemeStory = ( + viewTemplate: ViewTemplate +): ((source: TSource, context: unknown) => Element) => { + return (source: TSource, context: unknown): Element => { + const wrappedViewTemplate = html` + <${themeProviderTag} + theme="${getGlobalTheme(context)}" + class="code-hide-top-container" + > + ${viewTemplate} + + `; + const fragment = renderViewTemplate(wrappedViewTemplate, source); + const content = fragment.firstElementChild!; + return content; + }; +}; + +/** + * Renders a FAST `html` template with a a specific theme. + * Useful when the template can't be tested multiple times in a single story + * and instead the test is broken up across multiple stories. + */ +export const createFixedThemeStory = ( + viewTemplate: ViewTemplate, + backgroundState: BackgroundState +): ((source: TSource) => Element) => { + return (source: TSource): Element => { + const wrappedViewTemplate = html` + <${themeProviderTag} + theme="${backgroundState.theme}" + class="code-hide-top-container" + > + +
+ ${viewTemplate} +
+ + `; + const fragment = renderViewTemplate(wrappedViewTemplate, source); + const content = fragment.firstElementChild!; + return content; + }; +}; + +/** + * Renders a FAST `html` template for each theme. + */ +export const createMatrixThemeStory = ( + viewTemplate: ViewTemplate +): ((source: TSource) => Element) => { + return (source: TSource): Element => { + const matrixTemplate = createMatrix( + ({ theme, value }: BackgroundState) => html` + <${themeProviderTag} + theme="${theme}"> +
+ ${viewTemplate} +
+ + `, + [backgroundStates] + ); + const wrappedMatrixTemplate = html` +
${matrixTemplate}
+ `; + const fragment = renderViewTemplate(wrappedMatrixTemplate, source); + const content = fragment.firstElementChild!; + return content; + }; +}; + +export const overrideWarning = ( + propertySummaryName: string, + howToOverride: string +): string => ` +
+Overriding ${propertySummaryName} Values +Overrides of properties are not recommended and are not theme-aware by default. If a needed value is not available, you should create an issue to discuss with the Nimble squad. + +${howToOverride} +
`; + +export interface IncubatingWarningConfig { + componentName: string; + statusLink: string; +} + +export const incubatingWarning = (config: IncubatingWarningConfig): string => ` + +
+WARNING - The ${config.componentName} is still incubating. It is not recommended for application use. +See the incubating component status. +
`; + +// On the Docs page, there is a div with a scale(1) transform that causes the dropdown to be +// confined to the div. We remove the transform to allow the dropdown to escape the div, but +// that also breaks zooming behavior, so we remove the zoom buttons on the docs page. +export const disableStorybookZoomTransform = ` + +`; diff --git a/packages/spright-components/tsconfig.json b/packages/spright-components/tsconfig.json new file mode 100644 index 00000000000..0b06829426f --- /dev/null +++ b/packages/spright-components/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "rootDir": "src", + "declaration": true, + "esModuleInterop": true, + "moduleResolution": "node", + "outDir": "dist/esm", + "strict": true, + "experimentalDecorators": true, + "target": "ES2020", + "module": "ES2020", + "allowJs": true, + "importHelpers": true, + "resolveJsonModule": true, + "sourceMap": true, + "inlineSources": true, + "lib": ["DOM", "ES2015", "ES2016.Array.Include", "ES2017.Object"], + // Whitelist imported @types instead of using them all + "types": ["jasmine", "webpack-env"], + // Require type only imports where possible + // so imports with side-effects are explicit + "importsNotUsedAsValues": "error", + "noImplicitOverride": true, + "noUncheckedIndexedAccess": true, + "noPropertyAccessFromIndexSignature": false + }, + "include": ["src"] +}