Skip to content
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

Open sidebar with touch navigation (swipe) [default-theme] (bug in useSidebar?) #4330

Open
4 tasks done
Joelius300 opened this issue Nov 1, 2024 · 0 comments
Open
4 tasks done

Comments

@Joelius300
Copy link

Joelius300 commented Nov 1, 2024

Is your feature request related to a problem? Please describe.

In the VuePress default theme, you can swipe left-to-right to open the sidebar. See for example on https://vuepress.vuejs.org/ (if on desktop, emulate phone mode in dev tools).

Vitepress' default theme does not have that feature, even though it would be nice. More importantly, I could not figure out how to use useSidebar to implement it myself (see below).

Describe the solution you'd like

Add this small feature (potentially as an option) to the default theme and/or enable users of VitePress to implement such thnigs themselves by extending the default theme.

Describe alternatives you've considered

Here are some approaches I attempted, none work. The code for the swipe logic is adapted from VuePress' default-theme and works great, the problem seems to be with my use of useSidebar.

Extending default theme with custom layout

<!-- .vitepress/theme/CustomLayout.vue -->
<script setup lang="ts">
import DefaultTheme from "vitepress/theme";
import { useSidebar } from "vitepress/theme";

const { Layout } = DefaultTheme;

// it seems like this creates a new ghost sidebar that isn't useful at all.
// need access to the Layout component's sidebar, but that's not exposed.
const {
  isOpen: isSidebarOpen,
  open: openSidebar,
  close: closeSidebar,
  sidebar: sidebar,
  sidebarGroups: sidebarGroups,
} = useSidebar();

const touchStart = { x: 0, y: 0 };

const onTouchStart = (e: TouchEvent): void => {
  console.log("touch started");
  touchStart.x = e.changedTouches[0].clientX;
  touchStart.y = e.changedTouches[0].clientY;
};

const onTouchEnd = (e: TouchEvent): void => {
  console.log("touch ended");
  const dx = e.changedTouches[0].clientX - touchStart.x;
  const dy = e.changedTouches[0].clientY - touchStart.y;
  if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 40) {
    if (dx > 0 && touchStart.x <= 80) {
      console.log("opening sidebar");
      openSidebar();
    } else {
      console.log("closing sidebar");
      closeSidebar();
    }
  }
};
</script>

<template>
  <Layout @touchstart="onTouchStart" @touchend="onTouchEnd">
    <template #layout-top>
	  <!-- this is just to show that isSidebarOpen does not correspond to the actual state of the sidebar -->
      <h1>{{ isSidebarOpen ? "OPEN" : "CLOSED" }}</h1>
    </template>
  </Layout>
</template>
// .vitepress/theme/index.js
import DefaultTheme from 'vitepress/theme'
import CustomLayout from "./CustomLayout.vue"

export default {
  extends: DefaultTheme,
  Layout: CustomLayout
}

As you will see, I can swipe and trigger the console.log correctly, but the sidebar does not open. When I swipe, isSidebarOpen is set to true, but the sidebar doesn't show, which makes me think, useSidebar is not connected to the actual sidebar :/

Although not as minimal, this solution is implemented here if you want a checkout-ready attempt (Joelius300/werewolf-guide@59a78d1).

Completely new layout without using the default-theme one

I copied the code from https://github.com/vuejs/vitepress/blob/2b3cd95ab112ffce3a168b41c8cca1446d3fb920/src/client/theme-default/Layout.vue and adjusted it to work with imports from the vitepress package instead of internal ones (also had to copy useCloseSidebarOnEscape.

<!-- .vitepress/theme/CustomLayout.vue -->
<!-- Copied and adapted from https://github.com/vuejs/vitepress/blob/2b3cd95ab112ffce3a168b41c8cca1446d3fb920/src/client/theme-default/Layout.vue -->
<!-- because calling useSidebar seems to create another sidebar control and the default-theme Layout -->
<!-- does not expose the openSidebar, closeSidebar and isSidebarOpen. -->
<script setup lang="ts">
import { useRoute } from "vitepress";
import {
  computed,
  provide,
  useSlots,
  watch,
  watchEffect,
  onMounted,
  onUnmounted,
} from "vue";
import VPBackdrop from "vitepress/theme";
import VPContent from "vitepress/theme";
import VPFooter from "vitepress/theme";
import VPLocalNav from "vitepress/theme";
import VPNav from "vitepress/theme";
import VPSidebar from "vitepress/theme";
import VPSkipLink from "vitepress/theme";
import { useData } from "vitepress";
import { useSidebar } from "vitepress/theme";

// copied from https://github.com/vuejs/vitepress/blob/2b3cd95ab112ffce3a168b41c8cca1446d3fb920/src/client/theme-default/composables/sidebar.ts#L109
// because it's not exported alongside useSidebar
const useCloseSidebarOnEscape = (isOpen: Ref<boolean>, close: () => void) => {
  let triggerElement: HTMLButtonElement | undefined;

  watchEffect(() => {
    triggerElement = isOpen.value
      ? (document.activeElement as HTMLButtonElement)
      : undefined;
  });

  onMounted(() => {
    window.addEventListener("keyup", onEscape);
  });

  onUnmounted(() => {
    window.removeEventListener("keyup", onEscape);
  });

  function onEscape(e: KeyboardEvent) {
    if (e.key === "Escape" && isOpen.value) {
      close();
      triggerElement?.focus();
    }
  }
};

const {
  isOpen: isSidebarOpen,
  open: openSidebar,
  close: closeSidebar,
} = useSidebar();

const route = useRoute();
watch(() => route.path, closeSidebar);

useCloseSidebarOnEscape(isSidebarOpen, closeSidebar);

const { frontmatter } = useData();

const slots = useSlots();
const heroImageSlotExists = computed(() => !!slots["home-hero-image"]);

provide("hero-image-slot-exists", heroImageSlotExists);

const touchStart = { x: 0, y: 0 };

const onTouchStart = (e: TouchEvent): void => {
  console.log("touch started");
  touchStart.x = e.changedTouches[0].clientX;
  touchStart.y = e.changedTouches[0].clientY;
};

const onTouchEnd = (e: TouchEvent): void => {
  console.log("touch ended");
  const dx = e.changedTouches[0].clientX - touchStart.x;
  const dy = e.changedTouches[0].clientY - touchStart.y;
  if (Math.abs(dx) > Math.abs(dy) && Math.abs(dx) > 40) {
    if (dx > 0 && touchStart.x <= 80) {
      console.log("opening sidebar");
      openSidebar();
    } else {
      console.log("closing sidebar");
      closeSidebar();
    }
  }
};
</script>

<template>
  <div
    v-if="frontmatter.layout !== false"
    class="layout"
    :class="frontmatter.pageClass"
    @touchstart="onTouchStart"
    @touchend="onTouchEnd"
  >
    <slot name="layout-top" />
    <VPSkipLink />
    <VPBackdrop class="backdrop" :show="isSidebarOpen" @click="closeSidebar" />
    <VPNav>
      <template #nav-bar-title-before>
        <slot name="nav-bar-title-before" />
      </template>
      <template #nav-bar-title-after>
        <slot name="nav-bar-title-after" />
      </template>
      <template #nav-bar-content-before>
        <slot name="nav-bar-content-before" />
      </template>
      <template #nav-bar-content-after>
        <slot name="nav-bar-content-after" />
      </template>
      <template #nav-screen-content-before>
        <slot name="nav-screen-content-before" />
      </template>
      <template #nav-screen-content-after>
        <slot name="nav-screen-content-after" />
      </template>
    </VPNav>
    <VPLocalNav :open="isSidebarOpen" @open-menu="openSidebar" />

    <VPSidebar :open="isSidebarOpen">
      <template #sidebar-nav-before>
        <slot name="sidebar-nav-before" />
      </template>
      <template #sidebar-nav-after><slot name="sidebar-nav-after" /></template>
    </VPSidebar>

    <VPContent>
      <template #page-top><slot name="page-top" /></template>
      <template #page-bottom><slot name="page-bottom" /></template>
      <template #not-found><slot name="not-found" /></template>
      <template #home-hero-before><slot name="home-hero-before" /></template>
      <template #home-hero-info-before>
        <slot name="home-hero-info-before" />
      </template>
      <template #home-hero-info><slot name="home-hero-info" /></template>
      <template #home-hero-info-after>
        <slot name="home-hero-info-after" />
      </template>
      <template #home-hero-actions-after>
        <slot name="home-hero-actions-after" />
      </template>
      <template #home-hero-image><slot name="home-hero-image" /></template>
      <template #home-hero-after><slot name="home-hero-after" /></template>
      <template #home-features-before>
        <slot name="home-features-before" />
      </template>
      <template #home-features-after>
        <slot name="home-features-after" />
      </template>
      <template #doc-footer-before><slot name="doc-footer-before" /></template>
      <template #doc-before><slot name="doc-before" /></template>
      <template #doc-after><slot name="doc-after" /></template>
      <template #doc-top><slot name="doc-top" /></template>
      <template #doc-bottom><slot name="doc-bottom" /></template>
      <template #aside-top><slot name="aside-top" /></template>
      <template #aside-bottom><slot name="aside-bottom" /></template>
      <template #aside-outline-before>
        <slot name="aside-outline-before" />
      </template>
      <template #aside-outline-after>
        <slot name="aside-outline-after" />
      </template>
      <template #aside-ads-before><slot name="aside-ads-before" /></template>
      <template #aside-ads-after><slot name="aside-ads-after" /></template>
    </VPContent>
    <VPFooter />
    <slot name="layout-bottom" />
  </div>
  <Content v-else />
</template>

<style scoped>
.layout {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}
</style>

This also doesn't work, but in a different way. It doesn't display any content and in the browser I get the following warning: [Vue warn]: Component is missing template or render function:. Despite that, swiping left to right actually works great and prints opening sidebar in the console, it just doesn't open anything because the page is blank.

This implementation can also be found here: Joelius300/werewolf-guide@f745d8c.

In general, you'll find multiple different attempts in this branch: https://github.com/Joelius300/werewolf-guide/commits/swipe-sidebar

Taking a ref to the layout and accessing openSidebar from there

Doesn't work because openSidebar is not exposed with defineExpose (see https://vuejs.org/guide/essentials/template-refs.html#ref-on-component). This would also tightly couple the two and I don't think is a good idea.

Additional context

I am more than happy to learn how to correctly use useSidebar to implement this without adding it to the default theme.

Validations

@Joelius300 Joelius300 changed the title Open Sidebar with touch navigation (swipe) [default-theme] Open Sidebar with touch navigation (swipe) [default-theme] (bug in useSidebar?) Nov 1, 2024
@Joelius300 Joelius300 changed the title Open Sidebar with touch navigation (swipe) [default-theme] (bug in useSidebar?) Open sidebar with touch navigation (swipe) [default-theme] (bug in useSidebar?) Nov 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant