Skip to content

Commit

Permalink
Merge pull request #1213 from centerofci/context_menu
Browse files Browse the repository at this point in the history
Add ContextMenu component
  • Loading branch information
pavish committed Mar 25, 2022
2 parents edd0231 + acc61ff commit b94bfa8
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 36 deletions.
19 changes: 17 additions & 2 deletions mathesar_ui/src/component-library/common/actions/clickOffBounds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,30 @@ export default function clickOffBounds(
}
}

document.body.addEventListener('click', outOfBoundsListener, true);
/**
* When the browser supports pointer events, we use the pointerdown event
* which is fired for all mouse buttons and touches. However, older Safari
* versions don't have pointer events, so we fallback to mouse events. Touches
* should fire a mousedown event too.
*/
const events =
'onpointerdown' in document.body
? ['pointerdown']
: ['mousedown', 'contextmenu'];

events.forEach((event) => {
document.body.addEventListener(event, outOfBoundsListener, true);
});

function update(opts: Options) {
callback = opts.callback;
references = opts.references;
}

function destroy() {
document.body.removeEventListener('click', outOfBoundsListener, true);
events.forEach((event) => {
document.body.removeEventListener(event, outOfBoundsListener, true);
});
}

return {
Expand Down
70 changes: 36 additions & 34 deletions mathesar_ui/src/component-library/common/actions/popper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,55 +4,57 @@ import type {
ModifierArguments,
Options,
Instance,
VirtualElement,
} from '@popperjs/core/lib/types';
import type { Action } from './types';

export default function popper(
node: HTMLElement,
actionOpts: {
reference: HTMLElement;
reference: VirtualElement;
options?: Partial<Options>;
},
): Action {
let popperInstance: Instance;
let prevReference: HTMLElement | undefined;

function create(reference: HTMLElement, options?: Partial<Options>) {
if (reference) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
popperInstance = createPopper(reference, node, {
placement: options?.placement || 'bottom-start',
modifiers: [
{
name: 'setMinWidth',
enabled: true,
phase: 'beforeWrite',
requires: ['computeStyles'],
// @ts-ignore: https://github.com/centerofci/mathesar/issues/1055
fn: (obj: ModifierArguments<unknown>): void => {
// eslint-disable-next-line no-param-reassign
obj.state.styles.popper.minWidth = `${obj.state.rects.reference.width}px`;
},
// @ts-ignore: https://github.com/centerofci/mathesar/issues/1055
effect: (obj: ModifierArguments<unknown>): void => {
const width = (obj.state.elements.reference as HTMLElement)
.offsetWidth;
// eslint-disable-next-line no-param-reassign
obj.state.elements.popper.style.minWidth = `${width}px`;
},
function create(reference?: VirtualElement, options?: Partial<Options>) {
if (!reference) {
return;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
popperInstance = createPopper(reference, node, {
placement: options?.placement || 'bottom-start',
modifiers: [
{
name: 'setMinWidth',
enabled: true,
phase: 'beforeWrite',
requires: ['computeStyles'],
// @ts-ignore: https://github.com/centerofci/mathesar/issues/1055
fn: (obj: ModifierArguments<unknown>): void => {
// eslint-disable-next-line no-param-reassign
obj.state.styles.popper.minWidth = `${obj.state.rects.reference.width}px`;
},
{
name: 'flip',
// @ts-ignore: https://github.com/centerofci/mathesar/issues/1055
effect: (obj: ModifierArguments<unknown>): void => {
const width = (obj.state.elements.reference as HTMLElement)
.offsetWidth;
// eslint-disable-next-line no-param-reassign
obj.state.elements.popper.style.minWidth = `${width}px`;
},
{
name: 'offset',
options: {
offset: [0, 0],
},
},
{
name: 'flip',
},
{
name: 'offset',
options: {
offset: [0, 0],
},
],
}) as Instance;
}
},
],
}) as Instance;
}

function destroy() {
Expand Down
90 changes: 90 additions & 0 deletions mathesar_ui/src/component-library/context-menu/ContextMenu.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<script lang="ts">
import Menu from '@mathesar-component-library-dir/menu/Menu.svelte';
import popper from '@mathesar-component-library-dir/common/actions/popper';
import portal from '@mathesar-component-library-dir/common/actions/portal';
import clickOffBounds from '@mathesar-component-library-dir/common/actions/clickOffBounds';
import { onMount } from 'svelte';
/**
* A reference to the DOM node where we'll attach the contextmenu event
* listener. Right-clicking anywhere inside this element will open the context
* menu. If you don't pass an element, we'll use the parent element wherever
* this component is mounted.
*/
let customTriggerElement: HTMLElement | undefined = undefined;
export { customTriggerElement as triggerElement };
export let closeOnInnerClick = true;
interface MousePosition {
x: number;
y: number;
}
let isVisible = false;
let element: HTMLDivElement | undefined;
let mousePosition: MousePosition = { x: 0, y: 0 };
function getVirtualReferenceElement(_mousePosition: MousePosition) {
const { x, y } = _mousePosition;
return {
getBoundingClientRect: () => ({
width: 0,
height: 0,
x,
y,
top: y,
right: x,
bottom: y,
left: x,
toJSON: () => ({ x, y }),
}),
};
}
function handleContextMenu(event: MouseEvent) {
event.preventDefault();
mousePosition = {
x: event.clientX,
y: event.clientY,
};
isVisible = !isVisible;
}
onMount(() => {
const triggerElement =
customTriggerElement ?? element?.parentElement ?? undefined;
if (!triggerElement) {
return () => {};
}
triggerElement.addEventListener('contextmenu', handleContextMenu);
return () => {
triggerElement.removeEventListener('contextmenu', handleContextMenu);
};
});
function close() {
isVisible = false;
}
function checkAndCloseOnInnerClick() {
if (closeOnInnerClick) {
close();
}
}
</script>

<div bind:this={element} class="context-menu-wrapper">
{#if isVisible}
<div
class="context-menu dropdown content"
use:clickOffBounds={{ callback: close }}
use:popper={{ reference: getVirtualReferenceElement(mousePosition) }}
use:portal
on:click={checkAndCloseOnInnerClick}
>
<Menu>
<slot />
</Menu>
</div>
{/if}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<script>
import {
faFillDrip,
faPlus,
faTimes,
} from '@fortawesome/free-solid-svg-icons';
import { Meta, Story } from '@storybook/addon-svelte-csf';
import MenuItem from '@mathesar-component-library-dir/menu/MenuItem.svelte';
import Checkbox from '@mathesar-component-library-dir/checkbox/Checkbox.svelte';
import ContextMenu from '../ContextMenu.svelte';
const meta = {
title: 'Components/ContextMenu',
};
let count = 0;
let hasBackground = true;
function incrementCount() {
count += 1;
}
function toggleBackground() {
hasBackground = !hasBackground;
}
</script>

<Meta {...meta} />

<Story name="Basic">
<div class="box without-context">
<p>This box does <em>not</em> have a context menu.</p>
</div>

<div class="box with-context" class:has-background={hasBackground}>
<p>This box has a context menu.</p>
<p>The count is: <strong>{count}</strong>.</p>
<ContextMenu>
<MenuItem icon={{ data: faFillDrip }} on:click={toggleBackground}>
<Checkbox slot="control" checked={hasBackground} />
Use Background
</MenuItem>
<MenuItem icon={{ data: faPlus }} on:click={incrementCount}>
Increment Counter
</MenuItem>
</ContextMenu>
</div>

<div class="box with-context" has-background>
<p>This box <em>also</em> has a context menu.</p>
<ContextMenu>
<MenuItem icon={{ data: faTimes }}>I don't do anything</MenuItem>
</ContextMenu>
</div>

<div class="box without-context">
<p>This box does <em>not</em> have a context menu.</p>
</div>
</Story>

<style>
.box {
padding: 0 1em;
border: solid 0.1em #ccc;
border-radius: 0.5em;
margin: 1em 0;
}
.with-context.has-background {
background: rgb(220, 255, 199);
}
.without-context {
background: rgb(255, 220, 199);
}
</style>
1 change: 1 addition & 0 deletions mathesar_ui/src/component-library/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export { default as Button } from './button/Button.svelte';
export { default as CancelOrProceedButtonPair } from './cancel-or-proceed-button-pair/CancelOrProceedButtonPair.svelte';
export { default as Checkbox } from './checkbox/Checkbox.svelte';
export { default as CheckboxGroup } from './checkbox-group/CheckboxGroup.svelte';
export { default as ContextMenu } from './context-menu/ContextMenu.svelte';
export { default as Help } from './help/Help.svelte';
export { default as Icon } from './icon/Icon.svelte';
export { InputGroup, InputGroupText } from './input-group';
Expand Down

0 comments on commit b94bfa8

Please sign in to comment.