Skip to content
Closed
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
113 changes: 113 additions & 0 deletions apps/docsite/src/__tests__/hero-glow-pinning.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.

/**
* @file hero-glow-pinning.test.ts
* @input The home hero's HeroThemeReel source
* @output Guards both halves of the aurora glow's contract
* @position Regression test for the fidelity regression #5415 shipped, and for
* the #5392 property it must keep
*
* The glow has to satisfy two things at once, and #5415 traded one for the
* other in each direction:
*
* 1. It must stay PINNED while the hero is on screen. The blobs sit low in
* its 1050px box, so a glow that scrolls with the hero walks them up
* through the viewport and visibly warms the page mid-scroll.
* 2. It must be BOUNDED, so it can't paint into the strip an overscroll
* opens past the end of the page — the exposure that had the whole site
* suppressing overscroll, which costs pull-to-refresh (#5392).
*
* `position: fixed` gives (1) but not (2); an ordinary in-flow `absolute`
* gives (2) but not (1). A sticky, zero-height layer with the glow absolute
* inside it gives both, which is why the structural assertion below matters as
* much as the property one.
*/

import {describe, it, expect} from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import {fileURLToPath} from 'node:url';

const HERE = path.dirname(fileURLToPath(import.meta.url));
const SOURCE = fs.readFileSync(
path.join(
HERE,
'..',
'app',
'(site)',
'_landing',
'hero',
'HeroThemeReel.tsx',
),
'utf8',
);

/** The `{...}` body of one `stylex.create` entry, e.g. `backdropGlow`. */
function styleBlock(name: string): string {
const start = SOURCE.indexOf(`${name}: {`);
expect(start, `no \`${name}\` style in the source`).toBeGreaterThan(-1);
const open = SOURCE.indexOf('{', start);
let depth = 0;
for (let i = open; i < SOURCE.length; i++) {
if (SOURCE[i] === '{') {
depth++;
} else if (SOURCE[i] === '}' && --depth === 0) {
return SOURCE.slice(open, i + 1);
}
}
throw new Error(`unbalanced braces in \`${name}\``);
}

/** The value a StyleX property takes with no media query in play. */
function narrowValue(block: string, property: string): string {
const at = block.indexOf(`${property}: `);
expect(at, `no \`${property}\` declaration`).toBeGreaterThan(-1);
const rest = block.slice(at + property.length + 2);
if (!rest.startsWith('{')) {
return rest.slice(0, rest.search(/[,\n]/)).trim();
}
const arm = /default:\s*([^,\n]+)/.exec(rest.slice(0, rest.indexOf('}')));
expect(arm, `\`${property}\` has no default arm`).not.toBeNull();
return arm![1].trim();
}

describe('home hero — the aurora glow', () => {
it('is bounded at narrow widths, so overscroll can stay enabled (#5392)', () => {
// Unbounded === `fixed`: glued to the viewport for the whole document
// scroll, so still painting in the gap at the end of the page.
expect(narrowValue(styleBlock('backdropGlow'), 'position')).not.toBe(
"'fixed'",
);
});

it('is still pinned at narrow widths, not scrolling with the hero (regressed by #5415)', () => {
// Bounded is not enough on its own — an `absolute` glow parked in ordinary
// flow scrolls away. It has to be absolute against the STICKY layer, which
// is what keeps it visually pinned. Assert the containment structurally:
// the glow's element must be rendered inside the element carrying
// `pinLayer`, and `pinLayer` must actually be sticky.
expect(narrowValue(styleBlock('pinLayer'), 'position')).toBe("'sticky'");
expect(narrowValue(styleBlock('pinLayer'), 'height')).toBe('0');

// Slice out the pin layer's JSX children by indentation: the source is
// prettier-formatted, so the element's closing tag sits at the same column
// its opening tag does. (Tag-depth counting is the obvious alternative and
// gets this wrong — the glow is self-closing and never emits a `</div>`.)
const lines = SOURCE.split('\n');
const open = lines.findIndex(l =>
l.includes('stylex.props(styles.pinLayer)'),
);
expect(open, 'nothing renders `pinLayer`').toBeGreaterThan(-1);
const indent = lines[open].length - lines[open].trimStart().length;
const close = lines.findIndex(
(l, i) => i > open && l === `${' '.repeat(indent)}</div>`,
);
expect(close, 'could not find the pin layer’s closing tag').toBeGreaterThan(
-1,
);

expect(lines.slice(open + 1, close).join('\n')).toContain(
'styles.backdropGlow',
);
});
});
66 changes: 36 additions & 30 deletions apps/docsite/src/app/(site)/_landing/hero/HeroThemeReel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,17 @@ const styles = stylex.create({
},
height: 'auto',
},
// Sticky, zero-height layer hosting the overlap cards so they pin with the
// hero and don't intercept clicks.
cardsLayer: {
// The hero's pinned layer: the aurora glow rides here with the overlap
// cards, so both pin with the hero and don't intercept clicks.
//
// Sticky, not fixed. Sticky pins to the viewport exactly like fixed but is
// bounded by its container, so the layer stops existing on screen once the
// hero band has scrolled by and can never paint into the strip an overscroll
// opens past the end of the page. Zero height is what makes that work: a
// sticky box only stays pinned for the slack between its own height and its
// container's, so a zero-height layer gets the whole band to travel, and the
// visuals inside it are positioned absolutely against it.
pinLayer: {
position: 'sticky',
top: 'var(--appshell-header-height, 0px)',
height: 0,
Expand Down Expand Up @@ -169,26 +177,24 @@ const styles = stylex.create({
zIndex: 0,
},
// Blurred aurora glow — in the same 1200px box as the cards so blobs and
// cards stay aligned; pinned at >=1024px and scrolling away with the hero
// below that (see `position`). Capped to 100vw to avoid horizontal scroll.
// Blob centers sit under the card clusters; colors come from --aurora-* per
// slide.
// cards stay aligned; it rides pinLayer with them. Capped to 100vw to avoid
// horizontal scroll. Blob centers sit under the card clusters; colors come
// from --aurora-* per slide.
backdropGlow: {
// Desktop: fixed, part of the pin-and-cover effect alongside heroContent
// and the cards stage. Narrow: absolute within heroScope (position:
// relative), so it scrolls away with the hero instead of staying pinned
// for the whole page — a fixed glow below 1024px reached past the footer
// into the bottom-overscroll gap. That exposure is what the app-global
// `overscroll-behavior-y: none` in globals.css was suppressing, at the
// cost of pull-to-refresh on every route on mobile; bounding the glow
// here is what lets that rule scope to desktop widths (#5392).
// and the cards stage. Narrow: absolute against pinLayer, which is sticky
// and so pins this exactly as `fixed` did while the hero is on screen —
// but bounded, so the glow can't reach past the footer into the
// bottom-overscroll gap. That exposure is what the app-global
// `overscroll-behavior-y: none` was suppressing at the cost of
// pull-to-refresh on every mobile route; bounding the glow is what lets
// the rule scope to desktop widths (#5392).
position: {
default: 'absolute',
'@media (min-width: 1024px)': 'fixed',
},
// heroScope already starts below the header (it's the sibling after
// navBackdrop in document flow), so the absolute case needs no offset;
// only the fixed case has to clear the header itself.
// pinLayer already carries the header offset, so the absolute case needs
// none; only the fixed case has to clear the header itself.
top: {
default: 0,
'@media (min-width: 1024px)': 'var(--appshell-header-height, 0px)',
Expand Down Expand Up @@ -451,19 +457,19 @@ export function HeroReelCards() {
<Theme theme={active.theme} mode={effectiveMode(active, reel.userMode)}>
<div {...stylex.props(styles.themeFill)} aria-hidden="true" />
<div {...stylex.props(styles.navBackdrop)} aria-hidden="true" />
<div
aria-hidden="true"
{...stylex.props(
styles.backdropGlow,
dynamic.aurora(
active.aurora.left,
active.aurora.center,
active.aurora.right,
),
)}
/>
{/* Floating cards layer */}
<div {...stylex.props(styles.cardsLayer)}>
{/* Pinned layer: aurora glow underneath, floating cards over it. */}
<div {...stylex.props(styles.pinLayer)}>
<div
aria-hidden="true"
{...stylex.props(
styles.backdropGlow,
dynamic.aurora(
active.aurora.left,
active.aurora.center,
active.aurora.right,
),
)}
/>
<HeroFloatingCards content={active.content} mounted={shown} />
</div>
</Theme>
Expand Down
Loading