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 {