Skip to content

Commit 05d8c59

Browse files
feat(widgets): ContextMenuWidget improvements (#10031)
1 parent 2ee3895 commit 05d8c59

File tree

9 files changed

+195
-192
lines changed

9 files changed

+195
-192
lines changed

docs/api-reference/widgets/context-menu-widget.md

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ const deck = new Deck({
1616
new ContextMenuWidget({
1717
getMenuItems: (info, widget) => {
1818
if (info.object) {
19-
const name = info.object.properties.name;
2019
return [
21-
{key: 'name', label: name},
22-
{key: 'delete', label: 'Delete'}
20+
{value: 'delete', label: 'Delete pin'}
2321
];
2422
}
25-
return [{label: 'Add Point', key: 'add'}];
23+
return [
24+
{value: 'add', label: 'Add pin'}
25+
];
2626
},
2727
onMenuItemSelected: (key, pickInfo) => {
2828
if (key === 'add') addPoint(pickInfo);
@@ -39,25 +39,52 @@ const deck = new Deck({
3939

4040
The `ContextMenuWidget` accepts the generic [`WidgetProps`](../core/widget.md#widgetprops) and:
4141

42-
- `getMenuItems` (function) - **Required.** Function that returns menu items based on the picked object. Receives `PickingInfo` and returns an array of `ContextWidgetMenuItem` objects or `null`.
43-
- `onMenuItemSelected` (function, optional) - Callback invoked when a menu item is selected. Receives the selected item key and `PickingInfo`.
44-
- `visible` (boolean, default `false`) - Controls visibility of the context menu.
45-
- `position` (object, default `{x: 0, y: 0}`) - Screen position where the menu appears.
46-
- `menuItems` (array, default `[]`) - Current menu items to display.
42+
#### getMenuItems (Function)
43+
44+
Function that returns menu items based on the picked object. Receives the following parameters:
45+
- `pickInfo` ([PickingInfo](../../developer-guide/interactivity.md#picking)) - descriptor of what's under the pointer
46+
47+
Expected to return an array of [ContextWidgetMenuItem](#contextwidgetmenuitem) objects, or `null` if no menu should be displayed.
48+
49+
#### onMenuItemSelected (Function, optional)
50+
51+
Callback invoked when a menu item is selected. Receives the following parameters:
52+
53+
- `value` (string) - the value of the selected menu item
54+
- `pickInfo` ([PickingInfo](../../developer-guide/interactivity.md#picking)) - descriptor of what's under the pointer
55+
56+
57+
#### placement (string, optional) {#placement}
58+
59+
Position content relative to the anchor.
60+
One of `bottom` | `left` | `right` | `top` | `bottom-start` | `bottom-end` | `left-start` | `left-end` | `right-start` | `right-end` | `top-start` | `top-end`
61+
62+
* Default: `'right'`
63+
64+
#### offset (number) {#offset}
65+
66+
Pixel offset from the anchor
67+
68+
* Default: `10`
69+
70+
#### arrow (false | number | [number, number]) {#arrow}
71+
72+
Show an arrow pointing at the anchor. Value can be one of the following:
73+
74+
* `false` - do not display an arrow
75+
* `number` - pixel size of the arrow
76+
* `[width: number, height: number]` - pixel size of the arrow
77+
78+
* Default: `10`
79+
4780

4881
### `ContextWidgetMenuItem` {#contextwidgetmenuitem}
4982

5083
Menu item definition:
5184

5285
- `label` (string) - Display text for the menu item
53-
- `key` (string) - Unique identifier for the menu item
54-
55-
## Behavior
56-
57-
- Right-click events trigger the context menu
58-
- Menu items are dynamically generated based on what was clicked
59-
- Click elsewhere to hide the menu
60-
- Menu automatically positions itself at the cursor location
86+
- `value` (string, optional) - Unique identifier for the menu item. If not supplied, then the item is not interactive.
87+
- `icon` (string, optional) - Data url of an icon that should be displayed with the menu item
6188

6289
## Source
6390

modules/widgets/src/context-menu-widget.tsx

Lines changed: 86 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -5,116 +5,126 @@
55
/* global document */
66
import {Widget} from '@deck.gl/core';
77
import type {Deck, PickingInfo, WidgetProps} from '@deck.gl/core';
8-
import {render} from 'preact';
9-
import {SimpleMenu} from './lib/components/simple-menu';
10-
11-
/** The standard, modern way is to use event.button === 2, where button is the standardized property (0 = left, 1 = middle, 2 = right). */
12-
const MOUSE_BUTTON_RIGHT = 2;
13-
/** A name for the legacy MouseEvent.which value that corresponds to the right-mouse button. In older browsers, the check is: if (event.which === 3) */
14-
const MOUSE_WHICH_RIGHT = 3;
15-
16-
export type ContextWidgetMenuItem = {
17-
label: string;
18-
key: string;
19-
};
8+
import {render, type JSX} from 'preact';
9+
import {SimpleMenu, SimpleMenuProps} from './lib/components/dropdown-menu';
10+
import {Popover, type PopoverProps} from './lib/components/popover';
2011

2112
export type ContextMenuWidgetProps = WidgetProps & {
2213
/** View to attach to and interact with. Required when using multiple views. */
2314
viewId?: string | null;
24-
/** Controls visibility of the context menu */
25-
visible?: boolean;
26-
/** Screen position at which to place the menu */
27-
position: {x: number; y: number};
28-
/** Items to render */
29-
menuItems: ContextWidgetMenuItem[];
3015
/** Provide menu items for the menu given the picked object */
31-
getMenuItems: (info: PickingInfo, widget: ContextMenuWidget) => ContextWidgetMenuItem[] | null;
16+
getMenuItems: (
17+
info: PickingInfo,
18+
widget: ContextMenuWidget
19+
) => SimpleMenuProps['menuItems'] | null;
3220
/** Callback with the selected item */
33-
onMenuItemSelected?: (key: string, pickInfo: PickingInfo | null) => void;
21+
onMenuItemSelected?: (value: string, pickInfo: PickingInfo | null) => void;
22+
/** Position menu relative to the anchor.
23+
* @default 'bottom-start'
24+
*/
25+
placement?: PopoverProps['placement'];
26+
/** Pixel offset
27+
* @default 10
28+
*/
29+
offset?: PopoverProps['offset'];
30+
/**
31+
* Show an arrow pointing at the anchor. Optionally accepts a pixel size.
32+
* @default false
33+
*/
34+
arrow?: PopoverProps['arrow'];
3435
};
3536

3637
export class ContextMenuWidget extends Widget<ContextMenuWidgetProps> {
3738
static defaultProps: Required<ContextMenuWidgetProps> = {
3839
...Widget.defaultProps,
3940
id: 'context',
4041
viewId: null,
41-
visible: false,
42-
position: {x: 0, y: 0},
4342
getMenuItems: undefined!,
44-
menuItems: [],
45-
// eslint-disable-next-line no-console
46-
onMenuItemSelected: (key, pickInfo) => console.log('Context menu item selected:', key, pickInfo)
43+
onMenuItemSelected: () => {},
44+
placement: 'bottom-start',
45+
offset: 10,
46+
arrow: false
4747
};
4848

4949
className = 'deck-widget-context-menu';
5050
placement = 'fill' as const;
5151

52-
pickInfo: PickingInfo | null = null;
52+
menu: {
53+
items: SimpleMenuProps['menuItems'];
54+
pickInfo: PickingInfo;
55+
} | null = null;
5356

5457
constructor(props: ContextMenuWidgetProps) {
5558
super(props);
56-
this.pickInfo = null;
5759
this.setProps(this.props);
5860
}
5961

60-
onAdd({deck}: {deck: Deck<any>}): HTMLDivElement {
61-
const element = document.createElement('div');
62-
element.classList.add('deck-widget', 'deck-widget-context-menu');
63-
const style = {
64-
margin: '0px',
65-
top: '0px',
66-
left: '0px',
67-
position: 'absolute',
68-
pointerEvents: 'auto'
69-
};
70-
Object.entries(style).forEach(([key, value]) => element.style.setProperty(key, value));
71-
72-
deck.getCanvas()?.addEventListener('click', () => this.hide());
62+
onAdd({deck}: {deck: Deck<any>}) {
7363
deck.getCanvas()?.addEventListener('contextmenu', event => this.handleContextMenu(event));
74-
return element;
7564
}
7665

77-
onRenderHTML(rootElement: HTMLElement): void {
78-
const {visible, position, menuItems} = this.props;
66+
handleContextMenu(srcEvent: MouseEvent) {
67+
const targetRect = (srcEvent.target as HTMLElement).getBoundingClientRect();
68+
const x = srcEvent.clientX - targetRect.x;
69+
const y = srcEvent.clientY - targetRect.y;
7970

80-
const ui =
81-
visible && menuItems.length ? (
82-
<SimpleMenu
83-
menuItems={menuItems}
84-
onItemSelected={key => this.props.onMenuItemSelected(key, this.pickInfo)}
85-
position={position}
86-
style={{pointerEvents: 'auto'}}
87-
/>
88-
) : null;
89-
render(ui, rootElement);
71+
const pickInfo = this.deck?.pickObject({x, y}) || {
72+
x,
73+
y,
74+
picked: false,
75+
layer: null,
76+
color: null,
77+
index: -1,
78+
pixelRatio: 1
79+
};
80+
const menuItems = this.props.getMenuItems(pickInfo, this) || [];
81+
this.menu =
82+
menuItems.length > 0
83+
? {
84+
items: menuItems,
85+
pickInfo
86+
}
87+
: null;
88+
srcEvent.preventDefault();
89+
this.updateHTML();
9090
}
9191

92-
handleContextMenu(srcEvent: MouseEvent): boolean {
93-
if (
94-
srcEvent &&
95-
(srcEvent.button === MOUSE_BUTTON_RIGHT || srcEvent.which === MOUSE_WHICH_RIGHT)
96-
) {
97-
this.pickInfo =
98-
this.deck?.pickObject({
99-
x: srcEvent.clientX,
100-
y: srcEvent.clientY
101-
}) || null;
102-
const menuItems = (this.pickInfo && this.props.getMenuItems?.(this.pickInfo, this)) || [];
103-
const visible = menuItems.length > 0;
104-
this.setProps({
105-
visible,
106-
position: {x: srcEvent.clientX, y: srcEvent.clientY},
107-
menuItems
108-
});
109-
this.updateHTML();
110-
srcEvent.preventDefault();
111-
return visible;
92+
onRenderHTML(rootElement: HTMLElement): void {
93+
if (!this.menu) {
94+
render(null, rootElement);
95+
return;
11296
}
97+
const {items, pickInfo} = this.menu;
11398

114-
return false;
99+
const style = {
100+
pointerEvents: 'auto',
101+
position: 'static',
102+
...this.props.style
103+
};
104+
105+
const ui = (
106+
<Popover
107+
x={pickInfo.x}
108+
y={pickInfo.y}
109+
placement={this.props.placement}
110+
arrow={this.props.arrow}
111+
arrowColor="var(--menu-background, #fff)"
112+
offset={this.props.offset}
113+
>
114+
<SimpleMenu
115+
menuItems={items}
116+
onSelect={value => this.props.onMenuItemSelected(value, pickInfo)}
117+
style={style}
118+
isOpen
119+
onClose={() => this.hide()}
120+
/>
121+
</Popover>
122+
);
123+
render(ui, rootElement);
115124
}
116125

117-
hide(): void {
118-
this.setProps({visible: false});
126+
hide() {
127+
this.menu = null;
128+
this.updateHTML();
119129
}
120130
}

modules/widgets/src/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,10 @@ export {
6262
} from './lib/components/grouped-icon-button';
6363
export {
6464
DropdownMenu as _DropdownMenu,
65-
type DropdownMenuProps
65+
type DropdownMenuProps,
66+
SimpleMenu as _SimpleMenu,
67+
type SimpleMenuProps
6668
} from './lib/components/dropdown-menu';
67-
export {SimpleMenu as _SimpleMenu, type SimpleMenuProps} from './lib/components/simple-menu';
6869
export {IconMenu as _IconMenu, type IconMenuProps} from './lib/components/icon-menu';
6970

7071
// Experimental geocoders. May be removed, use at your own risk!

modules/widgets/src/info-widget.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,6 @@ export class InfoWidget extends Widget<InfoWidgetProps> {
131131
};
132132
// Project the clicked geographic coordinate to canvas (x, y)
133133
const [x, y] = this.viewport.project(this.tooltip.position);
134-
const background = style.backgroundColor || style.background || 'white';
135134

136135
// Render the popup container with a content box and a placeholder for the arrow.
137136
// The container is positioned absolutely (initially at 0,0) and will be repositioned after measuring.
@@ -141,17 +140,12 @@ export class InfoWidget extends Widget<InfoWidgetProps> {
141140
y={y}
142141
placement={this.props.placement}
143142
arrow={this.props.arrow}
144-
arrowColor={background}
143+
arrowColor="var(--menu-background, #fff)"
145144
offset={this.props.offset}
146145
>
147146
<UserContent
148147
className={`deck-widget-popup-content ${this.tooltip.className} ${this.props.className}`}
149-
style={{
150-
background,
151-
padding: '10px',
152-
boxShadow: '2px 2px 8px rgba(0, 0, 0, 0.15)',
153-
...(style as JSX.CSSProperties)
154-
}}
148+
style={style as JSX.CSSProperties}
155149
html={this.tooltip.html}
156150
text={this.tooltip.text}
157151
element={this.tooltip.element}

0 commit comments

Comments
 (0)