fix(web): increase sidebar breakpoint (#17436)

This commit is contained in:
Ben 2025-04-10 13:00:30 -04:00 committed by GitHub
parent 6d3f3d8616
commit e3995fb5f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 145 additions and 49 deletions

View File

@ -51,7 +51,7 @@
</header>
<main
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}
<AdminSideBar />

View File

@ -23,7 +23,7 @@
import ThemeButton from '../theme-button.svelte';
import UserAvatar from '../user-avatar.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';
interface Props {
@ -62,32 +62,30 @@
>
<SkipLink text={$t('skip_to_content')} />
<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>
<IconButton
id={menuButtonId}
shape="round"
color="secondary"
variant="ghost"
size="medium"
aria-label={$t('main_menu')}
icon={mdiMenu}
onclick={() => {
isSidebarOpen.value = !isSidebarOpen.value;
}}
onmousedown={(event: MouseEvent) => {
if (isSidebarOpen.value) {
// stops event from reaching the default handler when clicking outside of the sidebar
event.stopPropagation();
}
}}
class="md:hidden"
/>
</div>
<IconButton
id={menuButtonId}
shape="round"
color="secondary"
variant="ghost"
size="medium"
aria-label={$t('main_menu')}
icon={mdiMenu}
onclick={() => {
sidebarStore.toggle();
}}
onmousedown={(event: MouseEvent) => {
if (sidebarStore.isOpen) {
// stops event from reaching the default handler when clicking outside of the sidebar
event.stopPropagation();
}
}}
class="sidebar:hidden"
/>
<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>
</div>
<div class="flex justify-between gap-4 lg:gap-8 pr-6">

View File

@ -110,7 +110,7 @@
<div>
<Icon
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"
/>
</div>
@ -123,7 +123,7 @@
{#if showMessage}
<dialog
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 }}
onmouseover={() => (hoverMessage = true)}
onmouseleave={() => (hoverMessage = false)}

View File

@ -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();
});
});

View File

@ -2,52 +2,45 @@
import { clickOutside } from '$lib/actions/click-outside';
import { focusTrap } from '$lib/actions/focus-trap';
import { menuButtonId } from '$lib/components/shared-components/navigation-bar/navigation-bar.svelte';
import { isSidebarOpen } from '$lib/stores/side-bar.svelte';
import { type Snippet } from 'svelte';
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
import { sidebarStore } from '$lib/stores/sidebar.svelte';
import { onMount, type Snippet } from 'svelte';
interface Props {
children?: Snippet;
}
const mdBreakpoint = 768;
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) => {
isSidebarOpen.value = width >= mdBreakpoint;
};
$effect(() => {
closeSidebar(innerWidth);
onMount(() => {
closeSidebar();
});
const isHidden = $derived(!isSidebarOpen.value && innerWidth < mdBreakpoint);
const isExpanded = $derived(isSidebarOpen.value && innerWidth < mdBreakpoint);
const handleClickOutside = () => {
if (!isSidebarOpen.value) {
const closeSidebar = () => {
if (!isExpanded) {
return;
}
closeSidebar(innerWidth);
sidebarStore.reset();
if (isHidden) {
document.querySelector<HTMLButtonElement>(`#${menuButtonId}`)?.focus();
}
};
</script>
<svelte:window bind:innerWidth />
<section
id="sidebar"
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:dark:border-r-immich-dark-gray={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}
use:clickOutside={{ onOutclick: handleClickOutside, onEscape: handleClickOutside }}
use:clickOutside={{ onOutclick: closeSidebar, onEscape: closeSidebar }}
use:focusTrap={{ active: isExpanded }}
>
<div class="pr-6 flex flex-col gap-1 h-max min-h-full">

View File

@ -2,6 +2,7 @@ import { MediaQuery } from 'svelte/reactivity';
const pointerCoarse = new MediaQuery('pointer:coarse');
const maxMd = new MediaQuery('max-width: 767px');
const sidebar = new MediaQuery(`min-width: 850px`);
export const mobileDevice = {
get pointerCoarse() {
@ -10,4 +11,7 @@ export const mobileDevice = {
get maxMd() {
return maxMd.current;
},
get isFullSidebar() {
return sidebar.current;
},
};

View File

@ -1 +0,0 @@
export const isSidebarOpen = $state({ value: false });

View 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();

View File

@ -55,6 +55,7 @@ export default {
'max-lg': { max: '1023px' },
'max-md': { max: '767px' },
'max-sm': { max: '639px' },
sidebar: { min: '850px' },
},
},
},