|
<!DOCTYPE
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Tree Navigation Menu</title>
|
|
<style>
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
padding: 20px;
|
|
background: #f5f5f5;
|
|
}
|
|
|
|
.tree-container {
|
|
max-width: 400px;
|
|
background: white;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
padding: 16px;
|
|
}
|
|
|
|
/* Tree structure */
|
|
.tree {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
|
|
.tree ul {
|
|
list-style: none;
|
|
padding-left: 24px;
|
|
margin: 0;
|
|
}
|
|
|
|
/* Tree items */
|
|
.tree-item {
|
|
margin: 2px 0;
|
|
}
|
|
|
|
.tree-item-content {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 8px 12px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
position: relative;
|
|
}
|
|
|
|
/* Focus and hover states */
|
|
.tree-item-content:hover {
|
|
background: #f0f0f0;
|
|
}
|
|
|
|
.tree-item-content:focus {
|
|
outline: 2px solid #0066cc;
|
|
outline-offset: -2px;
|
|
background: #e6f2ff;
|
|
}
|
|
|
|
.tree-item-content.selected {
|
|
background: #0066cc;
|
|
color: white;
|
|
}
|
|
|
|
/* Icons */
|
|
.tree-icon {
|
|
width: 20px;
|
|
height: 20px;
|
|
margin-right: 8px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.folder-icon::before {
|
|
content: "📁";
|
|
font-size: 16px;
|
|
}
|
|
|
|
.folder-icon.open::before {
|
|
content: "📂";
|
|
}
|
|
|
|
.file-icon::before {
|
|
content: "📄";
|
|
font-size: 16px;
|
|
}
|
|
|
|
/* Expand/collapse arrow */
|
|
.expand-icon {
|
|
width: 16px;
|
|
height: 16px;
|
|
margin-right: 4px;
|
|
transition: transform 0.2s ease;
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.expand-icon::before {
|
|
content: "▶";
|
|
font-size: 10px;
|
|
}
|
|
|
|
.expand-icon.expanded {
|
|
transform: rotate(90deg);
|
|
}
|
|
|
|
.expand-icon.no-children {
|
|
visibility: hidden;
|
|
}
|
|
|
|
/* Hidden when collapsed */
|
|
.collapsed {
|
|
display: none;
|
|
}
|
|
|
|
/* Instructions */
|
|
.instructions {
|
|
margin-top: 20px;
|
|
padding: 16px;
|
|
background: #f9f9f9;
|
|
border-radius: 4px;
|
|
font-size: 14px;
|
|
color: #666;
|
|
}
|
|
|
|
.instructions h3 {
|
|
margin-top: 0;
|
|
color: #333;
|
|
}
|
|
|
|
.instructions kbd {
|
|
background: #eee;
|
|
padding: 2px 6px;
|
|
border-radius: 3px;
|
|
border: 1px solid #ccc;
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="tree-container">
|
|
<nav role="navigation" aria-label="File tree navigation">
|
|
<ul class="tree" role="tree" id="fileTree">
|
|
<li class="tree-item" role="treeitem" aria-expanded="false" data-type="folder">
|
|
<div class="tree-item-content" tabindex="0">
|
|
<span class="expand-icon"></span>
|
|
<span class="tree-icon folder-icon"></span>
|
|
<span class="tree-label">Another</span>
|
|
</div>
|
|
<ul role="group" class="collapsed">
|
|
<li class="tree-item" role="treeitem" data-type="file">
|
|
<div class="tree-item-content" tabindex="-1">
|
|
<span class="expand-icon no-children"></span>
|
|
<span class="tree-icon file-icon"></span>
|
|
<span class="tree-label">Subfilee1.txt</span>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</li>
|
|
|
|
<li class="tree-item" role="treeitem" aria-expanded="false" data-type="folder">
|
|
<div class="tree-item-content" tabindex="-1">
|
|
<span class="expand-icon"></span>
|
|
<span class="tree-icon folder-icon"></span>
|
|
<span class="tree-label">Folder</span>
|
|
</div>
|
|
<ul role="group" class="collapsed">
|
|
<li class="tree-item" role="treeitem" data-type="file">
|
|
<div class="tree-item-content" tabindex="-1">
|
|
<span class="expand-icon no-children"></span>
|
|
<span class="tree-icon file-icon"></span>
|
|
<span class="tree-label">Document.pdf</span>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</li>
|
|
|
|
<li class="tree-item" role="treeitem" aria-expanded="false" data-type="folder">
|
|
<div class="tree-item-content" tabindex="-1">
|
|
<span class="expand-icon"></span>
|
|
<span class="tree-icon folder-icon"></span>
|
|
<span class="tree-label">Folder</span>
|
|
</div>
|
|
<ul role="group" class="collapsed"></ul>
|
|
</li>
|
|
|
|
<li class="tree-item" role="treeitem" aria-expanded="true" data-type="folder">
|
|
<div class="tree-item-content" tabindex="-1">
|
|
<span class="expand-icon expanded"></span>
|
|
<span class="tree-icon folder-icon open"></span>
|
|
<span class="tree-label">Folder 1</span>
|
|
</div>
|
|
<ul role="group">
|
|
<li class="tree-item" role="treeitem" data-type="file">
|
|
<div class="tree-item-content" tabindex="-1">
|
|
<span class="expand-icon no-children"></span>
|
|
<span class="tree-icon file-icon"></span>
|
|
<span class="tree-label">Nochmal ne Liste</span>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</li>
|
|
|
|
<li class="tree-item" role="treeitem" aria-expanded="false" data-type="folder">
|
|
<div class="tree-item-content" tabindex="-1">
|
|
<span class="expand-icon"></span>
|
|
<span class="tree-icon folder-icon"></span>
|
|
<span class="tree-label">Nice</span>
|
|
</div>
|
|
<ul role="group" class="collapsed"></ul>
|
|
</li>
|
|
|
|
<li class="tree-item" role="treeitem" aria-expanded="true" data-type="folder">
|
|
<div class="tree-item-content" tabindex="-1">
|
|
<span class="expand-icon expanded"></span>
|
|
<span class="tree-icon folder-icon open"></span>
|
|
<span class="tree-label">Some</span>
|
|
</div>
|
|
<ul role="group">
|
|
<li class="tree-item" role="treeitem" data-type="file">
|
|
<div class="tree-item-content" tabindex="-1">
|
|
<span class="expand-icon no-children"></span>
|
|
<span class="tree-icon file-icon"></span>
|
|
<span class="tree-label">List</span>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</li>
|
|
|
|
<li class="tree-item" role="treeitem" aria-expanded="false" data-type="folder">
|
|
<div class="tree-item-content" tabindex="-1">
|
|
<span class="expand-icon"></span>
|
|
<span class="tree-icon folder-icon"></span>
|
|
<span class="tree-label">Test Folder 06:54:51</span>
|
|
</div>
|
|
<ul role="group" class="collapsed"></ul>
|
|
</li>
|
|
|
|
<li class="tree-item" role="treeitem" aria-expanded="true" data-type="folder">
|
|
<div class="tree-item-content" tabindex="-1">
|
|
<span class="expand-icon expanded"></span>
|
|
<span class="tree-icon folder-icon open"></span>
|
|
<span class="tree-label">Test Folder 23:31:47</span>
|
|
</div>
|
|
<ul role="group">
|
|
<li class="tree-item" role="treeitem" data-type="file">
|
|
<div class="tree-item-content" tabindex="-1">
|
|
<span class="expand-icon no-children"></span>
|
|
<span class="tree-icon file-icon"></span>
|
|
<span class="tree-label">Geiler Schyze</span>
|
|
</div>
|
|
</li>
|
|
<li class="tree-item" role="treeitem" data-type="file">
|
|
<div class="tree-item-content" tabindex="-1">
|
|
<span class="expand-icon no-children"></span>
|
|
<span class="tree-icon file-icon"></span>
|
|
<span class="tree-label">Ne Liste</span>
|
|
</div>
|
|
</li>
|
|
<li class="tree-item" role="treeitem" data-type="file">
|
|
<div class="tree-item-content" tabindex="-1">
|
|
<span class="expand-icon no-children"></span>
|
|
<span class="tree-icon file-icon"></span>
|
|
<span class="tree-label">Das ne Testlist</span>
|
|
</div>
|
|
</li>
|
|
<li class="tree-item" role="treeitem" data-type="file">
|
|
<div class="tree-item-content" tabindex="-1">
|
|
<span class="expand-icon no-children"></span>
|
|
<span class="tree-icon file-icon"></span>
|
|
<span class="tree-label">HHB</span>
|
|
</div>
|
|
</li>
|
|
<li class="tree-item" role="treeitem" data-type="file">
|
|
<div class="tree-item-content" tabindex="-1">
|
|
<span class="expand-icon no-children"></span>
|
|
<span class="tree-icon file-icon"></span>
|
|
<span class="tree-label">Ab ins</span>
|
|
</div>
|
|
</li>
|
|
<li class="tree-item" role="treeitem" data-type="file">
|
|
<div class="tree-item-content" tabindex="-1">
|
|
<span class="expand-icon no-children"></span>
|
|
<span class="tree-icon file-icon"></span>
|
|
<span class="tree-label">Test</span>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
|
|
<div class="instructions">
|
|
<h3>Keyboard Navigation:</h3>
|
|
<ul>
|
|
<li><kbd>↑</kbd> <kbd>↓</kbd> - Navigate between items</li>
|
|
<li><kbd>←</kbd> - Collapse folder or move to parent</li>
|
|
<li><kbd>→</kbd> - Expand folder or move to first child</li>
|
|
<li><kbd>Enter</kbd> or <kbd>Space</kbd> - Select item / Toggle folder</li>
|
|
<li><kbd>Home</kbd> - Jump to first item</li>
|
|
<li><kbd>End</kbd> - Jump to last item</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<script>
|
|
class TreeNavigation {
|
|
constructor(treeElement) {
|
|
this.tree = treeElement;
|
|
this.items = [];
|
|
this.currentIndex = 0;
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.updateItemsList();
|
|
this.attachEventListeners();
|
|
|
|
// Set initial focus
|
|
if (this.items.length > 0) {
|
|
this.setFocus(0);
|
|
}
|
|
}
|
|
|
|
updateItemsList() {
|
|
// Get all visible tree items
|
|
this.items = Array.from(this.tree.querySelectorAll('.tree-item-content'))
|
|
.filter(item => {
|
|
// Check if item is visible (not in collapsed parent)
|
|
let parent = item.parentElement;
|
|
while (parent && parent !== this.tree) {
|
|
if (parent.classList.contains('collapsed')) {
|
|
return false;
|
|
}
|
|
parent = parent.parentElement;
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
|
|
attachEventListeners() {
|
|
// Keyboard navigation
|
|
this.tree.addEventListener('keydown', (e) => this.handleKeyDown(e));
|
|
|
|
// Mouse click
|
|
this.tree.addEventListener('click', (e) => {
|
|
const itemContent = e.target.closest('.tree-item-content');
|
|
if (itemContent) {
|
|
const index = this.items.indexOf(itemContent);
|
|
if (index !== -1) {
|
|
this.setFocus(index);
|
|
this.toggleItem(itemContent);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
handleKeyDown(e) {
|
|
const item = e.target.closest('.tree-item-content');
|
|
if (!item) return;
|
|
|
|
switch (e.key) {
|
|
case 'ArrowDown':
|
|
e.preventDefault();
|
|
this.focusNext();
|
|
break;
|
|
|
|
case 'ArrowUp':
|
|
e.preventDefault();
|
|
this.focusPrevious();
|
|
break;
|
|
|
|
case 'ArrowRight':
|
|
e.preventDefault();
|
|
this.expandOrMoveToChild(item);
|
|
break;
|
|
|
|
case 'ArrowLeft':
|
|
e.preventDefault();
|
|
this.collapseOrMoveToParent(item);
|
|
break;
|
|
|
|
case 'Enter':
|
|
case ' ':
|
|
e.preventDefault();
|
|
this.toggleItem(item);
|
|
break;
|
|
|
|
case 'Home':
|
|
e.preventDefault();
|
|
this.setFocus(0);
|
|
break;
|
|
|
|
case 'End':
|
|
e.preventDefault();
|
|
this.setFocus(this.items.length - 1);
|
|
break;
|
|
}
|
|
}
|
|
|
|
focusNext() {
|
|
if (this.currentIndex < this.items.length - 1) {
|
|
this.setFocus(this.currentIndex + 1);
|
|
}
|
|
}
|
|
|
|
focusPrevious() {
|
|
if (this.currentIndex > 0) {
|
|
this.setFocus(this.currentIndex - 1);
|
|
}
|
|
}
|
|
|
|
setFocus(index) {
|
|
// Remove tabindex from all items
|
|
this.items.forEach(item => item.setAttribute('tabindex', '-1'));
|
|
|
|
// Set tabindex and focus on current item
|
|
if (this.items[index]) {
|
|
this.currentIndex = index;
|
|
this.items[index].setAttribute('tabindex', '0');
|
|
this.items[index].focus();
|
|
}
|
|
}
|
|
|
|
expandOrMoveToChild(item) {
|
|
const treeItem = item.parentElement;
|
|
const isFolder = treeItem.getAttribute('data-type') === 'folder';
|
|
|
|
if (isFolder) {
|
|
const isExpanded = treeItem.getAttribute('aria-expanded') === 'true';
|
|
if (!isExpanded) {
|
|
this.expandFolder(treeItem);
|
|
} else {
|
|
// Move to first child if expanded
|
|
const firstChild = treeItem.querySelector('ul > li > .tree-item-content');
|
|
if (firstChild) {
|
|
this.updateItemsList();
|
|
const index = this.items.indexOf(firstChild);
|
|
if (index !== -1) {
|
|
this.setFocus(index);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
collapseOrMoveToParent(item) {
|
|
const treeItem = item.parentElement;
|
|
const isFolder = treeItem.getAttribute('data-type') === 'folder';
|
|
const isExpanded = treeItem.getAttribute('aria-expanded') === 'true';
|
|
|
|
if (isFolder && isExpanded) {
|
|
this.collapseFolder(treeItem);
|
|
} else {
|
|
// Move to parent folder
|
|
const parentGroup = treeItem.parentElement.closest('li[role="treeitem"]');
|
|
if (parentGroup) {
|
|
const parentContent = parentGroup.querySelector('.tree-item-content');
|
|
const index = this.items.indexOf(parentContent);
|
|
if (index !== -1) {
|
|
this.setFocus(index);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
toggleItem(item) {
|
|
const treeItem = item.parentElement;
|
|
const isFolder = treeItem.getAttribute('data-type') === 'folder';
|
|
|
|
// Remove selected class from all items
|
|
this.items.forEach(i => i.classList.remove('selected'));
|
|
|
|
// Add selected class to current item
|
|
item.classList.add('selected');
|
|
|
|
if (isFolder) {
|
|
const isExpanded = treeItem.getAttribute('aria-expanded') === 'true';
|
|
if (isExpanded) {
|
|
this.collapseFolder(treeItem);
|
|
} else {
|
|
this.expandFolder(treeItem);
|
|
}
|
|
} else {
|
|
// File selected - you can add custom behavior here
|
|
console.log('Selected file:', item.querySelector('.tree-label').textContent);
|
|
}
|
|
}
|
|
|
|
expandFolder(treeItem) {
|
|
treeItem.setAttribute('aria-expanded', 'true');
|
|
const group = treeItem.querySelector('ul[role="group"]');
|
|
const expandIcon = treeItem.querySelector('.expand-icon');
|
|
const folderIcon = treeItem.querySelector('.folder-icon');
|
|
|
|
if (group) {
|
|
group.classList.remove('collapsed');
|
|
}
|
|
if (expandIcon) {
|
|
expandIcon.classList.add('expanded');
|
|
}
|
|
if (folderIcon) {
|
|
folderIcon.classList.add('open');
|
|
}
|
|
|
|
this.updateItemsList();
|
|
}
|
|
|
|
collapseFolder(treeItem) {
|
|
treeItem.setAttribute('aria-expanded', 'false');
|
|
const group = treeItem.querySelector('ul[role="group"]');
|
|
const expandIcon = treeItem.querySelector('.expand-icon');
|
|
const folderIcon = treeItem.querySelector('.folder-icon');
|
|
|
|
if (group) {
|
|
group.classList.add('collapsed');
|
|
}
|
|
if (expandIcon) {
|
|
expandIcon.classList.remove('expanded');
|
|
}
|
|
if (folderIcon) {
|
|
folderIcon.classList.remove('open');
|
|
}
|
|
|
|
this.updateItemsList();
|
|
}
|
|
}
|
|
|
|
// Initialize the tree navigation
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const tree = document.getElementById('fileTree');
|
|
new TreeNavigation(tree);
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|