Support custom target(s) for ContextMenu (#916)

* feat(context-menu): add target prop to selectively trigger context menu

* feat(context-menu): include clicked element in "open" event detail

* docs(context-menu): add target, multiple target examples
This commit is contained in:
Eric Liu 2021-11-18 13:32:13 -08:00 committed by GitHub
commit ccc733f3f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 261 additions and 52 deletions

View file

@ -1,4 +1,15 @@
<script>
/**
* @event {HTMLElement} open
*/
/**
* Specify an element or list of elements to trigger the context menu.
* If no element is specified, the context menu applies to the entire window
* @type {null | HTMLElement | HTMLElement[]}
*/
export let target = null;
/**
* Set to `true` to open the menu
* Either `x` and `y` must be greater than zero
@ -15,6 +26,7 @@
export let ref = null;
import {
onMount,
setContext,
getContext,
afterUpdate,
@ -34,6 +46,7 @@
let prevX = 0;
let prevY = 0;
let focusIndex = -1;
let openDetail = null;
function close() {
open = false;
@ -44,43 +57,8 @@
focusIndex = -1;
}
setContext("ContextMenu", {
menuOffsetX,
currentIndex,
position,
close,
setPopup: (popup) => {
hasPopup.set(popup);
},
});
afterUpdate(() => {
if (open) {
options = [...ref.querySelectorAll("li[data-nested='false']")];
if (level === 1) {
if (prevX !== x || prevY !== y) ref.focus();
prevX = x;
prevY = y;
}
dispatch("open");
} else {
dispatch("close");
}
if (!$hasPopup && options[focusIndex]) options[focusIndex].focus();
});
$: level = !ctx ? 1 : 2;
$: currentIndex.set(focusIndex);
</script>
<svelte:window
on:contextmenu|preventDefault="{(e) => {
if (level > 1) return;
if (!ref) return;
/** @type {(e: MouseEvent) => void} */
function openMenu(e) {
const { height, width } = ref.getBoundingClientRect();
if (open || x === 0) {
@ -102,6 +80,69 @@
}
position.set([x, y]);
open = true;
openDetail = e.target;
}
$: if (target != null) {
if (Array.isArray(target)) {
target.forEach((node) => node?.addEventListener("contextmenu", openMenu));
} else {
target.addEventListener("contextmenu", openMenu);
}
}
onMount(() => {
return () => {
if (target != null) {
if (Array.isArray(target)) {
target.forEach((node) =>
node?.removeEventListener("contextmenu", openMenu)
);
} else {
target.removeEventListener("contextmenu", openMenu);
}
}
};
});
setContext("ContextMenu", {
menuOffsetX,
currentIndex,
position,
close,
setPopup: (popup) => {
hasPopup.set(popup);
},
});
afterUpdate(() => {
if (open) {
options = [...ref.querySelectorAll("li[data-nested='false']")];
if (level === 1) {
if (prevX !== x || prevY !== y) ref.focus();
prevX = x;
prevY = y;
}
dispatch("open", openDetail);
} else {
dispatch("close");
}
if (!$hasPopup && options[focusIndex]) options[focusIndex].focus();
});
$: level = !ctx ? 1 : 2;
$: currentIndex.set(focusIndex);
</script>
<svelte:window
on:contextmenu|preventDefault="{(e) => {
if (target != null) return;
if (level > 1) return;
if (!ref) return;
openMenu(e);
}}"
on:click="{(e) => {
if (!open) return;