← 中文资源

简体中文版本

无障碍文件选择与实时反馈

保留可聚焦的原生文件输入,用标签模拟按钮,实时播报已选文件并在提交前拦截超过 5MB 的文件。

阅读英文原文

功能说明

表单要求上传 PDF 或 PNG,支持多选,每个文件不超过 5MB。原生 input type=“file” 被视觉隐藏但仍可通过键盘聚焦,旁边的 label 被样式化为“选择文件”按钮。选择后脚本统计文件数量与名称,写入 aria-live 区域;若有超大文件,错误段落以 role=“alert” 显示并设置 aria-invalid。提交时再次检查空选择与大小。

关键代码

隐藏输入使用 1px 尺寸与 clip 技巧,而不是 display:none,否则会失去键盘焦点和辅助技术访问。input:focus-visible + .file-button 把焦点环画在可见标签上,因为输入本身不可见。accept 限制选择器过滤类型,multiple 允许一次选多个。aria-describedby 同时关联帮助文本与错误段落,使读屏器在聚焦时能读到要求。文件列表通过 textContent 写入,避免文件名被当作 HTML。

使用注意

accept 只是选择器提示,用户仍可切换“所有文件”,服务端必须校验 MIME 类型与文件内容。客户端大小检查可被绕过,不能替代服务端限制。示例不会真正发送数据,提交只更新状态文本。文件名可能很长或包含恶意字符,展示时应截断并转义。若使用拖放区域,还需提供键盘可操作的浏览按钮。

验证方式

用 Tab 聚焦文件选择,确认可见按钮出现焦点环,按回车或空格打开系统选择器。选择两个文件后,计数区域应读出数量与名称。选择一个超过 5MB 的文件,错误应立即出现且输入被标记无效;移除该文件后错误消失。空表单提交应聚焦文件输入并提示至少选择一个文件。

代码示例

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Accessible supporting documents upload</title>
  <style>
    body {
      margin: 0;
      padding: 2rem;
      font-family: system-ui, sans-serif;
    }

    form {
      max-width: 32rem;
      display: grid;
      gap: 0.75rem;
    }

    .file-picker {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      gap: 0.75rem;
    }

    .file-input {
      position: absolute;
      inline-size: 1px;
      block-size: 1px;
      padding: 0;
      margin: -1px;
      overflow: hidden;
      clip: rect(0 0 0 0);
      white-space: nowrap;
      border: 0;
    }

    .file-button {
      display: inline-block;
      padding: 0.55rem 1rem;
      border: 1px solid currentColor;
      border-radius: 0.4rem;
      cursor: pointer;
    }

    .file-input:focus-visible + .file-button {
      outline: 3px solid #4f46e5;
      outline-offset: 2px;
    }

    .field-error {
      margin: 0;
      color: #b91c1c;
    }
  </style>
</head>
<body>
  <main>
    <h1>Supporting documents</h1>

    <form id="upload-form">
      <label for="documents">Supporting documents</label>
      <p id="documents-help">PDF or PNG, up to 5 MB per file. Multiple files are allowed.</p>

      <div class="file-picker">
        <input
          class="file-input"
          id="documents"
          name="documents"
          type="file"
          accept=".pdf,image/png"
          multiple
          aria-describedby="documents-help documents-error">
        <label class="file-button" for="documents">Choose files</label>
        <span id="file-count" aria-live="polite">No files selected</span>
      </div>

      <p class="field-error" id="documents-error" role="alert" hidden></p>
      <button type="submit">Upload</button>
      <p id="status" role="status" aria-live="polite"></p>
    </form>
  </main>

  <script>
    const form = document.querySelector('#upload-form');
    const input = document.querySelector('#documents');
    const count = document.querySelector('#file-count');
    const error = document.querySelector('#documents-error');
    const status = document.querySelector('#status');
    const MAX_BYTES = 5 * 1024 * 1024;

    function showError(message) {
      error.textContent = message;
      error.hidden = false;
      input.setAttribute('aria-invalid', 'true');
    }

    function clearError() {
      error.textContent = '';
      error.hidden = true;
      input.removeAttribute('aria-invalid');
    }

    input.addEventListener('change', () => {
      clearError();
      const files = [...input.files];

      if (files.length === 0) {
        count.textContent = 'No files selected';
        return;
      }

      const tooLarge = files.filter((file) => file.size > MAX_BYTES).map((file) => file.name);
      if (tooLarge.length > 0) {
        showError('These files exceed 5 MB: ' + tooLarge.join(', ') + '. Remove them before uploading.');
      }

      const names = files.map((file) => file.name).join(', ');
      count.textContent = files.length + ' file' + (files.length === 1 ? '' : 's') + ' selected: ' + names;
    });

    form.addEventListener('submit', (event) => {
      event.preventDefault();
      const files = [...input.files];

      if (files.length === 0) {
        showError('Choose at least one file to upload.');
        input.focus();
        return;
      }

      if (files.some((file) => file.size > MAX_BYTES)) {
        showError('Remove files larger than 5 MB before uploading.');
        input.focus();
        return;
      }

      status.textContent = files.length + ' file(s) ready to upload. This demo does not send data.';
    });
  </script>
</body>
</html>