mirror of
https://github.com/carbon-design-system/carbon-components-svelte.git
synced 2025-09-15 02:11:05 +00:00
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:
parent
d8f8ac2b73
commit
ccc733f3f7
8 changed files with 261 additions and 52 deletions
|
@ -780,12 +780,13 @@ None.
|
||||||
|
|
||||||
### Props
|
### Props
|
||||||
|
|
||||||
| Prop name | Kind | Reactive | Type | Default value | Description |
|
| Prop name | Kind | Reactive | Type | Default value | Description |
|
||||||
| :-------- | :--------------- | :------- | :---------------------------------------- | ------------------ | -------------------------------------------------------------------------------- |
|
| :-------- | :--------------- | :------- | :-------------------------------------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| ref | <code>let</code> | Yes | <code>null | HTMLUListElement</code> | <code>null</code> | Obtain a reference to the unordered list HTML element |
|
| ref | <code>let</code> | Yes | <code>null | HTMLUListElement</code> | <code>null</code> | Obtain a reference to the unordered list HTML element |
|
||||||
| y | <code>let</code> | Yes | <code>number</code> | <code>0</code> | Specify the vertical offset of the menu position |
|
| y | <code>let</code> | Yes | <code>number</code> | <code>0</code> | Specify the vertical offset of the menu position |
|
||||||
| x | <code>let</code> | Yes | <code>number</code> | <code>0</code> | Specify the horizontal offset of the menu position |
|
| x | <code>let</code> | Yes | <code>number</code> | <code>0</code> | Specify the horizontal offset of the menu position |
|
||||||
| open | <code>let</code> | Yes | <code>boolean</code> | <code>false</code> | Set to `true` to open the menu<br />Either `x` and `y` must be greater than zero |
|
| open | <code>let</code> | Yes | <code>boolean</code> | <code>false</code> | Set to `true` to open the menu<br />Either `x` and `y` must be greater than zero |
|
||||||
|
| target | <code>let</code> | No | <code>null | HTMLElement | HTMLElement[]</code> | <code>null</code> | Specify an element or list of elements to trigger the context menu.<br />If no element is specified, the context menu applies to the entire window |
|
||||||
|
|
||||||
### Slots
|
### Slots
|
||||||
|
|
||||||
|
@ -795,12 +796,12 @@ None.
|
||||||
|
|
||||||
### Events
|
### Events
|
||||||
|
|
||||||
| Event name | Type | Detail |
|
| Event name | Type | Detail |
|
||||||
| :--------- | :--------- | :----- |
|
| :--------- | :--------- | :----------------------- |
|
||||||
| click | forwarded | -- |
|
| open | dispatched | <code>HTMLElement</code> |
|
||||||
| keydown | forwarded | -- |
|
| click | forwarded | -- |
|
||||||
| open | dispatched | -- |
|
| keydown | forwarded | -- |
|
||||||
| close | dispatched | -- |
|
| close | dispatched | -- |
|
||||||
|
|
||||||
## `ContextMenuDivider`
|
## `ContextMenuDivider`
|
||||||
|
|
||||||
|
|
|
@ -1809,6 +1809,17 @@
|
||||||
"moduleName": "ContextMenu",
|
"moduleName": "ContextMenu",
|
||||||
"filePath": "src/ContextMenu/ContextMenu.svelte",
|
"filePath": "src/ContextMenu/ContextMenu.svelte",
|
||||||
"props": [
|
"props": [
|
||||||
|
{
|
||||||
|
"name": "target",
|
||||||
|
"kind": "let",
|
||||||
|
"description": "Specify an element or list of elements to trigger the context menu.\nIf no element is specified, the context menu applies to the entire window",
|
||||||
|
"type": "null | HTMLElement | HTMLElement[]",
|
||||||
|
"value": "null",
|
||||||
|
"isFunction": false,
|
||||||
|
"isFunctionDeclaration": false,
|
||||||
|
"constant": false,
|
||||||
|
"reactive": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "open",
|
"name": "open",
|
||||||
"kind": "let",
|
"kind": "let",
|
||||||
|
@ -1856,9 +1867,9 @@
|
||||||
],
|
],
|
||||||
"slots": [{ "name": "__default__", "default": true, "slot_props": "{}" }],
|
"slots": [{ "name": "__default__", "default": true, "slot_props": "{}" }],
|
||||||
"events": [
|
"events": [
|
||||||
|
{ "type": "dispatched", "name": "open", "detail": "HTMLElement" },
|
||||||
{ "type": "forwarded", "name": "click", "element": "ul" },
|
{ "type": "forwarded", "name": "click", "element": "ul" },
|
||||||
{ "type": "forwarded", "name": "keydown", "element": "ul" },
|
{ "type": "forwarded", "name": "keydown", "element": "ul" },
|
||||||
{ "type": "dispatched", "name": "open" },
|
|
||||||
{ "type": "dispatched", "name": "close" }
|
{ "type": "dispatched", "name": "close" }
|
||||||
],
|
],
|
||||||
"typedefs": [],
|
"typedefs": [],
|
||||||
|
|
|
@ -10,8 +10,22 @@ In the examples, right click anywhere within the iframe.
|
||||||
|
|
||||||
### Default
|
### Default
|
||||||
|
|
||||||
|
By default, the context menu will trigger when right clicking anywhere in the `window`.
|
||||||
|
|
||||||
<FileSource src="/framed/ContextMenu/ContextMenu" />
|
<FileSource src="/framed/ContextMenu/ContextMenu" />
|
||||||
|
|
||||||
|
### Custom target
|
||||||
|
|
||||||
|
Specify a custom `HTMLElement` using the `target` prop.
|
||||||
|
|
||||||
|
<FileSource src="/framed/ContextMenu/ContextMenuTarget" />
|
||||||
|
|
||||||
|
### Multiple targets
|
||||||
|
|
||||||
|
The `target` prop also accepts an array of elements.
|
||||||
|
|
||||||
|
<FileSource src="/framed/ContextMenu/ContextMenuTargets" />
|
||||||
|
|
||||||
### Radio groups
|
### Radio groups
|
||||||
|
|
||||||
<FileSource src="/framed/ContextMenu/ContextMenuGroups" />
|
<FileSource src="/framed/ContextMenu/ContextMenuGroups" />
|
65
docs/src/pages/framed/ContextMenu/ContextMenuTarget.svelte
Normal file
65
docs/src/pages/framed/ContextMenu/ContextMenuTarget.svelte
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuDivider,
|
||||||
|
ContextMenuGroup,
|
||||||
|
ContextMenuOption,
|
||||||
|
} from "carbon-components-svelte";
|
||||||
|
import CopyFile16 from "carbon-icons-svelte/lib/CopyFile16";
|
||||||
|
import Cut16 from "carbon-icons-svelte/lib/Cut16";
|
||||||
|
|
||||||
|
let target;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ContextMenu target="{target}" on:open="{(e) => console.log(e.detail)}">
|
||||||
|
<ContextMenuOption
|
||||||
|
indented
|
||||||
|
labelText="Copy"
|
||||||
|
shortcutText="⌘C"
|
||||||
|
icon="{CopyFile16}"
|
||||||
|
/>
|
||||||
|
<ContextMenuOption
|
||||||
|
indented
|
||||||
|
labelText="Cut"
|
||||||
|
shortcutText="⌘X"
|
||||||
|
icon="{Cut16}"
|
||||||
|
/>
|
||||||
|
<ContextMenuDivider />
|
||||||
|
<ContextMenuOption indented labelText="Export as">
|
||||||
|
<ContextMenuGroup labelText="Export options">
|
||||||
|
<ContextMenuOption id="pdf" labelText="PDF" />
|
||||||
|
<ContextMenuOption id="txt" labelText="TXT" />
|
||||||
|
<ContextMenuOption id="mp3" labelText="MP3" />
|
||||||
|
</ContextMenuGroup>
|
||||||
|
</ContextMenuOption>
|
||||||
|
<ContextMenuDivider />
|
||||||
|
<ContextMenuOption selectable labelText="Remove metadata" />
|
||||||
|
<ContextMenuDivider />
|
||||||
|
<ContextMenuGroup labelText="Style options">
|
||||||
|
<ContextMenuOption id="0" labelText="Font smoothing" selected />
|
||||||
|
<ContextMenuOption id="1" labelText="Reduce noise" />
|
||||||
|
<ContextMenuOption id="2" labelText="Auto-sharpen" />
|
||||||
|
</ContextMenuGroup>
|
||||||
|
<ContextMenuDivider />
|
||||||
|
<ContextMenuOption indented kind="danger" labelText="Delete" />
|
||||||
|
</ContextMenu>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p bind:this="{target}">Right click this element</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
position: absolute;
|
||||||
|
width: calc(100% - var(--cds-spacing-05));
|
||||||
|
height: calc(100% - var(--cds-spacing-06));
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--cds-text-02);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
outline: 1px solid var(--cds-interactive-01);
|
||||||
|
}
|
||||||
|
</style>
|
70
docs/src/pages/framed/ContextMenu/ContextMenuTargets.svelte
Normal file
70
docs/src/pages/framed/ContextMenu/ContextMenuTargets.svelte
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuDivider,
|
||||||
|
ContextMenuGroup,
|
||||||
|
ContextMenuOption,
|
||||||
|
} from "carbon-components-svelte";
|
||||||
|
import CopyFile16 from "carbon-icons-svelte/lib/CopyFile16";
|
||||||
|
import Cut16 from "carbon-icons-svelte/lib/Cut16";
|
||||||
|
|
||||||
|
let target;
|
||||||
|
let target2;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ContextMenu
|
||||||
|
target="{[target, target2]}"
|
||||||
|
on:open="{(e) => console.log(e.detail)}"
|
||||||
|
>
|
||||||
|
<ContextMenuOption
|
||||||
|
indented
|
||||||
|
labelText="Copy"
|
||||||
|
shortcutText="⌘C"
|
||||||
|
icon="{CopyFile16}"
|
||||||
|
/>
|
||||||
|
<ContextMenuOption
|
||||||
|
indented
|
||||||
|
labelText="Cut"
|
||||||
|
shortcutText="⌘X"
|
||||||
|
icon="{Cut16}"
|
||||||
|
/>
|
||||||
|
<ContextMenuDivider />
|
||||||
|
<ContextMenuOption indented labelText="Export as">
|
||||||
|
<ContextMenuGroup labelText="Export options">
|
||||||
|
<ContextMenuOption id="pdf" labelText="PDF" />
|
||||||
|
<ContextMenuOption id="txt" labelText="TXT" />
|
||||||
|
<ContextMenuOption id="mp3" labelText="MP3" />
|
||||||
|
</ContextMenuGroup>
|
||||||
|
</ContextMenuOption>
|
||||||
|
<ContextMenuDivider />
|
||||||
|
<ContextMenuOption selectable labelText="Remove metadata" />
|
||||||
|
<ContextMenuDivider />
|
||||||
|
<ContextMenuGroup labelText="Style options">
|
||||||
|
<ContextMenuOption id="0" labelText="Font smoothing" selected />
|
||||||
|
<ContextMenuOption id="1" labelText="Reduce noise" />
|
||||||
|
<ContextMenuOption id="2" labelText="Auto-sharpen" />
|
||||||
|
</ContextMenuGroup>
|
||||||
|
<ContextMenuDivider />
|
||||||
|
<ContextMenuOption indented kind="danger" labelText="Delete" />
|
||||||
|
</ContextMenu>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p bind:this="{target}">Right click this element</p>
|
||||||
|
<p bind:this="{target2}">... or this one</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
position: absolute;
|
||||||
|
width: calc(100% - var(--cds-spacing-05));
|
||||||
|
height: calc(100% - var(--cds-spacing-06));
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--cds-text-02);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
outline: 1px solid var(--cds-interactive-01);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,4 +1,15 @@
|
||||||
<script>
|
<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
|
* 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
|
||||||
|
@ -15,6 +26,7 @@
|
||||||
export let ref = null;
|
export let ref = null;
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
onMount,
|
||||||
setContext,
|
setContext,
|
||||||
getContext,
|
getContext,
|
||||||
afterUpdate,
|
afterUpdate,
|
||||||
|
@ -34,6 +46,7 @@
|
||||||
let prevX = 0;
|
let prevX = 0;
|
||||||
let prevY = 0;
|
let prevY = 0;
|
||||||
let focusIndex = -1;
|
let focusIndex = -1;
|
||||||
|
let openDetail = null;
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
open = false;
|
open = false;
|
||||||
|
@ -44,43 +57,8 @@
|
||||||
focusIndex = -1;
|
focusIndex = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
setContext("ContextMenu", {
|
/** @type {(e: MouseEvent) => void} */
|
||||||
menuOffsetX,
|
function openMenu(e) {
|
||||||
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;
|
|
||||||
|
|
||||||
const { height, width } = ref.getBoundingClientRect();
|
const { height, width } = ref.getBoundingClientRect();
|
||||||
|
|
||||||
if (open || x === 0) {
|
if (open || x === 0) {
|
||||||
|
@ -102,6 +80,69 @@
|
||||||
}
|
}
|
||||||
position.set([x, y]);
|
position.set([x, y]);
|
||||||
open = true;
|
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) => {
|
on:click="{(e) => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
$: console.log("selectedId", selectedId);
|
$: console.log("selectedId", selectedId);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ContextMenu>
|
<ContextMenu open on:open="{(e) => console.log(e.detail)}">
|
||||||
<ContextMenuOption
|
<ContextMenuOption
|
||||||
kind="danger"
|
kind="danger"
|
||||||
indented
|
indented
|
||||||
|
|
9
types/ContextMenu/ContextMenu.svelte.d.ts
vendored
9
types/ContextMenu/ContextMenu.svelte.d.ts
vendored
|
@ -3,6 +3,13 @@ import { SvelteComponentTyped } from "svelte";
|
||||||
|
|
||||||
export interface ContextMenuProps
|
export interface ContextMenuProps
|
||||||
extends svelte.JSX.HTMLAttributes<HTMLElementTagNameMap["ul"]> {
|
extends svelte.JSX.HTMLAttributes<HTMLElementTagNameMap["ul"]> {
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* @default null
|
||||||
|
*/
|
||||||
|
target?: null | HTMLElement | HTMLElement[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
|
@ -32,9 +39,9 @@ export interface ContextMenuProps
|
||||||
export default class ContextMenu extends SvelteComponentTyped<
|
export default class ContextMenu extends SvelteComponentTyped<
|
||||||
ContextMenuProps,
|
ContextMenuProps,
|
||||||
{
|
{
|
||||||
|
open: CustomEvent<HTMLElement>;
|
||||||
click: WindowEventMap["click"];
|
click: WindowEventMap["click"];
|
||||||
keydown: WindowEventMap["keydown"];
|
keydown: WindowEventMap["keydown"];
|
||||||
open: CustomEvent<any>;
|
|
||||||
close: CustomEvent<any>;
|
close: CustomEvent<any>;
|
||||||
},
|
},
|
||||||
{ default: {} }
|
{ default: {} }
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue