mirror of
https://github.com/litruv/Docs-Viewer.git
synced 2026-07-24 02:36:07 +10:00
add roles and focus trap for mobile sidebar navigation
This commit is contained in:
@@ -33,9 +33,9 @@
|
|||||||
<div class="search-container">
|
<div class="search-container">
|
||||||
<div class="search-box" role="search">
|
<div class="search-box" role="search">
|
||||||
<i class="fas fa-search" aria-hidden="true"></i>
|
<i class="fas fa-search" aria-hidden="true"></i>
|
||||||
<input type="text" id="search-input" placeholder="Search docs..." aria-label="Search documentation">
|
<input type="text" id="search-input" placeholder="Search docs..." aria-label="Search documentation" role="searchbox">
|
||||||
<div class="keyboard-shortcut keyboard-shortcut-alt" aria-hidden="true">Alt+S</div>
|
<div class="keyboard-shortcut keyboard-shortcut-alt" aria-hidden="true">Alt+S</div>
|
||||||
<i class="fas fa-times" id="clear-search" aria-label="Clear search"></i>
|
<i class="fas fa-times" id="clear-search" aria-label="Clear search" role="button" tabindex="0"></i>
|
||||||
</div>
|
</div>
|
||||||
<div id="search-results" role="region" aria-label="Search results" aria-live="polite"></div>
|
<div id="search-results" role="region" aria-label="Search results" aria-live="polite"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,10 +36,73 @@ export class DOMService {
|
|||||||
this.elements.menuButton.addEventListener('click', () => {
|
this.elements.menuButton.addEventListener('click', () => {
|
||||||
const isExpanded = this.elements.leftSidebar.classList.toggle('show');
|
const isExpanded = this.elements.leftSidebar.classList.toggle('show');
|
||||||
this.elements.menuButton.setAttribute('aria-expanded', isExpanded ? 'true' : 'false');
|
this.elements.menuButton.setAttribute('aria-expanded', isExpanded ? 'true' : 'false');
|
||||||
|
|
||||||
|
if (isExpanded) {
|
||||||
|
this.enableFocusTrap();
|
||||||
|
} else {
|
||||||
|
this.disableFocusTrap();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.elements.content.addEventListener('click', () =>
|
this.elements.content.addEventListener('click', () => {
|
||||||
this.elements.leftSidebar.classList.remove('show'));
|
this.elements.leftSidebar.classList.remove('show');
|
||||||
|
this.elements.menuButton.setAttribute('aria-expanded', 'false');
|
||||||
|
this.disableFocusTrap();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables focus trap within the mobile sidebar.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
enableFocusTrap() {
|
||||||
|
this.focusTrapHandler = (e) => {
|
||||||
|
if (window.innerWidth > 1000) return;
|
||||||
|
if (!this.elements.leftSidebar.classList.contains('show')) return;
|
||||||
|
|
||||||
|
const focusableElements = this.elements.leftSidebar.querySelectorAll(
|
||||||
|
'a[href], button, input, [tabindex]:not([tabindex="-1"])'
|
||||||
|
);
|
||||||
|
const firstFocusable = focusableElements[0];
|
||||||
|
const lastFocusable = focusableElements[focusableElements.length - 1];
|
||||||
|
|
||||||
|
if (e.key === 'Tab') {
|
||||||
|
if (e.shiftKey && document.activeElement === firstFocusable) {
|
||||||
|
e.preventDefault();
|
||||||
|
lastFocusable.focus();
|
||||||
|
} else if (!e.shiftKey && document.activeElement === lastFocusable) {
|
||||||
|
e.preventDefault();
|
||||||
|
firstFocusable.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
this.elements.leftSidebar.classList.remove('show');
|
||||||
|
this.elements.menuButton.setAttribute('aria-expanded', 'false');
|
||||||
|
this.elements.menuButton.focus();
|
||||||
|
this.disableFocusTrap();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', this.focusTrapHandler);
|
||||||
|
|
||||||
|
const firstFocusable = this.elements.leftSidebar.querySelector(
|
||||||
|
'a[href], button, input, [tabindex]:not([tabindex="-1"])'
|
||||||
|
);
|
||||||
|
if (firstFocusable) {
|
||||||
|
firstFocusable.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables the focus trap.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
disableFocusTrap() {
|
||||||
|
if (this.focusTrapHandler) {
|
||||||
|
document.removeEventListener('keydown', this.focusTrapHandler);
|
||||||
|
this.focusTrapHandler = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -182,7 +245,13 @@ export class DOMService {
|
|||||||
*/
|
*/
|
||||||
updateActiveDocument(path) {
|
updateActiveDocument(path) {
|
||||||
this.elements.fileIndex.querySelectorAll('a').forEach(link => {
|
this.elements.fileIndex.querySelectorAll('a').forEach(link => {
|
||||||
link.classList.toggle('active', link.dataset.path === path);
|
const isActive = link.dataset.path === path;
|
||||||
|
link.classList.toggle('active', isActive);
|
||||||
|
if (isActive) {
|
||||||
|
link.setAttribute('aria-current', 'page');
|
||||||
|
} else {
|
||||||
|
link.removeAttribute('aria-current');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user