diff --git a/entry_types/scrolled/package/spec/frontend/Layout-spec.js b/entry_types/scrolled/package/spec/frontend/Layout-spec.js index 4f3f0a1b08..097126b490 100644 --- a/entry_types/scrolled/package/spec/frontend/Layout-spec.js +++ b/entry_types/scrolled/package/spec/frontend/Layout-spec.js @@ -10,6 +10,7 @@ import twoColumnStyles from 'frontend/layouts/TwoColumn.module.css'; import {widthName} from 'frontend/layouts/widths'; import {renderInEntry} from 'testHelpers'; +import {fakeResizeObserver} from 'support/fakeResizeObserver'; describe('Layout', () => { describe('placeholder', () => { @@ -1604,6 +1605,45 @@ describe('Layout', () => { }); }); + describe('sticky box height in two column variant', () => { + beforeAll(() => { + frontend.contentElementTypes.register('probe', { + component: function Probe() { + return
; + } + }); + }); + + it('exposes observed height of sticky box as custom property', () => { + fakeResizeObserver.contentRect = {width: 300, height: 600}; + const items = [ + {id: 2, type: 'probe', position: 'sticky'} + ]; + const {getByTestId} = renderInEntry( + + {children => children} + + ); + + const box = findParentWithClass(getByTestId('probe'), twoColumnStyles.box); + + expect(box.style.getPropertyValue('--sticky-box-height')).toEqual('600px'); + }); + + it('does not observe inline boxes', () => { + const items = [ + {id: 2, type: 'probe', position: 'inline'} + ]; + renderInEntry( + + {children => children} + + ); + + expect(fakeResizeObserver.observe).not.toHaveBeenCalled(); + }); + }); + function findParentWithClass(element, className) { let currentElement = element; diff --git a/entry_types/scrolled/package/src/frontend/layouts/TwoColumn.js b/entry_types/scrolled/package/src/frontend/layouts/TwoColumn.js index d3337b56be..bca5d19b2c 100644 --- a/entry_types/scrolled/package/src/frontend/layouts/TwoColumn.js +++ b/entry_types/scrolled/package/src/frontend/layouts/TwoColumn.js @@ -1,4 +1,4 @@ -import React, {useCallback, useRef} from 'react'; +import React, {useCallback, useEffect, useRef} from 'react'; import classNames from 'classnames'; import {api} from '../api'; @@ -97,6 +97,8 @@ function renderItemGroup(props, box, key) { function Box({box, children}) { const ref = useRef(); + useStickyBoxHeight(ref, box.position === 'sticky'); + return (
{ + if (!sticky || typeof ResizeObserver === 'undefined') { + return; + } + + const box = ref.current; + + const observer = new ResizeObserver(entries => + box.style.setProperty('--sticky-box-height', + `${entries[entries.length - 1].contentRect.height}px`) + ); + + observer.observe(box); + + return () => { + observer.disconnect(); + box.style.removeProperty('--sticky-box-height'); + }; + }, [ref, sticky]); +} + // Sticky boxes stay pinned while the rest of their group scrolls past. // The group therefore is the subject that drives view timelines of // content elements inside the box. diff --git a/entry_types/scrolled/package/src/frontend/layouts/TwoColumn.module.css b/entry_types/scrolled/package/src/frontend/layouts/TwoColumn.module.css index 3fca9a53b7..59fae58370 100644 --- a/entry_types/scrolled/package/src/frontend/layouts/TwoColumn.module.css +++ b/entry_types/scrolled/package/src/frontend/layouts/TwoColumn.module.css @@ -22,6 +22,8 @@ .box { max-width: var(--content-max-width); + margin-left: calc(var(--side-offset-left, 0px) - var(--margin-to-undo, 0px)); + margin-right: calc(var(--side-offset-right, 0px) - var(--margin-to-undo, 0px)); } .inline { @@ -103,8 +105,6 @@ --f: calc(1 / (1 - 2 * var(--content-margin-fraction))); --margin-to-undo: var(--theme-content-margin, calc(100% * var(--content-margin-fraction) * var(--f))); - margin-left: calc(-1 * var(--margin-to-undo)); - margin-right: calc(-1 * var(--margin-to-undo)); max-width: calc(var(--margin-to-undo) * 2 + var(--content-max-width)); } @@ -117,25 +117,37 @@ --content-max-width: min(var(--content-width), var(--two-column-sticky-content-max-width, 600px)); float: right; clear: both; - top: 33%; width: var(--content-width); } .constrainContentWidth .side { - position: relative; --side-offset: calc((min(var(--layout-inline-content-max-width), var(--constrained-content-max-width, 9999px)) - var(--content-max-width)) / 2); - right: var(--side-offset); + --side-offset-right: var(--side-offset); } .constrainContentWidth.right .side { - right: auto; - left: var(--side-offset); + --side-offset-right: 0px; + --side-offset-left: var(--side-offset); +} + +/* + cssnano strips units from zero lengths, turning `0px` into `0`. That + would invalidate `initial-value` as well as any zero inside a math + function, so the property uses the universal syntax and the box + height is clamped to the viewport height instead of the result. +*/ +@property --sticky-box-height { + syntax: '*'; + inherits: false; } .sticky { composes: side; position: sticky; + --sticky-box-height: 0px; + top: min(33%, + (100 * var(--vh) - min(var(--sticky-box-height), 100 * var(--vh))) / 2); } .right .side {