Skip to content

Commit e5c7cc4

Browse files
committed
Reserve space for target value during counting
Prevent layout shifts during counting animation by rendering a hidden placeholder with the target value. Uses CSS grid to overlay the current value on top of the placeholder, ensuring stable dimensions throughout the animation. REDMINE-21218
1 parent 7c5c6e5 commit e5c7cc4

4 files changed

Lines changed: 103 additions & 9 deletions

File tree

entry_types/scrolled/package/spec/contentElements/counter/Counter-spec.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,4 +318,51 @@ describe('Counter', () => {
318318
expect(descriptionDiv).toHaveClass(styles.textRight);
319319
});
320320
});
321+
322+
describe('space reservation', () => {
323+
it('renders hidden placeholder with target value when counting up', () => {
324+
const {container} = renderCounter({
325+
targetValue: 1000,
326+
startValue: 0,
327+
countingSpeed: 'medium'
328+
});
329+
const placeholder = container.querySelector('[aria-hidden="true"]');
330+
331+
expect(placeholder).toBeInTheDocument();
332+
expect(placeholder).toHaveTextContent('1,000');
333+
});
334+
335+
it('renders placeholder when targetValue equals startValue', () => {
336+
const {container} = renderCounter({
337+
targetValue: 100,
338+
startValue: 100,
339+
countingSpeed: 'medium'
340+
});
341+
const placeholder = container.querySelector('[aria-hidden="true"]');
342+
343+
expect(placeholder).toBeInTheDocument();
344+
});
345+
346+
it('does not render placeholder when counting down', () => {
347+
const {container} = renderCounter({
348+
targetValue: 0,
349+
startValue: 100,
350+
countingSpeed: 'medium'
351+
});
352+
const placeholder = container.querySelector('[aria-hidden="true"]');
353+
354+
expect(placeholder).not.toBeInTheDocument();
355+
});
356+
357+
it('does not render placeholder when countingSpeed is none', () => {
358+
const {container} = renderCounter({
359+
targetValue: 1000,
360+
startValue: 0,
361+
countingSpeed: 'none'
362+
});
363+
const placeholder = container.querySelector('[aria-hidden="true"]');
364+
365+
expect(placeholder).not.toBeInTheDocument();
366+
});
367+
});
321368
});

entry_types/scrolled/package/src/contentElements/counter/Counter.js

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from 'pageflow-scrolled/frontend';
1414

1515
import styles from './Counter.module.css';
16+
import {PlainNumber} from './PlainNumber';
1617

1718
export function Counter({configuration, contentElementId, contentElementWidth, sectionProps}) {
1819
const updateConfiguration = useContentElementConfigurationUpdate();
@@ -91,14 +92,6 @@ export function Counter({configuration, contentElementId, contentElementWidth, s
9192
}
9293
});
9394

94-
function format(value) {
95-
return value.toLocaleString(locale, {
96-
useGrouping: !configuration.hideThousandsSeparators,
97-
minimumFractionDigits: decimalPlaces,
98-
maximumFractionDigits: decimalPlaces
99-
});
100-
}
101-
10295
function renderUnit() {
10396
if (!configuration.unit) {
10497
return null;
@@ -156,7 +149,15 @@ export function Counter({configuration, contentElementId, contentElementWidth, s
156149
inline>
157150
<span style={{color: paletteColor(configuration.numberColor)}}>
158151
{configuration.unitPlacement === 'leading' && renderUnit()}
159-
{format(currentValue)}
152+
<PlainNumber
153+
value={currentValue}
154+
targetValue={countingDuration > 0 ? targetValue : null}
155+
formatOptions={{
156+
locale,
157+
decimalPlaces,
158+
useGrouping: !configuration.hideThousandsSeparators
159+
}}
160+
/>
160161
{configuration.unitPlacement !== 'leading' && renderUnit()}
161162
</span>
162163
</Text>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from 'react';
2+
3+
import styles from './PlainNumber.module.css';
4+
5+
export function PlainNumber({
6+
value,
7+
targetValue,
8+
formatOptions
9+
}) {
10+
const needsPlaceholder = targetValue != null && targetValue >= value;
11+
12+
function format(val) {
13+
return val.toLocaleString(formatOptions.locale, {
14+
useGrouping: formatOptions.useGrouping,
15+
minimumFractionDigits: formatOptions.decimalPlaces,
16+
maximumFractionDigits: formatOptions.decimalPlaces
17+
});
18+
}
19+
20+
if (!needsPlaceholder) {
21+
return format(value);
22+
}
23+
24+
return (
25+
<span className={styles.numberGrid}>
26+
<span aria-hidden="true" className={styles.numberPlaceholder}>
27+
{format(targetValue)}
28+
</span>
29+
<span>
30+
{format(value)}
31+
</span>
32+
</span>
33+
);
34+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.numberGrid {
2+
display: inline-grid;
3+
}
4+
5+
.numberGrid > * {
6+
grid-area: 1 / 1;
7+
text-align: right;
8+
}
9+
10+
.numberPlaceholder {
11+
visibility: hidden;
12+
}

0 commit comments

Comments
 (0)