R

React 中可访问的暗模式切换

2025-05-25

React 中可访问的暗模式切换

审核我的投资组合网站的可访问性 - 第 3 部分

阅读第 1 部分 - 审计第 2 部分 - 快速修复

当我使用@dailydevtips1教程制作暗黑模式切换开关时,我专注于在整个网站上设置具有足够对比度的颜色主题。我选择的颜色不太可能给色盲用户带来困扰。我没有考虑确保键盘用户和屏幕阅读器用户都能使用。

结果,我在可访问性审核过程中遇到了几个相关的错误。我需要让它可聚焦,并添加描述性文字。此外,由于切换按钮的可见部分通过 CSS 规则与隐藏<label>部分绑定,我需要找到一种方法来将内容放入其中,既能增加可访问性,又不会影响功能。另外,我还遇到了一个错误,说我的 属性中没有有效的匹配属性<input type="checkbox">display: none;<label>for<label>id<input>

让我们聚焦

我的投资组合 Github 仓库包含所有切换组件代码CSS。切换组件的结构如下:



<div className="container--toggle">
    {
        togClass === "light" ? 
            <input type="checkbox" id="toggle" className="toggle--checkbox" onClick={handleOnClick} checked />
        :
            <input type="checkbox" id="toggle" className="toggle--checkbox" onClick={handleOnClick} />
     }
     <label htmlFor="toggle" className="toggle--label">
          <span className="toggle--label-background"></span>
     </label>
</div>


Enter fullscreen mode Exit fullscreen mode

经过一番阅读,我将隐藏复选框的 CSS 从 更改为 ,display: none;opacity: 0;使其可聚焦。

经过反复尝试,我发现虽然理论上可以聚焦<label>,但它会将焦点转移到 上<input>。然后,我以为复选框没有获得焦点。没有焦点轮廓。我点击了Enter,却什么也没发生。最终,我意识到我没有在 上编程Enter!我添加了一个handleKeypress这样的函数:



const handleKeypress = e => {
  if (e.key === "Enter") {
    if (localStorage.getItem('theme') === 'theme-dark') {
      setTheme('theme-light');
      setTogClass('light')
    } else {
      setTheme('theme-dark');
      setTogClass('dark')
    }
  }
}


Enter fullscreen mode Exit fullscreen mode

我最初使用了e.keyCode === 13,但什么也没发生。当我在控制台中记录事件对象时,我发现keyCode当我点击 时,该属性返回的是 0。Enter不知道为什么。

现在我的切换按钮将执行某些操作Enter,我有两个选择:

  1. 添加onKeyPress={handleKeypress}tabIndex="0"<div>具有继承的默认焦点轮廓的容器中
  2. 添加onKeyPress={handleKeypress}<input type="checkbox">并尝试获得围绕的焦点轮廓<label>

我不喜欢选项#1,因为切换组件不在<div>容器内居中,我记得花了一段时间才将容器和切换定位到容器内居中<nav>

经过一番尝试和尝试,我终于成功实现了 #2。我尝试将 CSS 属性设置outline为类似 的值inherit,但无法显示默认的蓝色焦点轮廓。幸运的是,在设计我的<nav>部分时,我确保了在焦点和悬停时激活的按钮边框在暗色和亮色模式下都具有足够的对比度。因此,我知道可以将该 CSS 颜色变量用于此轮廓。我将切换轮廓设置为比按钮边框略粗,以便更容易看到。



.toggle--checkbox:focus + .toggle--label {
    outline: solid 3px var(--button-border);
}


Enter fullscreen mode Exit fullscreen mode

黑暗模式切换在黑暗模式和亮模式下被聚焦和激活

标签内的标签

我注意到的第一件事是,尽管有错误,但我确实有一个htmlForid属性。现在标签没有设置为,我必须重新测试display: none;

我立刻在<label>右侧的 后面添加了“暗黑模式切换”按钮<span>。它刚好能和我的标签完美契合,所以我琢磨了好一会儿,想找到让文本不可见的最佳方法。我发现 CSScolor属性的值无效hsla(),所以没法用这种方法让它透明。最后,我想:“为什么不直接把颜色设置为和背景一样的 CSS 变量呢?” 瞧!……我就是这么想的。

带有三颗星的月亮一侧的开关看起来像一个矩形/线,而不是一个圆圈

文字被隐藏了,但我注意到一颗星看起来像一个矩形或直线,而不是一个圆形。我开始移动文字——把它放在 之前<span>和 之中<span>,这开始以各种滑稽的方式破坏 CSS。原来我一开始不小心选了破坏程度最小的选项。

我将文本放回后方<span>,发现我所要做的就是将规则width中的属性值.toggle--label-background从 4px 调整为 6px。

月亮一侧的开关与第三颗星再次环视

最后,我开始研究屏幕阅读器如何与切换按钮交互。最终,我希望传达出该组件是一个暗黑模式切换按钮,并让屏幕阅读器在暗黑模式启用或禁用时通知用户。我一开始用了一个很长的aria-label,但复选框状态改变后,屏幕阅读器并没有再次读取文本。我开始研究aria-checked,发现了role="switch"。现在,我使用的屏幕阅读器在聚焦时会清晰地显示“暗黑模式切换”,在暗黑模式启用时显示“开启”,在亮黑模式启用时显示“关闭”。由于我的 CSS 代码编写方式,这实际上与复选框是否选中相反。哎呀。



<div className="container--toggle">
  {
    togClass === "light" ?
      <input aria-label="dark mode toggle" role="switch" aria-checked="false" onKeyPress={handleKeypress} type="checkbox" id="toggle" className="toggle--checkbox" onClick={handleOnClick} checked />
    :
      <input aria-label="dark mode toggle" role="switch" aria-checked="true" onKeyPress={handleKeypress} type="checkbox" id="toggle" className="toggle--checkbox" onClick={handleOnClick} />
  }
  <label htmlFor="toggle" className="toggle--label">
    <span className="toggle--label-background"></span>
    dark mode toggle
  </label>
</div>


Enter fullscreen mode Exit fullscreen mode

你控制不了我!

在编写此组件时,我根据用户浏览器 localStorage 中的主题,使用条件运算符返回了一个<input type="checkbox" checked>“或” ,以便太阳始终以亮色模式显示,月亮始终以暗色模式显示。我无法让该属性执行我想要的操作,并且 React 无法编译带有条件逻辑的受控组件,该逻辑返回该属性或组件内的任何内容。自从构建此组件以来,每当用户点击切换按钮时,我都会收到一条警告,提示我必须“在组件的整个生命周期内,决定使用受控还是非受控输入元素”。<input type="checkbox">defaultCheckedchecked

进一步的研究表明,该defaultChecked属性会忽略状态变化。真正让人头疼的是 StackOverflow 上的一个回复,它显示你可以将该checked属性设置为 true 或 false。然而,这样做却导致了另一个错误:

“警告:您checked向没有处理程序的表单字段提供了一个 prop onChange。这将呈现只读字段。如果该字段应该是可变的,请使用defaultChecked。否则,请设置onChangereadOnly。”

由于onChange用于记录用户输入,我添加了readOnly,现在所有受控组件的错误都已修复。接下来,我重构了handleKeypresshandleOnClick逻辑,将其改为调用,changeThemeAndToggle而不是重复逻辑。

最后,由于我编写 CSS 和重构的方式,我必须添加一个ariaActive变量,以便屏幕阅读器在暗黑模式开启时显示“on”,在暗黑模式关闭时显示“off”。现在组件如下所示:



import React, { useEffect, useState } from 'react';
import '../styles/toggle.css';
import { setTheme } from '../utils/themes';

function Toggle() {
    // false = dark mode because of the way I wrote the CSS
    const [active, setActive] = useState(false)
    // the opposite, for screen readers
    const [ariaActive, setAriaActive] = useState(true)
    let theme = localStorage.getItem('theme')

    const changeThemeAndToggle = () => {
      if (localStorage.getItem('theme') === 'theme-dark') {
        setTheme('theme-light')
        setActive(true)
        setAriaActive(false)
      } else {
        setTheme('theme-dark')
        setActive(false)
        setAriaActive(true)
      }
    }

    const handleOnClick = () => {
      changeThemeAndToggle()
    }

    const handleKeypress = e => {
      changeThemeAndToggle()
    }

    useEffect(() => {
      if (localStorage.getItem('theme') === 'theme-dark') {
        setActive(false)
        setAriaActive(true)
      } else if (localStorage.getItem('theme') === 'theme-light') {
        setActive(true)
        setAriaActive(false)
      }
    }, [theme])

    return (
      <div className="container--toggle">
        <input aria-label="dark mode toggle" role="switch" aria-checked={ariaActive} onKeyPress={handleKeypress} type="checkbox" id="toggle" className="toggle--checkbox" onClick={handleOnClick} checked={active} readOnly />
        <label htmlFor="toggle" className="toggle--label">
          <span className="toggle--label-background"></span>
          dark mode toggle
        </label>
      </div>
    )
}

export default Toggle;


Enter fullscreen mode Exit fullscreen mode

测试

我一直在使用键盘和屏幕阅读器进行手动测试,但现在是时候重新启动IBM Equal Access Accessibility Checker了。

上一篇博文写完后,我真应该重新测试一下。我立刻发现,在文本中又出现了两个“上方”和“下方”的用法,如果没有视觉效果,这些用法根本说不通。我已经在“我的投资组合网站可访问性审核 - 第二部分”中移除了一个,现在我又移除了这两个。

ARC 工具包告诉我我的shiba SVG需要focusable="false",所以我在它们的代码中都添加了这个。当我在实际网站上测试时,它们的可见时间不够长,导致出现错误,所以幸好我在本地测试时关闭了 lambda 函数。从技术上讲,这些 SVG 以及我落地页按钮中的箭头 SVG 不需要 alt 文本,因为它们是装饰性的,但我为它们感到自豪。希望屏幕阅读器用户不会介意听到我为作品集网站添加的一些额外内容。

我还发现了一些与使用aria-label和方式相关的错误aria-labelledby。在进一步了解地标角色和 aria 属性后,我将所有内容部分<div>s更改为 ,<sections>这样就一举解决了 aria 错误和“多个<h1>”警告。现在,我还有一些关于博客预览组件标题的新问题需要在本系列的下一篇博客中修复。

required的联系表单中的属性也出现了错误。我最终在表单字段中添加了aria-required="true"autoComplete="on",现在 ARC Toolkit 已经满足要求了。

我只收到两条关于切换开关的警告。一条是对比度警告,提示我通过将文本设置为与背景相同的颜色来隐藏文本——这很合理。太阳和月亮的视觉效果传达了文本的含义,所以我并不担心。另一条警告说,由于我以多种方式标记了该组件,我需要检查屏幕阅读器如何与其交互,我已经检查过了。

根据反馈进行更新

我查看了 @inhuofficial 的报告,报告称点击 时,切换按钮会闪烁Space。原来是我在重构时不小心把条件语句 删掉了handleKeypress()。即使没有条件语句 ,Enter仍然会触发切换按钮——我猜测是因为 HTML 的问题。点击Space会导致它闪到另一侧,然后恢复到原始状态。我已将函数更新为如下所示:



const handleKeypress = e => {
  if (e.code === "Enter") {
    changeThemeAndToggle()
  }
}


Enter fullscreen mode Exit fullscreen mode

最初修改时,我再次将事件对象记录到控制台,以验证 的代码Space。这时,我注意到EnterSpace都能完美触发切换。我更新了条件语句if (e.code === "Enter" || "Space"), 和 都能Enter正常工作,但又Space闪了一下!这段代码现在已在我的网站上线, 和Enter都能Space正常工作。

结论

向@overtureweb致谢,他对我原来的暗模式切换博客进行了评论并给出了checked={active}修复方案 - 很抱歉我在回复时没有理解。

我玩这个玩得很开心。对焦和星点修正非常令人满意,而且整个切换开关也变得没那么容易被黑了,这让我很开心。

阅读“我的投资组合网站的可访问性审核 - 第 4 部分”,其中我修复了主页上有关我的博客预览组件的一些问题。

阅读第 5 部分 - 博客页面可访问性深度探讨
,我发现了一个安全漏洞,编写了数量惊人的正则表达式,并且这个系列成为了一篇论文。

阅读第六部分——结局

我修复了暗模式切换的颜色对比度问题,并加快了其焦点轮廓动画的速度。

请继续关注第 6 部分、最终测试和想法。

文章来源:https://dev.to/abbeyperini/an-accessible-dark-mode-toggle-in-react-aop
PREV
编码和注意力缺陷多动症——无法继续
NEXT
支持女性开发者的 8 种方法