diff --git a/change/@ni-nimble-blazor-f343ecb0-c88b-4d86-ae5d-5a25ef104809.json b/change/@ni-nimble-blazor-f343ecb0-c88b-4d86-ae5d-5a25ef104809.json
new file mode 100644
index 0000000000..55def647ff
--- /dev/null
+++ b/change/@ni-nimble-blazor-f343ecb0-c88b-4d86-ae5d-5a25ef104809.json
@@ -0,0 +1,7 @@
+{
+ "type": "minor",
+ "comment": "Blazor integration for switch, text area, toggle button, icons. Fix 2-way binding for checkbox.",
+ "packageName": "@ni/nimble-blazor",
+ "email": "20709258+msmithNI@users.noreply.github.com",
+ "dependentChangeType": "patch"
+}
diff --git a/package-lock.json b/package-lock.json
index c587b63399..667377a673 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -41452,8 +41452,10 @@
"@ni/eslint-config-javascript": "^3.1.0",
"@ni/nimble-components": "*",
"@ni/nimble-tokens": "*",
+ "@rollup/plugin-node-resolve": "^13.1.3",
"cross-env": "^7.0.3",
- "glob": "^7.2.0"
+ "glob": "^7.2.0",
+ "rollup": "^2.61.1"
}
},
"packages/nimble-blazor/node_modules/glob": {
@@ -45164,8 +45166,10 @@
"@ni/eslint-config-javascript": "^3.1.0",
"@ni/nimble-components": "*",
"@ni/nimble-tokens": "*",
+ "@rollup/plugin-node-resolve": "^13.1.3",
"cross-env": "^7.0.3",
- "glob": "^7.2.0"
+ "glob": "^7.2.0",
+ "rollup": "^2.61.1"
},
"dependencies": {
"glob": {
diff --git a/packages/nimble-blazor/.eslintrc.js b/packages/nimble-blazor/.eslintrc.js
index 5282d15e86..f14de2782c 100644
--- a/packages/nimble-blazor/.eslintrc.js
+++ b/packages/nimble-blazor/.eslintrc.js
@@ -6,14 +6,17 @@ module.exports = {
overrides: [
{
files: [
- 'build/copyNimbleResources.js'
+ 'build/**/*.js'
],
rules: {
// Okay to use dev dependencies in build scripts
'import/no-extraneous-dependencies': 'off',
// Okay to use console.log in build scripts
- 'no-console': 'off'
+ 'no-console': 'off',
+
+ // Rollup config files use default exports
+ 'import/no-default-export': 'off'
}
}
]
diff --git a/packages/nimble-blazor/.gitignore b/packages/nimble-blazor/.gitignore
index a8dc543ada..28488c7536 100644
--- a/packages/nimble-blazor/.gitignore
+++ b/packages/nimble-blazor/.gitignore
@@ -1,5 +1,6 @@
# Folders
NimbleBlazor.Components/wwwroot/nimble-*/
+NimbleBlazor.Components/Components/Icons/
artifacts/
bin/
obj/
diff --git a/packages/nimble-blazor/CONTRIBUTING.md b/packages/nimble-blazor/CONTRIBUTING.md
index 41a22be10c..1530160cf2 100644
--- a/packages/nimble-blazor/CONTRIBUTING.md
+++ b/packages/nimble-blazor/CONTRIBUTING.md
@@ -1,5 +1,16 @@
# Contributing to Nimble Blazor
+## Getting Started (Windows)
+
+For Nimble Blazor development on Windows, the suggested tools to install are:
+- Visual Studio 2022 (Enterprise, if available): Choose the "ASP.NET and Web Development" Workload in the installer
+- (Optional) Enable IIS (see "Enabling IIS", below)
+- ASP.NET Core Runtime 6.0.x (6.0.3 or higher): Choose "Hosting Bundle" under ASP.NET Core Runtime, on the [.NET 6.0 Download Page](https://dotnet.microsoft.com/en-us/download/dotnet/6.0)
+
+Initialize and build the Nimble monorepo (`npm install` + `npm run build` from the root `nimble` directory) before working with the solution in Visual Studio.
+
+In Visual Studio, run either the `NimbleBlazor.Demo.Server` or `NimbleBlazor.Demo.Projects` to see the Blazor demo apps.
+
## Creating a Blazor wrapper for a Nimble element
In Nimble Blazor, the Nimble web components are wrapped as [Razor Components](https://docs.microsoft.com/en-us/aspnet/core/blazor/?view=aspnetcore-6.0#components) that consist of a `.razor` template file and a corresponding `.razor.cs` C# implementation file.
@@ -37,4 +48,20 @@ public partial class NimbleButton : ComponentBase
Testing the Nimble Blazor components is possible through the use of xUnit and bUnit. Each Nimble Blazor component should have a corresponding test file.
-Each Nimble Blazor component should also be showcased in the `NimbleBlazor.Demo` example projects. Simple component examples can be added directly in the `ComponentsDemo.razor` file (in the `NimbleBlazor.Demo.Shared` project).
\ No newline at end of file
+Each Nimble Blazor component should also be showcased in the `NimbleBlazor.Demo` example projects. Simple component examples can be added directly in the `ComponentsDemo.razor` file (in the `NimbleBlazor.Demo.Shared` project).
+
+## Additional Tips
+
+### Enabling IIS
+
+Click Start, open "Turn Windows features on or off", and configure "Web Management Tools" and "World Wide Web Services" in the following way:
+
+### Running published output
+
+The commandline build will create a published distribution of the Blazor client example app, which can also be tested via IIS:
+- Open Internet Information Services (IIS) Manager
+- In the left pane, right click "Sites" and click "Add Website..."
+- Pick a site name
+- Under "Physical Path", click [...] and browse to your `nimble-blazor\dist\blazor-client-app` directory
+- Under "Binding", pick a port other than 80 (such as 8080), then click "OK"
+- Open http://localhost:8080 (or whatever port you chose)
\ No newline at end of file
diff --git a/packages/nimble-blazor/Examples/NimbleBlazor.Demo.Shared/Pages/ComponentsDemo.razor b/packages/nimble-blazor/Examples/NimbleBlazor.Demo.Shared/Pages/ComponentsDemo.razor
index 5ccbc7fe4d..97cc45ee8b 100644
--- a/packages/nimble-blazor/Examples/NimbleBlazor.Demo.Shared/Pages/ComponentsDemo.razor
+++ b/packages/nimble-blazor/Examples/NimbleBlazor.Demo.Shared/Pages/ComponentsDemo.razor
@@ -16,19 +16,37 @@
Block Button
Ghost Button
+
+
Buttons - Toggle
+
Outline Toggle Button
+
Block Toggle Button
+
Ghost Toggle Button
+
+
+ Icon Toggle Button
+
+
Checkbox
Checkbox label
Checkbox label
Checkbox label
+
Menu
Item 1
- [+]
+
Item 2
@@ -44,6 +62,19 @@
Option 3
+
+
Switch
+
Switch
+
+ Switch with checked/unchecked messages
+ Off
+ On
+
+
+
+
Text Area
+
Text Area Label
+
Text Field
Text Field Label
diff --git a/packages/nimble-blazor/Examples/NimbleBlazor.Demo.Shared/Pages/ComponentsDemo.razor.cs b/packages/nimble-blazor/Examples/NimbleBlazor.Demo.Shared/Pages/ComponentsDemo.razor.cs
index 34c378922a..1ec8b7a44a 100644
--- a/packages/nimble-blazor/Examples/NimbleBlazor.Demo.Shared/Pages/ComponentsDemo.razor.cs
+++ b/packages/nimble-blazor/Examples/NimbleBlazor.Demo.Shared/Pages/ComponentsDemo.razor.cs
@@ -1,9 +1,9 @@
namespace NimbleBlazor.Demo.Shared.Pages
{
///
- /// The CustomApp Demo.
+ /// The components demo page
///
- public partial class CustomApp
+ public partial class ComponentsDemo
{
}
}
diff --git a/packages/nimble-blazor/Examples/NimbleBlazor.Demo.Shared/Pages/ComponentsDemo.razor.css b/packages/nimble-blazor/Examples/NimbleBlazor.Demo.Shared/Pages/ComponentsDemo.razor.css
index d1ee6746ad..2892b75b94 100644
--- a/packages/nimble-blazor/Examples/NimbleBlazor.Demo.Shared/Pages/ComponentsDemo.razor.css
+++ b/packages/nimble-blazor/Examples/NimbleBlazor.Demo.Shared/Pages/ComponentsDemo.razor.css
@@ -1,4 +1,8 @@
-.container-label {
+.root {
+ background-color: var(--ni-nimble-application-background-color);
+}
+
+.container-label {
font: var(--ni-nimble-group-header-font);
color: var(--ni-nimble-group-header-font-color);
padding-bottom: var(--ni-nimble-standard-padding);
diff --git a/packages/nimble-blazor/Examples/NimbleBlazor.Demo.Shared/Shared/MainLayout.razor b/packages/nimble-blazor/Examples/NimbleBlazor.Demo.Shared/Shared/MainLayout.razor
index e75f731c6a..01ddfa8532 100644
--- a/packages/nimble-blazor/Examples/NimbleBlazor.Demo.Shared/Shared/MainLayout.razor
+++ b/packages/nimble-blazor/Examples/NimbleBlazor.Demo.Shared/Shared/MainLayout.razor
@@ -17,4 +17,4 @@
->
+
diff --git a/packages/nimble-blazor/NimbleBlazor.Components/Components/NimbleCheckbox.razor b/packages/nimble-blazor/NimbleBlazor.Components/Components/NimbleCheckbox.razor
index af6d96dd1b..e059dd46c6 100644
--- a/packages/nimble-blazor/NimbleBlazor.Components/Components/NimbleCheckbox.razor
+++ b/packages/nimble-blazor/NimbleBlazor.Components/Components/NimbleCheckbox.razor
@@ -3,6 +3,7 @@
CurrentValue = __value.Checked"
+ indeterminate="@Indeterminate"
required="@Required"
readonly="@ReadOnly"
@attributes="AdditionalAttributes">
diff --git a/packages/nimble-blazor/NimbleBlazor.Components/Components/NimbleCheckbox.razor.cs b/packages/nimble-blazor/NimbleBlazor.Components/Components/NimbleCheckbox.razor.cs
index 6d626a9851..4b35d85ad9 100644
--- a/packages/nimble-blazor/NimbleBlazor.Components/Components/NimbleCheckbox.razor.cs
+++ b/packages/nimble-blazor/NimbleBlazor.Components/Components/NimbleCheckbox.razor.cs
@@ -11,6 +11,9 @@ public partial class NimbleCheckbox : NimbleInputBase
[Parameter]
public bool? Required { get; set; }
+ [Parameter]
+ public bool? Indeterminate { get; set; }
+
[Parameter]
public bool? ReadOnly { get; set; }
diff --git a/packages/nimble-blazor/NimbleBlazor.Components/Components/NimbleSwitch.razor b/packages/nimble-blazor/NimbleBlazor.Components/Components/NimbleSwitch.razor
new file mode 100644
index 0000000000..f5d08462a5
--- /dev/null
+++ b/packages/nimble-blazor/NimbleBlazor.Components/Components/NimbleSwitch.razor
@@ -0,0 +1,10 @@
+@namespace NimbleBlazor.Components
+@inherits NimbleInputBase
+ CurrentValue = __value.Checked"
+ required="@Required"
+ readonly="@ReadOnly"
+ @attributes="AdditionalAttributes">
+ @ChildContent
+
\ No newline at end of file
diff --git a/packages/nimble-blazor/NimbleBlazor.Components/Components/NimbleSwitch.razor.cs b/packages/nimble-blazor/NimbleBlazor.Components/Components/NimbleSwitch.razor.cs
new file mode 100644
index 0000000000..8bc6a0a139
--- /dev/null
+++ b/packages/nimble-blazor/NimbleBlazor.Components/Components/NimbleSwitch.razor.cs
@@ -0,0 +1,21 @@
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.AspNetCore.Components;
+
+namespace NimbleBlazor.Components;
+
+public partial class NimbleSwitch : NimbleInputBase
+{
+ [Parameter]
+ public bool? Disabled { get; set; }
+
+ [Parameter]
+ public bool? Required { get; set; }
+
+ [Parameter]
+ public bool? ReadOnly { get; set; }
+
+ [Parameter]
+ public RenderFragment? ChildContent { get; set; }
+
+ protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out bool result, [NotNullWhen(false)] out string? validationErrorMessage) => throw new NotSupportedException($"This component does not parse string inputs. Bind to the '{nameof(CurrentValue)}' property, not '{nameof(CurrentValueAsString)}'.");
+}
diff --git a/packages/nimble-blazor/NimbleBlazor.Components/Components/NimbleTextArea.razor b/packages/nimble-blazor/NimbleBlazor.Components/Components/NimbleTextArea.razor
new file mode 100644
index 0000000000..a8e8147f14
--- /dev/null
+++ b/packages/nimble-blazor/NimbleBlazor.Components/Components/NimbleTextArea.razor
@@ -0,0 +1,20 @@
+@namespace NimbleBlazor.Components
+@inherits NimbleInputBase
+(this, __value => CurrentValueAsString = __value, CurrentValueAsString))"
+ size="@Size"
+ minlength="@MinLength"
+ maxlength="@MaxLength"
+ resize="@TextAreaResize.ToAttributeValue()"
+ rows="@Rows"
+ cols="@Cols"
+ spellcheck="@Spellcheck"
+ readonly="@ReadOnly"
+ disabled="@Disabled"
+ required="@Required"
+ placeholder="@Placeholder"
+ @attributes="AdditionalAttributes">
+ @ChildContent
+
\ No newline at end of file
diff --git a/packages/nimble-blazor/NimbleBlazor.Components/Components/NimbleTextArea.razor.cs b/packages/nimble-blazor/NimbleBlazor.Components/Components/NimbleTextArea.razor.cs
new file mode 100644
index 0000000000..3564142067
--- /dev/null
+++ b/packages/nimble-blazor/NimbleBlazor.Components/Components/NimbleTextArea.razor.cs
@@ -0,0 +1,56 @@
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.AspNetCore.Components;
+
+namespace NimbleBlazor.Components;
+
+public partial class NimbleTextArea : NimbleInputBase
+{
+ [Parameter]
+ public bool? Disabled { get; set; }
+
+ [Parameter]
+ public bool? ReadOnly { get; set; }
+
+ [Parameter]
+ public bool? Required { get; set; }
+
+ [Parameter]
+ public bool? AutoFocus { get; set; }
+
+ [Parameter]
+ public int? Size { get; set; }
+
+ [Parameter]
+ public Appearance? Appearance { get; set; }
+
+ [Parameter]
+ public TextAreaResize? TextAreaResize { get; set; }
+
+ [Parameter]
+ public string? Placeholder { get; set; }
+
+ [Parameter]
+ public int? MinLength { get; set; }
+
+ [Parameter]
+ public int? MaxLength { get; set; }
+
+ [Parameter]
+ public int? Rows { get; set; }
+
+ [Parameter]
+ public int? Cols { get; set; }
+
+ [Parameter]
+ public bool? Spellcheck { get; set; }
+
+ [Parameter]
+ public RenderFragment? ChildContent { get; set; }
+
+ protected override bool TryParseValueFromString(string? value, out string? result, [NotNullWhen(false)] out string? validationErrorMessage)
+ {
+ result = value;
+ validationErrorMessage = null;
+ return true;
+ }
+}
diff --git a/packages/nimble-blazor/NimbleBlazor.Components/Components/NimbleToggleButton.razor b/packages/nimble-blazor/NimbleBlazor.Components/Components/NimbleToggleButton.razor
new file mode 100644
index 0000000000..4ebd0ab9db
--- /dev/null
+++ b/packages/nimble-blazor/NimbleBlazor.Components/Components/NimbleToggleButton.razor
@@ -0,0 +1,12 @@
+@namespace NimbleBlazor.Components
+@inherits NimbleInputBase
+ CurrentValue = __value.Checked"
+ appearance="@Appearance.ToAttributeValue()"
+ disabled="@Disabled"
+ autofocus="@AutoFocus"
+ content-hidden="@ContentHidden"
+ @attributes="AdditionalAttributes">
+ @ChildContent
+
\ No newline at end of file
diff --git a/packages/nimble-blazor/NimbleBlazor.Components/Components/NimbleToggleButton.razor.cs b/packages/nimble-blazor/NimbleBlazor.Components/Components/NimbleToggleButton.razor.cs
new file mode 100644
index 0000000000..3a7a534a89
--- /dev/null
+++ b/packages/nimble-blazor/NimbleBlazor.Components/Components/NimbleToggleButton.razor.cs
@@ -0,0 +1,24 @@
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.AspNetCore.Components;
+
+namespace NimbleBlazor.Components;
+
+public partial class NimbleToggleButton : NimbleInputBase
+{
+ [Parameter]
+ public Appearance? Appearance { get; set; }
+
+ [Parameter]
+ public bool? Disabled { get; set; }
+
+ [Parameter]
+ public bool? ContentHidden { get; set; }
+
+ [Parameter]
+ public bool? AutoFocus { get; set; }
+
+ [Parameter]
+ public RenderFragment? ChildContent { get; set; }
+
+ protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out bool result, [NotNullWhen(false)] out string? validationErrorMessage) => throw new NotSupportedException($"This component does not parse string inputs. Bind to the '{nameof(CurrentValue)}' property, not '{nameof(CurrentValueAsString)}'.");
+}
diff --git a/packages/nimble-blazor/NimbleBlazor.Components/NimbleIconBase.cs b/packages/nimble-blazor/NimbleBlazor.Components/NimbleIconBase.cs
new file mode 100644
index 0000000000..a9e00def48
--- /dev/null
+++ b/packages/nimble-blazor/NimbleBlazor.Components/NimbleIconBase.cs
@@ -0,0 +1,14 @@
+using Microsoft.AspNetCore.Components;
+
+namespace NimbleBlazor.Components;
+
+///
+/// Base class for Nimble icons.
+///
+public abstract class NimbleIconBase : ComponentBase
+{
+ ///
+ /// Gets or sets a collection of additional attributes that will be applied to the created element.
+ ///
+ [Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary? AdditionalAttributes { get; set; }
+}
diff --git a/packages/nimble-blazor/NimbleBlazor.Components/TextAreaResize.cs b/packages/nimble-blazor/NimbleBlazor.Components/TextAreaResize.cs
new file mode 100644
index 0000000000..38c2d22c7a
--- /dev/null
+++ b/packages/nimble-blazor/NimbleBlazor.Components/TextAreaResize.cs
@@ -0,0 +1,16 @@
+namespace NimbleBlazor.Components;
+
+public enum TextAreaResize
+{
+ None,
+ Both,
+ Horizontal,
+ Vertical
+}
+
+internal static class TextAreaResizeExtensions
+{
+ private static readonly Dictionary _textAreaResizeValues = AttributeHelpers.GetEnumNamesAsKebabCaseValues();
+
+ public static string? ToAttributeValue(this TextAreaResize? value) => value == null ? null : _textAreaResizeValues[value.Value];
+}
diff --git a/packages/nimble-blazor/NimbleBlazor.Components/wwwroot/NimbleBlazor.Components.lib.module.js b/packages/nimble-blazor/NimbleBlazor.Components/wwwroot/NimbleBlazor.Components.lib.module.js
new file mode 100644
index 0000000000..5bcd66fac6
--- /dev/null
+++ b/packages/nimble-blazor/NimbleBlazor.Components/wwwroot/NimbleBlazor.Components.lib.module.js
@@ -0,0 +1,19 @@
+/**
+ * Register the custom event type used for the change events for NimbleCheckbox/NimbleSwitch/NimbleToggleButton.
+ * Necessary because the control's value property is always just the value 'on', so we need to look
+ * at the checked property to correctly get the value.
+ * @see NimbleCheckbox.razor, NimbleSwitch.razor, NimbleToggleButton.razor
+ *
+ * JavaScript initializer for NimbleBlazor.Components project, see
+ * https://docs.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/?view=aspnetcore-6.0#javascript-initializers
+ */
+export function afterStarted(Blazor) {
+ Blazor.registerCustomEventType('nimblecheckedchange', {
+ browserEventName: 'change',
+ createEventArgs: event => {
+ return {
+ checked: event.target.currentChecked
+ };
+ }
+ });
+}
\ No newline at end of file
diff --git a/packages/nimble-blazor/NimbleBlazor.Components/wwwroot/Readme.md b/packages/nimble-blazor/NimbleBlazor.Components/wwwroot/Readme.md
deleted file mode 100644
index 36ba014af1..0000000000
--- a/packages/nimble-blazor/NimbleBlazor.Components/wwwroot/Readme.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Nimble Blazor
-
-Find out more at https://github.com/ni/nimble
diff --git a/packages/nimble-blazor/Tests.NimbleBlazor/Unit/Components/NimbleSwitchTests.cs b/packages/nimble-blazor/Tests.NimbleBlazor/Unit/Components/NimbleSwitchTests.cs
new file mode 100644
index 0000000000..5a6b56506b
--- /dev/null
+++ b/packages/nimble-blazor/Tests.NimbleBlazor/Unit/Components/NimbleSwitchTests.cs
@@ -0,0 +1,23 @@
+using Bunit;
+using NimbleBlazor.Components;
+using Xunit;
+
+namespace NimbleBlazor.Tests.Unit.Components;
+
+///
+/// Tests for
+///
+public class NimbleSwitchTests
+{
+ [Fact]
+ public void NimbleSwitch_Rendered_HasSwitchMarkup()
+ {
+ var context = new TestContext();
+ context.JSInterop.Mode = JSRuntimeMode.Loose;
+ var expectedMarkup = "nimble-switch";
+
+ var switchComponent = context.RenderComponent();
+
+ Assert.Contains(expectedMarkup, switchComponent.Markup);
+ }
+}
diff --git a/packages/nimble-blazor/Tests.NimbleBlazor/Unit/Components/NimbleTextAreaTests.cs b/packages/nimble-blazor/Tests.NimbleBlazor/Unit/Components/NimbleTextAreaTests.cs
new file mode 100644
index 0000000000..3b42a12fab
--- /dev/null
+++ b/packages/nimble-blazor/Tests.NimbleBlazor/Unit/Components/NimbleTextAreaTests.cs
@@ -0,0 +1,59 @@
+using Bunit;
+using NimbleBlazor.Components;
+using Xunit;
+
+namespace NimbleBlazor.Tests.Unit.Components;
+
+///
+/// Tests for
+///
+public class NimbleTextAreaTests
+{
+ [Fact]
+ public void NimbleTextArea_Rendered_HasTextAreaMarkup()
+ {
+ var context = new TestContext();
+ context.JSInterop.Mode = JSRuntimeMode.Loose;
+ var expectedMarkup = "nimble-text-area";
+
+ var textField = context.RenderComponent();
+
+ Assert.Contains(expectedMarkup, textField.Markup);
+ }
+
+ [Theory]
+ [InlineData(TextAreaResize.None, "none")]
+ [InlineData(TextAreaResize.Both, "both")]
+ [InlineData(TextAreaResize.Horizontal, "horizontal")]
+ [InlineData(TextAreaResize.Vertical, "vertical")]
+ public void TextAreaTextAreaResize_AttributeIsSet(TextAreaResize value, string expectedAttribute)
+ {
+ var textArea = RenderNimbleTextArea(value);
+
+ Assert.Contains(expectedAttribute, textArea.Markup);
+ }
+
+ [Theory]
+ [InlineData(Appearance.Block, "block")]
+ [InlineData(Appearance.Outline, "outline")]
+ public void TextAreaAppearance_AttributeIsSet(Appearance value, string expectedAttribute)
+ {
+ var textArea = RenderNimbleTextArea(value);
+
+ Assert.Contains(expectedAttribute, textArea.Markup);
+ }
+
+ private IRenderedComponent RenderNimbleTextArea(TextAreaResize textAreaResize)
+ {
+ var context = new TestContext();
+ context.JSInterop.Mode = JSRuntimeMode.Loose;
+ return context.RenderComponent(p => p.Add(x => x.TextAreaResize, textAreaResize));
+ }
+
+ private IRenderedComponent RenderNimbleTextArea(Appearance appearance)
+ {
+ var context = new TestContext();
+ context.JSInterop.Mode = JSRuntimeMode.Loose;
+ return context.RenderComponent(p => p.Add(x => x.Appearance, appearance));
+ }
+}
diff --git a/packages/nimble-blazor/Tests.NimbleBlazor/Unit/Components/NimbleToggleButtonTests.cs b/packages/nimble-blazor/Tests.NimbleBlazor/Unit/Components/NimbleToggleButtonTests.cs
new file mode 100644
index 0000000000..8ef7e0cc31
--- /dev/null
+++ b/packages/nimble-blazor/Tests.NimbleBlazor/Unit/Components/NimbleToggleButtonTests.cs
@@ -0,0 +1,41 @@
+using Bunit;
+using NimbleBlazor.Components;
+using Xunit;
+
+namespace NimbleBlazor.Tests.Unit.Components;
+
+///
+/// Tests for
+///
+public class NimbleToggleButtonTests
+{
+ [Fact]
+ public void NimbleToggleButton_Rendered_HasButtonMarkup()
+ {
+ var context = new TestContext();
+ context.JSInterop.Mode = JSRuntimeMode.Loose;
+ var expectedMarkup = "nimble-toggle-button";
+
+ var button = context.RenderComponent();
+
+ Assert.Contains(expectedMarkup, button.Markup);
+ }
+
+ [Theory]
+ [InlineData(Appearance.Block, "block")]
+ [InlineData(Appearance.Underline, "underline")]
+ [InlineData(Appearance.Ghost, "ghost")]
+ public void ButtonAppearance_AttributeIsSet(Appearance value, string expectedAttribute)
+ {
+ var button = RenderNimbleToggleButton(value);
+
+ Assert.Contains(expectedAttribute, button.Markup);
+ }
+
+ private IRenderedComponent RenderNimbleToggleButton(Appearance appearance)
+ {
+ var context = new TestContext();
+ context.JSInterop.Mode = JSRuntimeMode.Loose;
+ return context.RenderComponent(p => p.Add(x => x.Appearance, appearance));
+ }
+}
diff --git a/packages/nimble-blazor/build/generate-icons/README.md b/packages/nimble-blazor/build/generate-icons/README.md
new file mode 100644
index 0000000000..1b59b885de
--- /dev/null
+++ b/packages/nimble-blazor/build/generate-icons/README.md
@@ -0,0 +1,15 @@
+# Generate Icons
+
+## Behavior
+
+- Depends on the build output of `nimble-tokens` to generate icon Angular integrations.
+- Generates a Razor component file for each icon.
+
+## How to run
+
+This script runs as part of the Nimble Blazor build.
+
+To run manually:
+
+1. Run a Nimble Blazor build.
+2. Edit `index.js` for this script and run `npm run generate-icons` (can re-run when modifying `index.js` behavior).
diff --git a/packages/nimble-blazor/build/generate-icons/rollup.config.js b/packages/nimble-blazor/build/generate-icons/rollup.config.js
new file mode 100644
index 0000000000..89a182bd41
--- /dev/null
+++ b/packages/nimble-blazor/build/generate-icons/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/index.js'),
+ output: {
+ file: path.resolve(__dirname, 'dist/index.js'),
+ format: 'cjs'
+ },
+ plugins: [nodeResolve()]
+};
diff --git a/packages/nimble-blazor/build/generate-icons/source/index.js b/packages/nimble-blazor/build/generate-icons/source/index.js
new file mode 100644
index 0000000000..f091a7cc07
--- /dev/null
+++ b/packages/nimble-blazor/build/generate-icons/source/index.js
@@ -0,0 +1,60 @@
+/**
+ * Build script for generating nimble-blazor integration for Nimble icons.
+ *
+ * Iterates through icons provided by nimble-tokens, and generates a Razor component
+ * file for each.
+ */
+
+import * as icons from '@ni/nimble-tokens/dist-icons-esm/nimble-icons-inline';
+
+const fs = require('fs');
+const path = require('path');
+
+const trimSizeFromName = text => {
+ // Remove dimensions from icon name, e.g. "add16X16" -> "add"
+ return text.replace(/\d+X\d+$/, '');
+};
+
+const camelToPascalCase = text => {
+ return text.substring(0, 1).toUpperCase() + text.substring(1);
+};
+
+const camelToKebabCase = text => {
+ // Adapted from https://stackoverflow.com/a/67243723
+ return text.replace(/[A-Z]+(?![a-z])|[A-Z]/g, (substring, offset) => (offset !== 0 ? '-' : '') + substring.toLowerCase());
+};
+
+const generatedFilePrefix = `@* AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY
+ // See generation source in nimble-blazor/build/generate-icons *@\n`;
+
+const packageDirectory = path.resolve(__dirname, '../../../');
+const iconsDirectory = path.resolve(packageDirectory, 'NimbleBlazor.Components/Components/Icons');
+console.log(iconsDirectory);
+
+if (fs.existsSync(iconsDirectory)) {
+ console.log(`Deleting existing icons directory "${iconsDirectory}"`);
+ fs.rmSync(iconsDirectory, { recursive: true });
+ console.log('Finished deleting existing icons directory');
+}
+console.log(`Creating icons directory "${iconsDirectory}"`);
+fs.mkdirSync(iconsDirectory);
+console.log('Finished creating icons directory');
+
+console.log('Writing icon Razor component files');
+for (const key of Object.keys(icons)) {
+ const iconName = trimSizeFromName(key); // "arrowExpanderLeft"
+ const elementName = `nimble-${camelToKebabCase(iconName)}-icon`; // e.g. "nimble-arrow-expander-left-icon"
+ const className = `${camelToPascalCase(iconName)}Icon`; // e.g. "ArrowExpanderLeftIcon"
+ const componentName = `Nimble${className}`; // e.g. "NimbleArrowExpanderLeftIcon"
+
+ const directiveFileContents = `${generatedFilePrefix}
+@namespace NimbleBlazor.Components
+@inherits NimbleIconBase
+<${elementName} @attributes="AdditionalAttributes">
+${elementName}>
+ `;
+ const componentFileName = `${componentName}.razor`;
+ const componentFilePath = path.resolve(iconsDirectory, componentFileName);
+ fs.writeFileSync(componentFilePath, directiveFileContents, { encoding: 'utf-8' });
+}
+console.log('Finshed writing icon Razor component files');
diff --git a/packages/nimble-blazor/docs/WindowsFeatures-IIS.jpg b/packages/nimble-blazor/docs/WindowsFeatures-IIS.jpg
new file mode 100644
index 0000000000..2515f8bdaa
Binary files /dev/null and b/packages/nimble-blazor/docs/WindowsFeatures-IIS.jpg differ
diff --git a/packages/nimble-blazor/package.json b/packages/nimble-blazor/package.json
index 06b6f5b4e1..1982743dcd 100644
--- a/packages/nimble-blazor/package.json
+++ b/packages/nimble-blazor/package.json
@@ -3,9 +3,12 @@
"version": "1.0.6",
"description": "Blazor components for the NI Nimble Design System",
"scripts": {
- "build": "npm run build:all && npm run build:client",
- "build:all": "dotnet build -c Release /p:TreatWarningsAsErrors=true /warnaserror",
+ "build": "npm run generate-icons && 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/NimbleBlazor.Demo.Client --output dist/blazor-client-app",
+ "generate-icons": "npm run generate-icons:bundle && npm run generate-icons:run",
+ "generate-icons:bundle": "rollup --config build/generate-icons/rollup.config.js",
+ "generate-icons:run": "node build/generate-icons/dist/index.js",
"lint": "npm run lint:cs && npm run lint:js",
"lint:cs": "dotnet format --verify-no-changes",
"lint:js": "eslint .",
@@ -36,6 +39,8 @@
"@ni/eslint-config-javascript": "^3.1.0",
"@ni/nimble-components": "*",
"@ni/nimble-tokens": "*",
+ "@rollup/plugin-node-resolve": "^13.1.3",
+ "rollup": "^2.61.1",
"cross-env": "^7.0.3",
"glob": "^7.2.0"
}