发布于 2026-01-06 1 阅读
0

Vue 中 Vuelidate 的无障碍表单错误自动聚焦

Vue 中 Vuelidate 的无障碍表单错误自动聚焦

Vuelidate 让开发者能够轻松处理最复杂的表单验证案例,但如何兼顾无障碍用户体验呢?让我们来看看一些简单的实践,您可以将它们应用到基于 Vuelidate 的表单中,从而让它们更好地兼容屏幕阅读器等无障碍工具。

表格

我们先创建一个标准表单,并对我们的数据应用一些验证规则。

    <template>
      <div>
        <form @submit.prevent="submit">
          <div>
            <label for="firstName">First Name</label>
            <input
              type="text"
              id="firstName"
              name="firstName"
            >
          </div>

          <div>
            <label for="lastName">Last Name</label>
            <input
              type="text"
              id="lastName"
              name="lastName"
            >
          </div>

          <div>
            <label for="email">Email</label>
            <input
              type="email"
              id="email"
              name="email"
            >
          </div>

          <button type="submit">Submit</button>
        </form>
      </div>
    </template>

我们的表单有三个输入框——前两个输入框类型为“文本” text,最后一个输入框类型为“文本” email。最后,我们还有一个submit按钮,用于触发元素submit上的事件form

form元素本身有一个@submit带有prevent修饰符的处理程序,以便我们可以停止浏览器的默认行为并自行处理表单提交。

  • 要了解更多关于事件修正器的信息,您可以查看官方文档。

现在让我们添加处理验证规则和提交方法的代码。

    <script>
    import { required, email } from "vuelidate/lib/validators";
    export default {
      name: "App",
      data() {
        return {
          firstName: "",
          lastName: "",
          email: ""
        };
      },
      validations: {
        firstName: { required },
        lastName: { required },
        email: { required, email }
      },
      methods: {
        submit() {
          // Submit the form here!
        }
      }
    };
    </script>

首先,我们导入 Vuelidate内置的几个验证器requiredemail

我们创建一个本地状态,data并为每个输入设置一个属性,然后创建一个validations对象。该对象进而为每个输入定义规则。

最后,我们需要回到 Vuelidate <template>,并通过 Vuelidate 将我们的输入连接起来v-model

    <div>
      <label for="firstName">First Name</label>
      <input
        type="text"
            id="firstName"
        name="firstName"
        v-model="$v.firstName.$model"
      >
    </div>

    <div>
      <label for="lastName">Last Name</label>
      <input
        type="text"
        id="lastName"
        name="lastName"
        v-model="$v.lastName.$model"
      >
    </div>

    <div>
      <label for="email">Email</label>
      <input
        type="email"
        id="email"
        name="email"
        v-model="email"
        @change="$v.email.$touch"
      >
    </div>

请注意,对于 firstName 和 lastName,我们直接在 Vuelidate 的内部$model对每个属性进行 v-modeling,这样我们就不必担心$dirty在 change/input 事件发生时触发每个输入的状态。

对于邮箱输入框,我选择直接使用 v-model 连接到data()本地状态并手动触发事件。这样,验证不会立即触发,而是在输入框失去焦点后才会触发,用户也不会因为刚开始输入$touch就看到不满足条件的错误信息。email

添加错误消息

我们先来添加输入验证失败时的描述性错误信息。首先,我们会<p>在输入框后面添加一个元素,并将错误信息输出给用户。

    <div>
      <label for="firstName">First Name</label>
      <input
        type="text"
        id="firstName"
        name="firstName"
        v-model="$v.firstName.$model"
      >
      <p
        v-if="$v.firstName.$error"
      >This field is required</p>
    </div>

    <div>
      <label for="lastName">Last Name</label>
      <input
        type="text"
        id="lastName"
        name="lastName"
        v-model="$v.lastName.$model"
      >
      <p v-if="$v.lastName.$error">This field is required</p>
    </div>

    <div>
      <label for="email">Email</label>
      <input
        type="email"
        id="email"
        name="email"
        v-model="email"
        @change="$v.email.$touch"
      >
      <p v-if="$v.email.$error">{{ email }} doesn't seem to be a valid email</p>
    </div>

请注意,每个p标签都是根据一条语句进行条件渲染的v-if。这条语句会检查 Vuelidate 对象内部$v,然后访问每个输入框的状态(基于我们在上一节中定义的验证和状态),最后访问$error该元素的状态。

Vuelidate 会跟踪每个元素的不同状态,$error它是一个布尔属性,将检查两个条件 - 它将检查输入的$dirty状态是否为真true,以及任何验证规则是否失败。

$dirty状态是一个布尔值,默认值为falsetrue。当用户更改输入内容并且 v-model 状态$v.element.$model设置为 true 时,它​​将自动更改为 false true,表示内容已修改,验证现在可以显示错误(否则表单在加载时将处于默认错误状态)。

对于我们的email输入,由于我们将其绑定v-model到本地状态,因此我们必须$touchchange事件发生时触发该方法——这$touch将把状态设置$dirty为 true。

既然我们已经为用户提供了验证失败时的清晰错误信息,接下来就让我们使其更易于访问。目前,屏幕阅读器无法识别这一变化,也无法在输入框重新聚焦时通知用户问题,这会造成极大的困惑。

幸运的是,我们有一个方便的工具可以将这条消息附加到输入框中——即aria-describedby属性。这个属性允许我们通过其描述元素的方式附加一个或多个元素id。现在,让我们修改表单以反映这一点。

    <form @submit.prevent="submit">
        <div>
          <label for="firstName">First Name</label>
          <input
            aria-describedby="firstNameError"
            type="text"
            id="firstName"
            name="firstName"
            v-model="$v.firstName.$model"
          >
          <p
            v-if="$v.firstName.$error"
            id="firstNameError"
          >This field is required</p>
        </div>

        <div>
          <label for="lastName">Last Name</label>
          <input
            aria-describedby="lastNameError"
            type="text"
            id="lastName"
            name="lastName"
            v-model="$v.lastName.$model"
          >
          <p v-if="$v.lastName.$error" id="lastNameError">This field is required</p>
        </div>

        <div>
          <label for="email">Email</label>
          <input
            aria-describedby="emailError"
            type="email"
            id="email"
            name="email"
            v-model="email"
            @change="$v.email.$touch"
          >
          <p v-if="$v.email.$error" id="emailError">{{ email }} doesn't seem to be a valid email</p>
        </div>

        <button type="submit">Submit</button>
    </form>

太好了!如果您现在使用ChromeVox等屏幕阅读器测试表单,您可以触发验证错误并使元素获得焦点——屏幕阅读器现在会在获得焦点时将错误作为输入信息的一部分进行朗读,从而让用户更清楚地了解发生了什么。

在 @submit 上触发验证

让我们更进一步,现在点击提交按钮不会有任何反应。我们需要在用户尝试提交表单时,对表单中的所有元素触发验证检查。

修改submit方法如下。

    methods: {
      submit() {
        this.$v.$touch();
        if (this.$v.$invalid) {
          // Something went wrong 
        } else {
          // Submit the form here
        }
      }
    }

这里发生了两件事,首先,我们通过调用 `.validate()` 来触发表单上每个输入框的验证$v.$touch()。Vuelidate 会遍历每个带有验证器的输入框,并触发验证函数,以便在出现任何错误时更新状态以显示错误信息。

Vuelidate 还管理着表单的“全局”状态,其中包含它自己的$invalid状态,我们将使用该状态来验证表单是否处于可以提交的有效状态 - 如果不是,我们将通过自动聚焦第一个具有错误状态的元素来帮助我们的用户。

自动对焦元素时出现错误

目前,当用户点击提交按钮并触发该submit()方法时,Vuelidate 会验证所有输入。如果其中某些输入存在错误,v-if则会满足每个错误输入的条件并显示错误消息。

但是,除非我们明确指示,否则屏幕阅读器不会自动朗读这些错误信息。为了提升用户体验,让我们自动聚焦出现问题的输入框。

首先,我们需要回到表单中,ref为每个输入添加一个属性,以便我们可以在submit()方法中引用和定位它。

    <form @submit.prevent="submit">
      <div>
        <label for="firstName">First Name</label>
        <input
          aria-describedby="firstNameError"
          type="text"
          id="firstName"
          name="firstName"
          ref="firstName"
          v-model="$v.firstName.$model"
        >
        <p
          v-if="$v.firstName.$error"
          id="firstNameError"
        >This field is required</p>
      </div>

      <div>
        <label for="lastName">Last Name</label>
        <input
          aria-describedby="lastNameError"
          type="text"
          id="lastName"
          name="lastName"
          ref="lastName"
          v-model="$v.lastName.$model"
        >
        <p v-if="$v.lastName.$error" id="lastNameError">This field is required</p>
      </div>

      <div>
        <label for="email">Email</label>
        <input
          aria-describedby="emailError"
          type="email"
          id="email"
          name="email"
          ref="email"
          v-model="email"
          @change="$v.email.$touch"
        >
        <p v-if="$v.email.$error" id="emailError">{{ email }} doesn't seem to be a valid email</p>
      </div>

      <button type="submit">Submit</button>
    </form>

请注意,我已将所有ref属性命名为与其对应模型相同的名称。这将使下一步的循环操作更加容易。

既然我们已经可以确定输入,让我们修改一下submit()方法,以便我们可以遍历不同的输入,并找出哪个输入有错误。

    submit() {
      this.$v.$touch();
      if (this.$v.$invalid) {
        // 1. Loop the keys
        for (let key in Object.keys(this.$v)) {
          // 2. Extract the input
          const input = Object.keys(this.$v)[key];
          // 3. Remove special properties
          if (input.includes("$")) return false;

          // 4. Check for errors
          if (this.$v[input].$error) {
            // 5. Focus the input with the error
            this.$refs[input].focus();

            // 6. Break out of the loop
            break;
          }
        }
      } else {
        // Submit the form here
      }
    }

代码很多!不过别担心,我们会把它分解成简单的步骤。

  1. 首先,我们创建一个for循环来遍历$v对象中的每个属性。该$v对象包含多个属性,其中包含正在验证的每个输入项,以及一些特殊的状态属性,例如$error整个$invalid表单的状态。
  2. 我们将输入/属性名称提取到一个变量中,以便于访问。
  3. 我们检查输入是否包含该$字符,如果包含,则跳过此输入,因为它是一个特殊的数据属性,我们现在不关心它。
  4. 我们检查$error状态,如果$error状态为真,则表示此特定输入存在问题,并且其中一项验证失败。
  5. 最后,我们使用名称input作为通过实例访问它的一种方式$refs,并触发元素的focus。这就是输入→引用名称关系,这也是为什么之前我们对引用和v-model状态使用相同命名的原因。
  6. 我们只想聚焦第一个元素,所以我们调用函数break来停止循环继续执行。

试试这个功能,现在当用户触发表单提交并出现错误时,表单将自动聚焦到第一个出现错误的输入框中。

还有一个小问题,屏幕阅读器仍然无法读取我们自定义的错误信息。我们需要告诉它,这个<p>描述输入框的标签将是一个“动态”区域,用于显示信息,而且信息可能会发生变化。

在这种情况下,我们将为错误消息添加提示。这样,当错误消息出现且焦点移至该元素时,屏幕阅读器就会通知用户。如果错误消息发生变化,例如从验证错误变为其他错误,aria-live="assertive"屏幕阅读器也会通知用户。requiredminLength

    <form @submit.prevent="submit">
      <div>
        <label for="firstName">First Name</label>
        <input
          aria-describedby="firstNameError"
          type="text"
          id="firstName"
          name="firstName"
          ref="firstName"
          v-model="$v.firstName.$model"
        >
        <p
          v-if="$v.firstName.$error"
          aria-live="assertive"
          id="firstNameError"
        >This field is required</p>
      </div>

      <div>
        <label for="lastName">Last Name</label>
        <input
          aria-describedby="lastNameError"
          type="text"
          id="lastName"
          name="lastName"
          ref="lastName"
          v-model="$v.lastName.$model"
        >
        <p v-if="$v.lastName.$error" aria-live="assertive" id="lastNameError">This field is required</p>
      </div>

      <div>
        <label for="email">Email</label>
        <input
          aria-describedby="emailError"
          type="email"
          id="email"
          name="email"
          ref="email"
          v-model="email"
          @change="$v.email.$touch"
        >
        <p
          v-if="$v.email.$error"
          aria-live="assertive"
          id="emailError"
        >{{ email }} doesn't seem to be a valid email</p>
      </div>

      <button type="submit">Submit</button>
    </form>

总结

当用户尝试提交无效表单时,自动聚焦元素是一种非常好的无障碍用户体验形式,而这并不需要我们开发人员付出太多努力和工作。

通过使用诸如 ` aria-describedbyand` 之类的属性,aria-live我们已经将表单增强到了一个易于访问的状态,而目前网络上大多数表单都没有实现这一点。当然,这还可以进一步增强,但这已经是一个很好的起点!

如果你想看看这个例子实际运行的效果,我已经在这里设置了一个 codesandbox

一如既往,感谢您的阅读,欢迎在推特上与我分享您在无障碍表单方面的体验:@marinamosti

PS:牛油果万岁🥑

附言:❤️🔥🐶☠️

文章来源:https://dev.to/marinamosti/accessible-form-error-auto-focus-with-vuelidate-in-vue-4cok