Skip to content

Commit 101c057

Browse files
committed
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
1 parent 0c198c5 commit 101c057

19 files changed

Lines changed: 465 additions & 251 deletions

File tree

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import {renderHook} from '@testing-library/react-hooks';
2+
3+
import {useAppearanceOverlayStyle} from 'frontend/appearance';
4+
5+
describe('useAppearanceOverlayStyle', () => {
6+
it('returns empty object for shadow appearance', () => {
7+
const {result} = renderHook(() =>
8+
useAppearanceOverlayStyle({appearance: 'shadow'})
9+
);
10+
11+
expect(result.current).toEqual({});
12+
});
13+
14+
it('returns empty object for transparent appearance', () => {
15+
const {result} = renderHook(() =>
16+
useAppearanceOverlayStyle({appearance: 'transparent'})
17+
);
18+
19+
expect(result.current).toEqual({});
20+
});
21+
22+
describe('cards appearance', () => {
23+
it('returns backdropFilter for translucent cardSurfaceColor', () => {
24+
const {result} = renderHook(() =>
25+
useAppearanceOverlayStyle({
26+
appearance: 'cards',
27+
cardSurfaceColor: '#ff000080'
28+
})
29+
);
30+
31+
expect(result.current).toEqual({
32+
backgroundColor: '#ff000080',
33+
backdropFilter: 'blur(10px)'
34+
});
35+
});
36+
37+
it('does not return backdropFilter for opaque cardSurfaceColor', () => {
38+
const {result} = renderHook(() =>
39+
useAppearanceOverlayStyle({
40+
appearance: 'cards',
41+
cardSurfaceColor: '#ff0000'
42+
})
43+
);
44+
45+
expect(result.current).toEqual({
46+
backgroundColor: '#ff0000'
47+
});
48+
});
49+
50+
it('does not return backdropFilter when no cardSurfaceColor is set', () => {
51+
const {result} = renderHook(() =>
52+
useAppearanceOverlayStyle({appearance: 'cards'})
53+
);
54+
55+
expect(result.current).toEqual({});
56+
});
57+
58+
it('scales overlayBackdropBlur to max 10px', () => {
59+
const {result} = renderHook(() =>
60+
useAppearanceOverlayStyle({
61+
appearance: 'cards',
62+
cardSurfaceColor: '#ff000080',
63+
overlayBackdropBlur: 50
64+
})
65+
);
66+
67+
expect(result.current).toEqual({
68+
backgroundColor: '#ff000080',
69+
backdropFilter: 'blur(5px)'
70+
});
71+
});
72+
73+
it('does not return backdropFilter when overlayBackdropBlur is 0', () => {
74+
const {result} = renderHook(() =>
75+
useAppearanceOverlayStyle({
76+
appearance: 'cards',
77+
cardSurfaceColor: '#ff000080',
78+
overlayBackdropBlur: 0
79+
})
80+
);
81+
82+
expect(result.current).toEqual({
83+
backgroundColor: '#ff000080'
84+
});
85+
});
86+
});
87+
88+
describe('split appearance', () => {
89+
it('returns backdropFilter for translucent splitOverlayColor', () => {
90+
const {result} = renderHook(() =>
91+
useAppearanceOverlayStyle({
92+
appearance: 'split',
93+
splitOverlayColor: '#ff000080',
94+
overlayBackdropBlur: 50
95+
})
96+
);
97+
98+
expect(result.current).toEqual({
99+
backgroundColor: '#ff000080',
100+
backdropFilter: 'blur(5px)'
101+
});
102+
});
103+
104+
it('returns default backdropFilter when no splitOverlayColor is set', () => {
105+
const {result} = renderHook(() =>
106+
useAppearanceOverlayStyle({appearance: 'split'})
107+
);
108+
109+
expect(result.current).toEqual({backdropFilter: 'blur(10px)'});
110+
});
111+
112+
it('does not return backdropFilter for opaque splitOverlayColor', () => {
113+
const {result} = renderHook(() =>
114+
useAppearanceOverlayStyle({
115+
appearance: 'split',
116+
splitOverlayColor: '#ff0000'
117+
})
118+
);
119+
120+
expect(result.current).toEqual({
121+
backgroundColor: '#ff0000'
122+
});
123+
});
124+
125+
it('returns default backdropFilter for translucent splitOverlayColor', () => {
126+
const {result} = renderHook(() =>
127+
useAppearanceOverlayStyle({
128+
appearance: 'split',
129+
splitOverlayColor: '#ff000080'
130+
})
131+
);
132+
133+
expect(result.current).toEqual({
134+
backgroundColor: '#ff000080',
135+
backdropFilter: 'blur(10px)'
136+
});
137+
});
138+
});
139+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import {renderEntry, usePageObjects} from 'support/pageObjects';
2+
3+
describe('fade transitions with backdrop blur', () => {
4+
usePageObjects();
5+
6+
it('uses per-element fade to preserve backdrop blur', () => {
7+
const {getSectionByPermaId} = renderEntry({
8+
seed: {
9+
sections: [
10+
{id: 1, permaId: 9, configuration: {fullHeight: true, transition: 'scroll'}},
11+
{id: 2, permaId: 10,
12+
configuration: {appearance: 'cards', cardSurfaceColor: '#ff000080',
13+
fullHeight: true, transition: 'fade'}}
14+
],
15+
contentElements: [{sectionId: 2}]
16+
}
17+
});
18+
19+
expect(getSectionByPermaId(10).usesPerElementFadeTransition()).toBe(true);
20+
});
21+
22+
it('uses regular fade when there is no backdrop blur', () => {
23+
const {getSectionByPermaId} = renderEntry({
24+
seed: {
25+
sections: [
26+
{id: 1, permaId: 9, configuration: {fullHeight: true, transition: 'scroll'}},
27+
{id: 2, permaId: 10,
28+
configuration: {appearance: 'cards', cardSurfaceColor: '#ff0000',
29+
fullHeight: true, transition: 'fade'}}
30+
],
31+
contentElements: [{sectionId: 2}]
32+
}
33+
});
34+
35+
expect(getSectionByPermaId(10).usesPerElementFadeTransition()).toBe(false);
36+
});
37+
});

entry_types/scrolled/package/spec/frontend/foregroundBoxes/CardBoxWrapper-spec.js

Lines changed: 51 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ import CardBoxWrapper from 'frontend/foregroundBoxes/CardBoxWrapper';
77
import cardBoxStyles from 'frontend/foregroundBoxes/CardBoxWrapper.module.css';
88
import boundaryMarginStyles from 'frontend/foregroundBoxes/BoxBoundaryMargin.module.css';
99

10+
const transitionStyles = {foregroundOpacity: 'foregroundOpacity'};
11+
1012
describe('CardBoxWrapper', () => {
1113
describe('at section boundaries', () => {
1214
it('does not have noTopMargin class when not at section start', () => {
1315
const {container} = render(
14-
<CardBoxWrapper openStart={false} openEnd={true} atSectionStart={false}>
16+
<CardBoxWrapper transitionStyles={transitionStyles}
17+
openStart={false} openEnd={true} atSectionStart={false}>
1518
Content
1619
</CardBoxWrapper>
1720
);
@@ -21,7 +24,8 @@ describe('CardBoxWrapper', () => {
2124

2225
it('has noTopMargin class when at section start', () => {
2326
const {container} = render(
24-
<CardBoxWrapper openStart={false} openEnd={true} atSectionStart={true}>
27+
<CardBoxWrapper transitionStyles={transitionStyles}
28+
openStart={false} openEnd={true} atSectionStart={true}>
2529
Content
2630
</CardBoxWrapper>
2731
);
@@ -31,7 +35,8 @@ describe('CardBoxWrapper', () => {
3135

3236
it('does not have noBottomMargin class when not at section end', () => {
3337
const {container} = render(
34-
<CardBoxWrapper openStart={true} openEnd={false} atSectionEnd={false}>
38+
<CardBoxWrapper transitionStyles={transitionStyles}
39+
openStart={true} openEnd={false} atSectionEnd={false}>
3540
Content
3641
</CardBoxWrapper>
3742
);
@@ -41,7 +46,8 @@ describe('CardBoxWrapper', () => {
4146

4247
it('has noBottomMargin class when at section end', () => {
4348
const {container} = render(
44-
<CardBoxWrapper openStart={true} openEnd={false} atSectionEnd={true}>
49+
<CardBoxWrapper transitionStyles={transitionStyles}
50+
openStart={true} openEnd={false} atSectionEnd={true}>
4551
Content
4652
</CardBoxWrapper>
4753
);
@@ -50,88 +56,82 @@ describe('CardBoxWrapper', () => {
5056
});
5157
});
5258

53-
describe('backdrop blur', () => {
54-
it('applies blur class when overlayBackdropBlur is set and color is translucent', () => {
55-
const {container} = render(
56-
<CardBoxWrapper cardSurfaceColor="#ff000080"
57-
overlayBackdropBlur={50}>
58-
Content
59-
</CardBoxWrapper>
60-
);
61-
62-
expect(container.firstChild).toHaveClass(cardBoxStyles.blur);
63-
});
64-
65-
it('does not apply blur class when color is opaque', () => {
66-
const {container} = render(
67-
<CardBoxWrapper cardSurfaceColor="#ff0000"
68-
overlayBackdropBlur={50}>
69-
Content
70-
</CardBoxWrapper>
71-
);
72-
73-
expect(container.firstChild).not.toHaveClass(cardBoxStyles.blur);
74-
});
75-
76-
it('does not apply blur when everything is default', () => {
59+
describe('background element', () => {
60+
it('applies foregroundOpacity class to background element', () => {
7761
const {container} = render(
78-
<CardBoxWrapper>
62+
<CardBoxWrapper transitionStyles={transitionStyles}
63+
openStart={false} openEnd={false}>
7964
Content
8065
</CardBoxWrapper>
8166
);
8267

83-
expect(container.firstChild).not.toHaveClass(cardBoxStyles.blur);
68+
expect(container.querySelector(`.${cardBoxStyles.cardBg}`))
69+
.toHaveClass('foregroundOpacity');
8470
});
71+
});
8572

86-
it('applies blur class by default for translucent color', () => {
73+
describe('content wrapper', () => {
74+
it('applies foregroundOpacity class to content wrapper', () => {
8775
const {container} = render(
88-
<CardBoxWrapper cardSurfaceColor="#ff000080">
89-
Content
76+
<CardBoxWrapper transitionStyles={transitionStyles}
77+
openStart={false} openEnd={false}>
78+
<span>Content</span>
9079
</CardBoxWrapper>
9180
);
9281

93-
expect(container.firstChild).toHaveClass(cardBoxStyles.blur);
82+
const contentWrapper = container.querySelector(`.foregroundOpacity:not(.${cardBoxStyles.cardBg})`);
83+
expect(contentWrapper).not.toBeNull();
84+
expect(contentWrapper).toHaveTextContent('Content');
9485
});
86+
});
9587

96-
it('sets backdrop blur CSS variable by default for translucent color', () => {
88+
describe('outside box', () => {
89+
it('wraps outsideBox children in foregroundOpacity', () => {
9790
const {container} = render(
98-
<CardBoxWrapper cardSurfaceColor="#ff000080">
99-
Content
91+
<CardBoxWrapper transitionStyles={transitionStyles}
92+
position="sticky">
93+
<span>Content</span>
10094
</CardBoxWrapper>
10195
);
10296

103-
expect(container.firstChild.style.getPropertyValue('--card-backdrop-blur'))
104-
.toBe('blur(10px)');
97+
expect(container.querySelector('.foregroundOpacity'))
98+
.toHaveTextContent('Content');
10599
});
100+
});
106101

107-
it('does not apply blur class when overlayBackdropBlur is 0', () => {
102+
describe('overlay style', () => {
103+
it('applies overlay style to background element', () => {
108104
const {container} = render(
109-
<CardBoxWrapper cardSurfaceColor="#ff000080"
110-
overlayBackdropBlur={0}>
105+
<CardBoxWrapper transitionStyles={transitionStyles}
106+
overlayStyle={{backgroundColor: '#ff000080', backdropFilter: 'blur(5px)'}}>
111107
Content
112108
</CardBoxWrapper>
113109
);
114110

115-
expect(container.firstChild).not.toHaveClass(cardBoxStyles.blur);
111+
const bg = container.querySelector(`.${cardBoxStyles.cardBg}`);
112+
expect(bg).toHaveStyle({backgroundColor: '#ff000080'});
113+
expect(bg.style.backdropFilter).toBe('blur(5px)');
116114
});
117115

118-
it('sets backdrop blur CSS variable when color is translucent', () => {
116+
it('does not set inline styles when overlayStyle is empty', () => {
119117
const {container} = render(
120-
<CardBoxWrapper cardSurfaceColor="#ff000080"
121-
overlayBackdropBlur={50}>
118+
<CardBoxWrapper transitionStyles={transitionStyles}
119+
overlayStyle={{}}>
122120
Content
123121
</CardBoxWrapper>
124122
);
125123

126-
expect(container.firstChild.style.getPropertyValue('--card-backdrop-blur'))
127-
.toBe('blur(5px)');
124+
const bg = container.querySelector(`.${cardBoxStyles.cardBg}`);
125+
expect(bg.style.backdropFilter).toBeFalsy();
126+
expect(bg.style.backgroundColor).toBeFalsy();
128127
});
129128
});
130129

131130
describe('cardEnd padding', () => {
132131
it('does not have cardEndPadding class when lastMarginBottom is set', () => {
133132
const {container} = render(
134-
<CardBoxWrapper openStart={true} openEnd={false} lastMarginBottom="lg">
133+
<CardBoxWrapper transitionStyles={transitionStyles}
134+
openStart={true} openEnd={false} lastMarginBottom="lg">
135135
Content
136136
</CardBoxWrapper>
137137
);
@@ -142,7 +142,8 @@ describe('CardBoxWrapper', () => {
142142

143143
it('has cardEndPadding class when lastMarginBottom is not set', () => {
144144
const {container} = render(
145-
<CardBoxWrapper openStart={true} openEnd={false}>
145+
<CardBoxWrapper transitionStyles={transitionStyles}
146+
openStart={true} openEnd={false}>
146147
Content
147148
</CardBoxWrapper>
148149
);

0 commit comments

Comments
 (0)