diff --git a/.changeset/fix-use-history-api-for-hash.md b/.changeset/fix-use-history-api-for-hash.md new file mode 100644 index 0000000..d59ac02 --- /dev/null +++ b/.changeset/fix-use-history-api-for-hash.md @@ -0,0 +1,5 @@ +--- +"@wethegit/react-modal": minor +--- + +Use the History API (`pushState`/`replaceState`) instead of directly assigning `window.location.hash` when opening and closing hash-linked modals. This prevents Safari from scrolling the user to the top of the page. Also adds a `popstate` listener so browser back/forward navigation correctly opens and closes the modal. diff --git a/package-lock.json b/package-lock.json index baa2a9f..d011b85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@wethegit/react-modal", - "version": "3.1.0", + "version": "3.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@wethegit/react-modal", - "version": "3.1.0", + "version": "3.2.0", "license": "MIT", "dependencies": { "@changesets/changelog-github": "~0.5.0", diff --git a/src/lib/hooks/use-modal.ts b/src/lib/hooks/use-modal.ts index e25ef76..647e9dc 100644 --- a/src/lib/hooks/use-modal.ts +++ b/src/lib/hooks/use-modal.ts @@ -28,7 +28,7 @@ export function useModal(props: UseModalOptions = {}) { // an extra dependecy and stay within the render loop current = cur - if (cur !== ModalStates.CLOSED) return ModalStates.CLOSED + if (cur !== ModalStates.CLOSED) return ModalStates.CLOSED return cur }) @@ -36,9 +36,12 @@ export function useModal(props: UseModalOptions = {}) { // we don't want focus back on the trigger if (current === ModalStates.CLOSED || !current) return - if (hash && window && window.location.hash === `#${hash}`) { - window.location.hash = "" - window.history.replaceState({}, "", window.location.pathname) + if (hash && typeof window !== "undefined" && window.location.hash === `#${hash}`) { + window.history.replaceState( + {}, + "", + window.location.pathname + window.location.search + ) } if (triggerRef && triggerRef.current) triggerRef.current.focus() @@ -58,8 +61,8 @@ export function useModal(props: UseModalOptions = {}) { if (current === ModalStates.OPEN) return - if (hash && window && window.location.hash !== `#${hash}`) { - window.location.hash = `#${hash}` + if (hash && typeof window !== "undefined" && window.location.hash !== `#${hash}`) { + window.history.pushState({}, "", `#${hash}`) } }, [hash]) @@ -81,10 +84,15 @@ export function useModal(props: UseModalOptions = {}) { // Check for a hash on mount handleHashChange() + // hashchange fires when navigating to same-page anchors (e.g. ) window.addEventListener("hashchange", handleHashChange) + // popstate fires when navigating via browser history (back/forward), + // which is what history.pushState entries use + window.addEventListener("popstate", handleHashChange) return () => { window.removeEventListener("hashchange", handleHashChange) + window.removeEventListener("popstate", handleHashChange) } }, [handleClose, handleOpen, hash])