Skip to content

Commit 6b5709a

Browse files
authored
Fix touch actions on iOS after entering disallowed area (#3801)
This PR fixes an issue where if you have an open Dialog on iOS that once you interacted with an area otuside of the dialog, you could no longer scroll or zoom the dialog itself anymore until it closed and reopened. The reason this was happening is that on `touchstart` we checked whether we are in an allowed area or not. - If we are in an allowed area, add `overscroll-behavior: contain` to the scrollable area to prevent scrolling the body behind it. - If we are not in an allowed area, add `touch-action: none` to the element you were touching to prevent any other touch events from being fired. The problem with this is that we never reset the state until the dialog is closed (and eventually unmounted). So to solve the problem, every time we get a `touchstart` event, we reset those CSS properties to their previous values, and then check again whether we are in an allowed area or not. Note: the `touchstart` event listener is set on the document itself, so we still get the event even if `touch-action: none` was set on the target element. ## Test plan Made 2 videos with a before / after comparison. The reproduction used is from #3234. The steps I'm applying here are: 1. Open the dialog by tapping on the toggle button 2. Scroll inside the dialog 3. Pinch to zoom inside the dialog 4. Tap outside / scroll outside the dialog to show that you can't with the outside 5. Scroll inside the dialog again 6. Pinch to zoom inside the dialog again **Before:** 1. ✅ Open the dialog by tapping on the toggle button 2. ✅ Scroll inside the dialog 3. ✅ Pinch to zoom inside the dialog 4. ✅ Tap outside / scroll outside the dialog to show that you can't with the outside 5. ❌ Scroll inside the dialog again 6. ❌ Pinch to zoom inside the dialog again https://github.com/user-attachments/assets/d79d9794-3732-4201-a4c9-43ea36d302ce **After:** 1. ✅ Open the dialog by tapping on the toggle button 2. ✅ Scroll inside the dialog 3. ✅ Pinch to zoom inside the dialog 4. ✅ Tap outside / scroll outside the dialog to show that you can't with the outside 5. ✅ Scroll inside the dialog again 6. ✅ Pinch to zoom inside the dialog again https://github.com/user-attachments/assets/f6c1c765-3c8c-4d3c-91c2-1b43507f6434 Fixes: #3234 Closes: #3602
1 parent 433b174 commit 6b5709a

File tree

2 files changed

+20
-15
lines changed

2 files changed

+20
-15
lines changed

packages/@headlessui-react/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- Ensure sibling `Dialog` components are scrollable on mobile ([#3796](https://github.com/tailwindlabs/headlessui/pull/3796))
1515
- Infer `Combobox` type based on `onChange` handler ([#3798](https://github.com/tailwindlabs/headlessui/pull/3798))
1616
- Allow home/end key default behavior inside `ComboboxInput` when `Combobox` is closed ([#3798](https://github.com/tailwindlabs/headlessui/pull/3798))
17+
- Ensure interacting with a `Dialog` on iOS works after interacting with a disallowed area ([#3801](https://github.com/tailwindlabs/headlessui/pull/3801))
1718

1819
## [2.2.8] - 2025-09-12
1920

packages/@headlessui-react/src/hooks/document-overflow/handle-ios-locking.ts

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -76,23 +76,27 @@ export function handleIOSLocking(): ScrollLockStep<ContainerMetadata> {
7676
)
7777

7878
// Rely on overscrollBehavior to prevent scrolling outside of the Dialog.
79-
d.addEventListener(doc, 'touchstart', (e) => {
80-
if (DOM.isHTMLorSVGElement(e.target) && DOM.hasInlineStyle(e.target)) {
81-
if (inAllowedContainer(e.target)) {
82-
// Find the root of the allowed containers
83-
let rootContainer = e.target
84-
while (
85-
rootContainer.parentElement &&
86-
inAllowedContainer(rootContainer.parentElement)
87-
) {
88-
rootContainer = rootContainer.parentElement!
89-
}
79+
d.group((_d) => {
80+
d.addEventListener(doc, 'touchstart', (e) => {
81+
_d.dispose()
82+
83+
if (DOM.isHTMLorSVGElement(e.target) && DOM.hasInlineStyle(e.target)) {
84+
if (inAllowedContainer(e.target)) {
85+
// Find the root of the allowed containers
86+
let rootContainer = e.target
87+
while (
88+
rootContainer.parentElement &&
89+
inAllowedContainer(rootContainer.parentElement)
90+
) {
91+
rootContainer = rootContainer.parentElement!
92+
}
9093

91-
d.style(rootContainer, 'overscrollBehavior', 'contain')
92-
} else {
93-
d.style(e.target, 'touchAction', 'none')
94+
_d.style(rootContainer, 'overscrollBehavior', 'contain')
95+
} else {
96+
_d.style(e.target, 'touchAction', 'none')
97+
}
9498
}
95-
}
99+
})
96100
})
97101

98102
d.addEventListener(

0 commit comments

Comments
 (0)