From 905fdd5ab0aa1966b01abe8277f2cf8be65d0a53 Mon Sep 17 00:00:00 2001 From: Dikran Samarjian Date: Fri, 11 Sep 2026 11:39:38 -0700 Subject: [PATCH 1/6] fix for mobile --- src/components/ScrollTrapDemo/index.tsx | 120 ++++++++++++------ .../ScrollTrapDemo/styles.module.css | 20 +++ 2 files changed, 104 insertions(+), 36 deletions(-) diff --git a/src/components/ScrollTrapDemo/index.tsx b/src/components/ScrollTrapDemo/index.tsx index 79071e81..f66778ca 100644 --- a/src/components/ScrollTrapDemo/index.tsx +++ b/src/components/ScrollTrapDemo/index.tsx @@ -5,25 +5,40 @@ import styles from "./styles.module.css"; type Example = "internalScroll" | "gestureLock" | "fixed"; const examples: Array<{ - description: string; + descriptions: { + desktop: string; + touch: string; + }; id: Example; label: string; }> = [ { - description: - "Start scrolling until you hit the trap, then hover over the app and scroll. The app captures the scroll, and the Reddit feed stops moving.", + descriptions: { + desktop: + "Start scrolling until you hit the trap, then hover over the app and scroll. The app captures the scroll, and the Reddit feed stops moving.", + touch: + "Swipe inside the app panel. The app creates a nested scroll area, so the surrounding feed can feel stuck until the gesture leaves the panel.", + }, id: "internalScroll", label: "Internal scroll trap", }, { - description: - "Start scrolling until you hit the trap, then hover over the app and scroll. The app captures the scroll even though no scrollbar is visible, and the Reddit feed stops moving.", + descriptions: { + desktop: + "Start scrolling until you hit the trap, then hover over the app and scroll. The app captures the scroll even though no scrollbar is visible, and the Reddit feed stops moving.", + touch: + "On touch screens, this example shows the rejected pattern without blocking this docs page. Full-surface gesture locks can capture vertical swipes even when no scrollbar is visible.", + }, id: "gestureLock", label: "No scrollbar trap", }, { - description: - "Start scrolling until you hit the app, then hover over it and scroll. The app does not capture the scroll, and the Reddit feed continues moving normally.", + descriptions: { + desktop: + "Start scrolling until you hit the app, then hover over it and scroll. The app does not capture the scroll, and the Reddit feed continues moving normally.", + touch: + "Swipe over the app. The app keeps vertical gestures available, so the feed continues moving normally.", + }, id: "fixed", label: "Feed stays scrollable", }, @@ -31,11 +46,14 @@ const examples: Array<{ export default function ScrollTrapDemo(): React.ReactElement { const [activeExample, setActiveExample] = useState("internalScroll"); + const isTouchDemo = useTouchDemo(); const internalScrollRef = useRef(null); const gestureTrapRef = useRef(null); const selectedExample = examples.find( (example) => example.id === activeExample, ); + const selectedDescription = + selectedExample?.descriptions[isTouchDemo ? "touch" : "desktop"]; useEffect(() => { if (activeExample === "internalScroll") { @@ -57,12 +75,14 @@ export default function ScrollTrapDemo(): React.ReactElement { return () => element.removeEventListener("wheel", onWheel); }; - const removeGestureTrap = addWheelTrap(gestureTrapRef.current); + const removeGestureTrap = isTouchDemo + ? undefined + : addWheelTrap(gestureTrapRef.current); return () => { removeGestureTrap?.(); }; - }, [activeExample]); + }, [activeExample, isTouchDemo]); return (
@@ -91,7 +111,7 @@ export default function ScrollTrapDemo(): React.ReactElement { role="tabpanel" aria-labelledby={`scroll-trap-tab-${activeExample}`} > -

{selectedExample?.description}

+

{selectedDescription}

@@ -107,7 +127,10 @@ export default function ScrollTrapDemo(): React.ReactElement { ) : null} {activeExample === "gestureLock" ? ( - + ) : null} {activeExample === "fixed" ? : null} @@ -125,6 +148,22 @@ export default function ScrollTrapDemo(): React.ReactElement { ); } +function useTouchDemo(): boolean { + const [isTouchDemo, setIsTouchDemo] = useState(false); + + useEffect(() => { + const mediaQuery = window.matchMedia("(hover: none), (pointer: coarse)"); + + const syncInputMode = () => setIsTouchDemo(mediaQuery.matches); + syncInputMode(); + + mediaQuery.addEventListener("change", syncInputMode); + return () => mediaQuery.removeEventListener("change", syncInputMode); + }, []); + + return isTouchDemo; +} + function PlainMockPost({ comments, label, @@ -263,35 +302,44 @@ function InternalScrollApp({ ); } -const GestureLockApp = React.forwardRef( - function GestureLockApp(_props, ref) { - return ( -
- -
-
-

No scrollbar, still trapped

-

- The surface is fixed, but it locks gestures across the whole - inline app. The feed cannot use the wheel or touch input. +const GestureLockApp = React.forwardRef< + HTMLDivElement, + { isTouchDemo: boolean } +>(function GestureLockApp({ isTouchDemo }, ref) { + return ( +

+ +
+
+

No scrollbar, still trapped

+

+ The surface is fixed, but it locks gestures across the whole inline + app. The feed cannot use the wheel or touch input. +

+ {isTouchDemo ? ( +

+ Mobile preview: rejected gesture lock shown without capturing this + page's swipe.

-
-
- Fixed canvas or game area -
-
- touch-action: none; overscroll-behavior: none; + ) : null} +
+
Fixed canvas or game area
+ touch-action: none; overscroll-behavior: none;
- ); - }, -); +
+ ); +}); function FixedApp() { return ( diff --git a/src/components/ScrollTrapDemo/styles.module.css b/src/components/ScrollTrapDemo/styles.module.css index 13c8f8c9..e90922f3 100644 --- a/src/components/ScrollTrapDemo/styles.module.css +++ b/src/components/ScrollTrapDemo/styles.module.css @@ -372,9 +372,25 @@ display: flex; justify-content: center; min-height: 19rem; + touch-action: pan-y; +} + +.gestureTrapBoard { touch-action: none; } +.mobileDemoNote { + background: #f6f7f8; + border: 1px solid #c8ced2; + border-radius: 8px; + color: #343a3d; + font-size: 0.9rem; + font-weight: 700; + margin: 0 0 1rem; + padding: 0.75rem; + text-align: left; +} + .previewCard { align-items: center; background: #ff4500; @@ -493,4 +509,8 @@ .centerPanel { margin-top: 0; } + + .gestureTrapBoard { + touch-action: pan-y; + } } From f33889bfc0de0c361fa317ae0122007c7d40bcfe Mon Sep 17 00:00:00 2001 From: Dikran Samarjian Date: Fri, 11 Sep 2026 12:08:15 -0700 Subject: [PATCH 2/6] design tweaks --- src/components/ScrollTrapDemo/index.tsx | 6 --- .../ScrollTrapDemo/styles.module.css | 44 ++++++++++++++++--- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/src/components/ScrollTrapDemo/index.tsx b/src/components/ScrollTrapDemo/index.tsx index f66778ca..d3e1fd0d 100644 --- a/src/components/ScrollTrapDemo/index.tsx +++ b/src/components/ScrollTrapDemo/index.tsx @@ -55,12 +55,6 @@ export default function ScrollTrapDemo(): React.ReactElement { const selectedDescription = selectedExample?.descriptions[isTouchDemo ? "touch" : "desktop"]; - useEffect(() => { - if (activeExample === "internalScroll") { - internalScrollRef.current?.focus({ preventScroll: true }); - } - }, [activeExample]); - useEffect(() => { const addWheelTrap = (element: HTMLDivElement | null) => { if (!element) { diff --git a/src/components/ScrollTrapDemo/styles.module.css b/src/components/ScrollTrapDemo/styles.module.css index e90922f3..eeaaf4bd 100644 --- a/src/components/ScrollTrapDemo/styles.module.css +++ b/src/components/ScrollTrapDemo/styles.module.css @@ -489,25 +489,57 @@ @media (max-width: 640px) { .feedViewport { - height: 38rem; - max-height: 76vh; + height: auto; + max-height: none; + overflow-y: visible; } .feedCanvas { - transform: scale(0.8); - width: 125%; + transform: none; + width: 100%; } .appSurface { - min-height: 36rem; + min-height: auto; + padding: 0.75rem; } .plainAppSurface { - min-height: 36rem; + min-height: 15rem; } .centerPanel { margin-top: 0; + padding: 0.85rem; + } + + .centerPanel h4 { + font-size: 1.1rem; + } + + .centerPanel p { + font-size: 0.9rem; + } + + .appBody { + padding: 1rem 0 0; + } + + .previewBoard { + min-height: 8.5rem; + } + + .previewCard { + min-height: 4.5rem; + } + + .mobileDemoNote { + font-size: 0.85rem; + padding: 0.65rem; + } + + .plainAppGrid span { + min-height: 5rem; } .gestureTrapBoard { From 75a2465574ded7405111724cfd55fb9745f80b6c Mon Sep 17 00:00:00 2001 From: Dikran Samarjian Date: Fri, 11 Sep 2026 12:21:06 -0700 Subject: [PATCH 3/6] one more redesign --- src/components/ScrollTrapDemo/index.tsx | 171 +++++++++++------- .../ScrollTrapDemo/styles.module.css | 95 ++++++++-- 2 files changed, 193 insertions(+), 73 deletions(-) diff --git a/src/components/ScrollTrapDemo/index.tsx b/src/components/ScrollTrapDemo/index.tsx index d3e1fd0d..98dc3c18 100644 --- a/src/components/ScrollTrapDemo/index.tsx +++ b/src/components/ScrollTrapDemo/index.tsx @@ -4,6 +4,38 @@ import styles from "./styles.module.css"; type Example = "internalScroll" | "gestureLock" | "fixed"; +const mobileExamples: Record< + Example, + { + code: string; + details: string; + outcome: string; + title: string; + } +> = { + fixed: { + code: "touch-action: pan-y;", + details: + "Use taps, buttons, and bounded controls in inline mode. Save full-screen drag, zoom, and long flows for expanded mode.", + outcome: "Allowed: vertical swipes continue scrolling the feed.", + title: "Feed stays scrollable", + }, + gestureLock: { + code: "touch-action: none;", + details: + "A fixed canvas, game board, map, or gesture layer can still capture vertical swipes even when the app has no visible scrollbar.", + outcome: "Rejected: the app owns the gesture instead of the feed.", + title: "No scrollbar can still trap swipes", + }, + internalScroll: { + code: "overflow-y: auto;", + details: + "Inline apps should not contain their own vertical scrolling areas. Users can get stuck moving the app panel instead of the feed.", + outcome: "Rejected: nested vertical scroll competes with the feed.", + title: "Internal scrolling competes with the feed", + }, +}; + const examples: Array<{ descriptions: { desktop: string; @@ -107,36 +139,37 @@ export default function ScrollTrapDemo(): React.ReactElement { >

{selectedDescription}

-
-
- - - {activeExample === "internalScroll" ? ( - - ) : null} + {isTouchDemo ? ( + + ) : ( +
+
+ + + {activeExample === "internalScroll" ? ( + + ) : null} - {activeExample === "gestureLock" ? ( - - ) : null} + {activeExample === "gestureLock" ? ( + + ) : null} - {activeExample === "fixed" ? : null} - - + {activeExample === "fixed" ? : null} + + +
-
+ )}
); @@ -158,6 +191,29 @@ function useTouchDemo(): boolean { return isTouchDemo; } +function MobileExample({ activeExample }: { activeExample: Example }) { + const example = mobileExamples[activeExample]; + const isAccepted = activeExample === "fixed"; + + return ( +
+
+ + {isAccepted ? "Acceptable" : "Rejected"} + +

{example.title}

+
+

{example.details}

+ {example.outcome} + {example.code} +
+ ); +} + function PlainMockPost({ comments, label, @@ -296,44 +352,37 @@ function InternalScrollApp({ ); } -const GestureLockApp = React.forwardRef< - HTMLDivElement, - { isTouchDemo: boolean } ->(function GestureLockApp({ isTouchDemo }, ref) { - return ( -
- -
-
-

No scrollbar, still trapped

-

- The surface is fixed, but it locks gestures across the whole inline - app. The feed cannot use the wheel or touch input. -

- {isTouchDemo ? ( -

- Mobile preview: rejected gesture lock shown without capturing this - page's swipe. +const GestureLockApp = React.forwardRef( + function GestureLockApp(_props, ref) { + return ( +

+ +
+
+

No scrollbar, still trapped

+

+ The surface is fixed, but it locks gestures across the whole + inline app. The feed cannot use the wheel or touch input.

- ) : null} -
-
Fixed canvas or game area
+
+
+ Fixed canvas or game area +
+
+ touch-action: none; overscroll-behavior: none;
- touch-action: none; overscroll-behavior: none;
-
- ); -}); + ); + }, +); function FixedApp() { return ( diff --git a/src/components/ScrollTrapDemo/styles.module.css b/src/components/ScrollTrapDemo/styles.module.css index eeaaf4bd..6aebea30 100644 --- a/src/components/ScrollTrapDemo/styles.module.css +++ b/src/components/ScrollTrapDemo/styles.module.css @@ -379,18 +379,6 @@ touch-action: none; } -.mobileDemoNote { - background: #f6f7f8; - border: 1px solid #c8ced2; - border-radius: 8px; - color: #343a3d; - font-size: 0.9rem; - font-weight: 700; - margin: 0 0 1rem; - padding: 0.75rem; - text-align: left; -} - .previewCard { align-items: center; background: #ff4500; @@ -487,7 +475,86 @@ width: 1rem; } +.mobileExample { + background: var(--demo-post-bg); + border: 1px solid var(--demo-border); + border-left: 4px solid #ff4500; + border-radius: 8px; + color: var(--demo-text); + display: none; + margin: 0 auto; + max-width: 740px; + padding: 1rem; +} + +.mobileExampleHeader { + align-items: flex-start; + display: flex; + flex-direction: column; + gap: 0.7rem; + margin-bottom: 0.7rem; +} + +.mobileExample h4 { + font-size: 1.25rem; + line-height: 1.25; + margin: 0; +} + +.mobileExample p { + color: var(--demo-muted-text); + line-height: 1.5; + margin: 0 0 0.85rem; +} + +.mobileExample strong { + display: block; + line-height: 1.35; + margin-bottom: 0.85rem; +} + +.mobileExample code { + display: block; + font-size: 0.85rem; + overflow-wrap: anywhere; + padding: 0.55rem 0.65rem; +} + +.mobileAcceptedBadge, +.mobileRejectedBadge { + border-radius: 999px; + color: #111111; + font-size: 0.72rem; + font-weight: 800; + padding: 0.25rem 0.6rem; + text-transform: uppercase; +} + +.mobileAcceptedBadge { + background: #caf4d0; +} + +.mobileRejectedBadge { + background: #ffd1c2; +} + @media (max-width: 640px) { + .tabs { + gap: 0.5rem; + padding: 0.75rem; + } + + .tab, + .activeTab { + font-size: 0.88rem; + min-height: 2.35rem; + padding: 0.35rem 0.7rem; + } + + .panel { + padding: 0.85rem; + } + .feedViewport { height: auto; max-height: none; @@ -545,4 +612,8 @@ .gestureTrapBoard { touch-action: pan-y; } + + .mobileExample { + display: block; + } } From a93a46a6488f4469e8f9733f7a7e683dbc9b29f4 Mon Sep 17 00:00:00 2001 From: Dikran Samarjian Date: Fri, 11 Sep 2026 12:39:34 -0700 Subject: [PATCH 4/6] minimal demo for mobile --- src/components/ScrollTrapDemo/index.tsx | 83 ++++++++++--- .../ScrollTrapDemo/styles.module.css | 117 ++++++++++++------ 2 files changed, 149 insertions(+), 51 deletions(-) diff --git a/src/components/ScrollTrapDemo/index.tsx b/src/components/ScrollTrapDemo/index.tsx index 98dc3c18..d04bf675 100644 --- a/src/components/ScrollTrapDemo/index.tsx +++ b/src/components/ScrollTrapDemo/index.tsx @@ -194,23 +194,78 @@ function useTouchDemo(): boolean { function MobileExample({ activeExample }: { activeExample: Example }) { const example = mobileExamples[activeExample]; const isAccepted = activeExample === "fixed"; + const gestureTrapRef = useRef(null); + + useEffect(() => { + if (activeExample !== "gestureLock") { + return undefined; + } + + const element = gestureTrapRef.current; + if (!element) { + return undefined; + } + + const stopScroll = (event: TouchEvent | WheelEvent) => { + event.preventDefault(); + }; + + element.addEventListener("touchmove", stopScroll, { passive: false }); + element.addEventListener("wheel", stopScroll, { passive: false }); + + return () => { + element.removeEventListener("touchmove", stopScroll); + element.removeEventListener("wheel", stopScroll); + }; + }, [activeExample]); return ( -
-
- - {isAccepted ? "Acceptable" : "Rejected"} - -

{example.title}

+
+ +

Daily Game #116

+ Daily Game + +
+ +
+

{example.title}

+
+

{example.details}

+ {example.outcome} +
+ {activeExample === "internalScroll" ? ( +
+ Swipe this nested list + + + + +
+ ) : null} + + {activeExample === "gestureLock" ? ( +
+ Swipe this fixed surface +
+ ) : null} + + {activeExample === "fixed" ? ( +
+ Swipe here; the page should keep moving +
+ ) : null} +
+ {example.code}
-

{example.details}

- {example.outcome} - {example.code} -
+ + + ); } diff --git a/src/components/ScrollTrapDemo/styles.module.css b/src/components/ScrollTrapDemo/styles.module.css index 6aebea30..cf77722f 100644 --- a/src/components/ScrollTrapDemo/styles.module.css +++ b/src/components/ScrollTrapDemo/styles.module.css @@ -475,67 +475,115 @@ width: 1rem; } -.mobileExample { - background: var(--demo-post-bg); - border: 1px solid var(--demo-border); - border-left: 4px solid #ff4500; - border-radius: 8px; - color: var(--demo-text); +.mobilePost { display: none; margin: 0 auto; max-width: 740px; - padding: 1rem; +} + +.mobileAppSurface { + background: #ffffff; + border-radius: 16px; + color: #111111; + overflow: hidden; + padding: 0.85rem; +} + +.mobileRejectedSurface { + outline: 3px solid #ff4500; +} + +.mobileAcceptedSurface { + outline: 3px solid #168a5b; } .mobileExampleHeader { - align-items: flex-start; - display: flex; - flex-direction: column; - gap: 0.7rem; - margin-bottom: 0.7rem; + margin: 1rem 0 0.7rem; } -.mobileExample h4 { - font-size: 1.25rem; +.mobileExampleHeader h4 { + font-size: 1.1rem; line-height: 1.25; margin: 0; } -.mobileExample p { - color: var(--demo-muted-text); +.mobileAppSurface p { + color: #343a3d; line-height: 1.5; margin: 0 0 0.85rem; } -.mobileExample strong { +.mobileAppSurface strong { display: block; line-height: 1.35; margin-bottom: 0.85rem; } -.mobileExample code { - display: block; - font-size: 0.85rem; - overflow-wrap: anywhere; - padding: 0.55rem 0.65rem; +.mobileSwipeDemo { + margin: 0 0 0.85rem; } -.mobileAcceptedBadge, -.mobileRejectedBadge { - border-radius: 999px; +.mobileNestedScroller, +.mobileGestureSurface, +.mobilePassThroughSurface { + border: 2px solid #111111; + border-radius: 8px; +} + +.mobileNestedScroller { + display: flex; + flex-direction: column; + gap: 0.65rem; + max-height: 9rem; + overscroll-behavior: contain; + overflow-y: auto; + padding: 0.75rem; +} + +.mobileNestedScroller span { + color: #343a3d; + font-weight: 800; +} + +.mobileNestedScroller button { + background: #ff4500; + border: 2px solid #111111; + border-radius: 8px; color: #111111; - font-size: 0.72rem; + font: inherit; font-weight: 800; - padding: 0.25rem 0.6rem; - text-transform: uppercase; + min-height: 3.25rem; + padding: 0.7rem; } -.mobileAcceptedBadge { +.mobileGestureSurface, +.mobilePassThroughSurface { + align-items: center; + display: flex; + font-weight: 800; + justify-content: center; + min-height: 7rem; + padding: 1rem; + text-align: center; +} + +.mobileGestureSurface { + background: #ff4500; + color: #111111; + touch-action: none; +} + +.mobilePassThroughSurface { background: #caf4d0; + color: #111111; + touch-action: pan-y; } -.mobileRejectedBadge { - background: #ffd1c2; +.mobileAppSurface code { + display: block; + font-size: 0.85rem; + overflow-wrap: anywhere; + padding: 0.55rem 0.65rem; } @media (max-width: 640px) { @@ -600,11 +648,6 @@ min-height: 4.5rem; } - .mobileDemoNote { - font-size: 0.85rem; - padding: 0.65rem; - } - .plainAppGrid span { min-height: 5rem; } @@ -613,7 +656,7 @@ touch-action: pan-y; } - .mobileExample { + .mobilePost { display: block; } } From 6035b4cccf0f86f4b7da56a188c161b503e4828b Mon Sep 17 00:00:00 2001 From: Dikran Samarjian Date: Fri, 11 Sep 2026 13:03:11 -0700 Subject: [PATCH 5/6] consolidate instructions into post --- src/components/ScrollTrapDemo/index.tsx | 56 ++++--------------- .../ScrollTrapDemo/styles.module.css | 7 --- 2 files changed, 12 insertions(+), 51 deletions(-) diff --git a/src/components/ScrollTrapDemo/index.tsx b/src/components/ScrollTrapDemo/index.tsx index d04bf675..cbddc0f9 100644 --- a/src/components/ScrollTrapDemo/index.tsx +++ b/src/components/ScrollTrapDemo/index.tsx @@ -15,62 +15,37 @@ const mobileExamples: Record< > = { fixed: { code: "touch-action: pan-y;", - details: - "Use taps, buttons, and bounded controls in inline mode. Save full-screen drag, zoom, and long flows for expanded mode.", - outcome: "Allowed: vertical swipes continue scrolling the feed.", + details: "Swipe the green surface. The page should keep moving.", + outcome: "Acceptable: vertical swipes stay with the feed.", title: "Feed stays scrollable", }, gestureLock: { code: "touch-action: none;", - details: - "A fixed canvas, game board, map, or gesture layer can still capture vertical swipes even when the app has no visible scrollbar.", - outcome: "Rejected: the app owns the gesture instead of the feed.", + details: "Swipe the orange surface. It captures the gesture.", + outcome: "Rejected: no scrollbar, but still trapped.", title: "No scrollbar can still trap swipes", }, internalScroll: { code: "overflow-y: auto;", - details: - "Inline apps should not contain their own vertical scrolling areas. Users can get stuck moving the app panel instead of the feed.", - outcome: "Rejected: nested vertical scroll competes with the feed.", + details: "Swipe the mini list. It scrolls inside the post.", + outcome: "Rejected: nested scrolling competes with the feed.", title: "Internal scrolling competes with the feed", }, }; const examples: Array<{ - descriptions: { - desktop: string; - touch: string; - }; id: Example; label: string; }> = [ { - descriptions: { - desktop: - "Start scrolling until you hit the trap, then hover over the app and scroll. The app captures the scroll, and the Reddit feed stops moving.", - touch: - "Swipe inside the app panel. The app creates a nested scroll area, so the surrounding feed can feel stuck until the gesture leaves the panel.", - }, id: "internalScroll", label: "Internal scroll trap", }, { - descriptions: { - desktop: - "Start scrolling until you hit the trap, then hover over the app and scroll. The app captures the scroll even though no scrollbar is visible, and the Reddit feed stops moving.", - touch: - "On touch screens, this example shows the rejected pattern without blocking this docs page. Full-surface gesture locks can capture vertical swipes even when no scrollbar is visible.", - }, id: "gestureLock", label: "No scrollbar trap", }, { - descriptions: { - desktop: - "Start scrolling until you hit the app, then hover over it and scroll. The app does not capture the scroll, and the Reddit feed continues moving normally.", - touch: - "Swipe over the app. The app keeps vertical gestures available, so the feed continues moving normally.", - }, id: "fixed", label: "Feed stays scrollable", }, @@ -81,11 +56,6 @@ export default function ScrollTrapDemo(): React.ReactElement { const isTouchDemo = useTouchDemo(); const internalScrollRef = useRef(null); const gestureTrapRef = useRef(null); - const selectedExample = examples.find( - (example) => example.id === activeExample, - ); - const selectedDescription = - selectedExample?.descriptions[isTouchDemo ? "touch" : "desktop"]; useEffect(() => { const addWheelTrap = (element: HTMLDivElement | null) => { @@ -137,8 +107,6 @@ export default function ScrollTrapDemo(): React.ReactElement { role="tabpanel" aria-labelledby={`scroll-trap-tab-${activeExample}`} > -

{selectedDescription}

- {isTouchDemo ? ( ) : ( @@ -379,8 +347,8 @@ function InternalScrollApp({

Internal app scroll blocks the feed

- The app contains its own scrolling panel. When that panel reaches an - edge, the Reddit feed still cannot continue scrolling. + Try it: hover over this nested list and scroll. The app captures the + scroll instead of letting the feed keep moving.

(

No scrollbar, still trapped

- The surface is fixed, but it locks gestures across the whole - inline app. The feed cannot use the wheel or touch input. + Try it: hover over this fixed surface and scroll. No scrollbar + appears, but the app still blocks the feed.

Inline lets the feed scroll

- The inline view fits in the post. It uses taps or buttons for - interaction and allows vertical feed scrolling. + Try it: hover over this preview and scroll. The app leaves vertical + scrolling with the feed.

diff --git a/src/components/ScrollTrapDemo/styles.module.css b/src/components/ScrollTrapDemo/styles.module.css index cf77722f..d3e7bce3 100644 --- a/src/components/ScrollTrapDemo/styles.module.css +++ b/src/components/ScrollTrapDemo/styles.module.css @@ -73,13 +73,6 @@ padding: 1rem; } -.instructions { - color: var(--demo-muted-text); - font-size: 0.9rem; - margin: 0 auto 0.75rem; - max-width: 740px; -} - .feedViewport { background: var(--demo-surface-bg); border: 1px solid var(--demo-border); From 7b0a1ff3d703a812fc8c54424fdcf790f2f39504 Mon Sep 17 00:00:00 2001 From: Dikran Samarjian Date: Fri, 11 Sep 2026 13:22:06 -0700 Subject: [PATCH 6/6] final touches for this demo both mobile and web --- src/components/ScrollTrapDemo/index.tsx | 72 +++------- .../ScrollTrapDemo/styles.module.css | 127 ++++++------------ 2 files changed, 65 insertions(+), 134 deletions(-) diff --git a/src/components/ScrollTrapDemo/index.tsx b/src/components/ScrollTrapDemo/index.tsx index cbddc0f9..1295fad0 100644 --- a/src/components/ScrollTrapDemo/index.tsx +++ b/src/components/ScrollTrapDemo/index.tsx @@ -4,6 +4,15 @@ import styles from "./styles.module.css"; type Example = "internalScroll" | "gestureLock" | "fixed"; +const desktopInstructions: Record = { + fixed: + "Hover over the inline app and scroll. The page keeps moving normally.", + gestureLock: + "Hover over the fixed app surface and scroll. The page stops even though there is no scrollbar.", + internalScroll: + "Hover over the nested list and scroll. The app captures the scroll inside the post.", +}; + const mobileExamples: Record< Example, { @@ -107,17 +116,17 @@ export default function ScrollTrapDemo(): React.ReactElement { role="tabpanel" aria-labelledby={`scroll-trap-tab-${activeExample}`} > + {!isTouchDemo ? ( +

+ {desktopInstructions[activeExample]} +

+ ) : null} + {isTouchDemo ? ( ) : (
- {activeExample === "internalScroll" ? ( @@ -129,12 +138,6 @@ export default function ScrollTrapDemo(): React.ReactElement { {activeExample === "fixed" ? : null} -
)} @@ -237,39 +240,6 @@ function MobileExample({ activeExample }: { activeExample: Example }) { ); } -function PlainMockPost({ - comments, - label, - title, - votes, -}: { - comments: string; - label: string; - title: string; - votes: string; -}) { - return ( -
- -

{title}

- Community App -
-
- {label} - Preview -
-
- - - - -
-
- -
- ); -} - function MockPost({ children }: { children: React.ReactNode }) { return (
@@ -347,8 +317,8 @@ function InternalScrollApp({

Internal app scroll blocks the feed

- Try it: hover over this nested list and scroll. The app captures the - scroll instead of letting the feed keep moving. + The app contains its own scrolling panel. When that panel reaches an + edge, the Reddit feed still cannot continue scrolling.

(

No scrollbar, still trapped

- Try it: hover over this fixed surface and scroll. No scrollbar - appears, but the app still blocks the feed. + The surface is fixed, but it locks gestures across the whole + inline app. The feed cannot use the wheel or touch input.

Inline lets the feed scroll

- Try it: hover over this preview and scroll. The app leaves vertical - scrolling with the feed. + The inline view fits in the post. It uses taps or buttons for + interaction and allows vertical feed scrolling.

diff --git a/src/components/ScrollTrapDemo/styles.module.css b/src/components/ScrollTrapDemo/styles.module.css index d3e7bce3..8bc7d7ad 100644 --- a/src/components/ScrollTrapDemo/styles.module.css +++ b/src/components/ScrollTrapDemo/styles.module.css @@ -73,23 +73,25 @@ padding: 1rem; } +.instructions { + color: var(--demo-muted-text); + font-size: 0.9rem; + margin: 0 auto 0.75rem; + max-width: 740px; +} + .feedViewport { background: var(--demo-surface-bg); border: 1px solid var(--demo-border); border-radius: 8px; - height: 760px; margin: 0 auto; max-width: 740px; - overflow-x: hidden; - overflow-y: auto; padding: 0.75rem; width: 100%; } .feedCanvas { - transform: scale(0.76); - transform-origin: top left; - width: 131.6%; + width: 100%; } .post { @@ -164,68 +166,6 @@ padding: 1rem; } -.plainPost { - opacity: 0.92; -} - -.plainPost .postTitle { - font-size: 1.15rem; -} - -.plainPost .flair { - margin-bottom: 0.65rem; -} - -.plainPost .postFooter { - padding-top: 0.65rem; -} - -.plainAppSurface { - background: #f6f7f8; - border: 1px solid #d7dde1; - border-radius: 16px; - color: #1a1a1b; - display: flex; - flex-direction: column; - min-height: 657px; - overflow: hidden; - padding: 0.8rem; -} - -.plainAppHeader { - align-items: center; - border-bottom: 1px solid #d7dde1; - display: flex; - justify-content: space-between; - margin: -0.8rem -0.8rem 0.8rem; - padding: 0.8rem; -} - -.plainAppHeader strong { - font-size: 1rem; -} - -.plainAppHeader span { - color: #667178; - font-size: 0.85rem; - font-weight: 700; - text-transform: uppercase; -} - -.plainAppGrid { - display: grid; - flex: 1; - gap: 0.6rem; - grid-template-columns: repeat(2, minmax(0, 1fr)); -} - -.plainAppGrid span { - background: #ffffff; - border: 1px solid #d7dde1; - border-radius: 8px; - min-height: 10rem; -} - .rejectedSurface { outline: 3px solid #ff4500; } @@ -596,15 +536,44 @@ padding: 0.85rem; } - .feedViewport { - height: auto; - max-height: none; - overflow-y: visible; + .mobilePost .postMeta { + align-items: center; + column-gap: 0.55rem; + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + row-gap: 0.2rem; + } + + .mobilePost .avatar { + grid-column: 1; + grid-row: 1 / span 2; + } + + .mobilePost .postMeta strong { + grid-column: 2; + grid-row: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .mobilePost .appBadge { + grid-column: 2; + grid-row: 2; + justify-self: start; } - .feedCanvas { - transform: none; - width: 100%; + .mobilePost .postMeta > span:nth-of-type(3) { + grid-column: 2; + grid-row: 2; + margin-left: 3.2rem; + } + + .mobilePost .moreButton { + grid-column: 3; + grid-row: 1 / span 2; + margin-left: 0; } .appSurface { @@ -612,10 +581,6 @@ padding: 0.75rem; } - .plainAppSurface { - min-height: 15rem; - } - .centerPanel { margin-top: 0; padding: 0.85rem; @@ -641,10 +606,6 @@ min-height: 4.5rem; } - .plainAppGrid span { - min-height: 5rem; - } - .gestureTrapBoard { touch-action: pan-y; }