Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/validate-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ jobs:
run: npm run test:nav-lazy-buckets
- name: Test code runtime gating
run: npm run test:has-code-blocks
- name: Test landing-page heroes clip without trapping the first swipe
run: npm run test:hero-scroll
- name: Test docs page weight (lazy images, trimmed Prism, on-demand highlighting, head)
run: npm run test:docs-page-weight
- name: Cache generated files
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"test:lazy-askai": "node --test tests/lazy-askai/*.test.js",
"test:nav-lazy-buckets": "node --test tests/nav-lazy-buckets/*.test.js",
"test:has-code-blocks": "node --test tests/has-code-blocks/*.test.js",
"test:hero-scroll": "node --test tests/hero-scroll/*.test.js",
"test:docs-page-weight": "node --test tests/docs-page-weight/*.test.js",
"test:all": "npm run test:playground && npm run test:interactive && npm run test:negative-cache && npm run test:head-meta && npm run test:property-tooltips && npm run test:signin-nudge && npm run test:chat-panel-navigation && npm run test:toc-collapsible && npm run test:toc-active-anchor && npm run test:nav-scroll && npm run test:kapa-source-groups && npm run test:tooltip-touch && npm run test:tooltip-single-open && npm run test:lazy-askai && npm run test:nav-lazy-buckets && npm run test:has-code-blocks && npm run test:docs-page-weight && npm run test:anon-quota",
"build:wasm": "cd blobl-editor/wasm && GOOS=js GOARCH=wasm go build -o ../../src/static/blobl.wasm .",
Expand Down
5 changes: 5 additions & 0 deletions src/css/data-platform.css
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,12 @@ html[data-theme="dark"] .data-platform.article {
/* ---- Hero Section ---- */
.dp-hero {
position: relative;

/* clip, not hidden: see .home-hero in home.css. An overflowing overflow:hidden
hero eats the first swipe on a phone because WebKit latches the touch to a
box the user can't scroll. */
overflow: hidden;
overflow: clip;
background: linear-gradient(180deg, #0e1a33 0%, #1a2a52 100%);
color: #fff;
padding: 40px 56px 32px;
Expand Down
10 changes: 10 additions & 0 deletions src/css/home.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,17 @@ main.article.home > .content {
/* Dark Hero Section */
.home-hero {
position: relative;

/* The glows are bigger than the hero and sit partly outside it, so the hero
has to clip. It must not become a SCROLL CONTAINER while doing it: this
section is the whole first screen on a phone, and in WebKit a touch that
starts inside an overflow:hidden box with overflowing content is latched to
that box. Because the box can't be scrolled by the user, the swipe did
nothing and the home page looked frozen until you tapped first. clip does
the same clipping without a scroll container, so the swipe reaches the page.
hidden stays first as the fallback for Safari < 16, which ignores clip. */
overflow: hidden;
overflow: clip;
background: #0b1430;
color: #fff;
padding: 80px 40px 72px;
Expand Down
5 changes: 5 additions & 0 deletions src/css/labs-home.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@
padding: 72px 5vw 56px;
text-align: center;
background: linear-gradient(180deg, #0b1430 0%, #10173a 100%);

/* clip, not hidden: see .home-hero in home.css. An overflowing overflow:hidden
hero eats the first swipe on a phone because WebKit latches the touch to a
box the user can't scroll. */
overflow: hidden;
overflow: clip;
}

.labs-hero-bg {
Expand Down
82 changes: 82 additions & 0 deletions tests/hero-scroll/hero-scroll.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* A full-bleed hero clips decorative glows that are deliberately bigger than
* the hero box. Clipping with `overflow: hidden` makes the hero a scroll
* container, and in WebKit a touch that starts inside a scroll container with
* overflowing content is latched to that container: because a hidden box can't
* be scrolled by the user, the swipe does nothing and never reaches the page.
* The hero IS the first screen on a phone, so the landing page looked frozen on
* load until you tapped somewhere first. `overflow: clip` clips identically
* without creating a scroll container.
*
* These are source assertions on purpose: the failure only shows up with real
* touch input on a real WebKit build, which no headless runner here can send.
* What is checkable, and what regressed, is the declaration itself.
*/
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const test = require('node:test')

const ROOT = path.join(__dirname, '../..')

// Every hero that paints decorative layers outside its own box and therefore
// has to clip. .ch3-hero (component-home-v3.css) is absent by design: it clips
// in a separate .ch3-hero-bg layer that is pointer-events: none, so no touch
// ever lands in it.
const HEROES = [
['src/css/home.css', '.home-hero'],
['src/css/data-platform.css', '.dp-hero'],
['src/css/labs-home.css', '.labs-hero'],
]

const read = (file) => fs.readFileSync(path.join(ROOT, file), 'utf8')
const stripComments = (css) => css.replace(/\/\*[\s\S]*?\*\//g, '')

// The declarations of the first rule whose selector list contains exactly this
// selector. Naive on purpose: a selector chunk keeps only what follows the last
// brace, which drops any enclosing at-rule prelude.
function ruleBody (css, selector) {
const chunks = stripComments(css).split('}')
for (const chunk of chunks) {
const brace = chunk.lastIndexOf('{')
if (brace < 0) continue
const selectors = chunk.slice(0, brace).split('{').pop().split(',').map((s) => s.trim())
if (selectors.includes(selector)) return chunk.slice(brace + 1)
}
return null
}

const overflowValues = (body) =>
[...body.matchAll(/(?:^|;)\s*overflow\s*:\s*([^;]+)/g)].map((m) => m[1].trim())

for (const [file, selector] of HEROES) {
test(`${selector} clips without becoming a scroll container`, () => {
const body = ruleBody(read(file), selector)
assert.ok(body, `${selector} has a rule in ${file}`)
const values = overflowValues(body)
assert.ok(values.length, `${selector} sets overflow`)
assert.equal(values.at(-1), 'clip', `${selector} ends on overflow: clip, not ${values.at(-1)}`)
assert.equal(values.at(-2), 'hidden', `${selector} keeps overflow: hidden ahead of clip for Safari < 16`)
})
}

test('no landing-page hero goes back to clipping with overflow: hidden alone', () => {
const files = ['src/css/home.css', 'src/css/data-platform.css', 'src/css/labs-home.css', 'src/css/component-home-v3.css']
const offenders = []
for (const file of files) {
const chunks = stripComments(read(file)).split('}')
for (const chunk of chunks) {
const brace = chunk.lastIndexOf('{')
if (brace < 0) continue
const selectors = chunk.slice(0, brace).split('{').pop().split(',').map((s) => s.trim())
// A hero box itself, not its inner layers (-hero-bg, -hero-inner, ...).
const heroBox = selectors.some((s) => /(^|[\s>])[.#][a-z0-9-]*-hero$/.test(s))
if (!heroBox) continue
const values = overflowValues(chunk.slice(brace + 1))
// visible and clip are both fine: neither makes a scroll container.
const last = values.at(-1)
if (/^(hidden|auto|scroll)\b/.test(last || '')) offenders.push(`${file}: ${selectors.join(', ')} -> overflow: ${last}`)
}
}
assert.deepEqual(offenders, [], 'heroes clip with overflow: clip so a first swipe still scrolls the page')
})
Loading