Skip to content

Commit 40e0108

Browse files
committed
Merge branch 'wip/adr/add-group-context-menu' of https://github.com/enso-org/enso into wip/adr/add-group-context-menu
2 parents b498bfe + 806c0cd commit 40e0108

File tree

6 files changed

+207
-72
lines changed

6 files changed

+207
-72
lines changed
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<script setup lang="ts">
2+
import ActionButton from '@/components/ActionButton.vue'
3+
import ConditionalTeleport from '@/components/ConditionalTeleport.vue'
4+
import MenuButton from '@/components/MenuButton.vue'
5+
import MenuPanel from '@/components/MenuPanel.vue'
6+
import SizeTransition from '@/components/SizeTransition.vue'
7+
import SvgIcon from '@/components/SvgIcon.vue'
8+
import { useHoverMenu } from '@/composables/hoverMenu'
9+
import { injectInteractionHandler } from '@/providers/interactionHandler'
10+
import { usePopoverRoot } from '@/providers/popoverRoot'
11+
import { endOnClickOutside } from '@/util/autoBlur'
12+
import { shift, useFloating, type Placement } from '@floating-ui/vue'
13+
import { shallowRef } from 'vue'
14+
15+
const open = defineModel<boolean>('open', { default: false })
16+
const { placement = 'bottom-start' } = defineProps<{
17+
placement?: Placement
18+
}>()
19+
20+
const rootElement = shallowRef<HTMLElement>()
21+
const floatElement = shallowRef<HTMLElement>()
22+
const popoverRoot = usePopoverRoot(true)
23+
24+
const alignmentMenu = useHoverMenu()
25+
26+
const end = () => {
27+
open.value = false
28+
alignmentMenu.menuOpen = false
29+
}
30+
const interaction = endOnClickOutside(floatElement, {
31+
cancel: end,
32+
end,
33+
parentInteraction: undefined,
34+
})
35+
injectInteractionHandler().setWhenWithParent(
36+
() => alignmentMenu.menuOpen,
37+
(parentInteraction) => {
38+
interaction.parentInteraction = parentInteraction
39+
return interaction
40+
},
41+
)
42+
43+
const { floatingStyles } = useFloating(rootElement, floatElement, {
44+
placement: () => placement,
45+
middleware: [shift()],
46+
})
47+
48+
function handleAlignmentClick() {
49+
alignmentMenu.menuOpen = false
50+
}
51+
</script>
52+
53+
<template>
54+
<div
55+
ref="rootElement"
56+
class="AlignmentMenu"
57+
@pointerenter="alignmentMenu.handleMenuEnter"
58+
@pointerleave="alignmentMenu.handleMenuLeave"
59+
@pointerdown.prevent
60+
>
61+
<MenuButton v-model="alignmentMenu.menuOpenModel" title="Align">
62+
<SvgIcon name="align_left" />
63+
</MenuButton>
64+
<SvgIcon v-show="!alignmentMenu.menuOpen" name="arrow_right_head_only" class="arrow visible" />
65+
<ConditionalTeleport :target="popoverRoot">
66+
<SizeTransition height :duration="100">
67+
<div
68+
v-if="alignmentMenu.menuOpen"
69+
ref="floatElement"
70+
class="AlignmentMenuPanel"
71+
:style="floatingStyles"
72+
@pointerenter="alignmentMenu.handleMenuEnter"
73+
@pointerleave="alignmentMenu.handleMenuLeave"
74+
@pointerdown.prevent
75+
>
76+
<MenuPanel class="alignmentMenuContent">
77+
<div class="alignmentMenuRow horizontal">
78+
<ActionButton action="components.alignLeft" @click="handleAlignmentClick" />
79+
<ActionButton action="components.alignCenter" @click="handleAlignmentClick" />
80+
<ActionButton action="components.alignRight" @click="handleAlignmentClick" />
81+
</div>
82+
<div class="alignmentMenuRow vertical">
83+
<ActionButton action="components.alignTop" @click="handleAlignmentClick" />
84+
<ActionButton action="components.alignBottom" @click="handleAlignmentClick" />
85+
</div>
86+
</MenuPanel>
87+
</div>
88+
</SizeTransition>
89+
</ConditionalTeleport>
90+
</div>
91+
</template>
92+
93+
<style scoped>
94+
.AlignmentMenu {
95+
position: relative;
96+
outline: 0;
97+
margin: -4px;
98+
}
99+
100+
.MenuButton {
101+
backdrop-filter: var(--blur-app-bg);
102+
}
103+
104+
.arrow {
105+
position: absolute;
106+
bottom: calc(-8px - var(--arrow-offset, 0px));
107+
left: 50%;
108+
opacity: 0;
109+
/* Prevent the button from receiving a pointerout event if the mouse is over the arrow, which causes flickering. */
110+
pointer-events: none;
111+
visibility: hidden;
112+
--icon-transform: translateX(-50%) rotate(90deg) scale(0.7);
113+
--icon-transform-origin: center;
114+
transition: opacity 100ms ease-in-out;
115+
.AlignmentMenu:has(.MenuButton:hover) &,
116+
&.visible {
117+
opacity: 0.8;
118+
visibility: inherit;
119+
}
120+
}
121+
122+
.AlignmentMenuPanel {
123+
z-index: 20;
124+
}
125+
126+
.alignmentMenuContent {
127+
display: flex;
128+
flex-direction: column;
129+
gap: 8px;
130+
padding: 10px 12px;
131+
}
132+
133+
/* Rows for horizontal and vertical alignment buttons */
134+
.alignmentMenuRow {
135+
display: flex;
136+
gap: 10px;
137+
}
138+
</style>

app/gui/src/project-view/components/ContextMenu.vue

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@ onMounted(() => {
7373
)
7474
}
7575
})
76+
77+
function isTargetOutside(event: Event) {
78+
return targetIsOutside(event, menu.value)
79+
}
80+
81+
defineExpose({
82+
isTargetOutside,
83+
})
7684
</script>
7785

7886
<template>

app/gui/src/project-view/components/ContextMenuTrigger.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import ContextMenu from '@/components/ContextMenu.vue'
33
import type { DisplayableActionName } from '@/providers/action'
44
import { provideActionContext } from '@/providers/actionContext'
5+
import { ref } from 'vue'
56
67
const { actions } = defineProps<{
78
actions: DisplayableActionName[]
@@ -12,6 +13,7 @@ const emit = defineEmits<{
1213
}>()
1314
1415
const ctx = provideActionContext()
16+
const menuComponent = ref<InstanceType<typeof ContextMenu>>()
1517
1618
function show(at: typeof ctx.openPosition) {
1719
ctx.openPosition = at
@@ -25,6 +27,7 @@ function hide() {
2527
2628
defineExpose({
2729
close: hide,
30+
isTargetOutside: (event: Event) => menuComponent.value?.isTargetOutside(event) ?? true,
2831
})
2932
</script>
3033

app/gui/src/project-view/components/GraphEditor/GraphNode.vue

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ import { asNodeId } from '$/providers/openedProjects/graph/graphDatabase'
1515
import { evaluationProgress } from '$/providers/openedProjects/project/computedValueRegistry'
1616
import { useNodeExecution } from '$/providers/openedProjects/project/nodeExecution'
1717
import { nodeEditBindings } from '@/bindings'
18+
import ActionMenu from '@/components/ActionMenu.vue'
1819
import ComponentMenu from '@/components/ComponentMenu.vue'
1920
import ContextMenuTrigger from '@/components/ContextMenuTrigger.vue'
20-
import MenuButton from '@/components/MenuButton.vue'
2121
import ComponentWidgetTree, {
2222
GRAB_HANDLE_X_MARGIN_L,
2323
GRAB_HANDLE_X_MARGIN_R,
@@ -29,7 +29,7 @@ import GraphNodeComment from '@/components/GraphEditor/GraphNodeComment.vue'
2929
import GraphNodeMessage from '@/components/GraphEditor/GraphNodeMessage.vue'
3030
import GraphVisualization from '@/components/GraphEditor/GraphVisualization.vue'
3131
import type { NodeCreationOptions } from '@/components/GraphEditor/nodeCreation'
32-
import ActionMenu from '@/components/ActionMenu.vue'
32+
import MenuButton from '@/components/MenuButton.vue'
3333
import { useResizeHandles } from '@/components/resizeHandles'
3434
import ResizeHandles from '@/components/ResizeHandles.vue'
3535
import SvgIcon from '@/components/SvgIcon.vue'
@@ -43,26 +43,18 @@ import { registerHandlers, toggledAction } from '@/providers/action'
4343
import { injectGraphNavigator } from '@/providers/graphNavigator'
4444
import { injectNodeColors } from '@/providers/graphNodeColors'
4545
import { useGraphSelection } from '@/providers/graphSelection'
46+
import { injectInteractionHandler, type Interaction } from '@/providers/interactionHandler'
4647
import { providePopoverRoot } from '@/providers/popoverRoot'
4748
import { provideWidgetControlledActions } from '@/providers/widgetActions'
4849
import { Ast } from '@/util/ast'
4950
import { prefixes } from '@/util/ast/node'
50-
import { onWindowBlur } from '@/util/autoBlur'
51+
import { onWindowBlur, targetIsOutside } from '@/util/autoBlur'
5152
import type { Opt } from '@/util/data/opt'
5253
import { Rect } from '@/util/data/rect'
5354
import { Vec2 } from '@/util/data/vec2'
54-
import { Ok } from 'enso-common/src/utilities/data/result'
5555
import { autoUpdate, flip, offset, shift, useFloating } from '@floating-ui/vue'
56-
import {
57-
computed,
58-
nextTick,
59-
onUnmounted,
60-
ref,
61-
toRef,
62-
watch,
63-
watchEffect,
64-
type ComponentInstance,
65-
} from 'vue'
56+
import { Ok } from 'enso-common/src/utilities/data/result'
57+
import { computed, nextTick, onUnmounted, ref, toRef, watch, watchEffect } from 'vue'
6658
import type { VisualizationIdentifier } from 'ydoc-shared/yjsModel'
6759
6860
const contentNodeStyle = {
@@ -97,6 +89,7 @@ const projectStore = useProjectStore()
9789
const graph = useGraphStore()
9890
const { module } = useCurrentProject()
9991
const navigator = injectGraphNavigator(true)
92+
const interaction = injectInteractionHandler()
10093
const nodeExecution = useNodeExecution()
10194
10295
const nodeId = computed(() => asNodeId(props.node.rootExpr.externalId))
@@ -450,8 +443,10 @@ const alignmentMenuActions: DisplayableActionName[] = [
450443
'components.alignBottom',
451444
]
452445
446+
const hasMultiSelection = computed(() => (nodeSelection?.selected.size ?? 0) > 1)
447+
453448
const contextMenuActions = computed<DisplayableActionName[]>(() =>
454-
nodeSelection.selected.size > 1 ? multiSelectionMenuActions : nodeMenuActions,
449+
hasMultiSelection.value ? multiSelectionMenuActions : nodeMenuActions,
455450
)
456451
457452
const alignmentMenu = useHoverMenu()
@@ -473,14 +468,48 @@ const { floatingStyles: alignmentMenuStyles, update: updateAlignmentMenu } = use
473468
watch(
474469
() => alignmentMenu.menuOpen,
475470
(open) => {
476-
if (open) nextTick(updateAlignmentMenu)
471+
if (open) nextTick(updateAlignmentMenu)
472+
},
473+
)
474+
475+
const alignmentSubmenuInteraction: Interaction = {
476+
cancel: () => {
477+
alignmentMenu.menuOpen = false
478+
},
479+
end: () => {
480+
alignmentMenu.menuOpen = false
481+
},
482+
pointerdown: (event) => {
483+
const outsideSubmenu =
484+
targetIsOutside(event, alignmentMenuTrigger.value) &&
485+
targetIsOutside(event, alignmentMenuPanel.value)
486+
if (!outsideSubmenu) return false
487+
488+
const parentInteraction = alignmentSubmenuInteraction.parentInteraction
489+
if (contextMenuTrigger.value?.isTargetOutside(event) && parentInteraction) {
490+
interaction.end(parentInteraction)
491+
} else {
492+
interaction.end(alignmentSubmenuInteraction)
493+
}
494+
return false
495+
},
496+
}
497+
498+
interaction.setWhenWithParent(
499+
() => alignmentMenu.menuOpen,
500+
(parentInteraction) => {
501+
alignmentSubmenuInteraction.parentInteraction = parentInteraction
502+
return alignmentSubmenuInteraction
477503
},
478504
)
479505
480506
function closeAllMenus() {
481-
// Close both menus
482-
alignmentMenu.menuOpen = false
483-
contextMenuTrigger.value?.close()
507+
const parentInteraction = alignmentSubmenuInteraction.parentInteraction
508+
if (parentInteraction) {
509+
interaction.end(parentInteraction)
510+
} else {
511+
interaction.end(alignmentSubmenuInteraction)
512+
}
484513
}
485514
486515
onWindowBlur(() => {
@@ -544,10 +573,10 @@ resizeHandles.onResizeHeight((value) => emit('update:height', value))
544573
ref="contextMenuTrigger"
545574
:actions="contextMenuActions"
546575
@contextmenu="ensureSelected"
547-
@hidden="(alignmentMenu.menuOpen = false)"
576+
@hidden="alignmentMenu.menuOpen = false"
548577
>
549578
<template #menuElements>
550-
<div v-if="nodeSelection.selected.size > 1">
579+
<div v-if="hasMultiSelection">
551580
<div
552581
ref="alignmentMenuTrigger"
553582
class="alignmentSubmenuTrigger"
@@ -568,7 +597,11 @@ resizeHandles.onResizeHeight((value) => emit('update:height', value))
568597
@pointerenter="alignmentMenu.handleMenuEnter"
569598
@pointerleave="alignmentMenu.handleMenuLeave"
570599
>
571-
<ActionMenu class="alignmentMenu" :actions="alignmentMenuActions" @close="closeAllMenus" />
600+
<ActionMenu
601+
class="alignmentMenu"
602+
:actions="alignmentMenuActions"
603+
@close="closeAllMenus"
604+
/>
572605
</div>
573606
</div>
574607
</template>

0 commit comments

Comments
 (0)