多级列表的语义正确层次结构是什么?

What is semantically correct hierarchy for multi-level lists?

我将有 2 个列表:带有单选按钮和复选框(多级) 我目前的 html 是:

<label>
<input type="radio" name="group" checked={checked} onChange={()=>callback(value)}/>
{label}
</label>

<label>
<input type="radio" name="subgroup" checked={checked} onChange={()=>callback(value)}/>
{label}
</label>
<label>
<input type="radio" name="subgroup" checked={checked} onChange={()=>callback(value)}/>
{label}
</label>

我的 subgroup 没有放在第一个标签内可以吗?我见过很多不同的例子,但我想知道哪个在语义上是正确的?

回答你的第一个问题,<label> 元素一次只能标记一个表单元素,所以你不应该在一个 <label>.[=22= 中放置多个表单元素。 ]

对于无线电组,由于您只能 select 所有 sub-item 中的一个 sub-item,因此最语义化和最易于理解的方式是将其作为<select> 个元素,每个 top-level 个分组有 <optgroup> 个元素,每个 sub-item 个分组有 <option> 个元素。这就是这个元素的用途。

<label for="dino-select">Choose a dinosaur:</label>
<select id="dino-select">
    <optgroup label="Theropods">
        <option>Tyrannosaurus</option>
        <option>Velociraptor</option>
        <option>Deinonychus</option>
    </optgroup>
    <optgroup label="Sauropods">
        <option>Diplodocus</option>
        <option>Saltasaurus</option>
        <option>Apatosaurus</option>
    </optgroup>
</select>

对于需要能够select多个东西的情况,可以添加multiple属性:

<label for="dino-select">Choose one or more dinosaurs:</label>
<select id="dino-select" multiple>
    <optgroup label="Theropods">
        <option>Tyrannosaurus</option>
        <option>Velociraptor</option>
        <option>Deinonychus</option>
    </optgroup>
    <optgroup label="Sauropods">
        <option>Diplodocus</option>
        <option>Saltasaurus</option>
        <option>Apatosaurus</option>
    </optgroup>
</select>

如果您不喜欢使用 <select multiple /> 元素(有些用户不知道如何使用它们,因为它们不常见),您可以使用嵌套的复选框列表。

务必以可访问的方式在语义上表示哪些项目属于其他项目,并包含 javascript 以添加必要的功能。

关于预期行为的注释:

  • 如果一个父项被 selected,所有子项都应该被 selected
  • 如果所有子项都变成 selected,则父项应该变成 selected
  • 如果所有子项都被删除select,父项也应该被删除select
  • 如果父项的某些子项被 select 编辑而其他子项未被编辑,则父项应处于不确定状态

这是一种方法:

const setInputState = (el, state) => {
  if (state === 'indeterminate') {
    el.indeterminate = true
  } else {
    el.indeterminate = false
    el.checked = state 
  }
}

const updateOwned = (el) => {
  if (el.hasAttribute('data-children')) {
    let state = el.checked
    el.getAttribute('data-children').split(' ').forEach(id => {
      let owned = document.getElementById(id)
      setInputState(owned, state)
      updateOwned(owned)
    })
  }
}

const updateOwner = (el) => {
  if (el.hasAttribute('data-parent')) {
    let owner = document.getElementById(el.getAttribute('data-parent'))
    let states = []
    let collectiveState
    owner.getAttribute('data-children').split(' ').every(id => {
      let owned = document.getElementById(id)
      let state = owned.indeterminate === true ? 'indeterminate' : owned.checked
      if (states.length > 0 && states.indexOf(state) === -1) {
        collectiveState = 'indeterminate'
        return false
      } else {
        states.push(state)
        return true
      }
    })
    collectiveState = collectiveState || states[0]
    setInputState(owner, collectiveState)
    updateOwner(owner)
  }
}

document.querySelectorAll('.nested-multiselect').forEach(multiselect => {
  multiselect.querySelectorAll('input[type="checkbox"][data-children], input[type="checkbox"][data-parent]').forEach(input => {
    input.addEventListener('change', event => {
      updateOwned(event.currentTarget)
      updateOwner(event.currentTarget)
    })
  })
})
body {
  padding: 2rem;
}
label {
  display: block;
}
label span:before {
  content: ' ';
}
fieldset fieldset {
  border: none;
  padding: 0 0 0 1ch;
}
<fieldset class="nested-multiselect">
  <legend>Categories </legend>
  <label id="label-fruit">
    <input id="fruit" type="checkbox" name="categories" value="fruit" aria-owns="subcategories-fruit" data-children="apple orange banana"/><span>fruit</span>
  </label>
  <fieldset id="subcategories-fruit" aria-label="fruit subcategories">
    <label id="label-apple">
      <input id="apple" type="checkbox" name="categories" value="apple" aria-owns="subcategories-apple" data-parent="fruit" data-children="gala macintosh honeycrisp"/><span>apple</span>
    </label>
    <fieldset id="subcategories-apple" aria-label="apple subcategories">
      <label id="label-gala">
        <input id="gala" type="checkbox" name="categories" value="gala" data-parent="apple"/><span>gala</span>
      </label>
      <label id="label-macintosh">
        <input id="macintosh" type="checkbox" name="categories" value="macintosh" data-parent="apple"/><span>macintosh</span>
      </label>
      <label id="label-honeycrisp">
        <input id="honeycrisp" type="checkbox" name="categories" value="honeycrisp" data-parent="apple"/><span>honeycrisp</span>
      </label>
    </fieldset>
    <label id="label-orange">
      <input id="orange" type="checkbox" name="categories" value="orange" data-parent="fruit"/><span>orange</span>
    </label>
    <label id="label-banana">
      <input id="banana" type="checkbox" name="categories" value="banana" data-parent="fruit"/><span>banana</span>
    </label>
  </fieldset>
  <label id="label-vegetables">
    <input id="vegetables" type="checkbox" name="categories" value="vegetables" aria-owns="subcategories-vegetables" data-children="squash peas leek"/><span>vegetables</span>
  </label>
  <fieldset id="subcategories-vegetables" aria-label="vegetables subcategories">
    <label id="label-squash">
      <input id="squash" type="checkbox" name="categories" value="squash" data-parent="vegetables"/><span>squash</span>
    </label>
    <label id="label-peas">
      <input id="peas" type="checkbox" name="categories" value="peas" data-parent="vegetables"/><span>peas</span>
    </label>
    <label id="label-leek">
      <input id="leek" type="checkbox" name="categories" value="leek" data-parent="vegetables"/><span>leek</span>
    </label>
  </fieldset>
</fieldset>