Skip to content

feat(web): expand/collapse sidebar #16768

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Apr 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fc43ec0
feat: expand/collapse sidebar
ben-basten Jan 19, 2025
ec9a70c
fix: general PR cleanup
ben-basten Mar 11, 2025
7d8f672
fix: cleaning up event listeners
ben-basten Mar 11, 2025
3097a25
fix: purchase modal and button on small screens
ben-basten Mar 11, 2025
de5e1cc
fix: explicit tailwind classes
ben-basten Mar 11, 2025
03137d4
fix: no animation on initial page load
ben-basten Mar 11, 2025
656232e
Merge branch 'main' of https://github.com/immich-app/immich into feat…
ben-basten Mar 11, 2025
568a049
fix: sidebar spacing and reactivity
ben-basten Mar 12, 2025
43b6b4f
Merge branch 'main' into feat/responsive-sidebar
ben-basten Mar 12, 2025
7b30a88
Merge branch 'main' of https://github.com/immich-app/immich into feat…
ben-basten Mar 14, 2025
bb9abf9
Merge branch 'main' of https://github.com/immich-app/immich into feat…
ben-basten Mar 19, 2025
26c5661
Merge branch 'main' of https://github.com/immich-app/immich into feat…
ben-basten Mar 21, 2025
f298087
chore: reverting changes to icons in nav and account info panel
ben-basten Mar 21, 2025
2be0c8d
Merge branch 'main' of https://github.com/immich-app/immich into feat…
ben-basten Mar 22, 2025
fbbecb5
fix: remove left margin from the asset grid after merging in new time…
ben-basten Mar 22, 2025
e3f9298
Merge branch 'main' of https://github.com/immich-app/immich into feat…
ben-basten Mar 25, 2025
d9c83b8
chore: extract search-bar changes for a separate PR
ben-basten Mar 27, 2025
cf53917
Merge branch 'main' of https://github.com/immich-app/immich into feat…
ben-basten Mar 27, 2025
cf2aeaf
fix: add margin to memories
ben-basten Mar 27, 2025
268c717
Merge branch 'main' of https://github.com/immich-app/immich into feat…
ben-basten Mar 29, 2025
e9f64fd
Merge branch 'main' into feat/responsive-sidebar
ben-basten Apr 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,7 @@
"loop_videos": "Loop videos",
"loop_videos_description": "Enable to automatically loop a video in the detail viewer.",
"main_branch_warning": "You’re using a development version; we strongly recommend using a release version!",
"main_menu": "Main menu",
"make": "Make",
"manage_shared_links": "Manage shared links",
"manage_sharing_with_partners": "Manage sharing with partners",
Expand Down
5 changes: 3 additions & 2 deletions web/src/lib/actions/__test__/focus-trap-test.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@

interface Props {
show: boolean;
active?: boolean;
}

let { show = $bindable() }: Props = $props();
let { show = $bindable(), active = $bindable() }: Props = $props();
</script>

<button type="button" onclick={() => (show = true)}>Open</button>

{#if show}
<div use:focusTrap>
<div use:focusTrap={{ active }}>
<div>
<span>text</span>
<button data-testid="one" type="button" onclick={() => (show = false)}>Close</button>
Expand Down
6 changes: 6 additions & 0 deletions web/src/lib/actions/__test__/focus-trap.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ describe('focusTrap action', () => {
expect(document.activeElement).toEqual(screen.getByTestId('one'));
});

it('should not set focus if inactive', async () => {
render(FocusTrapTest, { show: true, active: false });
await tick();
expect(document.activeElement).toBe(document.body);
});

it('supports backward focus wrapping', async () => {
render(FocusTrapTest, { show: true });
await tick();
Expand Down
4 changes: 2 additions & 2 deletions web/src/lib/actions/click-outside.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ export function clickOutside(node: HTMLElement, options: Options = {}): ActionRe
}
};

document.addEventListener('mousedown', handleClick, true);
document.addEventListener('mousedown', handleClick, false);
node.addEventListener('keydown', handleKey, false);

return {
destroy() {
document.removeEventListener('mousedown', handleClick, true);
document.removeEventListener('mousedown', handleClick, false);
node.removeEventListener('keydown', handleKey, false);
},
};
Expand Down
38 changes: 31 additions & 7 deletions web/src/lib/actions/focus-trap.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
import { shortcuts } from '$lib/actions/shortcut';
import { tick } from 'svelte';

interface Options {
/**
* Set whether the trap is active or not.
*/
active?: boolean;
}

const selectors =
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
'button:not([disabled], .hidden), [href]:not(.hidden), input:not([disabled], .hidden), select:not([disabled], .hidden), textarea:not([disabled], .hidden), [tabindex]:not([tabindex="-1"], .hidden)';

export function focusTrap(container: HTMLElement) {
export function focusTrap(container: HTMLElement, options?: Options) {
const triggerElement = document.activeElement;

const focusableElement = container.querySelector<HTMLElement>(selectors);
const withDefaults = (options?: Options) => {
return {
active: options?.active ?? true,
};
};

// Use tick() to ensure focus trap works correctly inside <Portal />
void tick().then(() => focusableElement?.focus());
const setInitialFocus = () => {
const focusableElement = container.querySelector<HTMLElement>(selectors);
// Use tick() to ensure focus trap works correctly inside <Portal />
void tick().then(() => focusableElement?.focus());
};

if (withDefaults(options).active) {
setInitialFocus();
}

const getFocusableElements = (): [HTMLElement | null, HTMLElement | null] => {
const focusableElements = container.querySelectorAll<HTMLElement>(selectors);
Expand All @@ -27,7 +45,7 @@ export function focusTrap(container: HTMLElement) {
shortcut: { key: 'Tab' },
onShortcut: (event) => {
const [firstElement, lastElement] = getFocusableElements();
if (document.activeElement === lastElement) {
if (document.activeElement === lastElement && withDefaults(options).active) {
event.preventDefault();
firstElement?.focus();
}
Expand All @@ -39,7 +57,7 @@ export function focusTrap(container: HTMLElement) {
shortcut: { key: 'Tab', shift: true },
onShortcut: (event) => {
const [firstElement, lastElement] = getFocusableElements();
if (document.activeElement === firstElement) {
if (document.activeElement === firstElement && withDefaults(options).active) {
event.preventDefault();
lastElement?.focus();
}
Expand All @@ -48,6 +66,12 @@ export function focusTrap(container: HTMLElement) {
]);

return {
update(newOptions?: Options) {
options = newOptions;
if (withDefaults(options).active) {
setInitialFocus();
}
},
destroy() {
destroyShortcuts?.();
if (triggerElement instanceof HTMLElement) {
Expand Down
33 changes: 32 additions & 1 deletion web/src/lib/components/elements/buttons/skip-link.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,55 @@
* Target for the skip link to move focus to.
*/
target?: string;
/**
* Text for the skip link button.
*/
text?: string;
/**
* Breakpoint at which the skip link is visible. Defaults to always being visible.
*/
breakpoint?: 'sm' | 'md' | 'lg' | 'xl' | '2xl';
}

let { target = 'main', text = $t('skip_to_content') }: Props = $props();
let { target = 'main', text = $t('skip_to_content'), breakpoint }: Props = $props();

let isFocused = $state(false);

const moveFocus = () => {
const targetEl = document.querySelector<HTMLElement>(target);
targetEl?.focus();
};

const getBreakpoint = () => {
if (!breakpoint) {
return '';
}
switch (breakpoint) {
case 'sm': {
return 'hidden sm:block';
}
case 'md': {
return 'hidden md:block';
}
case 'lg': {
return 'hidden lg:block';
}
case 'xl': {
return 'hidden xl:block';
}
case '2xl': {
return 'hidden 2xl:block';
}
}
};
</script>

<div class="absolute z-50 top-2 left-2 transition-transform {isFocused ? 'translate-y-0' : '-translate-y-10 sr-only'}">
<Button
size="sm"
rounded="none"
onclick={moveFocus}
class={getBreakpoint()}
onfocus={() => (isFocused = true)}
onblur={() => (isFocused = false)}
>
Expand Down
4 changes: 2 additions & 2 deletions web/src/lib/components/layouts/user-page-layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
</header>
<main
tabindex="-1"
class="relative grid h-screen grid-cols-[theme(spacing.18)_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 md:grid-cols-[theme(spacing.64)_auto]"
>
{#if sidebar}{@render sidebar()}{:else if admin}
<AdminSideBar />
Expand All @@ -66,7 +66,7 @@
>
<div class="flex gap-2 items-center">
{#if title}
<div class="font-medium" tabindex="-1" id={headerId}>{title}</div>
<div class="font-medium outline-none" tabindex="-1" id={headerId}>{title}</div>
{/if}
{#if description}
<p class="text-sm text-gray-400 dark:text-gray-600">{description}</p>
Expand Down
2 changes: 1 addition & 1 deletion web/src/lib/components/photos-page/asset-grid.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,7 @@
class={[
'scrollbar-hidden h-full overflow-y-auto outline-none',
{ 'm-0': isEmpty },
{ 'ml-4 tall:ml-0': !isEmpty },
{ 'ml-0': !isEmpty },
{ 'mr-[60px]': !isEmpty && !usingMobileDevice },
]}
tabindex="-1"
Expand Down
2 changes: 1 addition & 1 deletion web/src/lib/components/photos-page/memory-lane.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
<section
id="memory-lane"
bind:this={memoryLaneElement}
class="relative mt-5 overflow-x-scroll overflow-y-hidden whitespace-nowrap transition-all"
class="relative mt-5 mx-2 overflow-x-scroll overflow-y-hidden whitespace-nowrap transition-all"
style="scrollbar-width:none"
use:resizeObserver={({ width }) => (offsetWidth = width)}
onscroll={onScroll}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
<script lang="ts" module>
export const menuButtonId = 'top-menu-button';
</script>

<script lang="ts">
import { page } from '$app/state';
import { clickOutside } from '$lib/actions/click-outside';
Expand All @@ -12,13 +16,14 @@
import { handleLogout } from '$lib/utils/auth';
import { getAboutInfo, logout, type ServerAboutResponseDto } from '@immich/sdk';
import { Button, IconButton } from '@immich/ui';
import { mdiHelpCircleOutline, mdiMagnify, mdiTrayArrowUp } from '@mdi/js';
import { mdiHelpCircleOutline, mdiMagnify, mdiMenu, mdiTrayArrowUp } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
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 { mobileDevice } from '$lib/stores/mobile-device.svelte';

interface Props {
Expand Down Expand Up @@ -57,11 +62,34 @@
>
<SkipLink text={$t('skip_to_content')} />
<div
class="grid h-full grid-cols-[theme(spacing.18)_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 md:grid-cols-[theme(spacing.64)_auto]"
>
<a data-sveltekit-preload-data="hover" class="ml-4" href={AppRoute.PHOTOS}>
<ImmichLogo class="max-md:h-[48px] h-[50px]" noText={mobileDevice.maxMd} />
</a>
<div class="flex flex-row gap-1 mx-4 items-center">
<div>
<IconButton
id={menuButtonId}
shape="round"
color="secondary"
variant="ghost"
size="large"
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>
<a data-sveltekit-preload-data="hover" href={AppRoute.PHOTOS}>
<ImmichLogo class="max-md:h-[48px] h-[50px]" noText={mobileDevice.maxMd} />
</a>
</div>
<div class="flex justify-between gap-4 lg:gap-8 pr-6">
<div class="hidden w-full max-w-5xl flex-1 tall:pl-0 sm:block">
{#if $featureFlags.search}
Expand All @@ -80,7 +108,6 @@
href={AppRoute.SEARCH}
id="search-button"
class="sm:hidden"
title={$t('go_to_search')}
aria-label={$t('go_to_search')}
/>
{/if}
Expand Down Expand Up @@ -120,7 +147,6 @@
color="secondary"
variant="ghost"
size="medium"
title={$t('support_and_feedback')}
icon={mdiHelpCircleOutline}
onclick={() => (shouldShowHelpPanel = !shouldShowHelpPanel)}
aria-label={$t('support_and_feedback')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
</script>

<!-- Individual Purchase Option -->
<div class="border border-gray-300 dark:border-gray-800 w-[375px] p-8 rounded-3xl bg-gray-100 dark:bg-gray-900">
<div
class="border border-gray-300 dark:border-gray-800 w-[min(375px,100%)] p-8 rounded-3xl bg-gray-100 dark:bg-gray-900"
>
<div class="text-immich-primary dark:text-immich-dark-primary">
<Icon path={mdiAccount} size="56" />
<p class="font-semibold text-lg mt-1">{$t('purchase_individual_title')}</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
</div>
{/if}

<div class="flex gap-6 mt-4 justify-between">
<div class="flex flex-col sm:flex-row gap-6 mt-4 justify-between">
<ServerPurchaseOptionCard />
<UserPurchaseOptionCard />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
</script>

<!-- SERVER Purchase Options -->
<div class="border border-gray-300 dark:border-gray-800 w-[375px] p-8 rounded-3xl bg-gray-100 dark:bg-gray-900">
<div
class="border border-gray-300 dark:border-gray-800 w-[min(375px,100%)] p-8 rounded-3xl bg-gray-100 dark:bg-gray-900"
>
<div class="text-immich-primary dark:text-immich-dark-primary">
<Icon path={mdiServer} size="56" />
<p class="font-semibold text-lg mt-1">{$t('purchase_server_title')}</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
<LicenseModal onClose={() => (isOpen = false)} />
{/if}

<div class="hidden md:block license-status pl-4 text-sm">
<div class="license-status pl-4 text-sm">
{#if $isPurchased && $preferences.purchase.showSupportBadge}
<button
onclick={() => goto(`${AppRoute.USER_SETTINGS}?isOpen=user-purchase-settings`)}
Expand All @@ -95,7 +95,7 @@
onmouseleave={() => (hoverButton = false)}
onfocus={onButtonHover}
onblur={() => (hoverButton = false)}
class="p-2 flex justify-between place-items-center place-content-center border border-immich-primary/20 dark:border-immich-dark-primary/10 mt-2 rounded-lg shadow-md dark:bg-immich-dark-primary/10 w-full"
class="p-2 flex justify-between place-items-center place-content-center border border-immich-primary/20 dark:border-immich-dark-primary/10 mt-2 rounded-lg shadow-md dark:bg-immich-dark-primary/10 min-w-52 w-full"
>
<div class="flex justify-between w-full place-items-center place-content-center">
<div class="flex place-items-center place-content-center gap-1">
Expand All @@ -110,7 +110,7 @@
<div>
<Icon
path={mdiInformationOutline}
class="flex text-immich-primary dark:text-immich-dark-primary font-medium"
class="hidden md:flex text-immich-primary dark:text-immich-dark-primary font-medium"
size="18"
/>
</div>
Expand All @@ -123,7 +123,7 @@
{#if showMessage}
<dialog
open
class="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 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"
transition:fade={{ duration: 150 }}
onmouseover={() => (hoverMessage = true)}
onmouseleave={() => (hoverMessage = false)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
{/if}

<div
class="text-sm hidden group-hover:sm:flex md:flex pl-5 pr-1 place-items-center place-content-center justify-between"
class="text-sm flex md:flex pl-5 pr-1 place-items-center place-content-center justify-between min-w-52 overflow-hidden"
>
{#if $connected}
<div class="flex gap-2 place-items-center place-content-center">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,9 @@
class="flex w-full place-items-center gap-4 rounded-r-full py-3 transition-[padding] delay-100 duration-100 hover:cursor-pointer hover:bg-immich-gray hover:text-immich-primary dark:text-immich-dark-fg dark:hover:bg-immich-dark-gray dark:hover:text-immich-dark-primary
{isSelected
? 'bg-immich-primary/10 text-immich-primary hover:bg-immich-primary/10 dark:bg-immich-dark-primary/10 dark:text-immich-dark-primary'
: ''}
pl-5 group-hover:sm:px-5 md:px-5
"
: ''}"
>
<div class="flex w-full place-items-center gap-4 overflow-hidden truncate">
<div class="flex w-full place-items-center gap-4 pl-5 overflow-hidden truncate">
<Icon path={icon} size="1.5em" class="shrink-0" flipped={flippedLogo} ariaHidden />
<span class="text-sm font-medium">{title}</span>
</div>
Expand Down
Loading