65.9K
CodeProject 正在变化。 阅读更多。
Home

Blazor Web Assembly (WASM) 开关

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.70/5 (4投票s)

2022年2月7日

CPOL

8分钟阅读

viewsIcon

10518

downloadIcon

195

符合 ARIA 规范的普通和 EditForm 切换开关,内置浅色和深色主题支持。包含六种额外自定义皮肤。

目录

引言

这是 Blazor 系列文章中的第二篇,重点关注内置浅色和深色主题的控件开发。请查看第一篇文章,它为本文及后续文章的主题支持奠定了基础。

灵感

以前,我写过一篇关于C# 和 VB 中灵活的 WPF ToggleSwitch 无外观控件的文章,因此切换开关自然成为本系列 Blazor 组件的首选。

设计

要求

组件需要支持

  • 在标准 HTML 和 Blazor EditForm 中使用
  • 控制状态
    • Enabled
    • 焦点
    • 悬停
    • Checked
    • 禁用
    • 只读
  • 控制部件的显示
    • 定位
    • Visibility
    • 所有文本均可更改和自定义标记
  • 类、样式和自定义属性
  • ARIA 合规性
  • 主题支持
  • C# 可空合规性
  • 可在多个项目之间重用
  • 如果无法避免,则尽量减少 JavaScript
  • 最新的 Blazor 和 CSS3 编码技术
  • BEM —(块元素修饰符)CSS 类命名约定

在各种 Blazor 第三方库中,有许多不同的切换开关设计。对于此控件,我将外观基于标准 Windows 外观。

浅色主题

深色主题

自定义主题

我们将探讨两种不同的实现方式来切换主色主题

  1. 通过样式直接在 HTML 标签上应用 CSS 变量
  2. 使用预设的 CSS 类名

下面是一个示例,我们已将红色主色方案应用于基色和悬停色。

自定义 UI

在本文的稍后部分,我们将探讨如何通过六种不同的设计来自定义外观。尽管每个组件的外观和动画都不同,但底层 HTML 保持不变。

实现

ARIA 合规性

虽然没有具体的文档说明要求,但由于它基于 input type="checkbox",我们可以遵循双状态要求。还提供了一个示例

切换开关 UI

切换开关控件有三个部分

  1. 标签/标题 - 文本,告知用户选择的用途
  2. 切换 - 可点击的开/关开关
  3. 状态 - 指示切换状态的文本

对于自定义 UI,我添加了第四部分。这不用于默认组件。当控件渲染时,标记如下

<div class="c-toggle">
    <div id="id_piLBO02PjEqg5wHdYvv4gA" class="c-toggle__label">
        Enabled and checked
    </div>
    <div class="c-toggle__container">
        <input type="checkbox" role="switch"
               id="id_VfH38gwO8UOP45Y_g7v5bg"
               class="c-toggle__pill"
               aria-labelledby="id_nB-HBiD9DUORGBYQXFU2sA"
               aria-checked="true"
               aria-readonly="false">
        <label class="c-toggle__thumb" 
               for="id_VfH38gwO8UOP45Y_g7v5bg"
               aria-labelledby="id_piLBO02PjEqg5wHdYvv4gA"
               data-label="On" data-label-on="On" data-label-off="Off"
               tabindex="0">
            <span class="c-toggle__thumb-inner"></span>
        </label>
        <span id="id_nB-HBiD9DUORGBYQXFU2sA"
              class="c-toggle__state-text">On</span>
    </div>
</div>

按类名分解

  • c-toggle - 根容器
  • c-toggle__label - 标题标签
  • c-toggle__container - 包含开关和状态。允许灵活定位
  • c-toggle__pill - 包含组件的状态值。它用于 HTML Form 和 Blazor EditForm 输入控制要求。readonlydisabledchecked 的 UI 属性和 onchange 事件跟踪在此标签上连接。
  • c-toggle__thumb - 组件的默认 UI。::after 选择器用于渲染开和关状态的开关滑块位置。还有 data-label(已删除状态)、data-label-on(开状态文本)和 data-label-off(关状态文本)用于自定义 UI 支持。tabindex 属性用于告诉浏览器将焦点设置在哪里。
  • c-toggle__thumb-inner - 这不用于默认渲染,但用于自定义 UI 支持。
  • c-toggle__state-text - 开和关状态的文本标签。

组件样式表

切换开关被实现为两个独立的组件 - 一个用于与 Blazor EditForm 配合使用,另一个用于一般用途。为了提供灵活性并减少重复,CSS 隔离未被使用,但 Razor 类库 (RCL) 中包含一个默认样式表。要使用,我们需要在 index.html 头部中包含它。

<link href="_content/Blazor.Toggle/css/styles.css" rel="stylesheet" />

样式表的顺序对于 CSS 特异性要求很重要

  1. 基本 CSS 框架 - 例如:Bootstrap
  2. 库样式表
  3. 应用程序样式表

因此,例如,对于演示项目,使用了以下内容

<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="_content/Blazor.Toggle/css/styles.css" rel="stylesheet" />
<link href="ToggleDemo.styles.css" rel="stylesheet" />
<link href="css/app.css" rel="stylesheet" />

主题

CSS 变量用于在浅色深色模式之间切换。请查看上一篇文章了解其实现方式。

我尽量不使用特定于组件的CSS 变量名称,而是使用通用名称,以便将来用于其他控件。

浅色主题

--primary-fill: #0078D4;
--primary-fill-hover: #006CBE;
--primary-foreground: #FFFFFF;
--neutral-fill: #EDEDED;
--neutral-fill-hover: #E5E5E5;
--neutral-outline: #646464;
--neutral-outline-hover: #3B3B3B;
--neutral-foreground: #2B2B2B;
--neutral-focus-visual: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
--neutral-shadow-visual:  0 10px 20px -8px #3B3B3B;
--neutral-border: #BEBEBE;
--neutral-color1: #767676;
--neutral-color2: #FFFFFF;
--neutral-background1: #FFFFFF;
--neutral-background2: #F7F7F7;
--icon-backkground: #b6d2e8;

深色主题

--primary-fill: #006CBE;
--primary-fill-hover: #0078D4;
--primary-foreground: #FFFFFF;
--neutral-fill: #363636;
--neutral-fill-hover: #3D3D3D;
--neutral-outline: #646464;
--neutral-outline-hover: #8A8A8A;
--neutral-foreground: #F5F5F5;
--neutral-focus-visual: 0 0 0 0.25rem rgba(120,120,120,0.25);
--neutral-shadow-visual: 0 10px 20px -8px #8A8A8A;
--neutral-border: #323232;
--neutral-color1: #8D8D8D;
--neutral-color2: #929292;
--neutral-background1: #202020;
--neutral-background2: #424242;
--icon-backkground: #0066B4

禁用

对于禁用状态,使用不透明度来减少使用的 CSS 变量数量。

--disabled-opacity: 0.3;

主色主题

包含一些备用颜色。

 .c-toggle__primary-indigo {
     --primary-fill: #6610f2;
     --primary-fill-hover: #580ED1;
 }

 .c-toggle__primary-purple {
     --primary-fill: #6f42c1;
     --primary-fill-hover: #633BAD;
 }

 .c-toggle__primary-red {
     --primary-fill: #dc3545;
     --primary-fill-hover: #C72F3E;
 }

 .c-toggle__primary-orange {
     --primary-fill: #fd7e14;
     --primary-fill-hover: #EB7512;
 }

 .c-toggle__primary-green {
     --primary-fill: #198754;
     --primary-fill-hover: #177A4C;
 }

作为演示代码的一部分,我演示了如何使用两种常用方法添加自己的自定义主主题颜色:使用 StylesClasses

自定义 UI

演示项目中还包含六个自定义 UI,除了 CSS 类之外,所有都使用相同的组件属性。CSS 类用于应用更改。

正如您从上面的屏幕截图可以看到的,ToggleToggleInput 组件的渲染标记是相同的,因此自定义 UI CSS 将与两者无缝配合。

这是渲染 Toggle 组件的代码

<div class="@CssSection">

    <h3>Standard <b>Toggle</b> Component</h3>

    <div class="o-custom__sections">
        @foreach (CustomToggleModel model in Toggles)
        {
          <section class="o-section__custom">
                <Toggle Value="@true"
                        Class="@model.CssClass"
                        OnText="@model.YesChoice"
                        OffText="@model.NoChoice" />
          </section>
        }
    </div>
</div>

这是渲染 ToggleInput 组件的代码

<EditForm class="@CssSection" Model="Toggles">

    <h3>EditForm <b>ToggleInput</b> Component</h3>

    <div class="o-custom__sections">
        @foreach (CustomToggleModel model in Toggles)
        {
            <section class="o-section__custom">
                <ToggleInput @bind-Value="model.IsChecked"
                        Class="@model.CssClass"
                        OnText="@model.YesChoice"
                        OffText="@model.NoChoice" />
            </section>
        }
    </div>
</EditForm>

用于分配 CSS 类、默认选择和名称的 C# 后台代码

#region BEM

private readonly string CssSection
    = "o-section".JoinName("body");

#endregion

#region Fields

    private const string NoChoiceValue = "No";
    private const string YesChoiceValue = "Yes";

#endregion

#region Properties

private List<CustomToggleModel> Toggles { get; } = new()
{
// [..trimmed..]
    new ()
    {
        NoChoice = NoChoiceValue,
        YesChoice = YesChoiceValue,
        CssClass = "custom__toggle
                    custom__toggle-2
                    custom__toggle--position"
    },
// [..trimmed..]
};

注意:上述自定义 UI 选自 Free Frontend 网站。值得一看,因为他们有很多可供选择。

代码

标记

Razor 相当不言自明

  1. 每个部分的位置可以通过属性和方法选择:HasLabelHasOnOffLabel()Position
  2. 通过 LabelContent 支持标题标签的自定义内容。
  3. 状态文本通过 OnTextOffText 属性设置。活动状态使用 StateLabel 私有属性设置。
  4. 组件 UI 的 disableenabledchecked UI 属性通过 input 标签上的 privatepublic 组件属性设置。
<div class="@Classname" style="@Style">
    @if (HasLabel())
    {
        <div id="@LabelId" class="@CSS.Label">
            @if (LabelContent is not null)
            {
                @LabelContent
            }
            else
            {
                @Label
            }
        </div>
    }
    <div class="@CSS.Container">
        @if (Position == TogglePosition.Left)
        {
            <input type="checkbox" role="switch"
                   id="@PillId" class="@CSS.Pill"
                   aria-labelledby="@StateLabelId"
                   aria-checked="@Value.ToString().ToLower()"
                   aria-readonly="@Disabled.ToString().ToLower()"
                   checked="@Value" disabled="@Disabled"
                   onchange="@OnChange"
                   @onclick:preventDefault="@ReadOnly"/>

            <label class="@CSS.Thumb" for="@PillId"
                   aria-labelledby="@LabelId"
                   data-label="@StateLabel"
                   data-label-on="@OnText"
                   data-label-off="@OffText"
                   tabindex="0"
                   @onkeydown="@(OnKeyDownAsync)"
                   @onkeydown:preventDefault="true"
                   @onkeydown:stopPropagation="true">
                <span class="@CSS.ThumbInner"></span>
            </label>

            @if (HasOnOffLabel())
            {
                <span id="@StateLabelId" class="@CSS.State">
                    @StateLabel
                </span>
            }
        }
        else
        {
            @if (HasOnOffLabel())
            {
                <span id="@StateLabelId" class="@CSS.State">
                    @StateLabel
                </span>
            }

            <input type="checkbox" role="switch"
                   id="@PillId" class="@CSS.Pill"
                   aria-labelledby="@StateLabelId"
                   aria-checked="@Value.ToString().ToLower()"
                   aria-readonly="@Disabled.ToString().ToLower()"
                   checked="@Value" disabled="@Disabled"
                   onchange="@OnChange"
                   @onclick:preventDefault="@ReadOnly"/>

            <label class="@CSS.Thumb" for="@PillId"
                   aria-labelledby="@LabelId"
                   data-label="@StateLabel"
                   data-label-on="@OnText"
                   data-label-off="@OffText"
                   tabindex="0"
                   @onkeydown="@(OnKeyDownAsync)"
                   @onkeydown:preventDefault="true"
                   @onkeydown:stopPropagation="true">
                <span class="@CSS.ThumbInner"></span>
            </label>
        }
    </div>
</div> 

ToggleToggleInput 组件之间主要的 razor 代码区别在于 input 标签。

对于 Toggle 组件

<input type="checkbox" role="switch"
       id="@PillId" class="@CSS.Pill"
       aria-labelledby="@StateLabelId"
       aria-checked="@Value.ToString().ToLower()"
       aria-readonly="@Disabled.ToString().ToLower()"
       checked="@Value" disabled="@Disabled"
       onchange="@OnChange" @onclick:preventDefault="@ReadOnly"/>

对于 Toggle 组件,我们手动绑定到 Value 属性并挂钩 onchange 事件。当 onchange 事件触发时,我们手动更新 Value 属性并调用 StateHasChanged() 来更新 UI。

对于 ToggleInput 组件

<input type="checkbox" role="switch"
       id="@PillId" class="@CSS.Pill"
       aria-labelledby="@StateLabelId"
       aria-checked="@CurrentValue.ToString().ToLower()"
       aria-readonly="@Disabled.ToString().ToLower()"
       @bind=CurrentValue disabled="@Disabled"
       @onclick:preventDefault="@ReadOnly"/>

对于 ToggleInput,我们双向绑定到基类 InputBase,基类将在值更改时处理 UI 刷新。

后台代码

我将重点介绍关键逻辑,并跳过属性。属性是直接的。

组件部分 ID

ARIA 和 HTML 标记要求各个部分相互指向。例如:aria-labelledbyfor。我们只需要在组件创建时设置这些一次。

protected override void OnInitialized()
{
    _labelId = GetUniqueId();
    _pillId = GetUniqueId();
    _stateLabelId = GetUniqueId();

    if (DefaultValue)
        Value = true;

    base.OnInitialized();
}

唯一的 ID 使用了一个辅助方法。可在 Blazor.Common 库中找到。

在组件基类中

public string GetUniqueId()
    => "id_" + Guid.NewGuid().ToShortString();

扩展方法

public static class GuidExtensions
{
    public static string ToShortString(this Guid guid)
        => Convert.ToBase64String(guid
                     .ToByteArray())
                     .Replace('+', '-').Replace('/', '_')[..22];
}

组件配置

CSS 类名用于根据设置的属性配置 UI

private string Classname
{
    get
    {
        CssBuilder builder = new CssBuilder(CSS.Root);

        if (Disabled)
            builder.AddClass(CSS.Modifier.Disabled);

        if (InlineLabel)
            builder.AddClass(CSS.Modifier.InlineLabel);

        if (!HasOnOffLabel())
            builder.AddClass(CSS.Modifier.NoOnOffLabel);

        if (!string.IsNullOrEmpty(Class))
            builder.AddClass(Class);

        return builder.Build();
    }
}

我正在使用 Ed Charbeneau · GitHub 的辅助类来构建 CSS 类——这是干净代码的必备工具!

ToggleInput 验证

ToggleInput 具有 InputBase 所需的额外代码,用于解析 Value 和验证

protected override bool TryParseValueFromString
(
    string? value,
    out bool result,
    out string validationErrorMessage
)
{
    if (bool.TryParse(value, out bool parsedValue))
    {
        result = parsedValue;
        validationErrorMessage = string.Empty;
        return true;
    }

    result = default;
    validationErrorMessage = $"The {FieldIdentifier.FieldName} field is not valid.";
    return false;
}

键盘支持

ARIA 对键盘支持的要求只有 space 键。但是,我还添加了对 enter 键的支持。

首先,我们需要挂接 onkeydown 事件。我们还需要消费按键

<label class="@CSS.Thumb" for="@PillId" aria-labelledby="@LabelId"
       data-label="@StateLabel" data-label-on="@OnText" data-label-off="@OffText"
       tabindex="0"
       @onkeydown="@(OnKeyDownAsync)"
       @onkeydown:preventDefault="true"
       @onkeydown:stopPropagation="true">

现在我们可以处理事件了。对于 Toggle 组件,我们手动处理 Value 更改事件。对于 ToggleInputInputBase 基类处理更改事件,所以我们需要采用不同的方法。

对于 Toggle 组件

private void OnKeyDownAsync(KeyboardEventArgs arg)
{
    switch (arg.Code)
    {
        case "Space":
        case "Enter":
            OnChange();
            break;
    }
}

private void OnChange()
{
    // check is here for browsers that do not manage the input disabled state
    if (_disabled)
        return;

    Value = !Value;

    InvokeAsync(async () => await ValueChanged.InvokeAsync(Value));
}

对于 ToggleInput 组件

private void OnKeyDownAsync(KeyboardEventArgs arg)
{
    //Console.WriteLine($"** KEY: {arg.Code} | {arg.Key}");

    switch (arg.Code)
    {
        case "Space":
        case "Enter":
            CurrentValue = !CurrentValue;
            break;
    }
}

用法

切换

示例项目有四个示例

  1. 基本用法与主色主题
  2. EditForm 用法与主色主题
  3. 自定义布局
  4. 自定义 UI 设计

Basic

<Toggle Label="Enabled and checked"
        DefaultValue="true"
        OnText="On" OffText="Off"
        Style="@CustomStyle"/>

自定义标签

<Toggle InlineLabel="true"
        OnText="On" OffText="Off"
        Class="@CustomCss"
        ValueChanged=@OnCheckedAsync>
    <LabelContent>
        Custom inline label 
    </LabelContent>
</Toggle>

ToggleInput

如果 ToggleInput 未包含在 EditForm 组件中,它将抛出异常。

<EditForm class="o-editform"
          Model=MyModel
          OnValidSubmit="@HandleValidSubmit">
    <ToggleInput @bind-Value=MyModel.BoundChecked2
                 Class="@CustomCss"
                 Label="Are you sure?"
                 InlineLabel="true"
                 OnText="Yes"
                 OffText="No" />
</EditForm>

自定义布局

有一个设置演示展示了自定义布局。我将 C# 和 VB 中灵活的 WPF ToggleSwitch 无外观控件文章中使用的示例进行了现代化改造。

在这里,我们在 LabelContent 中设置图标、标题和选定状态,并使用 display: flex 来定位元素

<ToggleInput @bind-Value="model.IsChecked"
             Class="@CssSenderItem"
             Position="TogglePosition.Right"
             OnText="@model.YesChoice"
             OffText="@model.NoChoice"
             Disabled="@NotificationsDisabled">
    <LabelContent>
            @if (!string.IsNullOrEmpty(model.IconType))
            {
                <box-icon name='@model.IconName'
                          type='@model.IconType'
                          class="@CssSenderItemIcon">
                </box-icon>
            }
            else
            {
                <box-icon name='@model.IconName'
                          class="@CssSenderItemIcon">
                </box-icon>
            }
            <div class="@CssSenderItemText">
                <span class="@CssSenderItemTitle">
                    @model.Title
                </span>
                <span class="@CssSenderItemState">
                    @GetStateLabel(model.IsChecked)
                </span>
            </div>
    </LabelContent>
</ToggleInput>

这是用于布局和调整部件大小的 CSS

.c-setting {
    display: flex;
    flex-direction: column;
    gap: 0.5em;
}

.c-sender__item {
    display: flex;
    align-items: center;
}

.c-sender__item .c-toggle__label {
    display: flex;
    flex-direction: row;
    flex-grow: 1;
}

.c-sender__item-text {
    display: flex;
    flex-direction: column;
    line-height: 1.35;
}

.c-sender__item-title {
    font-size: 1.15em;
}

.c-sender__item-state {
    font-size: 0.85em;
}

ColorSelect - 额外组件

为了演示主色主题,可以使用标准的 Select 标签下拉菜单。但我想要一个更可定制的,能显示正在选择的颜色。快速搜索了 Code Pen,找到了这个 纯 CSS 选择框,我将其改编并制作成一个带有浅色和深色主题的 Blazor 组件。

总结

我们已经将简单的 input type="checkbox" 变成了用于普通和 EditForm 的切换开关。在您自己的项目中,只需少量属性和单个事件即可轻松实现。

我们还添加了浅色、深色和主色主题支持。我们也看到了如何自定义部件的布局。最后,我们还演示了如何将其提升到更高水平,具有酷炫的动画自定义 UI。

作为额外奖励,包含了一个 ColorSelect 控件。

下载代码并查看实际效果。

尽情享用!

历史

  • v1.0 - 2022年2月7日 - 首次发布
© . All rights reserved.