chat-ui-energy / src /lib /components /MobileNav.svelte
nsarrazin's picture
nsarrazin HF Staff
fix(front): mobile nav not closing sometimes
9af277e
<script lang="ts">
import { browser } from "$app/environment";
import { beforeNavigate } from "$app/navigation";
import { base } from "$app/paths";
import { page } from "$app/state";
import IconNew from "$lib/components/icons/IconNew.svelte";
import { Spring } from "svelte/motion";
import CarbonClose from "~icons/carbon/close";
import CarbonTextAlignJustify from "~icons/carbon/text-align-justify";
import { pan, type GestureCustomEvent, type PanCustomEvent } from "svelte-gestures";
interface Props {
title: string | undefined;
children?: import("svelte").Snippet;
}
let { title = $bindable(), children }: Props = $props();
let closeEl: HTMLButtonElement | undefined = $state();
let openEl: HTMLButtonElement | undefined = $state();
let isOpen = $state(false);
let panX: number | undefined = $state(undefined);
let panStart: number | undefined = $state(undefined);
let panStartTime: number | undefined = undefined;
const tween = Spring.of(
() => {
if (panX !== undefined) {
return panX;
}
if (isOpen) {
return 0 as number;
}
return -100 as number;
},
{ stiffness: 0.2, damping: 0.8 }
);
$effect(() => {
title ??= "New Chat";
});
beforeNavigate(() => {
isOpen = false;
panX = undefined;
});
let shouldFocusClose = $derived(isOpen && closeEl);
let shouldRefocusOpen = $derived(!isOpen && browser && document.activeElement === closeEl);
$effect(() => {
if (shouldFocusClose) {
closeEl?.focus();
} else if (shouldRefocusOpen) {
openEl?.focus();
}
});
</script>
<nav
class="flex h-12 items-center justify-between border-b bg-gray-50 px-3 dark:border-gray-800 dark:bg-gray-800/70 md:hidden"
>
<button
type="button"
class="-ml-3 flex size-12 shrink-0 items-center justify-center text-lg"
onclick={() => (isOpen = true)}
aria-label="Open menu"
bind:this={openEl}><CarbonTextAlignJustify /></button
>
<div class="flex h-full items-center justify-center">
{#if page.params?.id}
<span class="truncate px-4" data-testid="chat-title">{title}</span>
{/if}
</div>
<a
class:invisible={!page.params?.id}
href="{base}/"
class="-mr-3 flex size-12 shrink-0 items-center justify-center text-lg"><IconNew /></a
>
</nav>
<nav
use:pan={() => ({ delay: 0, preventdefault: true, touchAction: "pan-left" })}
onpanup={(e: GestureCustomEvent) => {
if (!panStart || !panStartTime || !panX) {
return;
}
// measure the pan velocity to determine if the menu should snap open or closed
const drawerWidth = window.innerWidth;
const trueX = e.detail.x + (panX / 100) * drawerWidth;
const panDuration = Date.now() - panStartTime;
const panVelocity = (trueX - panStart) / panDuration;
panX = undefined;
panStart = undefined;
panStartTime = undefined;
if (panVelocity < -0.5 || trueX < 50) {
isOpen = !isOpen;
}
}}
onpan={(e: PanCustomEvent) => {
if (e.detail.pointerType !== "touch") {
panX = undefined;
panStart = undefined;
panStartTime = undefined;
return;
}
panX ??= 0;
panStart ??= e.detail.x;
panStartTime ??= Date.now();
const drawerWidth = window.innerWidth;
const trueX = e.detail.x + (panX / 100) * drawerWidth;
const percentage = ((trueX - panStart) / drawerWidth) * 100;
panX = Math.max(-100, Math.min(0, percentage));
tween.set(panX, { instant: true });
}}
style="transform: translateX({Math.max(-100, Math.min(0, tween.current))}%);"
class="fixed inset-0 z-30 grid max-h-screen
grid-cols-1 grid-rows-[auto,1fr,auto,auto] bg-white pt-4 dark:bg-gray-900 md:hidden"
>
{#if page.url.pathname === base + "/"}
<button
type="button"
class="absolute right-0 top-0 z-50 flex size-12 items-center justify-center text-lg"
onclick={() => (isOpen = false)}
aria-label="Close menu"
bind:this={closeEl}><CarbonClose /></button
>
{/if}
{@render children?.()}
</nav>