Hey all,
Someone asked on the DMC Discord whether it was possible to do actions on right click which made me think about how we could do a custom context menu with Dash.
So I built this POC with dash-mantine-components:
The context menu:
- Opens when right-clicking on given elements (this leverages the excellent
dash_clientside.set_props
improvement) - Stays in the viewport even when right-clicking towards the extremes of the window
- Allows to keep track of the clicked element
Hope this can help the community!
And here is the code for it:
app.py
import uuid
import dash_mantine_components as dmc
from dash import (
ALL,
MATCH,
ClientsideFunction,
Dash,
Input,
Output,
State,
_dash_renderer,
callback,
clientside_callback,
ctx,
html,
)
from dash_iconify import DashIconify
_dash_renderer._set_react_version("18.2.0")
app = Dash(__name__, external_stylesheets=dmc.styles.ALL)
class ids:
contextmenu_div = lambda idx: {"type": "contextmenu-div", "idx": idx}
menu = "context-menu"
menu_button = "context-menu-trigger"
menu_label = "context-menu-label"
menu_action = lambda action: {"type": "context-menu-action", "action": action}
output = "output"
ACTION_ICONS = {
"open": "carbon:arrow-up-right",
"copy": "carbon:copy",
"share": "carbon:share",
"delete": "carbon:trash-can",
}
app.layout = dmc.MantineProvider(
[
dmc.NotificationProvider(containerWidth=200, position="top-right"),
html.Div(id=ids.output),
dmc.Menu(
[
dmc.MenuTarget(
html.Div(id=ids.menu_button),
boxWrapperProps={
"style": {
"position": "fixed",
"zIndex": -1,
"inset": 0,
},
}
),
dmc.MenuDropdown(
[
dmc.MenuLabel("", id=ids.menu_label),
dmc.MenuItem(
"Open",
leftSection=DashIconify(icon=ACTION_ICONS["open"], height=16),
id=ids.menu_action("open"),
),
dmc.MenuItem(
"Copy",
leftSection=DashIconify(icon=ACTION_ICONS["copy"], height=16),
id=ids.menu_action("copy"),
),
dmc.MenuItem(
"Share",
leftSection=DashIconify(icon=ACTION_ICONS["share"], height=16),
id=ids.menu_action("share"),
),
dmc.MenuDivider(),
dmc.MenuItem(
"Delete",
leftSection=DashIconify(icon=ACTION_ICONS["delete"], height=16),
id=ids.menu_action("delete"),
),
],
),
],
id=ids.menu,
position="left-start",
shadow="sm",
trapFocus=False,
transitionProps={"transition": "pop-top-left"},
width=150,
),
dmc.SimpleGrid(
[
dmc.Paper(
children=dmc.Text(i + 1, fw=600, size="3rem", c="dimmed", opacity=0.5),
id=ids.contextmenu_div(i + 1),
h=250,
withBorder=True,
bg="var(--mantine-color-default-hover)",
style={"display": "grid", "placeItems": "center"},
)
for i in range(6)
],
cols=3,
spacing="1.5rem",
p="1.5rem",
),
],
)
clientside_callback(
ClientsideFunction(namespace="contextmenu", function_name="cardContextMenu"),
Output(ids.contextmenu_div(MATCH), "id"),
Input(ids.contextmenu_div(MATCH), "id"),
State(ids.menu_button, "id"),
State(ids.menu, "id"),
State(ids.menu, "width"),
State(ids.menu_label, "id"),
)
@callback(
Output(ids.output, "children"),
Input(ids.menu_action(ALL), "n_clicks"),
State(ids.menu, "data-item"),
)
def update_output(_trigger, item_id):
if not any(_trigger):
return None
return dmc.Notification(
message=f"{ctx.triggered_id['action'].title()} item {item_id['idx']}",
action="show",
id=uuid.uuid4().hex,
withBorder=True,
icon=DashIconify(icon=ACTION_ICONS[ctx.triggered_id["action"]], height=16),
)
if __name__ == "__main__":
app.run_server(debug=True, host="localhost")
assets/scripts.js
if (!window.dash_clientside) {
window.dash_clientside = {};
}
dash_clientside.contextmenu = {
cardContextMenu: (divId, menuBtnId, menuId, menuWidth, menuLabelId) => {
const stringify = (id) => typeof id === "string" ? id : JSON.stringify(id, Object.keys(id).sort())
const el = document.getElementById(stringify(divId))
const btn = document.getElementById(stringify(menuBtnId))
el.oncontextmenu = (e) => {
e.preventDefault()
const overflowsRight = e.pageX + menuWidth + 8 >= window.innerWidth
const props = {
offset: {
mainAxis: e.pageX + (overflowsRight ? -menuWidth - 8 : 8),
crossAxis: e.pageY - 8,
},
transitionProps: {
transition: overflowsRight ? "pop-top-right" : "pop-top-left",
},
"data-item": divId,
}
dash_clientside.set_props(menuId, props)
dash_clientside.set_props(menuLabelId, {children: `Item ${divId.idx}`})
btn.click()
}
return dash_clientside.no_update
}
}