mirror of
https://github.com/immich-app/immich.git
synced 2025-06-03 05:34:32 -04:00
fix(web): increase sidebar breakpoint (#17436)
This commit is contained in:
parent
6d3f3d8616
commit
e3995fb5f4
@ -51,7 +51,7 @@
|
|||||||
</header>
|
</header>
|
||||||
<main
|
<main
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
class="relative grid h-screen grid-cols-[theme(spacing.0)_auto] overflow-hidden bg-immich-bg max-md:pt-[var(--navbar-height-md)] pt-[var(--navbar-height)] dark:bg-immich-dark-bg md:grid-cols-[theme(spacing.64)_auto]"
|
class="relative grid h-screen grid-cols-[theme(spacing.0)_auto] overflow-hidden bg-immich-bg max-md:pt-[var(--navbar-height-md)] pt-[var(--navbar-height)] dark:bg-immich-dark-bg sidebar:grid-cols-[theme(spacing.64)_auto]"
|
||||||
>
|
>
|
||||||
{#if sidebar}{@render sidebar()}{:else if admin}
|
{#if sidebar}{@render sidebar()}{:else if admin}
|
||||||
<AdminSideBar />
|
<AdminSideBar />
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
import ThemeButton from '../theme-button.svelte';
|
import ThemeButton from '../theme-button.svelte';
|
||||||
import UserAvatar from '../user-avatar.svelte';
|
import UserAvatar from '../user-avatar.svelte';
|
||||||
import AccountInfoPanel from './account-info-panel.svelte';
|
import AccountInfoPanel from './account-info-panel.svelte';
|
||||||
import { isSidebarOpen } from '$lib/stores/side-bar.svelte';
|
import { sidebarStore } from '$lib/stores/sidebar.svelte';
|
||||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -62,32 +62,30 @@
|
|||||||
>
|
>
|
||||||
<SkipLink text={$t('skip_to_content')} />
|
<SkipLink text={$t('skip_to_content')} />
|
||||||
<div
|
<div
|
||||||
class="grid h-full grid-cols-[theme(spacing.32)_auto] items-center border-b bg-immich-bg py-2 dark:border-b-immich-dark-gray dark:bg-immich-dark-bg md:grid-cols-[theme(spacing.64)_auto]"
|
class="grid h-full grid-cols-[theme(spacing.32)_auto] items-center border-b bg-immich-bg py-2 dark:border-b-immich-dark-gray dark:bg-immich-dark-bg sidebar:grid-cols-[theme(spacing.64)_auto]"
|
||||||
>
|
>
|
||||||
<div class="flex flex-row gap-1 mx-4 items-center">
|
<div class="flex flex-row gap-1 mx-4 items-center">
|
||||||
<div>
|
<IconButton
|
||||||
<IconButton
|
id={menuButtonId}
|
||||||
id={menuButtonId}
|
shape="round"
|
||||||
shape="round"
|
color="secondary"
|
||||||
color="secondary"
|
variant="ghost"
|
||||||
variant="ghost"
|
size="medium"
|
||||||
size="medium"
|
aria-label={$t('main_menu')}
|
||||||
aria-label={$t('main_menu')}
|
icon={mdiMenu}
|
||||||
icon={mdiMenu}
|
onclick={() => {
|
||||||
onclick={() => {
|
sidebarStore.toggle();
|
||||||
isSidebarOpen.value = !isSidebarOpen.value;
|
}}
|
||||||
}}
|
onmousedown={(event: MouseEvent) => {
|
||||||
onmousedown={(event: MouseEvent) => {
|
if (sidebarStore.isOpen) {
|
||||||
if (isSidebarOpen.value) {
|
// stops event from reaching the default handler when clicking outside of the sidebar
|
||||||
// stops event from reaching the default handler when clicking outside of the sidebar
|
event.stopPropagation();
|
||||||
event.stopPropagation();
|
}
|
||||||
}
|
}}
|
||||||
}}
|
class="sidebar:hidden"
|
||||||
class="md:hidden"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<a data-sveltekit-preload-data="hover" href={AppRoute.PHOTOS}>
|
<a data-sveltekit-preload-data="hover" href={AppRoute.PHOTOS}>
|
||||||
<ImmichLogo class="max-md:h-[48px] h-[50px]" noText={mobileDevice.maxMd} />
|
<ImmichLogo class="max-md:h-[48px] h-[50px]" noText={!mobileDevice.isFullSidebar} />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between gap-4 lg:gap-8 pr-6">
|
<div class="flex justify-between gap-4 lg:gap-8 pr-6">
|
||||||
|
@ -110,7 +110,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<Icon
|
<Icon
|
||||||
path={mdiInformationOutline}
|
path={mdiInformationOutline}
|
||||||
class="hidden md:flex text-immich-primary dark:text-immich-dark-primary font-medium"
|
class="hidden sidebar:flex text-immich-primary dark:text-immich-dark-primary font-medium"
|
||||||
size="18"
|
size="18"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -123,7 +123,7 @@
|
|||||||
{#if showMessage}
|
{#if showMessage}
|
||||||
<dialog
|
<dialog
|
||||||
open
|
open
|
||||||
class="hidden md:block w-[500px] absolute bottom-[75px] left-[255px] bg-gray-50 dark:border-gray-800 border border-gray-200 dark:bg-immich-dark-gray dark:text-white text-black rounded-3xl z-10 shadow-2xl px-8 py-6"
|
class="hidden sidebar:block w-[500px] absolute bottom-[75px] left-[255px] bg-gray-50 dark:border-gray-800 border border-gray-200 dark:bg-immich-dark-gray dark:text-white text-black rounded-3xl z-10 shadow-2xl px-8 py-6"
|
||||||
transition:fade={{ duration: 150 }}
|
transition:fade={{ duration: 150 }}
|
||||||
onmouseover={() => (hoverMessage = true)}
|
onmouseover={() => (hoverMessage = true)}
|
||||||
onmouseleave={() => (hoverMessage = false)}
|
onmouseleave={() => (hoverMessage = false)}
|
||||||
|
@ -0,0 +1,80 @@
|
|||||||
|
import SideBarSection from '$lib/components/shared-components/side-bar/side-bar-section.svelte';
|
||||||
|
import { sidebarStore } from '$lib/stores/sidebar.svelte';
|
||||||
|
import { render, screen } from '@testing-library/svelte';
|
||||||
|
import { vi } from 'vitest';
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => {
|
||||||
|
return {
|
||||||
|
mobileDevice: {
|
||||||
|
isFullSidebar: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock('$lib/stores/mobile-device.svelte', () => ({
|
||||||
|
mobileDevice: mocks.mobileDevice,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('$lib/stores/sidebar.svelte', () => ({
|
||||||
|
sidebarStore: {
|
||||||
|
isOpen: false,
|
||||||
|
reset: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('SideBarSection component', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
mocks.mobileDevice.isFullSidebar = false;
|
||||||
|
sidebarStore.isOpen = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each`
|
||||||
|
isFullSidebar | isSidebarOpen | expectedInert
|
||||||
|
${false} | ${false} | ${true}
|
||||||
|
${false} | ${true} | ${false}
|
||||||
|
${true} | ${false} | ${false}
|
||||||
|
${true} | ${true} | ${false}
|
||||||
|
`(
|
||||||
|
'inert is $expectedInert when isFullSidebar=$isFullSidebar and isSidebarOpen=$isSidebarOpen',
|
||||||
|
({ isFullSidebar, isSidebarOpen, expectedInert }) => {
|
||||||
|
// setup
|
||||||
|
mocks.mobileDevice.isFullSidebar = isFullSidebar;
|
||||||
|
sidebarStore.isOpen = isSidebarOpen;
|
||||||
|
|
||||||
|
// when
|
||||||
|
render(SideBarSection);
|
||||||
|
const parent = screen.getByTestId('sidebar-parent');
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(parent.inert).toBe(expectedInert);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it('should set width when sidebar is expanded', () => {
|
||||||
|
// setup
|
||||||
|
mocks.mobileDevice.isFullSidebar = false;
|
||||||
|
sidebarStore.isOpen = true;
|
||||||
|
|
||||||
|
// when
|
||||||
|
render(SideBarSection);
|
||||||
|
const parent = screen.getByTestId('sidebar-parent');
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(parent.classList).toContain('sidebar:w-[16rem]'); // sets the initial width for page load
|
||||||
|
expect(parent.classList).toContain('w-[min(100vw,16rem)]');
|
||||||
|
expect(parent.classList).toContain('shadow-2xl');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should close the sidebar if it is open on initial render', () => {
|
||||||
|
// setup
|
||||||
|
mocks.mobileDevice.isFullSidebar = false;
|
||||||
|
sidebarStore.isOpen = true;
|
||||||
|
|
||||||
|
// when
|
||||||
|
render(SideBarSection);
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(sidebarStore.reset).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
@ -2,52 +2,45 @@
|
|||||||
import { clickOutside } from '$lib/actions/click-outside';
|
import { clickOutside } from '$lib/actions/click-outside';
|
||||||
import { focusTrap } from '$lib/actions/focus-trap';
|
import { focusTrap } from '$lib/actions/focus-trap';
|
||||||
import { menuButtonId } from '$lib/components/shared-components/navigation-bar/navigation-bar.svelte';
|
import { menuButtonId } from '$lib/components/shared-components/navigation-bar/navigation-bar.svelte';
|
||||||
import { isSidebarOpen } from '$lib/stores/side-bar.svelte';
|
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||||
import { type Snippet } from 'svelte';
|
import { sidebarStore } from '$lib/stores/sidebar.svelte';
|
||||||
|
import { onMount, type Snippet } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mdBreakpoint = 768;
|
|
||||||
|
|
||||||
let { children }: Props = $props();
|
let { children }: Props = $props();
|
||||||
|
|
||||||
let innerWidth: number = $state(0);
|
const isHidden = $derived(!sidebarStore.isOpen && !mobileDevice.isFullSidebar);
|
||||||
|
const isExpanded = $derived(sidebarStore.isOpen && !mobileDevice.isFullSidebar);
|
||||||
|
|
||||||
const closeSidebar = (width: number) => {
|
onMount(() => {
|
||||||
isSidebarOpen.value = width >= mdBreakpoint;
|
closeSidebar();
|
||||||
};
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
closeSidebar(innerWidth);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const isHidden = $derived(!isSidebarOpen.value && innerWidth < mdBreakpoint);
|
const closeSidebar = () => {
|
||||||
const isExpanded = $derived(isSidebarOpen.value && innerWidth < mdBreakpoint);
|
if (!isExpanded) {
|
||||||
|
|
||||||
const handleClickOutside = () => {
|
|
||||||
if (!isSidebarOpen.value) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
closeSidebar(innerWidth);
|
sidebarStore.reset();
|
||||||
if (isHidden) {
|
if (isHidden) {
|
||||||
document.querySelector<HTMLButtonElement>(`#${menuButtonId}`)?.focus();
|
document.querySelector<HTMLButtonElement>(`#${menuButtonId}`)?.focus();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window bind:innerWidth />
|
|
||||||
<section
|
<section
|
||||||
id="sidebar"
|
id="sidebar"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
class="immich-scrollbar relative z-10 w-0 md:w-[16rem] overflow-y-auto overflow-x-hidden bg-immich-bg pt-8 transition-all duration-200 dark:bg-immich-dark-bg"
|
class="immich-scrollbar relative z-10 w-0 sidebar:w-[16rem] overflow-y-auto overflow-x-hidden bg-immich-bg pt-8 transition-all duration-200 dark:bg-immich-dark-bg"
|
||||||
class:shadow-2xl={isExpanded}
|
class:shadow-2xl={isExpanded}
|
||||||
class:dark:border-r-immich-dark-gray={isExpanded}
|
class:dark:border-r-immich-dark-gray={isExpanded}
|
||||||
class:border-r={isExpanded}
|
class:border-r={isExpanded}
|
||||||
class:w-[min(100vw,16rem)]={isSidebarOpen.value}
|
class:w-[min(100vw,16rem)]={sidebarStore.isOpen}
|
||||||
|
data-testid="sidebar-parent"
|
||||||
inert={isHidden}
|
inert={isHidden}
|
||||||
use:clickOutside={{ onOutclick: handleClickOutside, onEscape: handleClickOutside }}
|
use:clickOutside={{ onOutclick: closeSidebar, onEscape: closeSidebar }}
|
||||||
use:focusTrap={{ active: isExpanded }}
|
use:focusTrap={{ active: isExpanded }}
|
||||||
>
|
>
|
||||||
<div class="pr-6 flex flex-col gap-1 h-max min-h-full">
|
<div class="pr-6 flex flex-col gap-1 h-max min-h-full">
|
||||||
|
@ -2,6 +2,7 @@ import { MediaQuery } from 'svelte/reactivity';
|
|||||||
|
|
||||||
const pointerCoarse = new MediaQuery('pointer:coarse');
|
const pointerCoarse = new MediaQuery('pointer:coarse');
|
||||||
const maxMd = new MediaQuery('max-width: 767px');
|
const maxMd = new MediaQuery('max-width: 767px');
|
||||||
|
const sidebar = new MediaQuery(`min-width: 850px`);
|
||||||
|
|
||||||
export const mobileDevice = {
|
export const mobileDevice = {
|
||||||
get pointerCoarse() {
|
get pointerCoarse() {
|
||||||
@ -10,4 +11,7 @@ export const mobileDevice = {
|
|||||||
get maxMd() {
|
get maxMd() {
|
||||||
return maxMd.current;
|
return maxMd.current;
|
||||||
},
|
},
|
||||||
|
get isFullSidebar() {
|
||||||
|
return sidebar.current;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export const isSidebarOpen = $state({ value: false });
|
|
21
web/src/lib/stores/sidebar.svelte.ts
Normal file
21
web/src/lib/stores/sidebar.svelte.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||||
|
|
||||||
|
class SidebarStore {
|
||||||
|
isOpen = $derived.by(() => mobileDevice.isFullSidebar);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the sidebar visibility to the default, based on the current screen width.
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
this.isOpen = mobileDevice.isFullSidebar;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the sidebar visibility, if available at the current screen width.
|
||||||
|
*/
|
||||||
|
toggle() {
|
||||||
|
this.isOpen = mobileDevice.isFullSidebar ? true : !this.isOpen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sidebarStore = new SidebarStore();
|
@ -55,6 +55,7 @@ export default {
|
|||||||
'max-lg': { max: '1023px' },
|
'max-lg': { max: '1023px' },
|
||||||
'max-md': { max: '767px' },
|
'max-md': { max: '767px' },
|
||||||
'max-sm': { max: '639px' },
|
'max-sm': { max: '639px' },
|
||||||
|
sidebar: { min: '850px' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user