Basic Input
A labelled text input with helper text wired to it via aria-describedby.
6 個のフレームワーク初級
A combobox that offers to create the tag you just typed when nothing matches.
<!--
A combobox that can invent a row. Four decisions worth stealing:
- The create row IS a role="option", not a button below the list. In the
combobox pattern it is exactly what it looks like: one more thing Enter can
land on. A button there would be unreachable by the arrow keys that got the
user to the bottom of the list in the first place.
- It appears only when there is no EXACT match, not when there are no results.
Type "des" against "design" and you want the filtered list; the offer to
create "des" would be noise. It also sits LAST, so it can never steal the
Enter meant for a real result.
- The label is quoted - Create “design system”. Without the quotes, a tag with
a trailing space or an odd capital is invisible before you commit it.
- Freshly created tags are announced. Otherwise the row silently changes shape
under the cursor and nothing says what happened.
-->
<div class="mcreate">
<label class="mcreate__label" for="mcreate-input">Tags</label>
<input
class="mcreate__input"
id="mcreate-input"
type="text"
role="combobox"
aria-expanded="false"
aria-controls="mcreate-listbox"
aria-autocomplete="list"
aria-describedby="mcreate-status"
autocomplete="off"
placeholder="Search or create a tag…"
/>
<p class="mcreate__status" id="mcreate-status" role="status" aria-live="polite">4 tags available.</p>
<ul class="mcreate__listbox" id="mcreate-listbox" role="listbox" aria-multiselectable="true" aria-label="Tags" hidden></ul>
</div>
<script>
document.querySelectorAll('.mcreate').forEach(function (root) {
var input = root.querySelector('.mcreate__input');
var listbox = root.querySelector('.mcreate__listbox');
var status = root.querySelector('.mcreate__status');
var known = ['design', 'engineering', 'research', 'roadmap'];
var chosen = [];
var rows = [];
var active = 0;
function optionId(index) { return 'mcreate-opt-' + index; }
function build() {
var query = input.value.trim();
var q = query.toLowerCase();
var matches = known.filter(function (t) { return t.toLowerCase().includes(q); });
rows = matches.map(function (tag) { return { kind: 'tag', tag: tag }; });
// Exact match, not "no results": typing "des" against "design" should
// filter, not offer to create a second nearly identical tag. Last in the
// list so it can never steal the Enter meant for a real one.
var exact = known.some(function (t) { return t.toLowerCase() === q; });
if (query && !exact) rows.push({ kind: 'create', tag: query });
if (active >= rows.length) active = Math.max(0, rows.length - 1);
}
function render() {
build();
listbox.innerHTML = '';
rows.forEach(function (row, index) {
var li = document.createElement('li');
li.id = optionId(index);
li.setAttribute('role', 'option');
li.setAttribute('aria-selected', String(row.kind === 'tag' && chosen.includes(row.tag)));
li.className =
(row.kind === 'create' ? 'mcreate__option mcreate__option--create' : 'mcreate__option') +
(index === active ? ' mcreate__option--active' : '');
if (row.kind === 'create') {
li.innerHTML =
'<span class="mcreate__marker" aria-hidden="true"></span>' +
'<svg class="mcreate__plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14" /></svg>' +
'<span class="mcreate__option-label">Create “' + row.tag + '”</span>';
} else {
li.innerHTML =
'<span class="mcreate__marker" aria-hidden="true"></span>' +
'<span class="mcreate__option-label">' + row.tag + '</span>' +
'<span class="mcreate__tick" aria-hidden="true">✓</span>';
}
li.addEventListener('mousedown', function (event) {
event.preventDefault();
commit(index);
});
li.addEventListener('mousemove', function () {
active = index;
render();
});
listbox.appendChild(li);
});
listbox.setAttribute('aria-activedescendant', rows.length ? optionId(active) : '');
listbox.hidden = !rows.length || input.getAttribute('aria-expanded') !== 'true';
if (!input.value.trim()) status.textContent = known.length + ' tags available.';
}
function commit(index) {
var row = rows[index];
if (!row) return;
if (row.kind === 'create') {
known.push(row.tag);
chosen.push(row.tag);
input.value = '';
active = 0;
render();
status.textContent = 'Created and added “' + row.tag + '”. ' + chosen.length + ' selected.';
return;
}
var at = chosen.indexOf(row.tag);
if (at === -1) chosen.push(row.tag);
else chosen.splice(at, 1);
render();
status.textContent = chosen.length + ' selected.';
}
input.addEventListener('focus', function () {
input.setAttribute('aria-expanded', 'true');
render();
});
input.addEventListener('input', function () {
input.setAttribute('aria-expanded', 'true');
active = 0;
render();
});
input.addEventListener('keydown', function (event) {
var key = event.key;
if (key === 'Escape') {
event.preventDefault();
input.setAttribute('aria-expanded', 'false');
listbox.hidden = true;
return;
}
if (!rows.length) return;
if (key === 'Enter') { event.preventDefault(); commit(active); return; }
var next = active;
if (key === 'ArrowDown') next = active + 1;
else if (key === 'ArrowUp') next = active - 1;
else return;
event.preventDefault();
active = (next + rows.length) % rows.length;
render();
});
document.addEventListener('mousedown', function (event) {
if (root.contains(event.target)) return;
input.setAttribute('aria-expanded', 'false');
listbox.hidden = true;
});
render();
});
</script>| Prop | 型 | デフォルト | 説明 |
|---|---|---|---|
label必須 | string | - | 読み込み中に読み上げられるアクセシブルなラベル。 |
items必須 | string[] | - | レンダリングする項目の配列。 |
value必須 | string[] | - | 指標の現在値。フォーマット済みの文字列を渡します。 |
onSelect | (next: string[]) => void | - | ユーザーが選択したメニュー項目を引数に呼ばれます。 |
disabled | boolean | false | 操作を無効にし、コントロールを淡色表示にします。 |
The create row is a `role="option"`, not a button under the list - in this pattern it is exactly what it looks like: one more thing Enter can land on. A button there would be unreachable by the arrows that got the user to the bottom of the list. It appears on no *exact* match rather than no results at all: type "des" against "design" and you want the filtered list, not an offer to make a near-duplicate one Enter away. It sits last so it can never steal the Enter meant for a real result. The label is quoted - `Create “design system”` - so a trailing space or an odd capital is visible before it becomes a permanent tag.