emilkowalski / vaul

An unstyled drawer component for React.
https://vaul.emilkowal.ski
MIT License
5.71k stars 189 forks source link

Prevent navigation to previous page and close drawer instead if its open #388

Open nahasco opened 1 month ago

nahasco commented 1 month ago

Mobile users are used to swipe back to close an app drawer. When they use my web app and swipe back, they are navigated to the previous page. How can I make it so the drawer gets closed and stay on the same page instead of navigating to a previoud page?

Thought of using query params, but I think its a very complicated solution.

afkcodes commented 3 weeks ago

i tried the same, with query params it opens but there is something wierd with vaul i guess it the moment you close it it also does force a navigation which is not the case with a normal portal if i use it

afkcodes commented 3 weeks ago
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Fragment, useEffect, useState } from 'react';
import { Route, Switch } from 'wouter';
import BottomSheet from '~components/BottomSheet/BottomSheet';
import MiniPlayer from '~components/MiniPlayer/MiniPlayer';
import PlayerScreen from '~components/Player/Player';
import BottomNavContainer from '~containers/BottomNavContainer';
import { LayoutContainer } from '~containers/LayoutContainer';
import { routes } from './routes';

const Modal = ({
  children,
  isOpen,
  onClose,
}: {
  children: React.ReactNode;
  isOpen: boolean;
  onClose: () => void;
}) => {
  if (!isOpen) return null;
  return (
    <div className='w-full h-full'>
      <BottomSheet isOpen={isOpen} onClose={onClose}>
        {children}
      </BottomSheet>
    </div>
  );
};

const Router = () => {
  const [isModalOpen, setIsModalOpen] = useState(false);

  const toggleModal = () => {
    const currentUrl = new URL(window.location.href);
    if (isModalOpen) {
      currentUrl.searchParams.delete('modal');
    } else {
      currentUrl.searchParams.set('modal', 'open');
    }
    window.history.pushState({}, '', currentUrl.toString());
    setIsModalOpen(!isModalOpen);
  };

  const closeModal = () => {
    const currentUrl = new URL(window.location.href);
    currentUrl.searchParams.delete('modal');
    window.history.back();
    setIsModalOpen(false);
  };

  useEffect(() => {
    const handlePopState = () => {
      const currentUrl = new URL(window.location.href);
      setIsModalOpen(currentUrl.searchParams.get('modal') === 'open');
    };

    window.addEventListener('popstate', handlePopState);

    // Initial check for modal query param
    const currentUrl = new URL(window.location.href);
    setIsModalOpen(currentUrl.searchParams.get('modal') === 'open');

    return () => {
      window.removeEventListener('popstate', handlePopState);
    };
  }, []);

  return (
    <Fragment>
      <LayoutContainer>
        <Switch>
          {routes.map((route) => (
            <Route path={route.path} component={route.element as any} key={route.path} />
          ))}
        </Switch>
      </LayoutContainer>
      <div onClick={toggleModal}>
        <MiniPlayer />
      </div>
      <BottomNavContainer />
      <Modal isOpen={isModalOpen} onClose={closeModal}>
        <PlayerScreen />
      </Modal>
    </Fragment>
  );
};

export default Router;

@nahasco this is how i have implemented, i am using wouter though hopefully it can help

nahasco commented 3 weeks ago

I went with a simpler method, but I wouldnt call it a solution since it messes up the browser history, a bit.

const Drawer = ({ shouldScaleBackground = true, ...props }: React.ComponentProps<typeof DrawerPrimitive.Root>) => {
    const router = useRouter()
    const pathname = usePathname()

    const handlePopState = React.useCallback(() => {
        if (props.open) {
            props.onOpenChange && props.onOpenChange(false)
            router.replace(pathname)
        }
    }, [props.open, props.onOpenChange, router])

    React.useEffect(() => {
        window.addEventListener("popstate", handlePopState)
        return () => {
            window.removeEventListener("popstate", handlePopState)
        }
    }, [handlePopState])

    React.useEffect(() => {
        if (props.open) {
            window.history.pushState({ drawerOpen: true }, "", pathname)
        }
    }, [props.open, pathname])

    return <DrawerPrimitive.Root shouldScaleBackground={shouldScaleBackground} {...props} />
}

Hoping we get a better built in solution