Skip to content

Commit 5cbceab

Browse files
ben-bastensavely-krasovsky
authored andcommitted
feat(web): expand/collapse sidebar (immich-app#16768)
* feat: expand/collapse sidebar * fix: general PR cleanup - add skip link unit test - remove unused tailwind styles - adjust asset grid spacing - fix event propogation * fix: cleaning up event listeners * fix: purchase modal and button on small screens * fix: explicit tailwind classes * fix: no animation on initial page load * fix: sidebar spacing and reactivity * chore: reverting changes to icons in nav and account info panel * fix: remove left margin from the asset grid after merging in new timeline * chore: extract search-bar changes for a separate PR * fix: add margin to memories
1 parent 8f9efee commit 5cbceab

File tree

23 files changed

+192
-65
lines changed

23 files changed

+192
-65
lines changed

i18n/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,7 @@
864864
"loop_videos": "Loop videos",
865865
"loop_videos_description": "Enable to automatically loop a video in the detail viewer.",
866866
"main_branch_warning": "You’re using a development version; we strongly recommend using a release version!",
867+
"main_menu": "Main menu",
867868
"make": "Make",
868869
"manage_shared_links": "Manage shared links",
869870
"manage_sharing_with_partners": "Manage sharing with partners",

web/src/lib/actions/__test__/focus-trap-test.svelte

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@
33
44
interface Props {
55
show: boolean;
6+
active?: boolean;
67
}
78
8-
let { show = $bindable() }: Props = $props();
9+
let { show = $bindable(), active = $bindable() }: Props = $props();
910
</script>
1011

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

1314
{#if show}
14-
<div use:focusTrap>
15+
<div use:focusTrap={{ active }}>
1516
<div>
1617
<span>text</span>
1718
<button data-testid="one" type="button" onclick={() => (show = false)}>Close</button>

web/src/lib/actions/__test__/focus-trap.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ describe('focusTrap action', () => {
1212
expect(document.activeElement).toEqual(screen.getByTestId('one'));
1313
});
1414

15+
it('should not set focus if inactive', async () => {
16+
render(FocusTrapTest, { show: true, active: false });
17+
await tick();
18+
expect(document.activeElement).toBe(document.body);
19+
});
20+
1521
it('supports backward focus wrapping', async () => {
1622
render(FocusTrapTest, { show: true });
1723
await tick();

web/src/lib/actions/click-outside.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,12 @@ export function clickOutside(node: HTMLElement, options: Options = {}): ActionRe
3535
}
3636
};
3737

38-
document.addEventListener('mousedown', handleClick, true);
38+
document.addEventListener('mousedown', handleClick, false);
3939
node.addEventListener('keydown', handleKey, false);
4040

4141
return {
4242
destroy() {
43-
document.removeEventListener('mousedown', handleClick, true);
43+
document.removeEventListener('mousedown', handleClick, false);
4444
node.removeEventListener('keydown', handleKey, false);
4545
},
4646
};

web/src/lib/actions/focus-trap.ts

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,34 @@
11
import { shortcuts } from '$lib/actions/shortcut';
22
import { tick } from 'svelte';
33

4+
interface Options {
5+
/**
6+
* Set whether the trap is active or not.
7+
*/
8+
active?: boolean;
9+
}
10+
411
const selectors =
5-
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
12+
'button:not([disabled], .hidden), [href]:not(.hidden), input:not([disabled], .hidden), select:not([disabled], .hidden), textarea:not([disabled], .hidden), [tabindex]:not([tabindex="-1"], .hidden)';
613

7-
export function focusTrap(container: HTMLElement) {
14+
export function focusTrap(container: HTMLElement, options?: Options) {
815
const triggerElement = document.activeElement;
916

10-
const focusableElement = container.querySelector<HTMLElement>(selectors);
17+
const withDefaults = (options?: Options) => {
18+
return {
19+
active: options?.active ?? true,
20+
};
21+
};
1122

12-
// Use tick() to ensure focus trap works correctly inside <Portal />
13-
void tick().then(() => focusableElement?.focus());
23+
const setInitialFocus = () => {
24+
const focusableElement = container.querySelector<HTMLElement>(selectors);
25+
// Use tick() to ensure focus trap works correctly inside <Portal />
26+
void tick().then(() => focusableElement?.focus());
27+
};
28+
29+
if (withDefaults(options).active) {
30+
setInitialFocus();
31+
}
1432

1533
const getFocusableElements = (): [HTMLElement | null, HTMLElement | null] => {
1634
const focusableElements = container.querySelectorAll<HTMLElement>(selectors);
@@ -27,7 +45,7 @@ export function focusTrap(container: HTMLElement) {
2745
shortcut: { key: 'Tab' },
2846
onShortcut: (event) => {
2947
const [firstElement, lastElement] = getFocusableElements();
30-
if (document.activeElement === lastElement) {
48+
if (document.activeElement === lastElement && withDefaults(options).active) {
3149
event.preventDefault();
3250
firstElement?.focus();
3351
}
@@ -39,7 +57,7 @@ export function focusTrap(container: HTMLElement) {
3957
shortcut: { key: 'Tab', shift: true },
4058
onShortcut: (event) => {
4159
const [firstElement, lastElement] = getFocusableElements();
42-
if (document.activeElement === firstElement) {
60+
if (document.activeElement === firstElement && withDefaults(options).active) {
4361
event.preventDefault();
4462
lastElement?.focus();
4563
}
@@ -48,6 +66,12 @@ export function focusTrap(container: HTMLElement) {
4866
]);
4967

5068
return {
69+
update(newOptions?: Options) {
70+
options = newOptions;
71+
if (withDefaults(options).active) {
72+
setInitialFocus();
73+
}
74+
},
5175
destroy() {
5276
destroyShortcuts?.();
5377
if (triggerElement instanceof HTMLElement) {

web/src/lib/components/elements/buttons/skip-link.svelte

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,55 @@
77
* Target for the skip link to move focus to.
88
*/
99
target?: string;
10+
/**
11+
* Text for the skip link button.
12+
*/
1013
text?: string;
14+
/**
15+
* Breakpoint at which the skip link is visible. Defaults to always being visible.
16+
*/
17+
breakpoint?: 'sm' | 'md' | 'lg' | 'xl' | '2xl';
1118
}
1219
13-
let { target = 'main', text = $t('skip_to_content') }: Props = $props();
20+
let { target = 'main', text = $t('skip_to_content'), breakpoint }: Props = $props();
1421
1522
let isFocused = $state(false);
1623
1724
const moveFocus = () => {
1825
const targetEl = document.querySelector<HTMLElement>(target);
1926
targetEl?.focus();
2027
};
28+
29+
const getBreakpoint = () => {
30+
if (!breakpoint) {
31+
return '';
32+
}
33+
switch (breakpoint) {
34+
case 'sm': {
35+
return 'hidden sm:block';
36+
}
37+
case 'md': {
38+
return 'hidden md:block';
39+
}
40+
case 'lg': {
41+
return 'hidden lg:block';
42+
}
43+
case 'xl': {
44+
return 'hidden xl:block';
45+
}
46+
case '2xl': {
47+
return 'hidden 2xl:block';
48+
}
49+
}
50+
};
2151
</script>
2252

2353
<div class="absolute z-50 top-2 left-2 transition-transform {isFocused ? 'translate-y-0' : '-translate-y-10 sr-only'}">
2454
<Button
2555
size="sm"
2656
rounded="none"
2757
onclick={moveFocus}
58+
class={getBreakpoint()}
2859
onfocus={() => (isFocused = true)}
2960
onblur={() => (isFocused = false)}
3061
>

web/src/lib/components/layouts/user-page-layout.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
</header>
5252
<main
5353
tabindex="-1"
54-
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]"
54+
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]"
5555
>
5656
{#if sidebar}{@render sidebar()}{:else if admin}
5757
<AdminSideBar />
@@ -66,7 +66,7 @@
6666
>
6767
<div class="flex gap-2 items-center">
6868
{#if title}
69-
<div class="font-medium" tabindex="-1" id={headerId}>{title}</div>
69+
<div class="font-medium outline-none" tabindex="-1" id={headerId}>{title}</div>
7070
{/if}
7171
{#if description}
7272
<p class="text-sm text-gray-400 dark:text-gray-600">{description}</p>

web/src/lib/components/photos-page/asset-grid.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -726,7 +726,7 @@
726726
class={[
727727
'scrollbar-hidden h-full overflow-y-auto outline-none',
728728
{ 'm-0': isEmpty },
729-
{ 'ml-4 tall:ml-0': !isEmpty },
729+
{ 'ml-0': !isEmpty },
730730
{ 'mr-[60px]': !isEmpty && !usingMobileDevice },
731731
]}
732732
tabindex="-1"

web/src/lib/components/photos-page/memory-lane.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
<section
3939
id="memory-lane"
4040
bind:this={memoryLaneElement}
41-
class="relative mt-5 overflow-x-scroll overflow-y-hidden whitespace-nowrap transition-all"
41+
class="relative mt-5 mx-2 overflow-x-scroll overflow-y-hidden whitespace-nowrap transition-all"
4242
style="scrollbar-width:none"
4343
use:resizeObserver={({ width }) => (offsetWidth = width)}
4444
onscroll={onScroll}

web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
<script lang="ts" module>
2+
export const menuButtonId = 'top-menu-button';
3+
</script>
4+
15
<script lang="ts">
26
import { page } from '$app/state';
37
import { clickOutside } from '$lib/actions/click-outside';
@@ -12,13 +16,14 @@
1216
import { handleLogout } from '$lib/utils/auth';
1317
import { getAboutInfo, logout, type ServerAboutResponseDto } from '@immich/sdk';
1418
import { Button, IconButton } from '@immich/ui';
15-
import { mdiHelpCircleOutline, mdiMagnify, mdiTrayArrowUp } from '@mdi/js';
19+
import { mdiHelpCircleOutline, mdiMagnify, mdiMenu, mdiTrayArrowUp } from '@mdi/js';
1620
import { onMount } from 'svelte';
1721
import { t } from 'svelte-i18n';
1822
import { fade } from 'svelte/transition';
1923
import ThemeButton from '../theme-button.svelte';
2024
import UserAvatar from '../user-avatar.svelte';
2125
import AccountInfoPanel from './account-info-panel.svelte';
26+
import { isSidebarOpen } from '$lib/stores/side-bar.svelte';
2227
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
2328
2429
interface Props {
@@ -57,11 +62,34 @@
5762
>
5863
<SkipLink text={$t('skip_to_content')} />
5964
<div
60-
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]"
65+
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]"
6166
>
62-
<a data-sveltekit-preload-data="hover" class="ml-4" href={AppRoute.PHOTOS}>
63-
<ImmichLogo class="max-md:h-[48px] h-[50px]" noText={mobileDevice.maxMd} />
64-
</a>
67+
<div class="flex flex-row gap-1 mx-4 items-center">
68+
<div>
69+
<IconButton
70+
id={menuButtonId}
71+
shape="round"
72+
color="secondary"
73+
variant="ghost"
74+
size="large"
75+
aria-label={$t('main_menu')}
76+
icon={mdiMenu}
77+
onclick={() => {
78+
isSidebarOpen.value = !isSidebarOpen.value;
79+
}}
80+
onmousedown={(event: MouseEvent) => {
81+
if (isSidebarOpen.value) {
82+
// stops event from reaching the default handler when clicking outside of the sidebar
83+
event.stopPropagation();
84+
}
85+
}}
86+
class="md:hidden"
87+
/>
88+
</div>
89+
<a data-sveltekit-preload-data="hover" href={AppRoute.PHOTOS}>
90+
<ImmichLogo class="max-md:h-[48px] h-[50px]" noText={mobileDevice.maxMd} />
91+
</a>
92+
</div>
6593
<div class="flex justify-between gap-4 lg:gap-8 pr-6">
6694
<div class="hidden w-full max-w-5xl flex-1 tall:pl-0 sm:block">
6795
{#if $featureFlags.search}
@@ -80,7 +108,6 @@
80108
href={AppRoute.SEARCH}
81109
id="search-button"
82110
class="sm:hidden"
83-
title={$t('go_to_search')}
84111
aria-label={$t('go_to_search')}
85112
/>
86113
{/if}
@@ -120,7 +147,6 @@
120147
color="secondary"
121148
variant="ghost"
122149
size="medium"
123-
title={$t('support_and_feedback')}
124150
icon={mdiHelpCircleOutline}
125151
onclick={() => (shouldShowHelpPanel = !shouldShowHelpPanel)}
126152
aria-label={$t('support_and_feedback')}

web/src/lib/components/shared-components/purchasing/individual-purchase-option-card.svelte

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
</script>
99

1010
<!-- Individual Purchase Option -->
11-
<div class="border border-gray-300 dark:border-gray-800 w-[375px] p-8 rounded-3xl bg-gray-100 dark:bg-gray-900">
11+
<div
12+
class="border border-gray-300 dark:border-gray-800 w-[min(375px,100%)] p-8 rounded-3xl bg-gray-100 dark:bg-gray-900"
13+
>
1214
<div class="text-immich-primary dark:text-immich-dark-primary">
1315
<Icon path={mdiAccount} size="56" />
1416
<p class="font-semibold text-lg mt-1">{$t('purchase_individual_title')}</p>

web/src/lib/components/shared-components/purchasing/purchase-content.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
</div>
5858
{/if}
5959

60-
<div class="flex gap-6 mt-4 justify-between">
60+
<div class="flex flex-col sm:flex-row gap-6 mt-4 justify-between">
6161
<ServerPurchaseOptionCard />
6262
<UserPurchaseOptionCard />
6363
</div>

web/src/lib/components/shared-components/purchasing/server-purchase-option-card.svelte

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
</script>
99

1010
<!-- SERVER Purchase Options -->
11-
<div class="border border-gray-300 dark:border-gray-800 w-[375px] p-8 rounded-3xl bg-gray-100 dark:bg-gray-900">
11+
<div
12+
class="border border-gray-300 dark:border-gray-800 w-[min(375px,100%)] p-8 rounded-3xl bg-gray-100 dark:bg-gray-900"
13+
>
1214
<div class="text-immich-primary dark:text-immich-dark-primary">
1315
<Icon path={mdiServer} size="56" />
1416
<p class="font-semibold text-lg mt-1">{$t('purchase_server_title')}</p>

web/src/lib/components/shared-components/side-bar/purchase-info.svelte

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
<LicenseModal onClose={() => (isOpen = false)} />
7979
{/if}
8080

81-
<div class="hidden md:block license-status pl-4 text-sm">
81+
<div class="license-status pl-4 text-sm">
8282
{#if $isPurchased && $preferences.purchase.showSupportBadge}
8383
<button
8484
onclick={() => goto(`${AppRoute.USER_SETTINGS}?isOpen=user-purchase-settings`)}
@@ -95,7 +95,7 @@
9595
onmouseleave={() => (hoverButton = false)}
9696
onfocus={onButtonHover}
9797
onblur={() => (hoverButton = false)}
98-
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"
98+
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"
9999
>
100100
<div class="flex justify-between w-full place-items-center place-content-center">
101101
<div class="flex place-items-center place-content-center gap-1">
@@ -110,7 +110,7 @@
110110
<div>
111111
<Icon
112112
path={mdiInformationOutline}
113-
class="flex text-immich-primary dark:text-immich-dark-primary font-medium"
113+
class="hidden md:flex text-immich-primary dark:text-immich-dark-primary font-medium"
114114
size="18"
115115
/>
116116
</div>
@@ -123,7 +123,7 @@
123123
{#if showMessage}
124124
<dialog
125125
open
126-
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"
126+
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"
127127
transition:fade={{ duration: 150 }}
128128
onmouseover={() => (hoverMessage = true)}
129129
onmouseleave={() => (hoverMessage = false)}

web/src/lib/components/shared-components/side-bar/server-status.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
{/if}
4343

4444
<div
45-
class="text-sm hidden group-hover:sm:flex md:flex pl-5 pr-1 place-items-center place-content-center justify-between"
45+
class="text-sm flex md:flex pl-5 pr-1 place-items-center place-content-center justify-between min-w-52 overflow-hidden"
4646
>
4747
{#if $connected}
4848
<div class="flex gap-2 place-items-center place-content-center">

web/src/lib/components/shared-components/side-bar/side-bar-link.svelte

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,9 @@
6262
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
6363
{isSelected
6464
? 'bg-immich-primary/10 text-immich-primary hover:bg-immich-primary/10 dark:bg-immich-dark-primary/10 dark:text-immich-dark-primary'
65-
: ''}
66-
pl-5 group-hover:sm:px-5 md:px-5
67-
"
65+
: ''}"
6866
>
69-
<div class="flex w-full place-items-center gap-4 overflow-hidden truncate">
67+
<div class="flex w-full place-items-center gap-4 pl-5 overflow-hidden truncate">
7068
<Icon path={icon} size="1.5em" class="shrink-0" flipped={flippedLogo} ariaHidden />
7169
<span class="text-sm font-medium">{title}</span>
7270
</div>

0 commit comments

Comments
 (0)