mirror of
https://github.com/aleksey-hoffman/sigma-file-manager.git
synced 2026-10-19 01:31:05 -05:00
Refine: Fix: sidebar drives not opening on middle click
This commit is contained in:
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,
|
||||
|
||||
@@ -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:/');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user