extract ContextMenuInner.svelte

This commit is contained in:
Moritz Bischof 2021-05-03 19:22:37 +02:00
commit 3308fd4116
3 changed files with 146 additions and 110 deletions

View file

@ -1,4 +1,6 @@
<script> <script>
import ContextMenuInner from "./ContextMenuInner.svelte";
/** /**
* Set to `true` to open the menu * Set to `true` to open the menu
* Either `x` and `y` must be greater than zero * Either `x` and `y` must be greater than zero
@ -14,91 +16,29 @@
/** Obtain a reference to the unordered list HTML element */ /** Obtain a reference to the unordered list HTML element */
export let ref = null; export let ref = null;
import { import { getContext } from "svelte";
setContext,
getContext,
afterUpdate,
createEventDispatcher,
} from "svelte";
import { writable } from "svelte/store";
const dispatch = createEventDispatcher();
const position = writable([x, y]);
const currentIndex = writable(-1);
const hasPopup = writable(false);
const ctx = getContext("ContextMenu"); const ctx = getContext("ContextMenu");
let options = [];
let direction = 1;
let prevX = 0;
let prevY = 0;
let focusIndex = -1;
let level; let level;
$: level = !ctx ? 1 : 2;
function close() { function close() {
open = false; open = false;
x = 0; x = 0;
y = 0; y = 0;
prevX = 0;
prevY = 0;
focusIndex = -1;
} }
setContext("ContextMenu", { function onContextMenu(e) {
currentIndex, if (level > 1) return;
position, open = true;
close, x = e.x;
setPopup: (popup) => { y = e.y;
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> </script>
<svelte:window <svelte:window
on:contextmenu|preventDefault="{(e) => { on:contextmenu|preventDefault="{onContextMenu}"
if (level > 1) return;
const { height, width } = ref.getBoundingClientRect();
if (open || x === 0) {
if (window.innerWidth - width < e.x) {
x = e.x - width;
} else {
x = e.x;
}
}
if (open || y === 0) {
if (window.innerHeight - height < e.y) {
y = e.y - height;
} else {
y = e.y;
}
}
position.set([x, y]);
open = true;
}}"
on:click="{(e) => { on:click="{(e) => {
if (!open) return; if (!open) return;
if (e.target.contains(ref)) close(); if (e.target.contains(ref)) close();
@ -108,41 +48,6 @@
}}" }}"
/> />
<ul <ContextMenuInner bind:open bind:ref x="{x}" y="{y}">
bind:this="{ref}"
role="menu"
tabindex="-1"
data-direction="{direction}"
data-level="{level}"
class:bx--context-menu="{true}"
class:bx--context-menu--open="{open}"
class:bx--context-menu--invisible="{open && x === 0 && y === 0}"
class:bx--context-menu--root="{level === 1}"
{...$$restProps}
style="left: {x}px; top: {y}px; {$$restProps.style}"
on:click
on:click="{({ target }) => {
const closestOption = target.closest('[tabindex]');
if (closestOption && closestOption.getAttribute('role') !== 'menuitem') {
close();
}
}}"
on:keydown
on:keydown="{(e) => {
if (open) e.preventDefault();
if ($hasPopup) return;
if (e.key === 'ArrowDown') {
if (focusIndex < options.length - 1) focusIndex++;
} else if (e.key === 'ArrowUp') {
if (focusIndex === -1) {
focusIndex = options.length - 1;
} else {
if (focusIndex > 0) focusIndex--;
}
}
}}"
>
<slot /> <slot />
</ul> </ContextMenuInner>

View file

@ -0,0 +1,126 @@
<script>
/**
* Set to `true` to open the menu
* Either `x` and `y` must be greater than zero
*/
export let open = false;
/** Specify the horizontal offset of the menu position */
export let x = 0;
/** Specify the vertical offset of the menu position */
export let y = 0;
/** Obtain a reference to the unordered list HTML element */
export let ref = null;
import {
setContext,
getContext,
afterUpdate,
createEventDispatcher,
} from "svelte";
import { writable } from "svelte/store";
const dispatch = createEventDispatcher();
const position = writable([x, y]);
const dimensions = writable([0, 0]);
const currentIndex = writable(-1);
const hasPopup = writable(false);
const ctx = getContext("ContextMenu");
let options = [];
let direction = 1;
let prevX = 0;
let prevY = 0;
let focusIndex = -1;
let level;
$: level = !ctx ? 1 : 2;
$: $position = [x, y];
$: $currentIndex = focusIndex;
$: if (open && level === 1 && ref != null) {
const { height, width } = ref.getBoundingClientRect();
$dimensions = [width, height];
// if the menu is too far to the right, display it on the left side of the cursor
if (window.innerWidth - width < x) x -= width;
// if the menu is too far down, display it above the cursor
if (window.innerHeight - height < y) y -= height;
}
function close() {
open = false;
x = 0;
y = 0;
prevX = 0;
prevY = 0;
focusIndex = -1;
}
setContext("ContextMenu", {
currentIndex,
position,
dimensions,
close,
setPopup: (popup) => ($hasPopup = 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();
});
</script>
<ul
bind:this="{ref}"
role="menu"
tabindex="-1"
data-direction="{direction}"
data-level="{level}"
class:bx--context-menu="{true}"
class:bx--context-menu--open="{open}"
class:bx--context-menu--invisible="{open && x === 0 && y === 0}"
class:bx--context-menu--root="{level === 1}"
{...$$restProps}
style="left: {x}px; top: {y}px; {$$restProps.style}"
on:click
on:click="{({ target }) => {
const closestOption = target.closest('[tabindex]');
if (closestOption && closestOption.getAttribute('role') !== 'menuitem') {
close();
}
}}"
on:keydown
on:keydown="{(e) => {
if (open) e.preventDefault();
if ($hasPopup) return;
if (e.key === 'ArrowDown') {
if (focusIndex < options.length - 1) focusIndex++;
} else if (e.key === 'ArrowUp') {
if (focusIndex === -1) {
focusIndex = options.length - 1;
} else {
if (focusIndex > 0) focusIndex--;
}
}
}}"
>
<slot />
</ul>

View file

@ -59,6 +59,7 @@
const ctxRadioGroup = getContext("ContextMenuRadioGroup"); const ctxRadioGroup = getContext("ContextMenuRadioGroup");
const rootMenuPosition = ctx.position; const rootMenuPosition = ctx.position;
const rootMenuDimensions = ctx.dimensions;
// "moderate-01" duration (ms) from Carbon motion recommended for small expansion, short distance movements // "moderate-01" duration (ms) from Carbon motion recommended for small expansion, short distance movements
const moderate01 = 150; const moderate01 = 150;
@ -120,11 +121,15 @@
$: ctx.setPopup(submenuOpen); $: ctx.setPopup(submenuOpen);
$: if (submenuOpen) { $: if (submenuOpen) {
const rootMenuX = $rootMenuPosition[0]; const rootMenuX = $rootMenuPosition[0];
const rootMenuWidth = $rootMenuDimensions[0];
const { width, y } = ref.getBoundingClientRect(); const { width, y } = ref.getBoundingClientRect();
let x = rootMenuX + width;
if (window.innerWidth - rootMenuX < width) { let x;
if (window.innerWidth < rootMenuX + rootMenuWidth + width) {
// submenu is too far to the right, so we display it on the left side
x = rootMenuX - width; x = rootMenuX - width;
} else {
x = rootMenuX + width;
} }
submenuPosition = [x, y]; submenuPosition = [x, y];