mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 15:12:15 +03:00
Inhibit clipping when dropdown is opened (#9434)
Fixes #9379. # Important Notes - The existence-registry could be implemented with a counter, but a set is more debuggable and the performance cost is negligible.
This commit is contained in:
parent
9a9eff1aa6
commit
babf4eba03
@ -7,7 +7,7 @@ import { provideWidgetTree } from '@/providers/widgetTree'
|
||||
import { useGraphStore, type NodeId } from '@/stores/graph'
|
||||
import { Ast } from '@/util/ast'
|
||||
import type { Icon } from '@/util/iconName'
|
||||
import { computed, toRef } from 'vue'
|
||||
import { computed, ref, toRef } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
ast: Ast.Ast
|
||||
@ -65,6 +65,26 @@ function handleWidgetUpdates(update: WidgetUpdate) {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* We have two goals for our DOM/CSS that are somewhat in conflict:
|
||||
* - We position widget dialogs drawn outside the widget, like dropdowns, relative to their parents. If we teleported
|
||||
* them, we'd have to maintain their positions through JS; the focus hierarchy would also be affected.
|
||||
* - We animate showing/hiding conditionally-visible placeholder arguments; the implementation of this animation
|
||||
* requires the use of `overflow-x: clip` for the placeholder argument. There doesn't seem to be any good alternative
|
||||
* to clipping in order to achieve a suitable style of animation with CSS.
|
||||
* Because clipping is absolute (there is no way for an element to draw outside its clipped ancestor), it is hard to
|
||||
* reconcile with dropdowns.
|
||||
*
|
||||
* However, we can have our cake and eat it to--as long as we don't need both at once. The solution implemented here is
|
||||
* for the widget tree to provide an interface for a widget to signal that it is in a state requiring drawing outside
|
||||
* the node, and for widgets implementing clipping-based animations to mark them with a CSS class.
|
||||
*
|
||||
* This is not a perfect solution; it's possible for the user to cause a dropdown to be displayed before the
|
||||
* showing-placeholders animation finishes. In that case the animation will run without clipping, which looks a little
|
||||
* off. However, it allows us to use the DOM/CSS both for positioning the dropdown and animating the placeholders.
|
||||
*/
|
||||
const deepDisableClipping = ref(false)
|
||||
|
||||
const layoutTransitions = useTransitioning(observedLayoutTransitions)
|
||||
provideWidgetTree(
|
||||
toRef(props, 'ast'),
|
||||
@ -77,6 +97,7 @@ provideWidgetTree(
|
||||
() => {
|
||||
emit('openFullMenu')
|
||||
},
|
||||
(clippingInhibitorsExist) => (deepDisableClipping.value = clippingInhibitorsExist),
|
||||
)
|
||||
</script>
|
||||
<script lang="ts">
|
||||
@ -86,7 +107,12 @@ export const ICON_WIDTH = 16
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="NodeWidgetTree" spellcheck="false" v-on="layoutTransitions.events">
|
||||
<div
|
||||
class="NodeWidgetTree"
|
||||
:class="{ deepDisableClipping }"
|
||||
spellcheck="false"
|
||||
v-on="layoutTransitions.events"
|
||||
>
|
||||
<!-- Display an icon for the node if no widget in the tree provides one. -->
|
||||
<SvgIcon
|
||||
v-if="!props.connectedSelfArgumentId"
|
||||
@ -128,4 +154,8 @@ export const ICON_WIDTH = 16
|
||||
color: white;
|
||||
margin: 0 v-bind('GRAB_HANDLE_X_MARGIN_PX');
|
||||
}
|
||||
|
||||
.deepDisableClipping :deep(.overridableClipState) {
|
||||
overflow: visible !important;
|
||||
}
|
||||
</style>
|
||||
|
@ -48,7 +48,10 @@ export const widgetDefinition = defineWidget(ArgumentApplicationKey, {
|
||||
<NodeWidget :input="WidgetInput.FromAst(application.infixOperator)" />
|
||||
</div>
|
||||
<Transition name="collapse-argument">
|
||||
<div v-if="tree.extended || !application.argument.hideByDefault" class="argument">
|
||||
<div
|
||||
v-if="tree.extended || !application.argument.hideByDefault"
|
||||
class="argument overridableClipState"
|
||||
>
|
||||
<NodeWidget :input="application.argument.toWidgetInput()" nest />
|
||||
</div>
|
||||
</Transition>
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
type ArgumentWidgetConfiguration,
|
||||
} from '@/providers/widgetRegistry/configuration'
|
||||
import { WidgetEditHandler } from '@/providers/widgetRegistry/editHandler'
|
||||
import { injectWidgetTree } from '@/providers/widgetTree.ts'
|
||||
import { useGraphStore } from '@/stores/graph'
|
||||
import { requiredImports, type RequiredImport } from '@/stores/graph/imports.ts'
|
||||
import { useSuggestionDbStore } from '@/stores/suggestionDatabase'
|
||||
@ -27,6 +28,9 @@ import { computed, ref, watch, type ComponentInstance } from 'vue'
|
||||
const props = defineProps(widgetProps(widgetDefinition))
|
||||
const suggestions = useSuggestionDbStore()
|
||||
const graph = useGraphStore()
|
||||
|
||||
const tree = injectWidgetTree()
|
||||
|
||||
const widgetRoot = ref<HTMLElement>()
|
||||
const dropdownElement = ref<ComponentInstance<typeof DropdownWidget>>()
|
||||
|
||||
@ -252,6 +256,17 @@ watch(selectedIndex, (_index) => {
|
||||
}
|
||||
props.onUpdate({ edit, portUpdate: { value, origin: props.input.portId } })
|
||||
})
|
||||
|
||||
let endClippingInhibition: (() => void) | undefined
|
||||
watch(dropdownVisible, (visible) => {
|
||||
if (visible) {
|
||||
const { unregister } = tree.inhibitClipping()
|
||||
endClippingInhibition = unregister
|
||||
} else {
|
||||
endClippingInhibition?.()
|
||||
endClippingInhibition = undefined
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -5,6 +5,24 @@ import { Ast } from '@/util/ast'
|
||||
import type { Icon } from '@/util/iconName'
|
||||
import { computed, proxyRefs, type Ref } from 'vue'
|
||||
|
||||
function makeExistenceRegistry(onChange: (anyExist: boolean) => void) {
|
||||
const registered = new Set<number>()
|
||||
let nextId = 0
|
||||
return {
|
||||
register: () => {
|
||||
const id = nextId++
|
||||
if (registered.size === 0) onChange(true)
|
||||
registered.add(id)
|
||||
return {
|
||||
unregister: () => {
|
||||
registered.delete(id)
|
||||
if (registered.size === 0) onChange(false)
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export { injectFn as injectWidgetTree, provideFn as provideWidgetTree }
|
||||
const { provideFn, injectFn } = createContextStore(
|
||||
'Widget tree',
|
||||
@ -17,9 +35,11 @@ const { provideFn, injectFn } = createContextStore(
|
||||
extended: Ref<boolean>,
|
||||
hasActiveAnimations: Ref<boolean>,
|
||||
emitOpenFullMenu: () => void,
|
||||
clippingInhibitorsChanged: (anyExist: boolean) => void,
|
||||
) => {
|
||||
const graph = useGraphStore()
|
||||
const nodeSpanStart = computed(() => graph.moduleSource.getSpan(astRoot.value.id)![0])
|
||||
const { register: inhibitClipping } = makeExistenceRegistry(clippingInhibitorsChanged)
|
||||
return proxyRefs({
|
||||
astRoot,
|
||||
nodeId,
|
||||
@ -30,6 +50,7 @@ const { provideFn, injectFn } = createContextStore(
|
||||
nodeSpanStart,
|
||||
hasActiveAnimations,
|
||||
emitOpenFullMenu,
|
||||
inhibitClipping,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user