Tree View <mv-tree-view>

Árvore acessível construída a partir dos seus próprios <ul>/<li> aninhados (que continuam sendo uma lista legível sem JavaScript): suporte completo de teclado APG com busca por digitação, seleção simples, múltipla ou por checkbox com pais mistos, ramos preguiçosos que carregam na primeira abertura com um indicador de carregamento, altura animada, linhas-guia opcionais e ícones de pasta ou arquivo fornecidos pelo host.

CategoriaNavegação
TipoWeb Component (<mv-tree-view>)
Statusestável
Keywordstree, treeview, file-tree, explorer, hierarchy, nested, checkbox-tree, permissions, lazy-loading, folders

When to use

  • A project, cloud drive or document space needs a file explorer with folders that open in place
  • Roles or sharing settings need nested permissions where checking a group checks everything under it
  • A deep hierarchy such as categories, org units or docs chapters must be browsed and picked from
  • Branches are too large to send up front and should load their children only when opened

Avoid when

  • The content is a few sections of text that expand and collapse, not a hierarchy of items → use Accordion instead
  • The entries are commands or actions to run, not items to browse or select → use Menu instead
  • The app needs its main navigation with sections, icons and a collapsible rail → use Sidebar instead

Instalação

node scripts/add.mjs tree-view --out ./src/marvelous

Agente de IA com o servidor MCP do Marvelous UI: install_components({ slugs: ["tree-view"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Arquivos copiados (dependências incluídas): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/focus.js, core/motion.js, components/tree-view/tree-view.js, components/tree-view/tree-view.css.

Uso

Início rápido, a menor marcação que funciona:

<mv-tree-view label="Files"><ul><li>src<ul><li>index.ts</li></ul></li><li>README.md</li></ul></mv-tree-view>

Marcação de referência, para usar como ponto de partida e personalizar com atributos, data-* e variáveis CSS:

<div id="mv-tree-demo" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,20rem),1fr));gap:1.25rem;width:100%;max-width:880px;align-items:start">
  <section style="display:grid;border:1px solid var(--mv-border);border-radius:var(--mv-radius-xl);background:var(--mv-surface);box-shadow:var(--mv-shadow-sm);overflow:hidden">
    <header style="display:flex;align-items:center;gap:.5rem;padding:.625rem .75rem .625rem 1rem;border-bottom:1px solid var(--mv-border)">
      <div style="flex:1;min-width:0">
        <h3 id="mv-tree-demo-files-title" style="margin:0;font-size:var(--mv-text-sm);font-weight:var(--mv-weight-semibold)">lumen-web</h3>
        <p style="margin:0;color:var(--mv-fg-muted);font-size:var(--mv-text-xs)">main · 2 changes</p>
      </div>
      <button class="mv-button" data-variant="ghost" data-size="icon" type="button" id="mv-tree-demo-expand" aria-label="Expand all" title="Expand all" style="width:2rem;height:2rem">
        <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m7 15 5 5 5-5M7 9l5-5 5 5"/></svg>
      </button>
      <button class="mv-button" data-variant="ghost" data-size="icon" type="button" id="mv-tree-demo-collapse" aria-label="Collapse all" title="Collapse all" style="width:2rem;height:2rem">
        <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m7 20 5-5 5 5M7 4l5 5 5-5"/></svg>
      </button>
    </header>
    <mv-tree-view id="mv-tree-demo-files" guides expanded="src, src/components" style="padding:.375rem">
      <template data-icon="folder"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linejoin="round" style="color:var(--mv-accent-fg)"><path d="M3 7a2 2 0 0 1 2-2h4l2 2.5h8a2 2 0 0 1 2 2V18a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z" fill="currentColor" fill-opacity=".14"/></svg></template>
      <template data-icon="folder-open"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linejoin="round" style="color:var(--mv-accent-fg)"><path d="M3 18V7a2 2 0 0 1 2-2h4l2 2.5h6a2 2 0 0 1 2 2V11"/><path d="M3 18l2.6-6.2A1.5 1.5 0 0 1 7 11h13.3a1 1 0 0 1 .9 1.4L19 18.8a2 2 0 0 1-1.9 1.2H5a2 2 0 0 1-2-2Z" fill="currentColor" fill-opacity=".14"/></svg></template>
      <template data-icon="file"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linejoin="round"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8Z"/><path d="M14 3v5h5"/></svg></template>
      <template data-icon="code"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8Z"/><path d="M14 3v5h5M10 12.5 8 14.5l2 2M14 12.5l2 2-2 2"/></svg></template>
      <ul aria-labelledby="mv-tree-demo-files-title">
        <li>.github
          <ul>
            <li>workflows
              <ul><li>ci.yml</li><li>release.yml</li></ul>
            </li>
          </ul>
        </li>
        <li id="mv-tree-demo-modules" data-lazy>node_modules</li>
        <li>public
          <ul><li>favicon.svg</li><li>robots.txt</li></ul>
        </li>
        <li>src
          <ul>
            <li>components
              <ul>
                <li data-icon="code" data-selected>Button.tsx <span class="mv-tree-view-meta" style="color:color-mix(in oklab, var(--mv-warning) 72%, var(--mv-fg))" title="Modified">M</span></li>
                <li data-icon="code" id="mv-tree-demo-picker">DatePicker.tsx <span class="mv-tree-view-meta" style="color:var(--mv-success-fg)" title="Untracked">U</span></li>
                <li data-icon="code">Dialog.tsx</li>
              </ul>
            </li>
            <li>hooks
              <ul><li data-icon="code">useLocale.ts</li><li data-icon="code">useMediaQuery.ts</li></ul>
            </li>
            <li id="mv-tree-demo-lib">lib
              <ul><li data-icon="code">currency.ts</li><li data-icon="code">dates.ts</li></ul>
            </li>
            <li data-icon="code">App.tsx</li>
          </ul>
        </li>
        <li>package.json</li>
        <li>README.md</li>
      </ul>
    </mv-tree-view>
    <p id="mv-tree-demo-path" style="margin:0;padding:.625rem 1rem;border-top:1px solid var(--mv-border);color:var(--mv-fg-muted);font:var(--mv-text-xs)/1.4 var(--mv-font-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">src/components/Button.tsx</p>
  </section>

  <section style="display:grid;border:1px solid var(--mv-border);border-radius:var(--mv-radius-xl);background:var(--mv-surface);box-shadow:var(--mv-shadow-sm);overflow:hidden">
    <header style="padding:.75rem 1rem;border-bottom:1px solid var(--mv-border)">
      <h3 id="mv-tree-demo-perms-title" style="margin:0;font-size:var(--mv-text-sm);font-weight:var(--mv-weight-semibold)">Editor role</h3>
      <p style="margin:0;color:var(--mv-fg-muted);font-size:var(--mv-text-xs)">Choose what editors can do in the Northwind workspace.</p>
    </header>
    <mv-tree-view id="mv-tree-demo-perms" selection="checkbox" style="padding:.375rem">
      <ul aria-labelledby="mv-tree-demo-perms-title">
        <li data-id="projects" data-expanded>Projects
          <ul>
            <li data-id="projects.view" data-checked>View projects</li>
            <li data-id="projects.create" data-checked>Create projects</li>
            <li data-id="projects.archive">Archive projects</li>
            <li data-id="projects.delete" data-disabled>Delete projects <span class="mv-tree-view-meta">Owner only</span></li>
          </ul>
        </li>
        <li data-id="members" data-expanded>Members
          <ul>
            <li data-id="members.invite" data-checked>Invite members</li>
            <li data-id="members.roles">Change roles</li>
            <li data-id="members.remove">Remove members</li>
          </ul>
        </li>
        <li data-id="billing" id="mv-tree-demo-billing">Billing
          <ul>
            <li data-id="billing.invoices">View invoices</li>
            <li data-id="billing.payment">Update payment method</li>
            <li data-id="billing.receipts">Download receipts</li>
          </ul>
        </li>
        <li data-id="api" data-checked>API access
          <ul>
            <li data-id="api.read">Read API keys</li>
            <li data-id="api.create">Create API keys</li>
            <li data-id="api.revoke">Revoke API keys</li>
          </ul>
        </li>
      </ul>
    </mv-tree-view>
    <p id="mv-tree-demo-count" style="margin:0;padding:.625rem 1rem;border-top:1px solid var(--mv-border);color:var(--mv-fg-muted);font-size:var(--mv-text-xs)"></p>
  </section>
</div>

<script type="module">
  const files = document.getElementById("mv-tree-demo-files");
  const perms = document.getElementById("mv-tree-demo-perms");
  const path = document.getElementById("mv-tree-demo-path");
  const count = document.getElementById("mv-tree-demo-count");

  document.getElementById("mv-tree-demo-expand").addEventListener("click", () => files.expandAll());
  document.getElementById("mv-tree-demo-collapse").addEventListener("click", () => files.collapseAll());
  files.addEventListener("mv-select", (e) => { path.textContent = e.detail.value[0] ?? "No file selected"; });

  // Lazy folders: children arrive a moment after the first opening, as if read from disk.
  const packages = {
    node_modules: [["@lumen", true], ["date-fns", true], ["react", true], ["typescript", true]],
    "node_modules/@lumen": [["tokens", true], ["ui", true]],
  };
  const leaves = ["package.json", "README.md", "index.js"];
  files.addEventListener("mv-load", (e) => {
    const { id, item } = e.detail;
    setTimeout(() => {
      const list = document.createElement("ul");
      for (const [name, lazy] of packages[id] ?? leaves.map((n) => [n, false])) {
        const li = document.createElement("li");
        li.textContent = name;
        if (lazy) li.dataset.lazy = "";
        list.append(li);
      }
      item.append(list);
    }, 900);
  });

  const leafCount = [...perms.querySelectorAll("li")].filter((li) => !li.querySelector("li")).length;
  const format = new Intl.NumberFormat("en-US");
  const show = () => {
    const granted = perms.value.filter((id) => id.includes(".")).length;
    count.textContent = `${format.format(granted)} of ${format.format(leafCount)} permissions granted`;
  };
  perms.addEventListener("mv-select", show);
  customElements.whenDefined("mv-tree-view").then(() => requestAnimationFrame(show));
</script>

API

Attributes

NameTipoDefaultDescription
selectionnone | single | multiple | checkboxsinglenone: browse only (a click on a branch toggles it). single: one selected item (aria-selected). multiple: click or Space toggles, Shift extends, Ctrl/Cmd+A selects all. checkbox: aria-checked on every item; checking a branch checks its descendants and parents show mixed.
expandedstringComma-separated ids of the branches open on load, or "all". Changing it later opens exactly those branches.
labelstringAccessible name of the tree (or put aria-label / aria-labelledby on the top <ul>).
guidesbooleanfalseVertical guide lines under each open branch; the line of the group holding the selection or focus is stronger.
iconsbooleanfalseDraw a simple folder (open or closed) or file icon on items that have none. Not needed when you provide templates.
data-idstringOn an <li>: id used by value, expand(), collapse(), the expanded attribute and events. Without it, the id is the path of labels joined by / (e.g. src/components/Button.tsx).
data-expandedbooleanOn an <li>: open on load.
data-selected / data-checkedbooleanOn an <li>: selected, or checked with all its descendants, on load (also read on items added later).
data-disabledbooleanOn an <li>: cannot be selected or checked, skipped by arrow keys.
data-lazybooleanOn an <li>: a branch whose children are not there yet. The first opening shows a spinner and emits mv-load; append an <ul> of <li> (or <li> into its list) and the loading ends. Removing data-lazy without adding children shows the empty note.
data-iconstringOn an <li>: name of a <template data-icon> to use for its icon.

Properties

NameTipoDescription
valuestring[]Ids of the selected items (checked items in checkbox mode, fully checked branches included), in tree order. Assign an array or a comma-separated string to change it. Can be set before the element is defined.
stringsobjectOverride any default text: loading, empty, loadingItem ({label}), loadedItem ({ one, other } with {label} and {n}), emptyItem ({label}). Numbers and plurals follow the nearest lang (Intl). Can be set before the element is defined.

Methods

NameDescription
expand(id)Open an item and all its ancestors so it becomes visible. Returns false for an unknown id.
collapse(id)Close an item. Returns false for an unknown id.
expandAll()Open every branch whose children are present (lazy branches stay closed, so nothing is fetched).
collapseAll()Close every branch; focus moves up to the nearest visible item.
focus()Focus the tree's current item (its single tab stop).

Events

NameDescription
mv-selectSelection or check changed. detail: { value, id, item, selected } or { value, id, item, checked } in checkbox mode.
mv-toggleA branch opened or closed. detail: { id, item, expanded }.
mv-loadA data-lazy branch was opened for the first time. detail: { id, item }. Append its children to item.

Content structure

NameDescription
ul > liOne item: its text (or a single <span> / <a href>) is the label, a nested <ul> or <ol> holds its children.
template[data-icon]Direct child of mv-tree-view: icon cloned into items. folder (and folder-open), file, or any name referenced by data-icon.
.mv-tree-view-iconAn element (or a leading <svg> / <img>) in an <li> used as that item's own icon.
.mv-tree-view-metaTrailing text in an <li>, aligned to the end: size, status letter, badge.

CSS classes

NameDescription
mv-tree-view-rowGenerated row of an item: chevron, checkbox, icon, label, meta.
mv-tree-view-labelThe item's label (ellipsis when too long).
mv-tree-view-groupGenerated on each nested list (role="group").

CSS variables

NameDefaultDescription
--mv-tree-view-indent1.25remIndentation per level.
--mv-tree-view-row-height2remMinimum row height.
--mv-tree-view-paddingvar(--mv-space-2)Inline padding of rows.
--mv-tree-view-durationvar(--mv-duration-normal)Base duration of the open and close animation.

Accessibility

APG Tree View: role tree on the top list, treeitem on each <li> (named by its row only, not its children), group on nested lists, aria-level, aria-expanded, aria-selected (aria-multiselectable in multiple mode) or aria-checked true/false/mixed in checkbox mode, aria-disabled, aria-busy while a branch loads. One tab stop (roving tabindex): Up/Down move through visible items, Right opens then goes to the first child, Left closes then goes to the parent (mirrored in RTL), Home/End, typeahead on labels, * opens all siblings, Enter/Space select or check (Enter also follows a link label), Shift+arrows and Shift+Space extend a multiple selection. Loading and loaded counts are announced in a polite live region. Collapsed children are hidden from assistive tech. Reduced motion: instant open, static spinner. Forced colors: system colors for selection, checkboxes, chevrons and guides.

Esta página foi traduzida com IA. Informar um problema de tradução