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

@ -780,12 +780,13 @@ None.
### Props
| Prop name | Kind | Reactive | Type | Default value | Description |
| :-------- | :--------------- | :------- | :---------------------------------------- | ------------------ | -------------------------------------------------------------------------------- |
| ref | <code>let</code> | Yes | <code>null &#124; 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 |
| 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 |
| Prop name | Kind | Reactive | Type | Default value | Description |
| :-------- | :--------------- | :------- | :-------------------------------------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| ref | <code>let</code> | Yes | <code>null &#124; 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 |
| 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 |
| target | <code>let</code> | No | <code>null &#124; HTMLElement &#124; 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
@ -795,12 +796,12 @@ None.
### Events
| Event name | Type | Detail |
| :--------- | :--------- | :----- |
| click | forwarded | -- |
| keydown | forwarded | -- |
| open | dispatched | -- |
| close | dispatched | -- |
| Event name | Type | Detail |
| :--------- | :--------- | :----------------------- |
| open | dispatched | <code>HTMLElement</code> |
| click | forwarded | -- |
| keydown | forwarded | -- |
| close | dispatched | -- |
## `ContextMenuDivider`

View file

@ -1809,6 +1809,17 @@
"moduleName": "ContextMenu",
"filePath": "src/ContextMenu/ContextMenu.svelte",
"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",
"kind": "let",
@ -1856,9 +1867,9 @@
],
"slots": [{ "name": "__default__", "default": true, "slot_props": "{}" }],
"events": [
{ "type": "dispatched", "name": "open", "detail": "HTMLElement" },
{ "type": "forwarded", "name": "click", "element": "ul" },
{ "type": "forwarded", "name": "keydown", "element": "ul" },
{ "type": "dispatched", "name": "open" },
{ "type": "dispatched", "name": "close" }
],
"typedefs": [],

View file

@ -10,8 +10,22 @@ In the examples, right click anywhere within the iframe.
### Default
By default, the context menu will trigger when right clicking anywhere in the `window`.
<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
<FileSource src="/framed/ContextMenu/ContextMenuGroups" />

View 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>

View 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>

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;

View file

@ -15,7 +15,7 @@
$: console.log("selectedId", selectedId);
</script>
<ContextMenu>
<ContextMenu open on:open="{(e) => console.log(e.detail)}">
<ContextMenuOption
kind="danger"
indented

View file

@ -3,6 +3,13 @@ import { SvelteComponentTyped } from "svelte";
export interface ContextMenuProps
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
* Either `x` and `y` must be greater than zero
@ -32,9 +39,9 @@ export interface ContextMenuProps
export default class ContextMenu extends SvelteComponentTyped<
ContextMenuProps,
{
open: CustomEvent<HTMLElement>;
click: WindowEventMap["click"];
keydown: WindowEventMap["keydown"];
open: CustomEvent<any>;
close: CustomEvent<any>;
},
{ default: {} }