From 101c057a8f5a20f990ec0ef767382294e76d6187 Mon Sep 17 00:00:00 2001 From: Tim Fischbach Date: Fri, 13 Mar 2026 08:12:45 +0100 Subject: [PATCH] Preserve backdrop blur during fade transitions Extract overlay style computation into a shared hook and fade individual foreground elements instead of the whole section. This prevents the backdrop blur from flickering or disappearing during fade transitions between sections, since the section itself no longer changes opacity. Card box backgrounds move from a CSS pseudo-element to a real DOM element so that their opacity can be controlled independently. REDMINE-21203 --- .../package/spec/frontend/appearance-spec.js | 139 ++++++++++++++++++ .../frontend/features/perElementFade-spec.js | 37 +++++ .../foregroundBoxes/CardBoxWrapper-spec.js | 101 ++++++------- .../frontend/foregroundBoxes/SplitBox-spec.js | 46 +++--- .../spec/frontend/shadows/SplitShadow-spec.js | 52 ++----- .../spec/frontend/splitOverlayStyle-spec.js | 43 ------ .../package/spec/frontend/transitions-spec.js | 58 ++++++++ .../package/spec/support/pageObjects.js | 4 + .../scrolled/package/src/frontend/Section.js | 18 +-- .../{appearance.js => appearance/index.js} | 18 ++- .../appearance/useAppearanceOverlayStyle.js | 47 ++++++ .../foregroundBoxes/CardBoxWrapper.js | 38 ++--- .../foregroundBoxes/CardBoxWrapper.module.css | 24 ++- .../src/frontend/foregroundBoxes/SplitBox.js | 13 +- .../src/frontend/shadows/SplitShadow.js | 6 +- .../package/src/frontend/splitOverlayStyle.js | 25 ---- .../package/src/frontend/transitions/index.js | 21 ++- .../frontend/transitions/shared.module.css | 20 +++ .../scrolled/package/values/properties.css | 6 + 19 files changed, 465 insertions(+), 251 deletions(-) create mode 100644 entry_types/scrolled/package/spec/frontend/appearance-spec.js create mode 100644 entry_types/scrolled/package/spec/frontend/features/perElementFade-spec.js delete mode 100644 entry_types/scrolled/package/spec/frontend/splitOverlayStyle-spec.js rename entry_types/scrolled/package/src/frontend/{appearance.js => appearance/index.js} (60%) create mode 100644 entry_types/scrolled/package/src/frontend/appearance/useAppearanceOverlayStyle.js delete mode 100644 entry_types/scrolled/package/src/frontend/splitOverlayStyle.js diff --git a/entry_types/scrolled/package/spec/frontend/appearance-spec.js b/entry_types/scrolled/package/spec/frontend/appearance-spec.js new file mode 100644 index 0000000000..68255ab521 --- /dev/null +++ b/entry_types/scrolled/package/spec/frontend/appearance-spec.js @@ -0,0 +1,139 @@ +import {renderHook} from '@testing-library/react-hooks'; + +import {useAppearanceOverlayStyle} from 'frontend/appearance'; + +describe('useAppearanceOverlayStyle', () => { + it('returns empty object for shadow appearance', () => { + const {result} = renderHook(() => + useAppearanceOverlayStyle({appearance: 'shadow'}) + ); + + expect(result.current).toEqual({}); + }); + + it('returns empty object for transparent appearance', () => { + const {result} = renderHook(() => + useAppearanceOverlayStyle({appearance: 'transparent'}) + ); + + expect(result.current).toEqual({}); + }); + + describe('cards appearance', () => { + it('returns backdropFilter for translucent cardSurfaceColor', () => { + const {result} = renderHook(() => + useAppearanceOverlayStyle({ + appearance: 'cards', + cardSurfaceColor: '#ff000080' + }) + ); + + expect(result.current).toEqual({ + backgroundColor: '#ff000080', + backdropFilter: 'blur(10px)' + }); + }); + + it('does not return backdropFilter for opaque cardSurfaceColor', () => { + const {result} = renderHook(() => + useAppearanceOverlayStyle({ + appearance: 'cards', + cardSurfaceColor: '#ff0000' + }) + ); + + expect(result.current).toEqual({ + backgroundColor: '#ff0000' + }); + }); + + it('does not return backdropFilter when no cardSurfaceColor is set', () => { + const {result} = renderHook(() => + useAppearanceOverlayStyle({appearance: 'cards'}) + ); + + expect(result.current).toEqual({}); + }); + + it('scales overlayBackdropBlur to max 10px', () => { + const {result} = renderHook(() => + useAppearanceOverlayStyle({ + appearance: 'cards', + cardSurfaceColor: '#ff000080', + overlayBackdropBlur: 50 + }) + ); + + expect(result.current).toEqual({ + backgroundColor: '#ff000080', + backdropFilter: 'blur(5px)' + }); + }); + + it('does not return backdropFilter when overlayBackdropBlur is 0', () => { + const {result} = renderHook(() => + useAppearanceOverlayStyle({ + appearance: 'cards', + cardSurfaceColor: '#ff000080', + overlayBackdropBlur: 0 + }) + ); + + expect(result.current).toEqual({ + backgroundColor: '#ff000080' + }); + }); + }); + + describe('split appearance', () => { + it('returns backdropFilter for translucent splitOverlayColor', () => { + const {result} = renderHook(() => + useAppearanceOverlayStyle({ + appearance: 'split', + splitOverlayColor: '#ff000080', + overlayBackdropBlur: 50 + }) + ); + + expect(result.current).toEqual({ + backgroundColor: '#ff000080', + backdropFilter: 'blur(5px)' + }); + }); + + it('returns default backdropFilter when no splitOverlayColor is set', () => { + const {result} = renderHook(() => + useAppearanceOverlayStyle({appearance: 'split'}) + ); + + expect(result.current).toEqual({backdropFilter: 'blur(10px)'}); + }); + + it('does not return backdropFilter for opaque splitOverlayColor', () => { + const {result} = renderHook(() => + useAppearanceOverlayStyle({ + appearance: 'split', + splitOverlayColor: '#ff0000' + }) + ); + + expect(result.current).toEqual({ + backgroundColor: '#ff0000' + }); + }); + + it('returns default backdropFilter for translucent splitOverlayColor', () => { + const {result} = renderHook(() => + useAppearanceOverlayStyle({ + appearance: 'split', + splitOverlayColor: '#ff000080' + }) + ); + + expect(result.current).toEqual({ + backgroundColor: '#ff000080', + backdropFilter: 'blur(10px)' + }); + }); + }); +}); diff --git a/entry_types/scrolled/package/spec/frontend/features/perElementFade-spec.js b/entry_types/scrolled/package/spec/frontend/features/perElementFade-spec.js new file mode 100644 index 0000000000..acf0caa257 --- /dev/null +++ b/entry_types/scrolled/package/spec/frontend/features/perElementFade-spec.js @@ -0,0 +1,37 @@ +import {renderEntry, usePageObjects} from 'support/pageObjects'; + +describe('fade transitions with backdrop blur', () => { + usePageObjects(); + + it('uses per-element fade to preserve backdrop blur', () => { + const {getSectionByPermaId} = renderEntry({ + seed: { + sections: [ + {id: 1, permaId: 9, configuration: {fullHeight: true, transition: 'scroll'}}, + {id: 2, permaId: 10, + configuration: {appearance: 'cards', cardSurfaceColor: '#ff000080', + fullHeight: true, transition: 'fade'}} + ], + contentElements: [{sectionId: 2}] + } + }); + + expect(getSectionByPermaId(10).usesPerElementFadeTransition()).toBe(true); + }); + + it('uses regular fade when there is no backdrop blur', () => { + const {getSectionByPermaId} = renderEntry({ + seed: { + sections: [ + {id: 1, permaId: 9, configuration: {fullHeight: true, transition: 'scroll'}}, + {id: 2, permaId: 10, + configuration: {appearance: 'cards', cardSurfaceColor: '#ff0000', + fullHeight: true, transition: 'fade'}} + ], + contentElements: [{sectionId: 2}] + } + }); + + expect(getSectionByPermaId(10).usesPerElementFadeTransition()).toBe(false); + }); +}); diff --git a/entry_types/scrolled/package/spec/frontend/foregroundBoxes/CardBoxWrapper-spec.js b/entry_types/scrolled/package/spec/frontend/foregroundBoxes/CardBoxWrapper-spec.js index 5e6ce49118..d6bb46dfe1 100644 --- a/entry_types/scrolled/package/spec/frontend/foregroundBoxes/CardBoxWrapper-spec.js +++ b/entry_types/scrolled/package/spec/frontend/foregroundBoxes/CardBoxWrapper-spec.js @@ -7,11 +7,14 @@ import CardBoxWrapper from 'frontend/foregroundBoxes/CardBoxWrapper'; import cardBoxStyles from 'frontend/foregroundBoxes/CardBoxWrapper.module.css'; import boundaryMarginStyles from 'frontend/foregroundBoxes/BoxBoundaryMargin.module.css'; +const transitionStyles = {foregroundOpacity: 'foregroundOpacity'}; + describe('CardBoxWrapper', () => { describe('at section boundaries', () => { it('does not have noTopMargin class when not at section start', () => { const {container} = render( - + Content ); @@ -21,7 +24,8 @@ describe('CardBoxWrapper', () => { it('has noTopMargin class when at section start', () => { const {container} = render( - + Content ); @@ -31,7 +35,8 @@ describe('CardBoxWrapper', () => { it('does not have noBottomMargin class when not at section end', () => { const {container} = render( - + Content ); @@ -41,7 +46,8 @@ describe('CardBoxWrapper', () => { it('has noBottomMargin class when at section end', () => { const {container} = render( - + Content ); @@ -50,88 +56,82 @@ describe('CardBoxWrapper', () => { }); }); - describe('backdrop blur', () => { - it('applies blur class when overlayBackdropBlur is set and color is translucent', () => { - const {container} = render( - - Content - - ); - - expect(container.firstChild).toHaveClass(cardBoxStyles.blur); - }); - - it('does not apply blur class when color is opaque', () => { - const {container} = render( - - Content - - ); - - expect(container.firstChild).not.toHaveClass(cardBoxStyles.blur); - }); - - it('does not apply blur when everything is default', () => { + describe('background element', () => { + it('applies foregroundOpacity class to background element', () => { const {container} = render( - + Content ); - expect(container.firstChild).not.toHaveClass(cardBoxStyles.blur); + expect(container.querySelector(`.${cardBoxStyles.cardBg}`)) + .toHaveClass('foregroundOpacity'); }); + }); - it('applies blur class by default for translucent color', () => { + describe('content wrapper', () => { + it('applies foregroundOpacity class to content wrapper', () => { const {container} = render( - - Content + + Content ); - expect(container.firstChild).toHaveClass(cardBoxStyles.blur); + const contentWrapper = container.querySelector(`.foregroundOpacity:not(.${cardBoxStyles.cardBg})`); + expect(contentWrapper).not.toBeNull(); + expect(contentWrapper).toHaveTextContent('Content'); }); + }); - it('sets backdrop blur CSS variable by default for translucent color', () => { + describe('outside box', () => { + it('wraps outsideBox children in foregroundOpacity', () => { const {container} = render( - - Content + + Content ); - expect(container.firstChild.style.getPropertyValue('--card-backdrop-blur')) - .toBe('blur(10px)'); + expect(container.querySelector('.foregroundOpacity')) + .toHaveTextContent('Content'); }); + }); - it('does not apply blur class when overlayBackdropBlur is 0', () => { + describe('overlay style', () => { + it('applies overlay style to background element', () => { const {container} = render( - + Content ); - expect(container.firstChild).not.toHaveClass(cardBoxStyles.blur); + const bg = container.querySelector(`.${cardBoxStyles.cardBg}`); + expect(bg).toHaveStyle({backgroundColor: '#ff000080'}); + expect(bg.style.backdropFilter).toBe('blur(5px)'); }); - it('sets backdrop blur CSS variable when color is translucent', () => { + it('does not set inline styles when overlayStyle is empty', () => { const {container} = render( - + Content ); - expect(container.firstChild.style.getPropertyValue('--card-backdrop-blur')) - .toBe('blur(5px)'); + const bg = container.querySelector(`.${cardBoxStyles.cardBg}`); + expect(bg.style.backdropFilter).toBeFalsy(); + expect(bg.style.backgroundColor).toBeFalsy(); }); }); describe('cardEnd padding', () => { it('does not have cardEndPadding class when lastMarginBottom is set', () => { const {container} = render( - + Content ); @@ -142,7 +142,8 @@ describe('CardBoxWrapper', () => { it('has cardEndPadding class when lastMarginBottom is not set', () => { const {container} = render( - + Content ); diff --git a/entry_types/scrolled/package/spec/frontend/foregroundBoxes/SplitBox-spec.js b/entry_types/scrolled/package/spec/frontend/foregroundBoxes/SplitBox-spec.js index 4c6b2232f5..99200d86d1 100644 --- a/entry_types/scrolled/package/spec/frontend/foregroundBoxes/SplitBox-spec.js +++ b/entry_types/scrolled/package/spec/frontend/foregroundBoxes/SplitBox-spec.js @@ -5,10 +5,13 @@ import '@testing-library/jest-dom/extend-expect'; import SplitBox from 'frontend/foregroundBoxes/SplitBox'; import styles from 'frontend/foregroundBoxes/SplitBox.module.css'; +const transitionStyles = {foregroundOpacity: 'foregroundOpacity'}; + describe('SplitBox', () => { it('renders children', () => { const {getByTestId} = render( - +
); @@ -18,7 +21,8 @@ describe('SplitBox', () => { it('applies paddingTop from motifAreaState', () => { const {container} = render( - +
); @@ -29,6 +33,7 @@ describe('SplitBox', () => { it('renders overlay when isContentPadded is true', () => { const {container} = render(
@@ -40,6 +45,7 @@ describe('SplitBox', () => { it('does not render overlay when isContentPadded is false', () => { const {container} = render(
@@ -51,6 +57,7 @@ describe('SplitBox', () => { it('sets overlay top from paddingTop', () => { const {container} = render(
@@ -63,6 +70,7 @@ describe('SplitBox', () => { it('applies dark overlay class when not inverted', () => { const {container} = render(
@@ -75,6 +83,7 @@ describe('SplitBox', () => { it('applies light overlay class when inverted', () => { const {container} = render(
@@ -84,44 +93,43 @@ describe('SplitBox', () => { .toHaveClass(styles.overlayLight); }); - it('applies backdrop filter when overlayBackdropBlur is set and color is translucent', () => { + it('applies foregroundOpacity class to overlay', () => { const {container} = render( + transitionStyles={transitionStyles} + inverted={false}>
); - expect(container.querySelector(`.${styles.overlay}`).style.backdropFilter) - .toBe('blur(5px)'); + expect(container.querySelector(`.${styles.overlay}`)) + .toHaveClass('foregroundOpacity'); }); - it('does not apply backdrop filter when color is opaque', () => { + it('applies foregroundOpacity class to content', () => { const {container} = render( - +
); - expect(container.querySelector(`.${styles.overlay}`).style.backdropFilter) - .toBeFalsy(); + expect(container.querySelector(`.${styles.content}`)) + .toHaveClass('foregroundOpacity'); }); - it('sets overlay background color from splitOverlayColor', () => { + it('applies overlay style to overlay element', () => { const {container} = render( + overlayStyle={{backgroundColor: '#ff000080', backdropFilter: 'blur(5px)'}}>
); - expect(container.querySelector(`.${styles.overlay}`)) - .toHaveStyle({backgroundColor: '#ff000080'}); + const overlay = container.querySelector(`.${styles.overlay}`); + expect(overlay).toHaveStyle({backgroundColor: '#ff000080'}); + expect(overlay.style.backdropFilter).toBe('blur(5px)'); }); }); diff --git a/entry_types/scrolled/package/spec/frontend/shadows/SplitShadow-spec.js b/entry_types/scrolled/package/spec/frontend/shadows/SplitShadow-spec.js index c6c3b6717b..8823a724ed 100644 --- a/entry_types/scrolled/package/spec/frontend/shadows/SplitShadow-spec.js +++ b/entry_types/scrolled/package/spec/frontend/shadows/SplitShadow-spec.js @@ -73,63 +73,29 @@ describe('SplitShadow', () => { expect(container.firstChild).toHaveClass(styles.light); }); - it('sets background color from splitOverlayColor prop', () => { - const {container} = render( - -
- - ); - - expect(container.querySelector(`.${styles.overlay}`)) - .toHaveStyle({backgroundColor: '#ff000080'}); - }); - - it('does not set inline background color when no splitOverlayColor', () => { - const {container} = render( - -
- - ); - - expect(container.querySelector(`.${styles.overlay}`).style.backgroundColor) - .toBe(''); - }); - - it('applies backdrop filter when overlayBackdropBlur is set and color is translucent', () => { - const {container} = render( - -
- - ); - - expect(container.querySelector(`.${styles.overlay}`).style.backdropFilter) - .toBe('blur(5px)'); - }); - - it('does not apply backdrop filter when color is opaque', () => { + it('applies overlay style to overlay element', () => { const {container} = render( + overlayStyle={{backgroundColor: '#ff000080', backdropFilter: 'blur(5px)'}}>
); - expect(container.querySelector(`.${styles.overlay}`).style.backdropFilter) - .toBeFalsy(); + const overlay = container.querySelector(`.${styles.overlay}`); + expect(overlay).toHaveStyle({backgroundColor: '#ff000080'}); + expect(overlay.style.backdropFilter).toBe('blur(5px)'); }); - it('applies default backdrop filter when no color is set', () => { + it('does not set inline styles when no overlayStyle is passed', () => { const {container} = render(
); - expect(container.querySelector(`.${styles.overlay}`).style.backdropFilter) - .toBe('blur(10px)'); + const overlay = container.querySelector(`.${styles.overlay}`); + expect(overlay.style.backgroundColor).toBe(''); + expect(overlay.style.backdropFilter).toBeFalsy(); }); it('does not render overlay when isContentPadded is true', () => { diff --git a/entry_types/scrolled/package/spec/frontend/splitOverlayStyle-spec.js b/entry_types/scrolled/package/spec/frontend/splitOverlayStyle-spec.js deleted file mode 100644 index a2489e9a07..0000000000 --- a/entry_types/scrolled/package/spec/frontend/splitOverlayStyle-spec.js +++ /dev/null @@ -1,43 +0,0 @@ -import {splitOverlayStyle} from 'frontend/splitOverlayStyle'; - -describe('splitOverlayStyle', () => { - it('sets backgroundColor from color', () => { - expect(splitOverlayStyle({color: '#ff0000'})) - .toEqual({backgroundColor: '#ff0000'}); - }); - - it('sets backdropFilter when color is translucent and backdropBlur is set', () => { - expect(splitOverlayStyle({color: '#ff000080', backdropBlur: 50})) - .toEqual({backgroundColor: '#ff000080', backdropFilter: 'blur(5px)'}); - }); - - it('does not set backdropFilter when color is opaque', () => { - expect(splitOverlayStyle({color: '#ff0000', backdropBlur: 50})) - .toEqual({backgroundColor: '#ff0000'}); - }); - - it('does not set backdropFilter when backdropBlur is 0', () => { - expect(splitOverlayStyle({color: '#ff000080', backdropBlur: 0})) - .toEqual({backgroundColor: '#ff000080'}); - }); - - it('scales blur value to max 10px', () => { - expect(splitOverlayStyle({color: '#ff000080', backdropBlur: 100})) - .toEqual({backgroundColor: '#ff000080', backdropFilter: 'blur(10px)'}); - }); - - it('defaults backdropFilter for translucent color', () => { - expect(splitOverlayStyle({color: '#ff000080'})) - .toEqual({backgroundColor: '#ff000080', backdropFilter: 'blur(10px)'}); - }); - - it('defaults backdropFilter when no color is set', () => { - expect(splitOverlayStyle({})) - .toEqual({backdropFilter: 'blur(10px)'}); - }); - - it('does not default backdropFilter for opaque color', () => { - expect(splitOverlayStyle({color: '#ff0000'})) - .toEqual({backgroundColor: '#ff0000'}); - }); -}); diff --git a/entry_types/scrolled/package/spec/frontend/transitions-spec.js b/entry_types/scrolled/package/spec/frontend/transitions-spec.js index 057743d1ac..63f5d1154e 100644 --- a/entry_types/scrolled/package/spec/frontend/transitions-spec.js +++ b/entry_types/scrolled/package/spec/frontend/transitions-spec.js @@ -1,9 +1,12 @@ import { getTransitionNames, getAvailableTransitionNames, + getTransitionStyles, getTransitionStylesName } from 'frontend/transitions'; +import sharedTransitionStyles from 'frontend/transitions/shared.module.css'; + import {useEntryStructure} from 'entryState/structure'; import {renderHookInEntry} from 'support'; @@ -51,6 +54,61 @@ describe('getAvailableTransitions', () => { }); }); +describe('getTransitionStyles', () => { + it('includes perElementFade class in foreground when overlayStyle has backdropFilter', () => { + const {result} = renderHookInEntry(() => useEntryStructure(), { + seed: { + sections: [ + {configuration: {fullHeight: true, transition: 'scroll'}}, + {configuration: {fullHeight: true, transition: 'fade'}}, + {configuration: {fullHeight: true, transition: 'scroll'}} + ] + } + }); + + const styles = getTransitionStyles( + result.current.main[0].sections[1], + {backdropFilter: 'blur(10px)'} + ); + + expect(styles.foreground).toContain(sharedTransitionStyles.perElementFade); + }); + + it('does not include perElementFade class in foreground by default', () => { + const {result} = renderHookInEntry(() => useEntryStructure(), { + seed: { + sections: [ + {configuration: {fullHeight: true, transition: 'scroll'}}, + {configuration: {fullHeight: true, transition: 'fade'}}, + {configuration: {fullHeight: true, transition: 'scroll'}} + ] + } + }); + + const styles = getTransitionStyles(result.current.main[0].sections[1]); + + expect(styles.foreground).not.toContain(sharedTransitionStyles.perElementFade); + }); + + it('does not include perElementFade class for scroll-only transitions', () => { + const {result} = renderHookInEntry(() => useEntryStructure(), { + seed: { + sections: [ + {configuration: {transition: 'scroll'}} + ] + } + }); + + const styles = getTransitionStyles( + result.current.main[0].sections[0], + {backdropFilter: 'blur(10px)'} + ); + + expect(styles.foreground).toBeUndefined(); + }); + +}); + describe('getTransitionStylesName', () => { it('uses fadeIn if both section and previous section have fullHeight', () => { const {result} = renderHookInEntry(() => useEntryStructure(), { diff --git a/entry_types/scrolled/package/spec/support/pageObjects.js b/entry_types/scrolled/package/spec/support/pageObjects.js index 575b740e97..9cc51aa54d 100644 --- a/entry_types/scrolled/package/spec/support/pageObjects.js +++ b/entry_types/scrolled/package/spec/support/pageObjects.js @@ -225,6 +225,10 @@ function createSectionPageObject(el) { return foreground.classList.contains(sharedTransitionStyles.fadedOut); }, + usesPerElementFadeTransition() { + return foreground.classList.contains(sharedTransitionStyles.perElementFade); + }, + getPaddingIndicator(position) { const {getByLabelText} = within(selectionRect); const labels = { diff --git a/entry_types/scrolled/package/src/frontend/Section.js b/entry_types/scrolled/package/src/frontend/Section.js index 6a2caaaa16..a2211d5ea5 100644 --- a/entry_types/scrolled/package/src/frontend/Section.js +++ b/entry_types/scrolled/package/src/frontend/Section.js @@ -22,7 +22,7 @@ import {BackgroundColorProvider} from './backgroundColor'; import {SelectableWidget} from './SelectableWidget'; import {useSectionPadding} from './useSectionPaddingCustomProperties'; import {SectionIntersectionProbe} from './SectionIntersectionObserver'; -import {getAppearanceComponents, getAppearanceSectionScopeName} from './appearance'; +import {getAppearanceComponents, getAppearanceSectionScopeName, useAppearanceOverlayStyle} from './appearance'; import * as v1 from './v1'; import * as v2 from './v2'; @@ -40,7 +40,8 @@ const Section = withInlineEditingDecorator('SectionDecorator', function Section( const ref = useScrollTarget(section.id); - const transitionStyles = getTransitionStyles(section); + const sectionOverlayStyle = useAppearanceOverlayStyle(section); + const transitionStyles = getTransitionStyles(section, sectionOverlayStyle); const backdropSectionClassNames = useBackdropSectionClassNames(backdrop, { layout: section.layout, @@ -83,6 +84,7 @@ const Section = withInlineEditingDecorator('SectionDecorator', function Section( contentElements={contentElements} state={state} transitionStyles={transitionStyles} + sectionOverlayStyle={sectionOverlayStyle} sectionPadding={sectionPadding} /> + overlayStyle={sectionOverlayStyle}> {children} } @@ -176,8 +177,7 @@ function SectionContents({ state={state} motifAreaState={motifAreaState} staticShadowOpacity={staticShadowOpacity} - splitOverlayColor={section.splitOverlayColor} - overlayBackdropBlur={section.overlayBackdropBlur}> + overlayStyle={sectionOverlayStyle}> {(children, boxProps) => {children} } diff --git a/entry_types/scrolled/package/src/frontend/appearance.js b/entry_types/scrolled/package/src/frontend/appearance/index.js similarity index 60% rename from entry_types/scrolled/package/src/frontend/appearance.js rename to entry_types/scrolled/package/src/frontend/appearance/index.js index 7a75d4625f..483f49fb9e 100644 --- a/entry_types/scrolled/package/src/frontend/appearance.js +++ b/entry_types/scrolled/package/src/frontend/appearance/index.js @@ -1,12 +1,14 @@ -import NoOpShadow from './shadows/NoOpShadow'; -import GradientShadow from './shadows/GradientShadow'; -import SplitShadow from './shadows/SplitShadow'; +import NoOpShadow from '../shadows/NoOpShadow'; +import GradientShadow from '../shadows/GradientShadow'; +import SplitShadow from '../shadows/SplitShadow'; -import {InvisibleBoxWrapper} from './foregroundBoxes/InvisibleBoxWrapper'; -import GradientBox from './foregroundBoxes/GradientBox'; -import CardBox from "./foregroundBoxes/CardBox"; -import CardBoxWrapper from "./foregroundBoxes/CardBoxWrapper"; -import SplitBox from "./foregroundBoxes/SplitBox"; +import {InvisibleBoxWrapper} from '../foregroundBoxes/InvisibleBoxWrapper'; +import GradientBox from '../foregroundBoxes/GradientBox'; +import CardBox from "../foregroundBoxes/CardBox"; +import CardBoxWrapper from "../foregroundBoxes/CardBoxWrapper"; +import SplitBox from "../foregroundBoxes/SplitBox"; + +export {useAppearanceOverlayStyle} from './useAppearanceOverlayStyle'; const components = { shadow: { diff --git a/entry_types/scrolled/package/src/frontend/appearance/useAppearanceOverlayStyle.js b/entry_types/scrolled/package/src/frontend/appearance/useAppearanceOverlayStyle.js new file mode 100644 index 0000000000..73912d3c85 --- /dev/null +++ b/entry_types/scrolled/package/src/frontend/appearance/useAppearanceOverlayStyle.js @@ -0,0 +1,47 @@ +import {useMemo} from 'react'; + +import {isTranslucentColor} from '../utils/isTranslucentColor'; + +export function useAppearanceOverlayStyle(section) { + const {appearance, cardSurfaceColor, splitOverlayColor, overlayBackdropBlur} = section; + + return useMemo(() => { + if (appearance === 'cards') { + return overlayStyle(cardSurfaceColor, overlayBackdropBlur); + } + else if (appearance === 'split') { + return overlayStyle(splitOverlayColor, overlayBackdropBlur, true); + } + + return {}; + }, [appearance, cardSurfaceColor, splitOverlayColor, overlayBackdropBlur]); +} + +function overlayStyle(color, backdropBlur, blurByDefault) { + const style = {}; + + if (color) { + style.backgroundColor = color; + } + + const blur = resolvedBackdropBlur(color, backdropBlur, blurByDefault); + + if (blur > 0) { + style.backdropFilter = `blur(${blur / 100 * 10}px)`; + } + + return style; +} + +function resolvedBackdropBlur(color, backdropBlur, blurByDefault) { + if (blurByDefault) { + if (color && !isTranslucentColor(color)) { + return 0; + } + } + else if (!isTranslucentColor(color)) { + return 0; + } + + return backdropBlur ?? 100; +} diff --git a/entry_types/scrolled/package/src/frontend/foregroundBoxes/CardBoxWrapper.js b/entry_types/scrolled/package/src/frontend/foregroundBoxes/CardBoxWrapper.js index ace4a857cb..cf34c0ef28 100644 --- a/entry_types/scrolled/package/src/frontend/foregroundBoxes/CardBoxWrapper.js +++ b/entry_types/scrolled/package/src/frontend/foregroundBoxes/CardBoxWrapper.js @@ -4,7 +4,6 @@ import classNames from 'classnames'; import {widths} from '../layouts'; import {BackgroundColorProvider} from '../backgroundColor'; import {TrimDefaultMarginTop} from '../TrimDefaultMarginTop'; -import {isTranslucentColor} from '../utils/isTranslucentColor'; import styles from "./CardBoxWrapper.module.css"; import boundaryMarginStyles from "./BoxBoundaryMargin.module.css"; @@ -13,17 +12,22 @@ export default function CardBoxWrapper(props) { if (outsideBox(props)) { return ( - {props.children} +
+ {props.children} +
); } return ( -
- - {props.children} - +
+
+
+ + {props.children} + +
) } @@ -34,30 +38,10 @@ function outsideBox(props) { props.customMargin } -function cardStyle(props) { - const style = {'--card-surface-color': props.cardSurfaceColor}; - const blur = resolvedBackdropBlur(props); - - if (blur > 0) { - style['--card-backdrop-blur'] = `blur(${blur / 100 * 10}px)`; - } - - return style; -} - -function resolvedBackdropBlur(props) { - if (!isTranslucentColor(props.cardSurfaceColor)) { - return 0; - } - - return props.overlayBackdropBlur ?? 100; -} - function className(props) { return classNames( styles.card, props.inverted ? styles.cardBgBlack : styles.cardBgWhite, - {[styles.blur]: resolvedBackdropBlur(props) > 0}, {[styles.cardStart]: !props.openStart}, {[styles.cardEnd]: !props.openEnd}, {[styles.cardEndPadding]: !props.openEnd && !props.lastMarginBottom}, diff --git a/entry_types/scrolled/package/src/frontend/foregroundBoxes/CardBoxWrapper.module.css b/entry_types/scrolled/package/src/frontend/foregroundBoxes/CardBoxWrapper.module.css index bb9060a4dd..44d6add0a2 100644 --- a/entry_types/scrolled/package/src/frontend/foregroundBoxes/CardBoxWrapper.module.css +++ b/entry_types/scrolled/package/src/frontend/foregroundBoxes/CardBoxWrapper.module.css @@ -13,8 +13,11 @@ padding: 0 1.5em; } -.card::before { - content: ''; +.cardContent { + display: flow-root; +} + +.cardBg { position: absolute; top: 0; left: 0; @@ -35,13 +38,12 @@ padding-bottom: 1.5em; } -.cardStart::before { +.cardStart > .cardBg { border-top-left-radius: var(--theme-cards-border-radius, 15px); border-top-right-radius: var(--theme-cards-border-radius, 15px); } - -.cardEnd::before { +.cardEnd > .cardBg { border-bottom-left-radius: var(--theme-cards-border-radius, 15px); border-bottom-right-radius: var(--theme-cards-border-radius, 15px); } @@ -54,17 +56,13 @@ composes: scope-lightContent from global; } -.blur::before { - backdrop-filter: var(--card-backdrop-blur); -} - @media screen { - .cardBgWhite::before { - background-color: var(--card-surface-color, lightContentSurfaceColor); + .cardBgWhite > .cardBg { + background-color: lightContentSurfaceColor; } - .cardBgBlack::before { - background-color: var(--card-surface-color, darkContentSurfaceColor); + .cardBgBlack > .cardBg { + background-color: darkContentSurfaceColor; } .cardBgWhite { diff --git a/entry_types/scrolled/package/src/frontend/foregroundBoxes/SplitBox.js b/entry_types/scrolled/package/src/frontend/foregroundBoxes/SplitBox.js index 9d28c6b9bf..61963f1159 100644 --- a/entry_types/scrolled/package/src/frontend/foregroundBoxes/SplitBox.js +++ b/entry_types/scrolled/package/src/frontend/foregroundBoxes/SplitBox.js @@ -1,7 +1,6 @@ import React from 'react'; import classNames from 'classnames'; -import {splitOverlayStyle} from '../splitOverlayStyle'; import styles from './SplitBox.module.css'; export default function SplitBox(props) { @@ -9,15 +8,15 @@ export default function SplitBox(props) {
{props.motifAreaState.isContentPadded &&
} -
+
{props.children}
diff --git a/entry_types/scrolled/package/src/frontend/shadows/SplitShadow.js b/entry_types/scrolled/package/src/frontend/shadows/SplitShadow.js index 584e2f5d86..a593b85c38 100644 --- a/entry_types/scrolled/package/src/frontend/shadows/SplitShadow.js +++ b/entry_types/scrolled/package/src/frontend/shadows/SplitShadow.js @@ -2,7 +2,6 @@ import React from 'react'; import classNames from 'classnames'; import Fullscreen from '../Fullscreen'; -import {splitOverlayStyle} from '../splitOverlayStyle'; import styles from './SplitShadow.module.css'; export default function SplitShadow(props) { @@ -19,10 +18,7 @@ export default function SplitShadow(props) { styles[`align-${props.align}`], props.inverted ? styles.light : styles.dark)}>
+ style={props.overlayStyle}>
{props.children} diff --git a/entry_types/scrolled/package/src/frontend/splitOverlayStyle.js b/entry_types/scrolled/package/src/frontend/splitOverlayStyle.js deleted file mode 100644 index fbe318b6f2..0000000000 --- a/entry_types/scrolled/package/src/frontend/splitOverlayStyle.js +++ /dev/null @@ -1,25 +0,0 @@ -import {isTranslucentColor} from './utils/isTranslucentColor'; - -export function splitOverlayStyle({color, backdropBlur}) { - const style = {}; - - if (color) { - style.backgroundColor = color; - } - - const blur = resolvedBackdropBlur({color, backdropBlur}); - - if (blur > 0) { - style.backdropFilter = `blur(${blur / 100 * 10}px)`; - } - - return style; -} - -function resolvedBackdropBlur({color, backdropBlur}) { - if (color && !isTranslucentColor(color)) { - return 0; - } - - return backdropBlur ?? 100; -} diff --git a/entry_types/scrolled/package/src/frontend/transitions/index.js b/entry_types/scrolled/package/src/frontend/transitions/index.js index 7facd5840a..be05da0393 100644 --- a/entry_types/scrolled/package/src/frontend/transitions/index.js +++ b/entry_types/scrolled/package/src/frontend/transitions/index.js @@ -18,6 +18,8 @@ import scrollInFadeOut from './scrollInFadeOut.module.css'; import scrollInFadeOutBg from './scrollInFadeOutBg.module.css'; import scrollInScrollOut from './scrollInScrollOut.module.css'; +import sharedStyles from './shared.module.css'; + const styles = { fadeInBgConceal, fadeInBgFadeOut, @@ -70,14 +72,29 @@ export function getAvailableTransitionNames(section, previousSection) { return getTransitionNames(); } -export function getTransitionStyles(section) { +export function getTransitionStyles(section, overlayStyle) { const name = getTransitionStylesName(section); if (!styles[name]) { throw new Error(`Unknown transition ${name}`); } - return styles[name]; + const base = styles[name]; + + const [enter, exit] = getEnterAndExitTransitions(section); + + if (overlayStyle?.backdropFilter && + (enter.startsWith('fade') || exit.startsWith('fade'))) { + return { + ...base, + foreground: base.foreground + ? `${base.foreground} ${sharedStyles.perElementFade}` + : sharedStyles.perElementFade, + foregroundOpacity: sharedStyles.foregroundOpacity + }; + } + + return base; } export function getEnterAndExitTransitions(section) { diff --git a/entry_types/scrolled/package/src/frontend/transitions/shared.module.css b/entry_types/scrolled/package/src/frontend/transitions/shared.module.css index e2acf9c077..8551ba95f7 100644 --- a/entry_types/scrolled/package/src/frontend/transitions/shared.module.css +++ b/entry_types/scrolled/package/src/frontend/transitions/shared.module.css @@ -1,3 +1,5 @@ +@value fade-duration from './values.module.css'; + .fixed { position: fixed; top: calc(var(--fixed-positioning-containing-block-offset, 0px) + @@ -20,3 +22,21 @@ opacity: 0; visibility: hidden; } + +/* + Override transition set on .foreground in individual transition + modules (e.g. scrollInFadeOut) to animate --foreground-opacity + instead of opacity. +*/ +.perElementFade { + transition: --foreground-opacity fade-duration ease, visibility fade-duration !important; +} + +.perElementFade.fadedOut { + --foreground-opacity: 0; + opacity: 1; +} + +.perElementFade .foregroundOpacity { + opacity: var(--foreground-opacity, 1); +} diff --git a/entry_types/scrolled/package/values/properties.css b/entry_types/scrolled/package/values/properties.css index abe516401e..5718829bf7 100644 --- a/entry_types/scrolled/package/values/properties.css +++ b/entry_types/scrolled/package/values/properties.css @@ -3,3 +3,9 @@ inherits: true; initial-value: 9999px; } + +@property --foreground-opacity { + syntax: ""; + inherits: true; + initial-value: 1; +}