refactoring of cmaps, widgets, etc.
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Present an existing menu at a viewport position.
|
||||
*
|
||||
* PopupMenu owns placement, dismissal and keyboard movement. The application
|
||||
* remains responsible for menu actions and for enabling individual items.
|
||||
*/
|
||||
export class PopupMenu {
|
||||
constructor(element, trigger = null) {
|
||||
this.element = element;
|
||||
this.trigger = trigger;
|
||||
|
||||
if (this.trigger) {
|
||||
this.trigger.setAttribute("aria-haspopup", "menu");
|
||||
this.trigger.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
|
||||
this.element.addEventListener("click", (event) => {
|
||||
if (event.target.closest('[role^="menuitem"]')) this.close();
|
||||
});
|
||||
this.element.addEventListener("keydown", (event) => this.handleKeydown(event));
|
||||
document.addEventListener("pointerdown", (event) => {
|
||||
if (!this.isOpen()) return;
|
||||
if (this.element.contains(event.target)) return;
|
||||
if (this.trigger && this.trigger.contains(event.target)) return;
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
|
||||
isOpen() {
|
||||
return !this.element.classList.contains("hidden");
|
||||
}
|
||||
|
||||
/** Open the menu at client coordinates and keep it inside the viewport. */
|
||||
openAt(clientX, clientY) {
|
||||
if (this.element.parentElement !== document.body) document.body.append(this.element);
|
||||
this.element.classList.remove("hidden");
|
||||
const left = Math.max(8, Math.min(clientX, window.innerWidth - this.element.offsetWidth - 8));
|
||||
const top = Math.max(8, Math.min(clientY, window.innerHeight - this.element.offsetHeight - 8));
|
||||
this.element.style.left = `${left}px`;
|
||||
this.element.style.top = `${top}px`;
|
||||
if (this.trigger) this.trigger.setAttribute("aria-expanded", "true");
|
||||
const first = this.items()[0];
|
||||
if (first) first.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
close() {
|
||||
const returnFocus = this.element.contains(document.activeElement);
|
||||
this.element.classList.add("hidden");
|
||||
if (this.trigger) this.trigger.setAttribute("aria-expanded", "false");
|
||||
if (returnFocus && this.trigger) this.trigger.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
items() {
|
||||
return Array.from(this.element.querySelectorAll('[role^="menuitem"]'))
|
||||
.filter((item) => !item.disabled && !item.classList.contains("hidden"));
|
||||
}
|
||||
|
||||
handleKeydown(event) {
|
||||
const items = this.items();
|
||||
const current = items.indexOf(document.activeElement);
|
||||
let next = null;
|
||||
if (event.key === "ArrowDown") next = current < items.length - 1 ? current + 1 : 0;
|
||||
if (event.key === "ArrowUp") next = current > 0 ? current - 1 : items.length - 1;
|
||||
if (event.key === "Home") next = 0;
|
||||
if (event.key === "End") next = items.length - 1;
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
if (next === null || !items[next]) return;
|
||||
event.preventDefault();
|
||||
items[next].focus({ preventScroll: true });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user