Skip to content
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

Refactor positioning of radios and checkboxes #4093

Merged
merged 3 commits into from
Jan 9, 2024

Conversation

owenatgov
Copy link
Contributor

What

Refactor radios and checkboxes to use flexbox to handle positioning. Specific changes:

  • Remove all float positioning from radios and checkboxes
  • Use flexbox on labels so that their content is always centre aligned against the input
  • Calculate position of radio rings and checkbox boxes using the "gutter" of the input touch target aka the extra 2px around the 40px width inputs
  • Set labels to have a max width of the container minus the input touch target width and the padding on either side of the label
  • Set hints to always be 100% width to prevent them appearing inline against small labels, now that __item's are using flex-wrap: wrap
  • Calculate the radio button position automatically based on the width of the touch target and the border width of the radio button

Why

To make the positioning of radios and checkboxes dynamic in preparation for the new typography scale.

Closes #3898

Based off work done in #4040

Notes

Since this is only a refactor of the underlying positioning of the components, I'm expecting the Percy diff to be next to nothing, no more than a few pixels.

Whilst this is a significant change for us, it is intended to make little to no difference to users and therefore should be treated as a bug fix change.

@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-4093 August 16, 2023 11:30 Inactive
@36degrees
Copy link
Contributor

36degrees commented Aug 16, 2023

The small radios and checkboxes have changed in that the input is no longer left-aligned with the label:

Before After
Small checkbox aligned with the label, with the extra touch area pushed into the margin Small checkbox not aligned with the label, with the extra touch area included in the form group area

Looks like this hasn't been caught by Percy either – worth adding small radios and checkboxes, and maybe examples with hints, to our list of extra visual regression examples? Could be worth adding them with a separate PR to main and then rebasing so we can see the diff here.

@owenatgov owenatgov force-pushed the checkbox-radio-positioning-refactor branch from 74cb3da to ddfa48d Compare August 17, 2023 16:45
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-4093 August 17, 2023 16:46 Inactive
@owenatgov
Copy link
Contributor Author

@36degrees Good idea, I'll make a separate PR for this. I've addressed this comment in the mean time.

@owenatgov
Copy link
Contributor Author

Percy PR spun off from this one #4108

@owenatgov owenatgov force-pushed the checkbox-radio-positioning-refactor branch from ddfa48d to 8f5d781 Compare August 23, 2023 10:47
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-4093 August 23, 2023 10:47 Inactive
@@ -4,23 +4,18 @@
@import "../label/index";

@include govuk-exports("govuk/component/checkboxes") {
$govuk-touch-target-size: 44px;
$govuk-touch-target-gutter: 4px;
Copy link
Contributor

Choose a reason for hiding this comment

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

This PR looks brill. Thanks for clarifying the gutter

@owenatgov owenatgov force-pushed the checkbox-radio-positioning-refactor branch from 8f5d781 to b83975f Compare August 23, 2023 15:58
@owenatgov owenatgov marked this pull request as ready for review December 15, 2023 16:19
@colinrotherham
Copy link
Contributor

This one's got some (good) conflicts to de-duplicate govuk-font() since #4267 merged

If you don't mind doing another rebase please 🙏

Copy link

github-actions bot commented Dec 18, 2023

📋 Stats

File sizes

File Size
dist/govuk-frontend-development.min.css 112.45 KiB
dist/govuk-frontend-development.min.js 38.58 KiB
packages/govuk-frontend/dist/govuk/all.bundle.js 78.74 KiB
packages/govuk-frontend/dist/govuk/all.bundle.mjs 73.99 KiB
packages/govuk-frontend/dist/govuk/all.mjs 3.86 KiB
packages/govuk-frontend/dist/govuk/govuk-frontend-component.mjs 359 B
packages/govuk-frontend/dist/govuk/govuk-frontend.min.css 112.44 KiB
packages/govuk-frontend/dist/govuk/govuk-frontend.min.js 38.57 KiB
packages/govuk-frontend/dist/govuk/i18n.mjs 5.38 KiB

Modules

File Size
all.mjs 70.32 KiB
components/accordion/accordion.mjs 21.67 KiB
components/button/button.mjs 4.7 KiB
components/character-count/character-count.mjs 21.24 KiB
components/checkboxes/checkboxes.mjs 5.83 KiB
components/error-summary/error-summary.mjs 6.57 KiB
components/exit-this-page/exit-this-page.mjs 16.08 KiB
components/header/header.mjs 4.46 KiB
components/notification-banner/notification-banner.mjs 4.93 KiB
components/radios/radios.mjs 4.83 KiB
components/skip-link/skip-link.mjs 4.39 KiB
components/tabs/tabs.mjs 10.16 KiB

View stats and visualisations on the review app


Action run for 2486fed

Copy link

github-actions bot commented Dec 18, 2023

Stylesheets changes to npm package

diff --git a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.css b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.css
index e97dd32b3..081ea2694 100644
--- a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.css
+++ b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.css
@@ -2464,12 +2464,10 @@
 }
 
 .govuk-checkboxes__item {
-    display: block;
+    display: flex;
+    flex-wrap: wrap;
     position: relative;
-    min-height: 40px;
-    margin-bottom: 10px;
-    padding-left: 40px;
-    clear: left
+    margin-bottom: 10px
 }
 
 .govuk-checkboxes__item:last-child,
@@ -2478,10 +2476,6 @@
 }
 
 .govuk-checkboxes__input {
-    position: absolute;
-    z-index: 1;
-    top: -2px;
-    left: -2px;
     width: 44px;
     height: 44px;
     margin: 0;
@@ -2490,16 +2484,17 @@
 }
 
 .govuk-checkboxes__label {
-    display: inline-block;
+    align-self: center;
+    max-width: calc(100% - 74px);
     margin-bottom: 0;
-    padding: 8px 15px 5px;
+    padding: 7px 15px;
     cursor: pointer;
     touch-action: manipulation
 }
 
 .govuk-checkboxes__label:before {
-    top: 0;
-    left: 0;
+    top: 2px;
+    left: 2px;
     width: 40px;
     height: 40px;
     border: 2px solid
@@ -2514,8 +2509,8 @@
 }
 
 .govuk-checkboxes__label:after {
-    top: 11px;
-    left: 9px;
+    top: 13px;
+    left: 10px;
     width: 23px;
     height: 12px;
     transform: rotate(-45deg);
@@ -2527,8 +2522,14 @@
 
 .govuk-checkboxes__hint {
     display: block;
+    width: 100%;
+    margin-top: -5px;
     padding-right: 15px;
-    padding-left: 15px
+    padding-left: 59px
+}
+
+.govuk-label:not(.govuk-label--m):not(.govuk-label--l):not(.govuk-label--xl)+.govuk-checkboxes__hint {
+    margin-bottom: 0
 }
 
 .govuk-checkboxes__input:focus+.govuk-checkboxes__label:before {
@@ -2610,47 +2611,28 @@ screen and (forced-colors:active) {
     display: none
 }
 
+.govuk-checkboxes--small .govuk-checkboxes__item,
 .govuk-checkboxes__conditional>:last-child {
     margin-bottom: 0
 }
 
-.govuk-checkboxes--small .govuk-checkboxes__item {
-    min-height: 0;
-    margin-bottom: 0;
-    padding-left: 34px;
-    float: left
-}
-
-.govuk-checkboxes--small .govuk-checkboxes__item:after {
-    content: "";
-    display: block;
-    clear: both
-}
-
 .govuk-checkboxes--small .govuk-checkboxes__input {
-    left: -10px
+    margin-left: -10px
 }
 
 .govuk-checkboxes--small .govuk-checkboxes__label {
-    margin-top: -2px;
-    padding: 13px 15px 13px 1px;
-    float: left
-}
-
-@media (min-width:40.0625em) {
-    .govuk-checkboxes--small .govuk-checkboxes__label {
-        padding: 11px 15px 10px 1px
-    }
+    padding-left: 1px
 }
 
 .govuk-checkboxes--small .govuk-checkboxes__label:before {
-    top: 8px;
+    top: 10px;
+    left: 0;
     width: 24px;
     height: 24px
 }
 
 .govuk-checkboxes--small .govuk-checkboxes__label:after {
-    top: 15px;
+    top: 17px;
     left: 6px;
     width: 12px;
     height: 6.5px;
@@ -2658,14 +2640,12 @@ screen and (forced-colors:active) {
 }
 
 .govuk-checkboxes--small .govuk-checkboxes__hint {
-    padding: 0;
-    clear: both
+    padding-left: 34px
 }
 
 .govuk-checkboxes--small .govuk-checkboxes__conditional {
     margin-left: 10px;
-    padding-left: 20px;
-    clear: both
+    padding-left: 20px
 }
 
 .govuk-checkboxes--small .govuk-checkboxes__item:hover .govuk-checkboxes__input:not(:disabled)+.govuk-checkboxes__label:before {
@@ -4823,12 +4803,10 @@ only screen and (min-resolution:2dppx) {
 }
 
 .govuk-radios__item {
-    display: block;
+    display: flex;
+    flex-wrap: wrap;
     position: relative;
-    min-height: 40px;
-    margin-bottom: 10px;
-    padding-left: 40px;
-    clear: left
+    margin-bottom: 10px
 }
 
 .govuk-radios__item:last-child,
@@ -4837,10 +4815,6 @@ only screen and (min-resolution:2dppx) {
 }
 
 .govuk-radios__input {
-    position: absolute;
-    z-index: 1;
-    top: -2px;
-    left: -2px;
     width: 44px;
     height: 44px;
     margin: 0;
@@ -4849,9 +4823,10 @@ only screen and (min-resolution:2dppx) {
 }
 
 .govuk-radios__label {
-    display: inline-block;
+    align-self: center;
+    max-width: calc(100% - 74px);
     margin-bottom: 0;
-    padding: 8px 15px 5px;
+    padding: 7px 15px;
     cursor: pointer;
     touch-action: manipulation
 }
@@ -4860,8 +4835,8 @@ only screen and (min-resolution:2dppx) {
     content: "";
     box-sizing: border-box;
     position: absolute;
-    top: 0;
-    left: 0;
+    top: 2px;
+    left: 2px;
     width: 40px;
     height: 40px;
     border: 2px solid;
@@ -4872,8 +4847,8 @@ only screen and (min-resolution:2dppx) {
 .govuk-radios__label:after {
     content: "";
     position: absolute;
-    top: 10px;
-    left: 10px;
+    top: 12px;
+    left: 12px;
     width: 0;
     height: 0;
     border: 10px solid;
@@ -4884,8 +4859,14 @@ only screen and (min-resolution:2dppx) {
 
 .govuk-radios__hint {
     display: block;
+    width: 100%;
+    margin-top: -5px;
     padding-right: 15px;
-    padding-left: 15px
+    padding-left: 59px
+}
+
+.govuk-label:not(.govuk-label--m):not(.govuk-label--l):not(.govuk-label--xl)+.govuk-radios__hint {
+    margin-bottom: 0
 }
 
 .govuk-radios__input:focus+.govuk-radios__label:before {
@@ -4917,16 +4898,14 @@ screen and (forced-colors:active) {
 }
 
 @media (min-width:40.0625em) {
-    .govuk-radios--inline:after {
-        content: "";
-        display: block;
-        clear: both
+    .govuk-radios--inline {
+        display: flex;
+        flex-wrap: wrap;
+        align-items: flex-start
     }
 
     .govuk-radios--inline .govuk-radios__item {
-        margin-right: 20px;
-        float: left;
-        clear: none
+        margin-right: 20px
     }
 }
 
@@ -4981,61 +4960,39 @@ screen and (forced-colors:active) {
     display: none
 }
 
+.govuk-radios--small .govuk-radios__item,
 .govuk-radios__conditional>:last-child {
     margin-bottom: 0
 }
 
-.govuk-radios--small .govuk-radios__item {
-    min-height: 0;
-    margin-bottom: 0;
-    padding-left: 34px;
-    float: left
-}
-
-.govuk-radios--small .govuk-radios__item:after {
-    content: "";
-    display: block;
-    clear: both
-}
-
 .govuk-radios--small .govuk-radios__input {
-    left: -10px
+    margin-left: -10px
 }
 
 .govuk-radios--small .govuk-radios__label {
-    margin-top: -2px;
-    padding: 13px 15px 13px 1px;
-    float: left
-}
-
-@media (min-width:40.0625em) {
-    .govuk-radios--small .govuk-radios__label {
-        padding: 11px 15px 10px 1px
-    }
+    padding-left: 1px
 }
 
 .govuk-radios--small .govuk-radios__label:before {
-    top: 8px;
+    top: 10px;
+    left: 0;
     width: 24px;
     height: 24px
 }
 
 .govuk-radios--small .govuk-radios__label:after {
-    top: 15px;
+    top: 17px;
     left: 7px;
     border-width: 5px
 }
 
 .govuk-radios--small .govuk-radios__hint {
-    padding: 0;
-    clear: both;
-    pointer-events: none
+    padding-left: 34px
 }
 
 .govuk-radios--small .govuk-radios__conditional {
     margin-left: 10px;
-    padding-left: 20px;
-    clear: both
+    padding-left: 20px
 }
 
 .govuk-radios--small .govuk-radios__divider {

Action run for 2486fed

Copy link

github-actions bot commented Dec 18, 2023

Other changes to npm package

diff --git a/packages/govuk-frontend/dist/govuk/components/checkboxes/_index.scss b/packages/govuk-frontend/dist/govuk/components/checkboxes/_index.scss
index 2f5d7f741..5b1223d69 100644
--- a/packages/govuk-frontend/dist/govuk/components/checkboxes/_index.scss
+++ b/packages/govuk-frontend/dist/govuk/components/checkboxes/_index.scss
@@ -4,21 +4,18 @@
 @import "../label/index";
 
 @include govuk-exports("govuk/component/checkboxes") {
-  $govuk-touch-target-size: 44px;
+  $govuk-touch-target-gutter: 4px;
   $govuk-checkboxes-size: 40px;
+  $govuk-touch-target-size: ($govuk-checkboxes-size + $govuk-touch-target-gutter);
   $govuk-small-checkboxes-size: 24px;
   $govuk-checkboxes-label-padding-left-right: govuk-spacing(3);
+  $govuk-checkbox-check-horizontal-position: 10px;
 
   .govuk-checkboxes__item {
-    display: block;
+    display: flex;
+    flex-wrap: wrap;
     position: relative;
-
-    min-height: $govuk-checkboxes-size;
-
     margin-bottom: govuk-spacing(2);
-    padding-left: $govuk-checkboxes-size;
-
-    clear: left;
   }
 
   .govuk-checkboxes__item:last-child,
@@ -27,27 +24,23 @@
   }
 
   .govuk-checkboxes__input {
-    $input-offset: ($govuk-touch-target-size - $govuk-checkboxes-size) / 2;
-
-    position: absolute;
-
-    z-index: 1;
-    top: $input-offset * -1;
-    left: $input-offset * -1;
-
     width: $govuk-touch-target-size;
     height: $govuk-touch-target-size;
     margin: 0;
-
     opacity: 0;
-
     cursor: pointer;
   }
 
   .govuk-checkboxes__label {
-    display: inline-block;
+    align-self: center;
+
+    // Ensure that the width of the label is never more than the width of the
+    // container minus the input width minus the padding on either side of
+    // the label. This prevents the label from going onto the next line due to
+    // __item using flex-wrap because we want hints on a separate line.
+    max-width: calc(100% - (($govuk-checkboxes-label-padding-left-right * 2) + $govuk-touch-target-size));
     margin-bottom: 0;
-    padding: 8px $govuk-checkboxes-label-padding-left-right govuk-spacing(1);
+    padding: (govuk-spacing(1) + $govuk-border-width-form-element) govuk-spacing(3);
     cursor: pointer;
     // remove 300ms pause on mobile
     touch-action: manipulation;
@@ -58,8 +51,8 @@
     content: "";
     box-sizing: border-box;
     position: absolute;
-    top: 0;
-    left: 0;
+    top: ($govuk-touch-target-gutter / 2);
+    left: ($govuk-touch-target-gutter / 2);
     width: $govuk-checkboxes-size;
     height: $govuk-checkboxes-size;
     border: $govuk-border-width-form-element solid currentcolor;
@@ -73,29 +66,37 @@
   .govuk-checkboxes__label::after {
     content: "";
     box-sizing: border-box;
-
     position: absolute;
-    top: 11px;
-    left: 9px;
+
+    // Use "magic numbers" to define shape and position of check mark because
+    // the complexity of the shape makes it difficult to calculate dynamically.
+    top: 13px;
+    left: $govuk-checkbox-check-horizontal-position;
     width: 23px;
     height: 12px;
-
     transform: rotate(-45deg);
     border: solid;
     border-width: 0 0 5px 5px;
     // Fix bug in IE11 caused by transform rotate (-45deg).
     // See: alphagov/govuk_elements/issues/518
     border-top-color: transparent;
-
     opacity: 0;
-
     background: transparent;
   }
 
   .govuk-checkboxes__hint {
     display: block;
+    width: 100%;
+    margin-top: govuk-spacing(-1);
     padding-right: $govuk-checkboxes-label-padding-left-right;
-    padding-left: $govuk-checkboxes-label-padding-left-right;
+    padding-left: ($govuk-checkboxes-label-padding-left-right + $govuk-touch-target-size);
+  }
+
+  // This is to bypass govuk-hint's specificity on hints following labels having
+  // a margin bottom of 10px (govuk-spacing(2)). Because checkboxes are flexbox,
+  // the margin doesn't collapse so we have to do this manually.
+  .govuk-label:not(.govuk-label--m):not(.govuk-label--l):not(.govuk-label--xl) + .govuk-checkboxes__hint {
+    margin-bottom: 0;
   }
 
   // Focused state
@@ -182,14 +183,9 @@
 
   .govuk-checkboxes--small {
     $input-offset: ($govuk-touch-target-size - $govuk-small-checkboxes-size) / 2;
-    $label-offset: $govuk-touch-target-size - $input-offset;
 
     .govuk-checkboxes__item {
-      @include govuk-clearfix;
-      min-height: 0;
       margin-bottom: 0;
-      padding-left: $label-offset;
-      float: left;
     }
 
     // Shift the touch target into the left margin so that the visible edge of
@@ -202,30 +198,23 @@
     //  ▲┆└─ Check box pseudo element, aligned with margin
     //  └─── Touch target (invisible input), shifted into the margin
     .govuk-checkboxes__input {
-      left: $input-offset * -1;
+      margin-left: $input-offset * -1;
     }
 
-    // Adjust the size and position of the label.
-    //
-    // Unlike larger checkboxes, we also have to float the label in order to
-    // 'shrink' it, preventing the hover state from kicking in across the full
-    // width of the parent element.
     .govuk-checkboxes__label {
-      margin-top: -2px;
-      padding: 13px govuk-spacing(3) 13px 1px;
-      float: left;
-
-      @include govuk-media-query($from: tablet) {
-        padding: 11px govuk-spacing(3) 10px 1px;
-      }
+      // Create a tiny space between the small checkbox hover state so that it
+      // doesn't clash with the label
+      padding-left: 1px;
     }
 
     // [ ] Check box
     //
     // Reduce the size of the check box [1], vertically center it within the
     // touch target [2]
+    // Left here is 0 because we've shifted the input into the left margin
     .govuk-checkboxes__label::before {
-      top: $input-offset - $govuk-border-width-form-element; // 2
+      top: $input-offset; // 2
+      left: 0;
       width: $govuk-small-checkboxes-size; // 1
       height: $govuk-small-checkboxes-size; // 1
     }
@@ -234,8 +223,11 @@
     //
     // Reduce the size of the check mark and re-align within the checkbox
     .govuk-checkboxes__label::after {
-      top: 15px;
-      left: 6px;
+      top: 17px;
+
+      // Horizontal position is just the normal sized left value accounting for
+      // the new width of the smaller checkbox
+      left: (16px - $govuk-checkbox-check-horizontal-position);
       width: 12px;
       height: 6.5px;
       border-width: 0 0 3px 3px;
@@ -250,16 +242,14 @@
     // (If you do use them, they won't look completely broken... but seriously,
     // don't use them)
     .govuk-checkboxes__hint {
-      padding: 0;
-      clear: both;
+      padding-left: ($govuk-small-checkboxes-size + $input-offset);
     }
 
     // Align conditional reveals with small checkboxes
     .govuk-checkboxes__conditional {
       $margin-left: ($govuk-small-checkboxes-size / 2) - ($conditional-border-width / 2);
       margin-left: $margin-left;
-      padding-left: $label-offset - ($margin-left + $conditional-border-width);
-      clear: both;
+      padding-left: ($govuk-touch-target-size - $input-offset) - ($margin-left + $conditional-border-width);
     }
 
     // Hover state for small checkboxes.
diff --git a/packages/govuk-frontend/dist/govuk/components/radios/_index.scss b/packages/govuk-frontend/dist/govuk/components/radios/_index.scss
index 0a506cb8e..804aeb214 100644
--- a/packages/govuk-frontend/dist/govuk/components/radios/_index.scss
+++ b/packages/govuk-frontend/dist/govuk/components/radios/_index.scss
@@ -4,8 +4,9 @@
 @import "../label/index";
 
 @include govuk-exports("govuk/component/radios") {
-  $govuk-touch-target-size: 44px;
+  $govuk-touch-target-gutter: 4px;
   $govuk-radios-size: 40px;
+  $govuk-touch-target-size: ($govuk-radios-size + $govuk-touch-target-gutter);
   $govuk-small-radios-size: 24px;
   $govuk-radios-label-padding-left-right: govuk-spacing(3);
   // When the default focus width is used on a curved edge it looks visually smaller.
@@ -13,15 +14,10 @@
   $govuk-radios-focus-width: $govuk-focus-width + 1px;
 
   .govuk-radios__item {
-    display: block;
+    display: flex;
+    flex-wrap: wrap;
     position: relative;
-
-    min-height: $govuk-radios-size;
-
     margin-bottom: govuk-spacing(2);
-    padding-left: $govuk-radios-size;
-
-    clear: left;
   }
 
   .govuk-radios__item:last-child,
@@ -30,27 +26,23 @@
   }
 
   .govuk-radios__input {
-    $input-offset: ($govuk-touch-target-size - $govuk-radios-size) / 2;
-
-    position: absolute;
-
-    z-index: 1;
-    top: $input-offset * -1;
-    left: $input-offset * -1;
-
     width: $govuk-touch-target-size;
     height: $govuk-touch-target-size;
     margin: 0;
-
     opacity: 0;
-
     cursor: pointer;
   }
 
   .govuk-radios__label {
-    display: inline-block;
+    align-self: center;
+
+    // Ensure that the width of the label is never more than the width of the
+    // container minus the input width minus the padding on either side of
+    // the label. This prevents the label from going onto the next line due to
+    // __item using flex-wrap because we want hints on a separate line
+    max-width: calc(100% - ($govuk-radios-label-padding-left-right + $govuk-touch-target-size + govuk-spacing(3)));
     margin-bottom: 0;
-    padding: 8px $govuk-radios-label-padding-left-right govuk-spacing(1);
+    padding: (govuk-spacing(1) + $govuk-border-width-form-element) govuk-spacing(3);
     cursor: pointer;
     // remove 300ms pause on mobile
     touch-action: manipulation;
@@ -61,12 +53,10 @@
     content: "";
     box-sizing: border-box;
     position: absolute;
-    top: 0;
-    left: 0;
-
+    top: ($govuk-touch-target-gutter / 2);
+    left: ($govuk-touch-target-gutter / 2);
     width: $govuk-radios-size;
     height: $govuk-radios-size;
-
     border: $govuk-border-width-form-element solid currentcolor;
     border-radius: 50%;
     background: transparent;
@@ -77,16 +67,19 @@
   // We create the 'button' entirely out of 'border' so that they remain
   // 'filled' even when colours are overridden in the browser.
   .govuk-radios__label::after {
-    content: "";
+    $radio-button-size: govuk-spacing(2);
 
+    content: "";
     position: absolute;
-    top: govuk-spacing(2);
-    left: govuk-spacing(2);
 
+    // Positioned by getting half the touch target, so we have the centre of the
+    // input, and then moving back by the button's border width, thus positioning
+    // the centre of the button in the centre of the input.
+    top: (($govuk-touch-target-size / 2) - $radio-button-size);
+    left: (($govuk-touch-target-size / 2) - $radio-button-size);
     width: 0;
     height: 0;
-
-    border: govuk-spacing(2) solid currentcolor;
+    border: $radio-button-size solid currentcolor;
     border-radius: 50%;
     opacity: 0;
     background: currentcolor;
@@ -94,8 +87,17 @@
 
   .govuk-radios__hint {
     display: block;
+    width: 100%;
+    margin-top: govuk-spacing(-1);
     padding-right: $govuk-radios-label-padding-left-right;
-    padding-left: $govuk-radios-label-padding-left-right;
+    padding-left: ($govuk-radios-label-padding-left-right + $govuk-touch-target-size);
+  }
+
+  // This is to bypass govuk-hint's specificity on hints following labels having
+  // a margin bottom of 10px (govuk-spacing(2)). Because radios are flexbox,
+  // the margin doesn't collapse so we have to do this manually.
+  .govuk-label:not(.govuk-label--m):not(.govuk-label--l):not(.govuk-label--xl) + .govuk-radios__hint {
+    margin-bottom: 0;
   }
 
   // Focused state
@@ -140,12 +142,12 @@
 
   .govuk-radios--inline {
     @include govuk-media-query($from: tablet) {
-      @include govuk-clearfix;
+      display: flex;
+      flex-wrap: wrap;
+      align-items: flex-start;
 
       .govuk-radios__item {
         margin-right: govuk-spacing(4);
-        float: left;
-        clear: none;
       }
     }
   }
@@ -198,14 +200,9 @@
 
   .govuk-radios--small {
     $input-offset: ($govuk-touch-target-size - $govuk-small-radios-size) / 2;
-    $label-offset: $govuk-touch-target-size - $input-offset;
 
     .govuk-radios__item {
-      @include govuk-clearfix;
-      min-height: 0;
       margin-bottom: 0;
-      padding-left: $label-offset;
-      float: left;
     }
 
     // Shift the touch target into the left margin so that the visible edge of
@@ -218,30 +215,23 @@
     //  ▲┆└─ Radio pseudo element, aligned with margin
     //  └─── Touch target (invisible input), shifted into the margin
     .govuk-radios__input {
-      left: $input-offset * -1;
+      margin-left: $input-offset * -1;
     }
 
-    // Adjust the size and position of the label.
-    //
-    // Unlike larger radios, we also have to float the label in order to
-    // 'shrink' it, preventing the hover state from kicking in across the full
-    // width of the parent element.
     .govuk-radios__label {
-      margin-top: -2px;
-      padding: 13px govuk-spacing(3) 13px 1px;
-      float: left;
-
-      @include govuk-media-query($from: tablet) {
-        padding: 11px govuk-spacing(3) 10px 1px;
-      }
+      // Create a tiny space between the small radio hover state so that it
+      // doesn't clash with the label
+      padding-left: 1px;
     }
 
     // ( ) Radio ring
     //
     // Reduce the size of the control [1], vertically centering it within the
     // touch target [2]
+    // Left here is 0 because we've shifted the input into the left margin
     .govuk-radios__label::before {
-      top: $input-offset - $govuk-border-width-form-element; // 2
+      top: $input-offset; // 2
+      left: 0;
       width: $govuk-small-radios-size; // 1
       height: $govuk-small-radios-size; // 1
     }
@@ -250,9 +240,12 @@
     //
     // Reduce the size of the 'button' and center it within the ring
     .govuk-radios__label::after {
-      top: 15px;
-      left: 7px;
-      border-width: 5px;
+      $radio-button-size: govuk-spacing(1);
+
+      // The same calculation as normal radio buttons but reduce the border width
+      top: (($govuk-touch-target-size / 2) - $radio-button-size);
+      left: ((($govuk-touch-target-size / 2) - $radio-button-size) - $input-offset);
+      border-width: $radio-button-size;
     }
 
     // Fix position of hint with small radios
@@ -264,17 +257,14 @@
     // (If you do use them, they won't look completely broken... but seriously,
     // don't use them)
     .govuk-radios__hint {
-      padding: 0;
-      clear: both;
-      pointer-events: none;
+      padding-left: ($govuk-small-radios-size + $input-offset);
     }
 
     // Align conditional reveals with small radios
     .govuk-radios__conditional {
       $margin-left: ($govuk-small-radios-size / 2) - ($conditional-border-width / 2);
       margin-left: $margin-left;
-      padding-left: $label-offset - ($margin-left + $conditional-border-width);
-      clear: both;
+      padding-left: ($govuk-touch-target-size - $input-offset) - ($margin-left + $conditional-border-width);
     }
 
     .govuk-radios__divider {

Action run for 2486fed

@colinrotherham
Copy link
Contributor

Are you happy with the Percy diff @owenatgov?

The gutter changes look good, but did we mean to intentionally space things out vertically?

@owenatgov
Copy link
Contributor Author

Now also applied to radios 👀

@colinrotherham
Copy link
Contributor

…The specificity is preventing this being removed simply without some raucous styling. For now I've removed the bottom margin from govuk-checkboxes__item, which I don't really like but it does the job.

@owenatgov Bit annoying, cheers for that

Hmm, might have to tackle it though as it's made checkboxes (without hints) really close together 😣

@owenatgov
Copy link
Contributor Author

🤦🏻 🤦🏻 🤦🏻 Yeah that'll do that won't it...

Let's have another go.

@owenatgov
Copy link
Contributor Author

@colinrotherham I cracked it by just replicating the hint's specificity. Probably the best solution for this for the moment, unless you can figure out how to collapse margins in flexbox (I couldn't!).

Copy link
Contributor

@colinrotherham colinrotherham left a comment

Choose a reason for hiding this comment

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

Thanks @owenatgov, looking good

Can you double check small radios/checkboxes for me?

Percy is showing wider spacing everywhere (good) but for small ones only the labels have shifted a tiny bit closer to their inputs. If design people are happy, go for it

This means that the label content is still centred against the input whilst not interfering with inline/block element dynamics within the label
@owenatgov owenatgov force-pushed the checkbox-radio-positioning-refactor branch from 93e0a93 to 2486fed Compare January 8, 2024 11:09
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-4093 January 8, 2024 11:09 Inactive
@owenatgov
Copy link
Contributor Author

@colinrotherham I think I've figured it out. On the live version, small variants have a 1px right padding. I couldn't find any concrete reasoning from a quick history skim but my suspicion is that it's to create a little bit of space between the small hover state and the label. For ease I've reintroduced it. Whatcha reckon?

@owenatgov owenatgov merged commit b41f4e7 into main Jan 9, 2024
44 of 45 checks passed
@owenatgov owenatgov deleted the checkbox-radio-positioning-refactor branch January 9, 2024 15:33
owenatgov added a commit that referenced this pull request Jan 11, 2024
…actor

Refactor positioning of radios and checkboxes
@querkmachine querkmachine mentioned this pull request Feb 5, 2024
jsrobertson added a commit to ministryofjustice/hmpps-accredited-programmes-ui that referenced this pull request Feb 8, 2024
This was raised by the renovate bot to begin with but was causing the integration tests to fail with the error `is being covered by another element`, because it couldn't check radio and checkboxes.

Looking at what is in the 5.1.0 release, it includes a change to radio and checkbox styling, which I believe is what is causing the issue. alphagov/govuk-frontend#4093. It looks like the removal of the `z-index` on `.govuk-radios__input` and `.govuk-checkboxes__input` has meant  they now have `z-index: auto` but the styled `label:before` and :`after` have `z-index: 1` which causes them to be higher in the `z-order` than the inputs, which causes the cypress error above.

Updating `selectRadioButton()` and adding `selectCheckbox()` to `check()` with `force: true`, forces the click on the radio button even though it is covered by the labels pseudo elements.
jsrobertson added a commit to ministryofjustice/hmpps-accredited-programmes-e2e that referenced this pull request Feb 8, 2024
Looking at what is in the 5.1.0 release, it includes a change to radio and checkbox styling, which I believe is what is causing the issue. alphagov/govuk-frontend#4093. It looks like the removal of the `z-index` on `.govuk-radios__input` and `.govuk-checkboxes__input` has meant  they now have `z-index: auto` but the styled `label:before` and :`after` have `z-index: 1` which causes them to be higher in the `z-order` than the inputs, which means they’re underneath another element and technically not clickable.

Adding `force: true`, forces the click on the radio button and checkbox even though it is covered by the labels pseudo elements.

This does not affect the actual usability.
tpmcgowan added a commit to ministryofjustice/book-a-prison-visit-staff-ui that referenced this pull request Feb 18, 2024
Force Cypress to check checkboxes if it thinks they're overlapped.

Looks to be caused by changes in GOV.UK Frontend v5.1.0
(alphagov/govuk-frontend#4093)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Fix alignment of small variants of radios and checkboxes
5 participants