Refine: Fix: sidebar drives not opening on middle click

This commit is contained in:
aleksey-hoffman committed 2026-08-24 15:19:34 +03:00
1 parent b116ef21aa
commit 662a6988b3
6 files changed
+143 -15

No files matched your search

@@ -0,0 +1,75 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// License: GNU GPLv3 or later. See the license file in the project root for more information.
// Copyright © 2021 - present Aleksey Hoffman. All rights reserved.
import { describe, expect, it } from 'vitest';
import { getScrollLeftToRevealInFadedContainer } from '@/composables/get-scroll-left-to-reveal-in-faded-container';
import { DEFAULT_HORIZONTAL_SCROLL_FADE_WIDTH } from '@/composables/use-horizontal-scroll-fade';
const FADE_WIDTH = DEFAULT_HORIZONTAL_SCROLL_FADE_WIDTH;
const CONTAINER_CLIENT_WIDTH = 200;
const CONTAINER_SCROLL_WIDTH = 800;
const MAX_SCROLL_LEFT = CONTAINER_SCROLL_WIDTH - CONTAINER_CLIENT_WIDTH;
function reveal(overrides: Partial<Parameters<typeof getScrollLeftToRevealInFadedContainer>[0]>) {
return getScrollLeftToRevealInFadedContainer({
containerScrollLeft: 200,
containerClientWidth: CONTAINER_CLIENT_WIDTH,
containerScrollWidth: CONTAINER_SCROLL_WIDTH,
elementOffsetLeft: 250,
elementWidth: 80,
fadeWidth: FADE_WIDTH,
...overrides,
});
}
describe('getScrollLeftToRevealInFadedContainer', () => {
it('keeps the current scroll when the element is already fully outside both fades', () => {
expect(reveal({
containerScrollLeft: 200,
elementOffsetLeft: 250,
elementWidth: 80,
})).toBe(200);
});
it('scrolls left so a tab under the left fade clears the fade', () => {
expect(reveal({
containerScrollLeft: 200,
elementOffsetLeft: 210,
elementWidth: 80,
})).toBe(186);
});
it('scrolls right so a tab under the right fade clears the fade', () => {
expect(reveal({
containerScrollLeft: 200,
elementOffsetLeft: 320,
elementWidth: 80,
})).toBe(224);
});
it('scrolls to the start so the first tab is not covered by a left fade', () => {
expect(reveal({
containerScrollLeft: 200,
elementOffsetLeft: 0,
elementWidth: 80,
})).toBe(0);
});
it('scrolls to the end so the last tab is not covered by a right fade', () => {
expect(reveal({
containerScrollLeft: 200,
elementOffsetLeft: 720,
elementWidth: 80,
})).toBe(MAX_SCROLL_LEFT);
});
it('does not scroll past the start or end', () => {
expect(reveal({
containerScrollLeft: 0,
containerScrollWidth: CONTAINER_CLIENT_WIDTH,
elementOffsetLeft: 0,
elementWidth: 80,
})).toBe(0);
});
});
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// License: GNU GPLv3 or later. See the license file in the project root for more information.
// Copyright © 2021 - present Aleksey Hoffman. All rights reserved.
export function getScrollLeftToRevealInFadedContainer(params: {
containerScrollLeft: number;
containerClientWidth: number;
containerScrollWidth: number;
elementOffsetLeft: number;
elementWidth: number;
fadeWidth: number;
}): number {
const maxScrollLeft = Math.max(0, params.containerScrollWidth - params.containerClientWidth);
const fadeWidth = Math.max(0, params.fadeWidth);
const elementRight = params.elementOffsetLeft + params.elementWidth;
const hasLeftFade = params.containerScrollLeft > 0;
const hasRightFade = params.containerScrollLeft < maxScrollLeft;
const unfadedLeft = params.containerScrollLeft + (hasLeftFade ? fadeWidth : 0);
const unfadedRight = params.containerScrollLeft + params.containerClientWidth
- (hasRightFade ? fadeWidth : 0);
if (params.elementOffsetLeft >= unfadedLeft && elementRight <= unfadedRight) {
return params.containerScrollLeft;
}
if (params.elementOffsetLeft < unfadedLeft) {
return clampScrollLeft(params.elementOffsetLeft - fadeWidth, maxScrollLeft);
}
return clampScrollLeft(
elementRight + fadeWidth - params.containerClientWidth,
maxScrollLeft,
);
}
function clampScrollLeft(scrollLeft: number, maxScrollLeft: number): number {
return Math.min(maxScrollLeft, Math.max(0, scrollLeft));
}
@@ -14,7 +14,7 @@ interface HorizontalScrollFadeOptions {
const DEFAULT_ANIMATION_DURATION_MS = 150;
const DEFAULT_EDGE_THRESHOLD = 1;
const DEFAULT_FADE_WIDTH = 24;
export const DEFAULT_HORIZONTAL_SCROLL_FADE_WIDTH = 24;
const HORIZONTAL_SCROLL_FADE_CLASS = 'horizontal-scroll-fade';
export function useHorizontalScrollFade(
@@ -27,7 +27,7 @@ export function useHorizontalScrollFade(
const rightFadeWidth = ref(0);
const animationDurationMs = options.animationDurationMs ?? DEFAULT_ANIMATION_DURATION_MS;
const edgeThreshold = options.edgeThreshold ?? DEFAULT_EDGE_THRESHOLD;
const fadeWidth = options.fadeWidth ?? DEFAULT_FADE_WIDTH;
const fadeWidth = options.fadeWidth ?? DEFAULT_HORIZONTAL_SCROLL_FADE_WIDTH;
let animationFrameId: number | null = null;
let animationStartTime: number | null = null;
let startLeftFadeWidth = 0;
@@ -134,6 +134,7 @@ export function useHorizontalScrollFade(
return {
canScrollLeft,
canScrollRight,
fadeWidth,
scrollFadeClass: HORIZONTAL_SCROLL_FADE_CLASS,
scrollFadeStyle,
updateScrollFade,
+22 -8
View File
@@ -19,6 +19,7 @@ import { ContextMenuShortcut } from '@/components/ui/context-menu';
import { useWorkspacesStore } from '@/stores/storage/workspaces';
import { useShortcutsStore } from '@/stores/runtime/shortcuts';
import { useHorizontalScrollFade } from '@/composables/use-horizontal-scroll-fade';
import { getScrollLeftToRevealInFadedContainer } from '@/composables/get-scroll-left-to-reveal-in-faded-container';
import type { Tab as TabType, TabGroup } from '@/types/workspaces';
import { PlusIcon } from '@lucide/vue';
@@ -44,7 +45,7 @@ const previewEnabled = ref(true);
const scrollContainerRef = ref<HTMLElement | null>(null);
const scrollContentRef = ref<HTMLElement | null>(null);
let scrollDisableTimeoutId: number | null = null;
const { scrollFadeClass, scrollFadeStyle, updateScrollFade } = useHorizontalScrollFade(scrollContainerRef, {
const { fadeWidth, scrollFadeClass, scrollFadeStyle, updateScrollFade } = useHorizontalScrollFade(scrollContainerRef, {
scrollContentRef,
});
@@ -110,16 +111,29 @@ function scrollTabBarToEnd() {
function scrollSelectedTabGroupIntoView() {
nextTick(() => {
const selectedTab = document.querySelector('.tab[is-active="true"]');
const container = scrollContainerRef.value;
const selectedTab = container?.querySelector('.tab[is-active="true"]');
if (selectedTab) {
selectedTab.scrollIntoView({
block: 'nearest',
inline: 'nearest',
behavior: 'instant',
});
if (!container || !(selectedTab instanceof HTMLElement)) {
return;
}
const elementOffsetLeft = selectedTab.getBoundingClientRect().left
- container.getBoundingClientRect().left
+ container.scrollLeft;
container.scrollTo({
left: getScrollLeftToRevealInFadedContainer({
containerScrollLeft: container.scrollLeft,
containerClientWidth: container.clientWidth,
containerScrollWidth: container.scrollWidth,
elementOffsetLeft,
elementWidth: selectedTab.offsetWidth,
fadeWidth,
}),
behavior: 'instant',
});
updateScrollFade();
});
}
@@ -159,7 +159,7 @@ describe('openNavigatorPathInNewTab', () => {
openPathInCurrentTabMock.mockReset().mockResolvedValue(undefined);
});
it('opens a background tab group and navigates to navigator', async () => {
it('opens and activates a tab group and navigates to navigator', async () => {
const router = createRouter('home');
openNavigatorPathInNewTab(router, 'D:/');
@@ -167,13 +167,13 @@ describe('openNavigatorPathInNewTab', () => {
expect(router.push).toHaveBeenCalledWith({ name: 'navigator' });
await vi.waitFor(() => {
expect(openNewTabGroupMock).toHaveBeenCalledWith('D:/', { activate: false });
expect(openNewTabGroupMock).toHaveBeenCalledWith('D:/');
});
expect(openPathInCurrentTabMock).not.toHaveBeenCalled();
});
it('opens a background tab group without navigating when already on navigator', async () => {
it('opens and activates a tab group without navigating when already on navigator', async () => {
const router = createRouter('navigator');
openNavigatorPathInNewTab(router, 'D:/');
@@ -181,7 +181,7 @@ describe('openNavigatorPathInNewTab', () => {
expect(router.push).not.toHaveBeenCalled();
await vi.waitFor(() => {
expect(openNewTabGroupMock).toHaveBeenCalledWith('D:/', { activate: false });
expect(openNewTabGroupMock).toHaveBeenCalledWith('D:/');
});
});
});
+1 -1
View File
@@ -89,7 +89,7 @@ export function openNavigatorPathInNewTab(router: Router, path: string): void {
ensureNavigatorRoute(router);
runNavigatorOpenTask(async () => {
await useWorkspacesStore().openNewTabGroup(path, { activate: false });
await useWorkspacesStore().openNewTabGroup(path);
});
}