feat(editor): Show minimap only while panning, zooming or while minimap is hovered (no-changelog) (#10677)
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Benchmark Docker Image CI / build (push) Waiting to run

This commit is contained in:
Alex Grozav 2024-09-05 15:41:33 +03:00 committed by GitHub
parent c5bc8e6eb9
commit 3ea114129b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 152 additions and 13 deletions

View file

@ -28,6 +28,7 @@ beforeEach(() => {
afterEach(() => { afterEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
vi.useRealTimers();
}); });
describe('Canvas', () => { describe('Canvas', () => {
@ -36,8 +37,8 @@ describe('Canvas', () => {
expect(getByTestId('canvas')).toBeVisible(); expect(getByTestId('canvas')).toBeVisible();
expect(getByTestId('canvas-background')).toBeVisible(); expect(getByTestId('canvas-background')).toBeVisible();
expect(getByTestId('canvas-minimap')).toBeVisible();
expect(getByTestId('canvas-controls')).toBeVisible(); expect(getByTestId('canvas-controls')).toBeVisible();
expect(getByTestId('canvas-minimap')).toBeInTheDocument();
}); });
it('should render nodes and edges', async () => { it('should render nodes and edges', async () => {
@ -142,4 +143,79 @@ describe('Canvas', () => {
], ],
]); ]);
}); });
describe('minimap', () => {
const minimapVisibilityDelay = 1000;
const minimapTransitionDuration = 300;
it('should show minimap for 1sec after panning', async () => {
vi.useFakeTimers();
const nodes = [createCanvasNodeElement()];
const { getByTestId, container } = renderComponent({
props: {
nodes,
},
});
await waitFor(() => expect(container.querySelectorAll('.vue-flow__node')).toHaveLength(1));
const canvas = getByTestId('canvas');
const pane = canvas.querySelector('.vue-flow__pane');
if (!pane) throw new Error('VueFlow pane not found');
await fireEvent.keyDown(pane, { view: window, key: 'Shift' });
await fireEvent.mouseDown(pane, { view: window });
await fireEvent.mouseMove(pane, {
view: window,
clientX: 100,
clientY: 100,
});
await fireEvent.mouseUp(pane, { view: window });
await fireEvent.keyUp(pane, { view: window, key: 'Shift' });
vi.advanceTimersByTime(minimapTransitionDuration);
await waitFor(() => expect(getByTestId('canvas-minimap')).toBeVisible());
vi.advanceTimersByTime(minimapVisibilityDelay + minimapTransitionDuration);
await waitFor(() => expect(getByTestId('canvas-minimap')).not.toBeVisible());
});
it('should keep minimap visible when hovered', async () => {
vi.useFakeTimers();
const nodes = [createCanvasNodeElement()];
const { getByTestId, container } = renderComponent({
props: {
nodes,
},
});
await waitFor(() => expect(container.querySelectorAll('.vue-flow__node')).toHaveLength(1));
const canvas = getByTestId('canvas');
const pane = canvas.querySelector('.vue-flow__pane');
if (!pane) throw new Error('VueFlow pane not found');
await fireEvent.keyDown(pane, { view: window, key: 'Shift' });
await fireEvent.mouseDown(pane, { view: window });
await fireEvent.mouseMove(pane, {
view: window,
clientX: 100,
clientY: 100,
});
await fireEvent.mouseUp(pane, { view: window });
await fireEvent.keyUp(pane, { view: window, key: 'Shift' });
vi.advanceTimersByTime(minimapTransitionDuration);
await waitFor(() => expect(getByTestId('canvas-minimap')).toBeVisible());
await fireEvent.mouseEnter(getByTestId('canvas-minimap'));
vi.advanceTimersByTime(minimapVisibilityDelay + minimapTransitionDuration);
await waitFor(() => expect(getByTestId('canvas-minimap')).toBeVisible());
await fireEvent.mouseLeave(getByTestId('canvas-minimap'));
vi.advanceTimersByTime(minimapVisibilityDelay + minimapTransitionDuration);
await waitFor(() => expect(getByTestId('canvas-minimap')).not.toBeVisible());
});
});
}); });

View file

@ -309,6 +309,7 @@ function emitWithLastSelectedNode(emitFn: (id: string) => void) {
const defaultZoom = 1; const defaultZoom = 1;
const zoom = ref(defaultZoom); const zoom = ref(defaultZoom);
const isPaneMoving = ref(false);
function getProjectedPosition(event?: MouseEvent) { function getProjectedPosition(event?: MouseEvent) {
const bounds = viewportRef.value?.getBoundingClientRect() ?? { left: 0, top: 0 }; const bounds = viewportRef.value?.getBoundingClientRect() ?? { left: 0, top: 0 };
@ -354,6 +355,14 @@ function setReadonly(value: boolean) {
elementsSelectable.value = true; elementsSelectable.value = true;
} }
function onPaneMoveStart() {
isPaneMoving.value = true;
}
function onPaneMoveEnd() {
isPaneMoving.value = false;
}
/** /**
* Context menu * Context menu
*/ */
@ -414,10 +423,45 @@ function onContextMenuAction(action: ContextMenuAction, nodeIds: string[]) {
* Minimap * Minimap
*/ */
const minimapVisibilityDelay = 1000;
const minimapHideTimeout = ref<NodeJS.Timeout | null>(null);
const isMinimapVisible = ref(false);
function minimapNodeClassnameFn(node: CanvasNode) { function minimapNodeClassnameFn(node: CanvasNode) {
return `minimap-node-${node.data?.render.type.replace(/\./g, '-') ?? 'default'}`; return `minimap-node-${node.data?.render.type.replace(/\./g, '-') ?? 'default'}`;
} }
watch(isPaneMoving, (value) => {
if (value) {
showMinimap();
} else {
hideMinimap();
}
});
function showMinimap() {
if (minimapHideTimeout.value) {
clearTimeout(minimapHideTimeout.value);
minimapHideTimeout.value = null;
}
isMinimapVisible.value = true;
}
function hideMinimap() {
minimapHideTimeout.value = setTimeout(() => {
isMinimapVisible.value = false;
}, minimapVisibilityDelay);
}
function onMinimapMouseEnter() {
showMinimap();
}
function onMinimapMouseLeave() {
hideMinimap();
}
/** /**
* Lifecycle * Lifecycle
*/ */
@ -474,6 +518,8 @@ provide(CanvasKey, {
@contextmenu="onOpenContextMenu" @contextmenu="onOpenContextMenu"
@viewport-change="onViewportChange" @viewport-change="onViewportChange"
@nodes-change="onNodesChange" @nodes-change="onNodesChange"
@move-start="onPaneMoveStart"
@move-end="onPaneMoveEnd"
> >
<template #node-canvas-node="canvasNodeProps"> <template #node-canvas-node="canvasNodeProps">
<Node <Node
@ -508,18 +554,23 @@ provide(CanvasKey, {
<Background data-test-id="canvas-background" pattern-color="#aaa" :gap="GRID_SIZE" /> <Background data-test-id="canvas-background" pattern-color="#aaa" :gap="GRID_SIZE" />
<MiniMap <Transition name="minimap">
data-test-id="canvas-minimap" <MiniMap
aria-label="n8n Minimap" v-show="isMinimapVisible"
:height="120" data-test-id="canvas-minimap"
:width="200" aria-label="n8n Minimap"
:position="PanelPosition.BottomLeft" :height="120"
pannable :width="200"
zoomable :position="PanelPosition.BottomLeft"
:node-class-name="minimapNodeClassnameFn" pannable
mask-color="var(--color-background-base)" zoomable
:node-border-radius="16" :node-class-name="minimapNodeClassnameFn"
/> mask-color="var(--color-background-base)"
:node-border-radius="16"
@mouseenter="onMinimapMouseEnter"
@mouseleave="onMinimapMouseLeave"
/>
</Transition>
<CanvasControlButtons <CanvasControlButtons
data-test-id="canvas-controls" data-test-id="canvas-controls"
@ -556,3 +607,15 @@ provide(CanvasKey, {
} }
} }
</style> </style>
<style lang="scss" scoped>
.minimap-enter-active,
.minimap-leave-active {
transition: opacity 0.3s ease;
}
.minimap-enter-from,
.minimap-leave-to {
opacity: 0;
}
</style>