Open querkmachine opened 2 months ago
File | Size |
---|---|
dist/govuk-frontend-development.min.css | 119.03 KiB |
dist/govuk-frontend-development.min.js | 45.82 KiB |
packages/govuk-frontend/dist/govuk/all.bundle.js | 97 KiB |
packages/govuk-frontend/dist/govuk/all.bundle.mjs | 91.1 KiB |
packages/govuk-frontend/dist/govuk/all.mjs | 1.25 KiB |
packages/govuk-frontend/dist/govuk/govuk-frontend-component.mjs | 1.74 KiB |
packages/govuk-frontend/dist/govuk/govuk-frontend.min.css | 119.02 KiB |
packages/govuk-frontend/dist/govuk/govuk-frontend.min.js | 45.81 KiB |
packages/govuk-frontend/dist/govuk/i18n.mjs | 5.55 KiB |
packages/govuk-frontend/dist/govuk/init.mjs | 7.22 KiB |
File | Size (bundled) | Size (minified) |
---|---|---|
all.mjs | 86.95 KiB | 43.11 KiB |
accordion.mjs | 25.46 KiB | 12.93 KiB |
button.mjs | 7.96 KiB | 3.31 KiB |
character-count.mjs | 24.39 KiB | 10.5 KiB |
checkboxes.mjs | 7.81 KiB | 3.42 KiB |
error-summary.mjs | 9.87 KiB | 4.07 KiB |
exit-this-page.mjs | 19.08 KiB | 9.86 KiB |
file-upload.mjs | 17 KiB | 8.19 KiB |
header.mjs | 6.46 KiB | 3.22 KiB |
notification-banner.mjs | 8.24 KiB | 3.23 KiB |
password-input.mjs | 17.13 KiB | 7.86 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 980b1c6fbc1bc170d03614183e989902a07b9a4f
diff --git a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
index 0416a210b..1769da29b 100644
--- a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
+++ b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
@@ -1,44 +1,44 @@
const version = "development";
function normaliseString(t, e) {
- const s = t ? t.trim() : "";
- let n, i = null == e ? void 0 : e.type;
- switch (i || (["true", "false"].includes(s) && (i = "boolean"), s.length > 0 && isFinite(Number(s)) && (i = "number")), i) {
+ const i = t ? t.trim() : "";
+ let s, n = null == e ? void 0 : e.type;
+ switch (n || (["true", "false"].includes(i) && (n = "boolean"), i.length > 0 && isFinite(Number(i)) && (n = "number")), n) {
case "boolean":
- n = "true" === s;
+ s = "true" === i;
break;
case "number":
- n = Number(s);
+ s = Number(i);
break;
default:
- n = t
+ s = t
}
- return n
+ return s
}
function mergeConfigs(...t) {
const e = {};
- for (const s of t)
- for (const t of Object.keys(s)) {
- const n = e[t],
- i = s[t];
- isObject(n) && isObject(i) ? e[t] = mergeConfigs(n, i) : e[t] = i
+ for (const i of t)
+ for (const t of Object.keys(i)) {
+ const s = e[t],
+ n = i[t];
+ isObject(s) && isObject(n) ? e[t] = mergeConfigs(s, n) : e[t] = n
}
return e
}
function extractConfigByNamespace(Component, t, e) {
- const s = Component.schema.properties[e];
- if ("object" !== (null == s ? void 0 : s.type)) return;
- const n = {
+ const i = Component.schema.properties[e];
+ if ("object" !== (null == i ? void 0 : i.type)) return;
+ const s = {
[e]: {}
};
- for (const [i, o] of Object.entries(t)) {
- let t = n;
- const s = i.split(".");
- for (const [n, r] of s.entries()) "object" == typeof t && (n < s.length - 1 ? (isObject(t[r]) || (t[r] = {}), t = t[r]) : i !== e && (t[r] = normaliseString(o)))
+ for (const [n, o] of Object.entries(t)) {
+ let t = s;
+ const i = n.split(".");
+ for (const [s, r] of i.entries()) "object" == typeof t && (s < i.length - 1 ? (isObject(t[r]) || (t[r] = {}), t = t[r]) : n !== e && (t[r] = normaliseString(o)))
}
- return n[e]
+ return s[e]
}
function getFragmentFromUrl(t) {
@@ -54,20 +54,20 @@ function getBreakpoint(t) {
}
function setFocus(t, e = {}) {
- var s;
- const n = t.getAttribute("tabindex");
+ var i;
+ const s = t.getAttribute("tabindex");
function onBlur() {
- var s;
- null == (s = e.onBlur) || s.call(t), n || t.removeAttribute("tabindex")
+ var i;
+ null == (i = e.onBlur) || i.call(t), s || t.removeAttribute("tabindex")
}
- n || t.setAttribute("tabindex", "-1"), t.addEventListener("focus", (function() {
+ s || t.setAttribute("tabindex", "-1"), t.addEventListener("focus", (function() {
t.addEventListener("blur", onBlur, {
once: !0
})
}), {
once: !0
- }), null == (s = e.onBeforeFocus) || s.call(t), t.focus()
+ }), null == (i = e.onBeforeFocus) || i.call(t), t.focus()
}
function isSupported(t = document.body) {
@@ -86,7 +86,7 @@ function formatErrorMessage(Component, t) {
function normaliseDataset(Component, t) {
const e = {};
- for (const [s, n] of Object.entries(Component.schema.properties)) s in t && (e[s] = normaliseString(t[s], n)), "object" === (null == n ? void 0 : n.type) && (e[s] = extractConfigByNamespace(Component, t, s));
+ for (const [i, s] of Object.entries(Component.schema.properties)) i in t && (e[i] = normaliseString(t[i], s)), "object" === (null == s ? void 0 : s.type) && (e[i] = extractConfigByNamespace(Component, t, i));
return e
}
class GOVUKFrontendError extends Error {
@@ -110,12 +110,12 @@ class ElementError extends GOVUKFrontendError {
let e = "string" == typeof t ? t : "";
if ("object" == typeof t) {
const {
- component: s,
- identifier: n,
- element: i,
+ component: i,
+ identifier: s,
+ element: n,
expectedType: o
} = t;
- e = n, e += i ? ` is not of type ${null!=o?o:"HTMLElement"}` : " not found", e = formatErrorMessage(s, e)
+ e = s, e += n ? ` is not of type ${null!=o?o:"HTMLElement"}` : " not found", e = formatErrorMessage(i, e)
}
super(e), this.name = "ElementError"
}
@@ -140,8 +140,8 @@ class GOVUKFrontendComponent {
expectedType: e.elementType.name
});
this._$root = t, e.checkSupport(), this.checkInitialised();
- const s = e.moduleName;
- this.$root.setAttribute(`data-${s}-init`, "")
+ const i = e.moduleName;
+ this.$root.setAttribute(`data-${i}-init`, "")
}
checkInitialised() {
const t = this.constructor,
@@ -157,31 +157,31 @@ class GOVUKFrontendComponent {
GOVUKFrontendComponent.elementType = HTMLElement;
class I18n {
constructor(t = {}, e = {}) {
- var s;
- this.translations = void 0, this.locale = void 0, this.translations = t, this.locale = null != (s = e.locale) ? s : document.documentElement.lang || "en"
+ var i;
+ this.translations = void 0, this.locale = void 0, this.translations = t, this.locale = null != (i = e.locale) ? i : document.documentElement.lang || "en"
}
t(t, e) {
if (!t) throw new Error("i18n: lookup key missing");
- let s = this.translations[t];
- if ("number" == typeof(null == e ? void 0 : e.count) && "object" == typeof s) {
- const n = s[this.getPluralSuffix(t, e.count)];
- n && (s = n)
+ let i = this.translations[t];
+ if ("number" == typeof(null == e ? void 0 : e.count) && "object" == typeof i) {
+ const s = i[this.getPluralSuffix(t, e.count)];
+ s && (i = s)
}
- if ("string" == typeof s) {
- if (s.match(/%{(.\S+)}/)) {
+ if ("string" == typeof i) {
+ if (i.match(/%{(.\S+)}/)) {
if (!e) throw new Error("i18n: cannot replace placeholders in string if no option data provided");
- return this.replacePlaceholders(s, e)
+ return this.replacePlaceholders(i, e)
}
- return s
+ return i
}
return t
}
replacePlaceholders(t, e) {
- const s = Intl.NumberFormat.supportedLocalesOf(this.locale).length ? new Intl.NumberFormat(this.locale) : void 0;
- return t.replace(/%{(.\S+)}/g, (function(t, n) {
- if (Object.prototype.hasOwnProperty.call(e, n)) {
- const t = e[n];
- return !1 === t || "number" != typeof t && "string" != typeof t ? "" : "number" == typeof t ? s ? s.format(t) : `${t}` : t
+ const i = 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 !1 === t || "number" != typeof t && "string" != typeof t ? "" : "number" == typeof t ? i ? i.format(t) : `${t}` : t
}
throw new Error(`i18n: no data found to replace ${t} placeholder in string`)
}))
@@ -191,11 +191,11 @@ class I18n {
}
getPluralSuffix(t, e) {
if (e = Number(e), !isFinite(e)) return "other";
- const s = this.translations[t],
- n = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(e) : this.selectPluralFormUsingFallbackRules(e);
- if ("object" == typeof s) {
- if (n in s) return n;
- if ("other" in s) return console.warn(`i18n: Missing plural form ".${n}" for "${this.locale}" locale. Falling back to ".other".`), "other"
+ const i = this.translations[t],
+ s = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(e) : this.selectPluralFormUsingFallbackRules(e);
+ if ("object" == typeof i) {
+ if (s in i) return s;
+ if ("other" in i) return console.warn(`i18n: Missing plural form ".${s}" for "${this.locale}" locale. Falling back to ".other".`), "other"
}
throw new Error(`i18n: Plural form ".other" is required for "${this.locale}" locale`)
}
@@ -207,8 +207,8 @@ class I18n {
getPluralRulesForLocale() {
const t = this.locale.split("-")[0];
for (const e in I18n.pluralRulesMap) {
- const s = I18n.pluralRulesMap[e];
- if (s.includes(this.locale) || s.includes(t)) return e
+ const i = I18n.pluralRulesMap[e];
+ if (i.includes(this.locale) || i.includes(t)) return e
}
}
}
@@ -230,8 +230,8 @@ I18n.pluralRulesMap = {
irish: t => 1 === t ? "one" : 2 === t ? "two" : t >= 3 && t <= 6 ? "few" : t >= 7 && t <= 10 ? "many" : "other",
russian(t) {
const e = t % 100,
- s = e % 10;
- return 1 === s && 11 !== e ? "one" : s >= 2 && s <= 4 && !(e >= 12 && e <= 14) ? "few" : 0 === s || s >= 5 && s <= 9 || e >= 11 && e <= 14 ? "many" : "other"
+ i = e % 10;
+ return 1 === i && 11 !== e ? "one" : i >= 2 && i <= 4 && !(e >= 12 && e <= 14) ? "few" : 0 === i || i >= 5 && i <= 9 || e >= 11 && e <= 14 ? "many" : "other"
},
scottish: t => 1 === t || 11 === t ? "one" : 2 === t || 12 === t ? "two" : t >= 3 && t <= 10 || t >= 13 && t <= 19 ? "few" : "other",
spanish: t => 1 === t ? "one" : t % 1e6 == 0 && 0 !== t ? "many" : "other",
@@ -240,12 +240,12 @@ I18n.pluralRulesMap = {
class Accordion extends GOVUKFrontendComponent {
constructor(t, e = {}) {
super(t), this.config = void 0, this.i18n = void 0, this.controlsClass = "govuk-accordion__controls", this.showAllClass = "govuk-accordion__show-all", this.showAllTextClass = "govuk-accordion__show-all-text", this.sectionClass = "govuk-accordion__section", this.sectionExpandedClass = "govuk-accordion__section--expanded", this.sectionButtonClass = "govuk-accordion__section-button", this.sectionHeaderClass = "govuk-accordion__section-header", this.sectionHeadingClass = "govuk-accordion__section-heading", this.sectionHeadingDividerClass = "govuk-accordion__section-heading-divider", this.sectionHeadingTextClass = "govuk-accordion__section-heading-text", this.sectionHeadingTextFocusClass = "govuk-accordion__section-heading-text-focus", this.sectionShowHideToggleClass = "govuk-accordion__section-toggle", this.sectionShowHideToggleFocusClass = "govuk-accordion__section-toggle-focus", this.sectionShowHideTextClass = "govuk-accordion__section-toggle-text", this.upChevronIconClass = "govuk-accordion-nav__chevron", this.downChevronIconClass = "govuk-accordion-nav__chevron--down", this.sectionSummaryClass = "govuk-accordion__section-summary", this.sectionSummaryFocusClass = "govuk-accordion__section-summary-focus", this.sectionContentClass = "govuk-accordion__section-content", this.$sections = void 0, this.$showAllButton = null, this.$showAllIcon = null, this.$showAllText = null, this.config = mergeConfigs(Accordion.defaults, e, normaliseDataset(Accordion, this.$root.dataset)), this.i18n = new I18n(this.config.i18n);
- const s = this.$root.querySelectorAll(`.${this.sectionClass}`);
- if (!s.length) throw new ElementError({
+ const i = this.$root.querySelectorAll(`.${this.sectionClass}`);
+ if (!i.length) throw new ElementError({
component: Accordion,
identifier: `Sections (\`<div class="${this.sectionClass}">\`)`
});
- this.$sections = s, this.initControls(), this.initSectionHeaders(), this.updateShowAllButton(this.areAllSectionsOpen())
+ this.$sections = i, this.initControls(), this.initSectionHeaders(), this.updateShowAllButton(this.areAllSectionsOpen())
}
initControls() {
this.$showAllButton = document.createElement("button"), this.$showAllButton.setAttribute("type", "button"), this.$showAllButton.setAttribute("class", this.showAllClass), this.$showAllButton.setAttribute("aria-expanded", "false"), this.$showAllIcon = document.createElement("span"), this.$showAllIcon.classList.add(this.upChevronIconClass), this.$showAllButton.appendChild(this.$showAllIcon);
@@ -254,53 +254,53 @@ class Accordion extends GOVUKFrontendComponent {
}
initSectionHeaders() {
this.$sections.forEach(((t, e) => {
- const s = t.querySelector(`.${this.sectionHeaderClass}`);
- if (!s) throw new ElementError({
+ const i = t.querySelector(`.${this.sectionHeaderClass}`);
+ if (!i) throw new ElementError({
component: Accordion,
identifier: `Section headers (\`<div class="${this.sectionHeaderClass}">\`)`
});
- this.constructHeaderMarkup(s, e), this.setExpanded(this.isExpanded(t), t), s.addEventListener("click", (() => this.onSectionToggle(t))), this.setInitialState(t)
+ this.constructHeaderMarkup(i, e), this.setExpanded(this.isExpanded(t), t), i.addEventListener("click", (() => this.onSectionToggle(t))), this.setInitialState(t)
}))
}
constructHeaderMarkup(t, e) {
- const s = t.querySelector(`.${this.sectionButtonClass}`),
- n = t.querySelector(`.${this.sectionHeadingClass}`),
- i = t.querySelector(`.${this.sectionSummaryClass}`);
- if (!n) throw new ElementError({
+ const i = t.querySelector(`.${this.sectionButtonClass}`),
+ s = t.querySelector(`.${this.sectionHeadingClass}`),
+ n = t.querySelector(`.${this.sectionSummaryClass}`);
+ if (!s) throw new ElementError({
component: Accordion,
identifier: `Section heading (\`.${this.sectionHeadingClass}\`)`
});
- if (!s) throw new ElementError({
+ if (!i) throw new ElementError({
component: Accordion,
identifier: `Section button placeholder (\`<span class="${this.sectionButtonClass}">\`)`
});
const o = document.createElement("button");
o.setAttribute("type", "button"), o.setAttribute("aria-controls", `${this.$root.id}-content-${e+1}`);
- for (const d of Array.from(s.attributes)) "id" !== d.name && o.setAttribute(d.name, d.value);
+ for (const d of Array.from(i.attributes)) "id" !== d.name && o.setAttribute(d.name, d.value);
const r = document.createElement("span");
- r.classList.add(this.sectionHeadingTextClass), r.id = s.id;
+ r.classList.add(this.sectionHeadingTextClass), r.id = i.id;
const a = document.createElement("span");
- a.classList.add(this.sectionHeadingTextFocusClass), r.appendChild(a), Array.from(s.childNodes).forEach((t => a.appendChild(t)));
- const c = document.createElement("span");
- c.classList.add(this.sectionShowHideToggleClass), c.setAttribute("data-nosnippet", "");
+ a.classList.add(this.sectionHeadingTextFocusClass), r.appendChild(a), Array.from(i.childNodes).forEach((t => a.appendChild(t)));
const l = document.createElement("span");
- l.classList.add(this.sectionShowHideToggleFocusClass), c.appendChild(l);
+ l.classList.add(this.sectionShowHideToggleClass), l.setAttribute("data-nosnippet", "");
+ const c = document.createElement("span");
+ c.classList.add(this.sectionShowHideToggleFocusClass), l.appendChild(c);
const h = document.createElement("span"),
u = document.createElement("span");
- if (u.classList.add(this.upChevronIconClass), l.appendChild(u), h.classList.add(this.sectionShowHideTextClass), l.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()), n) {
const t = document.createElement("span"),
e = document.createElement("span");
e.classList.add(this.sectionSummaryFocusClass), t.appendChild(e);
- for (const s of Array.from(i.attributes)) t.setAttribute(s.name, s.value);
- Array.from(i.childNodes).forEach((t => e.appendChild(t))), i.remove(), o.appendChild(t), o.appendChild(this.getButtonPunctuationEl())
+ for (const i of Array.from(n.attributes)) t.setAttribute(i.name, i.value);
+ Array.from(n.childNodes).forEach((t => e.appendChild(t))), n.remove(), o.appendChild(t), o.appendChild(this.getButtonPunctuationEl())
}
- o.appendChild(c), n.removeChild(s), n.appendChild(o)
+ o.appendChild(l), s.removeChild(i), s.appendChild(o)
}
onBeforeMatch(t) {
const e = t.target;
if (!(e instanceof Element)) return;
- const s = e.closest(`.${this.sectionClass}`);
- s && this.setExpanded(!0, s)
+ const i = e.closest(`.${this.sectionClass}`);
+ i && this.setExpanded(!0, i)
}
onSectionToggle(t) {
const e = !this.isExpanded(t);
@@ -313,24 +313,24 @@ class Accordion extends GOVUKFrontendComponent {
})), this.updateShowAllButton(t)
}
setExpanded(t, e) {
- const s = e.querySelector(`.${this.upChevronIconClass}`),
- n = e.querySelector(`.${this.sectionShowHideTextClass}`),
- i = e.querySelector(`.${this.sectionButtonClass}`),
+ const i = e.querySelector(`.${this.upChevronIconClass}`),
+ s = e.querySelector(`.${this.sectionShowHideTextClass}`),
+ n = e.querySelector(`.${this.sectionButtonClass}`),
o = e.querySelector(`.${this.sectionContentClass}`);
if (!o) throw new ElementError({
component: Accordion,
identifier: `Section content (\`<div class="${this.sectionContentClass}">\`)`
});
- if (!s || !n || !i) return;
+ if (!i || !s || !n) return;
const r = t ? this.i18n.t("hideSection") : this.i18n.t("showSection");
- n.textContent = r, i.setAttribute("aria-expanded", `${t}`);
+ s.textContent = r, n.setAttribute("aria-expanded", `${t}`);
const a = [],
- c = e.querySelector(`.${this.sectionHeadingTextClass}`);
- c && a.push(`${c.textContent}`.trim());
- const l = e.querySelector(`.${this.sectionSummaryClass}`);
+ 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), s.classList.remove(this.downChevronIconClass)) : (o.setAttribute("hidden", "until-found"), e.classList.remove(this.sectionExpandedClass), s.classList.add(this.downChevronIconClass)), this.updateShowAllButton(this.areAllSectionsOpen())
+ a.push(h), n.setAttribute("aria-label", a.join(" , ")), t ? (o.removeAttribute("hidden"), e.classList.add(this.sectionExpandedClass), i.classList.remove(this.downChevronIconClass)) : (o.setAttribute("hidden", "until-found"), e.classList.remove(this.sectionExpandedClass), i.classList.add(this.downChevronIconClass)), this.updateShowAllButton(this.areAllSectionsOpen())
}
isExpanded(t) {
return t.classList.contains(this.sectionExpandedClass)
@@ -347,18 +347,18 @@ class Accordion extends GOVUKFrontendComponent {
}
storeState(t, e) {
if (!this.config.rememberExpanded) return;
- const s = this.getIdentifier(t);
- if (s) try {
- window.sessionStorage.setItem(s, e.toString())
- } catch (n) {}
+ const i = this.getIdentifier(t);
+ if (i) try {
+ window.sessionStorage.setItem(i, e.toString())
+ } catch (s) {}
}
setInitialState(t) {
if (!this.config.rememberExpanded) return;
const e = this.getIdentifier(t);
if (e) try {
- const s = window.sessionStorage.getItem(e);
- null !== s && this.setExpanded("true" === s, t)
- } catch (s) {}
+ const i = window.sessionStorage.getItem(e);
+ null !== i && this.setExpanded("true" === i, t)
+ } catch (i) {}
}
getButtonPunctuationEl() {
const t = document.createElement("span");
@@ -401,8 +401,8 @@ class Button extends GOVUKFrontendComponent {
}
function closestAttributeValue(t, e) {
- const s = t.closest(`[${e}]`);
- return s ? s.getAttribute(e) : null
+ const i = t.closest(`[${e}]`);
+ return i ? i.getAttribute(e) : null
}
Button.moduleName = "govuk-button", Button.defaults = Object.freeze({
preventDoubleClick: !1
@@ -415,12 +415,12 @@ Button.moduleName = "govuk-button", Button.defaults = Object.freeze({
});
class CharacterCount extends GOVUKFrontendComponent {
constructor(t, e = {}) {
- var s, n;
+ var i, s;
super(t), this.$textarea = void 0, this.$visibleCountMessage = void 0, this.$screenReaderCountMessage = void 0, this.lastInputTimestamp = null, this.lastInputValue = "", this.valueChecker = null, this.config = void 0, 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 n = this.$root.querySelector(".govuk-js-character-count");
+ if (!(n instanceof HTMLTextAreaElement || n instanceof HTMLInputElement)) throw new ElementError({
component: CharacterCount,
- element: i,
+ element: n,
expectedType: "HTMLTextareaElement or HTMLInputElement",
identifier: "Form field (`.govuk-js-character-count`)"
});
@@ -431,38 +431,38 @@ class CharacterCount extends GOVUKFrontendComponent {
maxwords: void 0
}), this.config = mergeConfigs(CharacterCount.defaults, e, r, o);
const a = function(t, e) {
- const s = [];
- for (const [n, i] of Object.entries(t)) {
+ const i = [];
+ for (const [s, n] of Object.entries(t)) {
const t = [];
- if (Array.isArray(i)) {
+ if (Array.isArray(n)) {
for (const {
- required: s,
- errorMessage: n
+ required: i,
+ errorMessage: s
}
- of i) s.every((t => !!e[t])) || t.push(n);
- "anyOf" !== n || i.length - t.length >= 1 || s.push(...t)
+ of n) i.every((t => !!e[t])) || t.push(s);
+ "anyOf" !== s || n.length - t.length >= 1 || i.push(...t)
}
}
- return s
+ return i
}(CharacterCount.schema, this.config);
if (a[0]) throw new ConfigError(formatErrorMessage(CharacterCount, a[0]));
this.i18n = new I18n(this.config.i18n, {
locale: closestAttributeValue(this.$root, "lang")
- }), this.maxLength = null != (s = null != (n = this.config.maxwords) ? n : this.config.maxlength) ? s : 1 / 0, this.$textarea = i;
- const c = `${this.$textarea.id}-info`,
- l = document.getElementById(c);
- if (!l) throw new ElementError({
+ }), this.maxLength = null != (i = null != (s = this.config.maxwords) ? s : this.config.maxlength) ? i : 1 / 0, this.$textarea = n;
+ const l = `${this.$textarea.id}-info`,
+ c = document.getElementById(l);
+ if (!c) throw new ElementError({
component: CharacterCount,
- element: l,
- identifier: `Count message (\`id="${c}"\`)`
+ element: c,
+ identifier: `Count message (\`id="${l}"\`)`
});
- `${l.textContent}`.match(/^\s*$/) && (l.textContent = this.i18n.t("textareaDescription", {
+ `${c.textContent}`.match(/^\s*$/) && (c.textContent = this.i18n.t("textareaDescription", {
count: this.maxLength
- })), this.$textarea.insertAdjacentElement("afterend", l);
+ })), this.$textarea.insertAdjacentElement("afterend", c);
const h = document.createElement("div");
- h.className = "govuk-character-count__sr-status govuk-visually-hidden", h.setAttribute("aria-live", "polite"), this.$screenReaderCountMessage = h, l.insertAdjacentElement("afterend", h);
+ h.className = "govuk-character-count__sr-status govuk-visually-hidden", h.setAttribute("aria-live", "polite"), this.$screenReaderCountMessage = h, c.insertAdjacentElement("afterend", h);
const u = document.createElement("div");
- u.className = l.className, u.classList.add("govuk-character-count__status"), u.setAttribute("aria-hidden", "true"), this.$visibleCountMessage = u, l.insertAdjacentElement("afterend", u), l.classList.add("govuk-visually-hidden"), this.$textarea.removeAttribute("maxlength"), this.bindChangeEvents(), window.addEventListener("pageshow", (() => this.updateCountMessage())), this.updateCountMessage()
+ u.className = c.className, u.classList.add("govuk-character-count__status"), u.setAttribute("aria-hidden", "true"), this.$visibleCountMessage = u, c.insertAdjacentElement("afterend", u), c.classList.add("govuk-visually-hidden"), this.$textarea.removeAttribute("maxlength"), this.bindChangeEvents(), window.addEventListener("pageshow", (() => this.updateCountMessage())), this.updateCountMessage()
}
bindChangeEvents() {
this.$textarea.addEventListener("keyup", (() => this.handleKeyUp())), this.$textarea.addEventListener("focus", (() => this.handleFocus())), this.$textarea.addEventListener("blur", (() => this.handleBlur()))
@@ -505,8 +505,8 @@ class CharacterCount extends GOVUKFrontendComponent {
}
formatCountMessage(t, e) {
if (0 === t) return this.i18n.t(`${e}AtLimit`);
- const s = t < 0 ? "OverLimit" : "UnderLimit";
- return this.i18n.t(`${e}${s}`, {
+ const i = t < 0 ? "OverLimit" : "UnderLimit";
+ return this.i18n.t(`${e}${i}`, {
count: Math.abs(t)
})
}
@@ -589,10 +589,10 @@ class Checkboxes extends GOVUKFrontendComponent {
syncConditionalRevealWithInputState(t) {
const e = t.getAttribute("aria-controls");
if (!e) return;
- const s = document.getElementById(e);
- if (null != s && s.classList.contains("govuk-checkboxes__conditional")) {
+ const i = document.getElementById(e);
+ if (null != i && i.classList.contains("govuk-checkboxes__conditional")) {
const e = t.checked;
- t.setAttribute("aria-expanded", e.toString()), s.classList.toggle("govuk-checkboxes__conditional--hidden", !e)
+ t.setAttribute("aria-expanded", e.toString()), i.classList.toggle("govuk-checkboxes__conditional--hidden", !e)
}
}
unCheckAllInputsExcept(t) {
@@ -625,25 +625,25 @@ class ErrorSummary extends GOVUKFrontendComponent {
if (!(t instanceof HTMLAnchorElement)) return !1;
const e = getFragmentFromUrl(t.href);
if (!e) return !1;
- const s = document.getElementById(e);
- if (!s) return !1;
- const n = this.getAssociatedLegendOrLabel(s);
- return !!n && (n.scrollIntoView(), s.focus({
+ const i = document.getElementById(e);
+ if (!i) return !1;
+ const s = this.getAssociatedLegendOrLabel(i);
+ return !!s && (s.scrollIntoView(), i.focus({
preventScroll: !0
}), !0)
}
getAssociatedLegendOrLabel(t) {
var e;
- const s = t.closest("fieldset");
- if (s) {
- const e = s.getElementsByTagName("legend");
+ const i = t.closest("fieldset");
+ if (i) {
+ const e = i.getElementsByTagName("legend");
if (e.length) {
- const s = e[0];
- if (t instanceof HTMLInputElement && ("checkbox" === t.type || "radio" === t.type)) return s;
- const n = s.getBoundingClientRect().top,
- i = t.getBoundingClientRect();
- if (i.height && window.innerHeight) {
- if (i.top + i.height - n < window.innerHeight / 2) return s
+ const i = e[0];
+ if (t instanceof HTMLInputElement && ("checkbox" === t.type || "radio" === t.type)) return i;
+ const s = i.getBoundingClientRect().top,
+ n = t.getBoundingClientRect();
+ if (n.height && window.innerHeight) {
+ if (n.top + n.height - s < window.innerHeight / 2) return i
}
}
}
@@ -662,16 +662,16 @@ ErrorSummary.moduleName = "govuk-error-summary", ErrorSummary.defaults = Object.
class ExitThisPage extends GOVUKFrontendComponent {
constructor(t, e = {}) {
super(t), this.config = void 0, this.i18n = void 0, this.$button = void 0, this.$skiplinkButton = null, this.$updateSpan = null, this.$indicatorContainer = null, this.$overlay = null, this.keypressCounter = 0, this.lastKeyWasModified = !1, this.timeoutTime = 5e3, this.keypressTimeoutId = null, this.timeoutMessageId = null;
- const s = this.$root.querySelector(".govuk-exit-this-page__button");
- if (!(s instanceof HTMLAnchorElement)) throw new ElementError({
+ const i = this.$root.querySelector(".govuk-exit-this-page__button");
+ if (!(i instanceof HTMLAnchorElement)) throw new ElementError({
component: ExitThisPage,
- element: s,
+ element: i,
expectedType: "HTMLAnchorElement",
identifier: "Button (`.govuk-exit-this-page__button`)"
});
- this.config = mergeConfigs(ExitThisPage.defaults, e, normaliseDataset(ExitThisPage, this.$root.dataset)), this.i18n = new I18n(this.config.i18n), this.$button = s;
- const n = document.querySelector(".govuk-js-exit-this-page-skiplink");
- n instanceof HTMLAnchorElement && (this.$skiplinkButton = n), 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))
+ this.config = mergeConfigs(ExitThisPage.defaults, e, normaliseDataset(ExitThisPage, this.$root.dataset)), this.i18n = new I18n(this.config.i18n), this.$button = i;
+ 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))
}
initUpdateSpan() {
this.$updateSpan = document.createElement("span"), this.$updateSpan.setAttribute("role", "status"), this.$updateSpan.className = "govuk-visually-hidden", this.$root.appendChild(this.$updateSpan)
@@ -732,23 +732,84 @@ ExitThisPage.moduleName = "govuk-exit-this-page", ExitThisPage.defaults = Object
}
}
});
+class FileUpload extends GOVUKFrontendComponent {
+ constructor(t, e = {}) {
+ if (super(t), this.$wrapper = void 0, this.$button = void 0, this.$status = void 0, this.config = void 0, this.i18n = void 0, !(this.$root instanceof HTMLInputElement)) return;
+ if ("file" !== this.$root.type) throw new ElementError("File upload: Form field must be an input of type `file`.");
+ if (this.config = mergeConfigs(FileUpload.defaults, e, normaliseDataset(FileUpload, this.$root.dataset)), this.i18n = new I18n(this.config.i18n, {
+ locale: closestAttributeValue(this.$root, "lang")
+ }), this.$label = document.querySelector(`[for="${this.$root.id}"]`), !this.$label) throw new ElementError({
+ component: FileUpload,
+ identifier: "No label"
+ });
+ const i = document.createElement("div");
+ i.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 n = document.createElement("span");
+ n.className = "govuk-body govuk-file-upload__status", n.innerText = this.i18n.t("filesSelectedDefault"), n.setAttribute("role", "status"), i.insertAdjacentElement("beforeend", s), i.insertAdjacentElement("beforeend", n), this.$root.insertAdjacentElement("afterend", i), i.insertAdjacentElement("afterbegin", this.$root), this.$wrapper = i, this.$button = s, this.$status = n, this.$root.setAttribute("tabindex", "-1"), this.updateDisabledState(), this.observeDisabledState(), this.$root.addEventListener("change", this.onChange.bind(this)), this.$wrapper.addEventListener("drop", this.onDragLeaveOrDrop.bind(this)), document.addEventListener("dragenter", this.onDragEnter.bind(this)), document.addEventListener("dragleave", this.onDragLeaveOrDrop.bind(this))
+ }
+ onChange() {
+ if (!("files" in this.$root)) return;
+ if (!this.$root.files) return;
+ const t = this.$root.files.length;
+ this.$status && this.i18n && (this.$status.innerText = 0 === t ? this.i18n.t("filesSelectedDefault") : 1 === t ? this.$root.files[0].name : this.i18n.t("filesSelected", {
+ count: t
+ }))
+ }
+ onClick() {
+ this.$label instanceof HTMLElement && this.$label.click()
+ }
+ onDragEnter(t) {
+ console.log(t), this.$wrapper.classList.add("govuk-file-upload-wrapper--show-dropzone")
+ }
+ onDragLeaveOrDrop() {
+ this.$wrapper.classList.remove("govuk-file-upload-wrapper--show-dropzone")
+ }
+ observeDisabledState() {
+ new MutationObserver((t => {
+ for (const e of t) console.log("mutation", e), "attributes" === e.type && "disabled" === e.attributeName && this.updateDisabledState()
+ })).observe(this.$root, {
+ attributes: !0
+ })
+ }
+ updateDisabledState() {
+ this.$root instanceof HTMLInputElement && this.$button instanceof HTMLButtonElement && (this.$button.disabled = this.$root.disabled)
+ }
+}
+FileUpload.moduleName = "govuk-file-upload", FileUpload.defaults = Object.freeze({
+ i18n: {
+ selectFilesButton: "Choose file",
+ filesSelectedDefault: "No file chosen",
+ filesSelected: {
+ one: "%{count} file chosen",
+ other: "%{count} files chosen"
+ }
+ }
+}), FileUpload.schema = Object.freeze({
+ properties: {
+ i18n: {
+ type: "object"
+ }
+ }
+});
class Header extends GOVUKFrontendComponent {
constructor(t) {
super(t), this.$menuButton = void 0, this.$menu = void 0, this.menuIsOpen = !1, this.mql = null;
const e = this.$root.querySelector(".govuk-js-header-toggle");
if (!e) return this;
- const s = e.getAttribute("aria-controls");
- if (!s) throw new ElementError({
+ const i = e.getAttribute("aria-controls");
+ if (!i) throw new ElementError({
component: Header,
identifier: 'Navigation button (`<button class="govuk-js-header-toggle">`) attribute (`aria-controls`)'
});
- const n = document.getElementById(s);
- if (!n) throw new ElementError({
+ const s = document.getElementById(i);
+ if (!s) throw new ElementError({
component: Header,
- element: n,
- identifier: `Navigation (\`<ul id="${s}">\`)`
+ element: s,
+ identifier: `Navigation (\`<ul id="${i}">\`)`
});
- this.$menu = n, this.$menuButton = e, this.setupResponsiveChecks(), this.$menuButton.addEventListener("click", (() => this.handleMenuButtonClick()))
+ this.$menu = s, this.$menuButton = e, this.setupResponsiveChecks(), this.$menuButton.addEventListener("click", (() => this.handleMenuButtonClick()))
}
setupResponsiveChecks() {
const t = getBreakpoint("desktop");
@@ -783,27 +844,27 @@ NotificationBanner.moduleName = "govuk-notification-banner", NotificationBanner.
class PasswordInput extends GOVUKFrontendComponent {
constructor(t, e = {}) {
super(t), this.config = void 0, this.i18n = void 0, this.$input = void 0, this.$showHideButton = void 0, this.$screenReaderStatusMessage = void 0;
- const s = this.$root.querySelector(".govuk-js-password-input-input");
- if (!(s instanceof HTMLInputElement)) throw new ElementError({
+ const i = this.$root.querySelector(".govuk-js-password-input-input");
+ if (!(i instanceof HTMLInputElement)) throw new ElementError({
component: PasswordInput,
- element: s,
+ element: i,
expectedType: "HTMLInputElement",
identifier: "Form field (`.govuk-js-password-input-input`)"
});
- if ("password" !== s.type) throw new ElementError("Password input: Form field (`.govuk-js-password-input-input`) must be of type `password`.");
- const n = this.$root.querySelector(".govuk-js-password-input-toggle");
- if (!(n instanceof HTMLButtonElement)) throw new ElementError({
+ if ("password" !== i.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({
component: PasswordInput,
- element: n,
+ element: s,
expectedType: "HTMLButtonElement",
identifier: "Button (`.govuk-js-password-input-toggle`)"
});
- if ("button" !== n.type) throw new ElementError("Password input: Button (`.govuk-js-password-input-toggle`) must be of type `button`.");
- this.$input = s, this.$showHideButton = n, this.config = mergeConfigs(PasswordInput.defaults, e, normaliseDataset(PasswordInput, this.$root.dataset)), this.i18n = new I18n(this.config.i18n, {
+ if ("button" !== s.type) throw new ElementError("Password input: Button (`.govuk-js-password-input-toggle`) must be of type `button`.");
+ this.$input = i, this.$showHideButton = s, this.config = mergeConfigs(PasswordInput.defaults, e, normaliseDataset(PasswordInput, this.$root.dataset)), 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 n = document.createElement("div");
+ n.className = "govuk-password-input__sr-status govuk-visually-hidden", n.setAttribute("aria-live", "polite"), this.$screenReaderStatusMessage = n, this.$input.insertAdjacentElement("afterend", n), 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()
}
@@ -820,9 +881,9 @@ class PasswordInput extends GOVUKFrontendComponent {
if (t === this.$input.type) return;
this.$input.setAttribute("type", t);
const e = "password" === t,
- s = e ? "show" : "hide",
- n = e ? "passwordHidden" : "passwordShown";
- this.$showHideButton.innerText = this.i18n.t(`${s}Password`), this.$showHideButton.setAttribute("aria-label", this.i18n.t(`${s}PasswordAriaLabel`)), this.$screenReaderStatusMessage.innerText = this.i18n.t(`${n}Announcement`)
+ i = e ? "show" : "hide",
+ s = e ? "passwordHidden" : "passwordShown";
+ this.$showHideButton.innerText = this.i18n.t(`${i}Password`), this.$showHideButton.setAttribute("aria-label", this.i18n.t(`${i}PasswordAriaLabel`)), this.$screenReaderStatusMessage.innerText = this.i18n.t(`${s}Announcement`)
}
}
PasswordInput.moduleName = "govuk-password-input", PasswordInput.defaults = Object.freeze({
@@ -866,21 +927,21 @@ class Radios extends GOVUKFrontendComponent {
syncConditionalRevealWithInputState(t) {
const e = t.getAttribute("aria-controls");
if (!e) return;
- const s = document.getElementById(e);
- if (null != s && s.classList.contains("govuk-radios__conditional")) {
+ const i = document.getElementById(e);
+ if (null != i && i.classList.contains("govuk-radios__conditional")) {
const e = t.checked;
- t.setAttribute("aria-expanded", e.toString()), s.classList.toggle("govuk-radios__conditional--hidden", !e)
+ t.setAttribute("aria-expanded", e.toString()), i.classList.toggle("govuk-radios__conditional--hidden", !e)
}
}
handleClick(t) {
const e = t.target;
if (!(e instanceof HTMLInputElement) || "radio" !== e.type) return;
- const s = document.querySelectorAll('input[type="radio"][aria-controls]'),
- n = e.form,
- i = e.name;
- s.forEach((t => {
- const e = t.form === n;
- t.name === i && e && this.syncConditionalRevealWithInputState(t)
+ const i = document.querySelectorAll('input[type="radio"][aria-controls]'),
+ s = e.form,
+ n = e.name;
+ i.forEach((t => {
+ const e = t.form === s;
+ t.name === n && e && this.syncConditionalRevealWithInputState(t)
}))
}
}
@@ -890,18 +951,18 @@ class ServiceNavigation extends GOVUKFrontendComponent {
super(t), this.$menuButton = void 0, this.$menu = void 0, this.menuIsOpen = !1, this.mql = null;
const e = this.$root.querySelector(".govuk-js-service-navigation-toggle");
if (!e) return this;
- const s = e.getAttribute("aria-controls");
- if (!s) throw new ElementError({
+ const i = e.getAttribute("aria-controls");
+ if (!i) throw new ElementError({
component: ServiceNavigation,
identifier: 'Navigation button (`<button class="govuk-js-service-navigation-toggle">`) attribute (`aria-controls`)'
});
- const n = document.getElementById(s);
- if (!n) throw new ElementError({
+ const s = document.getElementById(i);
+ if (!s) throw new ElementError({
component: ServiceNavigation,
- element: n,
- identifier: `Navigation (\`<ul id="${s}">\`)`
+ element: s,
+ identifier: `Navigation (\`<ul id="${i}">\`)`
});
- this.$menu = n, this.$menuButton = e, this.setupResponsiveChecks(), this.$menuButton.addEventListener("click", (() => this.handleMenuButtonClick()))
+ this.$menu = s, this.$menuButton = e, this.setupResponsiveChecks(), this.$menuButton.addEventListener("click", (() => this.handleMenuButtonClick()))
}
setupResponsiveChecks() {
const t = getBreakpoint("tablet");
@@ -923,17 +984,17 @@ class SkipLink extends GOVUKFrontendComponent {
constructor(t) {
var e;
super(t);
- const s = this.$root.hash,
- n = null != (e = this.$root.getAttribute("href")) ? e : "";
- let i;
+ const i = this.$root.hash,
+ s = null != (e = this.$root.getAttribute("href")) ? e : "";
+ let n;
try {
- i = new window.URL(this.$root.href)
+ n = new window.URL(this.$root.href)
} catch (a) {
- throw new ElementError(`Skip link: Target link (\`href="${n}"\`) is invalid`)
+ throw new ElementError(`Skip link: Target link (\`href="${s}"\`) is invalid`)
}
- if (i.origin !== window.location.origin || i.pathname !== window.location.pathname) return;
- const o = getFragmentFromUrl(s);
- if (!o) throw new ElementError(`Skip link: Target link (\`href="${n}"\`) has no hash fragment`);
+ if (n.origin !== window.location.origin || n.pathname !== window.location.pathname) return;
+ const o = getFragmentFromUrl(i);
+ if (!o) throw new ElementError(`Skip link: Target link (\`href="${s}"\`) has no hash fragment`);
const r = document.getElementById(o);
if (!r) throw new ElementError({
component: SkipLink,
@@ -960,17 +1021,17 @@ class Tabs extends GOVUKFrontendComponent {
identifier: 'Links (`<a class="govuk-tabs__tab">`)'
});
this.$tabs = e, this.boundTabClick = this.onTabClick.bind(this), this.boundTabKeydown = this.onTabKeydown.bind(this), this.boundOnHashChange = this.onHashChange.bind(this);
- const s = this.$root.querySelector(".govuk-tabs__list"),
- n = this.$root.querySelectorAll("li.govuk-tabs__list-item");
- if (!s) throw new ElementError({
+ const i = this.$root.querySelector(".govuk-tabs__list"),
+ s = this.$root.querySelectorAll("li.govuk-tabs__list-item");
+ if (!i) throw new ElementError({
component: Tabs,
identifier: 'List (`<ul class="govuk-tabs__list">`)'
});
- if (!n.length) throw new ElementError({
+ if (!s.length) throw new ElementError({
component: Tabs,
identifier: 'List items (`<li class="govuk-tabs__list-item">`)'
});
- this.$tabList = s, this.$tabListItems = n, this.setupResponsiveChecks()
+ this.$tabList = i, this.$tabListItems = s, this.setupResponsiveChecks()
}
setupResponsiveChecks() {
const t = getBreakpoint("tablet");
@@ -1006,8 +1067,8 @@ class Tabs extends GOVUKFrontendComponent {
e = this.getTab(t);
if (!e) return;
if (this.changingHash) return void(this.changingHash = !1);
- const s = this.getCurrentTab();
- s && (this.hideTab(s), this.showTab(e), e.focus())
+ const i = this.getCurrentTab();
+ i && (this.hideTab(i), this.showTab(e), e.focus())
}
hideTab(t) {
this.unhighlightTab(t), this.hidePanel(t)
@@ -1022,8 +1083,8 @@ class Tabs extends GOVUKFrontendComponent {
const e = getFragmentFromUrl(t.href);
if (!e) return;
t.setAttribute("id", `tab_${e}`), t.setAttribute("role", "tab"), t.setAttribute("aria-controls", e), t.setAttribute("aria-selected", "false"), t.setAttribute("tabindex", "-1");
- const s = this.getPanel(t);
- s && (s.setAttribute("role", "tabpanel"), s.setAttribute("aria-labelledby", t.id), s.classList.add(this.jsHiddenClass))
+ const i = this.getPanel(t);
+ i && (i.setAttribute("role", "tabpanel"), i.setAttribute("aria-labelledby", t.id), i.classList.add(this.jsHiddenClass))
}
unsetAttributes(t) {
t.removeAttribute("id"), t.removeAttribute("role"), t.removeAttribute("aria-controls"), t.removeAttribute("aria-selected"), t.removeAttribute("tabindex");
@@ -1032,14 +1093,14 @@ class Tabs extends GOVUKFrontendComponent {
}
onTabClick(t) {
const e = this.getCurrentTab(),
- s = t.currentTarget;
- e && s instanceof HTMLAnchorElement && (t.preventDefault(), this.hideTab(e), this.showTab(s), this.createHistoryEntry(s))
+ i = t.currentTarget;
+ e && i instanceof HTMLAnchorElement && (t.preventDefault(), this.hideTab(e), this.showTab(i), this.createHistoryEntry(i))
}
createHistoryEntry(t) {
const e = this.getPanel(t);
if (!e) return;
- const s = e.id;
- e.id = "", this.changingHash = !0, window.location.hash = s, e.id = s
+ const i = e.id;
+ e.id = "", this.changingHash = !0, window.location.hash = i, e.id = i
}
onTabKeydown(t) {
switch (t.key) {
@@ -1057,16 +1118,16 @@ class Tabs extends GOVUKFrontendComponent {
if (null == t || !t.parentElement) return;
const e = t.parentElement.nextElementSibling;
if (!e) return;
- const s = e.querySelector("a.govuk-tabs__tab");
- s && (this.hideTab(t), this.showTab(s), s.focus(), this.createHistoryEntry(s))
+ const i = e.querySelector("a.govuk-tabs__tab");
+ i && (this.hideTab(t), this.showTab(i), i.focus(), this.createHistoryEntry(i))
}
activatePreviousTab() {
const t = this.getCurrentTab();
if (null == t || !t.parentElement) return;
const e = t.parentElement.previousElementSibling;
if (!e) return;
- const s = e.querySelector("a.govuk-tabs__tab");
- s && (this.hideTab(t), this.showTab(s), s.focus(), this.createHistoryEntry(s))
+ const i = e.querySelector("a.govuk-tabs__tab");
+ i && (this.hideTab(t), this.showTab(i), i.focus(), this.createHistoryEntry(i))
}
getPanel(t) {
const e = getFragmentFromUrl(t.href);
@@ -1096,13 +1157,14 @@ function initAll(t) {
if (t = void 0 !== t ? t : {}, !isSupported()) return void(t.onError ? t.onError(new SupportError, {
config: t
}) : console.log(new SupportError));
- const s = [
+ const i = [
[Accordion, t.accordion],
[Button, t.button],
[CharacterCount, t.characterCount],
[Checkboxes],
[ErrorSummary, t.errorSummary],
[ExitThisPage, t.exitThisPage],
+ [FileUpload, t.fileUpload],
[Header],
[NotificationBanner, t.notificationBanner],
[PasswordInput, t.passwordInput],
@@ -1111,32 +1173,32 @@ function initAll(t) {
[SkipLink],
[Tabs]
],
- n = {
+ s = {
scope: null != (e = t.scope) ? e : document,
onError: t.onError
};
- s.forEach((([Component, t]) => {
- createAll(Component, t, n)
+ i.forEach((([Component, t]) => {
+ createAll(Component, t, s)
}))
}
function createAll(Component, t, e) {
- let s, n = document;
- var i;
- "object" == typeof e && (n = null != (i = e.scope) ? i : n, s = e.onError);
- "function" == typeof e && (s = e), e instanceof HTMLElement && (n = e);
- const o = n.querySelectorAll(`[data-module="${Component.moduleName}"]`);
+ let i, s = document;
+ var n;
+ "object" == typeof e && (s = null != (n = e.scope) ? n : s, i = e.onError);
+ "function" == typeof e && (i = e), e instanceof HTMLElement && (s = e);
+ const o = s.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 (n) {
- return s ? s(n, {
+ } catch (s) {
+ return i ? i(s, {
element: e,
component: Component,
config: t
- }) : console.log(n), null
+ }) : console.log(s), null
}
- })).filter(Boolean) : (s ? s(new SupportError, {
+ })).filter(Boolean) : (i ? i(new SupportError, {
component: Component,
config: t
}) : console.log(new SupportError), [])
@@ -1150,6 +1212,7 @@ export {
GOVUKFrontendComponent as Component,
ErrorSummary,
ExitThisPage,
+ FileUpload,
Header,
NotificationBanner,
PasswordInput,
Action run for 980b1c6fbc1bc170d03614183e989902a07b9a4f
diff --git a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.css b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.css
index 647e22444..f1b48d569 100644
--- a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.css
+++ b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.css
@@ -3384,6 +3384,47 @@ screen and (forced-colors:active) {
cursor: not-allowed
}
+.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;
+ background-color: #fff
+}
+
+.govuk-file-upload-wrapper--show-dropzone .govuk-file-upload__button,
+.govuk-file-upload-wrapper--show-dropzone .govuk-file-upload__status {
+ pointer-events: none
+}
+
+.govuk-file-upload-wrapper .govuk-file-upload {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ padding: 0;
+ opacity: 0
+}
+
+.govuk-file-upload__button {
+ width: auto;
+ margin-bottom: 0;
+ flex-grow: 0;
+ flex-shrink: 0
+}
+
+.govuk-file-upload__status {
+ margin-bottom: 0;
+ margin-left: 10px
+}
+
.govuk-footer {
font-family: GDS Transport, arial, sans-serif;
-webkit-font-smoothing: antialiased;
Action run for 980b1c6fbc1bc170d03614183e989902a07b9a4f
diff --git a/packages/govuk-frontend/dist/govuk/components/file-upload/template-allows-direct-media-capture.html b/packages/govuk-frontend/dist/govuk/components/file-upload/template-allows-direct-media-capture.html
new file mode 100644
index 000000000..0547b9801
--- /dev/null
+++ b/packages/govuk-frontend/dist/govuk/components/file-upload/template-allows-direct-media-capture.html
@@ -0,0 +1,6 @@
+<div class="govuk-form-group">
+ <label class="govuk-label" for="file-upload-1">
+ Upload a file
+ </label>
+ <input class="govuk-file-upload" id="file-upload-1" name="file-upload-1" type="file" data-module="govuk-file-upload" capture="user">
+</div>
diff --git a/packages/govuk-frontend/dist/govuk/components/file-upload/template-allows-image-files-only.html b/packages/govuk-frontend/dist/govuk/components/file-upload/template-allows-image-files-only.html
new file mode 100644
index 000000000..5c215788c
--- /dev/null
+++ b/packages/govuk-frontend/dist/govuk/components/file-upload/template-allows-image-files-only.html
@@ -0,0 +1,6 @@
+<div class="govuk-form-group">
+ <label class="govuk-label" for="file-upload-1">
+ Upload a file
+ </label>
+ <input class="govuk-file-upload" id="file-upload-1" name="file-upload-1" type="file" data-module="govuk-file-upload" accept="image/*">
+</div>
diff --git a/packages/govuk-frontend/dist/govuk/components/file-upload/template-with-value.html b/packages/govuk-frontend/dist/govuk/components/file-upload/template-allows-multiple-files.html
similarity index 19%
rename from packages/govuk-frontend/dist/govuk/components/file-upload/template-with-value.html
rename to packages/govuk-frontend/dist/govuk/components/file-upload/template-allows-multiple-files.html
index 68d350f46..3d12d7825 100644
--- a/packages/govuk-frontend/dist/govuk/components/file-upload/template-with-value.html
+++ b/packages/govuk-frontend/dist/govuk/components/file-upload/template-allows-multiple-files.html
@@ -1,6 +1,6 @@
<div class="govuk-form-group">
- <label class="govuk-label" for="file-upload-4">
- Upload a photo
+ <label class="govuk-label" for="file-upload-1">
+ Upload a file
</label>
- <input class="govuk-file-upload" id="file-upload-4" name="file-upload-4" type="file" value="C:\fakepath\myphoto.jpg">
+ <input class="govuk-file-upload" id="file-upload-1" name="file-upload-1" type="file" data-module="govuk-file-upload" multiple>
</div>
diff --git a/packages/govuk-frontend/dist/govuk/components/file-upload/template-default.html b/packages/govuk-frontend/dist/govuk/components/file-upload/template-default.html
index 61ba3dedf..9ec77fcf2 100644
--- a/packages/govuk-frontend/dist/govuk/components/file-upload/template-default.html
+++ b/packages/govuk-frontend/dist/govuk/components/file-upload/template-default.html
@@ -2,5 +2,5 @@
<label class="govuk-label" for="file-upload-1">
Upload a file
</label>
- <input class="govuk-file-upload" id="file-upload-1" name="file-upload-1" type="file">
+ <input class="govuk-file-upload" id="file-upload-1" name="file-upload-1" type="file" data-module="govuk-file-upload">
</div>
diff --git a/packages/govuk-frontend/dist/govuk/components/file-upload/template-disabled.html b/packages/govuk-frontend/dist/govuk/components/file-upload/template-disabled.html
new file mode 100644
index 000000000..ec0873804
--- /dev/null
+++ b/packages/govuk-frontend/dist/govuk/components/file-upload/template-disabled.html
@@ -0,0 +1,6 @@
+<div class="govuk-form-group">
+ <label class="govuk-label" for="file-upload-1">
+ Upload a file
+ </label>
+ <input class="govuk-file-upload" id="file-upload-1" name="file-upload-1" type="file" data-module="govuk-file-upload" disabled>
+</div>
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
new file mode 100644
index 000000000..a39fab0a5
--- /dev/null
+++ b/packages/govuk-frontend/dist/govuk/components/file-upload/template-translated.html
@@ -0,0 +1,6 @@
+<div class="govuk-form-group">
+ <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" data-module="govuk-file-upload" multiple data-i18n.select-files-button="Dewiswch ffeil" data-i18n.files-selected-default="Dim ffeiliau wedi'u dewis" data-i18n.files-selected.other="%{count} ffeil wedi'u dewis" data-i18n.files-selected.one="%{count} ffeil wedi'i dewis">
+</div>
diff --git a/packages/govuk-frontend/dist/govuk/components/file-upload/template-with-error-message-and-hint.html b/packages/govuk-frontend/dist/govuk/components/file-upload/template-with-error-message-and-hint.html
index c3a5b6e63..545e1be0a 100644
--- a/packages/govuk-frontend/dist/govuk/components/file-upload/template-with-error-message-and-hint.html
+++ b/packages/govuk-frontend/dist/govuk/components/file-upload/template-with-error-message-and-hint.html
@@ -8,5 +8,5 @@
<p id="file-upload-3-error" class="govuk-error-message">
<span class="govuk-visually-hidden">Error:</span> Error message goes here
</p>
- <input class="govuk-file-upload govuk-file-upload--error" id="file-upload-3" name="file-upload-3" type="file" aria-describedby="file-upload-3-hint file-upload-3-error">
+ <input class="govuk-file-upload govuk-file-upload--error" id="file-upload-3" name="file-upload-3" type="file" data-module="govuk-file-upload" aria-describedby="file-upload-3-hint file-upload-3-error">
</div>
diff --git a/packages/govuk-frontend/dist/govuk/components/file-upload/template-with-hint-text.html b/packages/govuk-frontend/dist/govuk/components/file-upload/template-with-hint-text.html
index 952c50d0b..46fe09af4 100644
--- a/packages/govuk-frontend/dist/govuk/components/file-upload/template-with-hint-text.html
+++ b/packages/govuk-frontend/dist/govuk/components/file-upload/template-with-hint-text.html
@@ -5,5 +5,5 @@
<div id="file-upload-2-hint" class="govuk-hint">
Your photo may be in your Pictures, Photos, Downloads or Desktop folder. Or in an app like iPhoto.
</div>
- <input class="govuk-file-upload" id="file-upload-2" name="file-upload-2" type="file" aria-describedby="file-upload-2-hint">
+ <input class="govuk-file-upload" id="file-upload-2" name="file-upload-2" type="file" data-module="govuk-file-upload" aria-describedby="file-upload-2-hint">
</div>
diff --git a/packages/govuk-frontend/dist/govuk/components/file-upload/template-with-label-as-page-heading.html b/packages/govuk-frontend/dist/govuk/components/file-upload/template-with-label-as-page-heading.html
index 85845be54..18570a361 100644
--- a/packages/govuk-frontend/dist/govuk/components/file-upload/template-with-label-as-page-heading.html
+++ b/packages/govuk-frontend/dist/govuk/components/file-upload/template-with-label-as-page-heading.html
@@ -4,5 +4,5 @@
Upload a file
</label>
</h1>
- <input class="govuk-file-upload" id="file-upload-1" name="file-upload-1" type="file">
+ <input class="govuk-file-upload" id="file-upload-1" name="file-upload-1" type="file" data-module="govuk-file-upload">
</div>
diff --git a/packages/govuk-frontend/dist/govuk/components/file-upload/template-with-optional-form-group-classes.html b/packages/govuk-frontend/dist/govuk/components/file-upload/template-with-optional-form-group-classes.html
index b5249c7b2..b67aea882 100644
--- a/packages/govuk-frontend/dist/govuk/components/file-upload/template-with-optional-form-group-classes.html
+++ b/packages/govuk-frontend/dist/govuk/components/file-upload/template-with-optional-form-group-classes.html
@@ -2,5 +2,5 @@
<label class="govuk-label" for="file-upload-1">
Upload a file
</label>
- <input class="govuk-file-upload" id="file-upload-1" name="file-upload-1" type="file">
+ <input class="govuk-file-upload" id="file-upload-1" name="file-upload-1" type="file" data-module="govuk-file-upload">
</div>
Action run for 980b1c6fbc1bc170d03614183e989902a07b9a4f
diff --git a/packages/govuk-frontend/dist/govuk/all.bundle.js b/packages/govuk-frontend/dist/govuk/all.bundle.js
index aad11d44a..bf21e0eda 100644
--- a/packages/govuk-frontend/dist/govuk/all.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/all.bundle.js
@@ -1624,6 +1624,170 @@
}
});
+ /**
+ * File upload component
+ *
+ * @preserve
+ */
+ class FileUpload extends GOVUKFrontendComponent {
+ /**
+ * @param {Element | null} $root - File input element
+ * @param {FileUploadConfig} [config] - File Upload config
+ */
+ constructor($root, config = {}) {
+ super($root);
+ this.$wrapper = void 0;
+ this.$button = void 0;
+ this.$status = void 0;
+ this.config = void 0;
+ this.i18n = void 0;
+ if (!(this.$root instanceof HTMLInputElement)) {
+ return;
+ }
+ if (this.$root.type !== 'file') {
+ throw new ElementError('File upload: Form field must be an input of type `file`.');
+ }
+ this.config = mergeConfigs(FileUpload.defaults, config, normaliseDataset(FileUpload, this.$root.dataset));
+ this.i18n = new I18n(this.config.i18n, {
+ locale: closestAttributeValue(this.$root, 'lang')
+ });
+ this.$label = document.querySelector(`[for="${this.$root.id}"]`);
+ if (!this.$label) {
+ throw new ElementError({
+ component: FileUpload,
+ identifier: 'No label'
+ });
+ }
+ 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.type = 'button';
+ $button.innerText = this.i18n.t('selectFilesButton');
+ $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');
+ $wrapper.insertAdjacentElement('beforeend', $button);
+ $wrapper.insertAdjacentElement('beforeend', $status);
+ this.$root.insertAdjacentElement('afterend', $wrapper);
+ $wrapper.insertAdjacentElement('afterbegin', this.$root);
+ this.$wrapper = $wrapper;
+ this.$button = $button;
+ this.$status = $status;
+ this.$root.setAttribute('tabindex', '-1');
+ this.updateDisabledState();
+ this.observeDisabledState();
+ this.$root.addEventListener('change', this.onChange.bind(this));
+ this.$wrapper.addEventListener('drop', this.onDragLeaveOrDrop.bind(this));
+ document.addEventListener('dragenter', this.onDragEnter.bind(this));
+ document.addEventListener('dragleave', this.onDragLeaveOrDrop.bind(this));
+ }
+ onChange() {
+ if (!('files' in this.$root)) {
+ return;
+ }
+ if (!this.$root.files) {
+ return;
+ }
+ const fileCount = this.$root.files.length;
+ if (!this.$status || !this.i18n) {
+ return;
+ }
+ if (fileCount === 0) {
+ this.$status.innerText = this.i18n.t('filesSelectedDefault');
+ } else if (fileCount === 1) {
+ this.$status.innerText = this.$root.files[0].name;
+ } else {
+ this.$status.innerText = this.i18n.t('filesSelected', {
+ count: fileCount
+ });
+ }
+ }
+ onClick() {
+ if (this.$label instanceof HTMLElement) {
+ this.$label.click();
+ }
+ }
+
+ /**
+ * When a file is dragged over the container, show a visual indicator that a
+ * file can be dropped here.
+ *
+ * @param {DragEvent} event - the drag event
+ */
+ onDragEnter(event) {
+ console.log(event);
+ this.$wrapper.classList.add('govuk-file-upload-wrapper--show-dropzone');
+ }
+ onDragLeaveOrDrop() {
+ this.$wrapper.classList.remove('govuk-file-upload-wrapper--show-dropzone');
+ }
+ observeDisabledState() {
+ const observer = new MutationObserver(mutationList => {
+ for (const mutation of mutationList) {
+ console.log('mutation', mutation);
+ if (mutation.type === 'attributes' && mutation.attributeName === 'disabled') {
+ this.updateDisabledState();
+ }
+ }
+ });
+ observer.observe(this.$root, {
+ attributes: true
+ });
+ }
+ updateDisabledState() {
+ if (!(this.$root instanceof HTMLInputElement) || !(this.$button instanceof HTMLButtonElement)) {
+ return;
+ }
+ this.$button.disabled = this.$root.disabled;
+ }
+ }
+
+ /**
+ * File upload config
+ *
+ * @see {@link FileUpload.defaults}
+ * @typedef {object} FileUploadConfig
+ * @property {FileUploadTranslations} [i18n=FileUpload.defaults.i18n] - File upload translations
+ */
+
+ /**
+ * File upload translations
+ *
+ * @see {@link FileUpload.defaults.i18n}
+ * @typedef {object} FileUploadTranslations
+ *
+ * Messages used by the component
+ * @property {string} [selectFiles] - Text of button that opens file browser
+ * @property {TranslationPluralForms} [filesSelected] - Text indicating how
+ * many files have been selected
+ */
+
+ /**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ * @typedef {import('../../i18n.mjs').TranslationPluralForms} TranslationPluralForms
+ */
+ FileUpload.moduleName = 'govuk-file-upload';
+ FileUpload.defaults = Object.freeze({
+ i18n: {
+ selectFilesButton: 'Choose file',
+ filesSelectedDefault: 'No file chosen',
+ filesSelected: {
+ one: '%{count} file chosen',
+ other: '%{count} files chosen'
+ }
+ }
+ });
+ FileUpload.schema = Object.freeze({
+ properties: {
+ i18n: {
+ type: 'object'
+ }
+ }
+ });
+
/**
* Header component
*
@@ -2408,7 +2572,7 @@
}
return;
}
- const components = [[Accordion, config.accordion], [Button, config.button], [CharacterCount, config.characterCount], [Checkboxes], [ErrorSummary, config.errorSummary], [ExitThisPage, config.exitThisPage], [Header], [NotificationBanner, config.notificationBanner], [PasswordInput, config.passwordInput], [Radios], [ServiceNavigation], [SkipLink], [Tabs]];
+ const components = [[Accordion, config.accordion], [Button, config.button], [CharacterCount, config.characterCount], [Checkboxes], [ErrorSummary, config.errorSummary], [ExitThisPage, config.exitThisPage], [FileUpload, config.fileUpload], [Header], [NotificationBanner, config.notificationBanner], [PasswordInput, config.passwordInput], [Radios], [ServiceNavigation], [SkipLink], [Tabs]];
const options = {
scope: (_config$scope = config.scope) != null ? _config$scope : document,
onError: config.onError
@@ -2489,6 +2653,7 @@
* @property {CharacterCountConfig} [characterCount] - Character Count config
* @property {ErrorSummaryConfig} [errorSummary] - Error Summary config
* @property {ExitThisPageConfig} [exitThisPage] - Exit This Page config
+ * @property {FileUploadConfig} [fileUpload] - File Upload config
* @property {NotificationBannerConfig} [notificationBanner] - Notification Banner config
* @property {PasswordInputConfig} [passwordInput] - Password input config
*/
@@ -2503,6 +2668,8 @@
* @typedef {import('./components/error-summary/error-summary.mjs').ErrorSummaryConfig} ErrorSummaryConfig
* @typedef {import('./components/exit-this-page/exit-this-page.mjs').ExitThisPageConfig} ExitThisPageConfig
* @typedef {import('./components/exit-this-page/exit-this-page.mjs').ExitThisPageTranslations} ExitThisPageTranslations
+ * @typedef {import('./components/file-upload/file-upload.mjs').FileUploadConfig} FileUploadConfig
+ * @typedef {import('./components/file-upload/file-upload.mjs').FileUploadTranslations} FileUploadTranslations
* @typedef {import('./components/notification-banner/notification-banner.mjs').NotificationBannerConfig} NotificationBannerConfig
* @typedef {import('./components/password-input/password-input.mjs').PasswordInputConfig} PasswordInputConfig
*/
@@ -2538,6 +2705,7 @@
exports.Component = GOVUKFrontendComponent;
exports.ErrorSummary = ErrorSummary;
exports.ExitThisPage = ExitThisPage;
+ exports.FileUpload = FileUpload;
exports.Header = Header;
exports.NotificationBanner = NotificationBanner;
exports.PasswordInput = PasswordInput;
diff --git a/packages/govuk-frontend/dist/govuk/all.bundle.mjs b/packages/govuk-frontend/dist/govuk/all.bundle.mjs
index c72c0b957..3a8f41816 100644
--- a/packages/govuk-frontend/dist/govuk/all.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/all.bundle.mjs
@@ -1618,6 +1618,170 @@ ExitThisPage.schema = Object.freeze({
}
});
+/**
+ * File upload component
+ *
+ * @preserve
+ */
+class FileUpload extends GOVUKFrontendComponent {
+ /**
+ * @param {Element | null} $root - File input element
+ * @param {FileUploadConfig} [config] - File Upload config
+ */
+ constructor($root, config = {}) {
+ super($root);
+ this.$wrapper = void 0;
+ this.$button = void 0;
+ this.$status = void 0;
+ this.config = void 0;
+ this.i18n = void 0;
+ if (!(this.$root instanceof HTMLInputElement)) {
+ return;
+ }
+ if (this.$root.type !== 'file') {
+ throw new ElementError('File upload: Form field must be an input of type `file`.');
+ }
+ this.config = mergeConfigs(FileUpload.defaults, config, normaliseDataset(FileUpload, this.$root.dataset));
+ this.i18n = new I18n(this.config.i18n, {
+ locale: closestAttributeValue(this.$root, 'lang')
+ });
+ this.$label = document.querySelector(`[for="${this.$root.id}"]`);
+ if (!this.$label) {
+ throw new ElementError({
+ component: FileUpload,
+ identifier: 'No label'
+ });
+ }
+ 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.type = 'button';
+ $button.innerText = this.i18n.t('selectFilesButton');
+ $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');
+ $wrapper.insertAdjacentElement('beforeend', $button);
+ $wrapper.insertAdjacentElement('beforeend', $status);
+ this.$root.insertAdjacentElement('afterend', $wrapper);
+ $wrapper.insertAdjacentElement('afterbegin', this.$root);
+ this.$wrapper = $wrapper;
+ this.$button = $button;
+ this.$status = $status;
+ this.$root.setAttribute('tabindex', '-1');
+ this.updateDisabledState();
+ this.observeDisabledState();
+ this.$root.addEventListener('change', this.onChange.bind(this));
+ this.$wrapper.addEventListener('drop', this.onDragLeaveOrDrop.bind(this));
+ document.addEventListener('dragenter', this.onDragEnter.bind(this));
+ document.addEventListener('dragleave', this.onDragLeaveOrDrop.bind(this));
+ }
+ onChange() {
+ if (!('files' in this.$root)) {
+ return;
+ }
+ if (!this.$root.files) {
+ return;
+ }
+ const fileCount = this.$root.files.length;
+ if (!this.$status || !this.i18n) {
+ return;
+ }
+ if (fileCount === 0) {
+ this.$status.innerText = this.i18n.t('filesSelectedDefault');
+ } else if (fileCount === 1) {
+ this.$status.innerText = this.$root.files[0].name;
+ } else {
+ this.$status.innerText = this.i18n.t('filesSelected', {
+ count: fileCount
+ });
+ }
+ }
+ onClick() {
+ if (this.$label instanceof HTMLElement) {
+ this.$label.click();
+ }
+ }
+
+ /**
+ * When a file is dragged over the container, show a visual indicator that a
+ * file can be dropped here.
+ *
+ * @param {DragEvent} event - the drag event
+ */
+ onDragEnter(event) {
+ console.log(event);
+ this.$wrapper.classList.add('govuk-file-upload-wrapper--show-dropzone');
+ }
+ onDragLeaveOrDrop() {
+ this.$wrapper.classList.remove('govuk-file-upload-wrapper--show-dropzone');
+ }
+ observeDisabledState() {
+ const observer = new MutationObserver(mutationList => {
+ for (const mutation of mutationList) {
+ console.log('mutation', mutation);
+ if (mutation.type === 'attributes' && mutation.attributeName === 'disabled') {
+ this.updateDisabledState();
+ }
+ }
+ });
+ observer.observe(this.$root, {
+ attributes: true
+ });
+ }
+ updateDisabledState() {
+ if (!(this.$root instanceof HTMLInputElement) || !(this.$button instanceof HTMLButtonElement)) {
+ return;
+ }
+ this.$button.disabled = this.$root.disabled;
+ }
+}
+
+/**
+ * File upload config
+ *
+ * @see {@link FileUpload.defaults}
+ * @typedef {object} FileUploadConfig
+ * @property {FileUploadTranslations} [i18n=FileUpload.defaults.i18n] - File upload translations
+ */
+
+/**
+ * File upload translations
+ *
+ * @see {@link FileUpload.defaults.i18n}
+ * @typedef {object} FileUploadTranslations
+ *
+ * Messages used by the component
+ * @property {string} [selectFiles] - Text of button that opens file browser
+ * @property {TranslationPluralForms} [filesSelected] - Text indicating how
+ * many files have been selected
+ */
+
+/**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ * @typedef {import('../../i18n.mjs').TranslationPluralForms} TranslationPluralForms
+ */
+FileUpload.moduleName = 'govuk-file-upload';
+FileUpload.defaults = Object.freeze({
+ i18n: {
+ selectFilesButton: 'Choose file',
+ filesSelectedDefault: 'No file chosen',
+ filesSelected: {
+ one: '%{count} file chosen',
+ other: '%{count} files chosen'
+ }
+ }
+});
+FileUpload.schema = Object.freeze({
+ properties: {
+ i18n: {
+ type: 'object'
+ }
+ }
+});
+
/**
* Header component
*
@@ -2402,7 +2566,7 @@ function initAll(config) {
}
return;
}
- const components = [[Accordion, config.accordion], [Button, config.button], [CharacterCount, config.characterCount], [Checkboxes], [ErrorSummary, config.errorSummary], [ExitThisPage, config.exitThisPage], [Header], [NotificationBanner, config.notificationBanner], [PasswordInput, config.passwordInput], [Radios], [ServiceNavigation], [SkipLink], [Tabs]];
+ const components = [[Accordion, config.accordion], [Button, config.button], [CharacterCount, config.characterCount], [Checkboxes], [ErrorSummary, config.errorSummary], [ExitThisPage, config.exitThisPage], [FileUpload, config.fileUpload], [Header], [NotificationBanner, config.notificationBanner], [PasswordInput, config.passwordInput], [Radios], [ServiceNavigation], [SkipLink], [Tabs]];
const options = {
scope: (_config$scope = config.scope) != null ? _config$scope : document,
onError: config.onError
@@ -2483,6 +2647,7 @@ function createAll(Component, config, createAllOptions) {
* @property {CharacterCountConfig} [characterCount] - Character Count config
* @property {ErrorSummaryConfig} [errorSummary] - Error Summary config
* @property {ExitThisPageConfig} [exitThisPage] - Exit This Page config
+ * @property {FileUploadConfig} [fileUpload] - File Upload config
* @property {NotificationBannerConfig} [notificationBanner] - Notification Banner config
* @property {PasswordInputConfig} [passwordInput] - Password input config
*/
@@ -2497,6 +2662,8 @@ function createAll(Component, config, createAllOptions) {
* @typedef {import('./components/error-summary/error-summary.mjs').ErrorSummaryConfig} ErrorSummaryConfig
* @typedef {import('./components/exit-this-page/exit-this-page.mjs').ExitThisPageConfig} ExitThisPageConfig
* @typedef {import('./components/exit-this-page/exit-this-page.mjs').ExitThisPageTranslations} ExitThisPageTranslations
+ * @typedef {import('./components/file-upload/file-upload.mjs').FileUploadConfig} FileUploadConfig
+ * @typedef {import('./components/file-upload/file-upload.mjs').FileUploadTranslations} FileUploadTranslations
* @typedef {import('./components/notification-banner/notification-banner.mjs').NotificationBannerConfig} NotificationBannerConfig
* @typedef {import('./components/password-input/password-input.mjs').PasswordInputConfig} PasswordInputConfig
*/
@@ -2525,5 +2692,5 @@ function createAll(Component, config, createAllOptions) {
* @property {OnErrorCallback<T>} [onError] - callback function if error throw by component on init
*/
-export { Accordion, Button, CharacterCount, Checkboxes, GOVUKFrontendComponent as Component, ErrorSummary, ExitThisPage, Header, NotificationBanner, PasswordInput, Radios, ServiceNavigation, SkipLink, Tabs, createAll, initAll, isSupported, version };
+export { Accordion, Button, CharacterCount, Checkboxes, GOVUKFrontendComponent as Component, ErrorSummary, ExitThisPage, FileUpload, Header, NotificationBanner, PasswordInput, Radios, ServiceNavigation, SkipLink, Tabs, createAll, initAll, isSupported, version };
//# sourceMappingURL=all.bundle.mjs.map
diff --git a/packages/govuk-frontend/dist/govuk/all.mjs b/packages/govuk-frontend/dist/govuk/all.mjs
index 1d37a44bd..8c5112020 100644
--- a/packages/govuk-frontend/dist/govuk/all.mjs
+++ b/packages/govuk-frontend/dist/govuk/all.mjs
@@ -5,6 +5,7 @@ export { CharacterCount } from './components/character-count/character-count.mjs
export { Checkboxes } from './components/checkboxes/checkboxes.mjs';
export { ErrorSummary } from './components/error-summary/error-summary.mjs';
export { ExitThisPage } from './components/exit-this-page/exit-this-page.mjs';
+export { FileUpload } from './components/file-upload/file-upload.mjs';
export { Header } from './components/header/header.mjs';
export { NotificationBanner } from './components/notification-banner/notification-banner.mjs';
export { PasswordInput } from './components/password-input/password-input.mjs';
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 af3de0ee9..34e781c51 100644
--- a/packages/govuk-frontend/dist/govuk/components/file-upload/_index.scss
+++ b/packages/govuk-frontend/dist/govuk/components/file-upload/_index.scss
@@ -46,6 +46,56 @@
cursor: not-allowed;
}
}
+
+ .govuk-file-upload-wrapper {
+ display: inline-flex;
+ align-items: baseline;
+ 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;
+ background-color: $govuk-body-background-colour;
+
+ .govuk-file-upload__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 {
+ // 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;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ padding: 0;
+ opacity: 0;
+ }
+
+ .govuk-file-upload__button {
+ width: auto;
+ margin-bottom: 0;
+ flex-grow: 0;
+ flex-shrink: 0;
+ }
+
+ .govuk-file-upload__status {
+ margin-bottom: 0;
+ margin-left: govuk-spacing(2);
+ }
}
/*# 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
new file mode 100644
index 000000000..220e675bc
--- /dev/null
+++ b/packages/govuk-frontend/dist/govuk/components/file-upload/file-upload.bundle.js
@@ -0,0 +1,613 @@
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.GOVUKFrontend = {}));
+})(this, (function (exports) { 'use strict';
+
+ function closestAttributeValue($element, attributeName) {
+ const $closestElementWithAttribute = $element.closest(`[${attributeName}]`);
+ return $closestElementWithAttribute ? $closestElementWithAttribute.getAttribute(attributeName) : null;
+ }
+
+ function normaliseString(value, property) {
+ const trimmedValue = value ? value.trim() : '';
+ let output;
+ let outputType = property == null ? void 0 : property.type;
+ if (!outputType) {
+ if (['true', 'false'].includes(trimmedValue)) {
+ outputType = 'boolean';
+ }
+ if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
+ outputType = 'number';
+ }
+ }
+ switch (outputType) {
+ case 'boolean':
+ output = trimmedValue === 'true';
+ break;
+ case 'number':
+ output = Number(trimmedValue);
+ break;
+ default:
+ output = value;
+ }
+ return output;
+ }
+
+ /**
+ * @typedef {import('./index.mjs').SchemaProperty} SchemaProperty
+ */
+
+ function mergeConfigs(...configObjects) {
+ const formattedConfigObject = {};
+ for (const configObject of configObjects) {
+ for (const key of Object.keys(configObject)) {
+ const option = formattedConfigObject[key];
+ const override = configObject[key];
+ if (isObject(option) && isObject(override)) {
+ formattedConfigObject[key] = mergeConfigs(option, override);
+ } else {
+ formattedConfigObject[key] = override;
+ }
+ }
+ }
+ return formattedConfigObject;
+ }
+ function extractConfigByNamespace(Component, dataset, namespace) {
+ const property = Component.schema.properties[namespace];
+ if ((property == null ? void 0 : property.type) !== 'object') {
+ return;
+ }
+ const newObject = {
+ [namespace]: ({})
+ };
+ for (const [key, value] of Object.entries(dataset)) {
+ let current = newObject;
+ const keyParts = key.split('.');
+ for (const [index, name] of keyParts.entries()) {
+ if (typeof current === 'object') {
+ if (index < keyParts.length - 1) {
+ if (!isObject(current[name])) {
+ current[name] = {};
+ }
+ current = current[name];
+ } else if (key !== namespace) {
+ current[name] = normaliseString(value);
+ }
+ }
+ }
+ }
+ return newObject[namespace];
+ }
+ function isInitialised($root, moduleName) {
+ return $root instanceof HTMLElement && $root.hasAttribute(`data-${moduleName}-init`);
+ }
+
+ /**
+ * Checks if GOV.UK Frontend is supported on this page
+ *
+ * Some browsers will load and run our JavaScript but GOV.UK Frontend
+ * won't be supported.
+ *
+ * @param {HTMLElement | null} [$scope] - (internal) `<body>` HTML element checked for browser support
+ * @returns {boolean} Whether GOV.UK Frontend is supported on this page
+ */
+ function isSupported($scope = document.body) {
+ if (!$scope) {
+ return false;
+ }
+ return $scope.classList.contains('govuk-frontend-supported');
+ }
+ function isArray(option) {
+ return Array.isArray(option);
+ }
+ function isObject(option) {
+ return !!option && typeof option === 'object' && !isArray(option);
+ }
+ function formatErrorMessage(Component, message) {
+ return `${Component.moduleName}: ${message}`;
+ }
+
+ /**
+ * Schema for component config
+ *
+ * @typedef {object} Schema
+ * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
+ * @property {SchemaCondition[]} [anyOf] - List of schema conditions
+ */
+
+ /**
+ * Schema property for component config
+ *
+ * @typedef {object} SchemaProperty
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
+ */
+
+ /**
+ * Schema condition for component config
+ *
+ * @typedef {object} SchemaCondition
+ * @property {string[]} required - List of required config fields
+ * @property {string} errorMessage - Error message when required config fields not provided
+ */
+ /**
+ * @typedef ComponentWithModuleName
+ * @property {string} moduleName - Name of the component
+ */
+
+ function normaliseDataset(Component, dataset) {
+ const out = {};
+ for (const [field, property] of Object.entries(Component.schema.properties)) {
+ if (field in dataset) {
+ out[field] = normaliseString(dataset[field], property);
+ }
+ if ((property == null ? void 0 : property.type) === 'object') {
+ out[field] = extractConfigByNamespace(Component, dataset, field);
+ }
+ }
+ return out;
+ }
+
+ class GOVUKFrontendError extends Error {
+ constructor(...args) {
+ super(...args);
+ this.name = 'GOVUKFrontendError';
+ }
+ }
+ class SupportError extends GOVUKFrontendError {
+ /**
+ * Checks if GOV.UK Frontend is supported on this page
+ *
+ * @param {HTMLElement | null} [$scope] - HTML element `<body>` checked for browser support
+ */
+ constructor($scope = document.body) {
+ const supportMessage = 'noModule' in HTMLScriptElement.prototype ? 'GOV.UK Frontend initialised without `<body class="govuk-frontend-supported">` from template `<script>` snippet' : 'GOV.UK Frontend is not supported in this browser';
+ super($scope ? supportMessage : 'GOV.UK Frontend initialised without `<script type="module">`');
+ this.name = 'SupportError';
+ }
+ }
+ class ElementError extends GOVUKFrontendError {
+ constructor(messageOrOptions) {
+ let message = typeof messageOrOptions === 'string' ? messageOrOptions : '';
+ if (typeof messageOrOptions === 'object') {
+ const {
+ component,
+ identifier,
+ element,
+ expectedType
+ } = messageOrOptions;
+ message = identifier;
+ message += element ? ` is not of type ${expectedType != null ? expectedType : 'HTMLElement'}` : ' not found';
+ message = formatErrorMessage(component, message);
+ }
+ super(message);
+ this.name = 'ElementError';
+ }
+ }
+ class InitError extends GOVUKFrontendError {
+ constructor(componentOrMessage) {
+ const message = typeof componentOrMessage === 'string' ? componentOrMessage : formatErrorMessage(componentOrMessage, `Root element (\`$root\`) already initialised`);
+ super(message);
+ this.name = 'InitError';
+ }
+ }
+ /**
+ * @typedef {import('../common/index.mjs').ComponentWithModuleName} ComponentWithModuleName
+ */
+
+ class GOVUKFrontendComponent {
+ /**
+ * Returns the root element of the component
+ *
+ * @protected
+ * @returns {RootElementType} - the root element of component
+ */
+ get $root() {
+ return this._$root;
+ }
+ constructor($root) {
+ this._$root = void 0;
+ const childConstructor = this.constructor;
+ if (typeof childConstructor.moduleName !== 'string') {
+ throw new InitError(`\`moduleName\` not defined in component`);
+ }
+ if (!($root instanceof childConstructor.elementType)) {
+ throw new ElementError({
+ element: $root,
+ component: childConstructor,
+ identifier: 'Root element (`$root`)',
+ expectedType: childConstructor.elementType.name
+ });
+ } else {
+ this._$root = $root;
+ }
+ childConstructor.checkSupport();
+ this.checkInitialised();
+ const moduleName = childConstructor.moduleName;
+ this.$root.setAttribute(`data-${moduleName}-init`, '');
+ }
+ checkInitialised() {
+ const constructor = this.constructor;
+ const moduleName = constructor.moduleName;
+ if (moduleName && isInitialised(this.$root, moduleName)) {
+ throw new InitError(constructor);
+ }
+ }
+ static checkSupport() {
+ if (!isSupported()) {
+ throw new SupportError();
+ }
+ }
+ }
+
+ /**
+ * @typedef ChildClass
+ * @property {string} moduleName - The module name that'll be looked for in the DOM when initialising the component
+ */
+
+ /**
+ * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+ */
+ GOVUKFrontendComponent.elementType = HTMLElement;
+
+ class I18n {
+ constructor(translations = {}, config = {}) {
+ var _config$locale;
+ this.translations = void 0;
+ this.locale = void 0;
+ this.translations = translations;
+ this.locale = (_config$locale = config.locale) != null ? _config$locale : document.documentElement.lang || 'en';
+ }
+ t(lookupKey, options) {
+ if (!lookupKey) {
+ throw new Error('i18n: lookup key missing');
+ }
+ let translation = this.translations[lookupKey];
+ if (typeof (options == null ? void 0 : options.count) === 'number' && typeof translation === 'object') {
+ const translationPluralForm = translation[this.getPluralSuffix(lookupKey, options.count)];
+ if (translationPluralForm) {
+ translation = translationPluralForm;
+ }
+ }
+ if (typeof translation === 'string') {
+ if (translation.match(/%{(.\S+)}/)) {
+ if (!options) {
+ throw new Error('i18n: cannot replace placeholders in string if no option data provided');
+ }
+ return this.replacePlaceholders(translation, options);
+ }
+ return translation;
+ }
+ return lookupKey;
+ }
+ replacePlaceholders(translationString, options) {
+ const formatter = Intl.NumberFormat.supportedLocalesOf(this.locale).length ? new Intl.NumberFormat(this.locale) : undefined;
+ return translationString.replace(/%{(.\S+)}/g, function (placeholderWithBraces, placeholderKey) {
+ if (Object.prototype.hasOwnProperty.call(options, placeholderKey)) {
+ const placeholderValue = options[placeholderKey];
+ if (placeholderValue === false || typeof placeholderValue !== 'number' && typeof placeholderValue !== 'string') {
+ return '';
+ }
+ if (typeof placeholderValue === 'number') {
+ return formatter ? formatter.format(placeholderValue) : `${placeholderValue}`;
+ }
+ return placeholderValue;
+ }
+ throw new Error(`i18n: no data found to replace ${placeholderWithBraces} placeholder in string`);
+ });
+ }
+ hasIntlPluralRulesSupport() {
+ return Boolean('PluralRules' in window.Intl && Intl.PluralRules.supportedLocalesOf(this.locale).length);
+ }
+ getPluralSuffix(lookupKey, count) {
+ count = Number(count);
+ if (!isFinite(count)) {
+ return 'other';
+ }
+ const translation = this.translations[lookupKey];
+ const preferredForm = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(count) : this.selectPluralFormUsingFallbackRules(count);
+ if (typeof translation === 'object') {
+ if (preferredForm in translation) {
+ return preferredForm;
+ } else if ('other' in translation) {
+ console.warn(`i18n: Missing plural form ".${preferredForm}" for "${this.locale}" locale. Falling back to ".other".`);
+ return 'other';
+ }
+ }
+ throw new Error(`i18n: Plural form ".other" is required for "${this.locale}" locale`);
+ }
+ selectPluralFormUsingFallbackRules(count) {
+ count = Math.abs(Math.floor(count));
+ const ruleset = this.getPluralRulesForLocale();
+ if (ruleset) {
+ return I18n.pluralRules[ruleset](count);
+ }
+ return 'other';
+ }
+ getPluralRulesForLocale() {
+ const localeShort = this.locale.split('-')[0];
+ for (const pluralRule in I18n.pluralRulesMap) {
+ const languages = I18n.pluralRulesMap[pluralRule];
+ if (languages.includes(this.locale) || languages.includes(localeShort)) {
+ return pluralRule;
+ }
+ }
+ }
+ }
+ I18n.pluralRulesMap = {
+ arabic: ['ar'],
+ chinese: ['my', 'zh', 'id', 'ja', 'jv', 'ko', 'ms', 'th', 'vi'],
+ french: ['hy', 'bn', 'fr', 'gu', 'hi', 'fa', 'pa', 'zu'],
+ german: ['af', 'sq', 'az', 'eu', 'bg', 'ca', 'da', 'nl', 'en', 'et', 'fi', 'ka', 'de', 'el', 'hu', 'lb', 'no', 'so', 'sw', 'sv', 'ta', 'te', 'tr', 'ur'],
+ irish: ['ga'],
+ russian: ['ru', 'uk'],
+ scottish: ['gd'],
+ spanish: ['pt-PT', 'it', 'es'],
+ welsh: ['cy']
+ };
+ I18n.pluralRules = {
+ arabic(n) {
+ if (n === 0) {
+ return 'zero';
+ }
+ if (n === 1) {
+ return 'one';
+ }
+ if (n === 2) {
+ return 'two';
+ }
+ if (n % 100 >= 3 && n % 100 <= 10) {
+ return 'few';
+ }
+ if (n % 100 >= 11 && n % 100 <= 99) {
+ return 'many';
+ }
+ return 'other';
+ },
+ chinese() {
+ return 'other';
+ },
+ french(n) {
+ return n === 0 || n === 1 ? 'one' : 'other';
+ },
+ german(n) {
+ return n === 1 ? 'one' : 'other';
+ },
+ irish(n) {
+ if (n === 1) {
+ return 'one';
+ }
+ if (n === 2) {
+ return 'two';
+ }
+ if (n >= 3 && n <= 6) {
+ return 'few';
+ }
+ if (n >= 7 && n <= 10) {
+ return 'many';
+ }
+ return 'other';
+ },
+ russian(n) {
+ const lastTwo = n % 100;
+ const last = lastTwo % 10;
+ if (last === 1 && lastTwo !== 11) {
+ return 'one';
+ }
+ if (last >= 2 && last <= 4 && !(lastTwo >= 12 && lastTwo <= 14)) {
+ return 'few';
+ }
+ if (last === 0 || last >= 5 && last <= 9 || lastTwo >= 11 && lastTwo <= 14) {
+ return 'many';
+ }
+ return 'other';
+ },
+ scottish(n) {
+ if (n === 1 || n === 11) {
+ return 'one';
+ }
+ if (n === 2 || n === 12) {
+ return 'two';
+ }
+ if (n >= 3 && n <= 10 || n >= 13 && n <= 19) {
+ return 'few';
+ }
+ return 'other';
+ },
+ spanish(n) {
+ if (n === 1) {
+ return 'one';
+ }
+ if (n % 1000000 === 0 && n !== 0) {
+ return 'many';
+ }
+ return 'other';
+ },
+ welsh(n) {
+ if (n === 0) {
+ return 'zero';
+ }
+ if (n === 1) {
+ return 'one';
+ }
+ if (n === 2) {
+ return 'two';
+ }
+ if (n === 3) {
+ return 'few';
+ }
+ if (n === 6) {
+ return 'many';
+ }
+ return 'other';
+ }
+ };
+
+ /**
+ * File upload component
+ *
+ * @preserve
+ */
+ class FileUpload extends GOVUKFrontendComponent {
+ /**
+ * @param {Element | null} $root - File input element
+ * @param {FileUploadConfig} [config] - File Upload config
+ */
+ constructor($root, config = {}) {
+ super($root);
+ this.$wrapper = void 0;
+ this.$button = void 0;
+ this.$status = void 0;
+ this.config = void 0;
+ this.i18n = void 0;
+ if (!(this.$root instanceof HTMLInputElement)) {
+ return;
+ }
+ if (this.$root.type !== 'file') {
+ throw new ElementError('File upload: Form field must be an input of type `file`.');
+ }
+ this.config = mergeConfigs(FileUpload.defaults, config, normaliseDataset(FileUpload, this.$root.dataset));
+ this.i18n = new I18n(this.config.i18n, {
+ locale: closestAttributeValue(this.$root, 'lang')
+ });
+ this.$label = document.querySelector(`[for="${this.$root.id}"]`);
+ if (!this.$label) {
+ throw new ElementError({
+ component: FileUpload,
+ identifier: 'No label'
+ });
+ }
+ 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.type = 'button';
+ $button.innerText = this.i18n.t('selectFilesButton');
+ $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');
+ $wrapper.insertAdjacentElement('beforeend', $button);
+ $wrapper.insertAdjacentElement('beforeend', $status);
+ this.$root.insertAdjacentElement('afterend', $wrapper);
+ $wrapper.insertAdjacentElement('afterbegin', this.$root);
+ this.$wrapper = $wrapper;
+ this.$button = $button;
+ this.$status = $status;
+ this.$root.setAttribute('tabindex', '-1');
+ this.updateDisabledState();
+ this.observeDisabledState();
+ this.$root.addEventListener('change', this.onChange.bind(this));
+ this.$wrapper.addEventListener('drop', this.onDragLeaveOrDrop.bind(this));
+ document.addEventListener('dragenter', this.onDragEnter.bind(this));
+ document.addEventListener('dragleave', this.onDragLeaveOrDrop.bind(this));
+ }
+ onChange() {
+ if (!('files' in this.$root)) {
+ return;
+ }
+ if (!this.$root.files) {
+ return;
+ }
+ const fileCount = this.$root.files.length;
+ if (!this.$status || !this.i18n) {
+ return;
+ }
+ if (fileCount === 0) {
+ this.$status.innerText = this.i18n.t('filesSelectedDefault');
+ } else if (fileCount === 1) {
+ this.$status.innerText = this.$root.files[0].name;
+ } else {
+ this.$status.innerText = this.i18n.t('filesSelected', {
+ count: fileCount
+ });
+ }
+ }
+ onClick() {
+ if (this.$label instanceof HTMLElement) {
+ this.$label.click();
+ }
+ }
+
+ /**
+ * When a file is dragged over the container, show a visual indicator that a
+ * file can be dropped here.
+ *
+ * @param {DragEvent} event - the drag event
+ */
+ onDragEnter(event) {
+ console.log(event);
+ this.$wrapper.classList.add('govuk-file-upload-wrapper--show-dropzone');
+ }
+ onDragLeaveOrDrop() {
+ this.$wrapper.classList.remove('govuk-file-upload-wrapper--show-dropzone');
+ }
+ observeDisabledState() {
+ const observer = new MutationObserver(mutationList => {
+ for (const mutation of mutationList) {
+ console.log('mutation', mutation);
+ if (mutation.type === 'attributes' && mutation.attributeName === 'disabled') {
+ this.updateDisabledState();
+ }
+ }
+ });
+ observer.observe(this.$root, {
+ attributes: true
+ });
+ }
+ updateDisabledState() {
+ if (!(this.$root instanceof HTMLInputElement) || !(this.$button instanceof HTMLButtonElement)) {
+ return;
+ }
+ this.$button.disabled = this.$root.disabled;
+ }
+ }
+
+ /**
+ * File upload config
+ *
+ * @see {@link FileUpload.defaults}
+ * @typedef {object} FileUploadConfig
+ * @property {FileUploadTranslations} [i18n=FileUpload.defaults.i18n] - File upload translations
+ */
+
+ /**
+ * File upload translations
+ *
+ * @see {@link FileUpload.defaults.i18n}
+ * @typedef {object} FileUploadTranslations
+ *
+ * Messages used by the component
+ * @property {string} [selectFiles] - Text of button that opens file browser
+ * @property {TranslationPluralForms} [filesSelected] - Text indicating how
+ * many files have been selected
+ */
+
+ /**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ * @typedef {import('../../i18n.mjs').TranslationPluralForms} TranslationPluralForms
+ */
+ FileUpload.moduleName = 'govuk-file-upload';
+ FileUpload.defaults = Object.freeze({
+ i18n: {
+ selectFilesButton: 'Choose file',
+ filesSelectedDefault: 'No file chosen',
+ filesSelected: {
+ one: '%{count} file chosen',
+ other: '%{count} files chosen'
+ }
+ }
+ });
+ FileUpload.schema = Object.freeze({
+ properties: {
+ i18n: {
+ type: 'object'
+ }
+ }
+ });
+
+ exports.FileUpload = FileUpload;
+
+}));
+//# sourceMappingURL=file-upload.bundle.js.map
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
new file mode 100644
index 000000000..beb37af21
--- /dev/null
+++ b/packages/govuk-frontend/dist/govuk/components/file-upload/file-upload.bundle.mjs
@@ -0,0 +1,605 @@
+function closestAttributeValue($element, attributeName) {
+ const $closestElementWithAttribute = $element.closest(`[${attributeName}]`);
+ return $closestElementWithAttribute ? $closestElementWithAttribute.getAttribute(attributeName) : null;
+}
+
+function normaliseString(value, property) {
+ const trimmedValue = value ? value.trim() : '';
+ let output;
+ let outputType = property == null ? void 0 : property.type;
+ if (!outputType) {
+ if (['true', 'false'].includes(trimmedValue)) {
+ outputType = 'boolean';
+ }
+ if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
+ outputType = 'number';
+ }
+ }
+ switch (outputType) {
+ case 'boolean':
+ output = trimmedValue === 'true';
+ break;
+ case 'number':
+ output = Number(trimmedValue);
+ break;
+ default:
+ output = value;
+ }
+ return output;
+}
+
+/**
+ * @typedef {import('./index.mjs').SchemaProperty} SchemaProperty
+ */
+
+function mergeConfigs(...configObjects) {
+ const formattedConfigObject = {};
+ for (const configObject of configObjects) {
+ for (const key of Object.keys(configObject)) {
+ const option = formattedConfigObject[key];
+ const override = configObject[key];
+ if (isObject(option) && isObject(override)) {
+ formattedConfigObject[key] = mergeConfigs(option, override);
+ } else {
+ formattedConfigObject[key] = override;
+ }
+ }
+ }
+ return formattedConfigObject;
+}
+function extractConfigByNamespace(Component, dataset, namespace) {
+ const property = Component.schema.properties[namespace];
+ if ((property == null ? void 0 : property.type) !== 'object') {
+ return;
+ }
+ const newObject = {
+ [namespace]: ({})
+ };
+ for (const [key, value] of Object.entries(dataset)) {
+ let current = newObject;
+ const keyParts = key.split('.');
+ for (const [index, name] of keyParts.entries()) {
+ if (typeof current === 'object') {
+ if (index < keyParts.length - 1) {
+ if (!isObject(current[name])) {
+ current[name] = {};
+ }
+ current = current[name];
+ } else if (key !== namespace) {
+ current[name] = normaliseString(value);
+ }
+ }
+ }
+ }
+ return newObject[namespace];
+}
+function isInitialised($root, moduleName) {
+ return $root instanceof HTMLElement && $root.hasAttribute(`data-${moduleName}-init`);
+}
+
+/**
+ * Checks if GOV.UK Frontend is supported on this page
+ *
+ * Some browsers will load and run our JavaScript but GOV.UK Frontend
+ * won't be supported.
+ *
+ * @param {HTMLElement | null} [$scope] - (internal) `<body>` HTML element checked for browser support
+ * @returns {boolean} Whether GOV.UK Frontend is supported on this page
+ */
+function isSupported($scope = document.body) {
+ if (!$scope) {
+ return false;
+ }
+ return $scope.classList.contains('govuk-frontend-supported');
+}
+function isArray(option) {
+ return Array.isArray(option);
+}
+function isObject(option) {
+ return !!option && typeof option === 'object' && !isArray(option);
+}
+function formatErrorMessage(Component, message) {
+ return `${Component.moduleName}: ${message}`;
+}
+
+/**
+ * Schema for component config
+ *
+ * @typedef {object} Schema
+ * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
+ * @property {SchemaCondition[]} [anyOf] - List of schema conditions
+ */
+
+/**
+ * Schema property for component config
+ *
+ * @typedef {object} SchemaProperty
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
+ */
+
+/**
+ * Schema condition for component config
+ *
+ * @typedef {object} SchemaCondition
+ * @property {string[]} required - List of required config fields
+ * @property {string} errorMessage - Error message when required config fields not provided
+ */
+/**
+ * @typedef ComponentWithModuleName
+ * @property {string} moduleName - Name of the component
+ */
+
+function normaliseDataset(Component, dataset) {
+ const out = {};
+ for (const [field, property] of Object.entries(Component.schema.properties)) {
+ if (field in dataset) {
+ out[field] = normaliseString(dataset[field], property);
+ }
+ if ((property == null ? void 0 : property.type) === 'object') {
+ out[field] = extractConfigByNamespace(Component, dataset, field);
+ }
+ }
+ return out;
+}
+
+class GOVUKFrontendError extends Error {
+ constructor(...args) {
+ super(...args);
+ this.name = 'GOVUKFrontendError';
+ }
+}
+class SupportError extends GOVUKFrontendError {
+ /**
+ * Checks if GOV.UK Frontend is supported on this page
+ *
+ * @param {HTMLElement | null} [$scope] - HTML element `<body>` checked for browser support
+ */
+ constructor($scope = document.body) {
+ const supportMessage = 'noModule' in HTMLScriptElement.prototype ? 'GOV.UK Frontend initialised without `<body class="govuk-frontend-supported">` from template `<script>` snippet' : 'GOV.UK Frontend is not supported in this browser';
+ super($scope ? supportMessage : 'GOV.UK Frontend initialised without `<script type="module">`');
+ this.name = 'SupportError';
+ }
+}
+class ElementError extends GOVUKFrontendError {
+ constructor(messageOrOptions) {
+ let message = typeof messageOrOptions === 'string' ? messageOrOptions : '';
+ if (typeof messageOrOptions === 'object') {
+ const {
+ component,
+ identifier,
+ element,
+ expectedType
+ } = messageOrOptions;
+ message = identifier;
+ message += element ? ` is not of type ${expectedType != null ? expectedType : 'HTMLElement'}` : ' not found';
+ message = formatErrorMessage(component, message);
+ }
+ super(message);
+ this.name = 'ElementError';
+ }
+}
+class InitError extends GOVUKFrontendError {
+ constructor(componentOrMessage) {
+ const message = typeof componentOrMessage === 'string' ? componentOrMessage : formatErrorMessage(componentOrMessage, `Root element (\`$root\`) already initialised`);
+ super(message);
+ this.name = 'InitError';
+ }
+}
+/**
+ * @typedef {import('../common/index.mjs').ComponentWithModuleName} ComponentWithModuleName
+ */
+
+class GOVUKFrontendComponent {
+ /**
+ * Returns the root element of the component
+ *
+ * @protected
+ * @returns {RootElementType} - the root element of component
+ */
+ get $root() {
+ return this._$root;
+ }
+ constructor($root) {
+ this._$root = void 0;
+ const childConstructor = this.constructor;
+ if (typeof childConstructor.moduleName !== 'string') {
+ throw new InitError(`\`moduleName\` not defined in component`);
+ }
+ if (!($root instanceof childConstructor.elementType)) {
+ throw new ElementError({
+ element: $root,
+ component: childConstructor,
+ identifier: 'Root element (`$root`)',
+ expectedType: childConstructor.elementType.name
+ });
+ } else {
+ this._$root = $root;
+ }
+ childConstructor.checkSupport();
+ this.checkInitialised();
+ const moduleName = childConstructor.moduleName;
+ this.$root.setAttribute(`data-${moduleName}-init`, '');
+ }
+ checkInitialised() {
+ const constructor = this.constructor;
+ const moduleName = constructor.moduleName;
+ if (moduleName && isInitialised(this.$root, moduleName)) {
+ throw new InitError(constructor);
+ }
+ }
+ static checkSupport() {
+ if (!isSupported()) {
+ throw new SupportError();
+ }
+ }
+}
+
+/**
+ * @typedef ChildClass
+ * @property {string} moduleName - The module name that'll be looked for in the DOM when initialising the component
+ */
+
+/**
+ * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
+ */
+GOVUKFrontendComponent.elementType = HTMLElement;
+
+class I18n {
+ constructor(translations = {}, config = {}) {
+ var _config$locale;
+ this.translations = void 0;
+ this.locale = void 0;
+ this.translations = translations;
+ this.locale = (_config$locale = config.locale) != null ? _config$locale : document.documentElement.lang || 'en';
+ }
+ t(lookupKey, options) {
+ if (!lookupKey) {
+ throw new Error('i18n: lookup key missing');
+ }
+ let translation = this.translations[lookupKey];
+ if (typeof (options == null ? void 0 : options.count) === 'number' && typeof translation === 'object') {
+ const translationPluralForm = translation[this.getPluralSuffix(lookupKey, options.count)];
+ if (translationPluralForm) {
+ translation = translationPluralForm;
+ }
+ }
+ if (typeof translation === 'string') {
+ if (translation.match(/%{(.\S+)}/)) {
+ if (!options) {
+ throw new Error('i18n: cannot replace placeholders in string if no option data provided');
+ }
+ return this.replacePlaceholders(translation, options);
+ }
+ return translation;
+ }
+ return lookupKey;
+ }
+ replacePlaceholders(translationString, options) {
+ const formatter = Intl.NumberFormat.supportedLocalesOf(this.locale).length ? new Intl.NumberFormat(this.locale) : undefined;
+ return translationString.replace(/%{(.\S+)}/g, function (placeholderWithBraces, placeholderKey) {
+ if (Object.prototype.hasOwnProperty.call(options, placeholderKey)) {
+ const placeholderValue = options[placeholderKey];
+ if (placeholderValue === false || typeof placeholderValue !== 'number' && typeof placeholderValue !== 'string') {
+ return '';
+ }
+ if (typeof placeholderValue === 'number') {
+ return formatter ? formatter.format(placeholderValue) : `${placeholderValue}`;
+ }
+ return placeholderValue;
+ }
+ throw new Error(`i18n: no data found to replace ${placeholderWithBraces} placeholder in string`);
+ });
+ }
+ hasIntlPluralRulesSupport() {
+ return Boolean('PluralRules' in window.Intl && Intl.PluralRules.supportedLocalesOf(this.locale).length);
+ }
+ getPluralSuffix(lookupKey, count) {
+ count = Number(count);
+ if (!isFinite(count)) {
+ return 'other';
+ }
+ const translation = this.translations[lookupKey];
+ const preferredForm = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(count) : this.selectPluralFormUsingFallbackRules(count);
+ if (typeof translation === 'object') {
+ if (preferredForm in translation) {
+ return preferredForm;
+ } else if ('other' in translation) {
+ console.warn(`i18n: Missing plural form ".${preferredForm}" for "${this.locale}" locale. Falling back to ".other".`);
+ return 'other';
+ }
+ }
+ throw new Error(`i18n: Plural form ".other" is required for "${this.locale}" locale`);
+ }
+ selectPluralFormUsingFallbackRules(count) {
+ count = Math.abs(Math.floor(count));
+ const ruleset = this.getPluralRulesForLocale();
+ if (ruleset) {
+ return I18n.pluralRules[ruleset](count);
+ }
+ return 'other';
+ }
+ getPluralRulesForLocale() {
+ const localeShort = this.locale.split('-')[0];
+ for (const pluralRule in I18n.pluralRulesMap) {
+ const languages = I18n.pluralRulesMap[pluralRule];
+ if (languages.includes(this.locale) || languages.includes(localeShort)) {
+ return pluralRule;
+ }
+ }
+ }
+}
+I18n.pluralRulesMap = {
+ arabic: ['ar'],
+ chinese: ['my', 'zh', 'id', 'ja', 'jv', 'ko', 'ms', 'th', 'vi'],
+ french: ['hy', 'bn', 'fr', 'gu', 'hi', 'fa', 'pa', 'zu'],
+ german: ['af', 'sq', 'az', 'eu', 'bg', 'ca', 'da', 'nl', 'en', 'et', 'fi', 'ka', 'de', 'el', 'hu', 'lb', 'no', 'so', 'sw', 'sv', 'ta', 'te', 'tr', 'ur'],
+ irish: ['ga'],
+ russian: ['ru', 'uk'],
+ scottish: ['gd'],
+ spanish: ['pt-PT', 'it', 'es'],
+ welsh: ['cy']
+};
+I18n.pluralRules = {
+ arabic(n) {
+ if (n === 0) {
+ return 'zero';
+ }
+ if (n === 1) {
+ return 'one';
+ }
+ if (n === 2) {
+ return 'two';
+ }
+ if (n % 100 >= 3 && n % 100 <= 10) {
+ return 'few';
+ }
+ if (n % 100 >= 11 && n % 100 <= 99) {
+ return 'many';
+ }
+ return 'other';
+ },
+ chinese() {
+ return 'other';
+ },
+ french(n) {
+ return n === 0 || n === 1 ? 'one' : 'other';
+ },
+ german(n) {
+ return n === 1 ? 'one' : 'other';
+ },
+ irish(n) {
+ if (n === 1) {
+ return 'one';
+ }
+ if (n === 2) {
+ return 'two';
+ }
+ if (n >= 3 && n <= 6) {
+ return 'few';
+ }
+ if (n >= 7 && n <= 10) {
+ return 'many';
+ }
+ return 'other';
+ },
+ russian(n) {
+ const lastTwo = n % 100;
+ const last = lastTwo % 10;
+ if (last === 1 && lastTwo !== 11) {
+ return 'one';
+ }
+ if (last >= 2 && last <= 4 && !(lastTwo >= 12 && lastTwo <= 14)) {
+ return 'few';
+ }
+ if (last === 0 || last >= 5 && last <= 9 || lastTwo >= 11 && lastTwo <= 14) {
+ return 'many';
+ }
+ return 'other';
+ },
+ scottish(n) {
+ if (n === 1 || n === 11) {
+ return 'one';
+ }
+ if (n === 2 || n === 12) {
+ return 'two';
+ }
+ if (n >= 3 && n <= 10 || n >= 13 && n <= 19) {
+ return 'few';
+ }
+ return 'other';
+ },
+ spanish(n) {
+ if (n === 1) {
+ return 'one';
+ }
+ if (n % 1000000 === 0 && n !== 0) {
+ return 'many';
+ }
+ return 'other';
+ },
+ welsh(n) {
+ if (n === 0) {
+ return 'zero';
+ }
+ if (n === 1) {
+ return 'one';
+ }
+ if (n === 2) {
+ return 'two';
+ }
+ if (n === 3) {
+ return 'few';
+ }
+ if (n === 6) {
+ return 'many';
+ }
+ return 'other';
+ }
+};
+
+/**
+ * File upload component
+ *
+ * @preserve
+ */
+class FileUpload extends GOVUKFrontendComponent {
+ /**
+ * @param {Element | null} $root - File input element
+ * @param {FileUploadConfig} [config] - File Upload config
+ */
+ constructor($root, config = {}) {
+ super($root);
+ this.$wrapper = void 0;
+ this.$button = void 0;
+ this.$status = void 0;
+ this.config = void 0;
+ this.i18n = void 0;
+ if (!(this.$root instanceof HTMLInputElement)) {
+ return;
+ }
+ if (this.$root.type !== 'file') {
+ throw new ElementError('File upload: Form field must be an input of type `file`.');
+ }
+ this.config = mergeConfigs(FileUpload.defaults, config, normaliseDataset(FileUpload, this.$root.dataset));
+ this.i18n = new I18n(this.config.i18n, {
+ locale: closestAttributeValue(this.$root, 'lang')
+ });
+ this.$label = document.querySelector(`[for="${this.$root.id}"]`);
+ if (!this.$label) {
+ throw new ElementError({
+ component: FileUpload,
+ identifier: 'No label'
+ });
+ }
+ 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.type = 'button';
+ $button.innerText = this.i18n.t('selectFilesButton');
+ $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');
+ $wrapper.insertAdjacentElement('beforeend', $button);
+ $wrapper.insertAdjacentElement('beforeend', $status);
+ this.$root.insertAdjacentElement('afterend', $wrapper);
+ $wrapper.insertAdjacentElement('afterbegin', this.$root);
+ this.$wrapper = $wrapper;
+ this.$button = $button;
+ this.$status = $status;
+ this.$root.setAttribute('tabindex', '-1');
+ this.updateDisabledState();
+ this.observeDisabledState();
+ this.$root.addEventListener('change', this.onChange.bind(this));
+ this.$wrapper.addEventListener('drop', this.onDragLeaveOrDrop.bind(this));
+ document.addEventListener('dragenter', this.onDragEnter.bind(this));
+ document.addEventListener('dragleave', this.onDragLeaveOrDrop.bind(this));
+ }
+ onChange() {
+ if (!('files' in this.$root)) {
+ return;
+ }
+ if (!this.$root.files) {
+ return;
+ }
+ const fileCount = this.$root.files.length;
+ if (!this.$status || !this.i18n) {
+ return;
+ }
+ if (fileCount === 0) {
+ this.$status.innerText = this.i18n.t('filesSelectedDefault');
+ } else if (fileCount === 1) {
+ this.$status.innerText = this.$root.files[0].name;
+ } else {
+ this.$status.innerText = this.i18n.t('filesSelected', {
+ count: fileCount
+ });
+ }
+ }
+ onClick() {
+ if (this.$label instanceof HTMLElement) {
+ this.$label.click();
+ }
+ }
+
+ /**
+ * When a file is dragged over the container, show a visual indicator that a
+ * file can be dropped here.
+ *
+ * @param {DragEvent} event - the drag event
+ */
+ onDragEnter(event) {
+ console.log(event);
+ this.$wrapper.classList.add('govuk-file-upload-wrapper--show-dropzone');
+ }
+ onDragLeaveOrDrop() {
+ this.$wrapper.classList.remove('govuk-file-upload-wrapper--show-dropzone');
+ }
+ observeDisabledState() {
+ const observer = new MutationObserver(mutationList => {
+ for (const mutation of mutationList) {
+ console.log('mutation', mutation);
+ if (mutation.type === 'attributes' && mutation.attributeName === 'disabled') {
+ this.updateDisabledState();
+ }
+ }
+ });
+ observer.observe(this.$root, {
+ attributes: true
+ });
+ }
+ updateDisabledState() {
+ if (!(this.$root instanceof HTMLInputElement) || !(this.$button instanceof HTMLButtonElement)) {
+ return;
+ }
+ this.$button.disabled = this.$root.disabled;
+ }
+}
+
+/**
+ * File upload config
+ *
+ * @see {@link FileUpload.defaults}
+ * @typedef {object} FileUploadConfig
+ * @property {FileUploadTranslations} [i18n=FileUpload.defaults.i18n] - File upload translations
+ */
+
+/**
+ * File upload translations
+ *
+ * @see {@link FileUpload.defaults.i18n}
+ * @typedef {object} FileUploadTranslations
+ *
+ * Messages used by the component
+ * @property {string} [selectFiles] - Text of button that opens file browser
+ * @property {TranslationPluralForms} [filesSelected] - Text indicating how
+ * many files have been selected
+ */
+
+/**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ * @typedef {import('../../i18n.mjs').TranslationPluralForms} TranslationPluralForms
+ */
+FileUpload.moduleName = 'govuk-file-upload';
+FileUpload.defaults = Object.freeze({
+ i18n: {
+ selectFilesButton: 'Choose file',
+ filesSelectedDefault: 'No file chosen',
+ filesSelected: {
+ one: '%{count} file chosen',
+ other: '%{count} files chosen'
+ }
+ }
+});
+FileUpload.schema = Object.freeze({
+ properties: {
+ i18n: {
+ type: 'object'
+ }
+ }
+});
+
+export { FileUpload };
+//# sourceMappingURL=file-upload.bundle.mjs.map
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
new file mode 100644
index 000000000..d44dd2308
--- /dev/null
+++ b/packages/govuk-frontend/dist/govuk/components/file-upload/file-upload.mjs
@@ -0,0 +1,173 @@
+import { closestAttributeValue } from '../../common/closest-attribute-value.mjs';
+import { mergeConfigs } from '../../common/index.mjs';
+import { normaliseDataset } from '../../common/normalise-dataset.mjs';
+import { ElementError } from '../../errors/index.mjs';
+import { GOVUKFrontendComponent } from '../../govuk-frontend-component.mjs';
+import { I18n } from '../../i18n.mjs';
+
+/**
+ * File upload component
+ *
+ * @preserve
+ */
+class FileUpload extends GOVUKFrontendComponent {
+ /**
+ * @param {Element | null} $root - File input element
+ * @param {FileUploadConfig} [config] - File Upload config
+ */
+ constructor($root, config = {}) {
+ super($root);
+ this.$wrapper = void 0;
+ this.$button = void 0;
+ this.$status = void 0;
+ this.config = void 0;
+ this.i18n = void 0;
+ if (!(this.$root instanceof HTMLInputElement)) {
+ return;
+ }
+ if (this.$root.type !== 'file') {
+ throw new ElementError('File upload: Form field must be an input of type `file`.');
+ }
+ this.config = mergeConfigs(FileUpload.defaults, config, normaliseDataset(FileUpload, this.$root.dataset));
+ this.i18n = new I18n(this.config.i18n, {
+ locale: closestAttributeValue(this.$root, 'lang')
+ });
+ this.$label = document.querySelector(`[for="${this.$root.id}"]`);
+ if (!this.$label) {
+ throw new ElementError({
+ component: FileUpload,
+ identifier: 'No label'
+ });
+ }
+ 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.type = 'button';
+ $button.innerText = this.i18n.t('selectFilesButton');
+ $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');
+ $wrapper.insertAdjacentElement('beforeend', $button);
+ $wrapper.insertAdjacentElement('beforeend', $status);
+ this.$root.insertAdjacentElement('afterend', $wrapper);
+ $wrapper.insertAdjacentElement('afterbegin', this.$root);
+ this.$wrapper = $wrapper;
+ this.$button = $button;
+ this.$status = $status;
+ this.$root.setAttribute('tabindex', '-1');
+ this.updateDisabledState();
+ this.observeDisabledState();
+ this.$root.addEventListener('change', this.onChange.bind(this));
+ this.$wrapper.addEventListener('drop', this.onDragLeaveOrDrop.bind(this));
+ document.addEventListener('dragenter', this.onDragEnter.bind(this));
+ document.addEventListener('dragleave', this.onDragLeaveOrDrop.bind(this));
+ }
+ onChange() {
+ if (!('files' in this.$root)) {
+ return;
+ }
+ if (!this.$root.files) {
+ return;
+ }
+ const fileCount = this.$root.files.length;
+ if (!this.$status || !this.i18n) {
+ return;
+ }
+ if (fileCount === 0) {
+ this.$status.innerText = this.i18n.t('filesSelectedDefault');
+ } else if (fileCount === 1) {
+ this.$status.innerText = this.$root.files[0].name;
+ } else {
+ this.$status.innerText = this.i18n.t('filesSelected', {
+ count: fileCount
+ });
+ }
+ }
+ onClick() {
+ if (this.$label instanceof HTMLElement) {
+ this.$label.click();
+ }
+ }
+
+ /**
+ * When a file is dragged over the container, show a visual indicator that a
+ * file can be dropped here.
+ *
+ * @param {DragEvent} event - the drag event
+ */
+ onDragEnter(event) {
+ console.log(event);
+ this.$wrapper.classList.add('govuk-file-upload-wrapper--show-dropzone');
+ }
+ onDragLeaveOrDrop() {
+ this.$wrapper.classList.remove('govuk-file-upload-wrapper--show-dropzone');
+ }
+ observeDisabledState() {
+ const observer = new MutationObserver(mutationList => {
+ for (const mutation of mutationList) {
+ console.log('mutation', mutation);
+ if (mutation.type === 'attributes' && mutation.attributeName === 'disabled') {
+ this.updateDisabledState();
+ }
+ }
+ });
+ observer.observe(this.$root, {
+ attributes: true
+ });
+ }
+ updateDisabledState() {
+ if (!(this.$root instanceof HTMLInputElement) || !(this.$button instanceof HTMLButtonElement)) {
+ return;
+ }
+ this.$button.disabled = this.$root.disabled;
+ }
+}
+
+/**
+ * File upload config
+ *
+ * @see {@link FileUpload.defaults}
+ * @typedef {object} FileUploadConfig
+ * @property {FileUploadTranslations} [i18n=FileUpload.defaults.i18n] - File upload translations
+ */
+
+/**
+ * File upload translations
+ *
+ * @see {@link FileUpload.defaults.i18n}
+ * @typedef {object} FileUploadTranslations
+ *
+ * Messages used by the component
+ * @property {string} [selectFiles] - Text of button that opens file browser
+ * @property {TranslationPluralForms} [filesSelected] - Text indicating how
+ * many files have been selected
+ */
+
+/**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ * @typedef {import('../../i18n.mjs').TranslationPluralForms} TranslationPluralForms
+ */
+FileUpload.moduleName = 'govuk-file-upload';
+FileUpload.defaults = Object.freeze({
+ i18n: {
+ selectFilesButton: 'Choose file',
+ filesSelectedDefault: 'No file chosen',
+ filesSelected: {
+ one: '%{count} file chosen',
+ other: '%{count} files chosen'
+ }
+ }
+});
+FileUpload.schema = Object.freeze({
+ properties: {
+ i18n: {
+ type: 'object'
+ }
+ }
+});
+
+export { FileUpload };
+//# sourceMappingURL=file-upload.mjs.map
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 4a36de2a3..74645e1bc 100644
--- a/packages/govuk-frontend/dist/govuk/components/file-upload/fixtures.json
+++ b/packages/govuk-frontend/dist/govuk/components/file-upload/fixtures.json
@@ -14,7 +14,75 @@
"description": "",
"previewLayoutModifiers": [],
"screenshot": false,
- "html": "<div class=\"govuk-form-group\">\n <label class=\"govuk-label\" for=\"file-upload-1\">\n Upload a file\n </label>\n <input class=\"govuk-file-upload\" id=\"file-upload-1\" name=\"file-upload-1\" type=\"file\">\n</div>"
+ "html": "<div class=\"govuk-form-group\">\n <label class=\"govuk-label\" for=\"file-upload-1\">\n Upload a file\n </label>\n <input class=\"govuk-file-upload\" id=\"file-upload-1\" name=\"file-upload-1\" type=\"file\" data-module=\"govuk-file-upload\">\n</div>"
+ },
+ {
+ "name": "allows multiple files",
+ "options": {
+ "id": "file-upload-1",
+ "name": "file-upload-1",
+ "label": {
+ "text": "Upload a file"
+ },
+ "multiple": true
+ },
+ "hidden": false,
+ "description": "",
+ "previewLayoutModifiers": [],
+ "screenshot": false,
+ "html": "<div class=\"govuk-form-group\">\n <label class=\"govuk-label\" for=\"file-upload-1\">\n Upload a file\n </label>\n <input class=\"govuk-file-upload\" id=\"file-upload-1\" name=\"file-upload-1\" type=\"file\" data-module=\"govuk-file-upload\" multiple>\n</div>"
+ },
+ {
+ "name": "allows image files only",
+ "options": {
+ "id": "file-upload-1",
+ "name": "file-upload-1",
+ "label": {
+ "text": "Upload a file"
+ },
+ "attributes": {
+ "accept": "image/*"
+ }
+ },
+ "hidden": false,
+ "description": "",
+ "previewLayoutModifiers": [],
+ "screenshot": false,
+ "html": "<div class=\"govuk-form-group\">\n <label class=\"govuk-label\" for=\"file-upload-1\">\n Upload a file\n </label>\n <input class=\"govuk-file-upload\" id=\"file-upload-1\" name=\"file-upload-1\" type=\"file\" data-module=\"govuk-file-upload\" accept=\"image/*\">\n</div>"
+ },
+ {
+ "name": "allows direct media capture",
+ "options": {
+ "id": "file-upload-1",
+ "name": "file-upload-1",
+ "label": {
+ "text": "Upload a file"
+ },
+ "attributes": {
+ "capture": "user"
+ }
+ },
+ "hidden": false,
+ "description": "Currently only works on mobile devices.",
+ "previewLayoutModifiers": [],
+ "screenshot": false,
+ "html": "<div class=\"govuk-form-group\">\n <label class=\"govuk-label\" for=\"file-upload-1\">\n Upload a file\n </label>\n <input class=\"govuk-file-upload\" id=\"file-upload-1\" name=\"file-upload-1\" type=\"file\" data-module=\"govuk-file-upload\" capture=\"user\">\n</div>"
+ },
+ {
+ "name": "disabled",
+ "options": {
+ "id": "file-upload-1",
+ "name": "file-upload-1",
+ "label": {
+ "text": "Upload a file"
+ },
+ "disabled": true
+ },
+ "hidden": false,
+ "description": "",
+ "previewLayoutModifiers": [],
+ "screenshot": false,
+ "html": "<div class=\"govuk-form-group\">\n <label class=\"govuk-label\" for=\"file-upload-1\">\n Upload a file\n </label>\n <input class=\"govuk-file-upload\" id=\"file-upload-1\" name=\"file-upload-1\" type=\"file\" data-module=\"govuk-file-upload\" disabled>\n</div>"
},
{
"name": "with hint text",
@@ -32,7 +100,7 @@
"description": "",
"previewLayoutModifiers": [],
"screenshot": false,
- "html": "<div class=\"govuk-form-group\">\n <label class=\"govuk-label\" for=\"file-upload-2\">\n Upload your photo\n </label>\n <div id=\"file-upload-2-hint\" class=\"govuk-hint\">\n Your photo may be in your Pictures, Photos, Downloads or Desktop folder. Or in an app like iPhoto.\n </div>\n <input class=\"govuk-file-upload\" id=\"file-upload-2\" name=\"file-upload-2\" type=\"file\" aria-describedby=\"file-upload-2-hint\">\n</div>"
+ "html": "<div class=\"govuk-form-group\">\n <label class=\"govuk-label\" for=\"file-upload-2\">\n Upload your photo\n </label>\n <div id=\"file-upload-2-hint\" class=\"govuk-hint\">\n Your photo may be in your Pictures, Photos, Downloads or Desktop folder. Or in an app like iPhoto.\n </div>\n <input class=\"govuk-file-upload\" id=\"file-upload-2\" name=\"file-upload-2\" type=\"file\" data-module=\"govuk-file-upload\" aria-describedby=\"file-upload-2-hint\">\n</div>"
},
{
"name": "with error message and hint",
@@ -53,58 +121,80 @@
"description": "",
"previewLayoutModifiers": [],
"screenshot": false,
- "html": "<div class=\"govuk-form-group govuk-form-group--error\">\n <label class=\"govuk-label\" for=\"file-upload-3\">\n Upload a file\n </label>\n <div id=\"file-upload-3-hint\" class=\"govuk-hint\">\n Your photo may be in your Pictures, Photos, Downloads or Desktop folder. Or in an app like iPhoto.\n </div>\n <p id=\"file-upload-3-error\" class=\"govuk-error-message\">\n <span class=\"govuk-visually-hidden\">Error:</span> Error message goes here\n </p>\n <input class=\"govuk-file-upload govuk-file-upload--error\" id=\"file-upload-3\" name=\"file-upload-3\" type=\"file\" aria-describedby=\"file-upload-3-hint file-upload-3-error\">\n</div>"
+ "html": "<div class=\"govuk-form-group govuk-form-group--error\">\n <label class=\"govuk-label\" for=\"file-upload-3\">\n Upload a file\n </label>\n <div id=\"file-upload-3-hint\" class=\"govuk-hint\">\n Your photo may be in your Pictures, Photos, Downloads or Desktop folder. Or in an app like iPhoto.\n </div>\n <p id=\"file-upload-3-error\" class=\"govuk-error-message\">\n <span class=\"govuk-visually-hidden\">Error:</span> Error message goes here\n </p>\n <input class=\"govuk-file-upload govuk-file-upload--error\" id=\"file-upload-3\" name=\"file-upload-3\" type=\"file\" data-module=\"govuk-file-upload\" aria-describedby=\"file-upload-3-hint file-upload-3-error\">\n</div>"
},
{
- "name": "with value",
+ "name": "with label as page heading",
"options": {
- "id": "file-upload-4",
- "name": "file-upload-4",
- "value": "C:\\fakepath\\myphoto.jpg",
+ "id": "file-upload-1",
+ "name": "file-upload-1",
"label": {
- "text": "Upload a photo"
+ "text": "Upload a file",
+ "classes": "govuk-label--l",
+ "isPageHeading": true
}
},
"hidden": false,
"description": "",
"previewLayoutModifiers": [],
"screenshot": false,
- "html": "<div class=\"govuk-form-group\">\n <label class=\"govuk-label\" for=\"file-upload-4\">\n Upload a photo\n </label>\n <input class=\"govuk-file-upload\" id=\"file-upload-4\" name=\"file-upload-4\" type=\"file\" value=\"C:\fakepath\myphoto.jpg\">\n</div>"
+ "html": "<div class=\"govuk-form-group\">\n <h1 class=\"govuk-label-wrapper\">\n <label class=\"govuk-label govuk-label--l\" for=\"file-upload-1\">\n Upload a file\n </label>\n </h1>\n <input class=\"govuk-file-upload\" id=\"file-upload-1\" name=\"file-upload-1\" type=\"file\" data-module=\"govuk-file-upload\">\n</div>"
},
{
- "name": "with label as page heading",
+ "name": "with optional form-group classes",
"options": {
"id": "file-upload-1",
"name": "file-upload-1",
"label": {
- "text": "Upload a file",
- "classes": "govuk-label--l",
- "isPageHeading": true
+ "text": "Upload a file"
+ },
+ "formGroup": {
+ "classes": "extra-class"
}
},
"hidden": false,
"description": "",
"previewLayoutModifiers": [],
"screenshot": false,
- "html": "<div class=\"govuk-form-group\">\n <h1 class=\"govuk-label-wrapper\">\n <label class=\"govuk-label govuk-label--l\" for=\"file-upload-1\">\n Upload a file\n </label>\n </h1>\n <input class=\"govuk-file-upload\" id=\"file-upload-1\" name=\"file-upload-1\" type=\"file\">\n</div>"
+ "html": "<div class=\"govuk-form-group extra-class\">\n <label class=\"govuk-label\" for=\"file-upload-1\">\n Upload a file\n </label>\n <input class=\"govuk-file-upload\" id=\"file-upload-1\" name=\"file-upload-1\" type=\"file\" data-module=\"govuk-file-upload\">\n</div>"
},
{
- "name": "with optional form-group classes",
+ "name": "translated",
"options": {
"id": "file-upload-1",
"name": "file-upload-1",
"label": {
- "text": "Upload a file"
+ "text": "Llwythwch ffeil i fyny"
},
- "formGroup": {
- "classes": "extra-class"
+ "multiple": true,
+ "selectFilesButtonText": "Dewiswch ffeil",
+ "filesSelectedDefaultText": "Dim ffeiliau wedi'u dewis",
+ "filesSelectedText": {
+ "other": "%{count} ffeil wedi'u dewis",
+ "one": "%{count} ffeil wedi'i dewis"
}
},
"hidden": false,
"description": "",
"previewLayoutModifiers": [],
"screenshot": false,
- "html": "<div class=\"govuk-form-group extra-class\">\n <label class=\"govuk-label\" for=\"file-upload-1\">\n Upload a file\n </label>\n <input class=\"govuk-file-upload\" id=\"file-upload-1\" name=\"file-upload-1\" type=\"file\">\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\" data-module=\"govuk-file-upload\" multiple data-i18n.select-files-button=\"Dewiswch ffeil\" data-i18n.files-selected-default=\"Dim ffeiliau wedi'u dewis\" data-i18n.files-selected.other=\"%{count} ffeil wedi'u dewis\" data-i18n.files-selected.one=\"%{count} ffeil wedi'i dewis\">\n</div>"
+ },
+ {
+ "name": "with value",
+ "options": {
+ "id": "file-upload-4",
+ "name": "file-upload-4",
+ "value": "C:\\fakepath\\myphoto.jpg",
+ "label": {
+ "text": "Upload a photo"
+ }
+ },
+ "hidden": true,
+ "description": "",
+ "previewLayoutModifiers": [],
+ "screenshot": false,
+ "html": "<div class=\"govuk-form-group\">\n <label class=\"govuk-label\" for=\"file-upload-4\">\n Upload a photo\n </label>\n <input class=\"govuk-file-upload\" id=\"file-upload-4\" name=\"file-upload-4\" type=\"file\" data-module=\"govuk-file-upload\" value=\"C:\fakepath\myphoto.jpg\">\n</div>"
},
{
"name": "attributes",
@@ -122,7 +212,7 @@
"description": "",
"previewLayoutModifiers": [],
"screenshot": false,
- "html": "<div class=\"govuk-form-group\">\n <label class=\"govuk-label\" for=\"file-upload-attributes\">\n Upload a file\n </label>\n <input class=\"govuk-file-upload\" id=\"file-upload-attributes\" name=\"file-upload-attributes\" type=\"file\" accept=\".jpg, .jpeg, .png\">\n</div>"
+ "html": "<div class=\"govuk-form-group\">\n <label class=\"govuk-label\" for=\"file-upload-attributes\">\n Upload a file\n </label>\n <input class=\"govuk-file-upload\" id=\"file-upload-attributes\" name=\"file-upload-attributes\" type=\"file\" data-module=\"govuk-file-upload\" accept=\".jpg, .jpeg, .png\">\n</div>"
},
{
"name": "classes",
@@ -138,7 +228,7 @@
"description": "",
"previewLayoutModifiers": [],
"screenshot": false,
- "html": "<div class=\"govuk-form-group\">\n <label class=\"govuk-label\" for=\"file-upload-classes\">\n Upload a file\n </label>\n <input class=\"govuk-file-upload app-file-upload--custom-modifier\" id=\"file-upload-classes\" name=\"file-upload-classes\" type=\"file\">\n</div>"
+ "html": "<div class=\"govuk-form-group\">\n <label class=\"govuk-label\" for=\"file-upload-classes\">\n Upload a file\n </label>\n <input class=\"govuk-file-upload app-file-upload--custom-modifier\" id=\"file-upload-classes\" name=\"file-upload-classes\" type=\"file\" data-module=\"govuk-file-upload\">\n</div>"
},
{
"name": "with describedBy",
@@ -154,7 +244,7 @@
"description": "",
"previewLayoutModifiers": [],
"screenshot": false,
- "html": "<div class=\"govuk-form-group\">\n <label class=\"govuk-label\" for=\"file-upload-describedby\">\n Upload a file\n </label>\n <input class=\"govuk-file-upload\" id=\"file-upload-describedby\" name=\"file-upload-describedby\" type=\"file\" aria-describedby=\"test-target-element\">\n</div>"
+ "html": "<div class=\"govuk-form-group\">\n <label class=\"govuk-label\" for=\"file-upload-describedby\">\n Upload a file\n </label>\n <input class=\"govuk-file-upload\" id=\"file-upload-describedby\" name=\"file-upload-describedby\" type=\"file\" data-module=\"govuk-file-upload\" aria-describedby=\"test-target-element\">\n</div>"
},
{
"name": "with hint and describedBy",
@@ -173,7 +263,7 @@
"description": "",
"previewLayoutModifiers": [],
"screenshot": false,
- "html": "<div class=\"govuk-form-group\">\n <label class=\"govuk-label\" for=\"file-upload-hint-describedby\">\n Upload a file\n </label>\n <div id=\"file-upload-hint-describedby-hint\" class=\"govuk-hint\">\n Your photo may be in your Pictures, Photos, Downloads or Desktop folder. Or in an app like iPhoto.\n </div>\n <input class=\"govuk-file-upload\" id=\"file-upload-hint-describedby\" name=\"file-upload-hint-describedby\" type=\"file\" aria-describedby=\"test-target-element file-upload-hint-describedby-hint\">\n</div>"
+ "html": "<div class=\"govuk-form-group\">\n <label class=\"govuk-label\" for=\"file-upload-hint-describedby\">\n Upload a file\n </label>\n <div id=\"file-upload-hint-describedby-hint\" class=\"govuk-hint\">\n Your photo may be in your Pictures, Photos, Downloads or Desktop folder. Or in an app like iPhoto.\n </div>\n <input class=\"govuk-file-upload\" id=\"file-upload-hint-describedby\" name=\"file-upload-hint-describedby\" type=\"file\" data-module=\"govuk-file-upload\" aria-describedby=\"test-target-element file-upload-hint-describedby-hint\">\n</div>"
},
{
"name": "error",
@@ -191,7 +281,7 @@
"description": "",
"previewLayoutModifiers": [],
"screenshot": false,
- "html": "<div class=\"govuk-form-group govuk-form-group--error\">\n <label class=\"govuk-label\" for=\"file-upload-with-error\">\n Upload a file\n </label>\n <p id=\"file-upload-with-error-error\" class=\"govuk-error-message\">\n <span class=\"govuk-visually-hidden\">Error:</span> Error message\n </p>\n <input class=\"govuk-file-upload govuk-file-upload--error\" id=\"file-upload-with-error\" name=\"file-upload-with-error\" type=\"file\" aria-describedby=\"file-upload-with-error-error\">\n</div>"
+ "html": "<div class=\"govuk-form-group govuk-form-group--error\">\n <label class=\"govuk-label\" for=\"file-upload-with-error\">\n Upload a file\n </label>\n <p id=\"file-upload-with-error-error\" class=\"govuk-error-message\">\n <span class=\"govuk-visually-hidden\">Error:</span> Error message\n </p>\n <input class=\"govuk-file-upload govuk-file-upload--error\" id=\"file-upload-with-error\" name=\"file-upload-with-error\" type=\"file\" data-module=\"govuk-file-upload\" aria-describedby=\"file-upload-with-error-error\">\n</div>"
},
{
"name": "with error and describedBy",
@@ -210,7 +300,7 @@
"description": "",
"previewLayoutModifiers": [],
"screenshot": false,
- "html": "<div class=\"govuk-form-group govuk-form-group--error\">\n <label class=\"govuk-label\" for=\"file-upload-error-describedby\">\n Upload a file\n </label>\n <p id=\"file-upload-error-describedby-error\" class=\"govuk-error-message\">\n <span class=\"govuk-visually-hidden\">Error:</span> Error message\n </p>\n <input class=\"govuk-file-upload govuk-file-upload--error\" id=\"file-upload-error-describedby\" name=\"file-upload-error-describedby\" type=\"file\" aria-describedby=\"test-target-element file-upload-error-describedby-error\">\n</div>"
+ "html": "<div class=\"govuk-form-group govuk-form-group--error\">\n <label class=\"govuk-label\" for=\"file-upload-error-describedby\">\n Upload a file\n </label>\n <p id=\"file-upload-error-describedby-error\" class=\"govuk-error-message\">\n <span class=\"govuk-visually-hidden\">Error:</span> Error message\n </p>\n <input class=\"govuk-file-upload govuk-file-upload--error\" id=\"file-upload-error-describedby\" name=\"file-upload-error-describedby\" type=\"file\" data-module=\"govuk-file-upload\" aria-describedby=\"test-target-element file-upload-error-describedby-error\">\n</div>"
},
{
"name": "with error, describedBy and hint",
@@ -232,7 +322,7 @@
"description": "",
"previewLayoutModifiers": [],
"screenshot": false,
- "html": "<div class=\"govuk-form-group govuk-form-group--error\">\n <label class=\"govuk-label\" for=\"file-upload-error-describedby-hint\">\n Upload a file\n </label>\n <div id=\"file-upload-error-describedby-hint-hint\" class=\"govuk-hint\">\n hint\n </div>\n <p id=\"file-upload-error-describedby-hint-error\" class=\"govuk-error-message\">\n <span class=\"govuk-visually-hidden\">Error:</span> Error message\n </p>\n <input class=\"govuk-file-upload govuk-file-upload--error\" id=\"file-upload-error-describedby-hint\" name=\"file-upload-error-describedby-hint\" type=\"file\" aria-describedby=\"test-target-element file-upload-error-describedby-hint-hint file-upload-error-describedby-hint-error\">\n</div>"
+ "html": "<div class=\"govuk-form-group govuk-form-group--error\">\n <label class=\"govuk-label\" for=\"file-upload-error-describedby-hint\">\n Upload a file\n </label>\n <div id=\"file-upload-error-describedby-hint-hint\" class=\"govuk-hint\">\n hint\n </div>\n <p id=\"file-upload-error-describedby-hint-error\" class=\"govuk-error-message\">\n <span class=\"govuk-visually-hidden\">Error:</span> Error message\n </p>\n <input class=\"govuk-file-upload govuk-file-upload--error\" id=\"file-upload-error-describedby-hint\" name=\"file-upload-error-describedby-hint\" type=\"file\" data-module=\"govuk-file-upload\" aria-describedby=\"test-target-element file-upload-error-describedby-hint-hint file-upload-error-describedby-hint-error\">\n</div>"
}
]
}
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 c7608479a..deb9c3604 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
@@ -23,6 +23,12 @@
"required": false,
"description": "If `true`, file input will be disabled."
},
+ {
+ "name": "multiple",
+ "type": "boolean",
+ "required": false,
+ "description": "If `true`, a user may select multiple files at the same time. The exact mechanism to do this differs depending on operating system."
+ },
{
"name": "describedBy",
"type": "string",
@@ -110,6 +116,24 @@
}
]
},
+ {
+ "name": "selectFilesButtonText",
+ "type": "string",
+ "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": "filesSelected",
+ "type": "object",
+ "required": false,
+ "description": "The text to display when multiple files has been chosen by the user. JavaScript enhanced version of the component only. The component will replace the `%{count}` placeholder with the number of files selected. This is a [pluralised list of messages](https://frontend.design-system.service.gov.uk/localise-govuk-frontend)."
+ },
+ {
+ "name": "filesSelectedDefault",
+ "type": "string",
+ "required": false,
+ "description": "The text to display when no file has been chosen by the user. JavaScript enhanced version of the component only. Default is \"No file chosen\"."
+ },
{
"name": "classes",
"type": "string",
diff --git a/packages/govuk-frontend/dist/govuk/init.mjs b/packages/govuk-frontend/dist/govuk/init.mjs
index 9b087239b..6f762f4da 100644
--- a/packages/govuk-frontend/dist/govuk/init.mjs
+++ b/packages/govuk-frontend/dist/govuk/init.mjs
@@ -5,6 +5,7 @@ import { CharacterCount } from './components/character-count/character-count.mjs
import { Checkboxes } from './components/checkboxes/checkboxes.mjs';
import { ErrorSummary } from './components/error-summary/error-summary.mjs';
import { ExitThisPage } from './components/exit-this-page/exit-this-page.mjs';
+import { FileUpload } from './components/file-upload/file-upload.mjs';
import { Header } from './components/header/header.mjs';
import { NotificationBanner } from './components/notification-banner/notification-banner.mjs';
import { PasswordInput } from './components/password-input/password-input.mjs';
@@ -35,7 +36,7 @@ function initAll(config) {
}
return;
}
- const components = [[Accordion, config.accordion], [Button, config.button], [CharacterCount, config.characterCount], [Checkboxes], [ErrorSummary, config.errorSummary], [ExitThisPage, config.exitThisPage], [Header], [NotificationBanner, config.notificationBanner], [PasswordInput, config.passwordInput], [Radios], [ServiceNavigation], [SkipLink], [Tabs]];
+ const components = [[Accordion, config.accordion], [Button, config.button], [CharacterCount, config.characterCount], [Checkboxes], [ErrorSummary, config.errorSummary], [ExitThisPage, config.exitThisPage], [FileUpload, config.fileUpload], [Header], [NotificationBanner, config.notificationBanner], [PasswordInput, config.passwordInput], [Radios], [ServiceNavigation], [SkipLink], [Tabs]];
const options = {
scope: (_config$scope = config.scope) != null ? _config$scope : document,
onError: config.onError
@@ -116,6 +117,7 @@ function createAll(Component, config, createAllOptions) {
* @property {CharacterCountConfig} [characterCount] - Character Count config
* @property {ErrorSummaryConfig} [errorSummary] - Error Summary config
* @property {ExitThisPageConfig} [exitThisPage] - Exit This Page config
+ * @property {FileUploadConfig} [fileUpload] - File Upload config
* @property {NotificationBannerConfig} [notificationBanner] - Notification Banner config
* @property {PasswordInputConfig} [passwordInput] - Password input config
*/
@@ -130,6 +132,8 @@ function createAll(Component, config, createAllOptions) {
* @typedef {import('./components/error-summary/error-summary.mjs').ErrorSummaryConfig} ErrorSummaryConfig
* @typedef {import('./components/exit-this-page/exit-this-page.mjs').ExitThisPageConfig} ExitThisPageConfig
* @typedef {import('./components/exit-this-page/exit-this-page.mjs').ExitThisPageTranslations} ExitThisPageTranslations
+ * @typedef {import('./components/file-upload/file-upload.mjs').FileUploadConfig} FileUploadConfig
+ * @typedef {import('./components/file-upload/file-upload.mjs').FileUploadTranslations} FileUploadTranslations
* @typedef {import('./components/notification-banner/notification-banner.mjs').NotificationBannerConfig} NotificationBannerConfig
* @typedef {import('./components/password-input/password-input.mjs').PasswordInputConfig} PasswordInputConfig
*/
Action run for 980b1c6fbc1bc170d03614183e989902a07b9a4f
I've started testing this today. But I've only tested with Dragon so far.
Unfortunately it doesn't work. Roughly 9 out of 10 times saying "click choose file" or "click button" only focused on the button but didn't open the file dialog. And weirdly 1 out of 10 times it did. But I didn't do anything differently between when it did and when it didn't.
When we've figured out why this is happening I will test again, including in other assistive technologies.
Rebased this whilst I do some local testing
Just in case it's helpful - we had a styled upload button on the passport service - which has presumably been tested by DAC multiple times.
@edwardhorsford Do you know if it was tested in Dragon NaturallySpeaking and, if so, whether it exhibited the same issues described by @selfthinker?
@edwardhorsford Do you know if it was tested in Dragon NaturallySpeaking and, if so, whether it exhibited the same issues described by @selfthinker?
I assume it was tested with it as DAC did the testing. But I don't recall more than that. You could try on the live service to see how it works now...
Adding aria-hidden
to the input is apparently ignored by Chromium and raises an error in the console.
Blocked
aria-hidden
on a<input>
element because the element that just received focus must not be hidden from assistive technology users. Avoid usingaria-hidden
on a focused element or its ancestor. Consider using theinert
attribute instead, which will also prevent focus. For more details, see thearia-hidden
section of the WAI-ARIA specification at https://w3c.github.io/aria/#aria-hidden.
tabindex="-1"
alone probably suffices for stopping tabbing but I'm not sure if it can prevent keyboard navigation entirely, especially of the kind that screenreaders do which is on a level not easily detected by webpages.
DAC's solution was using the disabled
attribute.
But they were partly using it to show the uploaded file.
As part of post-audit investigations, a spike into layering a new UI onto the File upload component that should be a bit more friendly to assistive technologies. https://github.com/alphagov/govuk-design-system/issues/4031
Note: This hasn't actually been tested across browsers and ATs yet,
Changes
multiple
,accepts
andcapture
attributes; etc.)Thoughts