Skip to content

Commit 5e62f45

Browse files
Merge pull request #99 from Digital-Alchemy-TS/preventIframeScroll
prevent app from bounce scrolling when in an iFrame
2 parents 5187aa0 + 9f6143c commit 5e62f45

5 files changed

Lines changed: 88 additions & 2 deletions

File tree

apps/client/src/main.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,18 @@ import { StrictMode } from "react"
33
import { createRoot } from "react-dom/client"
44
import "unfonts.css"
55

6+
import { preventBounceScroll } from "@code-glue/paradigm"
67
import { Frame } from "@/pages/Frame/Frame"
78

89
// init the editor
910
import "@/components/Editor/init"
1011

12+
// Prevent bounce scrolling on document while allowing internal scroll
13+
const cleanupBounceScroll = preventBounceScroll()
14+
15+
// Ensure bounce scroll listeners are cleaned up when the page unloads
16+
window.addEventListener("unload", cleanupBounceScroll)
17+
// Make sure we have a root element to mount the app
1118
const rootElement = document.getElementById("root")
1219
if (!rootElement) {
1320
throw new Error('Root element with id "root" not found')

packages/paradigm/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
"private": true,
44
"type": "module",
55
"version": "0.0.1",
6+
"main": "./src/index.ts",
7+
"types": "./src/index.ts",
8+
"exports": {
9+
".": "./src/index.ts"
10+
},
611
"scripts": {
712
"start": "storybook dev -p 6006 --no-open --quiet",
813
"storybook:build": "storybook build",

packages/paradigm/src/components/List/Header/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ export type ListHeaderProps = {
1111
* Set to true if being rendered in a section list or anything else that has headers stick to the top of the list.
1212
* TODO Use this to set a blur on the wrapping View (Row)
1313
*/
14-
isSticky?: boolean
14+
// isSticky?: boolean
1515
}
1616

17-
export const Header = ({ children, isSticky }: ListHeaderProps) => {
17+
export const Header = ({ children }: ListHeaderProps) => {
1818
return (
1919
<Row
2020
noShrink

packages/paradigm/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export * from "./components/View"
2323

2424
export * from "./utils/platform"
2525
export * from "./utils/pluralize"
26+
export * from "./utils/preventBounceScroll"
2627

2728
// etc
2829

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
* Prevents scroll events from escaping the given root (e.g., your app wrapper) into the host page/iframe parent.
3+
* It stops propagation on wheel/touchmove at the root and only preventsDefault when attempting to scroll past edges.
4+
* Returns a cleanup function to remove listeners.
5+
*/
6+
export function preventBounceScroll(): () => void {
7+
const touchStartYById = new Map<number, number>()
8+
9+
const isAtEdge = (el: HTMLElement, delta: number): boolean => {
10+
const atTop = el.scrollTop <= 0
11+
const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 1
12+
const scrollingDown = delta > 0
13+
const scrollingUp = delta < 0
14+
return (atTop && scrollingUp) || (atBottom && scrollingDown)
15+
}
16+
17+
const onWheel = (e: WheelEvent): void => {
18+
// Keep the event inside the iframe/root
19+
e.stopPropagation()
20+
if (isAtEdge(document.documentElement, e.deltaY)) {
21+
e.preventDefault()
22+
}
23+
}
24+
25+
const onTouchStart = (e: TouchEvent): void => {
26+
if (e.touches.length > 0) {
27+
const touch = e.touches[0]
28+
if (touch) {
29+
touchStartYById.set(touch.identifier, touch.clientY)
30+
}
31+
}
32+
}
33+
34+
const onTouchMove = (e: TouchEvent): void => {
35+
if (e.touches.length === 0) return
36+
const touch = e.touches[0]
37+
if (!touch) return
38+
const startY = touchStartYById.get(touch.identifier) ?? touch.clientY
39+
const deltaY = startY - touch.clientY // positive when moving up
40+
41+
// Keep the event inside the iframe/root
42+
e.stopPropagation()
43+
if (isAtEdge(document.documentElement, deltaY)) {
44+
e.preventDefault()
45+
}
46+
}
47+
48+
document.documentElement.addEventListener("wheel", onWheel, {
49+
passive: false,
50+
capture: true,
51+
})
52+
document.documentElement.addEventListener("touchstart", onTouchStart, {
53+
passive: true,
54+
capture: true,
55+
})
56+
document.documentElement.addEventListener("touchmove", onTouchMove, {
57+
passive: false,
58+
capture: true,
59+
})
60+
61+
return () => {
62+
document.documentElement.removeEventListener("wheel", onWheel, {
63+
capture: true,
64+
} as EventListenerOptions)
65+
document.documentElement.removeEventListener("touchstart", onTouchStart, {
66+
capture: true,
67+
} as EventListenerOptions)
68+
document.documentElement.removeEventListener("touchmove", onTouchMove, {
69+
capture: true,
70+
} as EventListenerOptions)
71+
touchStartYById.clear()
72+
}
73+
}

0 commit comments

Comments
 (0)