Repository: kyeotic/raviger (ts)

Issue:
Scroll restoration
I currently use this extended version of `useRoutes`: 

```typescript

type ScrollRouteOptionParams = RouteOptionParams & {
  scrollRef: RefObject<HTMLElement> | 'body';
};

const useRoutesWithScroll = <T extends string>(
  routes: Routes<T>,
  { scrollRef, ...options }: ScrollRouteOptionParams
): JSX.Element => {
  const result = useRoutes(routes, options);

  const scrollToTop = useCallback(() => {
    if (scrollRef === 'body') {
      window.scrollTo(0, 0);
    } else {
      scrollRef.current.scrollTo(0, 0);
    }
  }, [scrollRef]);

  useEffect(scrollToTop, [result, scrollToTop]);

  return result;
};

```

It kinda works, but the true implementation would be able to restore scroll positions. I think it should be easy enough, but since you emit a `popstate` event when navigating (https://github.com/kyeotic/raviger/blob/main/src/navigate.ts#L73), it's hard to tell the difference between naviagtion and back-navigation. Could you use a different event or maybe tag the event somehow, so it's easy to filter out?

Pull request description:
feat: add custom dispatch tag for navigate
closes #111

Adds a `__tag: 'raviger:navigation'` to navigate `popstate` events, in order to distinguish them from other sources.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e674e5c..2ce2338 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,11 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
-## [3.0.0] - 2012-11-02
+## [3.1.0] - 2022-01-17
+### Added
+- `__tag` to the PopState events dispatched by `navigate`
+
+## [3.0.0] - 2021-11-02
 ### Changed
 - **BREAKING**:`usePath` returns `decodeURIComponent`-ed path
 - **BREAKING**:`useRoutes`, `useMatch`, and `usePathParams` match paths that have been `decodeURIComponent`-ed (e.g. `/weird (route)` will match a path of `/weird%20(route)`)
diff --git a/docs/api/navigate.md b/docs/api/navigate.md
index 4c41ff4..ff92f6c 100644
--- a/docs/api/navigate.md
+++ b/docs/api/navigate.md
@@ -71,3 +71,7 @@ export async function createUser () {
   navigate(`/users/${user.id}`, undefined, undefined, { user: user })
 }
 ```
+
+## Detecting `navigate` events
+
+`navigate` has two modes: intra-domain navigation and extra-domain navigation. When navigating outside the current origin navigation is done directly, and no `popstate` event is dispatched. When navigating to the current origin a custom `popstate` event is dispatched. a `__tag: 'raviger:navigation'` property is attached to help programmatically distinguish these events from `popstate` events dispatched from other sources, such as the browser **back button**.
\ No newline at end of file
diff --git a/src/navigate.ts b/src/navigate.ts
index d417beb..2c48457 100644
--- a/src/navigate.ts
+++ b/src/navigate.ts
@@ -70,7 +70,10 @@ export function navigate(
   if (replace) window.history.replaceState(state, '', url)
   else window.history.pushState(state, '', url)
 
-  dispatchEvent(new PopStateEvent('popstate'))
+  const event = new PopStateEvent('popstate')
+  // Tag the event so navigation can be filtered out from browser events
+  ;(event as any).__tag = 'raviger:navigation'
+  dispatchEvent(event)
 }
 
 export function useNavigationPrompt(predicate = true, prompt: string = defaultPrompt): void {


Test patch:
diff --git a/test/navigate.spec.tsx b/test/navigate.spec.tsx
index 3293642..8b74f14 100644
--- a/test/navigate.spec.tsx
+++ b/test/navigate.spec.tsx
@@ -272,4 +272,19 @@ describe('navigate', () => {
 
     navigate(currentHref)
   })
+  test('navigate dispatches custom event', () => {
+    window.history.replaceState = jest.fn()
+    window.history.pushState = jest.fn()
+
+    const listen = jest.fn()
+
+    window.addEventListener('popstate', listen)
+
+    navigate('/foo')
+
+    window.removeEventListener('popstate', listen)
+
+    expect(listen).toHaveBeenCalledWith(expect.objectContaining({ __tag: 'raviger:navigation' }))
+    jest.clearAllMocks()
+  })
 })