Only Escape cancels edits (#9913)

This commit is contained in:
Kaz Wesley 2024-05-10 09:13:59 -07:00 committed by GitHub
parent 4376a5a851
commit a14a95c057
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 85 additions and 49 deletions

View File

@ -207,11 +207,11 @@ Enso consists of several sub projects:
command line tools.
- **Enso IDE:** The
[Enso IDE](https://github.com/enso-org/enso/tree/develop/app/gui2) is a desktop
application that allows working with the visual form of Enso. It consists of
an Electron application, a high performance WebGL UI framework, and the
searcher which provides contextual search, hints, and documentation for all of
Enso's functionality.
[Enso IDE](https://github.com/enso-org/enso/tree/develop/app/gui2) is a
desktop application that allows working with the visual form of Enso. It
consists of an Electron application, a high performance WebGL UI framework,
and the searcher which provides contextual search, hints, and documentation
for all of Enso's functionality.
<br/>

View File

@ -5,7 +5,7 @@ import {
rangesForInputs,
} from '@/components/ColorRing/gradient'
import { injectInteractionHandler } from '@/providers/interactionHandler'
import { targetIsOutside } from '@/util/autoBlur'
import { endOnClickOutside } from '@/util/autoBlur'
import { cssSupported, ensoColor, formatCssColor, parseCssColor } from '@/util/colors'
import { Rect } from '@/util/data/rect'
import { Vec2 } from '@/util/data/vec2'
@ -49,13 +49,12 @@ const svgElement = ref<HTMLElement>()
const interaction = injectInteractionHandler()
onMounted(() => {
interaction.setCurrent({
cancel: () => emit('close'),
pointerdown: (e: PointerEvent) => {
if (targetIsOutside(e, svgElement.value)) emit('close')
return false
},
})
interaction.setCurrent(
endOnClickOutside(svgElement, {
cancel: () => emit('close'),
end: () => emit('close'),
}),
)
})
const mouseSelectedAngle = ref<number>()

View File

@ -18,7 +18,7 @@ import { useProjectStore } from '@/stores/project'
import { groupColorStyle, useSuggestionDbStore } from '@/stores/suggestionDatabase'
import { SuggestionKind } from '@/stores/suggestionDatabase/entry'
import type { VisualizationDataSource } from '@/stores/visualization'
import { targetIsOutside } from '@/util/autoBlur'
import { endOnClickOutside } from '@/util/autoBlur'
import { tryGetIndex } from '@/util/data/array'
import type { Opt } from '@/util/data/opt'
import { allRanges } from '@/util/data/range'
@ -63,22 +63,19 @@ const emit = defineEmits<{
canceled: []
}>()
const cbOpen: Interaction = {
cancel: () => {
emit('canceled')
},
pointerdown: (e: PointerEvent) => {
if (targetIsOutside(e, cbRoot.value)) {
// In AI prompt mode likely the input is not a valid mode.
if (input.anyChange.value && input.context.value.type !== 'aiPrompt') {
acceptInput()
} else {
interaction.cancel(cbOpen)
}
const cbRoot = ref<HTMLElement>()
const cbOpen: Interaction = endOnClickOutside(cbRoot, {
cancel: () => emit('canceled'),
end: () => {
// In AI prompt mode likely the input is not a valid mode.
if (input.anyChange.value && input.context.value.type !== 'aiPrompt') {
acceptInput()
} else {
emit('canceled')
}
return false
},
}
})
function scaleValues<T extends Record<any, number>>(
values: T,
@ -141,7 +138,6 @@ const transform = computed(() => {
// === Input and Filtering ===
const cbRoot = ref<HTMLElement>()
const inputField = ref<HTMLInputElement>()
const input = useComponentBrowserInput()
const filterFlags = ref({ showUnstable: false, showLocal: false })
@ -413,7 +409,7 @@ function acceptSuggestion(component: Opt<Component> = null) {
function acceptInput() {
emit('accepted', input.code.value.trim(), input.importsToAdd())
interaction.end(cbOpen)
interaction.ended(cbOpen)
}
// === Key Events Handler ===

View File

@ -25,13 +25,11 @@ const emits = defineEmits<{
const MIN_DRAG_MOVE = 10
const editingEdge: Interaction = {
cancel() {
graph.clearUnconnected()
},
pointerdown(_e: PointerEvent, graphNavigator: GraphNavigator): boolean {
return edgeInteractionClick(graphNavigator)
},
pointerup(e: PointerEvent, graphNavigator: GraphNavigator): boolean {
cancel: () => graph.clearUnconnected(),
end: () => graph.clearUnconnected(),
pointerdown: (_e: PointerEvent, graphNavigator: GraphNavigator) =>
edgeInteractionClick(graphNavigator),
pointerup: (e: PointerEvent, graphNavigator: GraphNavigator) => {
const originEvent = graph.unconnectedEdge?.event
if (originEvent?.type === 'pointerdown') {
const delta = new Vec2(e.screenX, e.screenY).sub(

View File

@ -26,6 +26,7 @@ const editor = ref<EditorViewType>()
const interactions = injectInteractionHandler()
const editInteraction = {
cancel: () => finishEdit(),
end: () => finishEdit(),
click: (e: Event) => {
if (e.target instanceof Element && !commentRoot.value?.contains(e.target)) finishEdit()
return false

View File

@ -226,6 +226,7 @@ provideSelectionArrow(
const isMulti = computed(() => props.input.dynamicConfig?.kind === 'Multiple_Choice')
const dropDownInteraction = WidgetEditHandler.New('WidgetSelection', props.input, {
cancel: () => {},
end: () => {},
pointerdown: (e, _) => {
if (targetIsOutside(e, unrefElement(dropdownElement))) {
dropDownInteraction.end()

View File

@ -28,7 +28,7 @@ export class InteractionHandler {
setCurrent(interaction: Interaction | undefined) {
if (!this.isActive(interaction)) {
this.currentInteraction?.cancel?.()
this.currentInteraction?.end()
this.currentInteraction = interaction
}
}
@ -37,19 +37,31 @@ export class InteractionHandler {
return this.currentInteraction
}
/** Unset the current interaction, if it is the specified instance. */
end(interaction: Interaction) {
/** Clear the current interaction without calling any callback, if the current interaction is `interaction`. */
ended(interaction: Interaction) {
if (this.isActive(interaction)) this.currentInteraction = undefined
}
/** End the current interaction, if it is the specified instance. */
end(interaction: Interaction) {
if (this.isActive(interaction)) {
this.currentInteraction = undefined
interaction.end()
}
}
/** Cancel the current interaction, if it is the specified instance. */
cancel(interaction: Interaction) {
if (this.isActive(interaction)) this.setCurrent(undefined)
if (this.isActive(interaction)) {
this.currentInteraction = undefined
interaction.cancel()
}
}
handleCancel(): boolean {
const hasCurrent = this.currentInteraction != null
if (hasCurrent) this.setCurrent(undefined)
this.currentInteraction?.cancel()
this.currentInteraction = undefined
return hasCurrent
}
@ -74,7 +86,10 @@ export class InteractionHandler {
type InteractionEventHandler = (event: PointerEvent, navigator: GraphNavigator) => boolean | void
export interface Interaction {
/** Called when the interaction is explicitly canceled, e.g. with the `Esc` key. */
cancel(): void
/** Called when the interaction is ended due to activity elsewhere. */
end(): void
/** Uses a `capture` event handler to allow an interaction to respond to clicks over any element. */
pointerdown?: InteractionEventHandler
/** Uses a `capture` event handler to allow an interaction to respond to mouse button release

View File

@ -112,7 +112,7 @@ test.each`
expect(editedHandler.handler.isActive()).toBeTruthy()
interactionHandler.setCurrent(undefined)
expect(widgetTree.currentEdit).toBeUndefined()
checkCallbackCall('cancel')
checkCallbackCall('end', undefined)
expect(editedHandler.handler.isActive()).toBeFalsy()
},
)

View File

@ -66,7 +66,7 @@ export class WidgetEditHandler {
noLongerActive()
hooks.cancel?.()
},
end: (origin: WidgetId) => {
end: (origin?: WidgetId) => {
noLongerActive()
hooks.end?.(origin)
},
@ -151,7 +151,7 @@ export interface WidgetEditHooks extends Interaction {
* {@link WidgetEditHandler} being called, or because a child is to be started.
*/
start?(origin: WidgetId): void
end?(origin: WidgetId): void
end(origin?: WidgetId | undefined): void
/**
* Hook called when a child widget, or this widget itself, provides an updated value.
*/
@ -223,7 +223,7 @@ class PortEditInteraction implements Interaction {
this.shutdown()
}
end(origin: WidgetId) {
end(origin?: WidgetId) {
for (const interaction of this.interactions) interaction.end?.(origin)
this.shutdown()
}
@ -231,7 +231,7 @@ class PortEditInteraction implements Interaction {
private shutdown() {
this.interactions.length = 0
this.active.value = false
this.interactionHandler.end(this)
this.interactionHandler.ended(this)
}
register(interaction: PortEditSubinteraction) {
@ -269,6 +269,8 @@ class SuspendedPortEdit implements Interaction {
}
cancel() {}
end() {}
}
/** A sub-interaction of a @{link PortEditInteraction} */
@ -276,7 +278,8 @@ interface PortEditSubinteraction extends Interaction {
widgetId: WidgetId
suspend?: () => { resume: () => void }
end?(origin: WidgetId): void
end(origin?: WidgetId | undefined): void
}
/** @internal Public for unit testing.

View File

@ -1,4 +1,6 @@
import { useEvent } from '@/composables/events'
import { unrefElement, useEvent } from '@/composables/events'
import { injectInteractionHandler, type Interaction } from '@/providers/interactionHandler'
import type { VueInstance } from '@vueuse/core'
import type { Opt } from 'shared/util/data/opt'
import { watchEffect, type Ref } from 'vue'
@ -45,3 +47,24 @@ export function registerAutoBlurHandler() {
export function targetIsOutside(e: Event, area: Opt<Element>): boolean {
return !!area && e.target instanceof Element && !area.contains(e.target)
}
/** Returns a new interaction based on the given `interaction`. The new interaction will be ended if a pointerdown event
* occurs outside the given `area` element. */
export function endOnClickOutside(
area: Ref<Element | VueInstance | null | undefined>,
interaction: Interaction,
): Interaction {
const chainedPointerdown = interaction.pointerdown
const handler = injectInteractionHandler()
const wrappedInteraction: Interaction = {
...interaction,
pointerdown: (e: PointerEvent, ...args) => {
if (targetIsOutside(e, unrefElement(area))) {
handler.end(wrappedInteraction)
return false
}
return chainedPointerdown ? chainedPointerdown(e, ...args) : false
},
}
return wrappedInteraction
}