diff --git a/packages/@headlessui-react/src/components/dialog/dialog.tsx b/packages/@headlessui-react/src/components/dialog/dialog.tsx index a7c0e40c0d..cf321d788b 100644 --- a/packages/@headlessui-react/src/components/dialog/dialog.tsx +++ b/packages/@headlessui-react/src/components/dialog/dialog.tsx @@ -101,6 +101,7 @@ function useScrollLock( if (!ownerDocument) return let d = disposables() + let scrollPosition = window.pageYOffset function style(node: HTMLElement, property: string, value: string) { let previous = node.style.getPropertyValue(property) @@ -123,10 +124,36 @@ function useScrollLock( } if (isIOS()) { - let scrollPosition = window.pageYOffset - style(document.body, 'marginTop', `-${scrollPosition}px`) + style(ownerDocument.body, 'marginTop', `-${scrollPosition}px`) window.scrollTo(0, 0) + // Relatively hacky, but if you click a link like `` in the Dialog, and there + // exists an element on the page (outside of the Dialog) with that id, then the browser will + // scroll to that position. However, this is not the case if the element we want to scroll to + // is higher and the browser needs to scroll up, but it doesn't do that. + // + // Let's try and capture that element and store it, so that we can later scroll to it once the + // Dialog closes. + let scrollToElement: HTMLElement | null = null + d.addEventListener( + ownerDocument, + 'click', + (e) => { + if (e.target instanceof HTMLElement) { + try { + let anchor = e.target.closest('a') + if (!anchor) return + let { hash } = new URL(anchor.href) + let el = ownerDocument.querySelector(hash) + if (el && !resolveAllowedContainers().some((container) => container.contains(el))) { + scrollToElement = el as HTMLElement + } + } catch (err) {} + } + }, + true + ) + d.addEventListener( ownerDocument, 'touchmove', @@ -161,6 +188,13 @@ function useScrollLock( // (Since the value of window.pageYOffset is 0 in the first case, we should be able to // always sum these values) window.scrollTo(0, window.pageYOffset + scrollPosition) + + // If we captured an element that should be scrolled to, then we can try to do that if the + // element is still connected (aka, still in the DOM). + if (scrollToElement && scrollToElement.isConnected) { + scrollToElement.scrollIntoView({ block: 'nearest' }) + scrollToElement = null + } }) } diff --git a/packages/@headlessui-vue/src/components/dialog/dialog.ts b/packages/@headlessui-vue/src/components/dialog/dialog.ts index fcc924dfc3..fec5d4ff6f 100644 --- a/packages/@headlessui-vue/src/components/dialog/dialog.ts +++ b/packages/@headlessui-vue/src/components/dialog/dialog.ts @@ -227,6 +227,7 @@ export let Dialog = defineComponent({ if (!owner) return let d = disposables() + let scrollPosition = window.pageYOffset function style(node: HTMLElement, property: string, value: string) { let previous = node.style.getPropertyValue(property) @@ -249,10 +250,36 @@ export let Dialog = defineComponent({ } if (isIOS()) { - let scrollPosition = window.pageYOffset - style(document.body, 'marginTop', `-${scrollPosition}px`) + style(owner.body, 'marginTop', `-${scrollPosition}px`) window.scrollTo(0, 0) + // Relatively hacky, but if you click a link like `` in the Dialog, and there + // exists an element on the page (outside of the Dialog) with that id, then the browser will + // scroll to that position. However, this is not the case if the element we want to scroll to + // is higher and the browser needs to scroll up, but it doesn't do that. + // + // Let's try and capture that element and store it, so that we can later scroll to it once the + // Dialog closes. + let scrollToElement: HTMLElement | null = null + d.addEventListener( + owner, + 'click', + (e) => { + if (e.target instanceof HTMLElement) { + try { + let anchor = e.target.closest('a') + if (!anchor) return + let { hash } = new URL(anchor.href) + let el = owner!.querySelector(hash) + if (el && !resolveAllowedContainers().some((container) => container.contains(el))) { + scrollToElement = el as HTMLElement + } + } catch (err) {} + } + }, + true + ) + d.addEventListener( owner, 'touchmove', @@ -288,6 +315,13 @@ export let Dialog = defineComponent({ // (Since the value of window.pageYOffset is 0 in the first case, we should be able to // always sum these values) window.scrollTo(0, window.pageYOffset + scrollPosition) + + // If we captured an element that should be scrolled to, then we can try to do that if the + // element is still connected (aka, still in the DOM). + if (scrollToElement && scrollToElement.isConnected) { + scrollToElement.scrollIntoView({ block: 'nearest' }) + scrollToElement = null + } }) }