Skip to content

ValuesWithChange Election Trackers Component #13920

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 20, 2025

Conversation

JamieB-gu
Copy link
Contributor

Adds a new election trackers component to display a list of values and the amount by which they've changed. This could be used, for example, to display a list of seats won in an election along with the changes since the last election. The included stories show examples of this.

This component is one of several that will allow rendering election trackers within DCAR.

Screenshots

Light Dark
UK General uk-general-light uk-general-dark
UK Local uk-local-light uk-local-dark
EU Parliament eu-parliament-light eu-parliament-dark

Adds a new election trackers component to display a list of values and
the amount by which they've changed. This could be used, for example, to
display a list of seats won in an election along with the changes since
the last election. The included stories show examples of this.

This component is one of several that will allow rendering election
trackers within DCAR.
Copy link

github-actions bot commented May 8, 2025

Hello 👋! When you're ready to run Chromatic, please apply the run_chromatic label to this PR.

You will need to reapply the label each time you want to run Chromatic.

Click here to see the Chromatic project.

@JamieB-gu JamieB-gu self-assigned this May 8, 2025
@JamieB-gu JamieB-gu added the run_chromatic Runs chromatic when label is applied label May 8, 2025
@github-actions github-actions bot removed the run_chromatic Runs chromatic when label is applied label May 8, 2025
Copy link

github-actions bot commented May 8, 2025

Size Change: +283 B (+0.03%)

Total Size: 974 kB

Filename Size Change
dotcom-rendering/dist/562.client.web.********************.js 11.8 kB +283 B (+2.46%)
ℹ️ View Unchanged
Filename Size
dotcom-rendering/dist/1008.client.web.********************.js 2.88 kB
dotcom-rendering/dist/1032.client.web.********************.js 3.31 kB
dotcom-rendering/dist/117.client.web.********************.js 2.87 kB
dotcom-rendering/dist/1562.client.web.********************.js 20.3 kB
dotcom-rendering/dist/1656.client.web.********************.js 49.6 kB
dotcom-rendering/dist/1823.client.web.********************.js 3.9 kB
dotcom-rendering/dist/2045.client.web.********************.js 2.63 kB
dotcom-rendering/dist/2161.client.web.********************.js 3.9 kB
dotcom-rendering/dist/223.client.web.********************.js 527 B
dotcom-rendering/dist/2270.client.web.********************.js 3.65 kB
dotcom-rendering/dist/2309.client.web.********************.js 3.27 kB
dotcom-rendering/dist/2345.client.web.********************.js 3.38 kB
dotcom-rendering/dist/2360.client.web.********************.js 20 kB
dotcom-rendering/dist/2502.client.web.********************.js 157 B
dotcom-rendering/dist/302.client.web.********************.js 11.3 kB
dotcom-rendering/dist/3292.client.web.********************.js 2.11 kB
dotcom-rendering/dist/3308.client.web.********************.js 3.12 kB
dotcom-rendering/dist/3384.client.web.********************.js 3.86 kB
dotcom-rendering/dist/3485.client.web.********************.js 4.67 kB
dotcom-rendering/dist/356.client.web.********************.js 6.4 kB
dotcom-rendering/dist/3598.client.web.********************.js 3.65 kB
dotcom-rendering/dist/3605.client.web.********************.js 15.1 kB
dotcom-rendering/dist/3619.client.web.********************.js 4.46 kB
dotcom-rendering/dist/3929.client.web.********************.js 3.45 kB
dotcom-rendering/dist/4133.client.web.********************.js 3.1 kB
dotcom-rendering/dist/4258.client.web.********************.js 4.08 kB
dotcom-rendering/dist/4306.client.web.********************.js 22.7 kB
dotcom-rendering/dist/4308.client.web.********************.js 3.57 kB
dotcom-rendering/dist/4324.client.web.********************.js 619 B
dotcom-rendering/dist/4457.client.web.********************.js 6.26 kB
dotcom-rendering/dist/4501.client.web.********************.js 4.29 kB
dotcom-rendering/dist/476.client.web.********************.js 5.42 kB
dotcom-rendering/dist/4776.client.web.********************.js 11.6 kB
dotcom-rendering/dist/5144.client.web.********************.js 2.55 kB
dotcom-rendering/dist/5448.client.web.********************.js 4.37 kB
dotcom-rendering/dist/545.client.web.********************.js 531 B
dotcom-rendering/dist/6038.client.web.********************.js 2.76 kB
dotcom-rendering/dist/6217.client.web.********************.js 3.75 kB
dotcom-rendering/dist/6480.client.web.********************.js 3.68 kB
dotcom-rendering/dist/6540.client.web.********************.js 2.62 kB
dotcom-rendering/dist/6541.client.web.********************.js 2.91 kB
dotcom-rendering/dist/6555.client.web.********************.js 5.45 kB
dotcom-rendering/dist/6562.client.web.********************.js 439 B
dotcom-rendering/dist/6584.client.web.********************.js 2.68 kB
dotcom-rendering/dist/6722.client.web.********************.js 3.03 kB
dotcom-rendering/dist/6873.client.web.********************.js 4.13 kB
dotcom-rendering/dist/695.client.web.********************.js 4.51 kB
dotcom-rendering/dist/7116.client.web.********************.js 23.2 kB
dotcom-rendering/dist/732.client.web.********************.js 3.09 kB
dotcom-rendering/dist/7398.client.web.********************.js 3.57 kB
dotcom-rendering/dist/7708.client.web.********************.js 5.05 kB
dotcom-rendering/dist/7725.client.web.********************.js 5.6 kB
dotcom-rendering/dist/7927.client.web.********************.js 2.76 kB
dotcom-rendering/dist/8151.client.web.********************.js 4.17 kB
dotcom-rendering/dist/8212.client.web.********************.js 5.04 kB
dotcom-rendering/dist/8385.client.web.********************.js 3.88 kB
dotcom-rendering/dist/8387.client.web.********************.js 2.78 kB
dotcom-rendering/dist/8436.client.web.********************.js 3.63 kB
dotcom-rendering/dist/8602.client.web.********************.js 3.39 kB
dotcom-rendering/dist/8619.client.web.********************.js 4.44 kB
dotcom-rendering/dist/864.client.web.********************.js 3.17 kB
dotcom-rendering/dist/8942.client.web.********************.js 4.44 kB
dotcom-rendering/dist/8973.client.web.********************.js 16.5 kB
dotcom-rendering/dist/9135.client.web.********************.js 3.54 kB
dotcom-rendering/dist/9139.client.web.********************.js 3.13 kB
dotcom-rendering/dist/9376.client.web.********************.js 2.52 kB
dotcom-rendering/dist/960.client.web.********************.js 7.99 kB
dotcom-rendering/dist/967.client.web.********************.js 2.87 kB
dotcom-rendering/dist/9822.client.web.********************.js 7.67 kB
dotcom-rendering/dist/9837.client.web.********************.js 3.29 kB
dotcom-rendering/dist/9870.client.web.********************.js 4.77 kB
dotcom-rendering/dist/Accessibility-importable.client.web.********************.js 8.43 kB
dotcom-rendering/dist/AdBlockAsk-importable.client.web.********************.js 2.99 kB
dotcom-rendering/dist/AdPortals-importable.client.web.********************.js 4.76 kB
dotcom-rendering/dist/AlreadyVisited-importable.client.web.********************.js 425 B
dotcom-rendering/dist/AppsEpic-importable.client.web.********************.js 3.61 kB
dotcom-rendering/dist/AppsFooter-importable.client.web.********************.js 2.69 kB
dotcom-rendering/dist/AppsLightboxImage-importable.client.web.********************.js 2.66 kB
dotcom-rendering/dist/AppsLightboxImageStore-importable.client.web.********************.js 2.6 kB
dotcom-rendering/dist/AudioAtomWrapper-importable.client.web.********************.js 2.76 kB
dotcom-rendering/dist/AudioPlayerWrapper-importable.client.web.********************.js 6.61 kB
dotcom-rendering/dist/AustralianTerritorySwitcher-importable.client.web.********************.js 4.61 kB
dotcom-rendering/dist/Branding-importable.client.web.********************.js 2.89 kB
dotcom-rendering/dist/braze-web-sdk-core.client.web.********************.js 67.7 kB
dotcom-rendering/dist/BrazeMessaging-importable.client.web.********************.js 1.68 kB
dotcom-rendering/dist/CalloutBlockComponent-importable.client.web.********************.js 6.73 kB
dotcom-rendering/dist/CalloutEmbedBlockComponent-importable.client.web.********************.js 5.77 kB
dotcom-rendering/dist/CardCommentCount-importable.client.web.********************.js 2.67 kB
dotcom-rendering/dist/Carousel-importable.client.web.********************.js 6.67 kB
dotcom-rendering/dist/CarouselForNewsletters-importable.client.web.********************.js 4.57 kB
dotcom-rendering/dist/ChartAtom-importable.client.web.********************.js 539 B
dotcom-rendering/dist/CommentCount-importable.client.web.********************.js 2.3 kB
dotcom-rendering/dist/CrosswordComponent-importable.client.web.********************.js 2.88 kB
dotcom-rendering/dist/DiscussionApps-importable.client.web.********************.js 1.07 kB
dotcom-rendering/dist/DiscussionMeta-importable.client.web.********************.js 2.41 kB
dotcom-rendering/dist/DiscussionWeb-importable.client.web.********************.js 3.45 kB
dotcom-rendering/dist/DocumentBlockComponent-importable.client.web.********************.js 2.85 kB
dotcom-rendering/dist/Dropdown-importable.client.web.********************.js 1.72 kB
dotcom-rendering/dist/EditionSwitcherBanner-importable.client.web.********************.js 4.45 kB
dotcom-rendering/dist/EmbedBlockComponent-importable.client.web.********************.js 3.98 kB
dotcom-rendering/dist/EnhancePinnedPost-importable.client.web.********************.js 2.02 kB
dotcom-rendering/dist/FetchOnwardsData-importable.client.web.********************.js 1.94 kB
dotcom-rendering/dist/FilterKeyEventsToggle-importable.client.web.********************.js 3.72 kB
dotcom-rendering/dist/FocusStyles-importable.client.web.********************.js 618 B
dotcom-rendering/dist/FollowWrapper-importable.client.web.********************.js 2.53 kB
dotcom-rendering/dist/FootballMatchesPageWrapper-importable.client.web.********************.js 7.2 kB
dotcom-rendering/dist/FootballTablesCompetitionSelect-importable.client.web.********************.js 3.26 kB
dotcom-rendering/dist/FooterLabel-importable.client.web.********************.js 364 B
dotcom-rendering/dist/FooterReaderRevenueLinks-importable.client.web.********************.js 3.43 kB
dotcom-rendering/dist/frameworks.client.web.********************.js 20.9 kB
dotcom-rendering/dist/FrontSubNav-importable.client.web.********************.js 7.5 kB
dotcom-rendering/dist/GetCricketScoreboard-importable.client.web.********************.js 6.29 kB
dotcom-rendering/dist/GetMatchNav-importable.client.web.********************.js 11.5 kB
dotcom-rendering/dist/GetMatchStats-importable.client.web.********************.js 8.12 kB
dotcom-rendering/dist/GetMatchTabs-importable.client.web.********************.js 2.58 kB
dotcom-rendering/dist/guardian-braze-components-banner.client.web.********************.js 16.1 kB
dotcom-rendering/dist/guardian-braze-components-end-of-article.client.web.********************.js 10.2 kB
dotcom-rendering/dist/GuideAtomWrapper-importable.client.web.********************.js 782 B
dotcom-rendering/dist/index.client.web.********************.js 46.4 kB
dotcom-rendering/dist/InstagramBlockComponent-importable.client.web.********************.js 2.9 kB
dotcom-rendering/dist/InteractiveAtomMessenger-importable.client.web.********************.js 854 B
dotcom-rendering/dist/InteractiveBlockComponent-importable.client.web.********************.js 8.83 kB
dotcom-rendering/dist/InteractiveContentsBlockComponent-importable.client.web.********************.js 3.78 kB
dotcom-rendering/dist/KeyEventsCarousel-importable.client.web.********************.js 5.69 kB
dotcom-rendering/dist/KnowledgeQuizAtom-importable.client.web.********************.js 3.19 kB
dotcom-rendering/dist/LatestLinks-importable.client.web.********************.js 7.98 kB
dotcom-rendering/dist/LightboxHash-importable.client.web.********************.js 436 B
dotcom-rendering/dist/LightboxLayout-importable.client.web.********************.js 6.58 kB
dotcom-rendering/dist/LiveBlogEpic-importable.client.web.********************.js 3.55 kB
dotcom-rendering/dist/LiveblogGutterAskWrapper-importable.client.web.********************.js 2.49 kB
dotcom-rendering/dist/LiveblogNotifications-importable.client.web.********************.js 4.84 kB
dotcom-rendering/dist/Liveness-importable.client.web.********************.js 4.71 kB
dotcom-rendering/dist/LoopVideo-importable.client.web.********************.js 3.67 kB
dotcom-rendering/dist/ManyNewsletterSignUp-importable.client.web.********************.js 7.65 kB
dotcom-rendering/dist/MapEmbedBlockComponent-importable.client.web.********************.js 6.04 kB
dotcom-rendering/dist/Metrics-importable.client.web.********************.js 2.7 kB
dotcom-rendering/dist/MostViewedFooter-importable.client.web.********************.js 3.85 kB
dotcom-rendering/dist/MostViewedFooterData-importable.client.web.********************.js 5.97 kB
dotcom-rendering/dist/MostViewedRightWithAd-importable.client.web.********************.js 5.24 kB
dotcom-rendering/dist/OnwardsUpper-importable.client.web.********************.js 5.32 kB
dotcom-rendering/dist/PersonalityQuizAtom-importable.client.web.********************.js 3.35 kB
dotcom-rendering/dist/ProfileAtom-importable.client.web.********************.js 545 B
dotcom-rendering/dist/ProfileAtomWrapper-importable.client.web.********************.js 805 B
dotcom-rendering/dist/PulsingDot-importable.client.web.********************.js 749 B
dotcom-rendering/dist/QandaAtom-importable.client.web.********************.js 542 B
dotcom-rendering/dist/ReaderRevenueDev-importable.client.web.********************.js 470 B
dotcom-rendering/dist/readerRevenueDevUtils.client.web.********************.js 1.7 kB
dotcom-rendering/dist/RelativeTime-importable.client.web.********************.js 2.56 kB
dotcom-rendering/dist/RichLinkComponent-importable.client.web.********************.js 6.14 kB
dotcom-rendering/dist/ScrollableFeature-importable.client.web.********************.js 6.63 kB
dotcom-rendering/dist/ScrollableHighlights-importable.client.web.********************.js 7.29 kB
dotcom-rendering/dist/ScrollableMedium-importable.client.web.********************.js 2.11 kB
dotcom-rendering/dist/ScrollableSmall-importable.client.web.********************.js 2.17 kB
dotcom-rendering/dist/SecureSignup-importable.client.web.********************.js 4.2 kB
dotcom-rendering/dist/SendTargetingParams-importable.client.web.********************.js 2.21 kB
dotcom-rendering/dist/sentry.client.web.********************.js 802 B
dotcom-rendering/dist/SetABTests-importable.client.web.********************.js 3.91 kB
dotcom-rendering/dist/SetAdTargeting-importable.client.web.********************.js 487 B
dotcom-rendering/dist/ShareButton-importable.client.web.********************.js 2.18 kB
dotcom-rendering/dist/shimport.client.web.********************.js 2.8 kB
dotcom-rendering/dist/ShowHideContainers-importable.client.web.********************.js 893 B
dotcom-rendering/dist/ShowMore-importable.client.web.********************.js 919 B
dotcom-rendering/dist/SignInGateMain.client.web.********************.js 1.11 kB
dotcom-rendering/dist/SignInGateMainCheckoutComplete.client.web.********************.js 2.6 kB
dotcom-rendering/dist/SignInGateSelector-importable.client.web.********************.js 4.97 kB
dotcom-rendering/dist/SlideshowCarousel-importable.client.web.********************.js 4.56 kB
dotcom-rendering/dist/SlotBodyEnd-importable.client.web.********************.js 4.9 kB
dotcom-rendering/dist/SpotifyBlockComponent-importable.client.web.********************.js 5.8 kB
dotcom-rendering/dist/StickyBottomBanner-importable.client.web.********************.js 6.13 kB
dotcom-rendering/dist/SubNav-importable.client.web.********************.js 2.44 kB
dotcom-rendering/dist/TableOfContents-importable.client.web.********************.js 3.66 kB
dotcom-rendering/dist/TimelineAtom-importable.client.web.********************.js 1.23 kB
dotcom-rendering/dist/Titlepiece-importable.client.web.********************.js 13.6 kB
dotcom-rendering/dist/TopBar-importable.client.web.********************.js 8.85 kB
dotcom-rendering/dist/TopBarSupport-importable.client.web.********************.js 2.49 kB
dotcom-rendering/dist/TweetBlockComponent-importable.client.web.********************.js 1.13 kB
dotcom-rendering/dist/UnsafeEmbedBlockComponent-importable.client.web.********************.js 2.92 kB
dotcom-rendering/dist/VideoFacebookBlockComponent-importable.client.web.********************.js 6.06 kB
dotcom-rendering/dist/VineBlockComponent-importable.client.web.********************.js 3.32 kB
dotcom-rendering/dist/YoutubeBlockComponent-importable.client.web.********************.js 843 B

compressed-size-action

@JamieB-gu JamieB-gu requested a review from a team May 12, 2025 13:20
Copy link
Contributor

@marjisound marjisound left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good 👍 Just left a few questions

gridTemplateColumns: 'repeat(auto-fill, minmax(84px, 1fr))',
rowGap,
'--column-gap': '10px',
columnGap: 'var(--column-gap)',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How come for row-gap we are using rowGap const defined on line 57 but for column-gap we're doing it differently?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although both are used in multiple places, the value of row gap never changes whereas the column gap needs to vary by breakpoint (line 80). By setting it as a CSS variable we can use it in multiple places and have it automatically update when changed via the media query here.

<li
style={{
'--before-background-colour': value.colour,
}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wondering why it's necessary to add the style prop here? Can we not directly use the value.colour in backgroundColour? like this: backgroundColor: value.colour,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The background colour is an example of what Emotion refers to as a "dynamic style", i.e. a style that's likely to be unique to each single instance of a component (an element)1.

Most of the properties passed here to the css prop are "static" styles, common to all instances of the Value component; every instance will have white-space: nowrap and position: relative. However, because the colour used for background-color comes from the value prop, it can be different every time the component is called with a different set of props (and it is different, because each party in an election typically has a different colour representing it). This means that the background-color declaration will be different for every instance of the component.

Emotion generates a new class and CSS rule for each unique collection of styles. In this case, even though most of the declarations are the same, because that one background-color declaration is different, a new CSS rule is generated each time. If you were to pass background-color directly to the css prop you'd see this in the DOM; each li in this list would have a different class. This generates duplicate CSS and thus increases page weight.

<style>
  .list-item-1 {
    white-space: nowrap;
    position: relative;
    background-color: blue;
  }

  .list-item-2 {
    white-space: nowrap;
    position: relative;
    background-color: red;
  }
</style>

<ul>
  <li class="list-item-1">Blue element</li>
  <li class="list-item-2">Red element</li>
</ul>

The solution to this is to put every CSS declaration that's common to all instances of a component (the "static" styles) in the css prop, and any that are not (the "dynamic styles") in the style prop. That way a single class and CSS rule can be shared across all instances of the component, and only the unique, dynamic styles create additional CSS in the page. You can see this in the DOM for these list items; each one has the same class, and only the style attributes contain the different background-colors. Generally speaking I think you can differentiate whether something should be a static or dynamic style by checking if it's derived from a prop; if it is then it's dynamic, if not then it's probably static.

<style>
  .list-item {
    white-space: nowrap;
    position: relative;
  }
</style>

<ul>
  <li class="list-item" style="background-color: blue;">Blue element</li>
  <li class="list-item" style="background-color: red;">Red element</li>
</ul>

The simpler cases of this pattern can be seen in #13921, in the Bar and ChangeText components, where some different styles are applied based on the value of the change prop. The case above though is slightly more complicated because you cannot style pseudo-elements2 like ::before3 with inline styles, i.e. via the style prop. Instead you can write the full ::before rule as static styles via the css prop, but defer the value of background-color to a CSS variable4. This is then set as a dynamic style derived from the props.

<style>
  .list-item {
    white-space: nowrap;
    position: relative;
  }

  .list-item::before {
    background-color: var(--before-background-colour);
  }
</style>

<ul>
  <li class="list-item" style="--before-background-colour: blue;">Blue element</li>
  <li class="list-item" style="--before-background-colour: red;">Red element</li>
</ul>

Footnotes

  1. https://emotion.sh/docs/best-practices#use-the-style-prop-for-dynamic-styles

  2. https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements

  3. https://developer.mozilla.org/en-US/docs/Web/CSS/::before

  4. https://emotion.sh/docs/best-practices#advanced-css-variables-with-style

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks so much for this thorough explanation. It makes full sense and a great learning for me 👌

css={{
['&']: css(headlineBold24),
lineHeight: 1,
}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this have been written like this as well?

css={[
  headlineBold24,
  {
    lineHeight: 1,
  },
]}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, and I have written it like that in the past. I just figured out that it could also be written like this, and I thought it was perhaps a little clearer using the commonly recognised nesting selector (&), and having everything in one object rather than an array. It also means less indentation, and I think makes it a little easier to read in situations where you also have media queries (see the Abbreviation and ChangeText components in #13921).

What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each of the presets has an object version so you should be able to spread the properties like so:

css={{
  ...headlineBold24Object,
  lineHeight: 1,
}}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great suggestion, I didn't realise we had this API, thanks @jamesmockett !

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, yea I agree having an object rather than an array looks easier to read 👍

Source has an object styles API for fonts, which integrates better with
the object styles in this component.
@JamieB-gu JamieB-gu added the run_chromatic Runs chromatic when label is applied label May 16, 2025
@github-actions github-actions bot removed the run_chromatic Runs chromatic when label is applied label May 16, 2025
@JamieB-gu JamieB-gu merged commit 8eba760 into main May 20, 2025
32 checks passed
@JamieB-gu JamieB-gu deleted the election-trackers-values-with-change branch May 20, 2025 10:40
SiAdcock pushed a commit that referenced this pull request May 20, 2025
Adds a new election trackers component to display a list of values and
the amount by which they've changed. This could be used, for example, to
display a list of seats won in an election along with the changes since
the last election. The included stories show examples of this.

This component is one of several that will allow rendering election
trackers within DCAR.
@prout-bot
Copy link

Seen on PROD (merged by @JamieB-gu 8 minutes and 48 seconds ago) Please check your changes!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants