Change grab cursors; fix some bugs (#10215)

Change grab cursors:
- Node has grab/grabbed icons for only halo and icon.

Fix bugs:
- Empty part of top bar no longer blocks mouse events.
- JSON viz: Clickable parts of inline elements now use pointer cursor when hovered.
- Doc panel breadcrumbs: Icon can be clicked (behavior now consistent with cursor shown).

https://github.com/enso-org/enso/assets/1047859/3e48a6c1-3f43-497f-82ad-eb787e9c9643

Closes #10166.

# Important Notes
- New global `clickable` class replaces `cursor: pointer`; the class can be applied closer to the event handler.
- Refactor: Extracted `GraphNode` output port logic to a new component.
This commit is contained in:
Kaz Wesley 2024-06-12 07:09:50 -07:00 committed by GitHub
parent 5339484285
commit 3e37faa34d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 240 additions and 220 deletions

View File

@ -52,7 +52,7 @@ test('Hover behaviour of edges', async ({ page }) => {
await expect(edgeElements).toHaveCount(EDGE_PARTS)
const targetEdge = edgeElements.first()
await expect(targetEdge).toHaveClass('edge io')
await expect(targetEdge).toHaveClass(/\bio\b/)
// Hover over edge to the left of node with binding `ten`.
await targetEdge.hover({

View File

@ -84,7 +84,6 @@ registerAutoBlurHandler()
font-family: var(--font-sans);
font-weight: 500;
font-size: 11.5px;
font-weight: 500;
line-height: 20px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;

View File

@ -113,3 +113,11 @@
height: 8px;
width: 8px;
}
.draggable {
cursor: grab;
}
.clickable {
cursor: pointer;
}

View File

@ -7,16 +7,15 @@ const emit = defineEmits<{ click: [] }>()
</script>
<template>
<div class="Breadcrumb">
<div class="Breadcrumb clickable" @click.stop="emit('click')">
<SvgIcon v-if="props.icon" :name="props.icon || ''" />
<span @click.stop="emit('click')" v-text="props.text"></span>
<span v-text="props.text"></span>
</div>
</template>
<style scoped>
.Breadcrumb {
user-select: none;
cursor: pointer;
display: flex;
align-items: center;
gap: 2px;

View File

@ -48,7 +48,11 @@ const annotations = computed<Array<string | undefined>>(() => {
<template>
<ul v-if="props.items.items.length > 0">
<li v-for="(item, index) in props.items.items" :key="index" :class="props.items.kind">
<a :class="['link', props.items.kind]" @click.stop="emit('linkClicked', item.id)">
<a
:class="props.items.kind"
class="link clickable"
@click.stop="emit('linkClicked', item.id)"
>
<span class="entryName">{{ qnSplit(item.name)[1] }}</span>
<span class="arguments">{{ ' ' + argumentsList(item.arguments) }}</span>
</a>
@ -61,7 +65,6 @@ const annotations = computed<Array<string | undefined>>(() => {
<style scoped>
.link {
cursor: pointer;
font-weight: 600;
&:hover {

View File

@ -571,7 +571,7 @@ const connected = computed(() => isConnected(props.edge))
<path
v-if="connected"
:d="basePath"
class="edge io"
class="edge io clickable"
:data-source-node-id="sourceNode"
:data-target-node-id="targetNode"
:data-testid="edgeIsBroken ? 'broken-edge' : null"
@ -628,7 +628,6 @@ const connected = computed(() => isConnected(props.edge))
stroke-width: 14;
stroke: transparent;
pointer-events: stroke;
cursor: pointer;
}
.edge.visible {
stroke-width: 4;

View File

@ -7,6 +7,7 @@ import GraphNodeMessage, {
iconForMessageType,
type MessageType,
} from '@/components/GraphEditor/GraphNodeMessage.vue'
import GraphNodeOutputPorts from '@/components/GraphEditor/GraphNodeOutputPorts.vue'
import GraphNodeSelection from '@/components/GraphEditor/GraphNodeSelection.vue'
import GraphVisualization from '@/components/GraphEditor/GraphVisualization.vue'
import type { NodeCreationOptions } from '@/components/GraphEditor/nodeCreation'
@ -16,7 +17,6 @@ import NodeWidgetTree, {
ICON_WIDTH,
} from '@/components/GraphEditor/NodeWidgetTree.vue'
import SvgIcon from '@/components/SvgIcon.vue'
import { useApproach } from '@/composables/animation'
import { useDoubleClick } from '@/composables/doubleClick'
import { usePointer, useResizeObserver } from '@/composables/events'
import { injectGraphNavigator } from '@/providers/graphNavigator'
@ -32,19 +32,8 @@ import type { Opt } from '@/util/data/opt'
import { Rect } from '@/util/data/rect'
import { Vec2 } from '@/util/data/vec2'
import { displayedIconOf } from '@/util/getIconName'
import { setIfUndefined } from 'lib0/map'
import type { ExternalId, VisualizationIdentifier } from 'shared/yjsModel'
import type { EffectScope } from 'vue'
import {
computed,
effectScope,
onScopeDispose,
onUnmounted,
ref,
shallowRef,
watch,
watchEffect,
} from 'vue'
import { computed, onUnmounted, ref, shallowRef, watch, watchEffect } from 'vue'
const MAXIMUM_CLICK_LENGTH_MS = 300
const MAXIMUM_CLICK_DISTANCE_SQ = 50
@ -91,12 +80,6 @@ const projectStore = useProjectStore()
const graph = useGraphStore()
const navigator = injectGraphNavigator(true)
const outputPortsSet = computed(() => {
const bindings = graph.db.nodeOutputPorts.lookup(nodeId.value)
if (bindings.size === 0) return new Set([nodeId.value])
return bindings
})
const nodeId = computed(() => asNodeId(props.node.rootExpr.id))
const potentialSelfArgumentId = computed(() => props.node.primarySubject)
const connectedSelfArgumentId = computed(() =>
@ -244,9 +227,11 @@ const isVisualizationFullscreen = computed(() => props.node.vis?.fullscreen ?? f
const bgStyleVariables = computed(() => {
const { x: width, y: height } = nodeSize.value
const visBelowNode = graphSelectionSize.value.y - nodeSize.value.y
return {
'--node-width': `${width}px`,
'--node-height': `${height}px`,
'--output-port-transform': `translateY(${visBelowNode}px)`,
}
})
@ -375,11 +360,6 @@ function getRelatedSpanOffset(domNode: globalThis.Node, domOffset: number): numb
return domOffset
}
const handlePortClick = useDoubleClick(
(event: PointerEvent, portId: AstId) => emit('outputPortClick', event, portId),
(event: PointerEvent, portId: AstId) => emit('outputPortDoubleClick', event, portId),
).handleClick
const handleNodeClick = useDoubleClick(
(e: MouseEvent) => {
if (!significantMove.value) {
@ -392,78 +372,6 @@ const handleNodeClick = useDoubleClick(
},
).handleClick
interface PortData {
clipRange: [number, number]
label: string | undefined
portId: AstId
}
const outputPorts = computed((): PortData[] => {
const ports = outputPortsSet.value
const numPorts = ports.size
return Array.from(ports, (portId, index): PortData => {
return {
clipRange: [index / numPorts, (index + 1) / numPorts],
label: numPorts > 1 ? graph.db.getOutputPortIdentifier(portId) : undefined,
portId,
}
})
})
const outputHovered = ref<AstId>()
const anyPortDisconnected = computed(() => {
for (const port of outputPortsSet.value) {
if (graph.disconnectedEdgePorts.has(port)) return true
}
return false
})
const portsVisible = computed(
() =>
selectionVisible.value ||
(outputHovered.value && outputPortsSet.value.has(outputHovered.value)) ||
anyPortDisconnected.value,
)
const portsHoverAnimation = useApproach(() => (portsVisible.value ? 1 : 0), 50, 0.01)
watchEffect(() => emit('update:hoverAnim', portsHoverAnimation.value))
const hoverAnimations = new Map<AstId, [ReturnType<typeof useApproach>, EffectScope]>()
watchEffect(() => {
const ports = outputPortsSet.value
for (const key of hoverAnimations.keys())
if (!ports.has(key)) {
hoverAnimations.get(key)?.[1].stop()
hoverAnimations.delete(key)
}
for (const port of outputPortsSet.value) {
setIfUndefined(hoverAnimations, port, () => {
// Because `useApproach` uses `onScopeDispose` and we are calling it dynamically (i.e. not at
// the setup top-level), we need to create a detached scope for each invocation.
const scope = effectScope(true)
const approach = scope.run(() =>
useApproach(() => (outputHovered.value === port ? 1 : 0), 50, 0.01),
)!
return [approach, scope]
})
}
})
// Clean up dynamically created detached scopes.
onScopeDispose(() => hoverAnimations.forEach(([_, scope]) => scope.stop()))
function portGroupStyle(port: PortData) {
const [start, end] = port.clipRange
const visBelowNode = graphSelectionSize.value.y - nodeSize.value.y
return {
'--hover-animation': portsHoverAnimation.value,
'--direct-hover-animation': hoverAnimations.get(port.portId)?.[0].value ?? 0,
'--port-clip-start': start,
'--port-clip-end': end,
transform: `translateY(${visBelowNode}px`,
}
}
const visRect = shallowRef<Rect>()
function updateVisualizationRect(rect: Rect | undefined) {
visRect.value = rect
@ -515,9 +423,9 @@ watchEffect(() => {
<Teleport :to="graphNodeSelections">
<GraphNodeSelection
v-if="navigator && !edited"
:class="{ dragged: isDragged }"
:nodePosition="props.node.position"
:nodeSize="graphSelectionSize"
:class="{ draggable: true, dragged: isDragged }"
:selected
:nodeId
:color
@ -533,7 +441,7 @@ watchEffect(() => {
<div class="binding" v-text="node.pattern?.code()" />
<button
v-if="!menuVisible && isRecordingOverridden"
class="overrideRecordButton"
class="overrideRecordButton clickable"
data-testid="recordingOverriddenButton"
@click="isRecordingOverridden = false"
>
@ -617,20 +525,13 @@ watchEffect(() => {
/>
<svg class="bgPaths" :style="bgStyleVariables">
<rect class="bgFill" />
<template v-for="port of outputPorts" :key="port.portId">
<g :style="portGroupStyle(port)">
<g class="portClip">
<rect
class="outputPortHoverArea"
@pointerenter="outputHovered = port.portId"
@pointerleave="outputHovered = undefined"
@pointerdown.stop.prevent="handlePortClick($event, port.portId)"
/>
<rect class="outputPort" />
</g>
<text class="outputPortLabel">{{ port.label }}</text>
</g>
</template>
<GraphNodeOutputPorts
:nodeId="nodeId"
:forceVisible="selectionVisible"
@portClick="(...args) => emit('outputPortClick', ...args)"
@portDoubleClick="(...args) => emit('outputPortDoubleClick', ...args)"
@update:hoverAnim="emit('update:hoverAnim', $event)"
/>
</svg>
</div>
</template>
@ -651,68 +552,6 @@ watchEffect(() => {
--output-port-hover-width: 20px;
}
.outputPort,
.outputPortHoverArea {
x: calc(0px - var(--output-port-width) / 2);
y: calc(0px - var(--output-port-width) / 2);
height: calc(var(--node-height) + var(--output-port-width));
width: calc(var(--node-width) + var(--output-port-width));
rx: calc(var(--node-border-radius) + var(--output-port-width) / 2);
fill: none;
stroke: var(--node-color-port);
stroke-width: calc(var(--output-port-width) + var(--output-port-overlap-anim));
transition: stroke 0.2s ease;
--horizontal-line: calc(var(--node-width) - var(--node-border-radius) * 2);
--vertical-line: calc(var(--node-height) - var(--node-border-radius) * 2);
--radius-arclength: calc(
(var(--node-border-radius) + var(--output-port-width) * 0.5) * 2 * 3.141592653589793
);
stroke-dasharray: calc(var(--horizontal-line) + var(--radius-arclength) * 0.5) 10000%;
stroke-dashoffset: calc(
0px - var(--horizontal-line) - var(--vertical-line) - var(--radius-arclength) * 0.25
);
stroke-linecap: round;
}
.outputPort {
--output-port-overlap-anim: calc(var(--hover-animation) * var(--output-port-overlap));
--output-port-width: calc(
var(--output-port-max-width) * var(--hover-animation) + var(--output-port-hovered-extra-width) *
var(--direct-hover-animation) - var(--output-port-overlap-anim)
);
pointer-events: none;
}
.outputPortHoverArea {
--output-port-width: var(--output-port-hover-width);
stroke-width: var(--output-port-hover-width);
stroke: transparent;
/* Make stroke visible to debug the active area: */
/* stroke: red; */
stroke-linecap: butt;
pointer-events: stroke;
cursor: pointer;
}
.portClip {
clip-path: inset(
0 calc((1 - var(--port-clip-end)) * (100% + 1px) - 0.5px) 0
calc(var(--port-clip-start) * (100% + 1px) + 0.5px)
);
}
.outputPortLabel {
user-select: none;
pointer-events: none;
z-index: 10;
text-anchor: middle;
opacity: calc(var(--hover-animation) * var(--hover-animation));
fill: var(--node-color-primary);
transform: translate(50%, calc(var(--node-height) + var(--output-port-max-width) + 16px));
}
.bgFill {
width: var(--node-width);
height: var(--node-height);
@ -828,7 +667,6 @@ watchEffect(() => {
.overrideRecordButton {
position: absolute;
cursor: pointer;
display: flex;
align-items: center;
backdrop-filter: var(--blur-app-bg);
@ -841,11 +679,7 @@ watchEffect(() => {
margin-right: 4px;
}
.draggable {
cursor: grab;
}
.dragged {
cursor: grabbing;
cursor: grabbing !important;
}
</style>

View File

@ -0,0 +1,185 @@
<script setup lang="ts">
import { useApproach } from '@/composables/animation'
import { useDoubleClick } from '@/composables/doubleClick'
import { useGraphStore, type NodeId } from '@/stores/graph'
import { setIfUndefined } from 'lib0/map'
import type { AstId } from 'shared/ast'
import { computed, effectScope, onScopeDispose, ref, watchEffect, type EffectScope } from 'vue'
const props = defineProps<{ nodeId: NodeId; forceVisible: boolean }>()
const emit = defineEmits<{
portClick: [event: PointerEvent, portId: AstId]
portDoubleClick: [event: PointerEvent, portId: AstId]
'update:hoverAnim': [progress: number]
}>()
const graph = useGraphStore()
// === Ports ===
interface PortData {
clipRange: [number, number]
label: string | undefined
portId: AstId
}
const outputPortsSet = computed(() => {
const bindings = graph.db.nodeOutputPorts.lookup(props.nodeId)
if (bindings.size === 0) return new Set([props.nodeId])
return bindings
})
const outputPorts = computed((): PortData[] => {
const ports = outputPortsSet.value
const numPorts = ports.size
return Array.from(ports, (portId, index): PortData => {
return {
clipRange: [index / numPorts, (index + 1) / numPorts],
label: numPorts > 1 ? graph.db.getOutputPortIdentifier(portId) : undefined,
portId,
}
})
})
// === Interactivity ===
const outputHovered = ref<AstId>()
const anyPortDisconnected = computed(() => {
for (const port of outputPortsSet.value) {
if (graph.disconnectedEdgePorts.has(port)) return true
}
return false
})
const handlePortClick = useDoubleClick(
(event: PointerEvent, portId: AstId) => emit('portClick', event, portId),
(event: PointerEvent, portId: AstId) => emit('portDoubleClick', event, portId),
).handleClick
// === Rendering ===
const portsVisible = computed(
() =>
props.forceVisible ||
(outputHovered.value && outputPortsSet.value.has(outputHovered.value)) ||
anyPortDisconnected.value,
)
const portsHoverAnimation = useApproach(() => (portsVisible.value ? 1 : 0), 50, 0.01)
watchEffect(() => emit('update:hoverAnim', portsHoverAnimation.value))
const hoverAnimations = new Map<AstId, [ReturnType<typeof useApproach>, EffectScope]>()
watchEffect(() => {
const ports = outputPortsSet.value
for (const key of hoverAnimations.keys())
if (!ports.has(key)) {
hoverAnimations.get(key)?.[1].stop()
hoverAnimations.delete(key)
}
for (const port of outputPortsSet.value) {
setIfUndefined(hoverAnimations, port, () => {
// Because `useApproach` uses `onScopeDispose` and we are calling it dynamically (i.e. not at
// the setup top-level), we need to create a detached scope for each invocation.
const scope = effectScope(true)
const approach = scope.run(() =>
useApproach(() => (outputHovered.value === port ? 1 : 0), 50, 0.01),
)!
return [approach, scope]
})
}
})
// Clean up dynamically created detached scopes.
onScopeDispose(() => hoverAnimations.forEach(([_, scope]) => scope.stop()))
function portGroupStyle(port: PortData) {
const [start, end] = port.clipRange
return {
'--hover-animation': portsHoverAnimation.value,
'--direct-hover-animation': hoverAnimations.get(port.portId)?.[0].value ?? 0,
'--port-clip-start': start,
'--port-clip-end': end,
transform: 'var(--output-port-transform)',
}
}
</script>
<template>
<template v-for="port of outputPorts" :key="port.portId">
<g :style="portGroupStyle(port)">
<g class="portClip">
<rect
class="outputPortHoverArea clickable"
@pointerenter="outputHovered = port.portId"
@pointerleave="outputHovered = undefined"
@pointerdown.stop.prevent="handlePortClick($event, port.portId)"
/>
<rect class="outputPort" />
</g>
<text class="outputPortLabel">{{ port.label }}</text>
</g>
</template>
</template>
<style scoped>
.outputPort,
.outputPortHoverArea {
x: calc(0px - var(--output-port-width) / 2);
y: calc(0px - var(--output-port-width) / 2);
height: calc(var(--node-height) + var(--output-port-width));
width: calc(var(--node-width) + var(--output-port-width));
rx: calc(var(--node-border-radius) + var(--output-port-width) / 2);
fill: none;
stroke: var(--node-color-port);
stroke-width: calc(var(--output-port-width) + var(--output-port-overlap-anim));
transition: stroke 0.2s ease;
--horizontal-line: calc(var(--node-width) - var(--node-border-radius) * 2);
--vertical-line: calc(var(--node-height) - var(--node-border-radius) * 2);
--radius-arclength: calc((var(--node-border-radius) + var(--output-port-width) * 0.5) * 2 * pi);
stroke-dasharray: calc(var(--horizontal-line) + var(--radius-arclength) * 0.5) 10000%;
stroke-dashoffset: calc(
0px - var(--horizontal-line) - var(--vertical-line) - var(--radius-arclength) * 0.25
);
stroke-linecap: round;
}
.outputPort {
--output-port-overlap-anim: calc(var(--hover-animation) * var(--output-port-overlap));
--output-port-width: calc(
var(--output-port-max-width) * var(--hover-animation) + var(--output-port-hovered-extra-width) *
var(--direct-hover-animation) - var(--output-port-overlap-anim)
);
pointer-events: none;
}
.outputPortHoverArea {
--output-port-width: var(--output-port-hover-width);
stroke-width: var(--output-port-hover-width);
stroke: transparent;
/* Make stroke visible to debug the active area: */
/* stroke: red; */
stroke-linecap: butt;
pointer-events: stroke;
}
.portClip {
clip-path: inset(
0 calc((1 - var(--port-clip-end)) * (100% + 1px) - 0.5px) 0
calc(var(--port-clip-start) * (100% + 1px) + 0.5px)
);
}
.outputPortLabel {
user-select: none;
pointer-events: none;
z-index: 10;
text-anchor: middle;
opacity: calc(var(--hover-animation) * var(--hover-animation));
fill: var(--node-color-primary);
transform: translate(50%, calc(var(--node-height) + var(--output-port-max-width) + 16px));
}
</style>

View File

@ -108,7 +108,7 @@ export const ICON_WIDTH = 16
<!-- Display an icon for the node if no widget in the tree provides one. -->
<SvgIcon
v-if="!props.connectedSelfArgumentId"
class="icon grab-handle nodeCategoryIcon"
class="icon grab-handle nodeCategoryIcon draggable"
: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')"

View File

@ -388,7 +388,7 @@ declare module '@/providers/widgetRegistry' {
<template>
<div
ref="widgetRoot"
class="WidgetSelection"
class="WidgetSelection clickable"
:class="{ multiSelect: isMulti }"
@click.stop="toggleDropdownWidget"
@pointerover="isHovered = true"
@ -424,7 +424,6 @@ declare module '@/providers/widgetRegistry' {
align-items: center;
position: relative;
min-height: var(--node-port-height);
cursor: pointer;
}
.arrow {

View File

@ -26,7 +26,7 @@ export const widgetDefinition = defineWidget(
<template>
<SvgIcon
class="WidgetSelfIcon icon nodeCategoryIcon r-24"
class="WidgetSelfIcon icon nodeCategoryIcon draggable r-24"
:name="icon"
@click.right.stop.prevent="tree.emitOpenFullMenu()"
/>

View File

@ -23,7 +23,7 @@ function onClick() {
<TooltipTrigger>
<template #default="triggerProps">
<button
class="MenuButton"
class="MenuButton clickable"
:aria-label="props.title ?? ''"
:class="{ toggledOn, toggledOff: toggledOn === false, disabled }"
:disabled="disabled ?? false"
@ -48,7 +48,6 @@ function onClick() {
padding: 4px;
border-radius: var(--radius-full);
border: none;
cursor: pointer;
transition: background-color 0.3s;
&:hover {
background-color: var(--color-menu-entry-hover-bg);

View File

@ -17,7 +17,6 @@ span {
.NavBreadcrumb {
user-select: none;
cursor: pointer;
border-radius: var(--radius-full);
> .blur-container {

View File

@ -24,6 +24,7 @@ const emit = defineEmits<{ selected: [index: number] }>()
:text="breadcrumb.label"
:active="breadcrumb.active"
:title="index === 0 ? 'Project Name' : ''"
class="clickable"
@click.stop="emit('selected', index)"
/>
</template>

View File

@ -42,7 +42,6 @@ const emit = defineEmits<{ recordOnce: []; 'update:recordMode': [enabled: boolea
backdrop-filter: var(--blur-app-bg);
padding: 4px 4px;
width: 42px;
cursor: pointer;
}
.left-end {

View File

@ -87,6 +87,10 @@ const barStyle = computed(() => {
/* FIXME[sb]: Get correct offset from dashboard. */
left: 9px;
width: 100%;
pointer-events: none;
> * {
pointer-events: auto;
}
}
.selection-menu-enter-active,

View File

@ -47,6 +47,7 @@ onMounted(() => setTimeout(() => rootNode.value?.querySelector('button')?.focus(
v-for="type_ in props.types"
:key="visIdKey(type_)"
:class="{ selected: visIdentifierEquals(props.modelValue, type_) }"
class="clickable"
@click.stop="emit('update:modelValue', type_)"
>
<button>
@ -97,7 +98,6 @@ button {
display: flex;
gap: 4px;
align-items: center;
cursor: pointer;
padding: 0 8px;
border-radius: 12px;
white-space: nowrap;

View File

@ -25,7 +25,7 @@ function entryTitle(index: number) {
v-for="(child, index) in props.data"
:key="index"
:title="entryTitle(index)"
class="element"
class="element clickable"
@click.stop="emit('createProjection', [$event.shiftKey ? [...props.data.keys()] : [index]])"
>
<JsonValueWidget
@ -50,7 +50,6 @@ function entryTitle(index: number) {
.block > .element {
display: block;
margin-left: 1em;
cursor: pointer;
}
.element:not(:last-child)::after {
display: inline;

View File

@ -29,7 +29,7 @@ function entryTitle(key: string) {
v-for="[key, value] in Object.entries(props.data)"
:key="key"
:title="entryTitle(key)"
class="field"
class="field clickable"
@click.stop="emit('createProjection', [$event.shiftKey ? Object.keys(props.data) : [key]])"
>
<span class="key" v-text="JSON.stringify(key)" />:
@ -63,7 +63,6 @@ function entryTitle(key: string) {
.block > .field {
display: block;
margin-left: 1em;
cursor: pointer;
}
.field:not(:last-child)::after {
display: inline;

View File

@ -4,14 +4,13 @@ const emit = defineEmits<{ 'update:modelValue': [modelValue: boolean] }>()
</script>
<template>
<div class="Checkbox r-24" @click.stop="emit('update:modelValue', !props.modelValue)">
<div class="Checkbox r-24 clickable" @click.stop="emit('update:modelValue', !props.modelValue)">
<div :class="{ hidden: !props.modelValue }"></div>
</div>
</template>
<style scoped>
.Checkbox {
cursor: pointer;
width: 24px;
height: 24px;
padding: 6px;

View File

@ -74,21 +74,21 @@ export interface DropdownEntry {
<template>
<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">
<div class="item selected" @click.stop="emit('clickEntry', entry, $event.altKey)">
<span v-text="entry.value"></span>
</div>
</li>
<li v-else class="item" @click.stop="emit('clickEntry', entry, $event.altKey)">
<span v-text="entry.value"></span>
</li>
</template>
<li
v-for="entry in sortedValues"
:key="entry.value"
:class="{ selected: entry.selected }"
class="item clickable"
@click.stop="emit('clickEntry', entry, $event.altKey)"
>
<div class="itemContent" v-text="entry.value"></div>
</li>
</ul>
<div v-if="enableSortButton" class="sort">
<div class="sort-background"></div>
<SvgIcon
:name="ICON_LOOKUP[sortDirection]"
class="clickable"
@click="sortDirection = NEXT_SORT_DIRECTION[sortDirection]"
/>
</div>
@ -145,7 +145,7 @@ li {
color: white;
}
.list span {
.list .itemContent {
display: inline-block;
max-width: 100%;
white-space: nowrap;
@ -216,9 +216,4 @@ li.item:hover {
background-color: var(--color-port-connected);
}
}
.item,
.sort {
cursor: pointer;
}
</style>