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

[SPIKE] Move focus and announce status on selection #5730

Conversation

romaricpascal
Copy link
Member

@romaricpascal romaricpascal commented Feb 20, 2025

Combines #5729 and #5726 to cater for JAWS + Firefox still announcing the accessible name of the button before the file picker is open.

After selecting a file, JAWS announces the accessible name of the button as it was before selecting. Sometimes this is the last announcement, which can make it confusing for users as the last thing they'll hear is the wrong state (potentially 'No file chosen' if it's the first time they use the component).

Moving the focus to another element than the button before picking and explicitely focusing the button once back seems to consistently let JAWS announce the correct state of the component.
This'll let us not forget that we have a fix for JAWS in place.

Testing requires us to trigger our own `focusin` event as Puppeteer does not
seem to support automatic focus/blur events: puppeteer/puppeteer#1462
Adds an automatic announcement of the status after users pick a file.

This should reassure JAWS users that their selection has been taken into account,
as JAWS announces the accessible name of the button before the file picker was open,
sometimes without re-announcing afterwards the accessible name once a file is picked.
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-5730 February 20, 2025 12:22 Inactive
@romaricpascal romaricpascal changed the title Move focus to an empty hidden span before picking files [SPIKE] Move focus and announce status on selection Feb 20, 2025
Copy link

📋 Stats

File sizes

File Size
dist/govuk-frontend-development.min.css 121.49 KiB
dist/govuk-frontend-development.min.js 48.32 KiB
packages/govuk-frontend/dist/govuk/all.bundle.js 102.78 KiB
packages/govuk-frontend/dist/govuk/all.bundle.mjs 96.54 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 121.47 KiB
packages/govuk-frontend/dist/govuk/govuk-frontend.min.js 48.3 KiB
packages/govuk-frontend/dist/govuk/i18n.mjs 5.55 KiB
packages/govuk-frontend/dist/govuk/init.mjs 6.89 KiB

Modules

File Size (bundled) Size (minified)
all.mjs 90.72 KiB 45.74 KiB
accordion.mjs 26.63 KiB 13.41 KiB
button.mjs 9.14 KiB 3.79 KiB
character-count.mjs 25.42 KiB 10.91 KiB
checkboxes.mjs 7.81 KiB 3.42 KiB
error-summary.mjs 11.04 KiB 4.55 KiB
exit-this-page.mjs 20.25 KiB 10.34 KiB
file-upload.mjs 21.46 KiB 11.32 KiB
header.mjs 6.46 KiB 3.22 KiB
notification-banner.mjs 9.4 KiB 3.71 KiB
password-input.mjs 18.21 KiB 8.34 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 9ba42b0

Copy link

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 f6220464f..64298272f 100644
--- a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
+++ b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
@@ -769,7 +769,7 @@ class FileUpload extends ConfigurableComponent {
         const o = this.$input.getAttribute("aria-describedby");
         o && s.setAttribute("aria-describedby", o);
         const r = document.createElement("span");
-        r.className = "govuk-body govuk-file-upload-button__status", r.innerText = this.i18n.t("noFileChosen"), s.appendChild(r);
+        r.className = "govuk-body govuk-file-upload-button__status", r.setAttribute("aria-live", "polite"), r.innerText = this.i18n.t("noFileChosen"), s.appendChild(r);
         const a = document.createElement("span");
         a.className = "govuk-visually-hidden", a.innerText = ", ", a.id = `${this.id}-comma`, s.appendChild(a);
         const l = document.createElement("span");
@@ -779,7 +779,7 @@ class FileUpload extends ConfigurableComponent {
         const u = document.createElement("span");
         u.className = "govuk-body govuk-file-upload-button__instruction", u.innerText = this.i18n.t("dropInstruction"), l.appendChild(u), s.appendChild(l), s.setAttribute("aria-labelledby", `${i.id} ${a.id} ${s.id}`), s.addEventListener("click", this.onClick.bind(this)), s.addEventListener("dragover", (t => {
             t.preventDefault()
-        })), this.$root.insertAdjacentElement("afterbegin", s), this.$input.setAttribute("tabindex", "-1"), this.$input.setAttribute("aria-hidden", "true"), this.$button = s, this.$status = r, this.$input.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.$root.insertAdjacentElement("afterend", this.$announcements), this.$button.addEventListener("drop", this.onDrop.bind(this)), document.addEventListener("dragenter", this.updateDropzoneVisibility.bind(this)), document.addEventListener("dragenter", (() => {
+        })), this.$root.insertAdjacentElement("afterbegin", s), this.$input.setAttribute("tabindex", "-1"), this.$input.setAttribute("aria-hidden", "true"), this.$focusMagnet = document.createElement("span"), this.$focusMagnet.setAttribute("tabindex", "-1"), this.$root.appendChild(this.$focusMagnet), this.$button = s, this.$status = r, this.$input.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.$root.insertAdjacentElement("afterend", this.$announcements), this.$button.addEventListener("drop", this.onDrop.bind(this)), document.addEventListener("dragenter", this.updateDropzoneVisibility.bind(this)), document.addEventListener("dragenter", (() => {
             this.enteredAnotherElement = !0
         })), document.addEventListener("dragleave", (() => {
             this.enteredAnotherElement || this.$button.disabled || (this.hideDraggingState(), this.$announcements.innerText = this.i18n.t("leftDropZone")), this.enteredAnotherElement = !1
@@ -795,7 +795,7 @@ class FileUpload extends ConfigurableComponent {
         this.$button.classList.remove("govuk-file-upload-button--dragging")
     }
     onDrop(t) {
-        t.preventDefault(), t.dataTransfer && isContainingFiles(t.dataTransfer) && (this.$input.files = t.dataTransfer.files, this.$input.dispatchEvent(new CustomEvent("change")), this.$announcements.innerText = this.$status.innerText, this.hideDraggingState())
+        t.preventDefault(), t.dataTransfer && isContainingFiles(t.dataTransfer) && (this.$input.files = t.dataTransfer.files, this.$input.dispatchEvent(new CustomEvent("change")), this.hideDraggingState())
     }
     onChange() {
         const t = this.$input.files.length;
@@ -812,7 +812,13 @@ class FileUpload extends ConfigurableComponent {
         return t
     }
     onClick() {
-        this.$input.click()
+        this.$focusMagnet.focus({
+            preventScroll: !0
+        }), document.addEventListener("focusin", (() => this.$button.focus({
+            preventScroll: !0
+        })), {
+            once: !0
+        }), this.$input.click()
     }
     observeDisabledState() {
         new MutationObserver((t => {

Action run for 9ba42b0

Copy link

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 b57e1a9a6..47c042a59 100644
--- a/packages/govuk-frontend/dist/govuk/all.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/all.bundle.js
@@ -1713,6 +1713,7 @@
       }
       const $status = document.createElement('span');
       $status.className = 'govuk-body govuk-file-upload-button__status';
+      $status.setAttribute('aria-live', 'polite');
       $status.innerText = this.i18n.t('noFileChosen');
       $button.appendChild($status);
       const commaSpan = document.createElement('span');
@@ -1740,6 +1741,9 @@
       this.$root.insertAdjacentElement('afterbegin', $button);
       this.$input.setAttribute('tabindex', '-1');
       this.$input.setAttribute('aria-hidden', 'true');
+      this.$focusMagnet = document.createElement('span');
+      this.$focusMagnet.setAttribute('tabindex', '-1');
+      this.$root.appendChild(this.$focusMagnet);
       this.$button = $button;
       this.$status = $status;
       this.$input.addEventListener('change', this.onChange.bind(this));
@@ -1804,7 +1808,6 @@
       if (event.dataTransfer && isContainingFiles(event.dataTransfer)) {
         this.$input.files = event.dataTransfer.files;
         this.$input.dispatchEvent(new CustomEvent('change'));
-        this.$announcements.innerText = this.$status.innerText;
         this.hideDraggingState();
       }
     }
@@ -1835,6 +1838,14 @@
       return $label;
     }
     onClick() {
+      this.$focusMagnet.focus({
+        preventScroll: true
+      });
+      document.addEventListener('focusin', () => this.$button.focus({
+        preventScroll: true
+      }), {
+        once: true
+      });
       this.$input.click();
     }
     observeDisabledState() {
diff --git a/packages/govuk-frontend/dist/govuk/all.bundle.mjs b/packages/govuk-frontend/dist/govuk/all.bundle.mjs
index bd59d9bb4..f735769e7 100644
--- a/packages/govuk-frontend/dist/govuk/all.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/all.bundle.mjs
@@ -1707,6 +1707,7 @@ class FileUpload extends ConfigurableComponent {
     }
     const $status = document.createElement('span');
     $status.className = 'govuk-body govuk-file-upload-button__status';
+    $status.setAttribute('aria-live', 'polite');
     $status.innerText = this.i18n.t('noFileChosen');
     $button.appendChild($status);
     const commaSpan = document.createElement('span');
@@ -1734,6 +1735,9 @@ class FileUpload extends ConfigurableComponent {
     this.$root.insertAdjacentElement('afterbegin', $button);
     this.$input.setAttribute('tabindex', '-1');
     this.$input.setAttribute('aria-hidden', 'true');
+    this.$focusMagnet = document.createElement('span');
+    this.$focusMagnet.setAttribute('tabindex', '-1');
+    this.$root.appendChild(this.$focusMagnet);
     this.$button = $button;
     this.$status = $status;
     this.$input.addEventListener('change', this.onChange.bind(this));
@@ -1798,7 +1802,6 @@ class FileUpload extends ConfigurableComponent {
     if (event.dataTransfer && isContainingFiles(event.dataTransfer)) {
       this.$input.files = event.dataTransfer.files;
       this.$input.dispatchEvent(new CustomEvent('change'));
-      this.$announcements.innerText = this.$status.innerText;
       this.hideDraggingState();
     }
   }
@@ -1829,6 +1832,14 @@ class FileUpload extends ConfigurableComponent {
     return $label;
   }
   onClick() {
+    this.$focusMagnet.focus({
+      preventScroll: true
+    });
+    document.addEventListener('focusin', () => this.$button.focus({
+      preventScroll: true
+    }), {
+      once: true
+    });
     this.$input.click();
   }
   observeDisabledState() {
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 6c37a8243..1121c9b63 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
@@ -540,6 +540,7 @@
       }
       const $status = document.createElement('span');
       $status.className = 'govuk-body govuk-file-upload-button__status';
+      $status.setAttribute('aria-live', 'polite');
       $status.innerText = this.i18n.t('noFileChosen');
       $button.appendChild($status);
       const commaSpan = document.createElement('span');
@@ -567,6 +568,9 @@
       this.$root.insertAdjacentElement('afterbegin', $button);
       this.$input.setAttribute('tabindex', '-1');
       this.$input.setAttribute('aria-hidden', 'true');
+      this.$focusMagnet = document.createElement('span');
+      this.$focusMagnet.setAttribute('tabindex', '-1');
+      this.$root.appendChild(this.$focusMagnet);
       this.$button = $button;
       this.$status = $status;
       this.$input.addEventListener('change', this.onChange.bind(this));
@@ -631,7 +635,6 @@
       if (event.dataTransfer && isContainingFiles(event.dataTransfer)) {
         this.$input.files = event.dataTransfer.files;
         this.$input.dispatchEvent(new CustomEvent('change'));
-        this.$announcements.innerText = this.$status.innerText;
         this.hideDraggingState();
       }
     }
@@ -662,6 +665,14 @@
       return $label;
     }
     onClick() {
+      this.$focusMagnet.focus({
+        preventScroll: true
+      });
+      document.addEventListener('focusin', () => this.$button.focus({
+        preventScroll: true
+      }), {
+        once: true
+      });
       this.$input.click();
     }
     observeDisabledState() {
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 307cbbd67..1199e11ab 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
@@ -534,6 +534,7 @@ class FileUpload extends ConfigurableComponent {
     }
     const $status = document.createElement('span');
     $status.className = 'govuk-body govuk-file-upload-button__status';
+    $status.setAttribute('aria-live', 'polite');
     $status.innerText = this.i18n.t('noFileChosen');
     $button.appendChild($status);
     const commaSpan = document.createElement('span');
@@ -561,6 +562,9 @@ class FileUpload extends ConfigurableComponent {
     this.$root.insertAdjacentElement('afterbegin', $button);
     this.$input.setAttribute('tabindex', '-1');
     this.$input.setAttribute('aria-hidden', 'true');
+    this.$focusMagnet = document.createElement('span');
+    this.$focusMagnet.setAttribute('tabindex', '-1');
+    this.$root.appendChild(this.$focusMagnet);
     this.$button = $button;
     this.$status = $status;
     this.$input.addEventListener('change', this.onChange.bind(this));
@@ -625,7 +629,6 @@ class FileUpload extends ConfigurableComponent {
     if (event.dataTransfer && isContainingFiles(event.dataTransfer)) {
       this.$input.files = event.dataTransfer.files;
       this.$input.dispatchEvent(new CustomEvent('change'));
-      this.$announcements.innerText = this.$status.innerText;
       this.hideDraggingState();
     }
   }
@@ -656,6 +659,14 @@ class FileUpload extends ConfigurableComponent {
     return $label;
   }
   onClick() {
+    this.$focusMagnet.focus({
+      preventScroll: true
+    });
+    document.addEventListener('focusin', () => this.$button.focus({
+      preventScroll: true
+    }), {
+      once: true
+    });
     this.$input.click();
   }
   observeDisabledState() {
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 482b1798c..b437df197 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
@@ -55,6 +55,7 @@ class FileUpload extends ConfigurableComponent {
     }
     const $status = document.createElement('span');
     $status.className = 'govuk-body govuk-file-upload-button__status';
+    $status.setAttribute('aria-live', 'polite');
     $status.innerText = this.i18n.t('noFileChosen');
     $button.appendChild($status);
     const commaSpan = document.createElement('span');
@@ -82,6 +83,9 @@ class FileUpload extends ConfigurableComponent {
     this.$root.insertAdjacentElement('afterbegin', $button);
     this.$input.setAttribute('tabindex', '-1');
     this.$input.setAttribute('aria-hidden', 'true');
+    this.$focusMagnet = document.createElement('span');
+    this.$focusMagnet.setAttribute('tabindex', '-1');
+    this.$root.appendChild(this.$focusMagnet);
     this.$button = $button;
     this.$status = $status;
     this.$input.addEventListener('change', this.onChange.bind(this));
@@ -146,7 +150,6 @@ class FileUpload extends ConfigurableComponent {
     if (event.dataTransfer && isContainingFiles(event.dataTransfer)) {
       this.$input.files = event.dataTransfer.files;
       this.$input.dispatchEvent(new CustomEvent('change'));
-      this.$announcements.innerText = this.$status.innerText;
       this.hideDraggingState();
     }
   }
@@ -177,6 +180,14 @@ class FileUpload extends ConfigurableComponent {
     return $label;
   }
   onClick() {
+    this.$focusMagnet.focus({
+      preventScroll: true
+    });
+    document.addEventListener('focusin', () => this.$button.focus({
+      preventScroll: true
+    }), {
+      once: true
+    });
     this.$input.click();
   }
   observeDisabledState() {

Action run for 9ba42b0

@romaricpascal
Copy link
Member Author

Closing in favour of #5729, which provides a better experience.

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.

Investigate workaround for screen readers not always announcing the selected file after picking
3 participants