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
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,21 @@ final class ViewportEntranceRuntime
* revision 7f4b5cc73eef0db4d61292eaa07ecd03c2d0e00b).
* Keep attribute poses, CSS-animation bindings and startup contexts together:
* the evidence is a replay contract, not an animation-provider heuristic.
*
* Evidence may carry `devices`: the device documents whose source plays the
* entrance. `$viewportQueries` is a JSON object of media queries keyed by
* device (the site's core viewport queries, the same ones block visibility
* uses); a device without a query matches the selected device document.
*/
public static function script(): string
public static function script(string $viewportQueries = '{}'): string
{
return str_replace(array('__ENTRANCE__', '__DEVICE_DOCUMENT__', '__ENTRANCE_LOSS__'), array(CaptureEvidence::VIEWPORT_ENTRANCE, CaptureEvidence::DEVICE_DOCUMENT, CaptureEvidence::VIEWPORT_ENTRANCE . '-loss'), <<<'JS'
return str_replace(array('__ENTRANCE__', '__DEVICE_DOCUMENT__', '__ENTRANCE_LOSS__', '__SELECTED_DOCUMENT__', '__VIEWPORT_QUERIES__'), array(CaptureEvidence::VIEWPORT_ENTRANCE, CaptureEvidence::DEVICE_DOCUMENT, CaptureEvidence::VIEWPORT_ENTRANCE . '-loss', CaptureEvidence::SELECTED_DOCUMENT, $viewportQueries), <<<'JS'
function replayViewportEntrances() {
const reduced = matchMedia('(prefers-reduced-motion: reduce)');
const viewportQueries = __VIEWPORT_QUERIES__;
const selected = document.documentElement.getAttribute('__SELECTED_DOCUMENT__');
const onDevice = (devices) => devices.some(device => typeof viewportQueries[device] === 'string'
? matchMedia(viewportQueries[device]).matches : selected === null || selected === device);
// Engines enumerate a keyframe's properties in different orders (Chromium
// changed it between releases), so the binding compares values, not order.
const signature = (value) => JSON.stringify(value, (_key, field) => field && typeof field === 'object' && !Array.isArray(field)
Expand Down Expand Up @@ -55,7 +64,9 @@ function replayViewportEntrances() {
element.setAttribute(name, value);
}
};
if (reduced.matches || !window.IntersectionObserver) {
// An entrance the source plays only on some devices shows its settled
// pose on the others.
if (reduced.matches || !window.IntersectionObserver || (Array.isArray(state.devices) && !onDevice(state.devices))) {
apply('after');
continue;
}
Expand Down
6 changes: 4 additions & 2 deletions php-transformer/src/WordPressSitePlan/WordPressSitePlan.php
Original file line number Diff line number Diff line change
Expand Up @@ -7144,8 +7144,10 @@ private static function bootstrap(array $assets, array $scripts = array(), array
}
foreach (array_merge($templates, $parts, $pages) as $document) {
if (!str_contains((string) ($document['canonical_block_markup'] ?? $document['block_markup'] ?? ''), CaptureEvidence::VIEWPORT_ENTRANCE)) continue;
$entranceScript = \Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\Generators\ViewportEntranceRuntime::script();
$lines[] = "add_action( 'wp_enqueue_scripts', static function (): void { wp_register_script( 'blocks-engine-viewport-entrances', false, array(), null, true ); wp_enqueue_script( 'blocks-engine-viewport-entrances' ); wp_add_inline_script( 'blocks-engine-viewport-entrances', " . var_export($entranceScript, true) . " ); } );";
// Device-scoped entrances resolve through the site's live core
// viewport queries, so they follow block visibility breakpoints.
$entranceScript = \Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\Generators\ViewportEntranceRuntime::script('__BLOCKS_ENGINE_VIEWPORT_QUERIES__');
$lines[] = "add_action( 'wp_enqueue_scripts', static function (): void { \$queries = array(); if ( method_exists( 'WP_Theme_JSON', 'get_viewport_media_queries' ) ) { foreach ( WP_Theme_JSON::get_viewport_media_queries( wp_get_global_settings( array( 'viewport' ) ), array( 'include_desktop' => true ) ) as \$state => \$query ) { \$queries[ ltrim( \$state, '@' ) ] = preg_replace( '/^@media\\s+/', '', \$query ); } } wp_register_script( 'blocks-engine-viewport-entrances', false, array(), null, true ); wp_enqueue_script( 'blocks-engine-viewport-entrances' ); wp_add_inline_script( 'blocks-engine-viewport-entrances', str_replace( '__BLOCKS_ENGINE_VIEWPORT_QUERIES__', (string) wp_json_encode( (object) \$queries ), " . var_export($entranceScript, true) . " ) ); } );";
break;
}
foreach (array_merge($templates, $parts, $pages) as $document) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import { execFileSync } from 'node:child_process';
import { chromium } from '../../tools/visual-parity/node_modules/playwright/index.mjs';

const root = new URL('../..', import.meta.url).pathname;
const runtime = execFileSync('php', ['-r', `require '${root}/vendor/autoload.php'; echo Automattic\\BlocksEngine\\PhpTransformer\\HtmlToBlocks\\Generators\\ViewportEntranceRuntime::script();`], { encoding: 'utf8' });
const state = repeat => JSON.stringify({ attributes: { class: { before: 'reveal', after: 'reveal in' }, style: { before: 'translate: 0 8px', after: null }, 'data-pose': { before: null, after: 'done' } }, threshold: [0.12], rootMargin: '0px 0px 0px 0px', repeat });
const html = `<style>.reveal{height:200px;opacity:0;transform:translateY(24px);transition:opacity .8s linear,transform .8s}.reveal.in{opacity:1;transform:none}</style><div style="height:1400px"></div>${[false, true].map((repeat, index) => `<section id="card${index}" class="wp-block-group reveal in" style="color: rgb(1, 2, 3)" data-blocks-engine-capture-viewport-entrance='${state(repeat)}'>Card</section>`).join('')}<div style="height:1400px"></div>`;
const runtime = execFileSync('php', ['-r', `require '${root}/vendor/autoload.php'; echo Automattic\\BlocksEngine\\PhpTransformer\\HtmlToBlocks\\Generators\\ViewportEntranceRuntime::script('{"mobile":"(width <= 750px)","desktop":"(width > 750px)"}');`], { encoding: 'utf8' });
const state = (repeat, devices) => JSON.stringify({ ...(devices ? { devices } : {}), attributes: { class: { before: 'reveal', after: 'reveal in' }, style: { before: 'translate: 0 8px', after: null }, 'data-pose': { before: null, after: 'done' } }, threshold: [0.12], rootMargin: '0px 0px 0px 0px', repeat });
const html = (devices) => `<style>.reveal{height:200px;opacity:0;transform:translateY(24px);transition:opacity .8s linear,transform .8s}.reveal.in{opacity:1;transform:none}</style><div style="height:1400px"></div>${[false, true].map((repeat, index) => `<section id="card${index}" class="wp-block-group reveal in" style="color: rgb(1, 2, 3)" data-blocks-engine-capture-viewport-entrance='${state(repeat, devices)}'>Card</section>`).join('')}<div style="height:1400px"></div>`;
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox'] });
try {
for (const mode of ['normal', 'reduced', 'no-observer', 'no-js']) {
for (const mode of ['normal', 'reduced', 'no-observer', 'no-js', 'other-device']) {
const page = await browser.newPage({ viewport: { width: 1440, height: 900 }, reducedMotion: mode === 'reduced' ? 'reduce' : 'no-preference', javaScriptEnabled: mode !== 'no-js' });
await page.setContent(html);
// At 1440px the desktop viewport query matches; a mobile-only entrance settles.
await page.setContent(html(mode === 'other-device' ? ['mobile'] : mode === 'normal' ? ['desktop'] : undefined));
if (mode === 'no-observer') await page.evaluate(() => { delete window.IntersectionObserver; });
if (mode !== 'no-js') await page.addScriptTag({ content: runtime });
const opacity = () => page.locator('#card0').evaluate(node => Number(getComputedStyle(node).opacity));
Expand Down
Loading