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

File input new dropzone design #5642

Open
wants to merge 2 commits into
base: spike-enhanced-file-upload
Choose a base branch
from

Conversation

patrickpatrickpatrick
Copy link
Contributor

@patrickpatrickpatrick patrickpatrickpatrick commented Jan 22, 2025

What

Implement the new file input design.

Why

Fixes #5611

@patrickpatrickpatrick patrickpatrickpatrick changed the base branch from main to use-output January 22, 2025 16:09
Copy link

github-actions bot commented Jan 22, 2025

📋 Stats

File sizes

File Size
dist/govuk-frontend-development.min.css 120.55 KiB
dist/govuk-frontend-development.min.js 47.63 KiB
packages/govuk-frontend/dist/govuk/all.bundle.js 101.84 KiB
packages/govuk-frontend/dist/govuk/all.bundle.mjs 95.69 KiB
packages/govuk-frontend/dist/govuk/all.mjs 1.32 KiB
packages/govuk-frontend/dist/govuk/govuk-frontend-component.mjs 1.74 KiB
packages/govuk-frontend/dist/govuk/govuk-frontend.min.css 120.54 KiB
packages/govuk-frontend/dist/govuk/govuk-frontend.min.js 47.61 KiB
packages/govuk-frontend/dist/govuk/i18n.mjs 5.55 KiB
packages/govuk-frontend/dist/govuk/init.mjs 7.5 KiB

Modules

File Size (bundled) Size (minified)
all.mjs 89.82 KiB 45.15 KiB
accordion.mjs 26.58 KiB 13.41 KiB
button.mjs 9.09 KiB 3.78 KiB
character-count.mjs 25.39 KiB 10.9 KiB
checkboxes.mjs 7.81 KiB 3.42 KiB
error-summary.mjs 10.99 KiB 4.54 KiB
exit-this-page.mjs 20.2 KiB 10.34 KiB
file-upload.mjs 20.39 KiB 10.74 KiB
header.mjs 6.46 KiB 3.22 KiB
notification-banner.mjs 9.35 KiB 3.7 KiB
password-input.mjs 18.24 KiB 8.33 KiB
radios.mjs 6.81 KiB 2.98 KiB
service-navigation.mjs 6.44 KiB 3.26 KiB
skip-link.mjs 6.4 KiB 2.76 KiB
tabs.mjs 12.04 KiB 6.67 KiB

View stats and visualisations on the review app


Action run for ede849e

@patrickpatrickpatrick patrickpatrickpatrick changed the title file dropzone design File input new dropzone design Jan 22, 2025
Copy link

github-actions bot commented Jan 22, 2025

JavaScript changes to npm package

diff --git a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
index 4b409f096..2e0a33ee4 100644
--- a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
+++ b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
@@ -757,14 +757,20 @@ class FileUpload extends ConfigurableComponent {
         }), this.$label = this.findLabel(), this.$root.id = `${this.id}-input`;
         const n = document.createElement("div");
         n.className = "govuk-file-upload-wrapper";
-        const i = document.createElement("button");
-        i.classList.add("govuk-file-upload__button"), i.type = "button", i.id = this.id;
-        const s = this.$root.getAttribute("aria-describedby");
-        s && i.setAttribute("aria-describedby", s);
-        const o = document.createElement("span");
-        o.className = "govuk-button govuk-button--secondary govuk-file-upload__pseudo-button", o.innerText = this.i18n.t("selectFilesButton"), o.setAttribute("aria-hidden", "true"), i.appendChild(o), i.addEventListener("click", this.onClick.bind(this));
+        const i = document.createElement("span");
+        i.className = "govuk-visually-hidden", i.innerText = ", ";
+        const s = document.createElement("button");
+        s.classList.add("govuk-file-upload__button"), s.type = "button", s.id = this.id;
+        const o = this.$root.getAttribute("aria-describedby");
+        o && s.setAttribute("aria-describedby", o);
         const r = document.createElement("span");
-        r.className = "govuk-body govuk-file-upload__status", r.innerText = this.i18n.t("filesSelectedDefault"), r.setAttribute("aria-hidden", "true"), i.appendChild(r), i.setAttribute("aria-label", `${this.$label.innerText}, ${this.i18n.t("selectFilesButton")}, ${this.i18n.t("filesSelectedDefault")}`), n.insertAdjacentElement("beforeend", i), this.$root.insertAdjacentElement("afterend", n), this.$root.setAttribute("tabindex", "-1"), this.$root.setAttribute("aria-hidden", "true"), n.insertAdjacentElement("afterbegin", this.$root), this.$wrapper = n, this.$button = i, this.$status = r, this.$root.addEventListener("change", this.onChange.bind(this)), this.updateDisabledState(), this.observeDisabledState(), this.$root.addEventListener("change", this.onChange.bind(this)), this.$announcements = document.createElement("span"), this.$announcements.classList.add("govuk-file-upload-announcements"), this.$announcements.classList.add("govuk-visually-hidden"), this.$announcements.setAttribute("aria-live", "assertive"), this.$wrapper.insertAdjacentElement("afterend", this.$announcements), this.$wrapper.addEventListener("drop", this.hideDropZone.bind(this)), document.addEventListener("dragenter", this.updateDropzoneVisibility.bind(this)), document.addEventListener("dragenter", (() => {
+        r.className = "govuk-body govuk-file-upload__status", r.innerText = this.i18n.t("filesSelectedDefault"), r.setAttribute("aria-hidden", "true"), r.classList.add("govuk-file-upload__status--empty"), s.appendChild(r), s.appendChild(i.cloneNode(!0));
+        const a = document.createElement("span");
+        a.className = "govuk-file-upload__pseudo-button-container";
+        const l = document.createElement("span");
+        l.className = "govuk-button govuk-button--secondary govuk-file-upload__pseudo-button", l.innerText = this.i18n.t("selectFilesButton"), l.setAttribute("aria-hidden", "true"), a.appendChild(l), a.appendChild(i.cloneNode(!0));
+        const c = document.createElement("span");
+        c.className = "govuk-body govuk-file-upload__instruction", c.innerText = this.i18n.t("instruction"), a.appendChild(c), s.appendChild(a), s.setAttribute("aria-label", `${this.$label.innerText}, ${this.i18n.t("selectFilesButton")} ${this.i18n.t("instruction")}, ${r.innerText}`), s.addEventListener("click", this.onClick.bind(this)), n.insertAdjacentElement("beforeend", s), this.$root.insertAdjacentElement("afterend", n), this.$root.setAttribute("tabindex", "-1"), this.$root.setAttribute("aria-hidden", "true"), n.insertAdjacentElement("afterbegin", this.$root), this.$wrapper = n, this.$button = s, this.$status = r, this.$root.addEventListener("change", this.onChange.bind(this)), this.updateDisabledState(), this.observeDisabledState(), this.$announcements = document.createElement("span"), this.$announcements.classList.add("govuk-file-upload-announcements"), this.$announcements.classList.add("govuk-visually-hidden"), this.$announcements.setAttribute("aria-live", "assertive"), this.$wrapper.insertAdjacentElement("afterend", this.$announcements), this.$wrapper.addEventListener("drop", this.hideDropZone.bind(this)), document.addEventListener("dragenter", this.updateDropzoneVisibility.bind(this)), document.addEventListener("dragenter", (() => {
             this.enteredAnotherElement = !0
         })), document.addEventListener("dragleave", (() => {
             this.enteredAnotherElement || this.hideDropZone(), this.enteredAnotherElement = !1
@@ -782,9 +788,9 @@ class FileUpload extends ConfigurableComponent {
     }
     onChange() {
         const t = this.$root.files.length;
-        this.$status.innerText = 0 === t ? this.i18n.t("filesSelectedDefault") : 1 === t ? this.$root.files[0].name : this.i18n.t("filesSelected", {
+        0 === t ? (this.$status.innerText = this.i18n.t("filesSelectedDefault"), this.$status.classList.add("govuk-file-upload__status--empty")) : (this.$status.innerText = 1 === t ? this.$root.files[0].name : this.i18n.t("filesSelected", {
             count: t
-        }), this.$button.setAttribute("aria-label", `${this.$label.innerText}, ${this.i18n.t("selectFilesButton")}, ${this.$status.innerText}`)
+        }), this.$status.classList.remove("govuk-file-upload__status--empty")), this.$button.setAttribute("aria-label", `${this.$label.innerText}, ${this.i18n.t("selectFilesButton")} ${this.i18n.t("instruction")}, ${this.$status.innerText}`)
     }
     findLabel() {
         const t = document.querySelector(`label[for="${this.$root.id}"]`);
@@ -817,7 +823,8 @@ FileUpload.moduleName = "govuk-file-upload", FileUpload.defaults = Object.freeze
             other: "%{count} files chosen"
         },
         dropZoneEntered: "Entered drop zone",
-        dropZoneLeft: "Left drop zone"
+        dropZoneLeft: "Left drop zone",
+        instruction: "or drop file"
     }
 }), FileUpload.schema = Object.freeze({
     properties: {

Action run for ede849e

Copy link

github-actions bot commented Jan 22, 2025

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 9417707ca..e401932ba 100644
--- a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.css
+++ b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.css
@@ -3385,25 +3385,15 @@ screen and (forced-colors:active) {
 }
 
 .govuk-file-upload-wrapper {
-    display: inline-flex;
-    align-items: baseline;
-    position: relative
-}
-
-.govuk-file-upload-wrapper--show-dropzone {
-    margin: -12px;
-    padding: 10px;
-    border: 2px dashed #0b0c0c;
+    display: block;
+    position: relative;
+    z-index: 0;
     background-color: #fff
 }
 
-.govuk-file-upload-wrapper--show-dropzone .govuk-file-upload__pseudo-button,
-.govuk-file-upload-wrapper--show-dropzone .govuk-file-upload__status {
-    pointer-events: none
-}
-
 .govuk-file-upload-wrapper .govuk-file-upload {
     position: absolute;
+    z-index: -1;
     top: 0;
     left: 0;
     width: 100%;
@@ -3413,63 +3403,101 @@ screen and (forced-colors:active) {
     opacity: 0
 }
 
+.govuk-file-upload-wrapper--show-dropzone .govuk-file-upload {
+    z-index: 1
+}
+
 .govuk-file-upload__pseudo-button {
     width: auto;
-    margin-bottom: 0;
-    flex-grow: 0;
+    margin-right: 10px;
+    margin-bottom: 3px;
     flex-shrink: 0
 }
 
-.govuk-file-upload__status {
+.govuk-file-upload__instruction {
+    margin-top: 7px;
     margin-bottom: 0;
-    margin-left: 10px
+    text-align: left
 }
 
-.govuk-file-upload__button:focus {
-    outline: none
+.govuk-file-upload__status {
+    display: block;
+    margin-bottom: 10px;
+    padding: 15px 10px;
+    text-align: left
 }
 
-.govuk-file-upload__button:focus .govuk-file-upload__pseudo-button {
-    outline: 3px solid transparent;
-    background-color: #fd0;
-    box-shadow: 0 2px 0 #0b0c0c
+.govuk-file-upload__status--empty {
+    color: #0c2d4a;
+    background-color: #bbd4ea
 }
 
-.govuk-file-upload__button:focus .govuk-file-upload__pseudo-button:hover {
-    border-color: #fd0;
-    outline: 3px solid transparent;
-    background-color: #f3f2f1;
-    box-shadow: inset 0 0 0 1px #fd0
+.govuk-file-upload__pseudo-button-container {
+    display: flex;
+    align-items: baseline;
+    flex-wrap: wrap
+}
+
+.govuk-file-upload__button {
+    width: 100%;
+    padding: 15px 18px;
+    border: 2px dashed #b1b4b6;
+    background-color: #fff;
+    cursor: pointer
 }
 
-.govuk-file-upload__button:active .govuk-file-upload__pseudo-button:hover {
-    background-color: #c2c2c1
+@media (min-width:40.0625em) {
+    .govuk-file-upload__button {
+        padding: 15px 23px
+    }
 }
 
-.govuk-file-upload__button {
-    align-items: center;
-    display: flex;
-    padding: 0;
-    border: 0;
-    background-color: transparent
+.govuk-file-upload-wrapper:hover .govuk-file-upload__button {
+    border-color: #8e9092
 }
 
-.govuk-file-upload:disabled+.govuk-file-upload__button {
-    pointer-events: none
+.govuk-file-upload-wrapper:hover .govuk-file-upload__pseudo-button {
+    background-color: #dbdad9
 }
 
-.govuk-file-upload:disabled+.govuk-file-upload__button .govuk-file-upload__pseudo-button {
-    opacity: .5
+.govuk-file-upload-wrapper--show-dropzone .govuk-file-upload__button,
+.govuk-file-upload-wrapper:hover .govuk-file-upload__button {
+    background-color: #f3f2f1
+}
+
+.govuk-file-upload-wrapper--show-dropzone .govuk-file-upload__status--empty,
+.govuk-file-upload-wrapper:hover .govuk-file-upload__status--empty,
+.govuk-file-upload__button:focus .govuk-file-upload__status--empty {
+    background-color: #d2e2f1
+}
+
+.govuk-file-upload-wrapper--show-dropzone .govuk-file-upload__button {
+    border: 2px solid #0b0c0c
 }
 
-.govuk-file-upload:disabled+.govuk-file-upload__button .govuk-file-upload__pseudo-button:hover {
+.govuk-file-upload-wrapper--show-dropzone .govuk-file-upload__pseudo-button {
+    background-color: #fff
+}
+
+.govuk-file-upload__button:active,
+.govuk-file-upload__button:focus {
+    border: 2px solid #0b0c0c;
+    outline: 3px solid #fd0;
+    outline-offset: 0;
     background-color: #f3f2f1;
-    cursor: not-allowed
+    box-shadow: inset 0 0 0 2px
 }
 
-.govuk-file-upload:disabled+.govuk-file-upload__button .govuk-file-upload__pseudo-button:active {
-    top: 0;
-    box-shadow: 0 2px 0 #666
+.govuk-file-upload__button:focus .govuk-file-upload__pseudo-button {
+    background-color: #fd0;
+    box-shadow: 0 2px 0 #0b0c0c
+}
+
+.govuk-file-upload-wrapper:hover .govuk-file-upload__button:focus .govuk-file-upload__pseudo-button {
+    border-color: #fd0;
+    outline: 3px solid transparent;
+    background-color: #f3f2f1;
+    box-shadow: inset 0 0 0 1px #fd0
 }
 
 .govuk-footer {

Action run for ede849e

Copy link

github-actions bot commented Jan 22, 2025

Other changes to npm package

diff --git a/packages/govuk-frontend/dist/govuk/all.bundle.js b/packages/govuk-frontend/dist/govuk/all.bundle.js
index 68d48e9ac..a58b5df65 100644
--- a/packages/govuk-frontend/dist/govuk/all.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/all.bundle.js
@@ -1686,6 +1686,9 @@
       this.$root.id = `${this.id}-input`;
       const $wrapper = document.createElement('div');
       $wrapper.className = 'govuk-file-upload-wrapper';
+      const commaSpan = document.createElement('span');
+      commaSpan.className = 'govuk-visually-hidden';
+      commaSpan.innerText = ', ';
       const $button = document.createElement('button');
       $button.classList.add('govuk-file-upload__button');
       $button.type = 'button';
@@ -1694,18 +1697,28 @@
       if (ariaDescribedBy) {
         $button.setAttribute('aria-describedby', ariaDescribedBy);
       }
-      const buttonSpan = document.createElement('span');
-      buttonSpan.className = 'govuk-button govuk-button--secondary govuk-file-upload__pseudo-button';
-      buttonSpan.innerText = this.i18n.t('selectFilesButton');
-      buttonSpan.setAttribute('aria-hidden', 'true');
-      $button.appendChild(buttonSpan);
-      $button.addEventListener('click', this.onClick.bind(this));
       const $status = document.createElement('span');
       $status.className = 'govuk-body govuk-file-upload__status';
       $status.innerText = this.i18n.t('filesSelectedDefault');
       $status.setAttribute('aria-hidden', 'true');
+      $status.classList.add('govuk-file-upload__status--empty');
       $button.appendChild($status);
-      $button.setAttribute('aria-label', `${this.$label.innerText}, ${this.i18n.t('selectFilesButton')}, ${this.i18n.t('filesSelectedDefault')}`);
+      $button.appendChild(commaSpan.cloneNode(true));
+      const containerSpan = document.createElement('span');
+      containerSpan.className = 'govuk-file-upload__pseudo-button-container';
+      const buttonSpan = document.createElement('span');
+      buttonSpan.className = 'govuk-button govuk-button--secondary govuk-file-upload__pseudo-button';
+      buttonSpan.innerText = this.i18n.t('selectFilesButton');
+      buttonSpan.setAttribute('aria-hidden', 'true');
+      containerSpan.appendChild(buttonSpan);
+      containerSpan.appendChild(commaSpan.cloneNode(true));
+      const instructionSpan = document.createElement('span');
+      instructionSpan.className = 'govuk-body govuk-file-upload__instruction';
+      instructionSpan.innerText = this.i18n.t('instruction');
+      containerSpan.appendChild(instructionSpan);
+      $button.appendChild(containerSpan);
+      $button.setAttribute('aria-label', `${this.$label.innerText}, ${this.i18n.t('selectFilesButton')} ${this.i18n.t('instruction')}, ${$status.innerText}`);
+      $button.addEventListener('click', this.onClick.bind(this));
       $wrapper.insertAdjacentElement('beforeend', $button);
       this.$root.insertAdjacentElement('afterend', $wrapper);
       this.$root.setAttribute('tabindex', '-1');
@@ -1717,7 +1730,6 @@
       this.$root.addEventListener('change', this.onChange.bind(this));
       this.updateDisabledState();
       this.observeDisabledState();
-      this.$root.addEventListener('change', this.onChange.bind(this));
       this.$announcements = document.createElement('span');
       this.$announcements.classList.add('govuk-file-upload-announcements');
       this.$announcements.classList.add('govuk-visually-hidden');
@@ -1765,14 +1777,18 @@
       const fileCount = this.$root.files.length;
       if (fileCount === 0) {
         this.$status.innerText = this.i18n.t('filesSelectedDefault');
-      } else if (fileCount === 1) {
-        this.$status.innerText = this.$root.files[0].name;
+        this.$status.classList.add('govuk-file-upload__status--empty');
       } else {
-        this.$status.innerText = this.i18n.t('filesSelected', {
-          count: fileCount
-        });
+        if (fileCount === 1) {
+          this.$status.innerText = this.$root.files[0].name;
+        } else {
+          this.$status.innerText = this.i18n.t('filesSelected', {
+            count: fileCount
+          });
+        }
+        this.$status.classList.remove('govuk-file-upload__status--empty');
       }
-      this.$button.setAttribute('aria-label', `${this.$label.innerText}, ${this.i18n.t('selectFilesButton')}, ${this.$status.innerText}`);
+      this.$button.setAttribute('aria-label', `${this.$label.innerText}, ${this.i18n.t('selectFilesButton')} ${this.i18n.t('instruction')}, ${this.$status.innerText}`);
     }
     findLabel() {
       const $label = document.querySelector(`label[for="${this.$root.id}"]`);
@@ -1814,7 +1830,8 @@
         other: '%{count} files chosen'
       },
       dropZoneEntered: 'Entered drop zone',
-      dropZoneLeft: 'Left drop zone'
+      dropZoneLeft: 'Left drop zone',
+      instruction: 'or drop file'
     }
   });
   FileUpload.schema = Object.freeze({
diff --git a/packages/govuk-frontend/dist/govuk/all.bundle.mjs b/packages/govuk-frontend/dist/govuk/all.bundle.mjs
index e32972b10..589c4ccf6 100644
--- a/packages/govuk-frontend/dist/govuk/all.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/all.bundle.mjs
@@ -1680,6 +1680,9 @@ class FileUpload extends ConfigurableComponent {
     this.$root.id = `${this.id}-input`;
     const $wrapper = document.createElement('div');
     $wrapper.className = 'govuk-file-upload-wrapper';
+    const commaSpan = document.createElement('span');
+    commaSpan.className = 'govuk-visually-hidden';
+    commaSpan.innerText = ', ';
     const $button = document.createElement('button');
     $button.classList.add('govuk-file-upload__button');
     $button.type = 'button';
@@ -1688,18 +1691,28 @@ class FileUpload extends ConfigurableComponent {
     if (ariaDescribedBy) {
       $button.setAttribute('aria-describedby', ariaDescribedBy);
     }
-    const buttonSpan = document.createElement('span');
-    buttonSpan.className = 'govuk-button govuk-button--secondary govuk-file-upload__pseudo-button';
-    buttonSpan.innerText = this.i18n.t('selectFilesButton');
-    buttonSpan.setAttribute('aria-hidden', 'true');
-    $button.appendChild(buttonSpan);
-    $button.addEventListener('click', this.onClick.bind(this));
     const $status = document.createElement('span');
     $status.className = 'govuk-body govuk-file-upload__status';
     $status.innerText = this.i18n.t('filesSelectedDefault');
     $status.setAttribute('aria-hidden', 'true');
+    $status.classList.add('govuk-file-upload__status--empty');
     $button.appendChild($status);
-    $button.setAttribute('aria-label', `${this.$label.innerText}, ${this.i18n.t('selectFilesButton')}, ${this.i18n.t('filesSelectedDefault')}`);
+    $button.appendChild(commaSpan.cloneNode(true));
+    const containerSpan = document.createElement('span');
+    containerSpan.className = 'govuk-file-upload__pseudo-button-container';
+    const buttonSpan = document.createElement('span');
+    buttonSpan.className = 'govuk-button govuk-button--secondary govuk-file-upload__pseudo-button';
+    buttonSpan.innerText = this.i18n.t('selectFilesButton');
+    buttonSpan.setAttribute('aria-hidden', 'true');
+    containerSpan.appendChild(buttonSpan);
+    containerSpan.appendChild(commaSpan.cloneNode(true));
+    const instructionSpan = document.createElement('span');
+    instructionSpan.className = 'govuk-body govuk-file-upload__instruction';
+    instructionSpan.innerText = this.i18n.t('instruction');
+    containerSpan.appendChild(instructionSpan);
+    $button.appendChild(containerSpan);
+    $button.setAttribute('aria-label', `${this.$label.innerText}, ${this.i18n.t('selectFilesButton')} ${this.i18n.t('instruction')}, ${$status.innerText}`);
+    $button.addEventListener('click', this.onClick.bind(this));
     $wrapper.insertAdjacentElement('beforeend', $button);
     this.$root.insertAdjacentElement('afterend', $wrapper);
     this.$root.setAttribute('tabindex', '-1');
@@ -1711,7 +1724,6 @@ class FileUpload extends ConfigurableComponent {
     this.$root.addEventListener('change', this.onChange.bind(this));
     this.updateDisabledState();
     this.observeDisabledState();
-    this.$root.addEventListener('change', this.onChange.bind(this));
     this.$announcements = document.createElement('span');
     this.$announcements.classList.add('govuk-file-upload-announcements');
     this.$announcements.classList.add('govuk-visually-hidden');
@@ -1759,14 +1771,18 @@ class FileUpload extends ConfigurableComponent {
     const fileCount = this.$root.files.length;
     if (fileCount === 0) {
       this.$status.innerText = this.i18n.t('filesSelectedDefault');
-    } else if (fileCount === 1) {
-      this.$status.innerText = this.$root.files[0].name;
+      this.$status.classList.add('govuk-file-upload__status--empty');
     } else {
-      this.$status.innerText = this.i18n.t('filesSelected', {
-        count: fileCount
-      });
+      if (fileCount === 1) {
+        this.$status.innerText = this.$root.files[0].name;
+      } else {
+        this.$status.innerText = this.i18n.t('filesSelected', {
+          count: fileCount
+        });
+      }
+      this.$status.classList.remove('govuk-file-upload__status--empty');
     }
-    this.$button.setAttribute('aria-label', `${this.$label.innerText}, ${this.i18n.t('selectFilesButton')}, ${this.$status.innerText}`);
+    this.$button.setAttribute('aria-label', `${this.$label.innerText}, ${this.i18n.t('selectFilesButton')} ${this.i18n.t('instruction')}, ${this.$status.innerText}`);
   }
   findLabel() {
     const $label = document.querySelector(`label[for="${this.$root.id}"]`);
@@ -1808,7 +1824,8 @@ FileUpload.defaults = Object.freeze({
       other: '%{count} files chosen'
     },
     dropZoneEntered: 'Entered drop zone',
-    dropZoneLeft: 'Left drop zone'
+    dropZoneLeft: 'Left drop zone',
+    instruction: 'or drop file'
   }
 });
 FileUpload.schema = Object.freeze({
diff --git a/packages/govuk-frontend/dist/govuk/components/file-upload/_index.scss b/packages/govuk-frontend/dist/govuk/components/file-upload/_index.scss
index 4dc478173..1da265081 100644
--- a/packages/govuk-frontend/dist/govuk/components/file-upload/_index.scss
+++ b/packages/govuk-frontend/dist/govuk/components/file-upload/_index.scss
@@ -3,6 +3,7 @@
 @import "../label/index";
 
 @include govuk-exports("govuk/component/file-upload") {
+  $file-upload-border-width: 2px;
   $component-padding: govuk-spacing(1);
 
   .govuk-file-upload {
@@ -48,34 +49,17 @@
   }
 
   .govuk-file-upload-wrapper {
-    display: inline-flex;
-    align-items: baseline;
+    display: block;
     position: relative;
-  }
-
-  .govuk-file-upload-wrapper--show-dropzone {
-    $dropzone-padding: govuk-spacing(2);
-    $dropzone-offset: $dropzone-padding + $govuk-border-width-form-element;
-
-    // Add negative margins to all sides so that content doesn't jump due to
-    // the addition of the padding and border.
-    margin: -$dropzone-offset;
-    padding: $dropzone-padding;
-    border: $govuk-border-width-form-element dashed $govuk-input-border-colour;
+    z-index: 0;
     background-color: $govuk-body-background-colour;
-
-    .govuk-file-upload__pseudo-button,
-    .govuk-file-upload__status {
-      // When the dropzone is hovered over, make these aspects not accept
-      // mouse events, so dropped files fall through to the input beneath them
-      pointer-events: none;
-    }
   }
 
   .govuk-file-upload-wrapper .govuk-file-upload {
+    position: absolute;
     // Make the native control take up the entire space of the element, but
     // invisible and behind the other elements until we need it
-    position: absolute;
+    z-index: -1;
     top: 0;
     left: 0;
     width: 100%;
@@ -85,63 +69,109 @@
     opacity: 0;
   }
 
+  .govuk-file-upload-wrapper--show-dropzone .govuk-file-upload {
+    z-index: 1;
+  }
+
   .govuk-file-upload__pseudo-button {
     width: auto;
-    margin-bottom: 0;
-    flex-grow: 0;
+    margin-right: govuk-spacing(2);
+    margin-bottom: $govuk-border-width-form-element + 1;
     flex-shrink: 0;
   }
 
-  .govuk-file-upload__status {
+  .govuk-file-upload__instruction {
+    margin-top: govuk-spacing(2) - ($govuk-border-width-form-element + 1);
     margin-bottom: 0;
-    margin-left: govuk-spacing(2);
+    text-align: left;
   }
-}
 
-.govuk-file-upload__button:focus {
-  outline: none;
-}
+  .govuk-file-upload__status {
+    display: block;
+    margin-bottom: govuk-spacing(2);
+    padding: govuk-spacing(3) govuk-spacing(2);
+    text-align: left;
+  }
 
-.govuk-file-upload__button:focus .govuk-file-upload__pseudo-button {
-  outline: 3px solid transparent;
-  background-color: $govuk-focus-colour;
-  box-shadow: 0 2px 0 govuk-colour("black");
-}
+  .govuk-file-upload__status--empty {
+    color: govuk-shade(govuk-colour("blue"), 60%);
+    background-color: govuk-tint(govuk-colour("blue"), 70%);
+  }
 
-.govuk-file-upload__button:focus .govuk-file-upload__pseudo-button:hover {
-  border-color: $govuk-focus-colour;
-  outline: 3px solid transparent;
-  background-color: govuk-colour("light-grey");
-  box-shadow: inset 0 0 0 1px $govuk-focus-colour;
-}
+  // bugs documented with button using flex
+  // https://github.com/philipwalton/flexbugs#flexbug-9
+  // so we need a container here
+  .govuk-file-upload__pseudo-button-container {
+    display: flex;
+    align-items: baseline;
+    flex-wrap: wrap;
+  }
 
-.govuk-file-upload__button:active .govuk-file-upload__pseudo-button:hover {
-  background-color: govuk-shade(govuk-colour("light-grey"), 20%);
-}
+  .govuk-file-upload__button {
+    width: 100%;
+    // align the padding to be same as notification banner and error summary accounting for the thicker borders
+    padding: govuk-spacing(3) (govuk-spacing(3) + $govuk-border-width - $file-upload-border-width);
+    border: $file-upload-border-width govuk-colour("mid-grey") dashed;
+    background-color: govuk-colour("white");
+    cursor: pointer;
+
+    @include govuk-media-query($from: tablet) {
+      padding: govuk-spacing(3) (govuk-spacing(4) + $govuk-border-width - $file-upload-border-width);
+    }
+  }
 
-.govuk-file-upload__button {
-  align-items: center;
-  display: flex;
-  padding: 0;
-  border: 0;
-  background-color: transparent;
-}
+  .govuk-file-upload-wrapper:hover .govuk-file-upload__button {
+    border-color: govuk-shade(govuk-colour("mid-grey"), 20%);
+  }
 
-.govuk-file-upload:disabled + .govuk-file-upload__button {
-  pointer-events: none;
-}
+  .govuk-file-upload-wrapper:hover .govuk-file-upload__pseudo-button {
+    background-color: govuk-shade(govuk-colour("light-grey"), 10%);
+  }
+
+  .govuk-file-upload-wrapper--show-dropzone .govuk-file-upload__button,
+  .govuk-file-upload-wrapper:hover .govuk-file-upload__button {
+    background-color: govuk-colour("light-grey");
+  }
+
+  .govuk-file-upload-wrapper--show-dropzone .govuk-file-upload__status--empty,
+  .govuk-file-upload-wrapper:hover .govuk-file-upload__status--empty,
+  .govuk-file-upload__button:focus .govuk-file-upload__status--empty {
+    background-color: govuk-tint(govuk-colour("blue"), 80%);
+  }
+
+  .govuk-file-upload-wrapper--show-dropzone .govuk-file-upload__button {
+    border: $file-upload-border-width solid govuk-colour("black");
+  }
 
-.govuk-file-upload:disabled + .govuk-file-upload__button .govuk-file-upload__pseudo-button {
-  opacity: (0.5);
+  .govuk-file-upload-wrapper--show-dropzone .govuk-file-upload__pseudo-button {
+    background-color: govuk-colour("white");
+  }
 
-  &:hover {
+  .govuk-file-upload__button:active,
+  .govuk-file-upload__button:focus {
+    border: 2px solid govuk-colour("black");
+    outline: $govuk-focus-width solid $govuk-focus-colour;
+    // Ensure outline appears outside of the element
+    outline-offset: 0;
     background-color: govuk-colour("light-grey");
-    cursor: not-allowed;
+    // Double the border by adding its width again. Use `box-shadow` for this
+    // instead of changing `border-width` - this is for consistency with
+    // components such as textarea where we avoid changing `border-width` as
+    // it will change the element size. Also, `outline` cannot be utilised
+    // here as it is already used for the yellow focus state.
+    box-shadow: inset 0 0 0 $govuk-border-width-form-element;
   }
 
-  &:active {
-    top: 0;
-    box-shadow: 0 $govuk-border-width-form-element 0 govuk-shade(govuk-colour("white"), 60%); // s0
+  .govuk-file-upload__button:focus .govuk-file-upload__pseudo-button {
+    background-color: $govuk-focus-colour;
+    box-shadow: 0 2px 0 govuk-colour("black");
+  }
+
+  .govuk-file-upload-wrapper:hover .govuk-file-upload__button:focus .govuk-file-upload__pseudo-button {
+    border-color: $govuk-focus-colour;
+    outline: 3px solid transparent;
+    background-color: govuk-colour("light-grey");
+    box-shadow: inset 0 0 0 1px $govuk-focus-colour;
   }
 }
 
diff --git a/packages/govuk-frontend/dist/govuk/components/file-upload/file-upload.bundle.js b/packages/govuk-frontend/dist/govuk/components/file-upload/file-upload.bundle.js
index 312098ada..0b4d1db88 100644
--- a/packages/govuk-frontend/dist/govuk/components/file-upload/file-upload.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/file-upload/file-upload.bundle.js
@@ -513,6 +513,9 @@
       this.$root.id = `${this.id}-input`;
       const $wrapper = document.createElement('div');
       $wrapper.className = 'govuk-file-upload-wrapper';
+      const commaSpan = document.createElement('span');
+      commaSpan.className = 'govuk-visually-hidden';
+      commaSpan.innerText = ', ';
       const $button = document.createElement('button');
       $button.classList.add('govuk-file-upload__button');
       $button.type = 'button';
@@ -521,18 +524,28 @@
       if (ariaDescribedBy) {
         $button.setAttribute('aria-describedby', ariaDescribedBy);
       }
-      const buttonSpan = document.createElement('span');
-      buttonSpan.className = 'govuk-button govuk-button--secondary govuk-file-upload__pseudo-button';
-      buttonSpan.innerText = this.i18n.t('selectFilesButton');
-      buttonSpan.setAttribute('aria-hidden', 'true');
-      $button.appendChild(buttonSpan);
-      $button.addEventListener('click', this.onClick.bind(this));
       const $status = document.createElement('span');
       $status.className = 'govuk-body govuk-file-upload__status';
       $status.innerText = this.i18n.t('filesSelectedDefault');
       $status.setAttribute('aria-hidden', 'true');
+      $status.classList.add('govuk-file-upload__status--empty');
       $button.appendChild($status);
-      $button.setAttribute('aria-label', `${this.$label.innerText}, ${this.i18n.t('selectFilesButton')}, ${this.i18n.t('filesSelectedDefault')}`);
+      $button.appendChild(commaSpan.cloneNode(true));
+      const containerSpan = document.createElement('span');
+      containerSpan.className = 'govuk-file-upload__pseudo-button-container';
+      const buttonSpan = document.createElement('span');
+      buttonSpan.className = 'govuk-button govuk-button--secondary govuk-file-upload__pseudo-button';
+      buttonSpan.innerText = this.i18n.t('selectFilesButton');
+      buttonSpan.setAttribute('aria-hidden', 'true');
+      containerSpan.appendChild(buttonSpan);
+      containerSpan.appendChild(commaSpan.cloneNode(true));
+      const instructionSpan = document.createElement('span');
+      instructionSpan.className = 'govuk-body govuk-file-upload__instruction';
+      instructionSpan.innerText = this.i18n.t('instruction');
+      containerSpan.appendChild(instructionSpan);
+      $button.appendChild(containerSpan);
+      $button.setAttribute('aria-label', `${this.$label.innerText}, ${this.i18n.t('selectFilesButton')} ${this.i18n.t('instruction')}, ${$status.innerText}`);
+      $button.addEventListener('click', this.onClick.bind(this));
       $wrapper.insertAdjacentElement('beforeend', $button);
       this.$root.insertAdjacentElement('afterend', $wrapper);
       this.$root.setAttribute('tabindex', '-1');
@@ -544,7 +557,6 @@
       this.$root.addEventListener('change', this.onChange.bind(this));
       this.updateDisabledState();
       this.observeDisabledState();
-      this.$root.addEventListener('change', this.onChange.bind(this));
       this.$announcements = document.createElement('span');
       this.$announcements.classList.add('govuk-file-upload-announcements');
       this.$announcements.classList.add('govuk-visually-hidden');
@@ -592,14 +604,18 @@
       const fileCount = this.$root.files.length;
       if (fileCount === 0) {
         this.$status.innerText = this.i18n.t('filesSelectedDefault');
-      } else if (fileCount === 1) {
-        this.$status.innerText = this.$root.files[0].name;
+        this.$status.classList.add('govuk-file-upload__status--empty');
       } else {
-        this.$status.innerText = this.i18n.t('filesSelected', {
-          count: fileCount
-        });
+        if (fileCount === 1) {
+          this.$status.innerText = this.$root.files[0].name;
+        } else {
+          this.$status.innerText = this.i18n.t('filesSelected', {
+            count: fileCount
+          });
+        }
+        this.$status.classList.remove('govuk-file-upload__status--empty');
       }
-      this.$button.setAttribute('aria-label', `${this.$label.innerText}, ${this.i18n.t('selectFilesButton')}, ${this.$status.innerText}`);
+      this.$button.setAttribute('aria-label', `${this.$label.innerText}, ${this.i18n.t('selectFilesButton')} ${this.i18n.t('instruction')}, ${this.$status.innerText}`);
     }
     findLabel() {
       const $label = document.querySelector(`label[for="${this.$root.id}"]`);
@@ -641,7 +657,8 @@
         other: '%{count} files chosen'
       },
       dropZoneEntered: 'Entered drop zone',
-      dropZoneLeft: 'Left drop zone'
+      dropZoneLeft: 'Left drop zone',
+      instruction: 'or drop file'
     }
   });
   FileUpload.schema = Object.freeze({
diff --git a/packages/govuk-frontend/dist/govuk/components/file-upload/file-upload.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/file-upload/file-upload.bundle.mjs
index e759a6c30..ce4dc45ba 100644
--- a/packages/govuk-frontend/dist/govuk/components/file-upload/file-upload.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/file-upload/file-upload.bundle.mjs
@@ -507,6 +507,9 @@ class FileUpload extends ConfigurableComponent {
     this.$root.id = `${this.id}-input`;
     const $wrapper = document.createElement('div');
     $wrapper.className = 'govuk-file-upload-wrapper';
+    const commaSpan = document.createElement('span');
+    commaSpan.className = 'govuk-visually-hidden';
+    commaSpan.innerText = ', ';
     const $button = document.createElement('button');
     $button.classList.add('govuk-file-upload__button');
     $button.type = 'button';
@@ -515,18 +518,28 @@ class FileUpload extends ConfigurableComponent {
     if (ariaDescribedBy) {
       $button.setAttribute('aria-describedby', ariaDescribedBy);
     }
-    const buttonSpan = document.createElement('span');
-    buttonSpan.className = 'govuk-button govuk-button--secondary govuk-file-upload__pseudo-button';
-    buttonSpan.innerText = this.i18n.t('selectFilesButton');
-    buttonSpan.setAttribute('aria-hidden', 'true');
-    $button.appendChild(buttonSpan);
-    $button.addEventListener('click', this.onClick.bind(this));
     const $status = document.createElement('span');
     $status.className = 'govuk-body govuk-file-upload__status';
     $status.innerText = this.i18n.t('filesSelectedDefault');
     $status.setAttribute('aria-hidden', 'true');
+    $status.classList.add('govuk-file-upload__status--empty');
     $button.appendChild($status);
-    $button.setAttribute('aria-label', `${this.$label.innerText}, ${this.i18n.t('selectFilesButton')}, ${this.i18n.t('filesSelectedDefault')}`);
+    $button.appendChild(commaSpan.cloneNode(true));
+    const containerSpan = document.createElement('span');
+    containerSpan.className = 'govuk-file-upload__pseudo-button-container';
+    const buttonSpan = document.createElement('span');
+    buttonSpan.className = 'govuk-button govuk-button--secondary govuk-file-upload__pseudo-button';
+    buttonSpan.innerText = this.i18n.t('selectFilesButton');
+    buttonSpan.setAttribute('aria-hidden', 'true');
+    containerSpan.appendChild(buttonSpan);
+    containerSpan.appendChild(commaSpan.cloneNode(true));
+    const instructionSpan = document.createElement('span');
+    instructionSpan.className = 'govuk-body govuk-file-upload__instruction';
+    instructionSpan.innerText = this.i18n.t('instruction');
+    containerSpan.appendChild(instructionSpan);
+    $button.appendChild(containerSpan);
+    $button.setAttribute('aria-label', `${this.$label.innerText}, ${this.i18n.t('selectFilesButton')} ${this.i18n.t('instruction')}, ${$status.innerText}`);
+    $button.addEventListener('click', this.onClick.bind(this));
     $wrapper.insertAdjacentElement('beforeend', $button);
     this.$root.insertAdjacentElement('afterend', $wrapper);
     this.$root.setAttribute('tabindex', '-1');
@@ -538,7 +551,6 @@ class FileUpload extends ConfigurableComponent {
     this.$root.addEventListener('change', this.onChange.bind(this));
     this.updateDisabledState();
     this.observeDisabledState();
-    this.$root.addEventListener('change', this.onChange.bind(this));
     this.$announcements = document.createElement('span');
     this.$announcements.classList.add('govuk-file-upload-announcements');
     this.$announcements.classList.add('govuk-visually-hidden');
@@ -586,14 +598,18 @@ class FileUpload extends ConfigurableComponent {
     const fileCount = this.$root.files.length;
     if (fileCount === 0) {
       this.$status.innerText = this.i18n.t('filesSelectedDefault');
-    } else if (fileCount === 1) {
-      this.$status.innerText = this.$root.files[0].name;
+      this.$status.classList.add('govuk-file-upload__status--empty');
     } else {
-      this.$status.innerText = this.i18n.t('filesSelected', {
-        count: fileCount
-      });
+      if (fileCount === 1) {
+        this.$status.innerText = this.$root.files[0].name;
+      } else {
+        this.$status.innerText = this.i18n.t('filesSelected', {
+          count: fileCount
+        });
+      }
+      this.$status.classList.remove('govuk-file-upload__status--empty');
     }
-    this.$button.setAttribute('aria-label', `${this.$label.innerText}, ${this.i18n.t('selectFilesButton')}, ${this.$status.innerText}`);
+    this.$button.setAttribute('aria-label', `${this.$label.innerText}, ${this.i18n.t('selectFilesButton')} ${this.i18n.t('instruction')}, ${this.$status.innerText}`);
   }
   findLabel() {
     const $label = document.querySelector(`label[for="${this.$root.id}"]`);
@@ -635,7 +651,8 @@ FileUpload.defaults = Object.freeze({
       other: '%{count} files chosen'
     },
     dropZoneEntered: 'Entered drop zone',
-    dropZoneLeft: 'Left drop zone'
+    dropZoneLeft: 'Left drop zone',
+    instruction: 'or drop file'
   }
 });
 FileUpload.schema = Object.freeze({
diff --git a/packages/govuk-frontend/dist/govuk/components/file-upload/file-upload.mjs b/packages/govuk-frontend/dist/govuk/components/file-upload/file-upload.mjs
index acccac3da..cee8c3418 100644
--- a/packages/govuk-frontend/dist/govuk/components/file-upload/file-upload.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/file-upload/file-upload.mjs
@@ -36,6 +36,9 @@ class FileUpload extends ConfigurableComponent {
     this.$root.id = `${this.id}-input`;
     const $wrapper = document.createElement('div');
     $wrapper.className = 'govuk-file-upload-wrapper';
+    const commaSpan = document.createElement('span');
+    commaSpan.className = 'govuk-visually-hidden';
+    commaSpan.innerText = ', ';
     const $button = document.createElement('button');
     $button.classList.add('govuk-file-upload__button');
     $button.type = 'button';
@@ -44,18 +47,28 @@ class FileUpload extends ConfigurableComponent {
     if (ariaDescribedBy) {
       $button.setAttribute('aria-describedby', ariaDescribedBy);
     }
-    const buttonSpan = document.createElement('span');
-    buttonSpan.className = 'govuk-button govuk-button--secondary govuk-file-upload__pseudo-button';
-    buttonSpan.innerText = this.i18n.t('selectFilesButton');
-    buttonSpan.setAttribute('aria-hidden', 'true');
-    $button.appendChild(buttonSpan);
-    $button.addEventListener('click', this.onClick.bind(this));
     const $status = document.createElement('span');
     $status.className = 'govuk-body govuk-file-upload__status';
     $status.innerText = this.i18n.t('filesSelectedDefault');
     $status.setAttribute('aria-hidden', 'true');
+    $status.classList.add('govuk-file-upload__status--empty');
     $button.appendChild($status);
-    $button.setAttribute('aria-label', `${this.$label.innerText}, ${this.i18n.t('selectFilesButton')}, ${this.i18n.t('filesSelectedDefault')}`);
+    $button.appendChild(commaSpan.cloneNode(true));
+    const containerSpan = document.createElement('span');
+    containerSpan.className = 'govuk-file-upload__pseudo-button-container';
+    const buttonSpan = document.createElement('span');
+    buttonSpan.className = 'govuk-button govuk-button--secondary govuk-file-upload__pseudo-button';
+    buttonSpan.innerText = this.i18n.t('selectFilesButton');
+    buttonSpan.setAttribute('aria-hidden', 'true');
+    containerSpan.appendChild(buttonSpan);
+    containerSpan.appendChild(commaSpan.cloneNode(true));
+    const instructionSpan = document.createElement('span');
+    instructionSpan.className = 'govuk-body govuk-file-upload__instruction';
+    instructionSpan.innerText = this.i18n.t('instruction');
+    containerSpan.appendChild(instructionSpan);
+    $button.appendChild(containerSpan);
+    $button.setAttribute('aria-label', `${this.$label.innerText}, ${this.i18n.t('selectFilesButton')} ${this.i18n.t('instruction')}, ${$status.innerText}`);
+    $button.addEventListener('click', this.onClick.bind(this));
     $wrapper.insertAdjacentElement('beforeend', $button);
     this.$root.insertAdjacentElement('afterend', $wrapper);
     this.$root.setAttribute('tabindex', '-1');
@@ -67,7 +80,6 @@ class FileUpload extends ConfigurableComponent {
     this.$root.addEventListener('change', this.onChange.bind(this));
     this.updateDisabledState();
     this.observeDisabledState();
-    this.$root.addEventListener('change', this.onChange.bind(this));
     this.$announcements = document.createElement('span');
     this.$announcements.classList.add('govuk-file-upload-announcements');
     this.$announcements.classList.add('govuk-visually-hidden');
@@ -115,14 +127,18 @@ class FileUpload extends ConfigurableComponent {
     const fileCount = this.$root.files.length;
     if (fileCount === 0) {
       this.$status.innerText = this.i18n.t('filesSelectedDefault');
-    } else if (fileCount === 1) {
-      this.$status.innerText = this.$root.files[0].name;
+      this.$status.classList.add('govuk-file-upload__status--empty');
     } else {
-      this.$status.innerText = this.i18n.t('filesSelected', {
-        count: fileCount
-      });
+      if (fileCount === 1) {
+        this.$status.innerText = this.$root.files[0].name;
+      } else {
+        this.$status.innerText = this.i18n.t('filesSelected', {
+          count: fileCount
+        });
+      }
+      this.$status.classList.remove('govuk-file-upload__status--empty');
     }
-    this.$button.setAttribute('aria-label', `${this.$label.innerText}, ${this.i18n.t('selectFilesButton')}, ${this.$status.innerText}`);
+    this.$button.setAttribute('aria-label', `${this.$label.innerText}, ${this.i18n.t('selectFilesButton')} ${this.i18n.t('instruction')}, ${this.$status.innerText}`);
   }
   findLabel() {
     const $label = document.querySelector(`label[for="${this.$root.id}"]`);
@@ -164,7 +180,8 @@ FileUpload.defaults = Object.freeze({
       other: '%{count} files chosen'
     },
     dropZoneEntered: 'Entered drop zone',
-    dropZoneLeft: 'Left drop zone'
+    dropZoneLeft: 'Left drop zone',
+    instruction: 'or drop file'
   }
 });
 FileUpload.schema = Object.freeze({
diff --git a/packages/govuk-frontend/dist/govuk/components/file-upload/fixtures.json b/packages/govuk-frontend/dist/govuk/components/file-upload/fixtures.json
index 09d9a8427..1de2b1191 100644
--- a/packages/govuk-frontend/dist/govuk/components/file-upload/fixtures.json
+++ b/packages/govuk-frontend/dist/govuk/components/file-upload/fixtures.json
@@ -207,6 +207,7 @@
                 "multiple": true,
                 "javascript": true,
                 "selectFilesButtonText": "Dewiswch ffeil",
+                "instructionText": "neu ollwng ffeil",
                 "filesSelectedDefaultText": "Dim ffeiliau wedi'u dewis",
                 "filesSelectedText": {
                     "other": "%{count} ffeil wedi'u dewis",
@@ -217,7 +218,7 @@
             "description": "",
             "previewLayoutModifiers": [],
             "screenshot": false,
-            "html": "<div class=\"govuk-form-group\">\n  <label class=\"govuk-label\" for=\"file-upload-1\">\n    Llwythwch ffeil i fyny\n  </label>\n  <input class=\"govuk-file-upload\" id=\"file-upload-1\" name=\"file-upload-1\" type=\"file\" multiple    data-module=\"govuk-file-upload\" data-i18n.select-files-button=\"Dewiswch ffeil\" data-i18n.files-selected-default=\"Dim ffeiliau wedi&#39;u dewis\" data-i18n.files-selected.other=\"%{count} ffeil wedi&#39;u dewis\" data-i18n.files-selected.one=\"%{count} ffeil wedi&#39;i dewis\">\n</div>"
+            "html": "<div class=\"govuk-form-group\">\n  <label class=\"govuk-label\" for=\"file-upload-1\">\n    Llwythwch ffeil i fyny\n  </label>\n  <input class=\"govuk-file-upload\" id=\"file-upload-1\" name=\"file-upload-1\" type=\"file\" multiple    data-module=\"govuk-file-upload\" data-i18n.select-files-button=\"Dewiswch ffeil\" data-i18n.files-selected-default=\"Dim ffeiliau wedi&#39;u dewis\" data-i18n.files-selected.other=\"%{count} ffeil wedi&#39;u dewis\" data-i18n.files-selected.one=\"%{count} ffeil wedi&#39;i dewis\" data-i18n.instruction=\"neu ollwng ffeil\">\n</div>"
         },
         {
             "name": "with value",
diff --git a/packages/govuk-frontend/dist/govuk/components/file-upload/macro-options.json b/packages/govuk-frontend/dist/govuk/components/file-upload/macro-options.json
index 8d1e0092e..18bca34d1 100644
--- a/packages/govuk-frontend/dist/govuk/components/file-upload/macro-options.json
+++ b/packages/govuk-frontend/dist/govuk/components/file-upload/macro-options.json
@@ -128,6 +128,12 @@
         "required": false,
         "description": "The text of the button that opens the file picker. JavaScript enhanced version of the component only. Default is \"Choose file\"."
     },
+    {
+        "name": "instructionText",
+        "type": "string",
+        "required": false,
+        "description": "The text of the instruction text that follows the button that opens the file picker. JavaScript enhanced version of the component only. Default is \"or drop file\"."
+    },
     {
         "name": "filesSelected",
         "type": "object",

Action run for ede849e

Base automatically changed from use-output to spike-enhanced-file-upload January 23, 2025 10:54
@patrickpatrickpatrick patrickpatrickpatrick marked this pull request as ready for review January 23, 2025 15:20
@CharlotteDowns
Copy link
Contributor

CharlotteDowns commented Jan 24, 2025

Nice work Patrick 💪🏻.

Design feedback

I think we need to use the default secondary button styling on focus, this would mean using #0b0c0c instead of #929191 on the box-shadow
Secondary button focus example

I also noticed that on hover and focus+hover that he secondary button doesn't take the hover background colour of #dbdad9.

The dragged-on state seems to use the yellow #fd0 instead of #0b0c0c for the border.

It should say 'or drop file' instead of 'instruction'

The padding on the should be 13px 23px 15px 24px; to ensure it's consistency with other elements in the Design System, for example, Notification banner etc.

@romaricpascal
Copy link
Member

Cheers Patrick, that's a good start! I've documented a couple of missed details in the design document (internal link).

The padding on the should be 13px 23px 15px 24px; to ensure it's consistency with other elements in the Design System, for example, Notification banner etc.

@CharlotteDowns It's a bit strange to have such variation in padding. Inspecting the Notification Banner on the Design System site, I'm seeing the following paddings:

  • .govuk-notification-banner__header: 2px 20px 5px (15px instead of 20px on narrow viewports)
  • .govuk-notification-banner__content: 20px (15px on narrow viewports)

Similarly on the Error Summary, the padding seems happily at 20px (15px on narrow viewports). What's the source of the values you're proposing?

@CharlotteDowns
Copy link
Contributor

CharlotteDowns commented Jan 24, 2025

@romaricpascal this could be my poor maths but I'm trying to recreate the same visual properties and alignment to the Notification banner and Error summary but working with a 2px border instead of a 5px border (used on those components). Maybe I should refer to the summary card styling instead 🤔, although that seems to only have a 1px border :(.

Copy link

github-actions bot commented Jan 24, 2025

Rendered HTML changes to npm package

diff --git a/packages/govuk-frontend/dist/govuk/components/file-upload/template-translated.html b/packages/govuk-frontend/dist/govuk/components/file-upload/template-translated.html
index 8c6060d23..1e275eb58 100644
--- a/packages/govuk-frontend/dist/govuk/components/file-upload/template-translated.html
+++ b/packages/govuk-frontend/dist/govuk/components/file-upload/template-translated.html
@@ -2,5 +2,5 @@
   <label class="govuk-label" for="file-upload-1">
     Llwythwch ffeil i fyny
   </label>
-  <input class="govuk-file-upload" id="file-upload-1" name="file-upload-1" type="file" multiple    data-module="govuk-file-upload" data-i18n.select-files-button="Dewiswch ffeil" data-i18n.files-selected-default="Dim ffeiliau wedi&#39;u dewis" data-i18n.files-selected.other="%{count} ffeil wedi&#39;u dewis" data-i18n.files-selected.one="%{count} ffeil wedi&#39;i dewis">
+  <input class="govuk-file-upload" id="file-upload-1" name="file-upload-1" type="file" multiple    data-module="govuk-file-upload" data-i18n.select-files-button="Dewiswch ffeil" data-i18n.files-selected-default="Dim ffeiliau wedi&#39;u dewis" data-i18n.files-selected.other="%{count} ffeil wedi&#39;u dewis" data-i18n.files-selected.one="%{count} ffeil wedi&#39;i dewis" data-i18n.instruction="neu ollwng ffeil">
 </div>

Action run for ede849e

Copy link
Member

@romaricpascal romaricpascal left a comment

Choose a reason for hiding this comment

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

Almost there code-wise I think 🙌🏻 Main issue is a flickering when hovering from the label to the drop zone. I've proposed a workaround, just needs checking with @CharlotteDowns.

Also made note of a couple of small code renamings or moves to tidy things up some more.

Noticed (but couldn't comment) that we listen to the change even handler twice (probably from a previous merge).

Comment on lines 127 to 125
.govuk-file-upload__button:hover,
.govuk-file-upload__button:hover .govuk-file-upload__pseudo-button {
Copy link
Member

Choose a reason for hiding this comment

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

issue Using the :hover selector of the <button> leads to two undesirable side effects:

  1. The hover triggers when hovering the <label> as it's linked to the button
  2. In turn, when moving the mouse from the <label> to the <button>, there's a white space that's neither of them which causes the hover state to flicker. This space would be even bigger if using the hint or error message.
Screen.Recording.2025-01-28.at.17.38.34.mov

I'd propose to use the .govuk-file-upload-wrapper:hover to hook the styles if we can, which would only show the hover state when actually hovering the dropzone and not the label, avoiding the flickering. Not that our other form fields have no hover state (as they're not really stuff to click on) so I don't think that'd be jarring if the hover only happened when hovering the drop zone.

Copy link
Member

Choose a reason for hiding this comment

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

@CharlotteDowns Can you confirm if the hover state happening only when hovering the drop zone itself would be OK, please?

Copy link
Contributor

Choose a reason for hiding this comment

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

@romaricpascal I'm happy with that suggestion, sounds like it will look better

// the addition of the padding and border.
margin: -$dropzone-offset;
padding: $dropzone-padding;
border: $govuk-border-width-form-element dashed $govuk-input-border-colour;
background-color: $govuk-body-background-colour;
Copy link
Member

Choose a reason for hiding this comment

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

suggestion I think we'd want the wrapper to always have a white background (a bit like our input fields always have a white background as well), in case they end up on something not white, so we can move that background inside govuk-file-upload-wrapper.

Comment on lines 84 to 86
.govuk-file-upload__pseudo-button-container > * {
margin-bottom: 0;
}
Copy link
Member

Choose a reason for hiding this comment

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

suggestion Could this be regrouped with the .govuk-file-upload__pseudo-button-container selector so both bits related to that element are in the same area?

$button.appendChild($status)
$button.appendChild(commaSpan.cloneNode(true))

const buttonParentSpan = document.createElement('span')
Copy link
Member

Choose a reason for hiding this comment

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

suggestion To align with the CSS naming, could this be:

Suggested change
const buttonParentSpan = document.createElement('span')
const containerSpan = document.createElement('span')

fileCount === 1
) {
this.$status.innerText = this.$root.files[0].name
this.$status.classList.add('govuk-tag--light-blue')
Copy link
Member

Choose a reason for hiding this comment

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

suggestion Should this be the following, instead of using the styles of the Tag component?

Suggested change
this.$status.classList.add('govuk-tag--light-blue')
this.$status.classList.add('govuk-file-upload__status--empty')

Button creation of file upload needs to be changed in line with the new
design. Includes new `instruction span` which has been added to the i18n
configuration.
New styles for file upload component. Includes adjusting the `z-index`
when the dropzone is toggled.
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.

4 participants