Tree
Genel Bakış
Ağaç, öğelerin alt öğelerini ortaya çıkarmak için genişletilebileceği veya gizlemek için daraltılabileceği hiyerarşik verileri görüntüler. Kullanıcılar ok tuşlarıyla gezinir, düğümleri genişletip daraltır ve isteğe bağlı olarak navigasyon veya veri seçim senaryoları için öğeleri seçer.
TS
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']);
}
HTML
<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>
CSS
@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;
}
Kullanım
Ağaçlar, kullanıcıların iç içe yapılar arasında gezinmesi gereken hiyerarşik verileri görüntülemek için iyi çalışır.
Ağaçları şu durumlarda kullanın:
- Dosya sistemi navigasyonu oluşturma
- Klasör ve belge hiyerarşilerini gösterme
- İç içe menü yapıları oluşturma
- Organizasyon şemalarını görüntüleme
- Hiyerarşik verilere göz atma
- İç içe bölümlerle site navigasyonu uygulama
Ağaçlardan şu durumlarda kaçının:
- Düz listeler görüntüleme (bunun yerine Listbox kullanın)
- Veri tabloları gösterme (bunun yerine Grid kullanın)
- Basit açılır menüler oluşturma (bunun yerine Select kullanın)
- Breadcrumb navigasyonu oluşturma (breadcrumb kalıplarını kullanın)
Özellikler
- Hiyerarşik navigasyon - Genişletme ve daraltma işlevselliğine sahip iç içe ağaç yapısı
- Seçim modları - Açık veya odağı takip eden davranışla tekli veya çoklu seçim
- Seçim odağı takip eder - Odak değiştiğinde isteğe bağlı otomatik seçim
- Klavye navigasyonu - Ok tuşları, Home, End ve yazarak arama
- Genişlet/daralt - Üst düğümleri değiştirmek için Sağ/Sol oklar veya Enter
- Devre dışı öğeler - Odak yönetimi ile belirli düğümleri devre dışı bırakma
- Odak modları - Dolaşan tabindex veya activedescendant odak stratejileri
- RTL desteği - Sağdan sola dil navigasyonu
Örnekler
Navigasyon Ağacı
Öğelere tıklamanın seçmek yerine eylem tetiklediği navigasyon için bir ağaç kullanın.
TS
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']);
}
HTML
<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">{{
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>
CSS
@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;
}
Navigasyon modunu etkinleştirmek için [nav]="true" ayarlayın. Bu, seçim yerine mevcut sayfayı belirtmek için aria-current kullanır.
Tekli Seçim
Kullanıcıların ağaçtan bir öğe seçtiği senaryolar için tekli seçimi etkinleştirin.
TS
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']);
}
HTML
<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>
CSS
@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;
}
TS
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);
}
HTML
<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>□</span></button>
<button tabindex="-1" aria-label="Close"><span>×</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">←</span> Back</button>
<button tabindex="-1" class="win95-btn"><span class="icon">→</span> Forward</button>
<button tabindex="-1" class="win95-btn"><span class="icon">↑</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"
>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>
CSS
@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;
}
Tekli seçim için [multi]="false" (varsayılan) olarak bırakın. Kullanıcılar odaklanılan öğeyi seçmek için Boşluk tuşuna basar.
Çoklu Seçim
Kullanıcıların ağaçtan birden fazla öğe seçmesine izin verin.
TS
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']);
}
HTML
<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"
>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>
CSS
@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;
}
TS
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);
}
HTML
<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>□</span></button>
<button tabindex="-1" aria-label="Close"><span>×</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">←</span> Back</button>
<button tabindex="-1" class="win95-btn"><span class="icon">→</span> Forward</button>
<button tabindex="-1" class="win95-btn"><span class="icon">↑</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"
>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>
CSS
@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;
}
Ağaçta [multi]="true" ayarlayın. Kullanıcılar Boşluk ile tek tek öğeleri seçer veya Shift+Ok tuşlarıyla aralıkları seçer.
Seçim Odağı Takip Eder
Seçim odağı takip ettiğinde, odaklanılan öğe otomatik olarak seçilir. Bu, navigasyon senaryoları için etkileşimi basitleştirir.
TS
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']);
}
HTML
<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"
>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>
CSS
@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;
}
TS
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);
}
HTML
<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>□</span></button>
<button tabindex="-1" aria-label="Close"><span>×</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">←</span> Back</button>
<button tabindex="-1" class="win95-btn"><span class="icon">→</span> Forward</button>
<button tabindex="-1" class="win95-btn"><span class="icon">↑</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"
>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>
CSS
@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;
}
Ağaçta [selectionMode]="'follow'" ayarlayın. Kullanıcılar ok tuşlarıyla gezindikçe seçim otomatik olarak güncellenir.
Devre Dışı Ağaç Öğeleri
Etkileşimi engellemek için belirli ağaç düğümlerini devre dışı bırakın. Devre dışı öğelerin odak alıp alamayacağını kontrol edin.
TS
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']);
}
HTML
<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>
CSS
@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;
}
TS
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);
}
HTML
<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>□</span></button>
<button tabindex="-1" aria-label="Close"><span>×</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">←</span> Back</button>
<button tabindex="-1" class="win95-btn"><span class="icon">→</span> Forward</button>
<button tabindex="-1" class="win95-btn"><span class="icon">↑</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"
>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>
CSS
@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;
}
Ağaçta [softDisabled]="true" olduğunda, devre dışı öğeler odak alabilir ancak etkinleştirilemez veya seçilemez. [softDisabled]="false" olduğunda, devre dışı öğeler klavye navigasyonu sırasında atlanır.
Test Etme
Angular Aria, ağaç bileşenlerini test etmek için bileşen koşumları (harness) sağlar. Bir bileşen testinde koşumların nasıl kullanılacağına dair bir örnek aşağıdadır:
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'; // Bileşeniniz
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);
// Üst düzey yapı temsilini al
expect(await tree.getTreeStructure()).toEqual({
children: [{text: 'public'}, {text: 'src'}, {text: 'package.json'}],
});
// Tüm öğeleri al (şu anda görünür olanlar)
const items = await tree.getItems();
expect(items.length).toBe(3);
// İlk öğeyi genişlet ('public')
expect(await items[0].isExpanded()).toBe(false);
await items[0].click();
expect(await items[0].isExpanded()).toBe(true);
// Genişletmeden sonra ağaç yapısının güncellendiğini doğrula
expect(await tree.getTreeStructure()).toEqual({
children: [
{
text: 'public',
children: [{text: 'index.html'}, {text: 'styles.css'}],
},
{text: 'src'},
{text: 'package.json'},
],
});
});
});
API referansı
Ayrıntılı API belgeleri için aşağıdaki API referanslarını inceleyin: