mirror of
https://github.com/carbon-design-system/carbon-components-svelte.git
synced 2025-09-18 19:46:36 +00:00
extract ContextMenuInner.svelte
This commit is contained in:
parent
92d1eba45c
commit
3308fd4116
3 changed files with 146 additions and 110 deletions
|
@ -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>
|
||||||
|
|
126
src/ContextMenu/ContextMenuInner.svelte
Normal file
126
src/ContextMenu/ContextMenuInner.svelte
Normal 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>
|
|
@ -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];
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue