Перейти к содержимому
EN
Масштаб
Плотность
Тема
Акцент

Inputs

Cascader

JS The arrows along a column and between the columns are done by `instrument.js`

Эта страница ещё не переведена — ниже текст на базовом языке справочника. Справочник API от этого не меняется: имена классов, токенов и атрибутов одинаковы в обоих языках.

Choosing one leaf of a hierarchy, level by level. The columns turn the depth into width: three levels of forty items are three columns that scroll separately rather than a hundred and twenty rows.

Компонент
Разметка
<span class="inst-select-wrap">
  <button class="inst-select" type="button" popovertarget="cas-region"
          aria-haspopup="listbox" aria-label="Region">
    <span class="inst-cascader-path">
      <span>Europe</span><span>Germany</span><span>Berlin</span>
    </span>
  </button>
</span>

<div class="inst-popover inst-popover--anchored" id="cas-region" popover>
  <div class="inst-cascader">
    <div class="inst-cascader-col" role="listbox" aria-label="Part of the world">
      <button class="inst-menu-item inst-cascader-item" type="button" role="option"
              aria-selected="true" aria-expanded="true" tabindex="0">Europe</button>
      <button class="inst-menu-item inst-cascader-item" type="button" role="option"
              aria-selected="false" aria-expanded="false" tabindex="-1">Asia</button>
      <button class="inst-menu-item inst-cascader-item" type="button" role="option"
              aria-selected="false" aria-expanded="false" tabindex="-1">Africa</button>
    </div>
    <div class="inst-cascader-col" role="listbox" aria-label="Country">
      <button class="inst-menu-item inst-cascader-item" type="button" role="option"
              aria-selected="true" aria-expanded="true" tabindex="0">Germany</button>
      <button class="inst-menu-item inst-cascader-item" type="button" role="option"
              aria-selected="false" aria-expanded="false" tabindex="-1">France</button>
      <button class="inst-menu-item inst-cascader-item" type="button" role="option"
              aria-selected="false" aria-expanded="false" tabindex="-1">Portugal</button>
    </div>
    <div class="inst-cascader-col" role="listbox" aria-label="City">
      <button class="inst-menu-item inst-cascader-item" type="button" role="option"
              aria-selected="true" tabindex="0">Berlin</button>
      <button class="inst-menu-item inst-cascader-item" type="button" role="option"
              aria-selected="false" tabindex="-1">Hamburg</button>
      <button class="inst-menu-item inst-cascader-item" type="button" role="option"
              aria-selected="false" tabindex="-1">Munich</button>
    </div>
  </div>
</div>

Контракт

The kit draws the columns, the application fills them. What stands in the column after this one is the children of the chosen node, that is data. No selector computes it, and a kit that pretended to would be holding a copy of somebody else's tree.

Above four levels the component stops working, and that is a property of it rather than a matter of taste: four columns of 11rem is 44rem of horizontal room, which a narrow screen does not have. There the case is a search over the leaves rather than a walk down the levels.

What Required Why
A role="listbox" on every column with an aria-label yes Each level is a group of its own with a name of its own: "Country" rather than a second nameless list
A role="option" on every item yes The carrier of the keyboard contract. Without it the arrows do nothing
An aria-expanded on an item with children yes The chevron follows it. A leaf carries no attribute, and the absence of a chevron is what says the road ends here
An aria-selected on what lies on the path yes In the last column that is the chosen value; in the ones before it, the road to it
A roving tabindex in every column yes, the initial one 0 on the selected one, -1 on the rest. One Tab stop per column
A type="button" yes Otherwise inside a form the item will submit it
The trigger is a <button class="inst-select"> no, but usually .inst-select is worn by a button when the native list is not enough. A class of its own would be a second field look

Accessibility

The name of a column An aria-label on the column: "Part of the world", "Country", "City". Three lists with no names are read as three lists
Traversal Tab enters a column and goes to the next one. ↑ ↓ along a column, ← → between the columns, Home and End to the ends
Selection Follows the focus, as in any single-select listbox. Walking a column with the arrows chooses as it goes — that is the promise of the role rather than a decision of this component
The mark on the path A surface plus a colour rather than colour alone: --surface-selected with --accent-text, the same carrier as a selected row of a queue
A branch and a leaf Told apart by the chevron, and the chevron by aria-expanded. By ear the same difference is "expanded" against nothing
The tap target --control-h-sm plus the gap between neighbours. Checked by cmd/targets
Scrolling Per column. One scrollbar over all three would move six regions along with two hundred cities

Устройство

The part What it does
.inst-cascader The row of columns. A ceiling of ten rows in height, counted in --control-h-sm rather than in pixels, so it rides density
.inst-cascader-col One level. 11rem wide — about twenty characters at the base scale; below that the column becomes a strip of ellipses, and choosing from ellipses is not choosing
.inst-cascader-item Worn together with .inst-menu-item: the height, the radius and the hover are already right there, and a second set of them would drift
.inst-cascader-path The path in the trigger. The separator is the chevron of breadcrumbs — both say "this lies inside that", and one shape per meaning is the kit's icon rule

The seam between columns is a hairline, the same one the shell puts between its regions. That is not the dividing line the kit refuses inside a menu: that one separates items of one list, this one separates lists — and without it three columns read as one list that has wrapped.

The scrollbar gutter is reserved rather than left to appear: a column that gains one on the fortieth item shifts the two beside it at the moment of a click.

Состояния

Пример
Разметка
<div class="inst-cascader" style="max-block-size:none">
  <div class="inst-cascader-col" role="listbox" aria-label="States">
    <button class="inst-menu-item inst-cascader-item" type="button" role="option"
            aria-selected="true" aria-expanded="true" tabindex="0">On the path</button>
    <button class="inst-menu-item inst-cascader-item" type="button" role="option"
            aria-selected="false" aria-expanded="false" tabindex="-1">A branch</button>
    <button class="inst-menu-item inst-cascader-item" type="button" role="option"
            aria-selected="false" tabindex="-1">A leaf</button>
    <button class="inst-menu-item inst-cascader-item" type="button" role="option"
            aria-selected="false" aria-disabled="true" tabindex="-1">Unavailable</button>
  </div>
</div>
State How it is set What happens
on the path aria-selected="true" --surface-selected and --accent-text. In the last column the same mark is the chosen value
a branch aria-expanded A chevron at the far edge. true and false are drawn alike: whether the column beside is open is said by the column beside
a leaf no aria-expanded No chevron. The absence is what says the road ends
unavailable aria-disabled="true" Transparency. The arrow still walks to it — which item is unavailable matters — and it cannot be acted on

JS

The module is included once per page — there is no need to initialise the components one by one.

What instrument.js does

A column is an ordinary listbox, and everything a listbox promises it gets for free: one Tab stop, ↑ ↓ along it, Home and End, selection following focus.

What a listbox has no notion of is the column beside it, and there the cross-axis arrows are free: in a vertical group ← and → mean nothing at all. So ← and → move between the columns — onto whatever the next column already has selected, or onto its first item.

That behaviour is scoped by class rather than by role, and the distinction is the point. Teaching every listbox in the kit to walk sideways would be changing a role's promise for the sake of one component. A listbox that happens to stand inside .inst-cascader gains a behaviour of the cascader instead, and a listbox anywhere else answers the arrows exactly as it did.

What the application does

cascader.addEventListener('inst:select', (e) => {
  const col = e.target.closest('.inst-cascader-col');
  const level = [...cascader.children].indexOf(col);
  render(level + 1, childrenOf(e.detail.value));   // the column after it
});

One handler. The kit sets aria-selected, moves the roving tabindex and sends the event; what the next column contains is known to the application alone.

What instrument.js does not do

It does not open the popover — that is the platform's popovertarget — and it does not write the path into the trigger. The path is the value of the field, and the value belongs to the application.

API

Классы
ИмяЧто делает
class
inst-cascaderThe row of columns. Stands inside an .inst-popover
inst-cascader-colOne level. A role="listbox" with scrolling of its own
inst-cascader-itemAn item of a level. Worn together with .inst-menu-item
inst-cascader-pathThe chosen path inside the trigger. The separator is a chevron
Атрибуты
ИмяЗначениеЧто делает
attribute
aria-expandedtrue · falseOn an item that HAS CHILDREN. The chevron follows it. A leaf carries no attribute
aria-selectedtrue · falseOn an item that lies on the path. In the last column that is the chosen value
event
inst:select—Bubbles from an item of a column. { value } — the application renders the column after it
Токены
ИмяЗначениеОткуда
token
--control-h-sm24pxбаза +14: плотность · масштаб
--size-chevron10pxбаза +4: масштаб
--space-12pxодно на все ячейки
--space-24pxодно на все ячейки
--hairline1pxбаза +1: экран
--border-subtlelight-dark(oklch(0 0 0 / 0.07), oklch(1 0 0 / 0.06))одно на все ячейки
--surface-selectedcolor-mix(in oklab, var(--a-4) 14%, transparent)одно на все ячейки
--accent-textlight-dark(var(--a-5), var(--a-3))база +1: тема
--accent-marklight-dark(var(--a-4), var(--a-3))одно на все ячейки
--text-mutedlight-dark(var(--n-8), var(--n-6))одно на все ячейки
--text-faintlight-dark(var(--n-7), var(--n-7))одно на все ячейки
--text-primarylight-dark(var(--n-12), var(--n-1))одно на все ячейки

Почему кит устроен именно так — конституция · Открытый код под MIT, github.com/keshon/instrument