让表单适应小屏幕

HTMLBeginner
立即练习

介绍

Still & Spoke 在周六举办小型陶艺工作坊。工作室页面已经准备好,但报名表还需要可见标签、浏览器原生输入检查、按条件必填的座位备注,以及能在窄屏上用键盘操作的布局。你将完成一次真实的本地报名,而不是只显示成功文字。独立的 Study form 会在你修改作品前提供完整示例。

你需要了解基本 HTML 元素、CSS 选择器和布局,以及 JavaScript 对象、函数、DOM 选择和事件。本实验介绍标签关联、浏览器表单约束、无障碍描述、条件 required、可见焦点和这里使用的简单响应式规则。第一个实验由准备好的提交处理程序和本地 API 处理请求;下一个实验再构建该处理程序。所有人物和地址都是合成数据,不会发送邮件或收取费用。这是实用的可用性练习,并非完整的无障碍认证。50 分钟是作者估计的时长,包含阅读和普通错误处理。

在 Workspace 中执行命令,在 Studio 中使用网站。可编辑文件是 /home/labex/project/stillspoke/form.html、validity.js 和 form.css。保留其他文件、准备好的 API、容量规则和已有访客。每个实验独立初始化。准备好的服务器会把 form.html 的原始内容放入品牌页面外壳,不会补全缺失标签或重新生成你的作品。刷新后先等待页面加载完成,再操作表单。所有文档语言中的网站标签均保持英文。

为每个控件提供可见名称

这一步先用键盘体验完整示例,再把作品页面中的每个控件与可见标签及说明关联起来。

在 Studio 中选择页面链接 Study form。这是网站内部导航,不是切换 LabEx Tab。示例拥有独立 API 和数据库,不会完成你的作品或占用作品页面的名额。按 Tab,直到 Your name 出现可见轮廓。输入 Example Guest,按 Tab,输入 sample@example.test。再按 Tab 到 Session,按 C 选择 Clay, slowly / 10:00,读清已选标题再继续。这个原生选择框支持输入选项的开头。按 Tab 到协助复选框,再按 Tab 到可选备注,再按 Tab 到 Reserve my place。按一次 Enter。等待 Submission result 显示 Reserved for Example Guest 和服务器签发的 Confirmation SS-…。被动的发送状态消息会消失。焦点移到结果区,便于找到完成结果。

下图显示单独带标题的结果区及真实确认编号。你获得的编号会不同。

独立示例表单中的真实确认

在示例页面选择 Studio records,找到同一个姓名、地址、可读时间和确认编号。此页面读取已接受的记录,不是浏览器结果中的文字。选择 Back to signup,再选择 Work form 返回自己的页面。你的页面仍未完成,也没有新增作品报名;示例结果不计入成果。

切换到 Workspace。cd 改变当前终端的工作目录;绝对路径从 / 开始。pwd 打印该目录。以下命令明确指定编辑位置:

cd /home/labex/project/stillspoke
pwd

预期输出:

/home/labex/project/stillspoke

表单的 name 值就是 API 键:name、email、session、support 和 note。HTML id 标识页面中的一个元素。标签的 for 值指向输入框的 id,因此点击可见标签或使用辅助技术读取时,可以识别对应控件。这里 Your name 使用 for="person",而提交键是 name;这两个属性职责不同。把复选框包在标签内会形成隐式关联。autocomplete="name" 和 autocomplete="email" 标识浏览器可建议自动填写的信息类型,本身不会提交数据。占位文字在输入时会消失,不能替代可见标签。

required 会在发送请求前拒绝空值。type="email" 要求浏览器拒绝格式错误的邮箱,但不能证明地址归某人所有。minlength="2" 要求姓名至少两个字符。第一个空选项使用户必须选择真实场次。选项的 value 是稳定的服务器身份,可见标题供人阅读;不要按选项位置推断二者关系。

aria-describedby 包含以空格分隔的 ID,把每个字段与持续可见的帮助文字及初始为空的错误段落关联起来。帮助在出错前解释规则,错误文字在服务器拒绝后补充原因。除颜色外还应使用明确文字。aria-hidden="true" 只向辅助技术隐藏装饰箭头。↗ 是 HTML 数字字符引用:ASCII 源码会在浏览器中显示为箭头。Sending 段落使用 role="status" 作被动通知。结果区的 aria-labelledby 指向标题 ID,为该区提供名称。初始 hidden 属性使空结果在有内容前不显示。独立结果区的 tabindex="-1" 允许脚本聚焦,却不会增加普通 Tab 顺序中的停靠点。我们不聚焦状态通知。

以下命令只替换 form.html。cat 写出后面的文字;> 覆盖这个指定文件。<< 后带引号的 HTML 启动 here-document:直到单独一行 HTML 为止,所有内容都作为文件原文。给标记加引号可防止 shell 解释 HTML 内的字符。粘贴整个块,包括最后的标记。shell 提示符返回表示写入完成,没有单独的保存键。不要粘贴 Markdown 的反引号。之后的小型 JavaScript 和 CSS 文件也使用这种编辑方式。

cd /home/labex/project/stillspoke
cat > form.html <<'HTML'
<form id="signup">
  <div class="pair">
    <div class="field">
      <label for="person">Your name (required)</label>
      <input id="person" name="name" required minlength="2" autocomplete="name"
             aria-describedby="name-help name-error">
      <small id="name-help">At least two characters.</small>
      <p class="error" id="name-error"></p>
    </div>
    <div class="field">
      <label for="email">Email (required)</label>
      <input id="email" name="email" type="email" required autocomplete="email"
             aria-describedby="email-help email-error">
      <small id="email-help">Practice address: you@example.test</small>
      <p class="error" id="email-error"></p>
    </div>
  </div>
  <div class="field">
    <label for="session">Session (required)</label>
    <select id="session" name="session" required aria-describedby="session-help session-error">
      <option value="">Choose a time</option>
      <option value="clay-morning">Clay, slowly / 10:00</option>
      <option value="clay-afternoon">Clay, slowly / 14:00</option>
    </select>
    <small id="session-help">Choose a listed time. The afternoon session is full.</small>
    <p class="error" id="session-error"></p>
  </div>
  <label class="check"><input type="checkbox" name="support"> I need seating assistance</label>
  <div class="field">
    <label for="note">Seating note (required when assistance is selected)</label>
    <textarea id="note" name="note" rows="2" aria-describedby="note-help note-error"></textarea>
    <small id="note-help">When assistance is selected, describe the help in at least 5 characters.</small>
    <p class="error" id="note-error"></p>
  </div>
  <button type="submit">Reserve my place <span aria-hidden="true">&#8599;</span></button>
  <p id="progress" role="status"></p>
  <section id="result" aria-labelledby="result-heading" tabindex="-1" hidden>
    <h3 id="result-heading">Submission result</h3>
    <p id="message"></p>
  </section>
</form>
HTML

返回 Studio 并刷新作品页面。输入后仍应能读到标签。点击 Email,观察焦点移入输入框,再按 Tab 按文档顺序继续。表单为空时激活 Reserve my place:浏览器应聚焦 Your name 并说明必填要求。输入一个字符再试,观察姓名长度规则。换成 Morgan Vale 之类的姓名;输入 not-an-email,观察不同的邮箱格式拒绝。这些是原生校验消息,不是 HTTP 422:浏览器尚未发送报名请求。修改值时,在该字段获得焦点后用 Ctrl+A 或 Cmd+A 选中文字,再输入替代值。拼写错误不需要重置 VM。

此时,即使勾选协助,备注仍是可选项。下一步就添加这一项行为。页面也还未适配窄屏。自检:五个控件均有可见且关联的标签,帮助始终可见,必填字段为空时不能提交。

让座位备注随复选框变化

这一步让备注只在访客请求座位协助时必填,并保留已经输入的所有文字。

复选框暴露布尔值 checked:选中时为 true,否则为 false。输入框或文本区暴露与 HTML 约束对应的 required 属性。STUDIO 是 scenario.js 加载的准备好的配置对象;此工作坊的 noteMin 是 5。读取这个数值,不要假定今后的每个工作坊都相同。该配置提供场景数据,不会替你实现条件规则。

querySelector 返回匹配 CSS 选择器的第一个元素;[name="support"] 和 [name="note"] 匹配这些属性。我们通过 API name 选择控件。复选框变化时,函数更新两个原生约束。在 condition ? first : second 中,条件运算符在条件为真时选择 first,否则选择 second。因此请求协助时备注最小长度为五,否则为零。调用一次 updateNote() 还会在页面加载时初始化正确状态。访客切换复选框后,change 监听器再次运行它。我们修改约束而非 .value,所以取消勾选不会抹掉草稿。

在 Workspace 中替换 validity.js。JS 标记的作用与上一步的 HTML 标记相同:

cd /home/labex/project/stillspoke
cat > validity.js <<'JS'
const support = document.querySelector('[name="support"]');
const note = document.querySelector('[name="note"]');
function updateNote() {
  note.required = support.checked;
  note.minLength = support.checked ? STUDIO.noteMin : 0;
}
support.addEventListener('change', updateNote);
updateNote();
JS

返回 Studio 并刷新作品页面。用 Tab 填入至少两个字符的姓名、morgan@example.test 这样的格式正确地址,并选择 Clay, slowly / 10:00。在 I need seating assistance 上按 Space 勾选。备注留空,激活 Reserve my place。浏览器现在应聚焦 Seating note 并要求填写。输入 near,用 Tab 离开备注,再次尝试提交:四个字符少于要求的五个。minlength 约束作用于用户输入文字;本练习正常键入,而非用脚本赋值。

根据需要按 Shift+Tab 返回复选框,按 Space 取消勾选。备注现在可选,单词 near 仍保留。再次选择协助,把备注替换为 A seat near the entrance。保留这些输入供下一步使用;刷新会丢弃未发送的表单输入,但切换 LabEx Tab 会让此浏览器页面保持打开。

如果复选框似乎没有作用,返回 Workspace,显示实际保存的文件:

cd /home/labex/project/stillspoke
cat validity.js

与上方短代码块比较,包括 checked 拼写和最后的 updateNote();。必要时用完整代码块替换文件,再刷新 Studio。缺失脚本或 JavaScript 拼写错误不同于浏览器拒绝空备注;不能仅凭没有报名就声称原生约束有效。自检:复选框会改变必填状态,仅在选中时拒绝输入过短的备注,并保留原有备注文字。

保持表单可读并让焦点可见

这一步完成表单布局,进行一次真实作品页面报名,并读取对应的已接受记录。

双列表单需要能够折叠。CSS Grid 的 1fr 1fr 让姓名和邮箱均分空间;min-width: 0 允许字段收缩。视口最大宽度为 800 像素时,媒体查询把该组合改为单列。width: 100% 配合 box-sizing: border-box 会把内边距和边框计入可用宽度。选择器 input:not([type="checkbox"]) 把复选框排除在全宽输入规则之外,让其保留小方框外形。文字无需悬停即可读取。准备好的页面外壳已会在小屏幕上把故事放在表单上方;这些规则让你的表单适应其中的空间。

:focus-visible 为键盘焦点提供可见轮廓。颜色并非唯一信号:必填文字、错误文字和轮廓形状也很重要。等效的清晰焦点边框或阴影也有效;去掉所有焦点提示则不行。[aria-invalid="true"] 为服务器标为无效的字段设置样式;.error:empty 只隐藏空错误段落,不隐藏标签或必要说明。overflow-wrap: anywhere 允许长确认编号或合成地址换行。减少动态效果偏好会避免对有此需求的人添加平滑移动;此表单不需要动画。

在 Workspace 中替换 form.css:

cd /home/labex/project/stillspoke
cat > form.css <<'CSS'
form { margin-top: 28px; }
.pair { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.field { min-width: 0; margin-bottom: 18px; }
label { display: block; margin-bottom: 7px; font-size: 13px; font-weight: bold; }
input:not([type="checkbox"]), select, textarea {
  width: 100%; box-sizing: border-box; padding: 11px 12px;
  border: 1px solid #89988a; border-radius: 3px;
  font: inherit; font-size: 15px; color: #243d35; background: #fcfaf5;
}
small { display: block; margin-top: 6px; font-size: 11px; color: #56645a; }
.check { display: flex; gap: 9px; align-items: center; margin: 20px 0; }
.check input { width: 18px; height: 18px; accent-color: #244c3d; }
button {
  padding: 14px 20px; border: 0; border-radius: 3px;
  font: inherit; font-size: 14px; color: #fffdf6; background: #254f41; cursor: pointer;
}
button[type="submit"] { width: 100%; display: flex; justify-content: space-between; }
button:disabled { background: #5b6c5e; cursor: wait; }
:focus-visible { outline: 3px solid #ad4d28; outline-offset: 4px; }
[aria-invalid="true"] { border: 2px solid #9d3327; }
.error { color: #963024; font-size: 12px; margin: 5px 0; }
.error:empty { display: none; }
#progress, #message { font-size: 14px; overflow-wrap: anywhere; }
#result { margin-top: 20px; border-top: 1px solid #c9cebf; padding-top: 12px; }
#result h3 { font-size: 15px; margin: 0 0 8px; }
#records article { border-top: 1px solid #c9cebf; overflow-wrap: anywhere; padding: 12px 0; }
@media (max-width: 800px) { .pair { grid-template-columns: 1fr; gap: 0; } }
@media (prefers-reduced-motion: reduce) { * { scroll-behavior: auto !important; } }
CSS

返回 Studio,刷新并使用作品表单。输入 Morgan Vale、morgan@example.test,用 C 选择 Clay, slowly / 10:00,用 Space 选择协助,再输入 A seat near the entrance。按 Tab 到提交按钮,按一次 Enter。等待准备好的处理程序显示真实确认。第一个实验提供该处理程序;你实现的是围绕它的表单语义、约束和样式。下一个实验解释并构建请求与反馈逻辑。

选择 Studio records,找到同样的姓名、邮箱、时间和确认编号。保留 Existing Visitor 与 Avery Reed。他们的记录独立于你的新报名,清空他们来腾出空间不属于任务范围。容量由服务器强制执行,不是由禁用的前端按钮执行。之后选择 Back to signup。即使刷新会清除未发送的字段,已保存报名仍在本实验中保留。

把浏览器或 Studio 面板缩窄到约 390 像素,也检查约 768 像素的平板宽度。表单不应出现横向滚动,标签和帮助应始终可见,姓名/邮箱组合应上下排列。用 Tab 遍历控件:每个获得焦点的控件都需要可见提示。恢复较宽面板,确认组合可使用两列。不要通过隐藏难以处理的字段来适配布局。

如果页面仍是旧布局,确认你在 /home/labex/project/stillspoke 写入的是 form.css,不是 base.css,然后刷新。路径错误不需要重置 VM。自检:访客能在窄屏和宽屏上阅读并操作整个表单,真实作品报名出现在记录中,原有访客保持不变。实验检查读取真实记录,并在临时隔离数据中运行未修改的文件副本;绝不会替你提交真实表单。

总结

你把可见标签、原生约束和帮助与真实表单控件关联起来,让座位备注按条件必填且不丢失输入,并构建了带可见键盘焦点的响应式布局。你先体验独立示例,再完成真实作品报名,并把确认编号与已接受记录核对。下一个独立实验将构建请求和反馈处理程序。