From 6e62c09d841ffa0bee36f739899ad3c0b413a16c Mon Sep 17 00:00:00 2001 From: Ben <45583362+ben-basten@users.noreply.github.com> Date: Tue, 1 Apr 2025 22:12:04 -0400 Subject: [PATCH] feat(web): expand/collapse sidebar (#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 --- i18n/en.json | 1 + .../actions/__test__/focus-trap-test.svelte | 5 ++- .../lib/actions/__test__/focus-trap.spec.ts | 6 +++ web/src/lib/actions/click-outside.ts | 4 +- web/src/lib/actions/focus-trap.ts | 40 +++++++++++++---- .../elements/buttons/skip-link.svelte | 33 +++++++++++++- .../layouts/user-page-layout.svelte | 4 +- .../components/photos-page/asset-grid.svelte | 2 +- .../components/photos-page/memory-lane.svelte | 2 +- .../navigation-bar/navigation-bar.svelte | 40 ++++++++++++++--- .../individual-purchase-option-card.svelte | 4 +- .../purchasing/purchase-content.svelte | 2 +- .../server-purchase-option-card.svelte | 4 +- .../side-bar/purchase-info.svelte | 8 ++-- .../side-bar/server-status.svelte | 2 +- .../side-bar/side-bar-link.svelte | 6 +-- .../side-bar/side-bar-section.svelte | 45 +++++++++++++++++-- .../side-bar/side-bar.svelte | 5 +-- .../side-bar/storage-space.svelte | 40 ++++++++--------- .../shared-components/tree/tree.svelte | 1 + web/src/lib/stores/side-bar.svelte.ts | 1 + .../[[assetId=id]]/+page.svelte | 2 +- .../[[assetId=id]]/+page.svelte | 2 +- 23 files changed, 193 insertions(+), 66 deletions(-) create mode 100644 web/src/lib/stores/side-bar.svelte.ts diff --git a/i18n/en.json b/i18n/en.json index 8d1cc3a2b3..de17cccebd 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -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", diff --git a/web/src/lib/actions/__test__/focus-trap-test.svelte b/web/src/lib/actions/__test__/focus-trap-test.svelte index e1cb6fa4fb..a19d2b75db 100644 --- a/web/src/lib/actions/__test__/focus-trap-test.svelte +++ b/web/src/lib/actions/__test__/focus-trap-test.svelte @@ -3,15 +3,16 @@ interface Props { show: boolean; + active?: boolean; } - let { show = $bindable() }: Props = $props(); + let { show = $bindable(), active = $bindable() }: Props = $props(); {#if show} -
+
text diff --git a/web/src/lib/actions/__test__/focus-trap.spec.ts b/web/src/lib/actions/__test__/focus-trap.spec.ts index 6ce5ad6d5b..d92d8e037d 100644 --- a/web/src/lib/actions/__test__/focus-trap.spec.ts +++ b/web/src/lib/actions/__test__/focus-trap.spec.ts @@ -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(); diff --git a/web/src/lib/actions/click-outside.ts b/web/src/lib/actions/click-outside.ts index 92775546aa..599a97af75 100644 --- a/web/src/lib/actions/click-outside.ts +++ b/web/src/lib/actions/click-outside.ts @@ -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); }, }; diff --git a/web/src/lib/actions/focus-trap.ts b/web/src/lib/actions/focus-trap.ts index 7483e76099..1a84f21729 100644 --- a/web/src/lib/actions/focus-trap.ts +++ b/web/src/lib/actions/focus-trap.ts @@ -1,16 +1,34 @@ import { shortcuts } from '$lib/actions/shortcut'; import { tick } from 'svelte'; -const selectors = - 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'; +interface Options { + /** + * Set whether the trap is active or not. + */ + active?: boolean; +} -export function focusTrap(container: HTMLElement) { +const selectors = + '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, options?: Options) { const triggerElement = document.activeElement; - const focusableElement = container.querySelector(selectors); + const withDefaults = (options?: Options) => { + return { + active: options?.active ?? true, + }; + }; - // Use tick() to ensure focus trap works correctly inside - void tick().then(() => focusableElement?.focus()); + const setInitialFocus = () => { + const focusableElement = container.querySelector(selectors); + // Use tick() to ensure focus trap works correctly inside + void tick().then(() => focusableElement?.focus()); + }; + + if (withDefaults(options).active) { + setInitialFocus(); + } const getFocusableElements = (): [HTMLElement | null, HTMLElement | null] => { const focusableElements = container.querySelectorAll(selectors); @@ -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(); } @@ -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(); } @@ -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) { diff --git a/web/src/lib/components/elements/buttons/skip-link.svelte b/web/src/lib/components/elements/buttons/skip-link.svelte index b80e7d1a44..a1a24634c4 100644 --- a/web/src/lib/components/elements/buttons/skip-link.svelte +++ b/web/src/lib/components/elements/buttons/skip-link.svelte @@ -7,10 +7,17 @@ * 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); @@ -18,6 +25,29 @@ const targetEl = document.querySelector(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'; + } + } + };
@@ -25,6 +55,7 @@ size="sm" rounded="none" onclick={moveFocus} + class={getBreakpoint()} onfocus={() => (isFocused = true)} onblur={() => (isFocused = false)} > diff --git a/web/src/lib/components/layouts/user-page-layout.svelte b/web/src/lib/components/layouts/user-page-layout.svelte index 27873f39a5..4944982b60 100644 --- a/web/src/lib/components/layouts/user-page-layout.svelte +++ b/web/src/lib/components/layouts/user-page-layout.svelte @@ -51,7 +51,7 @@
{#if sidebar}{@render sidebar()}{:else if admin} @@ -66,7 +66,7 @@ >
{#if title} -
{title}
+
{title}
{/if} {#if description}

{description}

diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index d3afdc6072..7f716e70ef 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -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" diff --git a/web/src/lib/components/photos-page/memory-lane.svelte b/web/src/lib/components/photos-page/memory-lane.svelte index 9536aaf746..ff9264b961 100644 --- a/web/src/lib/components/photos-page/memory-lane.svelte +++ b/web/src/lib/components/photos-page/memory-lane.svelte @@ -38,7 +38,7 @@
(offsetWidth = width)} onscroll={onScroll} diff --git a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte index 02b55a1d07..bd4bffd2f6 100644 --- a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte +++ b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte @@ -1,3 +1,7 @@ + + -
+

{$t('purchase_individual_title')}

diff --git a/web/src/lib/components/shared-components/purchasing/purchase-content.svelte b/web/src/lib/components/shared-components/purchasing/purchase-content.svelte index 6a4e7f1a4b..567fce9281 100644 --- a/web/src/lib/components/shared-components/purchasing/purchase-content.svelte +++ b/web/src/lib/components/shared-components/purchasing/purchase-content.svelte @@ -57,7 +57,7 @@
{/if} -
+
diff --git a/web/src/lib/components/shared-components/purchasing/server-purchase-option-card.svelte b/web/src/lib/components/shared-components/purchasing/server-purchase-option-card.svelte index ffc015233c..19db461229 100644 --- a/web/src/lib/components/shared-components/purchasing/server-purchase-option-card.svelte +++ b/web/src/lib/components/shared-components/purchasing/server-purchase-option-card.svelte @@ -8,7 +8,9 @@ -
+

{$t('purchase_server_title')}

diff --git a/web/src/lib/components/shared-components/side-bar/purchase-info.svelte b/web/src/lib/components/shared-components/side-bar/purchase-info.svelte index a42e340eae..47e46c59b5 100644 --- a/web/src/lib/components/shared-components/side-bar/purchase-info.svelte +++ b/web/src/lib/components/shared-components/side-bar/purchase-info.svelte @@ -78,7 +78,7 @@ (isOpen = false)} /> {/if} -