Spec-Zone.ru › Angular

Дерево

Шаблон ARIA для дерева Справочник API дерева

Обзор

Дерево отображает иерархические данные, элементы которых можно раскрывать, чтобы показать дочерние элементы, или сворачивать, чтобы скрыть их. Пользователи перемещаются с помощью клавиш со стрелками, раскрывают и сворачивают узлы и при необходимости выбирают элементы для навигации или работы с данными.

import {Component, signal} from '@angular/core';
import {NgTemplateOutlet} from '@angular/common';
import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';

type TreeNode = {
  name: string;
  value: string;
  children?: TreeNode[];
  disabled?: boolean;
  expanded?: boolean;
};

@Component({
  selector: 'app-root',
  templateUrl: 'app.html',
  styleUrl: 'app.css',
  imports: [Tree, TreeItem, TreeItemGroup, NgTemplateOutlet],
})
export class App {
  readonly nodes: TreeNode[] = [
    {
      name: 'public',
      value: 'public',
      children: [
        {name: 'index.html', value: 'public/index.html'},
        {name: 'favicon.ico', value: 'public/favicon.ico'},
        {name: 'styles.css', value: 'public/styles.css'},
      ],
      expanded: true,
    },
    {
      name: 'src',
      value: 'src',
      children: [
        {
          name: 'app',
          value: 'src/app',
          children: [
            {name: 'app.ts', value: 'src/app/app.ts'},
            {name: 'app.html', value: 'src/app/app.html'},
            {name: 'app.css', value: 'src/app/app.css'},
          ],
          expanded: false,
        },
        {
          name: 'assets',
          value: 'src/assets',
          children: [{name: 'logo.png', value: 'src/assets/logo.png'}],
          expanded: false,
        },
        {
          name: 'environments',
          value: 'src/environments',
          children: [
            {
              name: 'environment.prod.ts',
              value: 'src/environments/environment.prod.ts',
              expanded: false,
            },
            {name: 'environment.ts', value: 'src/environments/environment.ts'},
          ],
          expanded: false,
        },
        {name: 'main.ts', value: 'src/main.ts'},
        {name: 'polyfills.ts', value: 'src/polyfills.ts'},
        {name: 'styles.css', value: 'src/styles.css', disabled: true},
        {name: 'test.ts', value: 'src/test.ts'},
      ],
      expanded: false,
    },
    {name: 'angular.json', value: 'angular.json'},
    {name: 'package.json', value: 'package.json'},
    {name: 'README.md', value: 'README.md'},
  ];

  readonly selected = signal(['angular.json']);
}
<ul ngTree #tree="ngTree" [(value)]="selected" class="basic-tree">
  <ng-template
    [ngTemplateOutlet]="treeNodes"
    [ngTemplateOutletContext]="{nodes: nodes, parent: tree}"
  />
</ul>

<ng-template #treeNodes let-nodes="nodes" let-parent="parent">
  @for (node of nodes; track node.value) {
    <li
      ngTreeItem
      [parent]="parent"
      [value]="node.value"
      [label]="node.name"
      [disabled]="node.disabled"
      [(expanded)]="node.expanded"
      #treeItem="ngTreeItem"
    >
      <span aria-hidden="true" class="material-symbols-outlined expand-icon" translate="no">{{
        node.children ? 'chevron_right' : ''
      }}</span>
      <span aria-hidden="true" class="material-symbols-outlined" translate="no">{{
        node.children ? 'folder' : 'docs'
      }}</span>
      {{ node.name }}
      <span aria-hidden="true" class="material-symbols-outlined selected-icon" translate="no"
        >check</span
      >
    </li>

    @if (node.children) {
      <ul role="group">
        <ng-template ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup">
          <ng-template
            [ngTemplateOutlet]="treeNodes"
            [ngTemplateOutletContext]="{nodes: node.children, parent: group}"
          />
        </ng-template>
      </ul>
    }
  }
</ng-template>
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');

:host {
  display: flex;
  justify-content: center;
  user-select: none;
  font-family: var(--inter-font);
}

[ngTree] {
  min-width: 24rem;
  background-color: var(--septenary-contrast);
  border-radius: 0.5rem;
  padding: 0.5rem;
}

[ngTreeItem] {
  cursor: pointer;
  list-style: none;
  text-decoration: none;
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 0.3rem 1rem;
}

[ngTreeItem][aria-disabled='true'] {
  opacity: 0.5;
  cursor: default;
}

[ngTreeItem]:focus,
[ngTreeItem]:hover {
  background-color: var(--quinary-contrast);
}

[ngTreeItem]:focus {
  outline: 1px solid color-mix(in srgb, var(--hot-pink) 60%, transparent);
}

[ngTreeItem][aria-selected='true'],
[ngTreeItem][aria-selected='true'] .expand-icon {
  background-image: var(--pink-to-purple-horizontal-gradient);
  background-clip: text;
  color: transparent;
}

.material-symbols-outlined {
  margin: 0;
  width: 24px;
}

.expand-icon {
  transition: transform 0.2s ease;
}

[ngTreeItem][aria-expanded='true'] .expand-icon {
  transform: rotate(90deg);
}

.selected-icon {
  visibility: hidden;
  margin-left: auto;
}

[ngTreeItem][aria-current] .selected-icon,
[ngTreeItem][aria-selected='true'] .selected-icon {
  visibility: visible;
}

li[aria-expanded='false'] + ul[role='group'] {
  display: none;
}

Использование

Деревья хорошо подходят для отображения иерархических данных, по которым пользователям нужно перемещаться во вложенных структурах.

Используйте деревья, когда нужно:

  • Создать навигацию по файловой системе
  • Показать иерархию папок и документов
  • Создать вложенную структуру меню
  • Отобразить организационную диаграмму
  • Просматривать иерархические данные
  • Реализовать навигацию по сайту с вложенными разделами

Не используйте деревья, когда нужно:

  • Отобразить плоские списки (вместо этого используйте Listbox)
  • Показать таблицы данных (вместо этого используйте Grid)
  • Создать простые раскрывающиеся списки (вместо этого используйте Select)
  • Создать навигацию в виде цепочки навигационных ссылок (используйте соответствующие шаблоны)

Возможности

  • Иерархическая навигация — вложенная структура дерева с возможностью раскрытия и сворачивания
  • Режимы выбора — выбор одного или нескольких элементов с явным подтверждением либо при перемещении фокуса
  • Выбор следует за фокусом — автоматический выбор при изменении фокуса (необязательно)
  • Навигация с клавиатуры — клавиши со стрелками, Home, End и поиск по вводимым символам
  • Раскрытие и сворачивание — клавиши со стрелками вправо/влево или Enter для переключения состояния родительских узлов
  • Отключённые элементы — отключение отдельных узлов с управлением фокусом
  • Режимы фокуса — стратегии фокуса roving tabindex или activedescendant
  • Поддержка RTL — навигация для языков с письмом справа налево

Примеры

Дерево навигации

Используйте дерево для навигации, если нажатие на элементы выполняет действия, а не выбирает их.

import {Component, signal} from '@angular/core';
import {NgTemplateOutlet} from '@angular/common';
import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';

type TreeNode = {
  name: string;
  value: string;
  icon: string;
  children?: TreeNode[];
  disabled?: boolean;
  expanded?: boolean;
};

@Component({
  selector: 'app-root',
  templateUrl: 'app.html',
  styleUrl: 'app.css',
  imports: [Tree, TreeItem, TreeItemGroup, NgTemplateOutlet],
})
export class App {
  readonly nodes: TreeNode[] = [
    {
      name: 'Inbox',
      value: 'inbox',
      icon: 'inbox',
    },
    {
      name: 'Sent',
      value: 'sent',
      icon: 'send',
    },
    {
      name: 'Drafts',
      value: 'drafts',
      icon: 'draft',
    },
    {
      name: 'Spam',
      value: 'spam',
      icon: 'report',
    },
    {
      name: 'Trash',
      value: 'trash',
      icon: 'delete',
    },
    {
      name: 'Labels',
      value: 'labels',
      expanded: true,
      icon: 'label',
      children: [
        {name: 'Personal', value: 'folders/personal', icon: 'label'},
        {name: 'Work', value: 'folders/work', icon: 'label'},
        {name: 'Travel', value: 'folders/travel', icon: 'label'},
        {name: 'Receipts', value: 'folders/receipts', icon: 'label'},
      ],
    },
  ];

  readonly selected = signal(['inbox']);
}
<ul ngTree #tree="ngTree" [nav]="true" [(value)]="selected" class="basic-tree">
  <ng-template
    [ngTemplateOutlet]="treeNodes"
    [ngTemplateOutletContext]="{nodes: nodes, parent: tree}"
  />
</ul>

<ng-template #treeNodes let-nodes="nodes" let-parent="parent">
  @for (node of nodes; track node.value) {
    <a
      ngTreeItem
      [parent]="parent"
      [value]="node.value"
      [label]="node.name"
      [disabled]="node.disabled"
      [selectable]="!node.children"
      [(expanded)]="node.expanded"
      #treeItem="ngTreeItem"
      href="#{{ node.name }}"
      (click)="$event.preventDefault()"
    >
      <span
        aria-hidden="true"
        class="material-symbols-outlined"
        translate="no"
        aria-hidden="true"
        >{{ node.icon }}</span
      >
      {{ node.name }}
      <span aria-hidden="true" class="material-symbols-outlined expand-icon" translate="no">{{
        node.children ? 'keyboard_arrow_up' : ''
      }}</span>
    </a>

    @if (node.children) {
      <ul role="group">
        <ng-template ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup">
          <ng-template
            [ngTemplateOutlet]="treeNodes"
            [ngTemplateOutletContext]="{nodes: node.children, parent: group}"
          />
        </ng-template>
      </ul>
    }
  }
</ng-template>
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');

:host {
  display: flex;
  justify-content: center;
  user-select: none;
  font-family: var(--inter-font);
}

[ngTree] {
  min-width: 24rem;
  background-color: var(--septenary-contrast);
  border-radius: 0.5rem;
  padding: 0.5rem;
}

[ngTreeItem] {
  cursor: pointer;
  list-style: none;
  text-decoration: none;
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 0.3rem 1rem;
  color: var(--primary-contrast);
}

[ngTreeItem][aria-disabled='true'] {
  opacity: 0.5;
  cursor: default;
}

[ngTreeItem]:focus,
[ngTreeItem]:hover {
  background-color: var(--quinary-contrast);
}

[ngTreeItem]:focus {
  outline: 1px solid color-mix(in srgb, var(--hot-pink) 60%, transparent);
}

[ngTreeItem][aria-current] {
  background-image: var(--pink-to-purple-horizontal-gradient);
  background-clip: text;
  color: transparent;
}

.material-symbols-outlined {
  margin: 0;
  width: 24px;
}

.expand-icon {
  transition: transform 0.2s ease;
}

[ngTreeItem][aria-expanded='true'] .expand-icon {
  transform: rotate(180deg);
}

li[aria-expanded='false'] + ul[role='group'] {
  display: none;
}

Установите [nav]="true", чтобы включить режим навигации. В этом режиме для обозначения текущей страницы используется aria-current, а не выбор элемента.

Выбор одного элемента

Включите выбор одного элемента, если пользователям нужно выбрать один элемент в дереве.

import {Component, signal} from '@angular/core';
import {NgTemplateOutlet} from '@angular/common';
import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';

type TreeNode = {
  name: string;
  value: string;
  children?: TreeNode[];
  disabled?: boolean;
  expanded?: boolean;
};

@Component({
  selector: 'app-root',
  templateUrl: 'app.html',
  styleUrl: 'app.css',
  imports: [Tree, TreeItem, TreeItemGroup, NgTemplateOutlet],
})
export class App {
  readonly nodes: TreeNode[] = [
    {
      name: 'public',
      value: 'public',
      children: [
        {name: 'index.html', value: 'public/index.html'},
        {name: 'favicon.ico', value: 'public/favicon.ico'},
        {name: 'styles.css', value: 'public/styles.css'},
      ],
      expanded: true,
    },
    {
      name: 'src',
      value: 'src',
      children: [
        {
          name: 'app',
          value: 'src/app',
          children: [
            {name: 'app.ts', value: 'src/app/app.ts'},
            {name: 'app.html', value: 'src/app/app.html'},
            {name: 'app.css', value: 'src/app/app.css'},
          ],
          expanded: false,
        },
        {
          name: 'assets',
          value: 'src/assets',
          children: [{name: 'logo.png', value: 'src/assets/logo.png'}],
          expanded: false,
        },
        {
          name: 'environments',
          value: 'src/environments',
          children: [
            {
              name: 'environment.prod.ts',
              value: 'src/environments/environment.prod.ts',
              expanded: false,
            },
            {name: 'environment.ts', value: 'src/environments/environment.ts'},
          ],
          expanded: false,
        },
        {name: 'main.ts', value: 'src/main.ts'},
        {name: 'polyfills.ts', value: 'src/polyfills.ts'},
        {name: 'styles.css', value: 'src/styles.css', disabled: true},
        {name: 'test.ts', value: 'src/test.ts'},
      ],
      expanded: false,
    },
    {name: 'angular.json', value: 'angular.json'},
    {name: 'package.json', value: 'package.json'},
    {name: 'README.md', value: 'README.md'},
  ];

  readonly selected = signal(['angular.json']);
}
<ul ngTree #tree="ngTree" [(value)]="selected" class="basic-tree">
  <ng-template
    [ngTemplateOutlet]="treeNodes"
    [ngTemplateOutletContext]="{nodes: nodes, parent: tree}"
  />
</ul>

<ng-template #treeNodes let-nodes="nodes" let-parent="parent">
  @for (node of nodes; track node.value) {
    <li
      ngTreeItem
      [parent]="parent"
      [value]="node.value"
      [label]="node.name"
      [disabled]="node.disabled"
      [(expanded)]="node.expanded"
      #treeItem="ngTreeItem"
    >
      <span aria-hidden="true" class="material-symbols-outlined expand-icon" translate="no">{{
        node.children ? 'chevron_right' : ''
      }}</span>
      <span aria-hidden="true" class="material-symbols-outlined" translate="no">{{
        node.children ? 'folder' : 'docs'
      }}</span>
      {{ node.name }}
      <span aria-hidden="true" class="material-symbols-outlined selected-icon" translate="no"
        >check</span
      >
    </li>

    @if (node.children) {
      <ul role="group">
        <ng-template ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup">
          <ng-template
            [ngTemplateOutlet]="treeNodes"
            [ngTemplateOutletContext]="{nodes: node.children, parent: group}"
          />
        </ng-template>
      </ul>
    }
  }
</ng-template>
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');

:host {
  display: flex;
  justify-content: center;
  user-select: none;
  font-family: var(--inter-font);
}

[ngTree] {
  min-width: 24rem;
  background-color: var(--septenary-contrast);
  border-radius: 0.5rem;
  padding: 0.5rem;
}

[ngTreeItem] {
  cursor: pointer;
  list-style: none;
  text-decoration: none;
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 0.3rem 1rem;
}

[ngTreeItem][aria-disabled='true'] {
  opacity: 0.5;
  cursor: default;
}

[ngTreeItem]:focus,
[ngTreeItem]:hover {
  background-color: var(--quinary-contrast);
}

[ngTreeItem]:focus {
  outline: 1px solid color-mix(in srgb, var(--hot-pink) 60%, transparent);
}

[ngTreeItem][aria-selected='true'],
[ngTreeItem][aria-selected='true'] .expand-icon {
  background-image: var(--pink-to-purple-horizontal-gradient);
  background-clip: text;
  color: transparent;
}

.material-symbols-outlined {
  margin: 0;
  width: 24px;
}

.expand-icon {
  transition: transform 0.2s ease;
}

[ngTreeItem][aria-expanded='true'] .expand-icon {
  transform: rotate(90deg);
}

.selected-icon {
  visibility: hidden;
  margin-left: auto;
}

[ngTreeItem][aria-current] .selected-icon,
[ngTreeItem][aria-selected='true'] .selected-icon {
  visibility: visible;
}

li[aria-expanded='false'] + ul[role='group'] {
  display: none;
}
import {Component, computed, signal} from '@angular/core';
import {NgTemplateOutlet} from '@angular/common';
import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';

type TreeNode = {
  name: string;
  value: string;
  children?: TreeNode[];
  disabled?: boolean;
  expanded?: boolean;
};

@Component({
  selector: 'app-root',
  templateUrl: 'app.html',
  styleUrl: 'app.css',
  imports: [Tree, TreeItem, TreeItemGroup, NgTemplateOutlet],
})
export class App {
  readonly nodes: readonly TreeNode[] = [
    {
      name: 'C:',
      value: 'C:',
      expanded: true,
      children: [
        {
          name: 'Program Files/',
          value: 'C:/Program Files',
          children: [
            {name: 'Common Files', value: 'C:/Program Files/Common Files'},
            {name: 'Internet Explorer', value: 'C:/Program Files/Internet Explorer'},
          ],
          expanded: false,
        },
        {
          name: 'Users/',
          value: 'C:/Users',
          children: [
            {name: 'Default', value: 'C:/Users/Default'},
            {name: 'Public', value: 'C:/Users/Public'},
          ],
          expanded: false,
        },
        {
          name: 'Windows/',
          value: 'C:/Windows',
          children: [
            {name: 'System32', value: 'C:/Windows/System32'},
            {name: 'Web', value: 'C:/Windows/Web'},
          ],
          expanded: false,
        },
        {name: 'pagefile.sys', value: 'C:/pagefile.sys'},
        {name: 'swapfile.sys', value: 'C:/swapfile.sys', disabled: true},
      ],
    },
  ];

  readonly selected = signal([]);
  readonly selectedCount = computed(() => this.selected().length);
}
<div class="win95-file-explorer">
  <div class="title-bar">
    <div class="title-bar-text">Exploring - (C:)</div>
    <div class="title-bar-controls">
      <button tabindex="-1" aria-label="Minimize"><span>-</span></button>
      <button tabindex="-1" aria-label="Maximize"><span>&#x25A1;</span></button>
      <button tabindex="-1" aria-label="Close"><span>&#xd7;</span></button>
    </div>
  </div>

  <div class="menu-bar">
    <div class="menu-item"><u>F</u>ile</div>
    <div class="menu-item"><u>E</u>dit</div>
    <div class="menu-item"><u>V</u>iew</div>
    <div class="menu-item"><u>T</u>ools</div>
    <div class="menu-item"><u>H</u>elp</div>
  </div>

  <div class="toolbar">
    <button tabindex="-1" class="win95-btn"><span class="icon">&#x2190;</span> Back</button>
    <button tabindex="-1" class="win95-btn"><span class="icon">&#x2192;</span> Forward</button>
    <button tabindex="-1" class="win95-btn"><span class="icon">&#x2191;</span> Up</button>
  </div>

  <div class="tree-view">
    <ul ngTree #tree="ngTree" class="retro-tree" [(value)]="selected">
      <ng-template
        [ngTemplateOutlet]="treeNodes"
        [ngTemplateOutletContext]="{nodes: nodes, parent: tree}"
      />
    </ul>
  </div>

  <div class="status-bar">
    <div class="status-panel status-panel-grow">{{ selectedCount() }} object(s) selected</div>
    <div class="status-panel status-panel-right">4.5MB</div>
  </div>
</div>

<ng-template #treeNodes let-nodes="nodes" let-parent="parent">
  @for (node of nodes; track node.value) {
    <li
      ngTreeItem
      [parent]="parent"
      [value]="node.value"
      [label]="node.name"
      [(expanded)]="node.expanded"
      [disabled]="node.disabled"
      #treeItem="ngTreeItem"
    >
      <span aria-hidden="true">
        @if (node.children) {
          {{ treeItem.expanded() ? '📂' : '📁' }}
        } @else {
          📄
        }
      </span>
      {{ node.name }}
      <span
        aria-hidden="true"
        class="material-symbols-outlined selected-icon"
        translate="no"
        aria-hidden="true"
        >check</span
      >
    </li>

    @if (node.children) {
      <ul role="group">
        <ng-template ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup">
          <ng-template
            [ngTemplateOutlet]="treeNodes"
            [ngTemplateOutletContext]="{nodes: node.children, parent: group}"
          />
        </ng-template>
      </ul>
    }
  }
</ng-template>
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
@import url('https://fonts.googleapis.com/css2?family=Jersey+20&display=swap');

:host {
  display: flex;
  justify-content: center;
  user-select: none;

  --win95-gray: #c0c0c0;
  --win95-dark-gray: #808080;
  --win95-light: #ffffff;
  --win95-shadow: #000000;
  --win95-blue: #000080;
  --win95-active-blue: linear-gradient(to right, #000080, #1084d0);
  --win95-font: "Jersey 20", sans-serif;

  font-family: var(--win95-font);
  font-size: 1.1rem;
}

.win95-file-explorer {
  border-style: solid;
  border-width: 1px;
  border-color: var(--win95-light) var(--win95-dark-gray) var(--win95-dark-gray) var(--win95-light);
  box-shadow: 1px 1px 0 var(--win95-shadow);
  background-color: var(--win95-gray);
  min-width: 350px;
}

.win95-btn {
  background-color: var(--win95-gray);
  padding: 3px 8px;
  cursor: pointer;
  border-style: solid;
  border-width: 1px;
  border-color: var(--win95-light) var(--win95-dark-gray) var(--win95-dark-gray) var(--win95-light);
  box-shadow: 1px 1px 0 var(--win95-shadow);
}

.win95-btn:active {
  box-shadow: 1px 1px 0 var(--win95-shadow) inset;
  padding: 4px 7px 2px 9px;
  border-style: solid;
  border-width: 1px;
  border-color: var(--win95-dark-gray) var(--win95-light) var(--win95-light) var(--win95-dark-gray);
}

.title-bar {
  background: var(--win95-active-blue);
  color: var(--win95-light);
  padding: 3px 6px;
  height: 20px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.title-bar-text {
  font-weight: bold;
}

.title-bar-controls button {
  background: var(--win95-gray);
  border: 1px solid var(--win95-light);
  color: var(--win95-shadow);
  width: 16px;
  height: 14px;
  line-height: 8px;
  font-size: 14px;
  padding: 0;
  margin-left: 1px;
  border-style: solid;
  border-width: 1px;
  border-color: var(--win95-light) var(--win95-dark-gray) var(--win95-dark-gray) var(--win95-light);
  box-shadow: 1px 1px 0 var(--win95-shadow);
}

.title-bar-controls button span {
  display: block;
  margin-top: -3px;
}

.menu-bar {
  display: flex;
  background-color: var(--win95-gray);
  border-bottom: 1px solid var(--win95-dark-gray);
  padding: 1px 2px;
}

.menu-item {
  padding: 0 6px;
  cursor: default;
  margin-right: 4px;
}

.menu-item:hover {
  background-color: var(--win95-blue);
  color: var(--win95-light);
}

.toolbar {
  display: flex;
  align-items: center;
  padding: 4px;
  border-top: 1px solid var(--win95-light);
  border-bottom: 1px solid var(--win95-dark-gray);
}

.toolbar .win95-btn {
  display: flex;
  align-items: center;
  margin-right: 8px;
}

.toolbar .icon {
  font-size: 1.25rem;
  line-height: 1;
  margin-right: 4px;
}

.tree-view {
  background-color: var(--win95-light);
  min-height: 300px;
  padding: 4px;
  border-width: 1px;
  border-style: solid;
  border-color: var(--win95-dark-gray) var(--win95-light) var(--win95-light) var(--win95-dark-gray);
  color: var(--win95-shadow);
}

.status-bar {
  margin-top: 2px;
}

.status-panel-grow {
  flex-grow: 1;
}

.status-panel-right {
  text-align: right;
}

.status-bar {
  height: 18px;
  background-color: var(--win95-gray);
  border-top: 1px solid var(--win95-light);
  border-left: 1px solid var(--win95-light);
  border-right: 1px solid var(--win95-dark-gray);
  border-bottom: 1px solid var(--win95-dark-gray);
  display: flex;
  font-size: 14px;
}

.status-panel {
  padding: 0 4px;
  height: 100%;
  display: flex;
  align-items: center;
  margin-right: 1px;
  border-style: solid;
  border-color: var(--win95-dark-gray) var(--win95-light) var(--win95-light) var(--win95-dark-gray);
  border-width: 1px;
  flex-grow: 1;
}

.status-panel:last-child {
  flex-grow: 0;
  width: 120px;
}

[ngTree] {
  padding: 0;
  margin: 0;
}

[ngTreeItem] {
  cursor: pointer;
  list-style: none;
  text-decoration: none;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.25rem 0.75rem;
}

[ngTreeItem][aria-disabled='true'] {
  opacity: 0.5;
  cursor: default;
}

[ngTreeItem]:focus,
[ngTreeItem]:hover {
  background-color: var(--senary-contrast);
  color: var(--primary-contrast);
  outline: none;
}

[ngTreeItem][aria-selected='true'] {
  background-color: var(--win95-blue);
  color: var(--win95-light);
}

.selected-icon {
  visibility: hidden;
  margin-left: auto;
}

[ngTreeItem][aria-current] .selected-icon,
[ngTreeItem][aria-selected='true'] .selected-icon {
  visibility: visible;
}

li[aria-expanded='false'] + ul[role='group'] {
  display: none;
}

Оставьте [multi]="false" (значение по умолчанию), чтобы разрешить выбор одного элемента. Пользователи нажимают пробел, чтобы выбрать элемент, на котором установлен фокус.

Выбор нескольких элементов

Разрешите пользователям выбирать несколько элементов в дереве.

import {Component, signal} from '@angular/core';
import {NgTemplateOutlet} from '@angular/common';
import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';

type TreeNode = {
  name: string;
  value: string;
  children?: TreeNode[];
  disabled?: boolean;
  expanded?: boolean;
};

@Component({
  selector: 'app-root',
  templateUrl: 'app.html',
  styleUrl: 'app.css',
  imports: [Tree, TreeItem, TreeItemGroup, NgTemplateOutlet],
})
export class App {
  readonly nodes: TreeNode[] = [
    {
      name: 'public',
      value: 'public',
      children: [
        {name: 'index.html', value: 'public/index.html'},
        {name: 'favicon.ico', value: 'public/favicon.ico'},
        {name: 'styles.css', value: 'public/styles.css'},
      ],
      expanded: true,
    },
    {
      name: 'src',
      value: 'src',
      children: [
        {
          name: 'app',
          value: 'src/app',
          children: [
            {name: 'app.ts', value: 'src/app/app.ts'},
            {name: 'app.html', value: 'src/app/app.html'},
            {name: 'app.css', value: 'src/app/app.css'},
          ],
          expanded: false,
        },
        {
          name: 'assets',
          value: 'src/assets',
          children: [{name: 'logo.png', value: 'src/assets/logo.png'}],
          expanded: false,
        },
        {
          name: 'environments',
          value: 'src/environments',
          children: [
            {
              name: 'environment.prod.ts',
              value: 'src/environments/environment.prod.ts',
              expanded: false,
            },
            {name: 'environment.ts', value: 'src/environments/environment.ts'},
          ],
          expanded: false,
        },
        {name: 'main.ts', value: 'src/main.ts'},
        {name: 'polyfills.ts', value: 'src/polyfills.ts'},
        {name: 'styles.css', value: 'src/styles.css', disabled: true},
        {name: 'test.ts', value: 'src/test.ts'},
      ],
      expanded: false,
    },
    {name: 'angular.json', value: 'angular.json'},
    {name: 'package.json', value: 'package.json'},
    {name: 'README.md', value: 'README.md'},
  ];

  readonly selected = signal(['angular.json', 'public/styles.css']);
}
<ul ngTree #tree="ngTree" [multi]="true" [(value)]="selected" class="basic-tree">
  <ng-template
    [ngTemplateOutlet]="treeNodes"
    [ngTemplateOutletContext]="{nodes: nodes, parent: tree}"
  />
</ul>

<ng-template #treeNodes let-nodes="nodes" let-parent="parent">
  @for (node of nodes; track node.value) {
    <li
      ngTreeItem
      [parent]="parent"
      [value]="node.value"
      [label]="node.name"
      [disabled]="node.disabled"
      [(expanded)]="node.expanded"
      #treeItem="ngTreeItem"
    >
      <span aria-hidden="true" class="material-symbols-outlined expand-icon" translate="no">{{
        node.children ? 'chevron_right' : ''
      }}</span>
      <span aria-hidden="true" class="material-symbols-outlined" translate="no">{{
        node.children ? 'folder' : 'docs'
      }}</span>
      {{ node.name }}
      <span
        aria-hidden="true"
        class="material-symbols-outlined selected-icon"
        translate="no"
        aria-hidden="true"
        >check</span
      >
    </li>

    @if (node.children) {
      <ul role="group">
        <ng-template ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup">
          <ng-template
            [ngTemplateOutlet]="treeNodes"
            [ngTemplateOutletContext]="{nodes: node.children, parent: group}"
          />
        </ng-template>
      </ul>
    }
  }
</ng-template>
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');

:host {
  display: flex;
  justify-content: center;
  user-select: none;
  font-family: var(--inter-font);
}

[ngTree] {
  min-width: 24rem;
  background-color: var(--septenary-contrast);
  border-radius: 0.5rem;
  padding: 0.5rem;
}

[ngTreeItem] {
  cursor: pointer;
  list-style: none;
  text-decoration: none;
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 0.3rem 1rem;
}

[ngTreeItem][aria-disabled='true'] {
  opacity: 0.5;
  cursor: default;
}

[ngTreeItem]:focus,
[ngTreeItem]:hover {
  background-color: var(--quinary-contrast);
}

[ngTreeItem]:focus {
  outline: 1px solid color-mix(in srgb, var(--hot-pink) 60%, transparent);
}

[ngTreeItem][aria-selected='true'],
[ngTreeItem][aria-selected='true'] .expand-icon {
  background-image: var(--pink-to-purple-horizontal-gradient);
  background-clip: text;
  color: transparent;
}

.material-symbols-outlined {
  margin: 0;
  width: 24px;
}

.expand-icon {
  transition: transform 0.2s ease;
}

[ngTreeItem][aria-expanded='true'] .expand-icon {
  transform: rotate(90deg);
}

.selected-icon {
  visibility: hidden;
  margin-left: auto;
}

[ngTreeItem][aria-current] .selected-icon,
[ngTreeItem][aria-selected='true'] .selected-icon {
  visibility: visible;
}

li[aria-expanded='false'] + ul[role='group'] {
  display: none;
}
import {Component, computed, signal} from '@angular/core';
import {NgTemplateOutlet} from '@angular/common';
import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';

type TreeNode = {
  name: string;
  value: string;
  children?: TreeNode[];
  disabled?: boolean;
  expanded?: boolean;
};

@Component({
  selector: 'app-root',
  templateUrl: 'app.html',
  styleUrl: 'app.css',
  imports: [Tree, TreeItem, TreeItemGroup, NgTemplateOutlet],
})
export class App {
  readonly nodes: readonly TreeNode[] = [
    {
      name: 'C:',
      value: 'C:',
      expanded: true,
      children: [
        {
          name: 'Program Files/',
          value: 'C:/Program Files',
          children: [
            {name: 'Common Files', value: 'C:/Program Files/Common Files'},
            {name: 'Internet Explorer', value: 'C:/Program Files/Internet Explorer'},
          ],
          expanded: false,
        },
        {
          name: 'Users/',
          value: 'C:/Users',
          children: [
            {name: 'Default', value: 'C:/Users/Default'},
            {name: 'Public', value: 'C:/Users/Public'},
          ],
          expanded: false,
        },
        {
          name: 'Windows/',
          value: 'C:/Windows',
          children: [
            {name: 'System32', value: 'C:/Windows/System32'},
            {name: 'Web', value: 'C:/Windows/Web'},
          ],
          expanded: false,
        },
        {name: 'pagefile.sys', value: 'C:/pagefile.sys'},
        {name: 'swapfile.sys', value: 'C:/swapfile.sys', disabled: true},
      ],
    },
  ];

  readonly selected = signal([]);
  readonly selectedCount = computed(() => this.selected().length);
}
<div class="win95-file-explorer">
  <div class="title-bar">
    <div class="title-bar-text">Exploring - (C:)</div>
    <div class="title-bar-controls">
      <button tabindex="-1" aria-label="Minimize"><span>-</span></button>
      <button tabindex="-1" aria-label="Maximize"><span>&#x25A1;</span></button>
      <button tabindex="-1" aria-label="Close"><span>&#xd7;</span></button>
    </div>
  </div>

  <div class="menu-bar">
    <div class="menu-item"><u>F</u>ile</div>
    <div class="menu-item"><u>E</u>dit</div>
    <div class="menu-item"><u>V</u>iew</div>
    <div class="menu-item"><u>T</u>ools</div>
    <div class="menu-item"><u>H</u>elp</div>
  </div>

  <div class="toolbar">
    <button tabindex="-1" class="win95-btn"><span class="icon">&#x2190;</span> Back</button>
    <button tabindex="-1" class="win95-btn"><span class="icon">&#x2192;</span> Forward</button>
    <button tabindex="-1" class="win95-btn"><span class="icon">&#x2191;</span> Up</button>
  </div>

  <div class="tree-view">
    <ul ngTree #tree="ngTree" class="retro-tree" [(value)]="selected" [multi]="true">
      <ng-template
        [ngTemplateOutlet]="treeNodes"
        [ngTemplateOutletContext]="{nodes: nodes, parent: tree}"
      />
    </ul>
  </div>

  <div class="status-bar">
    <div class="status-panel status-panel-grow">{{ selectedCount() }} object(s) selected</div>
    <div class="status-panel status-panel-right">4.5MB</div>
  </div>
</div>

<ng-template #treeNodes let-nodes="nodes" let-parent="parent">
  @for (node of nodes; track node.value) {
    <li
      ngTreeItem
      [parent]="parent"
      [value]="node.value"
      [label]="node.name"
      [(expanded)]="node.expanded"
      [disabled]="node.disabled"
      #treeItem="ngTreeItem"
    >
      <span aria-hidden="true">
        @if (node.children) {
          {{ treeItem.expanded() ? '📂' : '📁' }}
        } @else {
          📄
        }
      </span>
      {{ node.name }}
      <span
        aria-hidden="true"
        class="material-symbols-outlined selected-icon"
        translate="no"
        aria-hidden="true"
        >check</span
      >
    </li>

    @if (node.children) {
      <ul role="group">
        <ng-template ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup">
          <ng-template
            [ngTemplateOutlet]="treeNodes"
            [ngTemplateOutletContext]="{nodes: node.children, parent: group}"
          />
        </ng-template>
      </ul>
    }
  }
</ng-template>
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
@import url('https://fonts.googleapis.com/css2?family=Jersey+20&display=swap');

:host {
  display: flex;
  justify-content: center;
  user-select: none;

  --win95-gray: #c0c0c0;
  --win95-dark-gray: #808080;
  --win95-light: #ffffff;
  --win95-shadow: #000000;
  --win95-blue: #000080;
  --win95-active-blue: linear-gradient(to right, #000080, #1084d0);
  --win95-font: "Jersey 20", sans-serif;

  font-family: var(--win95-font);
  font-size: 1.1rem;
}

.win95-file-explorer {
  border-style: solid;
  border-width: 1px;
  border-color: var(--win95-light) var(--win95-dark-gray) var(--win95-dark-gray) var(--win95-light);
  box-shadow: 1px 1px 0 var(--win95-shadow);
  background-color: var(--win95-gray);
  min-width: 350px;
}

.win95-btn {
  background-color: var(--win95-gray);
  padding: 3px 8px;
  cursor: pointer;
  border-style: solid;
  border-width: 1px;
  border-color: var(--win95-light) var(--win95-dark-gray) var(--win95-dark-gray) var(--win95-light);
  box-shadow: 1px 1px 0 var(--win95-shadow);
}

.win95-btn:active {
  box-shadow: 1px 1px 0 var(--win95-shadow) inset;
  padding: 4px 7px 2px 9px;
  border-style: solid;
  border-width: 1px;
  border-color: var(--win95-dark-gray) var(--win95-light) var(--win95-light) var(--win95-dark-gray);
}

.title-bar {
  background: var(--win95-active-blue);
  color: var(--win95-light);
  padding: 3px 6px;
  height: 20px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.title-bar-text {
  font-weight: bold;
}

.title-bar-controls button {
  background: var(--win95-gray);
  border: 1px solid var(--win95-light);
  color: var(--win95-shadow);
  width: 16px;
  height: 14px;
  line-height: 8px;
  font-size: 14px;
  padding: 0;
  margin-left: 1px;
  border-style: solid;
  border-width: 1px;
  border-color: var(--win95-light) var(--win95-dark-gray) var(--win95-dark-gray) var(--win95-light);
  box-shadow: 1px 1px 0 var(--win95-shadow);
}

.title-bar-controls button span {
  display: block;
  margin-top: -3px;
}

.menu-bar {
  display: flex;
  background-color: var(--win95-gray);
  border-bottom: 1px solid var(--win95-dark-gray);
  padding: 1px 2px;
}

.menu-item {
  padding: 0 6px;
  cursor: default;
  margin-right: 4px;
}

.menu-item:hover {
  background-color: var(--win95-blue);
  color: var(--win95-light);
}

.toolbar {
  display: flex;
  align-items: center;
  padding: 4px;
  border-top: 1px solid var(--win95-light);
  border-bottom: 1px solid var(--win95-dark-gray);
}

.toolbar .win95-btn {
  display: flex;
  align-items: center;
  margin-right: 8px;
}

.toolbar .icon {
  font-size: 1.25rem;
  line-height: 1;
  margin-right: 4px;
}

.tree-view {
  background-color: var(--win95-light);
  min-height: 300px;
  padding: 4px;
  border-width: 1px;
  border-style: solid;
  border-color: var(--win95-dark-gray) var(--win95-light) var(--win95-light) var(--win95-dark-gray);
  color: var(--win95-shadow);
}

.status-bar {
  margin-top: 2px;
}

.status-panel-grow {
  flex-grow: 1;
}

.status-panel-right {
  text-align: right;
}

.status-bar {
  height: 18px;
  background-color: var(--win95-gray);
  border-top: 1px solid var(--win95-light);
  border-left: 1px solid var(--win95-light);
  border-right: 1px solid var(--win95-dark-gray);
  border-bottom: 1px solid var(--win95-dark-gray);
  display: flex;
  font-size: 14px;
}

.status-panel {
  padding: 0 4px;
  height: 100%;
  display: flex;
  align-items: center;
  margin-right: 1px;
  border-style: solid;
  border-color: var(--win95-dark-gray) var(--win95-light) var(--win95-light) var(--win95-dark-gray);
  border-width: 1px;
  flex-grow: 1;
}

.status-panel:last-child {
  flex-grow: 0;
  width: 120px;
}

[ngTree] {
  padding: 0;
  margin: 0;
}

[ngTreeItem] {
  cursor: pointer;
  list-style: none;
  text-decoration: none;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.25rem 0.75rem;
}

[ngTreeItem][aria-disabled='true'] {
  opacity: 0.5;
  cursor: default;
}

[ngTreeItem]:focus,
[ngTreeItem]:hover {
  background-color: var(--senary-contrast);
  color: var(--primary-contrast);
  outline: none;
}

[ngTreeItem][aria-selected='true'] {
  background-color: var(--win95-blue);
  color: var(--win95-light);
}

.selected-icon {
  visibility: hidden;
  margin-left: auto;
}

[ngTreeItem][aria-current] .selected-icon,
[ngTreeItem][aria-selected='true'] .selected-icon {
  visibility: visible;
}

li[aria-expanded='false'] + ul[role='group'] {
  display: none;
}

Установите [multi]="true" для дерева. Пользователи выбирают элементы по отдельности клавишей пробела или диапазоны клавишами Shift+Arrow.

Выбор следует за фокусом

Когда выбор следует за фокусом, элемент, на котором установлен фокус, выбирается автоматически. Это упрощает взаимодействие в сценариях навигации.

import {Component, signal} from '@angular/core';
import {NgTemplateOutlet} from '@angular/common';
import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';

type TreeNode = {
  name: string;
  value: string;
  children?: TreeNode[];
  disabled?: boolean;
  expanded?: boolean;
};

@Component({
  selector: 'app-root',
  templateUrl: 'app.html',
  styleUrl: 'app.css',
  imports: [Tree, TreeItem, TreeItemGroup, NgTemplateOutlet],
})
export class App {
  readonly nodes: TreeNode[] = [
    {
      name: 'public',
      value: 'public',
      children: [
        {name: 'index.html', value: 'public/index.html'},
        {name: 'favicon.ico', value: 'public/favicon.ico'},
        {name: 'styles.css', value: 'public/styles.css'},
      ],
      expanded: true,
    },
    {
      name: 'src',
      value: 'src',
      children: [
        {
          name: 'app',
          value: 'src/app',
          children: [
            {name: 'app.ts', value: 'src/app/app.ts'},
            {name: 'app.html', value: 'src/app/app.html'},
            {name: 'app.css', value: 'src/app/app.css'},
          ],
          expanded: false,
        },
        {
          name: 'assets',
          value: 'src/assets',
          children: [{name: 'logo.png', value: 'src/assets/logo.png'}],
          expanded: false,
        },
        {
          name: 'environments',
          value: 'src/environments',
          children: [
            {
              name: 'environment.prod.ts',
              value: 'src/environments/environment.prod.ts',
              expanded: false,
            },
            {name: 'environment.ts', value: 'src/environments/environment.ts'},
          ],
          expanded: false,
        },
        {name: 'main.ts', value: 'src/main.ts'},
        {name: 'polyfills.ts', value: 'src/polyfills.ts'},
        {name: 'styles.css', value: 'src/styles.css', disabled: true},
        {name: 'test.ts', value: 'src/test.ts'},
      ],
      expanded: false,
    },
    {name: 'angular.json', value: 'angular.json'},
    {name: 'package.json', value: 'package.json'},
    {name: 'README.md', value: 'README.md'},
  ];

  readonly selected = signal(['angular.json']);
}
<ul ngTree #tree="ngTree" [(value)]="selected" selectionMode="follow" class="basic-tree">
  <ng-template
    [ngTemplateOutlet]="treeNodes"
    [ngTemplateOutletContext]="{nodes: nodes, parent: tree}"
  />
</ul>

<ng-template #treeNodes let-nodes="nodes" let-parent="parent">
  @for (node of nodes; track node.value) {
    <li
      ngTreeItem
      [parent]="parent"
      [value]="node.value"
      [label]="node.name"
      [disabled]="node.disabled"
      [(expanded)]="node.expanded"
      #treeItem="ngTreeItem"
    >
      <span aria-hidden="true" class="material-symbols-outlined expand-icon" translate="no">{{
        node.children ? 'chevron_right' : ''
      }}</span>
      <span aria-hidden="true" class="material-symbols-outlined" translate="no">{{
        node.children ? 'folder' : 'docs'
      }}</span>
      {{ node.name }}
      <span
        aria-hidden="true"
        class="material-symbols-outlined selected-icon"
        translate="no"
        aria-hidden="true"
        >check</span
      >
    </li>

    @if (node.children) {
      <ul role="group">
        <ng-template ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup">
          <ng-template
            [ngTemplateOutlet]="treeNodes"
            [ngTemplateOutletContext]="{nodes: node.children, parent: group}"
          />
        </ng-template>
      </ul>
    }
  }
</ng-template>
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');

:host {
  display: flex;
  justify-content: center;
  user-select: none;
  font-family: var(--inter-font);
}

[ngTree] {
  min-width: 24rem;
  background-color: var(--septenary-contrast);
  border-radius: 0.5rem;
  padding: 0.5rem;
}

[ngTreeItem] {
  cursor: pointer;
  list-style: none;
  text-decoration: none;
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 0.3rem 1rem;
}

[ngTreeItem][aria-disabled='true'] {
  opacity: 0.5;
  cursor: default;
}

[ngTreeItem]:focus,
[ngTreeItem]:hover {
  background-color: var(--quinary-contrast);
}

[ngTreeItem]:focus {
  outline: 1px solid color-mix(in srgb, var(--hot-pink) 60%, transparent);
}

[ngTreeItem][aria-selected='true'],
[ngTreeItem][aria-selected='true'] .expand-icon {
  background-image: var(--pink-to-purple-horizontal-gradient);
  background-clip: text;
  color: transparent;
}

.material-symbols-outlined {
  margin: 0;
  width: 24px;
}

.expand-icon {
  transition: transform 0.2s ease;
}

[ngTreeItem][aria-expanded='true'] .expand-icon {
  transform: rotate(90deg);
}

.selected-icon {
  visibility: hidden;
  margin-left: auto;
}

[ngTreeItem][aria-current] .selected-icon,
[ngTreeItem][aria-selected='true'] .selected-icon {
  visibility: visible;
}

li[aria-expanded='false'] + ul[role='group'] {
  display: none;
}
import {Component, computed, signal} from '@angular/core';
import {NgTemplateOutlet} from '@angular/common';
import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';

type TreeNode = {
  name: string;
  value: string;
  children?: TreeNode[];
  disabled?: boolean;
  expanded?: boolean;
};

@Component({
  selector: 'app-root',
  templateUrl: 'app.html',
  styleUrl: 'app.css',
  imports: [Tree, TreeItem, TreeItemGroup, NgTemplateOutlet],
})
export class App {
  readonly nodes: readonly TreeNode[] = [
    {
      name: 'C:',
      value: 'C:',
      expanded: true,
      children: [
        {
          name: 'Program Files/',
          value: 'C:/Program Files',
          children: [
            {name: 'Common Files', value: 'C:/Program Files/Common Files'},
            {name: 'Internet Explorer', value: 'C:/Program Files/Internet Explorer'},
          ],
          expanded: false,
        },
        {
          name: 'Users/',
          value: 'C:/Users',
          children: [
            {name: 'Default', value: 'C:/Users/Default'},
            {name: 'Public', value: 'C:/Users/Public'},
          ],
          expanded: false,
        },
        {
          name: 'Windows/',
          value: 'C:/Windows',
          children: [
            {name: 'System32', value: 'C:/Windows/System32'},
            {name: 'Web', value: 'C:/Windows/Web'},
          ],
          expanded: false,
        },
        {name: 'pagefile.sys', value: 'C:/pagefile.sys'},
        {name: 'swapfile.sys', value: 'C:/swapfile.sys', disabled: true},
      ],
    },
  ];

  readonly selected = signal([]);
  readonly selectedCount = computed(() => this.selected().length);
}
<div class="win95-file-explorer">
  <div class="title-bar">
    <div class="title-bar-text">Exploring - (C:)</div>
    <div class="title-bar-controls">
      <button tabindex="-1" aria-label="Minimize"><span>-</span></button>
      <button tabindex="-1" aria-label="Maximize"><span>&#x25A1;</span></button>
      <button tabindex="-1" aria-label="Close"><span>&#xd7;</span></button>
    </div>
  </div>

  <div class="menu-bar">
    <div class="menu-item"><u>F</u>ile</div>
    <div class="menu-item"><u>E</u>dit</div>
    <div class="menu-item"><u>V</u>iew</div>
    <div class="menu-item"><u>T</u>ools</div>
    <div class="menu-item"><u>H</u>elp</div>
  </div>

  <div class="toolbar">
    <button tabindex="-1" class="win95-btn"><span class="icon">&#x2190;</span> Back</button>
    <button tabindex="-1" class="win95-btn"><span class="icon">&#x2192;</span> Forward</button>
    <button tabindex="-1" class="win95-btn"><span class="icon">&#x2191;</span> Up</button>
  </div>

  <div class="tree-view">
    <ul ngTree #tree="ngTree" class="retro-tree" selectionMode="follow" [(value)]="selected">
      <ng-template
        [ngTemplateOutlet]="treeNodes"
        [ngTemplateOutletContext]="{nodes: nodes, parent: tree}"
      />
    </ul>
  </div>

  <div class="status-bar">
    <div class="status-panel status-panel-grow">{{ selectedCount() }} object(s) selected</div>
    <div class="status-panel status-panel-right">4.5MB</div>
  </div>
</div>

<ng-template #treeNodes let-nodes="nodes" let-parent="parent">
  @for (node of nodes; track node.value) {
    <li
      ngTreeItem
      [parent]="parent"
      [value]="node.value"
      [label]="node.name"
      [(expanded)]="node.expanded"
      [disabled]="node.disabled"
      #treeItem="ngTreeItem"
    >
      <span aria-hidden="true">
        @if (node.children) {
          {{ treeItem.expanded() ? '📂' : '📁' }}
        } @else {
          📄
        }
      </span>
      {{ node.name }}
      <span
        aria-hidden="true"
        class="material-symbols-outlined selected-icon"
        translate="no"
        aria-hidden="true"
        >check</span
      >
    </li>

    @if (node.children) {
      <ul role="group">
        <ng-template ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup">
          <ng-template
            [ngTemplateOutlet]="treeNodes"
            [ngTemplateOutletContext]="{nodes: node.children, parent: group}"
          />
        </ng-template>
      </ul>
    }
  }
</ng-template>
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
@import url('https://fonts.googleapis.com/css2?family=Jersey+20&display=swap');

:host {
  display: flex;
  justify-content: center;
  user-select: none;

  --win95-gray: #c0c0c0;
  --win95-dark-gray: #808080;
  --win95-light: #ffffff;
  --win95-shadow: #000000;
  --win95-blue: #000080;
  --win95-active-blue: linear-gradient(to right, #000080, #1084d0);
  --win95-font: "Jersey 20", sans-serif;

  font-family: var(--win95-font);
  font-size: 1.1rem;
}

.win95-file-explorer {
  border-style: solid;
  border-width: 1px;
  border-color: var(--win95-light) var(--win95-dark-gray) var(--win95-dark-gray) var(--win95-light);
  box-shadow: 1px 1px 0 var(--win95-shadow);
  background-color: var(--win95-gray);
  min-width: 350px;
}

.win95-btn {
  background-color: var(--win95-gray);
  padding: 3px 8px;
  cursor: pointer;
  border-style: solid;
  border-width: 1px;
  border-color: var(--win95-light) var(--win95-dark-gray) var(--win95-dark-gray) var(--win95-light);
  box-shadow: 1px 1px 0 var(--win95-shadow);
}

.win95-btn:active {
  box-shadow: 1px 1px 0 var(--win95-shadow) inset;
  padding: 4px 7px 2px 9px;
  border-style: solid;
  border-width: 1px;
  border-color: var(--win95-dark-gray) var(--win95-light) var(--win95-light) var(--win95-dark-gray);
}

.title-bar {
  background: var(--win95-active-blue);
  color: var(--win95-light);
  padding: 3px 6px;
  height: 20px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.title-bar-text {
  font-weight: bold;
}

.title-bar-controls button {
  background: var(--win95-gray);
  border: 1px solid var(--win95-light);
  color: var(--win95-shadow);
  width: 16px;
  height: 14px;
  line-height: 8px;
  font-size: 14px;
  padding: 0;
  margin-left: 1px;
  border-style: solid;
  border-width: 1px;
  border-color: var(--win95-light) var(--win95-dark-gray) var(--win95-dark-gray) var(--win95-light);
  box-shadow: 1px 1px 0 var(--win95-shadow);
}

.title-bar-controls button span {
  display: block;
  margin-top: -3px;
}

.menu-bar {
  display: flex;
  background-color: var(--win95-gray);
  border-bottom: 1px solid var(--win95-dark-gray);
  padding: 1px 2px;
}

.menu-item {
  padding: 0 6px;
  cursor: default;
  margin-right: 4px;
}

.menu-item:hover {
  background-color: var(--win95-blue);
  color: var(--win95-light);
}

.toolbar {
  display: flex;
  align-items: center;
  padding: 4px;
  border-top: 1px solid var(--win95-light);
  border-bottom: 1px solid var(--win95-dark-gray);
}

.toolbar .win95-btn {
  display: flex;
  align-items: center;
  margin-right: 8px;
}

.toolbar .icon {
  font-size: 1.25rem;
  line-height: 1;
  margin-right: 4px;
}

.tree-view {
  background-color: var(--win95-light);
  min-height: 300px;
  padding: 4px;
  border-width: 1px;
  border-style: solid;
  border-color: var(--win95-dark-gray) var(--win95-light) var(--win95-light) var(--win95-dark-gray);
  color: var(--win95-shadow);
}

.status-bar {
  margin-top: 2px;
}

.status-panel-grow {
  flex-grow: 1;
}

.status-panel-right {
  text-align: right;
}

.status-bar {
  height: 18px;
  background-color: var(--win95-gray);
  border-top: 1px solid var(--win95-light);
  border-left: 1px solid var(--win95-light);
  border-right: 1px solid var(--win95-dark-gray);
  border-bottom: 1px solid var(--win95-dark-gray);
  display: flex;
  font-size: 14px;
}

.status-panel {
  padding: 0 4px;
  height: 100%;
  display: flex;
  align-items: center;
  margin-right: 1px;
  border-style: solid;
  border-color: var(--win95-dark-gray) var(--win95-light) var(--win95-light) var(--win95-dark-gray);
  border-width: 1px;
  flex-grow: 1;
}

.status-panel:last-child {
  flex-grow: 0;
  width: 120px;
}

[ngTree] {
  padding: 0;
  margin: 0;
}

[ngTreeItem] {
  cursor: pointer;
  list-style: none;
  text-decoration: none;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.25rem 0.75rem;
}

[ngTreeItem][aria-disabled='true'] {
  opacity: 0.5;
  cursor: default;
}

[ngTreeItem]:focus,
[ngTreeItem]:hover {
  background-color: var(--senary-contrast);
  color: var(--primary-contrast);
  outline: none;
}

[ngTreeItem][aria-selected='true'] {
  background-color: var(--win95-blue);
  color: var(--win95-light);
}

.selected-icon {
  visibility: hidden;
  margin-left: auto;
}

[ngTreeItem][aria-current] .selected-icon,
[ngTreeItem][aria-selected='true'] .selected-icon {
  visibility: visible;
}

li[aria-expanded='false'] + ul[role='group'] {
  display: none;
}

Установите [selectionMode]="'follow'" для дерева. Выбор автоматически обновляется при перемещении пользователей с помощью клавиш со стрелками.

Отключённые элементы дерева

Отключите определённые узлы дерева, чтобы запретить взаимодействие с ними. Настройте, смогут ли отключённые элементы получать фокус.

import {Component, signal} from '@angular/core';
import {NgTemplateOutlet} from '@angular/common';
import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';

type TreeNode = {
  name: string;
  value: string;
  children?: TreeNode[];
  disabled?: boolean;
  expanded?: boolean;
};

@Component({
  selector: 'app-root',
  templateUrl: 'app.html',
  styleUrl: 'app.css',
  imports: [Tree, TreeItem, TreeItemGroup, NgTemplateOutlet],
})
export class App {
  readonly nodes: TreeNode[] = [
    {
      name: 'public',
      value: 'public',
      children: [
        {name: 'index.html', value: 'public/index.html'},
        {name: 'favicon.ico', value: 'public/favicon.ico'},
        {name: 'styles.css', value: 'public/styles.css'},
      ],
      expanded: true,
      disabled: true,
    },
    {
      name: 'src',
      value: 'src',
      children: [
        {
          name: 'app',
          value: 'src/app',
          children: [
            {name: 'app.ts', value: 'src/app/app.ts'},
            {name: 'app.html', value: 'src/app/app.html'},
            {name: 'app.css', value: 'src/app/app.css'},
          ],
          expanded: false,
        },
        {
          name: 'assets',
          value: 'src/assets',
          children: [{name: 'logo.png', value: 'src/assets/logo.png'}],
          expanded: false,
        },
        {
          name: 'environments',
          value: 'src/environments',
          children: [
            {
              name: 'environment.prod.ts',
              value: 'src/environments/environment.prod.ts',
              expanded: false,
            },
            {name: 'environment.ts', value: 'src/environments/environment.ts'},
          ],
          expanded: false,
        },
        {name: 'main.ts', value: 'src/main.ts'},
        {name: 'polyfills.ts', value: 'src/polyfills.ts'},
        {name: 'styles.css', value: 'src/styles.css', disabled: true},
        {name: 'test.ts', value: 'src/test.ts'},
      ],
      expanded: false,
      disabled: true,
    },
    {name: 'angular.json', value: 'angular.json'},
    {name: 'package.json', value: 'package.json'},
    {name: 'README.md', value: 'README.md'},
  ];

  readonly selected = signal(['angular.json']);
}
<ul ngTree #tree="ngTree" [(value)]="selected" class="basic-tree">
  <ng-template
    [ngTemplateOutlet]="treeNodes"
    [ngTemplateOutletContext]="{nodes: nodes, parent: tree}"
  />
</ul>

<ng-template #treeNodes let-nodes="nodes" let-parent="parent">
  @for (node of nodes; track node.value) {
    <li
      ngTreeItem
      [parent]="parent"
      [value]="node.value"
      [label]="node.name"
      [disabled]="node.disabled"
      [(expanded)]="node.expanded"
      #treeItem="ngTreeItem"
    >
      <span aria-hidden="true" class="material-symbols-outlined expand-icon" translate="no">{{
        node.children ? 'chevron_right' : ''
      }}</span>
      <span aria-hidden="true" class="material-symbols-outlined" translate="no">{{
        node.children ? 'folder' : 'docs'
      }}</span>
      {{ node.name }}
      <span
        aria-hidden="true"
        class="material-symbols-outlined selected-icon"
        translate="no"
        aria-hidden="true"
        >check</span
      >
    </li>

    @if (node.children) {
      <ul role="group">
        <ng-template ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup">
          <ng-template
            [ngTemplateOutlet]="treeNodes"
            [ngTemplateOutletContext]="{nodes: node.children, parent: group}"
          />
        </ng-template>
      </ul>
    }
  }
</ng-template>
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');

:host {
  display: flex;
  justify-content: center;
  user-select: none;
  font-family: var(--inter-font);
}

[ngTree] {
  min-width: 24rem;
  background-color: var(--septenary-contrast);
  border-radius: 0.5rem;
  padding: 0.5rem;
}

[ngTreeItem] {
  cursor: pointer;
  list-style: none;
  text-decoration: none;
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 0.3rem 1rem;
}

[ngTreeItem][aria-disabled='true'] {
  opacity: 0.5;
  cursor: default;
}

[ngTreeItem]:focus,
[ngTreeItem]:hover {
  background-color: var(--quinary-contrast);
}

[ngTreeItem]:focus {
  outline: 1px solid color-mix(in srgb, var(--hot-pink) 60%, transparent);
}

[ngTreeItem][aria-selected='true'],
[ngTreeItem][aria-selected='true'] .expand-icon {
  background-image: var(--pink-to-purple-horizontal-gradient);
  background-clip: text;
  color: transparent;
}

.material-symbols-outlined {
  margin: 0;
  width: 24px;
}

.expand-icon {
  transition: transform 0.2s ease;
}

[ngTreeItem][aria-expanded='true'] .expand-icon {
  transform: rotate(90deg);
}

.selected-icon {
  visibility: hidden;
  margin-left: auto;
}

[ngTreeItem][aria-current] .selected-icon,
[ngTreeItem][aria-selected='true'] .selected-icon {
  visibility: visible;
}

li[aria-expanded='false'] + ul[role='group'] {
  display: none;
}
import {Component, computed, signal} from '@angular/core';
import {NgTemplateOutlet} from '@angular/common';
import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';

type TreeNode = {
  name: string;
  value: string;
  children?: TreeNode[];
  disabled?: boolean;
  expanded?: boolean;
};

@Component({
  selector: 'app-root',
  templateUrl: 'app.html',
  styleUrl: 'app.css',
  imports: [Tree, TreeItem, TreeItemGroup, NgTemplateOutlet],
})
export class App {
  readonly nodes: readonly TreeNode[] = [
    {
      name: 'C:',
      value: 'C:',
      expanded: true,
      children: [
        {
          name: 'Program Files/',
          value: 'C:/Program Files',
          children: [
            {name: 'Common Files', value: 'C:/Program Files/Common Files'},
            {name: 'Internet Explorer', value: 'C:/Program Files/Internet Explorer'},
          ],
          expanded: true,
          disabled: true,
        },
        {
          name: 'Users/',
          value: 'C:/Users',
          children: [
            {name: 'Default', value: 'C:/Users/Default'},
            {name: 'Public', value: 'C:/Users/Public'},
          ],
          expanded: false,
        },
        {
          name: 'Windows/',
          value: 'C:/Windows',
          children: [
            {name: 'System32', value: 'C:/Windows/System32'},
            {name: 'Web', value: 'C:/Windows/Web'},
          ],
          expanded: false,
        },
        {name: 'pagefile.sys', value: 'C:/pagefile.sys'},
        {name: 'swapfile.sys', value: 'C:/swapfile.sys', disabled: true},
      ],
    },
  ];

  readonly selected = signal([]);
  readonly selectedCount = computed(() => this.selected().length);
}
<div class="win95-file-explorer">
  <div class="title-bar">
    <div class="title-bar-text">Exploring - (C:)</div>
    <div class="title-bar-controls">
      <button tabindex="-1" aria-label="Minimize"><span>-</span></button>
      <button tabindex="-1" aria-label="Maximize"><span>&#x25A1;</span></button>
      <button tabindex="-1" aria-label="Close"><span>&#xd7;</span></button>
    </div>
  </div>

  <div class="menu-bar">
    <div class="menu-item"><u>F</u>ile</div>
    <div class="menu-item"><u>E</u>dit</div>
    <div class="menu-item"><u>V</u>iew</div>
    <div class="menu-item"><u>T</u>ools</div>
    <div class="menu-item"><u>H</u>elp</div>
  </div>

  <div class="toolbar">
    <button tabindex="-1" class="win95-btn"><span class="icon">&#x2190;</span> Back</button>
    <button tabindex="-1" class="win95-btn"><span class="icon">&#x2192;</span> Forward</button>
    <button tabindex="-1" class="win95-btn"><span class="icon">&#x2191;</span> Up</button>
  </div>

  <div class="tree-view">
    <ul ngTree #tree="ngTree" class="retro-tree" [(value)]="selected">
      <ng-template
        [ngTemplateOutlet]="treeNodes"
        [ngTemplateOutletContext]="{nodes: nodes, parent: tree}"
      />
    </ul>
  </div>

  <div class="status-bar">
    <div class="status-panel status-panel-grow">{{ selectedCount() }} object(s) selected</div>
    <div class="status-panel status-panel-right">4.5MB</div>
  </div>
</div>

<ng-template #treeNodes let-nodes="nodes" let-parent="parent">
  @for (node of nodes; track node.value) {
    <li
      ngTreeItem
      [parent]="parent"
      [value]="node.value"
      [label]="node.name"
      [(expanded)]="node.expanded"
      [disabled]="node.disabled"
      #treeItem="ngTreeItem"
    >
      <span aria-hidden="true">
        @if (node.children) {
          {{ treeItem.expanded() ? '📂' : '📁' }}
        } @else {
          📄
        }
      </span>
      {{ node.name }}
      <span
        aria-hidden="true"
        class="material-symbols-outlined selected-icon"
        translate="no"
        aria-hidden="true"
        >check</span
      >
    </li>

    @if (node.children) {
      <ul role="group">
        <ng-template ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup">
          <ng-template
            [ngTemplateOutlet]="treeNodes"
            [ngTemplateOutletContext]="{nodes: node.children, parent: group}"
          />
        </ng-template>
      </ul>
    }
  }
</ng-template>
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
@import url('https://fonts.googleapis.com/css2?family=Jersey+20&display=swap');

:host {
  display: flex;
  justify-content: center;
  user-select: none;

  --win95-gray: #c0c0c0;
  --win95-dark-gray: #808080;
  --win95-light: #ffffff;
  --win95-shadow: #000000;
  --win95-blue: #000080;
  --win95-active-blue: linear-gradient(to right, #000080, #1084d0);
  --win95-font: "Jersey 20", sans-serif;

  font-family: var(--win95-font);
  font-size: 1.1rem;
}

.win95-file-explorer {
  border-style: solid;
  border-width: 1px;
  border-color: var(--win95-light) var(--win95-dark-gray) var(--win95-dark-gray) var(--win95-light);
  box-shadow: 1px 1px 0 var(--win95-shadow);
  background-color: var(--win95-gray);
  min-width: 350px;
}

.win95-btn {
  background-color: var(--win95-gray);
  padding: 3px 8px;
  cursor: pointer;
  border-style: solid;
  border-width: 1px;
  border-color: var(--win95-light) var(--win95-dark-gray) var(--win95-dark-gray) var(--win95-light);
  box-shadow: 1px 1px 0 var(--win95-shadow);
}

.win95-btn:active {
  box-shadow: 1px 1px 0 var(--win95-shadow) inset;
  padding: 4px 7px 2px 9px;
  border-style: solid;
  border-width: 1px;
  border-color: var(--win95-dark-gray) var(--win95-light) var(--win95-light) var(--win95-dark-gray);
}

.title-bar {
  background: var(--win95-active-blue);
  color: var(--win95-light);
  padding: 3px 6px;
  height: 20px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.title-bar-text {
  font-weight: bold;
}

.title-bar-controls button {
  background: var(--win95-gray);
  border: 1px solid var(--win95-light);
  color: var(--win95-shadow);
  width: 16px;
  height: 14px;
  line-height: 8px;
  font-size: 14px;
  padding: 0;
  margin-left: 1px;
  border-style: solid;
  border-width: 1px;
  border-color: var(--win95-light) var(--win95-dark-gray) var(--win95-dark-gray) var(--win95-light);
  box-shadow: 1px 1px 0 var(--win95-shadow);
}

.title-bar-controls button span {
  display: block;
  margin-top: -3px;
}

.menu-bar {
  display: flex;
  background-color: var(--win95-gray);
  border-bottom: 1px solid var(--win95-dark-gray);
  padding: 1px 2px;
}

.menu-item {
  padding: 0 6px;
  cursor: default;
  margin-right: 4px;
}

.menu-item:hover {
  background-color: var(--win95-blue);
  color: var(--win95-light);
}

.toolbar {
  display: flex;
  align-items: center;
  padding: 4px;
  border-top: 1px solid var(--win95-light);
  border-bottom: 1px solid var(--win95-dark-gray);
}

.toolbar .win95-btn {
  display: flex;
  align-items: center;
  margin-right: 8px;
}

.toolbar .icon {
  font-size: 1.25rem;
  line-height: 1;
  margin-right: 4px;
}

.tree-view {
  background-color: var(--win95-light);
  min-height: 300px;
  padding: 4px;
  border-width: 1px;
  border-style: solid;
  border-color: var(--win95-dark-gray) var(--win95-light) var(--win95-light) var(--win95-dark-gray);
  color: var(--win95-shadow);
}

.status-bar {
  margin-top: 2px;
}

.status-panel-grow {
  flex-grow: 1;
}

.status-panel-right {
  text-align: right;
}

.status-bar {
  height: 18px;
  background-color: var(--win95-gray);
  border-top: 1px solid var(--win95-light);
  border-left: 1px solid var(--win95-light);
  border-right: 1px solid var(--win95-dark-gray);
  border-bottom: 1px solid var(--win95-dark-gray);
  display: flex;
  font-size: 14px;
}

.status-panel {
  padding: 0 4px;
  height: 100%;
  display: flex;
  align-items: center;
  margin-right: 1px;
  border-style: solid;
  border-color: var(--win95-dark-gray) var(--win95-light) var(--win95-light) var(--win95-dark-gray);
  border-width: 1px;
  flex-grow: 1;
}

.status-panel:last-child {
  flex-grow: 0;
  width: 120px;
}

[ngTree] {
  padding: 0;
  margin: 0;
}

[ngTreeItem] {
  cursor: pointer;
  list-style: none;
  text-decoration: none;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.25rem 0.75rem;
}

[ngTreeItem][aria-disabled='true'] {
  opacity: 0.5;
  cursor: default;
}

[ngTreeItem]:focus,
[ngTreeItem]:hover {
  background-color: var(--senary-contrast);
  color: var(--primary-contrast);
  outline: none;
}

[ngTreeItem][aria-selected='true'] {
  background-color: var(--win95-blue);
  color: var(--win95-light);
}

.selected-icon {
  visibility: hidden;
  margin-left: auto;
}

[ngTreeItem][aria-current] .selected-icon,
[ngTreeItem][aria-selected='true'] .selected-icon {
  visibility: visible;
}

li[aria-expanded='false'] + ul[role='group'] {
  display: none;
}

Если для дерева установлено [softDisabled]="true", отключённые элементы могут получать фокус, но их нельзя активировать или выбирать. Если установлено [softDisabled]="false", отключённые элементы пропускаются при навигации с клавиатуры.

Тестирование

Angular Aria предоставляет harness-компоненты для тестирования компонентов дерева. Ниже приведён пример использования harness-компонентов в тесте компонента:

import {ComponentFixture, TestBed} from '@angular/core/testing';
import {HarnessLoader} from '@angular/cdk/testing';
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
import {TreeHarness} from '@angular/aria/tree/testing';
import {MyTreeComponent} from './my-tree'; // Your component

describe('MyTreeComponent', () => {
  let fixture: ComponentFixture<MyTreeComponent>;
  let loader: HarnessLoader;

  beforeEach(async () => {
    TestBed.configureTestingModule({
      imports: [MyTreeComponent],
    });

    fixture = TestBed.createComponent(MyTreeComponent);
    await fixture.whenStable();
    loader = TestbedHarnessEnvironment.loader(fixture);
  });

  it('should navigate and expand tree items', async () => {
    const tree = await loader.getHarness(TreeHarness);

    // Get top-level structure representation
    expect(await tree.getTreeStructure()).toEqual({
      children: [{text: 'public'}, {text: 'src'}, {text: 'package.json'}],
    });

    // Get all items (currently visible)
    const items = await tree.getItems();
    expect(items.length).toBe(3);

    // Expand the first item ('public')
    expect(await items[0].isExpanded()).toBe(false);
    await items[0].click();
    expect(await items[0].isExpanded()).toBe(true);

    // Verifying tree structure updates after expansion
    expect(await tree.getTreeStructure()).toEqual({
      children: [
        {
          text: 'public',
          children: [{text: 'index.html'}, {text: 'styles.css'}],
        },
        {text: 'src'},
        {text: 'package.json'},
      ],
    });
  });
});

API

Tree

Директива контейнера, управляющая иерархической навигацией и выбором.

Входные параметры

Свойство Тип По умолчанию Описание
disabled boolean false Отключает всё дерево
softDisabled boolean true Если установлено true, отключённые элементы получают фокус, но не поддерживают взаимодействие
multi boolean false Можно ли выбрать несколько элементов
selectionMode 'explicit' | 'follow' 'explicit' Требуется ли для выбора явное действие или выбор следует за фокусом
nav boolean false Находится ли дерево в режиме навигации (используется aria-current)
wrap boolean true Зацикливается ли навигация с клавиатуры с переходом от последнего элемента к первому
focusMode 'roving' | 'activedescendant' 'roving' Используемая деревом стратегия фокуса
values any[] [] Значения выбранных элементов (поддерживается двусторонняя привязка)

Методы

Метод Параметры Описание
expandAll нет Раскрывает все узлы дерева
collapseAll нет Сворачивает все узлы дерева
selectAll нет Выбирает все элементы (только в режиме множественного выбора)
clearSelection нет Снимает выбор со всех элементов

TreeItem

Отдельный узел дерева, который может содержать дочерние узлы.

Входные параметры

Свойство Тип По умолчанию Описание
parent Tree | TreeItemGroup — Обязательный. Корневой элемент родительского Tree или TreeItemGroup.
value any — Обязательный. Уникальное значение этого элемента дерева
disabled boolean false Отключает этот элемент
expanded boolean false Раскрыт ли узел (поддерживается двусторонняя привязка)

Сигналы

Свойство Тип Описание
selected Signal<boolean> Выбран ли элемент
active Signal<boolean> Находится ли фокус в данный момент на этом элементе
hasChildren Signal<boolean> Есть ли у элемента дочерние узлы

Методы

Метод Параметры Описание
expand нет Раскрывает этот узел
collapse нет Сворачивает этот узел
toggle нет Переключает состояние раскрытия

TreeItemGroup

Структурная директива, применяемая к ng-template, в котором хранятся дочерние узлы раскрываемого элемента дерева.

Входные параметры

Свойство Тип По умолчанию Описание
ownedBy TreeItem — Обязательный. Ссылка на родительский ngTreeItem.

Использование

<ul ngTree #tree="ngTree">
  <li ngTreeItem [parent]="tree" value="parent" #parentItem="ngTreeItem">
    Parent Item
    <ul role="group">
      <ng-template ngTreeItemGroup [ownedBy]="parentItem" #group="ngTreeItemGroup">
        <li ngTreeItem [parent]="group" value="child1">Child 1</li>
        <li ngTreeItem [parent]="group" value="child2">Child 2</li>
      </ng-template>
    </ul>
  </li>
</ul>

Super-powered by Google ©2010–2025.
Code licensed under an MIT-style License. Documentation licensed under CC BY 4.0.
https://angular.dev/guide/aria/tree

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API