-
Notifications
You must be signed in to change notification settings - Fork 336
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
Use button element for entire input replacement #5609
Conversation
📋 StatsFile sizes
Modules
View stats and visualisations on the review app Action run for 5d7fb7f |
JavaScript changes to npm packagediff --git a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
index 5a65848c9..374ce22cc 100644
--- a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
+++ b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
@@ -14,13 +14,13 @@ function getBreakpoint(t) {
function setFocus(t, e = {}) {
var n;
- const s = t.getAttribute("tabindex");
+ const i = t.getAttribute("tabindex");
function onBlur() {
var n;
- null == (n = e.onBlur) || n.call(t), s || t.removeAttribute("tabindex")
+ null == (n = e.onBlur) || n.call(t), i || t.removeAttribute("tabindex")
}
- s || t.setAttribute("tabindex", "-1"), t.addEventListener("focus", (function() {
+ i || t.setAttribute("tabindex", "-1"), t.addEventListener("focus", (function() {
t.addEventListener("blur", onBlur, {
once: !0
})
@@ -64,11 +64,11 @@ class ElementError extends GOVUKFrontendError {
if ("object" == typeof t) {
const {
component: n,
- identifier: s,
- element: i,
+ identifier: i,
+ element: s,
expectedType: o
} = t;
- e = s, e += i ? ` is not of type ${null!=o?o:"HTMLElement"}` : " not found", e = formatErrorMessage(n, e)
+ e = i, e += s ? ` is not of type ${null!=o?o:"HTMLElement"}` : " not found", e = formatErrorMessage(n, e)
}
super(e), this.name = "ElementError"
}
@@ -118,57 +118,57 @@ class ConfigurableComponent extends GOVUKFrontendComponent {
}
constructor(e, n) {
super(e), this._config = void 0;
- const s = this.constructor;
- if (void 0 === s.defaults) throw new ConfigError(formatErrorMessage(s, "Config passed as parameter into constructor but no defaults defined"));
- const i = function(Component, t) {
+ const i = this.constructor;
+ if (void 0 === i.defaults) throw new ConfigError(formatErrorMessage(i, "Config passed as parameter into constructor but no defaults defined"));
+ const s = function(Component, t) {
if (void 0 === Component.schema) throw new ConfigError(formatErrorMessage(Component, "Config passed as parameter into constructor but no schema defined"));
const e = {};
- for (const [n, s] of Object.entries(Component.schema.properties)) n in t && (e[n] = normaliseString(t[n], s)), "object" === (null == s ? void 0 : s.type) && (e[n] = extractConfigByNamespace(Component.schema, t, n));
+ for (const [n, i] of Object.entries(Component.schema.properties)) n in t && (e[n] = normaliseString(t[n], i)), "object" === (null == i ? void 0 : i.type) && (e[n] = extractConfigByNamespace(Component.schema, t, n));
return e
- }(s, this._$root.dataset);
- this._config = mergeConfigs(s.defaults, null != n ? n : {}, this[t](i), i)
+ }(i, this._$root.dataset);
+ this._config = mergeConfigs(i.defaults, null != n ? n : {}, this[t](s), s)
}
}
function normaliseString(t, e) {
const n = t ? t.trim() : "";
- let s, i = null == e ? void 0 : e.type;
- switch (i || (["true", "false"].includes(n) && (i = "boolean"), n.length > 0 && isFinite(Number(n)) && (i = "number")), i) {
+ let i, s = null == e ? void 0 : e.type;
+ switch (s || (["true", "false"].includes(n) && (s = "boolean"), n.length > 0 && isFinite(Number(n)) && (s = "number")), s) {
case "boolean":
- s = "true" === n;
+ i = "true" === n;
break;
case "number":
- s = Number(n);
+ i = Number(n);
break;
default:
- s = t
+ i = t
}
- return s
+ return i
}
function mergeConfigs(...t) {
const e = {};
for (const n of t)
for (const t of Object.keys(n)) {
- const s = e[t],
- i = n[t];
- isObject(s) && isObject(i) ? e[t] = mergeConfigs(s, i) : e[t] = i
+ const i = e[t],
+ s = n[t];
+ isObject(i) && isObject(s) ? e[t] = mergeConfigs(i, s) : e[t] = s
}
return e
}
function extractConfigByNamespace(t, e, n) {
- const s = t.properties[n];
- if ("object" !== (null == s ? void 0 : s.type)) return;
- const i = {
+ const i = t.properties[n];
+ if ("object" !== (null == i ? void 0 : i.type)) return;
+ const s = {
[n]: {}
};
for (const [o, r] of Object.entries(e)) {
- let t = i;
+ let t = s;
const e = o.split(".");
- for (const [s, i] of e.entries()) "object" == typeof t && (s < e.length - 1 ? (isObject(t[i]) || (t[i] = {}), t = t[i]) : o !== n && (t[i] = normaliseString(r)))
+ for (const [i, s] of e.entries()) "object" == typeof t && (i < e.length - 1 ? (isObject(t[s]) || (t[s] = {}), t = t[s]) : o !== n && (t[s] = normaliseString(r)))
}
- return i[n]
+ return s[n]
}
class I18n {
constructor(t = {}, e = {}) {
@@ -179,8 +179,8 @@ class I18n {
if (!t) throw new Error("i18n: lookup key missing");
let n = this.translations[t];
if ("number" == typeof(null == e ? void 0 : e.count) && "object" == typeof n) {
- const s = n[this.getPluralSuffix(t, e.count)];
- s && (n = s)
+ const i = n[this.getPluralSuffix(t, e.count)];
+ i && (n = i)
}
if ("string" == typeof n) {
if (n.match(/%{(.\S+)}/)) {
@@ -193,9 +193,9 @@ class I18n {
}
replacePlaceholders(t, e) {
const n = Intl.NumberFormat.supportedLocalesOf(this.locale).length ? new Intl.NumberFormat(this.locale) : void 0;
- return t.replace(/%{(.\S+)}/g, (function(t, s) {
- if (Object.prototype.hasOwnProperty.call(e, s)) {
- const t = e[s];
+ return t.replace(/%{(.\S+)}/g, (function(t, i) {
+ if (Object.prototype.hasOwnProperty.call(e, i)) {
+ const t = e[i];
return !1 === t || "number" != typeof t && "string" != typeof t ? "" : "number" == typeof t ? n ? n.format(t) : `${t}` : t
}
throw new Error(`i18n: no data found to replace ${t} placeholder in string`)
@@ -207,10 +207,10 @@ class I18n {
getPluralSuffix(t, e) {
if (e = Number(e), !isFinite(e)) return "other";
const n = this.translations[t],
- s = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(e) : this.selectPluralFormUsingFallbackRules(e);
+ i = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(e) : this.selectPluralFormUsingFallbackRules(e);
if ("object" == typeof n) {
- if (s in n) return s;
- if ("other" in n) return console.warn(`i18n: Missing plural form ".${s}" for "${this.locale}" locale. Falling back to ".other".`), "other"
+ if (i in n) return i;
+ if ("other" in n) return console.warn(`i18n: Missing plural form ".${i}" for "${this.locale}" locale. Falling back to ".other".`), "other"
}
throw new Error(`i18n: Plural form ".other" is required for "${this.locale}" locale`)
}
@@ -279,9 +279,9 @@ class Accordion extends ConfigurableComponent {
}
constructHeaderMarkup(t, e) {
const n = t.querySelector(`.${this.sectionButtonClass}`),
- s = t.querySelector(`.${this.sectionHeadingClass}`),
- i = t.querySelector(`.${this.sectionSummaryClass}`);
- if (!s) throw new ElementError({
+ i = t.querySelector(`.${this.sectionHeadingClass}`),
+ s = t.querySelector(`.${this.sectionSummaryClass}`);
+ if (!i) throw new ElementError({
component: Accordion,
identifier: `Section heading (\`.${this.sectionHeadingClass}\`)`
});
@@ -302,14 +302,14 @@ class Accordion extends ConfigurableComponent {
c.classList.add(this.sectionShowHideToggleFocusClass), l.appendChild(c);
const h = document.createElement("span"),
u = document.createElement("span");
- if (u.classList.add(this.upChevronIconClass), c.appendChild(u), h.classList.add(this.sectionShowHideTextClass), c.appendChild(h), o.appendChild(r), o.appendChild(this.getButtonPunctuationEl()), i) {
+ if (u.classList.add(this.upChevronIconClass), c.appendChild(u), h.classList.add(this.sectionShowHideTextClass), c.appendChild(h), o.appendChild(r), o.appendChild(this.getButtonPunctuationEl()), s) {
const t = document.createElement("span"),
e = document.createElement("span");
e.classList.add(this.sectionSummaryFocusClass), t.appendChild(e);
- for (const n of Array.from(i.attributes)) t.setAttribute(n.name, n.value);
- Array.from(i.childNodes).forEach((t => e.appendChild(t))), i.remove(), o.appendChild(t), o.appendChild(this.getButtonPunctuationEl())
+ for (const n of Array.from(s.attributes)) t.setAttribute(n.name, n.value);
+ Array.from(s.childNodes).forEach((t => e.appendChild(t))), s.remove(), o.appendChild(t), o.appendChild(this.getButtonPunctuationEl())
}
- o.appendChild(l), s.removeChild(n), s.appendChild(o)
+ o.appendChild(l), i.removeChild(n), i.appendChild(o)
}
onBeforeMatch(t) {
const e = t.target;
@@ -329,23 +329,23 @@ class Accordion extends ConfigurableComponent {
}
setExpanded(t, e) {
const n = e.querySelector(`.${this.upChevronIconClass}`),
- s = e.querySelector(`.${this.sectionShowHideTextClass}`),
- i = e.querySelector(`.${this.sectionButtonClass}`),
+ i = e.querySelector(`.${this.sectionShowHideTextClass}`),
+ s = e.querySelector(`.${this.sectionButtonClass}`),
o = e.querySelector(`.${this.sectionContentClass}`);
if (!o) throw new ElementError({
component: Accordion,
identifier: `Section content (\`<div class="${this.sectionContentClass}">\`)`
});
- if (!n || !s || !i) return;
+ if (!n || !i || !s) return;
const r = t ? this.i18n.t("hideSection") : this.i18n.t("showSection");
- s.textContent = r, i.setAttribute("aria-expanded", `${t}`);
+ i.textContent = r, s.setAttribute("aria-expanded", `${t}`);
const a = [],
l = e.querySelector(`.${this.sectionHeadingTextClass}`);
l && a.push(`${l.textContent}`.trim());
const c = e.querySelector(`.${this.sectionSummaryClass}`);
c && a.push(`${c.textContent}`.trim());
const h = t ? this.i18n.t("hideSectionAriaLabel") : this.i18n.t("showSectionAriaLabel");
- a.push(h), i.setAttribute("aria-label", a.join(" , ")), t ? (o.removeAttribute("hidden"), e.classList.add(this.sectionExpandedClass), n.classList.remove(this.downChevronIconClass)) : (o.setAttribute("hidden", "until-found"), e.classList.remove(this.sectionExpandedClass), n.classList.add(this.downChevronIconClass)), this.updateShowAllButton(this.areAllSectionsOpen())
+ a.push(h), s.setAttribute("aria-label", a.join(" , ")), t ? (o.removeAttribute("hidden"), e.classList.add(this.sectionExpandedClass), n.classList.remove(this.downChevronIconClass)) : (o.setAttribute("hidden", "until-found"), e.classList.remove(this.sectionExpandedClass), n.classList.add(this.downChevronIconClass)), this.updateShowAllButton(this.areAllSectionsOpen())
}
isExpanded(t) {
return t.classList.contains(this.sectionExpandedClass)
@@ -365,7 +365,7 @@ class Accordion extends ConfigurableComponent {
const n = this.getIdentifier(t);
if (n) try {
window.sessionStorage.setItem(n, e.toString())
- } catch (s) {}
+ } catch (i) {}
}
setInitialState(t) {
if (!this.config.rememberExpanded) return;
@@ -437,26 +437,26 @@ class CharacterCount extends ConfigurableComponent {
}), e
}
constructor(t, e = {}) {
- var n, s;
+ var n, i;
super(t, e), this.$textarea = void 0, this.$visibleCountMessage = void 0, this.$screenReaderCountMessage = void 0, this.lastInputTimestamp = null, this.lastInputValue = "", this.valueChecker = null, this.i18n = void 0, this.maxLength = void 0;
- const i = this.$root.querySelector(".govuk-js-character-count");
- if (!(i instanceof HTMLTextAreaElement || i instanceof HTMLInputElement)) throw new ElementError({
+ const s = this.$root.querySelector(".govuk-js-character-count");
+ if (!(s instanceof HTMLTextAreaElement || s instanceof HTMLInputElement)) throw new ElementError({
component: CharacterCount,
- element: i,
+ element: s,
expectedType: "HTMLTextareaElement or HTMLInputElement",
identifier: "Form field (`.govuk-js-character-count`)"
});
const o = function(t, e) {
const n = [];
- for (const [s, i] of Object.entries(t)) {
+ for (const [i, s] of Object.entries(t)) {
const t = [];
- if (Array.isArray(i)) {
+ if (Array.isArray(s)) {
for (const {
required: n,
- errorMessage: s
+ errorMessage: i
}
- of i) n.every((t => !!e[t])) || t.push(s);
- "anyOf" !== s || i.length - t.length >= 1 || n.push(...t)
+ of s) n.every((t => !!e[t])) || t.push(i);
+ "anyOf" !== i || s.length - t.length >= 1 || n.push(...t)
}
}
return n
@@ -464,7 +464,7 @@ class CharacterCount extends ConfigurableComponent {
if (o[0]) throw new ConfigError(formatErrorMessage(CharacterCount, o[0]));
this.i18n = new I18n(this.config.i18n, {
locale: closestAttributeValue(this.$root, "lang")
- }), this.maxLength = null != (n = null != (s = this.config.maxwords) ? s : this.config.maxlength) ? n : 1 / 0, this.$textarea = i;
+ }), this.maxLength = null != (n = null != (i = this.config.maxwords) ? i : this.config.maxlength) ? n : 1 / 0, this.$textarea = s;
const r = `${this.$textarea.id}-info`,
a = document.getElementById(r);
if (!a) throw new ElementError({
@@ -643,8 +643,8 @@ class ErrorSummary extends ConfigurableComponent {
if (!e) return !1;
const n = document.getElementById(e);
if (!n) return !1;
- const s = this.getAssociatedLegendOrLabel(n);
- return !!s && (s.scrollIntoView(), n.focus({
+ const i = this.getAssociatedLegendOrLabel(n);
+ return !!i && (i.scrollIntoView(), n.focus({
preventScroll: !0
}), !0)
}
@@ -656,10 +656,10 @@ class ErrorSummary extends ConfigurableComponent {
if (e.length) {
const n = e[0];
if (t instanceof HTMLInputElement && ("checkbox" === t.type || "radio" === t.type)) return n;
- const s = n.getBoundingClientRect().top,
- i = t.getBoundingClientRect();
- if (i.height && window.innerHeight) {
- if (i.top + i.height - s < window.innerHeight / 2) return n
+ const i = n.getBoundingClientRect().top,
+ s = t.getBoundingClientRect();
+ if (s.height && window.innerHeight) {
+ if (s.top + s.height - i < window.innerHeight / 2) return n
}
}
}
@@ -686,8 +686,8 @@ class ExitThisPage extends ConfigurableComponent {
identifier: "Button (`.govuk-exit-this-page__button`)"
});
this.i18n = new I18n(this.config.i18n), this.$button = n;
- const s = document.querySelector(".govuk-js-exit-this-page-skiplink");
- s instanceof HTMLAnchorElement && (this.$skiplinkButton = s), this.buildIndicator(), this.initUpdateSpan(), this.initButtonClickHandler(), "govukFrontendExitThisPageKeypress" in document.body.dataset || (document.addEventListener("keyup", this.handleKeypress.bind(this), !0), document.body.dataset.govukFrontendExitThisPageKeypress = "true"), window.addEventListener("pageshow", this.resetPage.bind(this))
+ const i = document.querySelector(".govuk-js-exit-this-page-skiplink");
+ i instanceof HTMLAnchorElement && (this.$skiplinkButton = i), this.buildIndicator(), this.initUpdateSpan(), this.initButtonClickHandler(), "govukFrontendExitThisPageKeypress" in document.body.dataset || (document.addEventListener("keyup", this.handleKeypress.bind(this), !0), document.body.dataset.govukFrontendExitThisPageKeypress = "true"), window.addEventListener("pageshow", this.resetPage.bind(this))
}
initUpdateSpan() {
this.$updateSpan = document.createElement("span"), this.$updateSpan.setAttribute("role", "status"), this.$updateSpan.className = "govuk-visually-hidden", this.$root.appendChild(this.$updateSpan)
@@ -750,16 +750,19 @@ ExitThisPage.moduleName = "govuk-exit-this-page", ExitThisPage.defaults = Object
});
class FileUpload extends ConfigurableComponent {
constructor(t, e = {}) {
- if (super(t, e), this.$wrapper = void 0, this.$button = void 0, this.$status = void 0, this.i18n = void 0, "file" !== this.$root.type) throw new ElementError(formatErrorMessage(FileUpload, "Form field must be an input of type `file`."));
- this.i18n = new I18n(this.config.i18n, {
+ if (super(t, e), this.$wrapper = void 0, this.$button = void 0, this.$status = void 0, this.i18n = void 0, this.id = void 0, "file" !== this.$root.type) throw new ElementError(formatErrorMessage(FileUpload, "Form field must be an input of type `file`."));
+ if (!this.$root.id.length) throw new ElementError(formatErrorMessage(FileUpload, "Form field must specify an `id`."));
+ this.id = this.$root.id, this.i18n = new I18n(this.config.i18n, {
locale: closestAttributeValue(this.$root, "lang")
- }), this.$label = this.findLabel();
+ }), this.$label = this.findLabel(), this.$root.id = `${this.id}-input`;
const n = document.createElement("div");
n.className = "govuk-file-upload-wrapper";
- const s = document.createElement("button");
- s.className = "govuk-button govuk-button--secondary govuk-file-upload__button", s.type = "button", s.innerText = this.i18n.t("selectFilesButton"), s.addEventListener("click", this.onClick.bind(this));
- const i = document.createElement("span");
- i.className = "govuk-body govuk-file-upload__status", i.innerText = this.i18n.t("filesSelectedDefault"), i.setAttribute("role", "status"), n.insertAdjacentElement("beforeend", s), n.insertAdjacentElement("beforeend", i), this.$root.insertAdjacentElement("afterend", n), n.insertAdjacentElement("afterbegin", this.$root), this.$wrapper = n, this.$button = s, this.$status = i, this.$root.setAttribute("tabindex", "-1"), 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", (() => {
+ const i = document.createElement("button");
+ i.classList.add("govuk-file-upload__button"), i.type = "button", i.id = this.id;
+ const s = document.createElement("span");
+ s.className = "govuk-button govuk-button--secondary govuk-file-upload__pseudo-button", s.innerText = this.i18n.t("selectFilesButton"), s.setAttribute("aria-hidden", "true"), i.appendChild(s), i.addEventListener("click", this.onClick.bind(this));
+ const o = document.createElement("span");
+ o.className = "govuk-body govuk-file-upload__status", o.innerText = this.i18n.t("filesSelectedDefault"), o.setAttribute("aria-hidden", "true"), i.appendChild(o), 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 = o, 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", (() => {
this.enteredAnotherElement = !0
})), document.addEventListener("dragleave", (() => {
this.enteredAnotherElement || this.hideDropZone(), this.enteredAnotherElement = !1
@@ -779,7 +782,7 @@ class FileUpload extends ConfigurableComponent {
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", {
count: t
- })
+ }), this.$button.setAttribute("aria-label", `${this.$label.innerText}, ${this.i18n.t("selectFilesButton")}, ${this.$status.innerText}`)
}
findLabel() {
const t = document.querySelector(`label[for="${this.$root.id}"]`);
@@ -790,7 +793,7 @@ class FileUpload extends ConfigurableComponent {
return t
}
onClick() {
- this.$label.click()
+ this.$root.click()
}
observeDisabledState() {
new MutationObserver((t => {
@@ -831,13 +834,13 @@ class Header extends GOVUKFrontendComponent {
component: Header,
identifier: 'Navigation button (`<button class="govuk-js-header-toggle">`) attribute (`aria-controls`)'
});
- const s = document.getElementById(n);
- if (!s) throw new ElementError({
+ const i = document.getElementById(n);
+ if (!i) throw new ElementError({
component: Header,
- element: s,
+ element: i,
identifier: `Navigation (\`<ul id="${n}">\`)`
});
- this.$menu = s, this.$menuButton = e, this.setupResponsiveChecks(), this.$menuButton.addEventListener("click", (() => this.handleMenuButtonClick()))
+ this.$menu = i, this.$menuButton = e, this.setupResponsiveChecks(), this.$menuButton.addEventListener("click", (() => this.handleMenuButtonClick()))
}
setupResponsiveChecks() {
const t = getBreakpoint("desktop");
@@ -880,19 +883,19 @@ class PasswordInput extends ConfigurableComponent {
identifier: "Form field (`.govuk-js-password-input-input`)"
});
if ("password" !== n.type) throw new ElementError("Password input: Form field (`.govuk-js-password-input-input`) must be of type `password`.");
- const s = this.$root.querySelector(".govuk-js-password-input-toggle");
- if (!(s instanceof HTMLButtonElement)) throw new ElementError({
+ const i = this.$root.querySelector(".govuk-js-password-input-toggle");
+ if (!(i instanceof HTMLButtonElement)) throw new ElementError({
component: PasswordInput,
- element: s,
+ element: i,
expectedType: "HTMLButtonElement",
identifier: "Button (`.govuk-js-password-input-toggle`)"
});
- if ("button" !== s.type) throw new ElementError("Password input: Button (`.govuk-js-password-input-toggle`) must be of type `button`.");
- this.$input = n, this.$showHideButton = s, this.i18n = new I18n(this.config.i18n, {
+ if ("button" !== i.type) throw new ElementError("Password input: Button (`.govuk-js-password-input-toggle`) must be of type `button`.");
+ this.$input = n, this.$showHideButton = i, this.i18n = new I18n(this.config.i18n, {
locale: closestAttributeValue(this.$root, "lang")
}), this.$showHideButton.removeAttribute("hidden");
- const i = document.createElement("div");
- i.className = "govuk-password-input__sr-status govuk-visually-hidden", i.setAttribute("aria-live", "polite"), this.$screenReaderStatusMessage = i, this.$input.insertAdjacentElement("afterend", i), this.$showHideButton.addEventListener("click", this.toggle.bind(this)), this.$input.form && this.$input.form.addEventListener("submit", (() => this.hide())), window.addEventListener("pageshow", (t => {
+ const s = document.createElement("div");
+ s.className = "govuk-password-input__sr-status govuk-visually-hidden", s.setAttribute("aria-live", "polite"), this.$screenReaderStatusMessage = s, this.$input.insertAdjacentElement("afterend", s), this.$showHideButton.addEventListener("click", this.toggle.bind(this)), this.$input.form && this.$input.form.addEventListener("submit", (() => this.hide())), window.addEventListener("pageshow", (t => {
t.persisted && "password" !== this.$input.type && this.hide()
})), this.hide()
}
@@ -910,8 +913,8 @@ class PasswordInput extends ConfigurableComponent {
this.$input.setAttribute("type", t);
const e = "password" === t,
n = e ? "show" : "hide",
- s = e ? "passwordHidden" : "passwordShown";
- this.$showHideButton.innerText = this.i18n.t(`${n}Password`), this.$showHideButton.setAttribute("aria-label", this.i18n.t(`${n}PasswordAriaLabel`)), this.$screenReaderStatusMessage.innerText = this.i18n.t(`${s}Announcement`)
+ i = e ? "passwordHidden" : "passwordShown";
+ this.$showHideButton.innerText = this.i18n.t(`${n}Password`), this.$showHideButton.setAttribute("aria-label", this.i18n.t(`${n}PasswordAriaLabel`)), this.$screenReaderStatusMessage.innerText = this.i18n.t(`${i}Announcement`)
}
}
PasswordInput.moduleName = "govuk-password-input", PasswordInput.defaults = Object.freeze({
@@ -965,11 +968,11 @@ class Radios extends GOVUKFrontendComponent {
const e = t.target;
if (!(e instanceof HTMLInputElement) || "radio" !== e.type) return;
const n = document.querySelectorAll('input[type="radio"][aria-controls]'),
- s = e.form,
- i = e.name;
+ i = e.form,
+ s = e.name;
n.forEach((t => {
- const e = t.form === s;
- t.name === i && e && this.syncConditionalRevealWithInputState(t)
+ const e = t.form === i;
+ t.name === s && e && this.syncConditionalRevealWithInputState(t)
}))
}
}
@@ -984,13 +987,13 @@ class ServiceNavigation extends GOVUKFrontendComponent {
component: ServiceNavigation,
identifier: 'Navigation button (`<button class="govuk-js-service-navigation-toggle">`) attribute (`aria-controls`)'
});
- const s = document.getElementById(n);
- if (!s) throw new ElementError({
+ const i = document.getElementById(n);
+ if (!i) throw new ElementError({
component: ServiceNavigation,
- element: s,
+ element: i,
identifier: `Navigation (\`<ul id="${n}">\`)`
});
- this.$menu = s, this.$menuButton = e, this.setupResponsiveChecks(), this.$menuButton.addEventListener("click", (() => this.handleMenuButtonClick()))
+ this.$menu = i, this.$menuButton = e, this.setupResponsiveChecks(), this.$menuButton.addEventListener("click", (() => this.handleMenuButtonClick()))
}
setupResponsiveChecks() {
const t = getBreakpoint("tablet");
@@ -1013,16 +1016,16 @@ class SkipLink extends GOVUKFrontendComponent {
var e;
super(t);
const n = this.$root.hash,
- s = null != (e = this.$root.getAttribute("href")) ? e : "";
- let i;
+ i = null != (e = this.$root.getAttribute("href")) ? e : "";
+ let s;
try {
- i = new window.URL(this.$root.href)
+ s = new window.URL(this.$root.href)
} catch (a) {
- throw new ElementError(`Skip link: Target link (\`href="${s}"\`) is invalid`)
+ throw new ElementError(`Skip link: Target link (\`href="${i}"\`) is invalid`)
}
- if (i.origin !== window.location.origin || i.pathname !== window.location.pathname) return;
+ if (s.origin !== window.location.origin || s.pathname !== window.location.pathname) return;
const o = getFragmentFromUrl(n);
- if (!o) throw new ElementError(`Skip link: Target link (\`href="${s}"\`) has no hash fragment`);
+ if (!o) throw new ElementError(`Skip link: Target link (\`href="${i}"\`) has no hash fragment`);
const r = document.getElementById(o);
if (!r) throw new ElementError({
component: SkipLink,
@@ -1050,16 +1053,16 @@ class Tabs extends GOVUKFrontendComponent {
});
this.$tabs = e, this.boundTabClick = this.onTabClick.bind(this), this.boundTabKeydown = this.onTabKeydown.bind(this), this.boundOnHashChange = this.onHashChange.bind(this);
const n = this.$root.querySelector(".govuk-tabs__list"),
- s = this.$root.querySelectorAll("li.govuk-tabs__list-item");
+ i = this.$root.querySelectorAll("li.govuk-tabs__list-item");
if (!n) throw new ElementError({
component: Tabs,
identifier: 'List (`<ul class="govuk-tabs__list">`)'
});
- if (!s.length) throw new ElementError({
+ if (!i.length) throw new ElementError({
component: Tabs,
identifier: 'List items (`<li class="govuk-tabs__list-item">`)'
});
- this.$tabList = n, this.$tabListItems = s, this.setupResponsiveChecks()
+ this.$tabList = n, this.$tabListItems = i, this.setupResponsiveChecks()
}
setupResponsiveChecks() {
const t = getBreakpoint("tablet");
@@ -1201,30 +1204,30 @@ function initAll(t) {
[SkipLink],
[Tabs]
],
- s = {
+ i = {
scope: null != (e = t.scope) ? e : document,
onError: t.onError
};
n.forEach((([Component, t]) => {
- createAll(Component, t, s)
+ createAll(Component, t, i)
}))
}
function createAll(Component, t, e) {
- let n, s = document;
- var i;
- "object" == typeof e && (s = null != (i = e.scope) ? i : s, n = e.onError);
- "function" == typeof e && (n = e), e instanceof HTMLElement && (s = e);
- const o = s.querySelectorAll(`[data-module="${Component.moduleName}"]`);
+ let n, i = document;
+ var s;
+ "object" == typeof e && (i = null != (s = e.scope) ? s : i, n = e.onError);
+ "function" == typeof e && (n = e), e instanceof HTMLElement && (i = e);
+ const o = i.querySelectorAll(`[data-module="${Component.moduleName}"]`);
return isSupported() ? Array.from(o).map((e => {
try {
return void 0 !== t ? new Component(e, t) : new Component(e)
- } catch (s) {
- return n ? n(s, {
+ } catch (i) {
+ return n ? n(i, {
element: e,
component: Component,
config: t
- }) : console.log(s), null
+ }) : console.log(i), null
}
})).filter(Boolean) : (n ? n(new SupportError, {
component: Component,
Action run for 5d7fb7f |
Stylesheets changes to npm packagediff --git a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.css b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.css
index f1b48d569..9417707ca 100644
--- a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.css
+++ b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.css
@@ -3397,7 +3397,7 @@ screen and (forced-colors:active) {
background-color: #fff
}
-.govuk-file-upload-wrapper--show-dropzone .govuk-file-upload__button,
+.govuk-file-upload-wrapper--show-dropzone .govuk-file-upload__pseudo-button,
.govuk-file-upload-wrapper--show-dropzone .govuk-file-upload__status {
pointer-events: none
}
@@ -3413,7 +3413,7 @@ screen and (forced-colors:active) {
opacity: 0
}
-.govuk-file-upload__button {
+.govuk-file-upload__pseudo-button {
width: auto;
margin-bottom: 0;
flex-grow: 0;
@@ -3425,6 +3425,53 @@ screen and (forced-colors:active) {
margin-left: 10px
}
+.govuk-file-upload__button:focus {
+ outline: none
+}
+
+.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__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__button:active .govuk-file-upload__pseudo-button:hover {
+ background-color: #c2c2c1
+}
+
+.govuk-file-upload__button {
+ align-items: center;
+ display: flex;
+ padding: 0;
+ border: 0;
+ background-color: transparent
+}
+
+.govuk-file-upload:disabled+.govuk-file-upload__button {
+ pointer-events: none
+}
+
+.govuk-file-upload:disabled+.govuk-file-upload__button .govuk-file-upload__pseudo-button {
+ opacity: .5
+}
+
+.govuk-file-upload:disabled+.govuk-file-upload__button .govuk-file-upload__pseudo-button:hover {
+ background-color: #f3f2f1;
+ cursor: not-allowed
+}
+
+.govuk-file-upload:disabled+.govuk-file-upload__button .govuk-file-upload__pseudo-button:active {
+ top: 0;
+ box-shadow: 0 2px 0 #666
+}
+
.govuk-footer {
font-family: GDS Transport, arial, sans-serif;
-webkit-font-smoothing: antialiased;
Action run for 5d7fb7f |
Other changes to npm packagediff --git a/packages/govuk-frontend/dist/govuk/all.bundle.js b/packages/govuk-frontend/dist/govuk/all.bundle.js
index 11771add6..a6d922cdd 100644
--- a/packages/govuk-frontend/dist/govuk/all.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/all.bundle.js
@@ -1671,32 +1671,46 @@
this.$button = void 0;
this.$status = void 0;
this.i18n = void 0;
+ this.id = void 0;
if (this.$root.type !== 'file') {
throw new ElementError(formatErrorMessage(FileUpload, 'Form field must be an input of type `file`.'));
}
+ if (!this.$root.id.length) {
+ throw new ElementError(formatErrorMessage(FileUpload, 'Form field must specify an `id`.'));
+ }
+ this.id = this.$root.id;
this.i18n = new I18n(this.config.i18n, {
locale: closestAttributeValue(this.$root, 'lang')
});
this.$label = this.findLabel();
+ this.$root.id = `${this.id}-input`;
const $wrapper = document.createElement('div');
$wrapper.className = 'govuk-file-upload-wrapper';
const $button = document.createElement('button');
- $button.className = 'govuk-button govuk-button--secondary govuk-file-upload__button';
+ $button.classList.add('govuk-file-upload__button');
$button.type = 'button';
- $button.innerText = this.i18n.t('selectFilesButton');
+ $button.id = this.id;
+ 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('role', 'status');
+ $status.setAttribute('aria-hidden', 'true');
+ $button.appendChild($status);
+ $button.setAttribute('aria-label', `${this.$label.innerText}, ${this.i18n.t('selectFilesButton')}, ${this.i18n.t('filesSelectedDefault')}`);
$wrapper.insertAdjacentElement('beforeend', $button);
- $wrapper.insertAdjacentElement('beforeend', $status);
this.$root.insertAdjacentElement('afterend', $wrapper);
+ this.$root.setAttribute('tabindex', '-1');
+ this.$root.setAttribute('aria-hidden', 'true');
$wrapper.insertAdjacentElement('afterbegin', this.$root);
this.$wrapper = $wrapper;
this.$button = $button;
this.$status = $status;
- this.$root.setAttribute('tabindex', '-1');
+ this.$root.addEventListener('change', this.onChange.bind(this));
this.updateDisabledState();
this.observeDisabledState();
this.$root.addEventListener('change', this.onChange.bind(this));
@@ -1754,6 +1768,7 @@
count: fileCount
});
}
+ this.$button.setAttribute('aria-label', `${this.$label.innerText}, ${this.i18n.t('selectFilesButton')}, ${this.$status.innerText}`);
}
findLabel() {
const $label = document.querySelector(`label[for="${this.$root.id}"]`);
@@ -1766,7 +1781,7 @@
return $label;
}
onClick() {
- this.$label.click();
+ this.$root.click();
}
observeDisabledState() {
const observer = new MutationObserver(mutationList => {
diff --git a/packages/govuk-frontend/dist/govuk/all.bundle.mjs b/packages/govuk-frontend/dist/govuk/all.bundle.mjs
index ebc802c20..62c861a22 100644
--- a/packages/govuk-frontend/dist/govuk/all.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/all.bundle.mjs
@@ -1665,32 +1665,46 @@ class FileUpload extends ConfigurableComponent {
this.$button = void 0;
this.$status = void 0;
this.i18n = void 0;
+ this.id = void 0;
if (this.$root.type !== 'file') {
throw new ElementError(formatErrorMessage(FileUpload, 'Form field must be an input of type `file`.'));
}
+ if (!this.$root.id.length) {
+ throw new ElementError(formatErrorMessage(FileUpload, 'Form field must specify an `id`.'));
+ }
+ this.id = this.$root.id;
this.i18n = new I18n(this.config.i18n, {
locale: closestAttributeValue(this.$root, 'lang')
});
this.$label = this.findLabel();
+ this.$root.id = `${this.id}-input`;
const $wrapper = document.createElement('div');
$wrapper.className = 'govuk-file-upload-wrapper';
const $button = document.createElement('button');
- $button.className = 'govuk-button govuk-button--secondary govuk-file-upload__button';
+ $button.classList.add('govuk-file-upload__button');
$button.type = 'button';
- $button.innerText = this.i18n.t('selectFilesButton');
+ $button.id = this.id;
+ 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('role', 'status');
+ $status.setAttribute('aria-hidden', 'true');
+ $button.appendChild($status);
+ $button.setAttribute('aria-label', `${this.$label.innerText}, ${this.i18n.t('selectFilesButton')}, ${this.i18n.t('filesSelectedDefault')}`);
$wrapper.insertAdjacentElement('beforeend', $button);
- $wrapper.insertAdjacentElement('beforeend', $status);
this.$root.insertAdjacentElement('afterend', $wrapper);
+ this.$root.setAttribute('tabindex', '-1');
+ this.$root.setAttribute('aria-hidden', 'true');
$wrapper.insertAdjacentElement('afterbegin', this.$root);
this.$wrapper = $wrapper;
this.$button = $button;
this.$status = $status;
- this.$root.setAttribute('tabindex', '-1');
+ this.$root.addEventListener('change', this.onChange.bind(this));
this.updateDisabledState();
this.observeDisabledState();
this.$root.addEventListener('change', this.onChange.bind(this));
@@ -1748,6 +1762,7 @@ class FileUpload extends ConfigurableComponent {
count: fileCount
});
}
+ this.$button.setAttribute('aria-label', `${this.$label.innerText}, ${this.i18n.t('selectFilesButton')}, ${this.$status.innerText}`);
}
findLabel() {
const $label = document.querySelector(`label[for="${this.$root.id}"]`);
@@ -1760,7 +1775,7 @@ class FileUpload extends ConfigurableComponent {
return $label;
}
onClick() {
- this.$label.click();
+ this.$root.click();
}
observeDisabledState() {
const observer = new MutationObserver(mutationList => {
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 34e781c51..4dc478173 100644
--- a/packages/govuk-frontend/dist/govuk/components/file-upload/_index.scss
+++ b/packages/govuk-frontend/dist/govuk/components/file-upload/_index.scss
@@ -64,7 +64,7 @@
border: $govuk-border-width-form-element dashed $govuk-input-border-colour;
background-color: $govuk-body-background-colour;
- .govuk-file-upload__button,
+ .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
@@ -85,7 +85,7 @@
opacity: 0;
}
- .govuk-file-upload__button {
+ .govuk-file-upload__pseudo-button {
width: auto;
margin-bottom: 0;
flex-grow: 0;
@@ -98,4 +98,51 @@
}
}
+.govuk-file-upload__button:focus {
+ outline: none;
+}
+
+.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__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;
+}
+
+.govuk-file-upload__button:active .govuk-file-upload__pseudo-button:hover {
+ background-color: govuk-shade(govuk-colour("light-grey"), 20%);
+}
+
+.govuk-file-upload__button {
+ align-items: center;
+ display: flex;
+ padding: 0;
+ border: 0;
+ background-color: transparent;
+}
+
+.govuk-file-upload:disabled + .govuk-file-upload__button {
+ pointer-events: none;
+}
+
+.govuk-file-upload:disabled + .govuk-file-upload__button .govuk-file-upload__pseudo-button {
+ opacity: (0.5);
+
+ &:hover {
+ background-color: govuk-colour("light-grey");
+ cursor: not-allowed;
+ }
+
+ &:active {
+ top: 0;
+ box-shadow: 0 $govuk-border-width-form-element 0 govuk-shade(govuk-colour("white"), 60%); // s0
+ }
+}
+
/*# sourceMappingURL=_index.scss.map */
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 11a85cff2..905b3c3e4 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
@@ -498,32 +498,46 @@
this.$button = void 0;
this.$status = void 0;
this.i18n = void 0;
+ this.id = void 0;
if (this.$root.type !== 'file') {
throw new ElementError(formatErrorMessage(FileUpload, 'Form field must be an input of type `file`.'));
}
+ if (!this.$root.id.length) {
+ throw new ElementError(formatErrorMessage(FileUpload, 'Form field must specify an `id`.'));
+ }
+ this.id = this.$root.id;
this.i18n = new I18n(this.config.i18n, {
locale: closestAttributeValue(this.$root, 'lang')
});
this.$label = this.findLabel();
+ this.$root.id = `${this.id}-input`;
const $wrapper = document.createElement('div');
$wrapper.className = 'govuk-file-upload-wrapper';
const $button = document.createElement('button');
- $button.className = 'govuk-button govuk-button--secondary govuk-file-upload__button';
+ $button.classList.add('govuk-file-upload__button');
$button.type = 'button';
- $button.innerText = this.i18n.t('selectFilesButton');
+ $button.id = this.id;
+ 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('role', 'status');
+ $status.setAttribute('aria-hidden', 'true');
+ $button.appendChild($status);
+ $button.setAttribute('aria-label', `${this.$label.innerText}, ${this.i18n.t('selectFilesButton')}, ${this.i18n.t('filesSelectedDefault')}`);
$wrapper.insertAdjacentElement('beforeend', $button);
- $wrapper.insertAdjacentElement('beforeend', $status);
this.$root.insertAdjacentElement('afterend', $wrapper);
+ this.$root.setAttribute('tabindex', '-1');
+ this.$root.setAttribute('aria-hidden', 'true');
$wrapper.insertAdjacentElement('afterbegin', this.$root);
this.$wrapper = $wrapper;
this.$button = $button;
this.$status = $status;
- this.$root.setAttribute('tabindex', '-1');
+ this.$root.addEventListener('change', this.onChange.bind(this));
this.updateDisabledState();
this.observeDisabledState();
this.$root.addEventListener('change', this.onChange.bind(this));
@@ -581,6 +595,7 @@
count: fileCount
});
}
+ this.$button.setAttribute('aria-label', `${this.$label.innerText}, ${this.i18n.t('selectFilesButton')}, ${this.$status.innerText}`);
}
findLabel() {
const $label = document.querySelector(`label[for="${this.$root.id}"]`);
@@ -593,7 +608,7 @@
return $label;
}
onClick() {
- this.$label.click();
+ this.$root.click();
}
observeDisabledState() {
const observer = new MutationObserver(mutationList => {
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 b21914e73..72724da01 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
@@ -492,32 +492,46 @@ class FileUpload extends ConfigurableComponent {
this.$button = void 0;
this.$status = void 0;
this.i18n = void 0;
+ this.id = void 0;
if (this.$root.type !== 'file') {
throw new ElementError(formatErrorMessage(FileUpload, 'Form field must be an input of type `file`.'));
}
+ if (!this.$root.id.length) {
+ throw new ElementError(formatErrorMessage(FileUpload, 'Form field must specify an `id`.'));
+ }
+ this.id = this.$root.id;
this.i18n = new I18n(this.config.i18n, {
locale: closestAttributeValue(this.$root, 'lang')
});
this.$label = this.findLabel();
+ this.$root.id = `${this.id}-input`;
const $wrapper = document.createElement('div');
$wrapper.className = 'govuk-file-upload-wrapper';
const $button = document.createElement('button');
- $button.className = 'govuk-button govuk-button--secondary govuk-file-upload__button';
+ $button.classList.add('govuk-file-upload__button');
$button.type = 'button';
- $button.innerText = this.i18n.t('selectFilesButton');
+ $button.id = this.id;
+ 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('role', 'status');
+ $status.setAttribute('aria-hidden', 'true');
+ $button.appendChild($status);
+ $button.setAttribute('aria-label', `${this.$label.innerText}, ${this.i18n.t('selectFilesButton')}, ${this.i18n.t('filesSelectedDefault')}`);
$wrapper.insertAdjacentElement('beforeend', $button);
- $wrapper.insertAdjacentElement('beforeend', $status);
this.$root.insertAdjacentElement('afterend', $wrapper);
+ this.$root.setAttribute('tabindex', '-1');
+ this.$root.setAttribute('aria-hidden', 'true');
$wrapper.insertAdjacentElement('afterbegin', this.$root);
this.$wrapper = $wrapper;
this.$button = $button;
this.$status = $status;
- this.$root.setAttribute('tabindex', '-1');
+ this.$root.addEventListener('change', this.onChange.bind(this));
this.updateDisabledState();
this.observeDisabledState();
this.$root.addEventListener('change', this.onChange.bind(this));
@@ -575,6 +589,7 @@ class FileUpload extends ConfigurableComponent {
count: fileCount
});
}
+ this.$button.setAttribute('aria-label', `${this.$label.innerText}, ${this.i18n.t('selectFilesButton')}, ${this.$status.innerText}`);
}
findLabel() {
const $label = document.querySelector(`label[for="${this.$root.id}"]`);
@@ -587,7 +602,7 @@ class FileUpload extends ConfigurableComponent {
return $label;
}
onClick() {
- this.$label.click();
+ this.$root.click();
}
observeDisabledState() {
const observer = new MutationObserver(mutationList => {
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 c859f2ebd..3eeaf0c49 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
@@ -21,32 +21,46 @@ class FileUpload extends ConfigurableComponent {
this.$button = void 0;
this.$status = void 0;
this.i18n = void 0;
+ this.id = void 0;
if (this.$root.type !== 'file') {
throw new ElementError(formatErrorMessage(FileUpload, 'Form field must be an input of type `file`.'));
}
+ if (!this.$root.id.length) {
+ throw new ElementError(formatErrorMessage(FileUpload, 'Form field must specify an `id`.'));
+ }
+ this.id = this.$root.id;
this.i18n = new I18n(this.config.i18n, {
locale: closestAttributeValue(this.$root, 'lang')
});
this.$label = this.findLabel();
+ this.$root.id = `${this.id}-input`;
const $wrapper = document.createElement('div');
$wrapper.className = 'govuk-file-upload-wrapper';
const $button = document.createElement('button');
- $button.className = 'govuk-button govuk-button--secondary govuk-file-upload__button';
+ $button.classList.add('govuk-file-upload__button');
$button.type = 'button';
- $button.innerText = this.i18n.t('selectFilesButton');
+ $button.id = this.id;
+ 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('role', 'status');
+ $status.setAttribute('aria-hidden', 'true');
+ $button.appendChild($status);
+ $button.setAttribute('aria-label', `${this.$label.innerText}, ${this.i18n.t('selectFilesButton')}, ${this.i18n.t('filesSelectedDefault')}`);
$wrapper.insertAdjacentElement('beforeend', $button);
- $wrapper.insertAdjacentElement('beforeend', $status);
this.$root.insertAdjacentElement('afterend', $wrapper);
+ this.$root.setAttribute('tabindex', '-1');
+ this.$root.setAttribute('aria-hidden', 'true');
$wrapper.insertAdjacentElement('afterbegin', this.$root);
this.$wrapper = $wrapper;
this.$button = $button;
this.$status = $status;
- this.$root.setAttribute('tabindex', '-1');
+ this.$root.addEventListener('change', this.onChange.bind(this));
this.updateDisabledState();
this.observeDisabledState();
this.$root.addEventListener('change', this.onChange.bind(this));
@@ -104,6 +118,7 @@ class FileUpload extends ConfigurableComponent {
count: fileCount
});
}
+ this.$button.setAttribute('aria-label', `${this.$label.innerText}, ${this.i18n.t('selectFilesButton')}, ${this.$status.innerText}`);
}
findLabel() {
const $label = document.querySelector(`label[for="${this.$root.id}"]`);
@@ -116,7 +131,7 @@ class FileUpload extends ConfigurableComponent {
return $label;
}
onClick() {
- this.$label.click();
+ this.$root.click();
}
observeDisabledState() {
const observer = new MutationObserver(mutationList => {
Action run for 5d7fb7f |
086e37a
to
cf6cfad
Compare
I've done some testing with assistive technologies. Details are in the testing spreadsheet (in the '16 Jan 2025' tab).
I suspect the only items from that list that are potentially gnarly are the two screen reader issues. We should definitely try to fix them for a bit. But the VO macOS / Safari issue has the lowest priority because of the pre-existing issue with the native input. It's worth trying to isolate the issue with NVDA to see where the issue comes from. It's possible that it's a bug in Firefox or NVDA. |
Addressed a few comments:
|
8b5dd7b
to
e8a02fe
Compare
e8a02fe
to
03752ec
Compare
0df14ce
to
28b132f
Compare
97ad4a9
to
331ca85
Compare
New visually hidden field for reading the contents of the button, I'm hoping this will fix the screen reader issues. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Almost there I think, that's neat! 🙌🏻
Spotted an issue with Voice Over and Safari due to the setting of the ID on the <button>
(good spot that we'd need to do that for the Error Summary links), but nothing unfixable, I think.
@@ -62,7 +64,7 @@ describe('/components/file-upload', () => { | |||
|
|||
it('moves the file input inside of the wrapper element', async () => { | |||
const inputElementParent = await page.$eval( | |||
inputSelector, | |||
buttonSelector, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue Looks like this test was meant to check that the input
was still inside the wrapper
(and not removed from the DOM altogether). This is really so that the drag'n'drop works by dropping on the input itself, however, I'm not sure how much I'd trust Puppeteer's drag'n'drop tests to detect that we have removed the <input>
by mistake, so I'd be keen to keep this test.
buttonSelector, | |
inputSelector, |
@@ -93,35 +95,25 @@ describe('/components/file-upload', () => { | |||
}) | |||
|
|||
it('renders the button with default text', async () => { | |||
const buttonElementText = await page.$eval(buttonSelector, (el) => | |||
el.innerHTML.trim() | |||
const buttonElementText = await page.$eval( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion Would it be worth checking that not only the elements this test looks for are on the page, but also inside the button itself?
packages/govuk-frontend/src/govuk/components/file-upload/file-upload.puppeteer.test.js
Show resolved
Hide resolved
packages/govuk-frontend/src/govuk/components/file-upload/file-upload.mjs
Show resolved
Hide resolved
331ca85
to
28b0b0f
Compare
28b0b0f
to
0cad9f0
Compare
0cad9f0
to
7f16b2c
Compare
Button replaces entire native file upload. This replacement has a within it a "psuedo button" span that has the same focus, hover and active behaviour as a secondary button.
7f16b2c
to
4cd0ca3
Compare
Hide the button text entirely from screen reader and add button content as `aria-label`. This should lead to more consistent reading out of the button content.
4cd0ca3
to
94a9f5c
Compare
- Tests now use the correct selector for when the input has been visually replaced by the button. - Added tests for button `aria-label`. - Added test for clicking on different elements within button
Adjustments made to the the file upload so that in the event of an error the correct part of the page can be linked to.
94a9f5c
to
5d7fb7f
Compare
I haven't done a full test yet, but just some findings from a quick initial test:
|
Because we're using The good news is that it translate even the text in attributes (so, works with So, #5640 works the most consistently, although the others work well enough. |
Another thing to consider is users who use userstyles. That's a very small minority of users. While we don't necessarily need to ensure the page looks good with different CSS (or no CSS), it should at least be functional. For those users the text on the button reads:
I guess it's mostly still functional for all three, but getting weird. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ready to merge on the spike branch, the only remark I had likely affects the branch for the styling so we can update there 😊
.govuk-file-upload:disabled + .govuk-file-upload__button { | ||
pointer-events: none; | ||
} | ||
|
||
.govuk-file-upload:disabled + .govuk-file-upload__button .govuk-file-upload__pseudo-button { | ||
opacity: (0.5); | ||
|
||
&:hover { | ||
background-color: govuk-colour("light-grey"); | ||
cursor: not-allowed; | ||
} | ||
|
||
&:active { | ||
top: 0; | ||
box-shadow: 0 $govuk-border-width-form-element 0 govuk-shade(govuk-colour("white"), 60%); // s0 | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@patrickpatrickpatrick One for the PR about the styles, but given we set the disabled
attribute on the <button>
, we should directly target the <button>
rather than use the state of the <input>
.
1a2852e
into
spike-enhanced-file-upload
What
Instead of replacing the native input with a collection of elements, replace the native input with a button that has the replacement encapsulated within it. Involves making a "pseudo-button" that has the same styling as a regular button.
display: none
andtabindex = '-1'
.aria-label
with a value of the button contentsid
of elements to ensure error summary focus will still workWhy
Fixes #5612