Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions PanoramicData.Blazor.Demo/Pages/PDFormDocumentation.razor
Original file line number Diff line number Diff line change
Expand Up @@ -214,13 +214,28 @@
<section id="colours">
<h2 class="doc-section">Colours</h2>
<p>
The frame around each field's editor comes from a CSS variable, so a host can match it to its own control
borders on <code>:root</code> or on a theme class, for example on a dark theme.
The form's frames, validation state and helper icons take their colours from CSS variables, so a host can
match them to its own theme on <code>:root</code> or on a theme class. Each default is the colour the form
drew before. PDForm 6 in the demo turns groups of them on and off.
</p>
<table class="table table-sm doc-props-table">
<thead><tr><th>Variable</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>--pd-form-editor-border-color</code></td><td><code>#ced4da</code></td><td>The border around a field's editor, to the right of its label</td></tr>
<tr><td><code>--pd-form-editor-border-color</code></td><td><code>#ced4da</code>, and Bootstrap's border colour under its dark mode</td><td>The border around a field's editor, to the right of its label</td></tr>
<tr><td><code>--pd-form-title-error-bg</code>, <code>--pd-form-title-error-color</code></td><td>Bootstrap's danger alert colours</td><td>The label of a field with errors, whether still waiting for a value or holding a wrong one</td></tr>
<tr><td><code>--pd-form-title-edge-width</code></td><td><code>0</code></td><td>The width of an edge down the left of such a label; none unless set</td></tr>
<tr><td><code>--pd-form-title-required-edge-color</code></td><td><code>transparent</code></td><td>The edge while a field is still waiting for a value (label class <code>pd-form-title-required</code>)</td></tr>
<tr><td><code>--pd-form-title-invalid-edge-color</code></td><td><code>transparent</code></td><td>The edge once a field holds a wrong value (label class <code>pd-form-title-invalid</code>)</td></tr>
<tr><td><code>--pd-form-status-valid-bg</code>, <code>--pd-form-status-valid-color</code></td><td>Bootstrap's success alert colours</td><td>The validation indicator at the end of a valid field (class <code>pd-form-status</code>)</td></tr>
<tr><td><code>--pd-form-status-required-bg</code>, <code>--pd-form-status-required-color</code></td><td>Bootstrap's warning alert colours</td><td>The indicator, an asterisk, while a field is still waiting for a value</td></tr>
<tr><td><code>--pd-form-status-invalid-bg</code>, <code>--pd-form-status-invalid-color</code></td><td>Bootstrap's danger alert colours</td><td>The indicator once a field holds a wrong value</td></tr>
<tr><td><code>--pd-form-helper-color</code></td><td>Bootstrap's info colour</td><td>The help link, helper and copy icons beside a label, and the header's help icon</td></tr>
<tr><td><code>--pd-form-error-color</code></td><td>Bootstrap's danger colour</td><td>The message under a field with a wrong value</td></tr>
<tr><td><code>--pd-form-footer-error-color</code>, <code>--pd-form-footer-error-icon-color</code></td><td>Bootstrap's danger colour</td><td>The footer's error count and its icon</td></tr>
<tr><td><code>--pd-form-footer-error-bg</code></td><td><code>transparent</code></td><td>Behind the footer's error count</td></tr>
<tr><td><code>--pd-form-footer-error-border-width</code>, <code>--pd-form-footer-error-border-color</code></td><td><code>0 0 1px 0</code>, the text colour</td><td>Its border, a line underneath by default</td></tr>
<tr><td><code>--pd-form-footer-error-edge-color</code></td><td>the border colour</td><td>Its left border, for a coloured edge</td></tr>
<tr><td><code>--pd-form-footer-error-padding</code>, <code>--pd-form-footer-error-radius</code></td><td><code>0.25rem 0</code>, <code>0</code></td><td>Its padding and corners</td></tr>
</tbody>
</table>
</section>
Expand Down
91 changes: 91 additions & 0 deletions PanoramicData.Blazor.Demo/Pages/PDFormPage6.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
@page "/pdform6"

<h1>PDForm</h1>

<DemoSourceView SourceFiles="Pages/PDFormPage6.razor, Pages/PDFormPage6.razor.cs, Pages/PDFormPage6.razor.css">

<p>
The form's validation and helper colours come from <code>--pd-form-*</code> CSS variables. Each switch below sets
one group of them on the box around the form. With every switch off the form uses Bootstrap's alert and info
colours, as it always has.
</p>

<div class="d-flex flex-wrap align-items-center gap-3 mb-3">
<div class="form-check form-switch">
<input id="fc-dark" class="form-check-input" type="checkbox" @bind="_dark" />
<label class="form-check-label" for="fc-dark">Dark</label>
</div>
<div class="form-check form-switch">
<input id="fc-labels" class="form-check-input" type="checkbox" @bind="_neutralLabels" />
<label class="form-check-label" for="fc-labels">Neutral labels</label>
</div>
<div class="form-check form-switch">
<input id="fc-edge" class="form-check-input" type="checkbox" @bind="_labelEdges" />
<label class="form-check-label" for="fc-edge">Label edges</label>
</div>
<div class="form-check form-switch">
<input id="fc-status" class="form-check-input" type="checkbox" @bind="_quietStatus" />
<label class="form-check-label" for="fc-status">Quiet status column</label>
</div>
<div class="form-check form-switch">
<input id="fc-helpers" class="form-check-input" type="checkbox" @bind="_mutedHelpers" />
<label class="form-check-label" for="fc-helpers">Muted helper icons</label>
</div>
<div class="form-check form-switch">
<input id="fc-footer" class="form-check-input" type="checkbox" @bind="_footerBox" />
<label class="form-check-label" for="fc-footer">Footer as a box</label>
</div>
<button type="button" class="btn btn-sm btn-secondary" @onclick="AllOn">All on</button>
<button type="button" class="btn btn-sm btn-secondary" @onclick="AllOff">All off</button>
<button type="button" class="btn btn-sm btn-secondary" @onclick="ResetFormAsync">New form</button>
</div>

<div class="form-colours-demo bg-body text-body border rounded p-3 @WrapperCssClass" data-bs-theme="@(_dark ? "dark" : "light")">
<PDForm @ref="_form"
CssClass="w-100"
DataProvider="_dataProvider"
TItem="FormColoursModel">

<PDFormHeader TItem="FormColoursModel" CreateTitle="Create new schedule" />

<PDFormBody TItem="FormColoursModel" TitleWidth="160">

<PDField TItem="FormColoursModel"
Description="A name to find the schedule by."
Field="x => x.Name" />

<PDField TItem="FormColoursModel"
Description="What the schedule produces."
Field="x => x.Description" />

<PDField TItem="FormColoursModel"
Description="The folder the report templates are read from."
Field="x => x.InputFolder"
Helper="_folderHelper"
Title="Input Folder" />

<PDField TItem="FormColoursModel"
Description="The folder the finished reports are written to."
Field="x => x.OutputFolder"
Helper="_folderHelper"
HelpUrl="https://github.com/panoramicdata/PanoramicData.Blazor"
Title="Output Folder" />

<PDField TItem="FormColoursModel"
Description="Who is told when the schedule finishes."
Field="x => x.NotifyEmail"
Title="Notify" />

<PDField TItem="FormColoursModel"
Description="Free text, with a copy button."
Field="x => x.Notes"
ShowCopyButton="_ => true" />

</PDFormBody>

<PDFormFooter TItem="FormColoursModel" ShowDelete="false" />

</PDForm>
</div>

</DemoSourceView>
93 changes: 93 additions & 0 deletions PanoramicData.Blazor.Demo/Pages/PDFormPage6.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using PanoramicData.Blazor.Services;
using System.ComponentModel.DataAnnotations;

namespace PanoramicData.Blazor.Demo.Pages;

public partial class PDFormPage6
{
private PDForm<FormColoursModel>? _form;

Check warning on line 8 in PanoramicData.Blazor.Demo/Pages/PDFormPage6.razor.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

PanoramicData.Blazor.Demo/Pages/PDFormPage6.razor.cs#L8

Remove unassigned field '_form', or set its value.
private bool _dark;
private bool _neutralLabels;
private bool _labelEdges;
private bool _quietStatus;
private bool _mutedHelpers;
private bool _footerBox;

private readonly DelegatedDataProviderService<FormColoursModel> _dataProvider = new()
{
CreateAsync = (_, _) => Task.FromResult(new OperationResponse { Success = true })
};

private readonly FormFieldHelper<FormColoursModel> _folderHelper = new()
{
IconCssClass = "fas fa-fw fa-folder-open",
ToolTip = "Choose a folder",
Click = _ => new FormFieldResult { NewValue = "/Reports" }
};

private string WrapperCssClass => string.Join(' ', new[]
{
_neutralLabels ? "neutral-labels" : null,
_labelEdges ? "label-edges" : null,
_quietStatus ? "quiet-status" : null,
_mutedHelpers ? "muted-helpers" : null,
_footerBox ? "footer-box" : null
}.Where(x => x is not null));

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await ResetFormAsync().ConfigureAwait(true);
}
}

private async Task ResetFormAsync()
{
if (_form is not null)
{
await _form.EditItemAsync(new FormColoursModel(), FormModes.Create).ConfigureAwait(true);
}
}

private void AllOn() => SetAll(true);

private void AllOff() => SetAll(false);

private void SetAll(bool value)
{
_neutralLabels = value;
_labelEdges = value;
_quietStatus = value;
_mutedHelpers = value;
_footerBox = value;
}

/// <summary>
/// A schedule-like item with required fields, a valid one and an invalid one, to show every validation state.
/// </summary>
public class FormColoursModel
{
/// <summary>Gets or sets the name, which is required.</summary>
[Required]
public string Name { get; set; } = string.Empty;

/// <summary>Gets or sets the description, which starts valid.</summary>
public string Description { get; set; } = "Weekly summary";

/// <summary>Gets or sets the input folder, which is required.</summary>
[Required]
public string InputFolder { get; set; } = string.Empty;

/// <summary>Gets or sets the output folder, which is required.</summary>
[Required]
public string OutputFolder { get; set; } = string.Empty;

/// <summary>Gets or sets the address to notify, which starts invalid.</summary>
[EmailAddress]
public string NotifyEmail { get; set; } = "not-an-address";

/// <summary>Gets or sets free-text notes.</summary>
public string Notes { get; set; } = "Copy me";
}
}
35 changes: 35 additions & 0 deletions PanoramicData.Blazor.Demo/Pages/PDFormPage6.razor.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* Each switch on the page sets one group of the form's colour variables; they inherit into the form */
.form-colours-demo.neutral-labels {
--pd-form-title-error-bg: var(--bs-tertiary-bg);
--pd-form-title-error-color: var(--bs-body-color);
}

.form-colours-demo.label-edges {
--pd-form-title-edge-width: 4px;
--pd-form-title-required-edge-color: var(--bs-warning);
--pd-form-title-invalid-edge-color: var(--bs-danger);
}

.form-colours-demo.quiet-status {
--pd-form-status-valid-bg: var(--bs-tertiary-bg);
--pd-form-status-valid-color: var(--bs-secondary-color);
--pd-form-status-required-bg: var(--bs-tertiary-bg);
--pd-form-status-required-color: var(--bs-warning-text-emphasis);
--pd-form-status-invalid-bg: var(--bs-tertiary-bg);
--pd-form-status-invalid-color: var(--bs-danger-text-emphasis);
}

.form-colours-demo.muted-helpers {
--pd-form-helper-color: var(--bs-secondary-color);
}

.form-colours-demo.footer-box {
--pd-form-footer-error-color: var(--bs-body-color);
--pd-form-footer-error-icon-color: var(--bs-danger);
--pd-form-footer-error-bg: var(--bs-body-bg);
--pd-form-footer-error-border-width: 1px 1px 1px 4px;
--pd-form-footer-error-border-color: var(--bs-border-color);
--pd-form-footer-error-edge-color: var(--bs-danger);
--pd-form-footer-error-padding: 0.375rem 0.75rem;
--pd-form-footer-error-radius: 0.375rem;
}
5 changes: 5 additions & 0 deletions PanoramicData.Blazor.Demo/Shared/NavMenu.razor
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@
<span class="fas fa-fw fa-file-alt me-2" aria-hidden="true"></span> PDForm 5
</PDNavLink>
</li>
<li class="nav-item px-3">
<PDNavLink class="nav-link" href="pdform6">
<span class="fas fa-fw fa-file-alt me-2" aria-hidden="true"></span> PDForm 6 (Colours)
</PDNavLink>
</li>
<li class="nav-item px-3">
<PDNavLink class="nav-link" href="form-title-width-test">
<span class="fas fa-fw fa-file-alt me-2" aria-hidden="true"></span> PDForm Title Test
Expand Down
73 changes: 73 additions & 0 deletions PanoramicData.Blazor.Test/Components/PDFormBodyTests.Colours.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using AwesomeAssertions;
using Bunit;
using PanoramicData.Blazor.Models;

namespace PanoramicData.Blazor.Test.Components;

/// <summary>
/// The classes <see cref="PDFormBody{TItem}"/> gives a host to colour its validation state and helper icons by.
/// </summary>
public partial class PDFormBodyTests
{
/// <summary>A label keeps alert-danger and says whether the field is still waiting for a value or holds a wrong one.</summary>
/// <param name="suppressed">Whether the field's errors are suppressed, as they are until the user edits it.</param>
/// <param name="stateClass">The state class the label should have.</param>
/// <param name="otherClass">The state class it should not have.</param>
[Theory]
[InlineData(true, "pd-form-title-required", "pd-form-title-invalid")]
[InlineData(false, "pd-form-title-invalid", "pd-form-title-required")]
public async Task A_label_in_error_says_which_state_it_is_in(bool suppressed, string stateClass, string otherClass)
{
var host = RenderForm([Field(x => x.Name)]);
await Edit(host, FormModes.Edit);
var form = FormOf(host);

await host.InvokeAsync(() =>
{
form.Fields[0].SuppressErrors = suppressed;
form.SetFieldErrors(nameof(Person.Name), "Required");
});
host.Render();

host.Find(".title-box").ClassList.Should().Contain("alert-danger").And.Contain(stateClass).And.NotContain(otherClass);
}

/// <summary>A valid field's label has no state class, and its validation indicator has the status class.</summary>
[Fact]
public async Task A_valid_label_has_no_state_class_and_the_indicator_has_the_status_class()
{
var host = RenderForm([Field(x => x.Name)]);

await Edit(host, FormModes.Edit);

host.Find(".title-box").ClassList.Should().NotContain("pd-form-title-required").And.NotContain("pd-form-title-invalid");
host.Find(".d-table.input-group-text").ClassList.Should().Contain("pd-form-status").And.Contain("alert-success");
}

/// <summary>The help link, helper and copy icons, and a field's error message, carry the library's classes rather than Bootstrap's fixed colours.</summary>
[Fact]
public async Task Helper_icons_and_messages_use_themable_classes()
{
var helper = new FormFieldHelper<Person> { IconCssClass = "fa-helper", ToolTip = "Pick" };
var host = RenderForm([Field(x => x.Name, extra: new()
{
[nameof(PDField<Person>.HelpUrl)] = "https://help/",
[nameof(PDField<Person>.Helper)] = helper,
[nameof(PDField<Person>.ShowCopyButton)] = (Func<Person?, bool>)(_ => true)
})]);
await Edit(host, FormModes.Edit);
var form = FormOf(host);

await host.InvokeAsync(() =>
{
form.Fields[0].SuppressErrors = false;
form.SetFieldErrors(nameof(Person.Name), "Too short");
});
host.Render();

host.Find(".fa-external-link-alt").ClassList.Should().Contain("pd-form-helper").And.NotContain("text-info");
host.Find("i.fa-helper").ClassList.Should().Contain("pd-form-helper").And.NotContain("text-info");
host.Find(".pdClipboard-button").ClassList.Should().Contain("pd-form-helper").And.NotContain("text-info");
host.Find(".pd-form-field-form-error-message").ClassList.Should().NotContain("text-danger");
}
}
2 changes: 1 addition & 1 deletion PanoramicData.Blazor.Test/Components/PDFormBodyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public async Task Validation_indicator_options()
]);
await Edit(host, FormModes.Edit);

host.FindAll(".d-table.input-group-text").Should().AllSatisfy(e => e.ClassList.Length.Should().Be(3));
host.FindAll(".d-table.input-group-text").Should().AllSatisfy(e => e.ClassList.Where(c => c.StartsWith("alert-", StringComparison.Ordinal)).Should().BeEmpty());

var hidden = RenderForm([Field(x => x.Name)], showValidationIndicator: false);
await Edit(hidden, FormModes.Edit);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ public async Task Errors_are_counted_named_and_disable_save()

form.WaitForAssertion(() => form.Find(".pd-form-footer-errors").TextContent.Should()
.Contain("Please correct the highlighted fields"));
form.Find(".pd-form-footer-errors").ClassList.Should().NotContain("text-danger", "its colours come from --pd-form-footer-error-* variables");
ButtonFor(form, "Save").HasAttribute("disabled").Should().BeTrue();
}

Expand Down
1 change: 1 addition & 0 deletions PanoramicData.Blazor.Test/Components/PDFormHeaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ public async Task ToggleHelp_ShowsAndHidesTheHelpText(FormModes mode)
await EditAsync(form, mode);
form.Markup.Should().NotContain("Fill in the name.");

form.Find("i.fa-question-circle").ClassList.Should().Contain("pd-form-header-help").And.NotContain("text-info");
form.Find("i.fa-question-circle").Click();
form.Find(".pd-form-header .small.text-muted").TextContent.Trim().Should().Be("Fill in the name.");
form.Instance.ShowHelp.Should().BeTrue();
Expand Down
15 changes: 13 additions & 2 deletions PanoramicData.Blazor.Test/ThemeVariableStylesheetTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
namespace PanoramicData.Blazor.Test;

/// <summary>
/// The tree, combo box and context menu take their colours from CSS variables a host can set, and each default is the
/// colour the component had before, so a host that sets nothing sees no change.
/// The tree, combo box, context menu and form take their colours from CSS variables a host can set, and each default is
/// the colour the component had before, so a host that sets nothing sees no change.
/// </summary>
public class ThemeVariableStylesheetTests
{
Expand All @@ -23,6 +23,17 @@ public class ThemeVariableStylesheetTests
[InlineData("PDContextMenu.razor.css", "color: var(--pd-contextmenu-disabled-color, #6c757d);")]
[InlineData("PDContextMenu.razor.css", "border-bottom: 1px solid var(--pd-contextmenu-border-color, rgba(0,0,0,.125));")]
[InlineData("PDFormFieldEditor.razor.css", "border-top: 1px solid var(--pd-form-editor-border-color, #ced4da);")]
[InlineData("PDFormFieldEditor.razor.css", "border-color: var(--pd-form-editor-border-color, var(--bs-border-color, #495057));")]
[InlineData("PDFormBody.razor.css", "background-color: var(--pd-form-title-error-bg, var(--bs-danger-bg-subtle)) !important;")]
[InlineData("PDFormBody.razor.css", "background-color: var(--pd-form-status-invalid-bg, var(--bs-danger-bg-subtle)) !important;")]
[InlineData("PDFormBody.razor.css", "background-color: var(--pd-form-status-required-bg, var(--bs-warning-bg-subtle)) !important;")]
[InlineData("PDFormBody.razor.css", "background-color: var(--pd-form-status-valid-bg, var(--bs-success-bg-subtle)) !important;")]
[InlineData("PDFormBody.razor.css", "box-shadow: inset var(--pd-form-title-edge-width, 0) 0 0 0 var(--pd-form-title-invalid-edge-color, transparent);")]
[InlineData("PDFormBody.razor.css", "color: var(--pd-form-helper-color, var(--bs-info, #0dcaf0));")]
[InlineData("PDFormBody.razor.css", "color: var(--pd-form-error-color, var(--bs-danger, #dc3545));")]
[InlineData("PDFormHeader.razor.css", "color: var(--pd-form-helper-color, var(--bs-info, #0dcaf0));")]
[InlineData("PDFormFooter.razor.css", "color: var(--pd-form-footer-error-color, var(--bs-danger, #dc3545));")]
[InlineData("PDFormFooter.razor.css", "border-width: var(--pd-form-footer-error-border-width, 0 0 1px 0);")]
public void A_colour_reads_its_variable_with_the_original_default(string stylesheet, string declaration)
=> Read(stylesheet).Should().Contain(declaration);

Expand Down
Loading
Loading