mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
WIP: Robust menu list arrow key navigation
This commit is contained in:
@@ -158,17 +158,23 @@
|
||||
dispatch("activeEntry", menuListEntry);
|
||||
dispatch("selectedEntryValuePath", [...parentsValuePath, menuListEntry.value]);
|
||||
|
||||
// Close the containing menu
|
||||
let childReference = getChildReference(menuListEntry);
|
||||
// Close the child menu, which we will re-open by calling `onPointerEnter` below, in order to remove its highlighted state
|
||||
let childReference = highlighted && getChildReference(highlighted);
|
||||
if (childReference) {
|
||||
childReference.open = false;
|
||||
entries = entries;
|
||||
}
|
||||
dispatch("open", false);
|
||||
open = false;
|
||||
|
||||
// Remove the highlighted state from the current (clicked) entry
|
||||
setHighlighted(undefined);
|
||||
|
||||
// Re-spawn the child menu that we have clicked on, just like we hovered over it (`onPointerEnter`)
|
||||
onEntryPointerEnter(menuListEntry);
|
||||
}
|
||||
|
||||
function onEntryPointerEnter(menuListEntry: MenuListEntry) {
|
||||
if (highlighted) return;
|
||||
|
||||
if (!menuListEntry.children?.length) {
|
||||
dispatch("hoverInEntry", menuListEntry);
|
||||
return;
|
||||
@@ -178,10 +184,14 @@
|
||||
if (childReference) {
|
||||
childReference.open = true;
|
||||
entries = entries;
|
||||
} else dispatch("open", true);
|
||||
} else {
|
||||
dispatch("open", true);
|
||||
}
|
||||
}
|
||||
|
||||
function onEntryPointerLeave(menuListEntry: MenuListEntry) {
|
||||
if (highlighted) return;
|
||||
|
||||
if (!menuListEntry.children?.length) {
|
||||
dispatch("hoverOutEntry");
|
||||
return;
|
||||
@@ -191,7 +201,9 @@
|
||||
if (childReference) {
|
||||
childReference.open = false;
|
||||
entries = entries;
|
||||
} else dispatch("open", false);
|
||||
} else {
|
||||
dispatch("open", false);
|
||||
}
|
||||
}
|
||||
|
||||
function isEntryOpen(menuListEntry: MenuListEntry): boolean {
|
||||
@@ -219,23 +231,21 @@
|
||||
|
||||
function openSubmenu(highlightedEntry: MenuListEntry): boolean {
|
||||
let childReference = getChildReference(highlightedEntry);
|
||||
// No submenu to open
|
||||
|
||||
// Exit if there is no submenu to open
|
||||
if (!childReference || !highlightedEntry.children?.length) return false;
|
||||
|
||||
childReference.open = true;
|
||||
// The reason we bother taking `highlightdEntry` as an argument is because, when this function is called, it can ensure `highlightedEntry` is not undefined.
|
||||
// But here we still have to set `highlighted` to itself so Svelte knows to reactively update it after we set its `childReference.open` property.
|
||||
highlighted = highlighted;
|
||||
|
||||
// Highlight first item
|
||||
childReference.setHighlighted(highlightedEntry.children[0][0]);
|
||||
// Ensure this item in the parent menu is highlighted
|
||||
setHighlighted(highlightedEntry);
|
||||
|
||||
// Submenu was opened
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Handles keyboard navigation for the menu.
|
||||
// Returns a boolean indicating whether the entire menu stack should be dismissed.
|
||||
/// Returns a boolean indicating whether the entire menu stack should be dismissed.
|
||||
export function keydown(e: KeyboardEvent, submenu = false): boolean {
|
||||
// Interactive menus should keep the active entry the same as the highlighted one
|
||||
if (interactive) highlighted = activeEntry;
|
||||
@@ -253,17 +263,24 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log("openChild:", openChild, "highlighted:", highlighted, "menuOpen:", menuOpen);
|
||||
|
||||
// If a submenu is open, have it handle this instead
|
||||
if (menuOpen && openChild >= 0) {
|
||||
const childMenuListEntry = flatEntries[openChild];
|
||||
const childMenu = getChildReference(childMenuListEntry);
|
||||
console.log("openChild:", openChild, "childMenu:", childMenu);
|
||||
|
||||
// Redirect the keyboard navigation to a submenu if one is open
|
||||
const shouldCloseStack = childMenu?.keydown(e, true) || false;
|
||||
if (!highlighted && (e.key === "ArrowRight" || e.key === " " || e.key === "Enter")) {
|
||||
openSubmenu(childMenuListEntry);
|
||||
}
|
||||
|
||||
// Highlight the menu item in the parent list that corresponds with the open submenu
|
||||
if (highlighted && e.key !== "Escape") setHighlighted(childMenuListEntry);
|
||||
|
||||
// Redirect the keyboard navigation to a submenu if one is open
|
||||
const shouldCloseStack = childMenu?.keydown(e, true) || false;
|
||||
|
||||
// Handle the child closing the entire menu stack
|
||||
if (shouldCloseStack) open = false;
|
||||
|
||||
@@ -271,6 +288,13 @@
|
||||
return shouldCloseStack;
|
||||
}
|
||||
|
||||
// If this is the deepest sub-menu being activated by keyboard navigation, highlight its first entry
|
||||
if (menuOpen && openChild === -1 && !highlighted && (e.key === "ArrowRight" || e.key === " " || e.key === "Enter")) {
|
||||
// If no entry is highlighted, highlight the first entry
|
||||
highlighted = flatEntries[0];
|
||||
setHighlighted(highlighted);
|
||||
}
|
||||
|
||||
// Navigate to the next and previous entries with arrow keys
|
||||
if ((menuOpen || interactive) && (e.key === "ArrowUp" || e.key === "ArrowDown")) {
|
||||
let newIndex = e.key === "ArrowUp" ? flatEntries.length - 1 : 0;
|
||||
@@ -292,9 +316,10 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
// Close menu with escape key
|
||||
// Close menu with escape key, or un-highlight the current entry if it's highlighted
|
||||
if (menuOpen && e.key === "Escape") {
|
||||
open = false;
|
||||
// Close the menu
|
||||
if (submenu || !highlighted) open = false;
|
||||
|
||||
// Reset active to before open
|
||||
setHighlighted(activeEntry);
|
||||
@@ -305,7 +330,7 @@
|
||||
|
||||
// Click on a highlighted entry with the enter key
|
||||
if (menuOpen && highlighted && e.key === "Enter") {
|
||||
// Handle clicking on an option if enter is pressed
|
||||
// Handle clicking on an option if enter is pressed, or open its submenu if it has one
|
||||
if (!highlighted.children?.length) onEntryClick(highlighted);
|
||||
else openSubmenu(highlighted);
|
||||
|
||||
@@ -335,6 +360,8 @@
|
||||
open = false;
|
||||
|
||||
e.preventDefault();
|
||||
} else if (highlighted) {
|
||||
setHighlighted(activeEntry);
|
||||
}
|
||||
|
||||
// Keep the menu stack open
|
||||
|
||||
@@ -420,7 +420,7 @@
|
||||
|
||||
function pointerDownHandler(e: PointerEvent) {
|
||||
// Close the floating menu if the pointer clicked outside the floating menu (but within stray distance)
|
||||
if (isPointerEventOutsideFloatingMenu(e)) {
|
||||
if (!self?.closest("[data-floating-menu-content]") && isPointerEventOutsideFloatingMenu(e)) {
|
||||
dispatch("open", false);
|
||||
|
||||
// Track if the left pointer button is now down so its later click event can be canceled
|
||||
@@ -448,10 +448,15 @@
|
||||
}
|
||||
|
||||
function isPointerEventOutsideFloatingMenu(e: PointerEvent, extraDistanceAllowed = 0): boolean {
|
||||
// Consider all child menus as well as the top-level one
|
||||
// Consider all self-or-child menus as well as the top-level one
|
||||
const allContainedFloatingMenus = [...(self?.querySelectorAll("[data-floating-menu-content]") || [])];
|
||||
|
||||
return !allContainedFloatingMenus.find((element) => !isPointerEventOutsideMenuElement(e, element, extraDistanceAllowed));
|
||||
// If we find no self-or-child menus that this pointer event is inside, then the pointer event is outside all the floating menus
|
||||
const result = !allContainedFloatingMenus.find((element) => !isPointerEventOutsideMenuElement(e, element, extraDistanceAllowed));
|
||||
|
||||
if (result) console.log("Result is true");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function isPointerEventOutsideMenuElement(e: PointerEvent, element: Element, extraDistanceAllowed = 0): boolean {
|
||||
|
||||
Reference in New Issue
Block a user