Skip to content

Commit a14a95c

Browse files
authored
Only Escape cancels edits (#9913)
1 parent 4376a5a commit a14a95c

File tree

10 files changed

+85
-49
lines changed

10 files changed

+85
-49
lines changed

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -207,11 +207,11 @@ Enso consists of several sub projects:
207207
command line tools.
208208

209209
- **Enso IDE:** The
210-
[Enso IDE](https://github.com/enso-org/enso/tree/develop/app/gui2) is a desktop
211-
application that allows working with the visual form of Enso. It consists of
212-
an Electron application, a high performance WebGL UI framework, and the
213-
searcher which provides contextual search, hints, and documentation for all of
214-
Enso's functionality.
210+
[Enso IDE](https://github.com/enso-org/enso/tree/develop/app/gui2) is a
211+
desktop application that allows working with the visual form of Enso. It
212+
consists of an Electron application, a high performance WebGL UI framework,
213+
and the searcher which provides contextual search, hints, and documentation
214+
for all of Enso's functionality.
215215

216216
<br/>
217217

app/gui2/src/components/ColorRing.vue

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
rangesForInputs,
66
} from '@/components/ColorRing/gradient'
77
import { injectInteractionHandler } from '@/providers/interactionHandler'
8-
import { targetIsOutside } from '@/util/autoBlur'
8+
import { endOnClickOutside } from '@/util/autoBlur'
99
import { cssSupported, ensoColor, formatCssColor, parseCssColor } from '@/util/colors'
1010
import { Rect } from '@/util/data/rect'
1111
import { Vec2 } from '@/util/data/vec2'
@@ -49,13 +49,12 @@ const svgElement = ref<HTMLElement>()
4949
const interaction = injectInteractionHandler()
5050
5151
onMounted(() => {
52-
interaction.setCurrent({
53-
cancel: () => emit('close'),
54-
pointerdown: (e: PointerEvent) => {
55-
if (targetIsOutside(e, svgElement.value)) emit('close')
56-
return false
57-
},
58-
})
52+
interaction.setCurrent(
53+
endOnClickOutside(svgElement, {
54+
cancel: () => emit('close'),
55+
end: () => emit('close'),
56+
}),
57+
)
5958
})
6059
6160
const mouseSelectedAngle = ref<number>()

app/gui2/src/components/ComponentBrowser.vue

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { useProjectStore } from '@/stores/project'
1818
import { groupColorStyle, useSuggestionDbStore } from '@/stores/suggestionDatabase'
1919
import { SuggestionKind } from '@/stores/suggestionDatabase/entry'
2020
import type { VisualizationDataSource } from '@/stores/visualization'
21-
import { targetIsOutside } from '@/util/autoBlur'
21+
import { endOnClickOutside } from '@/util/autoBlur'
2222
import { tryGetIndex } from '@/util/data/array'
2323
import type { Opt } from '@/util/data/opt'
2424
import { allRanges } from '@/util/data/range'
@@ -63,22 +63,19 @@ const emit = defineEmits<{
6363
canceled: []
6464
}>()
6565
66-
const cbOpen: Interaction = {
67-
cancel: () => {
68-
emit('canceled')
69-
},
70-
pointerdown: (e: PointerEvent) => {
71-
if (targetIsOutside(e, cbRoot.value)) {
72-
// In AI prompt mode likely the input is not a valid mode.
73-
if (input.anyChange.value && input.context.value.type !== 'aiPrompt') {
74-
acceptInput()
75-
} else {
76-
interaction.cancel(cbOpen)
77-
}
66+
const cbRoot = ref<HTMLElement>()
67+
68+
const cbOpen: Interaction = endOnClickOutside(cbRoot, {
69+
cancel: () => emit('canceled'),
70+
end: () => {
71+
// In AI prompt mode likely the input is not a valid mode.
72+
if (input.anyChange.value && input.context.value.type !== 'aiPrompt') {
73+
acceptInput()
74+
} else {
75+
emit('canceled')
7876
}
79-
return false
8077
},
81-
}
78+
})
8279
8380
function scaleValues<T extends Record<any, number>>(
8481
values: T,
@@ -141,7 +138,6 @@ const transform = computed(() => {
141138
142139
// === Input and Filtering ===
143140
144-
const cbRoot = ref<HTMLElement>()
145141
const inputField = ref<HTMLInputElement>()
146142
const input = useComponentBrowserInput()
147143
const filterFlags = ref({ showUnstable: false, showLocal: false })
@@ -413,7 +409,7 @@ function acceptSuggestion(component: Opt<Component> = null) {
413409
414410
function acceptInput() {
415411
emit('accepted', input.code.value.trim(), input.importsToAdd())
416-
interaction.end(cbOpen)
412+
interaction.ended(cbOpen)
417413
}
418414
419415
// === Key Events Handler ===

app/gui2/src/components/GraphEditor/GraphEdges.vue

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,11 @@ const emits = defineEmits<{
2525
const MIN_DRAG_MOVE = 10
2626
2727
const editingEdge: Interaction = {
28-
cancel() {
29-
graph.clearUnconnected()
30-
},
31-
pointerdown(_e: PointerEvent, graphNavigator: GraphNavigator): boolean {
32-
return edgeInteractionClick(graphNavigator)
33-
},
34-
pointerup(e: PointerEvent, graphNavigator: GraphNavigator): boolean {
28+
cancel: () => graph.clearUnconnected(),
29+
end: () => graph.clearUnconnected(),
30+
pointerdown: (_e: PointerEvent, graphNavigator: GraphNavigator) =>
31+
edgeInteractionClick(graphNavigator),
32+
pointerup: (e: PointerEvent, graphNavigator: GraphNavigator) => {
3533
const originEvent = graph.unconnectedEdge?.event
3634
if (originEvent?.type === 'pointerdown') {
3735
const delta = new Vec2(e.screenX, e.screenY).sub(

app/gui2/src/components/GraphEditor/GraphNodeComment.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const editor = ref<EditorViewType>()
2626
const interactions = injectInteractionHandler()
2727
const editInteraction = {
2828
cancel: () => finishEdit(),
29+
end: () => finishEdit(),
2930
click: (e: Event) => {
3031
if (e.target instanceof Element && !commentRoot.value?.contains(e.target)) finishEdit()
3132
return false

app/gui2/src/components/GraphEditor/widgets/WidgetSelection.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ provideSelectionArrow(
226226
const isMulti = computed(() => props.input.dynamicConfig?.kind === 'Multiple_Choice')
227227
const dropDownInteraction = WidgetEditHandler.New('WidgetSelection', props.input, {
228228
cancel: () => {},
229+
end: () => {},
229230
pointerdown: (e, _) => {
230231
if (targetIsOutside(e, unrefElement(dropdownElement))) {
231232
dropDownInteraction.end()

app/gui2/src/providers/interactionHandler.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export class InteractionHandler {
2828

2929
setCurrent(interaction: Interaction | undefined) {
3030
if (!this.isActive(interaction)) {
31-
this.currentInteraction?.cancel?.()
31+
this.currentInteraction?.end()
3232
this.currentInteraction = interaction
3333
}
3434
}
@@ -37,19 +37,31 @@ export class InteractionHandler {
3737
return this.currentInteraction
3838
}
3939

40-
/** Unset the current interaction, if it is the specified instance. */
41-
end(interaction: Interaction) {
40+
/** Clear the current interaction without calling any callback, if the current interaction is `interaction`. */
41+
ended(interaction: Interaction) {
4242
if (this.isActive(interaction)) this.currentInteraction = undefined
4343
}
4444

45+
/** End the current interaction, if it is the specified instance. */
46+
end(interaction: Interaction) {
47+
if (this.isActive(interaction)) {
48+
this.currentInteraction = undefined
49+
interaction.end()
50+
}
51+
}
52+
4553
/** Cancel the current interaction, if it is the specified instance. */
4654
cancel(interaction: Interaction) {
47-
if (this.isActive(interaction)) this.setCurrent(undefined)
55+
if (this.isActive(interaction)) {
56+
this.currentInteraction = undefined
57+
interaction.cancel()
58+
}
4859
}
4960

5061
handleCancel(): boolean {
5162
const hasCurrent = this.currentInteraction != null
52-
if (hasCurrent) this.setCurrent(undefined)
63+
this.currentInteraction?.cancel()
64+
this.currentInteraction = undefined
5365
return hasCurrent
5466
}
5567

@@ -74,7 +86,10 @@ export class InteractionHandler {
7486
type InteractionEventHandler = (event: PointerEvent, navigator: GraphNavigator) => boolean | void
7587

7688
export interface Interaction {
89+
/** Called when the interaction is explicitly canceled, e.g. with the `Esc` key. */
7790
cancel(): void
91+
/** Called when the interaction is ended due to activity elsewhere. */
92+
end(): void
7893
/** Uses a `capture` event handler to allow an interaction to respond to clicks over any element. */
7994
pointerdown?: InteractionEventHandler
8095
/** Uses a `capture` event handler to allow an interaction to respond to mouse button release

app/gui2/src/providers/widgetRegistry/__tests__/editHandler.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ test.each`
112112
expect(editedHandler.handler.isActive()).toBeTruthy()
113113
interactionHandler.setCurrent(undefined)
114114
expect(widgetTree.currentEdit).toBeUndefined()
115-
checkCallbackCall('cancel')
115+
checkCallbackCall('end', undefined)
116116
expect(editedHandler.handler.isActive()).toBeFalsy()
117117
},
118118
)

app/gui2/src/providers/widgetRegistry/editHandler.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export class WidgetEditHandler {
6666
noLongerActive()
6767
hooks.cancel?.()
6868
},
69-
end: (origin: WidgetId) => {
69+
end: (origin?: WidgetId) => {
7070
noLongerActive()
7171
hooks.end?.(origin)
7272
},
@@ -151,7 +151,7 @@ export interface WidgetEditHooks extends Interaction {
151151
* {@link WidgetEditHandler} being called, or because a child is to be started.
152152
*/
153153
start?(origin: WidgetId): void
154-
end?(origin: WidgetId): void
154+
end(origin?: WidgetId | undefined): void
155155
/**
156156
* Hook called when a child widget, or this widget itself, provides an updated value.
157157
*/
@@ -223,15 +223,15 @@ class PortEditInteraction implements Interaction {
223223
this.shutdown()
224224
}
225225

226-
end(origin: WidgetId) {
226+
end(origin?: WidgetId) {
227227
for (const interaction of this.interactions) interaction.end?.(origin)
228228
this.shutdown()
229229
}
230230

231231
private shutdown() {
232232
this.interactions.length = 0
233233
this.active.value = false
234-
this.interactionHandler.end(this)
234+
this.interactionHandler.ended(this)
235235
}
236236

237237
register(interaction: PortEditSubinteraction) {
@@ -269,14 +269,17 @@ class SuspendedPortEdit implements Interaction {
269269
}
270270

271271
cancel() {}
272+
273+
end() {}
272274
}
273275

274276
/** A sub-interaction of a @{link PortEditInteraction} */
275277
interface PortEditSubinteraction extends Interaction {
276278
widgetId: WidgetId
277279

278280
suspend?: () => { resume: () => void }
279-
end?(origin: WidgetId): void
281+
282+
end(origin?: WidgetId | undefined): void
280283
}
281284

282285
/** @internal Public for unit testing.

app/gui2/src/util/autoBlur.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { useEvent } from '@/composables/events'
1+
import { unrefElement, useEvent } from '@/composables/events'
2+
import { injectInteractionHandler, type Interaction } from '@/providers/interactionHandler'
3+
import type { VueInstance } from '@vueuse/core'
24
import type { Opt } from 'shared/util/data/opt'
35
import { watchEffect, type Ref } from 'vue'
46

@@ -45,3 +47,24 @@ export function registerAutoBlurHandler() {
4547
export function targetIsOutside(e: Event, area: Opt<Element>): boolean {
4648
return !!area && e.target instanceof Element && !area.contains(e.target)
4749
}
50+
51+
/** Returns a new interaction based on the given `interaction`. The new interaction will be ended if a pointerdown event
52+
* occurs outside the given `area` element. */
53+
export function endOnClickOutside(
54+
area: Ref<Element | VueInstance | null | undefined>,
55+
interaction: Interaction,
56+
): Interaction {
57+
const chainedPointerdown = interaction.pointerdown
58+
const handler = injectInteractionHandler()
59+
const wrappedInteraction: Interaction = {
60+
...interaction,
61+
pointerdown: (e: PointerEvent, ...args) => {
62+
if (targetIsOutside(e, unrefElement(area))) {
63+
handler.end(wrappedInteraction)
64+
return false
65+
}
66+
return chainedPointerdown ? chainedPointerdown(e, ...args) : false
67+
},
68+
}
69+
return wrappedInteraction
70+
}

0 commit comments

Comments
 (0)