Skip to content

Commit 5994283

Browse files
committed
improve scrolling to in-page location
1 parent 79818fc commit 5994283

File tree

2 files changed

+72
-4
lines changed
  • packages
    • @headlessui-react/src/components/dialog
    • @headlessui-vue/src/components/dialog

2 files changed

+72
-4
lines changed

packages/@headlessui-react/src/components/dialog/dialog.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ function useScrollLock(
101101
if (!ownerDocument) return
102102

103103
let d = disposables()
104+
let scrollPosition = window.pageYOffset
104105

105106
function style(node: HTMLElement, property: string, value: string) {
106107
let previous = node.style.getPropertyValue(property)
@@ -123,10 +124,36 @@ function useScrollLock(
123124
}
124125

125126
if (isIOS()) {
126-
let scrollPosition = window.pageYOffset
127-
style(document.body, 'marginTop', `-${scrollPosition}px`)
127+
style(ownerDocument.body, 'marginTop', `-${scrollPosition}px`)
128128
window.scrollTo(0, 0)
129129

130+
// Relatively hacky, but if you click a link like `<a href="#foo">` in the Dialog, and there
131+
// exists an element on the page (outside of the Dialog) with that id, then the browser will
132+
// scroll to that position. However, this is not the case if the element we want to scroll to
133+
// is higher and the browser needs to scroll up, but it doesn't do that.
134+
//
135+
// Let's try and capture that element and store it, so that we can later scroll to it once the
136+
// Dialog closes.
137+
let scrollToElement: HTMLElement | null = null
138+
d.addEventListener(
139+
ownerDocument,
140+
'click',
141+
(e) => {
142+
if (e.target instanceof HTMLElement) {
143+
try {
144+
let anchor = e.target.closest('a')
145+
if (!anchor) return
146+
let { hash } = new URL(anchor.href)
147+
let el = ownerDocument.querySelector(hash)
148+
if (el && !resolveAllowedContainers().some((container) => container.contains(el))) {
149+
scrollToElement = el as HTMLElement
150+
}
151+
} catch (err) {}
152+
}
153+
},
154+
true
155+
)
156+
130157
d.addEventListener(
131158
ownerDocument,
132159
'touchmove',
@@ -161,6 +188,13 @@ function useScrollLock(
161188
// (Since the value of window.pageYOffset is 0 in the first case, we should be able to
162189
// always sum these values)
163190
window.scrollTo(0, window.pageYOffset + scrollPosition)
191+
192+
// If we captured an element that should be scrolled to, then we can try to do that if the
193+
// element is still connected (aka, still in the DOM).
194+
if (scrollToElement && scrollToElement.isConnected) {
195+
scrollToElement.scrollIntoView({ block: 'nearest' })
196+
scrollToElement = null
197+
}
164198
})
165199
}
166200

packages/@headlessui-vue/src/components/dialog/dialog.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ export let Dialog = defineComponent({
227227
if (!owner) return
228228

229229
let d = disposables()
230+
let scrollPosition = window.pageYOffset
230231

231232
function style(node: HTMLElement, property: string, value: string) {
232233
let previous = node.style.getPropertyValue(property)
@@ -249,10 +250,36 @@ export let Dialog = defineComponent({
249250
}
250251

251252
if (isIOS()) {
252-
let scrollPosition = window.pageYOffset
253-
style(document.body, 'marginTop', `-${scrollPosition}px`)
253+
style(owner.body, 'marginTop', `-${scrollPosition}px`)
254254
window.scrollTo(0, 0)
255255

256+
// Relatively hacky, but if you click a link like `<a href="#foo">` in the Dialog, and there
257+
// exists an element on the page (outside of the Dialog) with that id, then the browser will
258+
// scroll to that position. However, this is not the case if the element we want to scroll to
259+
// is higher and the browser needs to scroll up, but it doesn't do that.
260+
//
261+
// Let's try and capture that element and store it, so that we can later scroll to it once the
262+
// Dialog closes.
263+
let scrollToElement: HTMLElement | null = null
264+
d.addEventListener(
265+
owner,
266+
'click',
267+
(e) => {
268+
if (e.target instanceof HTMLElement) {
269+
try {
270+
let anchor = e.target.closest('a')
271+
if (!anchor) return
272+
let { hash } = new URL(anchor.href)
273+
let el = owner!.querySelector(hash)
274+
if (el && !resolveAllowedContainers().some((container) => container.contains(el))) {
275+
scrollToElement = el as HTMLElement
276+
}
277+
} catch (err) {}
278+
}
279+
},
280+
true
281+
)
282+
256283
d.addEventListener(
257284
owner,
258285
'touchmove',
@@ -288,6 +315,13 @@ export let Dialog = defineComponent({
288315
// (Since the value of window.pageYOffset is 0 in the first case, we should be able to
289316
// always sum these values)
290317
window.scrollTo(0, window.pageYOffset + scrollPosition)
318+
319+
// If we captured an element that should be scrolled to, then we can try to do that if the
320+
// element is still connected (aka, still in the DOM).
321+
if (scrollToElement && scrollToElement.isConnected) {
322+
scrollToElement.scrollIntoView({ block: 'nearest' })
323+
scrollToElement = null
324+
}
291325
})
292326
}
293327

0 commit comments

Comments
 (0)