mirror of
https://github.com/enso-org/enso.git
synced 2024-12-22 12:11:31 +03:00
Implement text ellipsis on dropdowns (#10198)
Fixes #10091 https://github.com/enso-org/enso/assets/919491/148d117a-8f2c-4176-b0c8-c7d2390173c3
This commit is contained in:
parent
8423f31884
commit
0e17beba73
@ -4,6 +4,8 @@
|
||||
|
||||
- [Arrows navigation][10179] selected nodes may be moved around, or entire scene
|
||||
if no node is selected.
|
||||
- [Added a limit for dropdown width][10198], implemented ellipsis and scrolling
|
||||
for long labels when hovered.
|
||||
- [Copy-pasting multiple nodes][10194].
|
||||
- The documentation editor has [formatting toolbars][10064].
|
||||
- The documentation editor supports [rendering images][10205].
|
||||
@ -11,6 +13,7 @@
|
||||
[10064]: https://github.com/enso-org/enso/pull/10064
|
||||
[10179]: https://github.com/enso-org/enso/pull/10179
|
||||
[10194]: https://github.com/enso-org/enso/pull/10194
|
||||
[10198]: https://github.com/enso-org/enso/pull/10198
|
||||
[10205]: https://github.com/enso-org/enso/pull/10205
|
||||
|
||||
#### Enso Standard Library
|
||||
|
@ -30,7 +30,7 @@ const showColorPicker = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="CircularMenu">
|
||||
<div class="CircularMenu" :class="{ partial: !props.isFullMenuVisible }">
|
||||
<div
|
||||
v-if="!showColorPicker"
|
||||
class="circle menu"
|
||||
|
@ -11,7 +11,8 @@ import GraphNodeSelection from '@/components/GraphEditor/GraphNodeSelection.vue'
|
||||
import GraphVisualization from '@/components/GraphEditor/GraphVisualization.vue'
|
||||
import type { NodeCreationOptions } from '@/components/GraphEditor/nodeCreation'
|
||||
import NodeWidgetTree, {
|
||||
GRAB_HANDLE_X_MARGIN,
|
||||
GRAB_HANDLE_X_MARGIN_L,
|
||||
GRAB_HANDLE_X_MARGIN_R,
|
||||
ICON_WIDTH,
|
||||
} from '@/components/GraphEditor/NodeWidgetTree.vue'
|
||||
import SvgIcon from '@/components/SvgIcon.vue'
|
||||
@ -210,7 +211,8 @@ watchEffect(() => {
|
||||
}
|
||||
const inZone = (pos: Vec2 | undefined) =>
|
||||
pos != null &&
|
||||
pos.sub(props.node.position).x < CONTENT_PADDING + ICON_WIDTH + GRAB_HANDLE_X_MARGIN * 2
|
||||
pos.sub(props.node.position).x <
|
||||
CONTENT_PADDING + ICON_WIDTH + GRAB_HANDLE_X_MARGIN_L + GRAB_HANDLE_X_MARGIN_R
|
||||
const hovered =
|
||||
menuHovered.value ||
|
||||
inZone(nodeHoverPos.value) ||
|
||||
@ -757,7 +759,7 @@ watchEffect(() => {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
z-index: 2;
|
||||
z-index: 24;
|
||||
transition: outline 0.2s ease;
|
||||
outline: 0px solid transparent;
|
||||
}
|
||||
@ -781,12 +783,6 @@ watchEffect(() => {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.CircularMenu {
|
||||
z-index: 25;
|
||||
}
|
||||
|
@ -97,7 +97,7 @@ const defaultVisualizationRaw = projectStore.useVisualizationData(
|
||||
const defaultVisualizationForCurrentNodeSource = computed<VisualizationIdentifier | undefined>(
|
||||
() => {
|
||||
const raw = defaultVisualizationRaw.value
|
||||
if (!raw?.ok || !raw.value) return
|
||||
if (!raw?.ok || !raw.value || !raw.value.name) return
|
||||
return {
|
||||
name: raw.value.name,
|
||||
module:
|
||||
|
@ -98,7 +98,8 @@ provideWidgetTree(
|
||||
)
|
||||
</script>
|
||||
<script lang="ts">
|
||||
export const GRAB_HANDLE_X_MARGIN = 4
|
||||
export const GRAB_HANDLE_X_MARGIN_L = 4
|
||||
export const GRAB_HANDLE_X_MARGIN_R = 8
|
||||
export const ICON_WIDTH = 16
|
||||
</script>
|
||||
|
||||
@ -108,7 +109,7 @@ export const ICON_WIDTH = 16
|
||||
<SvgIcon
|
||||
v-if="!props.connectedSelfArgumentId"
|
||||
class="icon grab-handle nodeCategoryIcon"
|
||||
:style="{ margin: `0 ${GRAB_HANDLE_X_MARGIN}px` }"
|
||||
:style="{ margin: `0 ${GRAB_HANDLE_X_MARGIN_R}px 0 ${GRAB_HANDLE_X_MARGIN_L}px` }"
|
||||
:name="props.icon"
|
||||
@click.right.stop.prevent="emit('openFullMenu')"
|
||||
/>
|
||||
|
@ -279,7 +279,8 @@ export const widgetDefinition = defineWidget(
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.isSelfArgument {
|
||||
margin-right: 2px;
|
||||
/* Selector specificity must be high enough to override r-24 specific styles. */
|
||||
.WidgetPort.isSelfArgument.isSelfArgument {
|
||||
margin-right: 6px;
|
||||
}
|
||||
</style>
|
||||
|
@ -26,7 +26,7 @@ import { ArgumentInfoKey } from '@/util/callTree'
|
||||
import { arrayEquals } from '@/util/data/array'
|
||||
import type { Opt } from '@/util/data/opt'
|
||||
import { qnLastSegment, tryQualifiedName } from '@/util/qualifiedName'
|
||||
import { autoUpdate, offset, size, useFloating } from '@floating-ui/vue'
|
||||
import { autoUpdate, offset, shift, size, useFloating } from '@floating-ui/vue'
|
||||
import { computed, proxyRefs, ref, type ComponentInstance, type RendererNode } from 'vue'
|
||||
|
||||
const props = defineProps(widgetProps(widgetDefinition))
|
||||
@ -42,6 +42,10 @@ const editedWidget = ref<string>()
|
||||
const editedValue = ref<Ast.Owned | string | undefined>()
|
||||
const isHovered = ref(false)
|
||||
|
||||
// How much wider a dropdown can be than a port it is attached to, when a long text is present.
|
||||
// Any text beyond that limit will receive an ellipsis and sliding animation on hover.
|
||||
const MAX_DROPDOWN_OVERSIZE_PX = 150
|
||||
|
||||
const { floatingStyles } = useFloating(widgetRoot, dropdownElement, {
|
||||
middleware: computed(() => {
|
||||
return [
|
||||
@ -51,13 +55,26 @@ const { floatingStyles } = useFloating(widgetRoot, dropdownElement, {
|
||||
mainAxis: (NODE_HEIGHT - state.rects.reference.height) / 2,
|
||||
}
|
||||
}),
|
||||
size({
|
||||
apply({ elements, rects }) {
|
||||
Object.assign(elements.floating.style, {
|
||||
minWidth: `${rects.reference.width + 16}px`,
|
||||
})
|
||||
size(() => ({
|
||||
elementContext: 'reference',
|
||||
apply({ elements, rects, availableWidth }) {
|
||||
const PORT_PADDING_X = 8
|
||||
const screenOverflow = Math.max(
|
||||
(rects.floating.width - availableWidth) / 2 + PORT_PADDING_X,
|
||||
0,
|
||||
)
|
||||
const portWidth = rects.reference.width + PORT_PADDING_X * 2
|
||||
|
||||
const minWidth = `${Math.max(portWidth - screenOverflow, 0)}px`
|
||||
const maxWidth = `${portWidth + MAX_DROPDOWN_OVERSIZE_PX}px`
|
||||
|
||||
Object.assign(elements.floating.style, { minWidth, maxWidth })
|
||||
elements.floating.style.setProperty('--dropdown-max-width', maxWidth)
|
||||
},
|
||||
}),
|
||||
})),
|
||||
// Try to keep the dropdown within node's bounds.
|
||||
shift(() => (tree.nodeElement ? { boundary: tree.nodeElement } : {})),
|
||||
shift(), // Always keep within screen bounds, overriding node bounds.
|
||||
]
|
||||
}),
|
||||
whileElementsMounted: autoUpdate,
|
||||
@ -185,6 +202,7 @@ const selectedExpressions = computed(() => {
|
||||
}
|
||||
} else {
|
||||
const code = removeSurroundingParens(WidgetInput.valueRepr(props.input))
|
||||
if (code?.includes(' ')) selected.add(code.substring(0, code.indexOf(' ')))
|
||||
if (code) selected.add(code)
|
||||
}
|
||||
return selected
|
||||
|
@ -26,14 +26,8 @@ export const widgetDefinition = defineWidget(
|
||||
|
||||
<template>
|
||||
<SvgIcon
|
||||
class="WidgetSelfIcon icon nodeCategoryIcon"
|
||||
class="WidgetSelfIcon icon nodeCategoryIcon r-24"
|
||||
:name="icon"
|
||||
@click.right.stop.prevent="tree.emitOpenFullMenu()"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.icon {
|
||||
margin: 0 4px;
|
||||
}
|
||||
</style>
|
||||
|
@ -23,6 +23,7 @@ const props = defineProps<{
|
||||
|
||||
<style scoped>
|
||||
svg {
|
||||
overflow: visible; /* Prevent slight cutting off icons that are using all available space. */
|
||||
width: var(--icon-width, var(--icon-size, 16px));
|
||||
height: var(--icon-height, var(--icon-size, 16px));
|
||||
transform: var(--icon-transform);
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script setup lang="ts" generic="Entry extends DropdownEntry">
|
||||
import SvgIcon from '@/components/SvgIcon.vue'
|
||||
import { injectGraphNavigator } from '@/providers/graphNavigator'
|
||||
import type { Icon } from '@/util/iconName'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
@ -13,6 +14,7 @@ const props = defineProps<{ color: string; entries: Entry[] }>()
|
||||
const emit = defineEmits<{ clickEntry: [entry: Entry, keepOpen: boolean] }>()
|
||||
|
||||
const sortDirection = ref<SortDirection>(SortDirection.none)
|
||||
const graphNavigator = injectGraphNavigator(true)
|
||||
|
||||
function lexicalCmp(a: string, b: string) {
|
||||
return (
|
||||
@ -51,6 +53,15 @@ const NEXT_SORT_DIRECTION: Record<SortDirection, SortDirection> = {
|
||||
|
||||
// Currently unused.
|
||||
const enableSortButton = ref(false)
|
||||
|
||||
const styleVars = computed(() => {
|
||||
return {
|
||||
'--dropdown-bg': props.color,
|
||||
// Slightly shift the top border of drawn dropdown away from node's top border by a fraction of
|
||||
// a pixel, to prevent it from poking through and disturbing node's siluette.
|
||||
'--extend-margin': `${0.2 / (graphNavigator?.scale ?? 1)}px`,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@ -61,7 +72,7 @@ export interface DropdownEntry {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="DropdownWidget" :style="{ '--dropdown-bg': color }">
|
||||
<div class="DropdownWidget" :style="styleVars">
|
||||
<ul class="list scrollable" @wheel.stop>
|
||||
<template v-for="entry in sortedValues" :key="entry.value">
|
||||
<li v-if="entry.selected">
|
||||
@ -88,12 +99,12 @@ export interface DropdownEntry {
|
||||
.DropdownWidget {
|
||||
position: relative;
|
||||
user-select: none;
|
||||
overflow: clip;
|
||||
min-width: 100%;
|
||||
z-index: 21;
|
||||
|
||||
/* When dropdown is displayed right below the last node's argument, the rounded corner needs to be
|
||||
covered. This is done by covering extra node-sized space at the top of the dropdown. */
|
||||
--dropdown-extend: calc(var(--node-height) - 1px);
|
||||
--dropdown-extend: calc(var(--node-height) - var(--extend-margin));
|
||||
margin-top: calc(0px - var(--dropdown-extend));
|
||||
padding-top: var(--dropdown-extend);
|
||||
background-color: var(--dropdown-bg);
|
||||
@ -112,21 +123,22 @@ export interface DropdownEntry {
|
||||
}
|
||||
.list {
|
||||
overflow: auto;
|
||||
width: min-content;
|
||||
border-radius: 0 0 16px 16px;
|
||||
min-width: 100%;
|
||||
min-height: 16px;
|
||||
max-height: 152px;
|
||||
list-style-type: none;
|
||||
color: var(--color-text-light);
|
||||
background: var(--dropdown-bg);
|
||||
scrollbar-width: thin;
|
||||
padding: 4px 0;
|
||||
border: 2px solid var(--dropdown-bg);
|
||||
padding: 6px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
li {
|
||||
text-align: left;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.item:not(.selected):hover {
|
||||
@ -135,8 +147,35 @@ li {
|
||||
|
||||
.list span {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
margin: 3px 0;
|
||||
text-wrap: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
li.item:hover {
|
||||
span {
|
||||
--text-scroll-max: calc(var(--dropdown-max-width) - 28px);
|
||||
will-change: transform;
|
||||
animation: 6s 1s infinite text-scroll;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes text-scroll {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
max-width: unset;
|
||||
transform: translateX(0);
|
||||
}
|
||||
50%,
|
||||
70% {
|
||||
max-width: unset;
|
||||
transform: translateX(calc(min(var(--text-scroll-max, 100%), 100%) - 100%));
|
||||
}
|
||||
}
|
||||
|
||||
.sort-background {
|
||||
@ -166,8 +205,6 @@ li {
|
||||
}
|
||||
|
||||
.item {
|
||||
margin-right: 4px;
|
||||
margin-left: 4px;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
border-radius: var(--radius-full);
|
||||
|
Loading…
Reference in New Issue
Block a user