Basic Input
A labelled text input with helper text wired to it via aria-describedby.
6 个框架初级
An invalid select wired with `aria-invalid` and `aria-describedby` - not just a red border.
<!--
An invalid select. Three things do the work, and none of them is the red
border:
- aria-invalid="true" is what makes a screen reader say "invalid entry". A red
border says nothing to anyone who cannot see it.
- aria-describedby carries BOTH ids - the hint and the error. Order matters:
they are announced in the order listed, so the error comes last and is what
the user is left with. Replacing the hint with the error instead of
appending it would silently drop the instruction the user still needs.
- The message pairs an icon with the text. Red alone is a colour-only signal
and roughly one man in twelve cannot act on it.
The error text is NOT role="alert" here: it is already described by the
control, so an alert would announce it a second time on every focus.
-->
<div class="esel esel--invalid">
<span class="esel__label" id="esel-label">Billing country</span>
<button
class="esel__trigger"
id="esel-trigger"
type="button"
aria-haspopup="listbox"
aria-expanded="false"
aria-controls="esel-listbox"
aria-labelledby="esel-label esel-trigger"
aria-invalid="true"
aria-describedby="esel-hint esel-error"
>
<span class="esel__value esel__value--empty">Choose a country…</span>
<svg class="esel__chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6" /></svg>
</button>
<ul class="esel__listbox" id="esel-listbox" role="listbox" aria-labelledby="esel-label" tabindex="-1" hidden>
<li class="esel__option" id="esel-opt-de" role="option" data-value="de" aria-selected="false"><span class="esel__marker" aria-hidden="true"></span>Germany</li>
<li class="esel__option" id="esel-opt-fr" role="option" data-value="fr" aria-selected="false"><span class="esel__marker" aria-hidden="true"></span>France</li>
<li class="esel__option" id="esel-opt-nl" role="option" data-value="nl" aria-selected="false"><span class="esel__marker" aria-hidden="true"></span>Netherlands</li>
<li class="esel__option" id="esel-opt-es" role="option" data-value="es" aria-selected="false"><span class="esel__marker" aria-hidden="true"></span>Spain</li>
</ul>
<p class="esel__hint" id="esel-hint">Sets the tax rate on your invoices.</p>
<p class="esel__error" id="esel-error">
<svg class="esel__error-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<circle cx="12" cy="12" r="10" /><path d="M12 8v4M12 16h.01" />
</svg>
Select a billing country to continue.
</p>
</div>
<script>
document.querySelectorAll('.esel').forEach(function (root) {
var trigger = root.querySelector('.esel__trigger');
var listbox = root.querySelector('.esel__listbox');
var valueEl = root.querySelector('.esel__value');
var options = Array.prototype.slice.call(root.querySelectorAll('.esel__option'));
var active = 0;
function paint() {
options.forEach(function (option, index) {
option.classList.toggle('esel__option--active', index === active);
});
listbox.setAttribute('aria-activedescendant', options[active] ? options[active].id : '');
}
function setOpen(open) {
listbox.hidden = !open;
trigger.setAttribute('aria-expanded', String(open));
if (open) paint();
}
function close() { setOpen(false); trigger.focus(); }
function commit(index) {
var option = options[index];
if (!option) return;
options.forEach(function (o) { o.setAttribute('aria-selected', String(o === option)); });
valueEl.textContent = option.textContent.trim();
valueEl.classList.remove('esel__value--empty');
active = index;
// Clearing the error is not cosmetic: aria-invalid and the describedby
// list have to come off together, or the control keeps announcing an
// error the user has already fixed.
root.classList.remove('esel--invalid');
trigger.removeAttribute('aria-invalid');
trigger.setAttribute('aria-describedby', 'esel-hint');
close();
}
trigger.addEventListener('click', function () {
setOpen(trigger.getAttribute('aria-expanded') !== 'true');
});
options.forEach(function (option, index) {
option.addEventListener('click', function () { commit(index); });
option.addEventListener('mousemove', function () { active = index; paint(); });
});
trigger.addEventListener('keydown', function (event) {
var open = trigger.getAttribute('aria-expanded') === 'true';
var key = event.key;
if (!open) {
if (key === 'ArrowDown' || key === 'ArrowUp' || key === 'Enter' || key === ' ') {
event.preventDefault();
setOpen(true);
}
return;
}
if (key === 'Escape') { event.preventDefault(); close(); return; }
if (key === 'Enter' || key === ' ') { event.preventDefault(); commit(active); return; }
if (key === 'Tab') { setOpen(false); return; }
var next = active;
if (key === 'ArrowDown') next = active + 1;
else if (key === 'ArrowUp') next = active - 1;
else if (key === 'Home') next = 0;
else if (key === 'End') next = options.length - 1;
else return;
event.preventDefault();
active = (next + options.length) % options.length;
paint();
});
document.addEventListener('mousedown', function (event) {
if (!root.contains(event.target)) setOpen(false);
});
});
</script>| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
label必填 | string | - | 加载时朗读的无障碍标签。 |
items必填 | SelectItem[] | - | 要渲染的项目数组。 |
value | string | - | 指标的当前值,需预先格式化。 |
message | string | - | 通知的正文文本。 |
onSelect | (value: string) => void | - | 用户选择菜单项时触发,参数为所选项。 |
disabled | boolean | false | 禁止交互并将控件置灰。 |
The red border is the least important part. `aria-invalid="true"` is what makes a screen reader say "invalid entry"; colour says nothing to anyone who cannot see it. `aria-describedby` carries both the hint and the error, in that order, because they are announced in the order listed and the error should be what the user is left holding - replacing the hint with the error silently drops an instruction they still need. The presence of `message` IS the invalid state: there is no separate boolean to fall out of step with it, so a red control with no message cannot be expressed. Note the error text has no `role="alert"` - the control already describes it, and an alert would announce it twice on every focus.