mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
fix(web): increase sidebar breakpoint (#17436)
This commit is contained in:
parent
6d3f3d8616
commit
e3995fb5f4
@ -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 />
|
||||
|
@ -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">
|
||||
|
@ -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)}
|
||||
|
@ -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 { 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">
|
||||
|
@ -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;
|
||||
},
|
||||
};
|
||||
|
@ -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-md': { max: '767px' },
|
||||
'max-sm': { max: '639px' },
|
||||
sidebar: { min: '850px' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user