-
Notifications
You must be signed in to change notification settings - Fork 322
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Create password input component #4442
Conversation
📋 StatsFile sizes
Modules
View stats and visualisations on the review app Action run for 2b5d976 |
83a98bb
to
09eb3e6
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have loads to add! I haven't looked at the code in great depth as this is just a prototype but it all seems to work as expected.
The button thing is tricky but not one for right now, I agree. It looks disturbing enough that it's a problem so we should solve it eventually.
From memory, the reason the original was designed the way it was was to distinguish between other potential buttons in a journey. A secondary button that's adjusted to line up with the input is probably fine. From poking around in DI's journeies, it looks like this is only used on pages where there's a primary button as a CTA.
Personally I wonder if we should consider our button ecosystem and see if we should add to it. I wonder if this is something we ask from the working group during the first round.
I've also heard rumours that the appearance of the secondary button in the Design System isn't favoured because it looks like it might be disabled (given it's all light grey / monochrome). I can't be sure of the veracity of said rumours, but it seems like a fair criticism of the current design. |
A couple of extra things from chatting to @andysellick:
|
I think the exception to this would be when setting the password for another user, e.g. as part of user management in a case working or admin system – in which case the I think it does make sense to default to
The I’d say spellcheck is implicitly off too, based on this part of the spec (emphasis mine):
If we know there are browsers that don’t follow the spec correctly on either of those attributes (i.e. they incorrectly autocapitalize or spellcheck them) then we could consider adding them. Otherwise, we shouldn't omit them as they shouldn't be necessary. |
User agents use their own heuristics to decide that a password input should be automatically fillable (e.g. based on the input type or the associated label) and many will still present the option to autofill even if the Given the autofill behaviour is almost always going to be presented to a user anyway, my thinking is that it's better to be explicit, rather than omitting it and potentially presenting the wrong thing (e.g. the option to autofill a new password field).
The attributes are primarily to avoid those issues when the password has been made visible, such as if the toggle feature is being used for data entry and not just reviewing input after the fact. |
Just to make sure we're on the same page, happy with defaulting to If we really think constraining to only
Ah, of course! 🤦🏻 That makes sense, thanks for explaining. It might be worth adding a comment along those lines? |
Yeah, fair point on the boolean parameter. Regarding constraints, I guess the question is what other
On that basis I personally wouldn't have a problem with constraining the value to being one of |
485daff
to
5ef295a
Compare
JavaScript changes to npm packagediff --git a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
index 4fa94bfc7..a9132bf77 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(e, t) {
- const n = e ? e.trim() : "";
- let i, s = null == t ? void 0 : t.type;
- switch (s || (["true", "false"].includes(n) && (s = "boolean"), n.length > 0 && isFinite(Number(n)) && (s = "number")), s) {
+ const s = e ? e.trim() : "";
+ let n, i = null == t ? void 0 : t.type;
+ switch (i || (["true", "false"].includes(s) && (i = "boolean"), s.length > 0 && isFinite(Number(s)) && (i = "number")), i) {
case "boolean":
- i = "true" === n;
+ n = "true" === s;
break;
case "number":
- i = Number(n);
+ n = Number(s);
break;
default:
- i = e
+ n = e
}
- return i
+ return n
}
function mergeConfigs(...e) {
const t = {};
- for (const n of e)
- for (const e of Object.keys(n)) {
- const i = t[e],
- s = n[e];
- isObject(i) && isObject(s) ? t[e] = mergeConfigs(i, s) : t[e] = s
+ for (const s of e)
+ for (const e of Object.keys(s)) {
+ const n = t[e],
+ i = s[e];
+ isObject(n) && isObject(i) ? t[e] = mergeConfigs(n, i) : t[e] = i
}
return t
}
-function extractConfigByNamespace(e, t, n) {
- const i = e.schema.properties[n];
- if ("object" !== (null == i ? void 0 : i.type)) return;
- const s = {
- [n]: {}
+function extractConfigByNamespace(e, t, s) {
+ const n = e.schema.properties[s];
+ if ("object" !== (null == n ? void 0 : n.type)) return;
+ const i = {
+ [s]: {}
};
for (const [o, r] of Object.entries(t)) {
- let e = s;
+ let e = i;
const t = o.split(".");
- for (const [i, s] of t.entries()) "object" == typeof e && (i < t.length - 1 ? (isObject(e[s]) || (e[s] = {}), e = e[s]) : o !== n && (e[s] = normaliseString(r)))
+ for (const [n, i] of t.entries()) "object" == typeof e && (n < t.length - 1 ? (isObject(e[i]) || (e[i] = {}), e = e[i]) : o !== s && (e[i] = normaliseString(r)))
}
- return s[n]
+ return i[s]
}
function getFragmentFromUrl(e) {
@@ -54,20 +54,20 @@ function getBreakpoint(e) {
}
function setFocus(e, t = {}) {
- var n;
- const i = e.getAttribute("tabindex");
+ var s;
+ const n = e.getAttribute("tabindex");
function onBlur() {
- var n;
- null == (n = t.onBlur) || n.call(e), i || e.removeAttribute("tabindex")
+ var s;
+ null == (s = t.onBlur) || s.call(e), n || e.removeAttribute("tabindex")
}
- i || e.setAttribute("tabindex", "-1"), e.addEventListener("focus", (function() {
+ n || e.setAttribute("tabindex", "-1"), e.addEventListener("focus", (function() {
e.addEventListener("blur", onBlur, {
once: !0
})
}), {
once: !0
- }), null == (n = t.onBeforeFocus) || n.call(e), e.focus()
+ }), null == (s = t.onBeforeFocus) || s.call(e), e.focus()
}
function isSupported(e = document.body) {
@@ -81,9 +81,9 @@ function isObject(e) {
}
function normaliseDataset(e, t) {
- const n = {};
- for (const [i, s] of Object.entries(e.schema.properties)) i in t && (n[i] = normaliseString(t[i], s)), "object" === (null == s ? void 0 : s.type) && (n[i] = extractConfigByNamespace(e, t, i));
- return n
+ const s = {};
+ for (const [n, i] of Object.entries(e.schema.properties)) n in t && (s[n] = normaliseString(t[n], i)), "object" === (null == i ? void 0 : i.type) && (s[n] = extractConfigByNamespace(e, t, n));
+ return s
}
class GOVUKFrontendError extends Error {
constructor(...e) {
@@ -106,12 +106,12 @@ class ElementError extends GOVUKFrontendError {
let t = "string" == typeof e ? e : "";
if ("object" == typeof e) {
const {
- componentName: n,
- identifier: i,
- element: s,
+ componentName: s,
+ identifier: n,
+ element: i,
expectedType: o
} = e;
- t = `${n}: ${i}`, t += s ? ` is not of type ${null!=o?o:"HTMLElement"}` : " not found"
+ t = `${s}: ${n}`, t += i ? ` is not of type ${null!=o?o:"HTMLElement"}` : " not found"
}
super(t), this.name = "ElementError"
}
@@ -126,31 +126,31 @@ class GOVUKFrontendComponent {
}
class I18n {
constructor(e = {}, t = {}) {
- var n;
- this.translations = void 0, this.locale = void 0, this.translations = e, this.locale = null != (n = t.locale) ? n : document.documentElement.lang || "en"
+ var s;
+ this.translations = void 0, this.locale = void 0, this.translations = e, this.locale = null != (s = t.locale) ? s : document.documentElement.lang || "en"
}
t(e, t) {
if (!e) throw new Error("i18n: lookup key missing");
- let n = this.translations[e];
- if ("number" == typeof(null == t ? void 0 : t.count) && "object" == typeof n) {
- const i = n[this.getPluralSuffix(e, t.count)];
- i && (n = i)
+ let s = this.translations[e];
+ if ("number" == typeof(null == t ? void 0 : t.count) && "object" == typeof s) {
+ const n = s[this.getPluralSuffix(e, t.count)];
+ n && (s = n)
}
- if ("string" == typeof n) {
- if (n.match(/%{(.\S+)}/)) {
+ if ("string" == typeof s) {
+ if (s.match(/%{(.\S+)}/)) {
if (!t) throw new Error("i18n: cannot replace placeholders in string if no option data provided");
- return this.replacePlaceholders(n, t)
+ return this.replacePlaceholders(s, t)
}
- return n
+ return s
}
return e
}
replacePlaceholders(e, t) {
- const n = Intl.NumberFormat.supportedLocalesOf(this.locale).length ? new Intl.NumberFormat(this.locale) : void 0;
- return e.replace(/%{(.\S+)}/g, (function(e, i) {
- if (Object.prototype.hasOwnProperty.call(t, i)) {
- const e = t[i];
- return !1 === e || "number" != typeof e && "string" != typeof e ? "" : "number" == typeof e ? n ? n.format(e) : `${e}` : e
+ const s = Intl.NumberFormat.supportedLocalesOf(this.locale).length ? new Intl.NumberFormat(this.locale) : void 0;
+ return e.replace(/%{(.\S+)}/g, (function(e, n) {
+ if (Object.prototype.hasOwnProperty.call(t, n)) {
+ const e = t[n];
+ return !1 === e || "number" != typeof e && "string" != typeof e ? "" : "number" == typeof e ? s ? s.format(e) : `${e}` : e
}
throw new Error(`i18n: no data found to replace ${e} placeholder in string`)
}))
@@ -160,11 +160,11 @@ class I18n {
}
getPluralSuffix(e, t) {
if (t = Number(t), !isFinite(t)) return "other";
- const n = this.translations[e],
- i = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(t) : this.selectPluralFormUsingFallbackRules(t);
- if ("object" == typeof n) {
- if (i in n) return i;
- if ("other" in n) return console.warn(`i18n: Missing plural form ".${i}" for "${this.locale}" locale. Falling back to ".other".`), "other"
+ const s = this.translations[e],
+ n = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(t) : this.selectPluralFormUsingFallbackRules(t);
+ 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"
}
throw new Error(`i18n: Plural form ".other" is required for "${this.locale}" locale`)
}
@@ -176,8 +176,8 @@ class I18n {
getPluralRulesForLocale() {
const e = this.locale.split("-")[0];
for (const t in I18n.pluralRulesMap) {
- const n = I18n.pluralRulesMap[t];
- if (n.includes(this.locale) || n.includes(e)) return t
+ const s = I18n.pluralRulesMap[t];
+ if (s.includes(this.locale) || s.includes(e)) return t
}
}
}
@@ -199,29 +199,29 @@ I18n.pluralRulesMap = {
irish: e => 1 === e ? "one" : 2 === e ? "two" : e >= 3 && e <= 6 ? "few" : e >= 7 && e <= 10 ? "many" : "other",
russian(e) {
const t = e % 100,
- n = t % 10;
- return 1 === n && 11 !== t ? "one" : n >= 2 && n <= 4 && !(t >= 12 && t <= 14) ? "few" : 0 === n || n >= 5 && n <= 9 || t >= 11 && t <= 14 ? "many" : "other"
+ s = t % 10;
+ return 1 === s && 11 !== t ? "one" : s >= 2 && s <= 4 && !(t >= 12 && t <= 14) ? "few" : 0 === s || s >= 5 && s <= 9 || t >= 11 && t <= 14 ? "many" : "other"
},
scottish: e => 1 === e || 11 === e ? "one" : 2 === e || 12 === e ? "two" : e >= 3 && e <= 10 || e >= 13 && e <= 19 ? "few" : "other",
spanish: e => 1 === e ? "one" : e % 1e6 == 0 && 0 !== e ? "many" : "other",
welsh: e => 0 === e ? "zero" : 1 === e ? "one" : 2 === e ? "two" : 3 === e ? "few" : 6 === e ? "many" : "other"
};
class Accordion extends GOVUKFrontendComponent {
- constructor(t, n = {}) {
+ constructor(t, s = {}) {
if (super(), this.$module = void 0, 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.browserSupportsSessionStorage = !1, this.$showAllButton = null, this.$showAllIcon = null, this.$showAllText = null, !(t instanceof HTMLElement)) throw new ElementError({
componentName: "Accordion",
element: t,
identifier: "Root element (`$module`)"
});
- this.$module = t, this.config = mergeConfigs(Accordion.defaults, n, normaliseDataset(Accordion, t.dataset)), this.i18n = new I18n(this.config.i18n);
- const i = this.$module.querySelectorAll(`.${this.sectionClass}`);
- if (!i.length) throw new ElementError({
+ this.$module = t, this.config = mergeConfigs(Accordion.defaults, s, normaliseDataset(Accordion, t.dataset)), this.i18n = new I18n(this.config.i18n);
+ const n = this.$module.querySelectorAll(`.${this.sectionClass}`);
+ if (!n.length) throw new ElementError({
componentName: "Accordion",
identifier: `Sections (\`<div class="${this.sectionClass}">\`)`
});
- this.$sections = i, this.browserSupportsSessionStorage = e.checkForSessionStorage(), this.initControls(), this.initSectionHeaders();
- const s = this.checkIfAllSectionsOpen();
- this.updateShowAllButton(s)
+ this.$sections = n, this.browserSupportsSessionStorage = e.checkForSessionStorage(), this.initControls(), this.initSectionHeaders();
+ const i = this.checkIfAllSectionsOpen();
+ this.updateShowAllButton(i)
}
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);
@@ -230,53 +230,53 @@ class Accordion extends GOVUKFrontendComponent {
}
initSectionHeaders() {
this.$sections.forEach(((e, t) => {
- const n = e.querySelector(`.${this.sectionHeaderClass}`);
- if (!n) throw new ElementError({
+ const s = e.querySelector(`.${this.sectionHeaderClass}`);
+ if (!s) throw new ElementError({
componentName: "Accordion",
identifier: `Section headers (\`<div class="${this.sectionHeaderClass}">\`)`
});
- this.constructHeaderMarkup(n, t), this.setExpanded(this.isExpanded(e), e), n.addEventListener("click", (() => this.onSectionToggle(e))), this.setInitialState(e)
+ this.constructHeaderMarkup(s, t), this.setExpanded(this.isExpanded(e), e), s.addEventListener("click", (() => this.onSectionToggle(e))), this.setInitialState(e)
}))
}
constructHeaderMarkup(e, t) {
- const n = e.querySelector(`.${this.sectionButtonClass}`),
- i = e.querySelector(`.${this.sectionHeadingClass}`),
- s = e.querySelector(`.${this.sectionSummaryClass}`);
- if (!i) throw new ElementError({
+ const s = e.querySelector(`.${this.sectionButtonClass}`),
+ n = e.querySelector(`.${this.sectionHeadingClass}`),
+ i = e.querySelector(`.${this.sectionSummaryClass}`);
+ if (!n) throw new ElementError({
componentName: "Accordion",
identifier: `Section heading (\`.${this.sectionHeadingClass}\`)`
});
- if (!n) throw new ElementError({
+ if (!s) throw new ElementError({
componentName: "Accordion",
identifier: `Section button placeholder (\`<span class="${this.sectionButtonClass}">\`)`
});
const o = document.createElement("button");
o.setAttribute("type", "button"), o.setAttribute("aria-controls", `${this.$module.id}-content-${t+1}`);
- for (const d of Array.from(n.attributes)) "id" !== d.nodeName && o.setAttribute(d.nodeName, `${d.nodeValue}`);
+ for (const d of Array.from(s.attributes)) "id" !== d.nodeName && o.setAttribute(d.nodeName, `${d.nodeValue}`);
const r = document.createElement("span");
- r.classList.add(this.sectionHeadingTextClass), r.id = n.id;
+ r.classList.add(this.sectionHeadingTextClass), r.id = s.id;
const a = document.createElement("span");
- a.classList.add(this.sectionHeadingTextFocusClass), r.appendChild(a), a.innerHTML = n.innerHTML;
+ a.classList.add(this.sectionHeadingTextFocusClass), r.appendChild(a), a.innerHTML = s.innerHTML;
const l = document.createElement("span");
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), c.appendChild(u), h.classList.add(this.sectionShowHideTextClass), c.appendChild(h), o.appendChild(r), o.appendChild(this.getButtonPunctuationEl()), null != s && s.parentNode) {
+ if (u.classList.add(this.upChevronIconClass), c.appendChild(u), h.classList.add(this.sectionShowHideTextClass), c.appendChild(h), o.appendChild(r), o.appendChild(this.getButtonPunctuationEl()), null != i && i.parentNode) {
const e = document.createElement("span"),
t = document.createElement("span");
t.classList.add(this.sectionSummaryFocusClass), e.appendChild(t);
- for (const n of Array.from(s.attributes)) e.setAttribute(n.nodeName, `${n.nodeValue}`);
- t.innerHTML = s.innerHTML, s.parentNode.replaceChild(e, s), o.appendChild(e), o.appendChild(this.getButtonPunctuationEl())
+ for (const s of Array.from(i.attributes)) e.setAttribute(s.nodeName, `${s.nodeValue}`);
+ t.innerHTML = i.innerHTML, i.parentNode.replaceChild(e, i), o.appendChild(e), o.appendChild(this.getButtonPunctuationEl())
}
- o.appendChild(l), i.removeChild(n), i.appendChild(o)
+ o.appendChild(l), n.removeChild(s), n.appendChild(o)
}
onBeforeMatch(e) {
const t = e.target;
if (!(t instanceof Element)) return;
- const n = t.closest(`.${this.sectionClass}`);
- n && this.setExpanded(!0, n)
+ const s = t.closest(`.${this.sectionClass}`);
+ s && this.setExpanded(!0, s)
}
onSectionToggle(e) {
const t = this.isExpanded(e);
@@ -289,24 +289,24 @@ class Accordion extends GOVUKFrontendComponent {
})), this.updateShowAllButton(e)
}
setExpanded(e, t) {
- const n = t.querySelector(`.${this.upChevronIconClass}`),
- i = t.querySelector(`.${this.sectionShowHideTextClass}`),
- s = t.querySelector(`.${this.sectionButtonClass}`),
+ const s = t.querySelector(`.${this.upChevronIconClass}`),
+ n = t.querySelector(`.${this.sectionShowHideTextClass}`),
+ i = t.querySelector(`.${this.sectionButtonClass}`),
o = t.querySelector(`.${this.sectionContentClass}`);
if (!o) throw new ElementError({
componentName: "Accordion",
identifier: `Section content (\`<div class="${this.sectionContentClass}">\`)`
});
- if (!n || !i || !s) return;
+ if (!s || !n || !i) return;
const r = e ? this.i18n.t("hideSection") : this.i18n.t("showSection");
- i.textContent = r, s.setAttribute("aria-expanded", `${e}`);
+ n.textContent = r, i.setAttribute("aria-expanded", `${e}`);
const a = [],
l = t.querySelector(`.${this.sectionHeadingTextClass}`);
l && a.push(`${l.textContent}`.trim());
const c = t.querySelector(`.${this.sectionSummaryClass}`);
c && a.push(`${c.textContent}`.trim());
const h = e ? this.i18n.t("hideSectionAriaLabel") : this.i18n.t("showSectionAriaLabel");
- a.push(h), s.setAttribute("aria-label", a.join(" , ")), e ? (o.removeAttribute("hidden"), t.classList.add(this.sectionExpandedClass), n.classList.remove(this.downChevronIconClass)) : (o.setAttribute("hidden", "until-found"), t.classList.remove(this.sectionExpandedClass), n.classList.add(this.downChevronIconClass));
+ a.push(h), i.setAttribute("aria-label", a.join(" , ")), e ? (o.removeAttribute("hidden"), t.classList.add(this.sectionExpandedClass), s.classList.remove(this.downChevronIconClass)) : (o.setAttribute("hidden", "until-found"), t.classList.remove(this.sectionExpandedClass), s.classList.add(this.downChevronIconClass));
const u = this.checkIfAllSectionsOpen();
this.updateShowAllButton(u)
}
@@ -324,8 +324,8 @@ class Accordion extends GOVUKFrontendComponent {
const t = e.querySelector(`.${this.sectionButtonClass}`);
if (t) {
const e = t.getAttribute("aria-controls"),
- n = t.getAttribute("aria-expanded");
- e && n && window.sessionStorage.setItem(e, n)
+ s = t.getAttribute("aria-expanded");
+ e && s && window.sessionStorage.setItem(e, s)
}
}
}
@@ -333,9 +333,9 @@ class Accordion extends GOVUKFrontendComponent {
if (this.browserSupportsSessionStorage && this.config.rememberExpanded) {
const t = e.querySelector(`.${this.sectionButtonClass}`);
if (t) {
- const n = t.getAttribute("aria-controls"),
- i = n ? window.sessionStorage.getItem(n) : null;
- null !== i && this.setExpanded("true" === i, e)
+ const s = t.getAttribute("aria-controls"),
+ n = s ? window.sessionStorage.getItem(s) : null;
+ null !== n && this.setExpanded("true" === n, e)
}
}
}
@@ -370,7 +370,7 @@ const e = {
let t;
try {
return window.sessionStorage.setItem(e, e), t = window.sessionStorage.getItem(e) === e.toString(), window.sessionStorage.removeItem(e), t
- } catch (n) {
+ } catch (s) {
return !1
}
}
@@ -396,8 +396,8 @@ class Button extends GOVUKFrontendComponent {
}
function closestAttributeValue(e, t) {
- const n = e.closest(`[${t}]`);
- return n ? n.getAttribute(t) : null
+ const s = e.closest(`[${t}]`);
+ return s ? s.getAttribute(t) : null
}
Button.moduleName = "govuk-button", Button.defaults = Object.freeze({
preventDoubleClick: !1
@@ -410,16 +410,16 @@ Button.moduleName = "govuk-button", Button.defaults = Object.freeze({
});
class CharacterCount extends GOVUKFrontendComponent {
constructor(e, t = {}) {
- var n, i;
+ var s, n;
if (super(), this.$module = void 0, 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, !(e instanceof HTMLElement)) throw new ElementError({
componentName: "Character count",
element: e,
identifier: "Root element (`$module`)"
});
- const s = e.querySelector(".govuk-js-character-count");
- if (!(s instanceof HTMLTextAreaElement || s instanceof HTMLInputElement)) throw new ElementError({
+ const i = e.querySelector(".govuk-js-character-count");
+ if (!(i instanceof HTMLTextAreaElement || i instanceof HTMLInputElement)) throw new ElementError({
componentName: "Character count",
- element: s,
+ element: i,
expectedType: "HTMLTextareaElement or HTMLInputElement",
identifier: "Form field (`.govuk-js-character-count`)"
});
@@ -430,24 +430,24 @@ class CharacterCount extends GOVUKFrontendComponent {
maxwords: void 0
}), this.config = mergeConfigs(CharacterCount.defaults, t, r, o);
const a = function(e, t) {
- const n = [];
- for (const [i, s] of Object.entries(e)) {
+ const s = [];
+ for (const [n, i] of Object.entries(e)) {
const e = [];
- if (Array.isArray(s)) {
+ if (Array.isArray(i)) {
for (const {
- required: n,
- errorMessage: i
+ required: s,
+ errorMessage: n
}
- of s) n.every((e => !!t[e])) || e.push(i);
- "anyOf" !== i || s.length - e.length >= 1 || n.push(...e)
+ of i) s.every((e => !!t[e])) || e.push(n);
+ "anyOf" !== n || i.length - e.length >= 1 || s.push(...e)
}
}
- return n
+ return s
}(CharacterCount.schema, this.config);
if (a[0]) throw new ConfigError(`Character count: ${a[0]}`);
this.i18n = new I18n(this.config.i18n, {
locale: closestAttributeValue(e, "lang")
- }), this.maxLength = null != (n = null != (i = this.config.maxwords) ? i : this.config.maxlength) ? n : 1 / 0, this.$module = e, this.$textarea = s;
+ }), this.maxLength = null != (s = null != (n = this.config.maxwords) ? n : this.config.maxlength) ? s : 1 / 0, this.$module = e, this.$textarea = i;
const l = `${this.$textarea.id}-info`,
c = document.getElementById(l);
if (!c) throw new ElementError({
@@ -504,8 +504,8 @@ class CharacterCount extends GOVUKFrontendComponent {
}
formatCountMessage(e, t) {
if (0 === e) return this.i18n.t(`${t}AtLimit`);
- const n = e < 0 ? "OverLimit" : "UnderLimit";
- return this.i18n.t(`${t}${n}`, {
+ const s = e < 0 ? "OverLimit" : "UnderLimit";
+ return this.i18n.t(`${t}${s}`, {
count: Math.abs(e)
})
}
@@ -592,10 +592,10 @@ class Checkboxes extends GOVUKFrontendComponent {
syncConditionalRevealWithInputState(e) {
const t = e.getAttribute("aria-controls");
if (!t) return;
- const n = document.getElementById(t);
- if (n && n.classList.contains("govuk-checkboxes__conditional")) {
+ const s = document.getElementById(t);
+ if (s && s.classList.contains("govuk-checkboxes__conditional")) {
const t = e.checked;
- e.setAttribute("aria-expanded", t.toString()), n.classList.toggle("govuk-checkboxes__conditional--hidden", !t)
+ e.setAttribute("aria-expanded", t.toString()), s.classList.toggle("govuk-checkboxes__conditional--hidden", !t)
}
}
unCheckAllInputsExcept(e) {
@@ -633,25 +633,25 @@ class ErrorSummary extends GOVUKFrontendComponent {
if (!(e instanceof HTMLAnchorElement)) return !1;
const t = getFragmentFromUrl(e.href);
if (!t) return !1;
- const n = document.getElementById(t);
- if (!n) return !1;
- const i = this.getAssociatedLegendOrLabel(n);
- return !!i && (i.scrollIntoView(), n.focus({
+ const s = document.getElementById(t);
+ if (!s) return !1;
+ const n = this.getAssociatedLegendOrLabel(s);
+ return !!n && (n.scrollIntoView(), s.focus({
preventScroll: !0
}), !0)
}
getAssociatedLegendOrLabel(e) {
var t;
- const n = e.closest("fieldset");
- if (n) {
- const t = n.getElementsByTagName("legend");
+ const s = e.closest("fieldset");
+ if (s) {
+ const t = s.getElementsByTagName("legend");
if (t.length) {
- const n = t[0];
- if (e instanceof HTMLInputElement && ("checkbox" === e.type || "radio" === e.type)) return n;
- const i = n.getBoundingClientRect().top,
- s = e.getBoundingClientRect();
- if (s.height && window.innerHeight) {
- if (s.top + s.height - i < window.innerHeight / 2) return n
+ const s = t[0];
+ if (e instanceof HTMLInputElement && ("checkbox" === e.type || "radio" === e.type)) return s;
+ const n = s.getBoundingClientRect().top,
+ i = e.getBoundingClientRect();
+ if (i.height && window.innerHeight) {
+ if (i.top + i.height - n < window.innerHeight / 2) return s
}
}
}
@@ -674,16 +674,16 @@ class ExitThisPage extends GOVUKFrontendComponent {
element: e,
identifier: "Root element (`$module`)"
});
- const n = e.querySelector(".govuk-exit-this-page__button");
- if (!(n instanceof HTMLAnchorElement)) throw new ElementError({
+ const s = e.querySelector(".govuk-exit-this-page__button");
+ if (!(s instanceof HTMLAnchorElement)) throw new ElementError({
componentName: "Exit this page",
- element: n,
+ element: s,
expectedType: "HTMLAnchorElement",
identifier: "Button (`.govuk-exit-this-page__button`)"
});
- this.config = mergeConfigs(ExitThisPage.defaults, t, normaliseDataset(ExitThisPage, e.dataset)), this.i18n = new I18n(this.config.i18n), this.$module = e, this.$button = n;
- const i = document.querySelector(".govuk-js-exit-this-page-skiplink");
- i instanceof HTMLAnchorElement && (this.$skiplinkButton = i), this.buildIndicator(), this.initUpdateSpan(), this.initButtonClickHandler(), "govukFrontendExitThisPageKeypress" in document.body.dataset || (document.addEventListener("keyup", this.handleKeypress.bind(this), !0), document.body.dataset.govukFrontendExitThisPageKeypress = "true"), window.addEventListener("pageshow", this.resetPage.bind(this))
+ this.config = mergeConfigs(ExitThisPage.defaults, t, normaliseDataset(ExitThisPage, e.dataset)), this.i18n = new I18n(this.config.i18n), this.$module = e, 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))
}
initUpdateSpan() {
this.$updateSpan = document.createElement("span"), this.$updateSpan.setAttribute("role", "status"), this.$updateSpan.className = "govuk-visually-hidden", this.$module.appendChild(this.$updateSpan)
@@ -754,18 +754,18 @@ class Header extends GOVUKFrontendComponent {
this.$module = e;
const t = e.querySelector(".govuk-js-header-toggle");
if (!t) return this;
- const n = t.getAttribute("aria-controls");
- if (!n) throw new ElementError({
+ const s = t.getAttribute("aria-controls");
+ if (!s) throw new ElementError({
componentName: "Header",
identifier: 'Navigation button (`<button class="govuk-js-header-toggle">`) attribute (`aria-controls`)'
});
- const i = document.getElementById(n);
- if (!i) throw new ElementError({
+ const n = document.getElementById(s);
+ if (!n) throw new ElementError({
componentName: "Header",
- element: i,
- identifier: `Navigation (\`<ul id="${n}">\`)`
+ element: n,
+ identifier: `Navigation (\`<ul id="${s}">\`)`
});
- this.$menu = i, this.$menuButton = t, this.setupResponsiveChecks(), this.$menuButton.addEventListener("click", (() => this.handleMenuButtonClick()))
+ this.$menu = n, this.$menuButton = t, this.setupResponsiveChecks(), this.$menuButton.addEventListener("click", (() => this.handleMenuButtonClick()))
}
setupResponsiveChecks() {
const e = getBreakpoint("desktop");
@@ -802,6 +802,71 @@ NotificationBanner.moduleName = "govuk-notification-banner", NotificationBanner.
}
}
});
+class PasswordInput extends GOVUKFrontendComponent {
+ constructor(e, t = {}) {
+ if (super(), this.$module = void 0, this.config = void 0, this.i18n = void 0, this.$input = void 0, this.$showHideButton = void 0, this.$screenReaderStatusMessage = void 0, !(e instanceof HTMLElement)) throw new ElementError({
+ componentName: "Password input",
+ element: e,
+ identifier: "Root element (`$module`)"
+ });
+ const s = e.querySelector(".govuk-js-password-input-input");
+ if (!(s instanceof HTMLInputElement)) throw new ElementError({
+ componentName: "Password input",
+ element: s,
+ 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 = e.querySelector(".govuk-js-password-input-toggle");
+ if (!(n instanceof HTMLButtonElement)) throw new ElementError({
+ componentName: "Password input",
+ element: n,
+ 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.$module = e, this.$input = s, this.$showHideButton = n, this.config = mergeConfigs(PasswordInput.defaults, t, normaliseDataset(PasswordInput, e.dataset)), this.i18n = new I18n(this.config.i18n, {
+ locale: closestAttributeValue(e, "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", (e => {
+ e.persisted && "password" !== this.$input.type && this.hide()
+ })), this.hide()
+ }
+ toggle(e) {
+ e.preventDefault(), "password" !== this.$input.type ? this.hide() : this.show()
+ }
+ show() {
+ this.setType("text")
+ }
+ hide() {
+ this.setType("password")
+ }
+ setType(e) {
+ if (e === this.$input.type) return;
+ this.$input.setAttribute("type", e);
+ const t = "password" === e,
+ s = t ? "show" : "hide",
+ n = t ? "passwordHidden" : "passwordShown";
+ this.$showHideButton.innerHTML = this.i18n.t(`${s}Password`), this.$showHideButton.setAttribute("aria-label", this.i18n.t(`${s}PasswordAriaLabel`)), this.$screenReaderStatusMessage.innerText = this.i18n.t(`${n}Announcement`)
+ }
+}
+PasswordInput.moduleName = "govuk-password-input", PasswordInput.defaults = Object.freeze({
+ i18n: {
+ showPassword: "Show",
+ hidePassword: "Hide",
+ showPasswordAriaLabel: "Show password",
+ hidePasswordAriaLabel: "Hide password",
+ passwordShownAnnouncement: "Your password is visible",
+ passwordHiddenAnnouncement: "Your password is hidden"
+ }
+}), PasswordInput.schema = Object.freeze({
+ properties: {
+ i18n: {
+ type: "object"
+ }
+ }
+});
class Radios extends GOVUKFrontendComponent {
constructor(e) {
if (super(), this.$module = void 0, this.$inputs = void 0, !(e instanceof HTMLElement)) throw new ElementError({
@@ -831,21 +896,21 @@ class Radios extends GOVUKFrontendComponent {
syncConditionalRevealWithInputState(e) {
const t = e.getAttribute("aria-controls");
if (!t) return;
- const n = document.getElementById(t);
- if (null != n && n.classList.contains("govuk-radios__conditional")) {
+ const s = document.getElementById(t);
+ if (null != s && s.classList.contains("govuk-radios__conditional")) {
const t = e.checked;
- e.setAttribute("aria-expanded", t.toString()), n.classList.toggle("govuk-radios__conditional--hidden", !t)
+ e.setAttribute("aria-expanded", t.toString()), s.classList.toggle("govuk-radios__conditional--hidden", !t)
}
}
handleClick(e) {
const t = e.target;
if (!(t instanceof HTMLInputElement) || "radio" !== t.type) return;
- const n = document.querySelectorAll('input[type="radio"][aria-controls]'),
- i = t.form,
- s = t.name;
- n.forEach((e => {
- const t = e.form === i;
- e.name === s && t && this.syncConditionalRevealWithInputState(e)
+ const s = document.querySelectorAll('input[type="radio"][aria-controls]'),
+ n = t.form,
+ i = t.name;
+ s.forEach((e => {
+ const t = e.form === n;
+ e.name === i && t && this.syncConditionalRevealWithInputState(e)
}))
}
}
@@ -860,17 +925,17 @@ class SkipLink extends GOVUKFrontendComponent {
identifier: "Root element (`$module`)"
});
this.$module = e;
- const n = this.$module.hash,
- i = null != (t = this.$module.getAttribute("href")) ? t : "";
- let s;
+ const s = this.$module.hash,
+ n = null != (t = this.$module.getAttribute("href")) ? t : "";
+ let i;
try {
- s = new window.URL(this.$module.href)
+ i = new window.URL(this.$module.href)
} catch (a) {
- throw new ElementError(`Skip link: Target link (\`href="${i}"\`) is invalid`)
+ throw new ElementError(`Skip link: Target link (\`href="${n}"\`) is invalid`)
}
- if (s.origin !== window.location.origin || s.pathname !== window.location.pathname) return;
- const o = getFragmentFromUrl(n);
- if (!o) throw new ElementError(`Skip link: Target link (\`href="${i}"\`) has no hash fragment`);
+ 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`);
const r = document.getElementById(o);
if (!r) throw new ElementError({
componentName: "Skip link",
@@ -901,17 +966,17 @@ class Tabs extends GOVUKFrontendComponent {
identifier: 'Links (`<a class="govuk-tabs__tab">`)'
});
this.$module = e, this.$tabs = t, this.boundTabClick = this.onTabClick.bind(this), this.boundTabKeydown = this.onTabKeydown.bind(this), this.boundOnHashChange = this.onHashChange.bind(this);
- const n = this.$module.querySelector(".govuk-tabs__list"),
- i = this.$module.querySelectorAll("li.govuk-tabs__list-item");
- if (!n) throw new ElementError({
+ const s = this.$module.querySelector(".govuk-tabs__list"),
+ n = this.$module.querySelectorAll("li.govuk-tabs__list-item");
+ if (!s) throw new ElementError({
componentName: "Tabs",
identifier: 'List (`<ul class="govuk-tabs__list">`)'
});
- if (!i.length) throw new ElementError({
+ if (!n.length) throw new ElementError({
componentName: "Tabs",
identifier: 'List items (`<li class="govuk-tabs__list-item">`)'
});
- this.$tabList = n, this.$tabListItems = i, this.setupResponsiveChecks()
+ this.$tabList = s, this.$tabListItems = n, this.setupResponsiveChecks()
}
setupResponsiveChecks() {
const e = getBreakpoint("tablet");
@@ -947,8 +1012,8 @@ class Tabs extends GOVUKFrontendComponent {
t = this.getTab(e);
if (!t) return;
if (this.changingHash) return void(this.changingHash = !1);
- const n = this.getCurrentTab();
- n && (this.hideTab(n), this.showTab(t), t.focus())
+ const s = this.getCurrentTab();
+ s && (this.hideTab(s), this.showTab(t), t.focus())
}
hideTab(e) {
this.unhighlightTab(e), this.hidePanel(e)
@@ -963,8 +1028,8 @@ class Tabs extends GOVUKFrontendComponent {
const t = getFragmentFromUrl(e.href);
if (!t) return;
e.setAttribute("id", `tab_${t}`), e.setAttribute("role", "tab"), e.setAttribute("aria-controls", t), e.setAttribute("aria-selected", "false"), e.setAttribute("tabindex", "-1");
- const n = this.getPanel(e);
- n && (n.setAttribute("role", "tabpanel"), n.setAttribute("aria-labelledby", e.id), n.classList.add(this.jsHiddenClass))
+ const s = this.getPanel(e);
+ s && (s.setAttribute("role", "tabpanel"), s.setAttribute("aria-labelledby", e.id), s.classList.add(this.jsHiddenClass))
}
unsetAttributes(e) {
e.removeAttribute("id"), e.removeAttribute("role"), e.removeAttribute("aria-controls"), e.removeAttribute("aria-selected"), e.removeAttribute("tabindex");
@@ -973,14 +1038,14 @@ class Tabs extends GOVUKFrontendComponent {
}
onTabClick(e) {
const t = this.getCurrentTab(),
- n = e.currentTarget;
- t && n instanceof HTMLAnchorElement && (e.preventDefault(), this.hideTab(t), this.showTab(n), this.createHistoryEntry(n))
+ s = e.currentTarget;
+ t && s instanceof HTMLAnchorElement && (e.preventDefault(), this.hideTab(t), this.showTab(s), this.createHistoryEntry(s))
}
createHistoryEntry(e) {
const t = this.getPanel(e);
if (!t) return;
- const n = t.id;
- t.id = "", this.changingHash = !0, window.location.hash = n, t.id = n
+ const s = t.id;
+ t.id = "", this.changingHash = !0, window.location.hash = s, t.id = s
}
onTabKeydown(e) {
switch (e.key) {
@@ -1002,16 +1067,16 @@ class Tabs extends GOVUKFrontendComponent {
if (null == e || !e.parentElement) return;
const t = e.parentElement.nextElementSibling;
if (!t) return;
- const n = t.querySelector("a.govuk-tabs__tab");
- n && (this.hideTab(e), this.showTab(n), n.focus(), this.createHistoryEntry(n))
+ const s = t.querySelector("a.govuk-tabs__tab");
+ s && (this.hideTab(e), this.showTab(s), s.focus(), this.createHistoryEntry(s))
}
activatePreviousTab() {
const e = this.getCurrentTab();
if (null == e || !e.parentElement) return;
const t = e.parentElement.previousElementSibling;
if (!t) return;
- const n = t.querySelector("a.govuk-tabs__tab");
- n && (this.hideTab(e), this.showTab(n), n.focus(), this.createHistoryEntry(n))
+ const s = t.querySelector("a.govuk-tabs__tab");
+ s && (this.hideTab(e), this.showTab(s), s.focus(), this.createHistoryEntry(s))
}
getPanel(e) {
const t = getFragmentFromUrl(e.href);
@@ -1039,7 +1104,7 @@ class Tabs extends GOVUKFrontendComponent {
function initAll(e) {
var t;
if (e = void 0 !== e ? e : {}, !isSupported()) return void console.log(new SupportError);
- const n = [
+ const s = [
[Accordion, e.accordion],
[Button, e.button],
[CharacterCount, e.characterCount],
@@ -1048,17 +1113,18 @@ function initAll(e) {
[ExitThisPage, e.exitThisPage],
[Header],
[NotificationBanner, e.notificationBanner],
+ [PasswordInput, e.passwordInput],
[Radios],
[SkipLink],
[Tabs]
],
- i = null != (t = e.scope) ? t : document;
- n.forEach((([e, t]) => {
- i.querySelectorAll(`[data-module="${e.moduleName}"]`).forEach((n => {
+ n = null != (t = e.scope) ? t : document;
+ s.forEach((([e, t]) => {
+ n.querySelectorAll(`[data-module="${e.moduleName}"]`).forEach((s => {
try {
- "defaults" in e ? new e(n, t) : new e(n)
- } catch (i) {
- console.log(i)
+ "defaults" in e ? new e(s, t) : new e(s)
+ } catch (n) {
+ console.log(n)
}
}))
}))
@@ -1073,6 +1139,7 @@ export {
ExitThisPage,
Header,
NotificationBanner,
+ PasswordInput,
Radios,
SkipLink,
Tabs,
Action run for 2b5d976 |
Stylesheets changes to npm packagediff --git a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.css b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.css
index fa1f74dc6..2c37191c5 100644
--- a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.css
+++ b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.css
@@ -4643,6 +4643,37 @@ only screen and (min-resolution:2dppx) {
margin-bottom: 0
}
+@media (min-width:20em) {
+ .govuk-password-input__wrapper {
+ flex-direction: row;
+ align-items: flex-start
+ }
+}
+
+.govuk-password-input__input::-ms-reveal {
+ display: none
+}
+
+.govuk-password-input__toggle {
+ margin-top: 5px;
+ margin-bottom: 0
+}
+
+.govuk-password-input__toggle[hidden] {
+ display: none
+}
+
+@media (min-width:20em) {
+ .govuk-password-input__toggle {
+ width: auto;
+ flex-grow: 1;
+ flex-shrink: 0;
+ flex-basis: 5em;
+ margin-top: 0;
+ margin-left: 5px
+ }
+}
+
.govuk-tag {
font-family: GDS Transport, arial, sans-serif;
-webkit-font-smoothing: antialiased;
Action run for 2b5d976 |
Other changes to npm packagediff --git a/packages/govuk-frontend/dist/govuk/all.bundle.js b/packages/govuk-frontend/dist/govuk/all.bundle.js
index a83698cb6..1f067d59e 100644
--- a/packages/govuk-frontend/dist/govuk/all.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/all.bundle.js
@@ -1758,6 +1758,160 @@
}
});
+ /**
+ * Password input component
+ *
+ * @preserve
+ */
+ class PasswordInput extends GOVUKFrontendComponent {
+ /**
+ * @param {Element | null} $module - HTML element to use for password input
+ * @param {PasswordInputConfig} [config] - Password input config
+ */
+ constructor($module, config = {}) {
+ super();
+ this.$module = void 0;
+ this.config = void 0;
+ this.i18n = void 0;
+ this.$input = void 0;
+ this.$showHideButton = void 0;
+ this.$screenReaderStatusMessage = void 0;
+ if (!($module instanceof HTMLElement)) {
+ throw new ElementError({
+ componentName: 'Password input',
+ element: $module,
+ identifier: 'Root element (`$module`)'
+ });
+ }
+ const $input = $module.querySelector('.govuk-js-password-input-input');
+ if (!($input instanceof HTMLInputElement)) {
+ throw new ElementError({
+ componentName: 'Password input',
+ element: $input,
+ expectedType: 'HTMLInputElement',
+ identifier: 'Form field (`.govuk-js-password-input-input`)'
+ });
+ }
+ if ($input.type !== 'password') {
+ throw new ElementError('Password input: Form field (`.govuk-js-password-input-input`) must be of type `password`.');
+ }
+ const $showHideButton = $module.querySelector('.govuk-js-password-input-toggle');
+ if (!($showHideButton instanceof HTMLButtonElement)) {
+ throw new ElementError({
+ componentName: 'Password input',
+ element: $showHideButton,
+ expectedType: 'HTMLButtonElement',
+ identifier: 'Button (`.govuk-js-password-input-toggle`)'
+ });
+ }
+ if ($showHideButton.type !== 'button') {
+ throw new ElementError('Password input: Button (`.govuk-js-password-input-toggle`) must be of type `button`.');
+ }
+ this.$module = $module;
+ this.$input = $input;
+ this.$showHideButton = $showHideButton;
+ this.config = mergeConfigs(PasswordInput.defaults, config, normaliseDataset(PasswordInput, $module.dataset));
+ this.i18n = new I18n(this.config.i18n, {
+ locale: closestAttributeValue($module, 'lang')
+ });
+ this.$showHideButton.removeAttribute('hidden');
+ const $screenReaderStatusMessage = document.createElement('div');
+ $screenReaderStatusMessage.className = 'govuk-password-input__sr-status govuk-visually-hidden';
+ $screenReaderStatusMessage.setAttribute('aria-live', 'polite');
+ this.$screenReaderStatusMessage = $screenReaderStatusMessage;
+ this.$input.insertAdjacentElement('afterend', $screenReaderStatusMessage);
+ this.$showHideButton.addEventListener('click', this.toggle.bind(this));
+ if (this.$input.form) {
+ this.$input.form.addEventListener('submit', () => this.hide());
+ }
+ window.addEventListener('pageshow', event => {
+ if (event.persisted && this.$input.type !== 'password') {
+ this.hide();
+ }
+ });
+ this.hide();
+ }
+ toggle(event) {
+ event.preventDefault();
+ if (this.$input.type === 'password') {
+ this.show();
+ return;
+ }
+ this.hide();
+ }
+ show() {
+ this.setType('text');
+ }
+ hide() {
+ this.setType('password');
+ }
+ setType(type) {
+ if (type === this.$input.type) {
+ return;
+ }
+ this.$input.setAttribute('type', type);
+ const isHidden = type === 'password';
+ const prefixButton = isHidden ? 'show' : 'hide';
+ const prefixStatus = isHidden ? 'passwordHidden' : 'passwordShown';
+ this.$showHideButton.innerHTML = this.i18n.t(`${prefixButton}Password`);
+ this.$showHideButton.setAttribute('aria-label', this.i18n.t(`${prefixButton}PasswordAriaLabel`));
+ this.$screenReaderStatusMessage.innerText = this.i18n.t(`${prefixStatus}Announcement`);
+ }
+ }
+
+ /**
+ * Password input config
+ *
+ * @typedef {object} PasswordInputConfig
+ * @property {PasswordInputTranslations} [i18n=PasswordInput.defaults.i18n] - Password input translations
+ */
+
+ /**
+ * Password input translations
+ *
+ * @see {@link PasswordInput.defaults.i18n}
+ * @typedef {object} PasswordInputTranslations
+ *
+ * Messages displayed to the user indicating the state of the show/hide toggle.
+ * @property {string} [showPassword] - Visible text of the button when the
+ * password is currently hidden. HTML is acceptable.
+ * @property {string} [hidePassword] - Visible text of the button when the
+ * password is currently visible. HTML is acceptable.
+ * @property {string} [showPasswordAriaLabel] - aria-label of the button when
+ * the password is currently hidden. Plain text only.
+ * @property {string} [hidePasswordAriaLabel] - aria-label of the button when
+ * the password is currently visible. Plain text only.
+ * @property {string} [passwordShownAnnouncement] - Screen reader
+ * announcement to make when the password has just become visible.
+ * Plain text only.
+ * @property {string} [passwordHiddenAnnouncement] - Screen reader
+ * announcement to make when the password has just been hidden.
+ * Plain text only.
+ */
+
+ /**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ * @typedef {import('../../i18n.mjs').TranslationPluralForms} TranslationPluralForms
+ */
+ PasswordInput.moduleName = 'govuk-password-input';
+ PasswordInput.defaults = Object.freeze({
+ i18n: {
+ showPassword: 'Show',
+ hidePassword: 'Hide',
+ showPasswordAriaLabel: 'Show password',
+ hidePasswordAriaLabel: 'Hide password',
+ passwordShownAnnouncement: 'Your password is visible',
+ passwordHiddenAnnouncement: 'Your password is hidden'
+ }
+ });
+ PasswordInput.schema = Object.freeze({
+ properties: {
+ i18n: {
+ type: 'object'
+ }
+ }
+ });
+
/**
* Radios component
*
@@ -2215,7 +2369,7 @@
console.log(new SupportError());
return;
}
- const components = [[Accordion, config.accordion], [Button, config.button], [CharacterCount, config.characterCount], [Checkboxes], [ErrorSummary, config.errorSummary], [ExitThisPage, config.exitThisPage], [Header], [NotificationBanner, config.notificationBanner], [Radios], [SkipLink], [Tabs]];
+ 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], [SkipLink], [Tabs]];
const $scope = (_config$scope = config.scope) != null ? _config$scope : document;
components.forEach(([Component, config]) => {
const $elements = $scope.querySelectorAll(`[data-module="${Component.moduleName}"]`);
@@ -2239,6 +2393,7 @@
* @property {ErrorSummaryConfig} [errorSummary] - Error Summary config
* @property {ExitThisPageConfig} [exitThisPage] - Exit This Page config
* @property {NotificationBannerConfig} [notificationBanner] - Notification Banner config
+ * @property {PasswordInputConfig} [passwordInput] - Password input config
*/
/**
@@ -2253,6 +2408,7 @@
* @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/notification-banner/notification-banner.mjs').NotificationBannerConfig} NotificationBannerConfig
+ * @typedef {import('./components/password-input/password-input.mjs').PasswordInputConfig} PasswordInputConfig
*/
/**
@@ -2269,6 +2425,7 @@
exports.ExitThisPage = ExitThisPage;
exports.Header = Header;
exports.NotificationBanner = NotificationBanner;
+ exports.PasswordInput = PasswordInput;
exports.Radios = Radios;
exports.SkipLink = SkipLink;
exports.Tabs = Tabs;
diff --git a/packages/govuk-frontend/dist/govuk/all.bundle.mjs b/packages/govuk-frontend/dist/govuk/all.bundle.mjs
index 7662df517..838129163 100644
--- a/packages/govuk-frontend/dist/govuk/all.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/all.bundle.mjs
@@ -1752,6 +1752,160 @@ NotificationBanner.schema = Object.freeze({
}
});
+/**
+ * Password input component
+ *
+ * @preserve
+ */
+class PasswordInput extends GOVUKFrontendComponent {
+ /**
+ * @param {Element | null} $module - HTML element to use for password input
+ * @param {PasswordInputConfig} [config] - Password input config
+ */
+ constructor($module, config = {}) {
+ super();
+ this.$module = void 0;
+ this.config = void 0;
+ this.i18n = void 0;
+ this.$input = void 0;
+ this.$showHideButton = void 0;
+ this.$screenReaderStatusMessage = void 0;
+ if (!($module instanceof HTMLElement)) {
+ throw new ElementError({
+ componentName: 'Password input',
+ element: $module,
+ identifier: 'Root element (`$module`)'
+ });
+ }
+ const $input = $module.querySelector('.govuk-js-password-input-input');
+ if (!($input instanceof HTMLInputElement)) {
+ throw new ElementError({
+ componentName: 'Password input',
+ element: $input,
+ expectedType: 'HTMLInputElement',
+ identifier: 'Form field (`.govuk-js-password-input-input`)'
+ });
+ }
+ if ($input.type !== 'password') {
+ throw new ElementError('Password input: Form field (`.govuk-js-password-input-input`) must be of type `password`.');
+ }
+ const $showHideButton = $module.querySelector('.govuk-js-password-input-toggle');
+ if (!($showHideButton instanceof HTMLButtonElement)) {
+ throw new ElementError({
+ componentName: 'Password input',
+ element: $showHideButton,
+ expectedType: 'HTMLButtonElement',
+ identifier: 'Button (`.govuk-js-password-input-toggle`)'
+ });
+ }
+ if ($showHideButton.type !== 'button') {
+ throw new ElementError('Password input: Button (`.govuk-js-password-input-toggle`) must be of type `button`.');
+ }
+ this.$module = $module;
+ this.$input = $input;
+ this.$showHideButton = $showHideButton;
+ this.config = mergeConfigs(PasswordInput.defaults, config, normaliseDataset(PasswordInput, $module.dataset));
+ this.i18n = new I18n(this.config.i18n, {
+ locale: closestAttributeValue($module, 'lang')
+ });
+ this.$showHideButton.removeAttribute('hidden');
+ const $screenReaderStatusMessage = document.createElement('div');
+ $screenReaderStatusMessage.className = 'govuk-password-input__sr-status govuk-visually-hidden';
+ $screenReaderStatusMessage.setAttribute('aria-live', 'polite');
+ this.$screenReaderStatusMessage = $screenReaderStatusMessage;
+ this.$input.insertAdjacentElement('afterend', $screenReaderStatusMessage);
+ this.$showHideButton.addEventListener('click', this.toggle.bind(this));
+ if (this.$input.form) {
+ this.$input.form.addEventListener('submit', () => this.hide());
+ }
+ window.addEventListener('pageshow', event => {
+ if (event.persisted && this.$input.type !== 'password') {
+ this.hide();
+ }
+ });
+ this.hide();
+ }
+ toggle(event) {
+ event.preventDefault();
+ if (this.$input.type === 'password') {
+ this.show();
+ return;
+ }
+ this.hide();
+ }
+ show() {
+ this.setType('text');
+ }
+ hide() {
+ this.setType('password');
+ }
+ setType(type) {
+ if (type === this.$input.type) {
+ return;
+ }
+ this.$input.setAttribute('type', type);
+ const isHidden = type === 'password';
+ const prefixButton = isHidden ? 'show' : 'hide';
+ const prefixStatus = isHidden ? 'passwordHidden' : 'passwordShown';
+ this.$showHideButton.innerHTML = this.i18n.t(`${prefixButton}Password`);
+ this.$showHideButton.setAttribute('aria-label', this.i18n.t(`${prefixButton}PasswordAriaLabel`));
+ this.$screenReaderStatusMessage.innerText = this.i18n.t(`${prefixStatus}Announcement`);
+ }
+}
+
+/**
+ * Password input config
+ *
+ * @typedef {object} PasswordInputConfig
+ * @property {PasswordInputTranslations} [i18n=PasswordInput.defaults.i18n] - Password input translations
+ */
+
+/**
+ * Password input translations
+ *
+ * @see {@link PasswordInput.defaults.i18n}
+ * @typedef {object} PasswordInputTranslations
+ *
+ * Messages displayed to the user indicating the state of the show/hide toggle.
+ * @property {string} [showPassword] - Visible text of the button when the
+ * password is currently hidden. HTML is acceptable.
+ * @property {string} [hidePassword] - Visible text of the button when the
+ * password is currently visible. HTML is acceptable.
+ * @property {string} [showPasswordAriaLabel] - aria-label of the button when
+ * the password is currently hidden. Plain text only.
+ * @property {string} [hidePasswordAriaLabel] - aria-label of the button when
+ * the password is currently visible. Plain text only.
+ * @property {string} [passwordShownAnnouncement] - Screen reader
+ * announcement to make when the password has just become visible.
+ * Plain text only.
+ * @property {string} [passwordHiddenAnnouncement] - Screen reader
+ * announcement to make when the password has just been hidden.
+ * Plain text only.
+ */
+
+/**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ * @typedef {import('../../i18n.mjs').TranslationPluralForms} TranslationPluralForms
+ */
+PasswordInput.moduleName = 'govuk-password-input';
+PasswordInput.defaults = Object.freeze({
+ i18n: {
+ showPassword: 'Show',
+ hidePassword: 'Hide',
+ showPasswordAriaLabel: 'Show password',
+ hidePasswordAriaLabel: 'Hide password',
+ passwordShownAnnouncement: 'Your password is visible',
+ passwordHiddenAnnouncement: 'Your password is hidden'
+ }
+});
+PasswordInput.schema = Object.freeze({
+ properties: {
+ i18n: {
+ type: 'object'
+ }
+ }
+});
+
/**
* Radios component
*
@@ -2209,7 +2363,7 @@ function initAll(config) {
console.log(new SupportError());
return;
}
- const components = [[Accordion, config.accordion], [Button, config.button], [CharacterCount, config.characterCount], [Checkboxes], [ErrorSummary, config.errorSummary], [ExitThisPage, config.exitThisPage], [Header], [NotificationBanner, config.notificationBanner], [Radios], [SkipLink], [Tabs]];
+ 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], [SkipLink], [Tabs]];
const $scope = (_config$scope = config.scope) != null ? _config$scope : document;
components.forEach(([Component, config]) => {
const $elements = $scope.querySelectorAll(`[data-module="${Component.moduleName}"]`);
@@ -2233,6 +2387,7 @@ function initAll(config) {
* @property {ErrorSummaryConfig} [errorSummary] - Error Summary config
* @property {ExitThisPageConfig} [exitThisPage] - Exit This Page config
* @property {NotificationBannerConfig} [notificationBanner] - Notification Banner config
+ * @property {PasswordInputConfig} [passwordInput] - Password input config
*/
/**
@@ -2247,6 +2402,7 @@ function initAll(config) {
* @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/notification-banner/notification-banner.mjs').NotificationBannerConfig} NotificationBannerConfig
+ * @typedef {import('./components/password-input/password-input.mjs').PasswordInputConfig} PasswordInputConfig
*/
/**
@@ -2255,5 +2411,5 @@ function initAll(config) {
* @typedef {keyof Config} ConfigKey
*/
-export { Accordion, Button, CharacterCount, Checkboxes, ErrorSummary, ExitThisPage, Header, NotificationBanner, Radios, SkipLink, Tabs, initAll, version };
+export { Accordion, Button, CharacterCount, Checkboxes, ErrorSummary, ExitThisPage, Header, NotificationBanner, PasswordInput, Radios, SkipLink, Tabs, initAll, 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 ba303fbd4..02cb75e14 100644
--- a/packages/govuk-frontend/dist/govuk/all.mjs
+++ b/packages/govuk-frontend/dist/govuk/all.mjs
@@ -8,6 +8,7 @@ import { ErrorSummary } from './components/error-summary/error-summary.mjs';
import { ExitThisPage } from './components/exit-this-page/exit-this-page.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';
import { Radios } from './components/radios/radios.mjs';
import { SkipLink } from './components/skip-link/skip-link.mjs';
import { Tabs } from './components/tabs/tabs.mjs';
@@ -28,7 +29,7 @@ function initAll(config) {
console.log(new SupportError());
return;
}
- const components = [[Accordion, config.accordion], [Button, config.button], [CharacterCount, config.characterCount], [Checkboxes], [ErrorSummary, config.errorSummary], [ExitThisPage, config.exitThisPage], [Header], [NotificationBanner, config.notificationBanner], [Radios], [SkipLink], [Tabs]];
+ 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], [SkipLink], [Tabs]];
const $scope = (_config$scope = config.scope) != null ? _config$scope : document;
components.forEach(([Component, config]) => {
const $elements = $scope.querySelectorAll(`[data-module="${Component.moduleName}"]`);
@@ -52,6 +53,7 @@ function initAll(config) {
* @property {ErrorSummaryConfig} [errorSummary] - Error Summary config
* @property {ExitThisPageConfig} [exitThisPage] - Exit This Page config
* @property {NotificationBannerConfig} [notificationBanner] - Notification Banner config
+ * @property {PasswordInputConfig} [passwordInput] - Password input config
*/
/**
@@ -66,6 +68,7 @@ function initAll(config) {
* @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/notification-banner/notification-banner.mjs').NotificationBannerConfig} NotificationBannerConfig
+ * @typedef {import('./components/password-input/password-input.mjs').PasswordInputConfig} PasswordInputConfig
*/
/**
@@ -74,5 +77,5 @@ function initAll(config) {
* @typedef {keyof Config} ConfigKey
*/
-export { Accordion, Button, CharacterCount, Checkboxes, ErrorSummary, ExitThisPage, Header, NotificationBanner, Radios, SkipLink, Tabs, initAll };
+export { Accordion, Button, CharacterCount, Checkboxes, ErrorSummary, ExitThisPage, Header, NotificationBanner, PasswordInput, Radios, SkipLink, Tabs, initAll };
//# sourceMappingURL=all.mjs.map
diff --git a/packages/govuk-frontend/dist/govuk/components/_all.scss b/packages/govuk-frontend/dist/govuk/components/_all.scss
index 5a5425470..e0f702951 100644
--- a/packages/govuk-frontend/dist/govuk/components/_all.scss
+++ b/packages/govuk-frontend/dist/govuk/components/_all.scss
@@ -23,6 +23,7 @@
@import "notification-banner/index";
@import "pagination/index";
@import "panel/index";
+@import "password-input/index";
@import "phase-banner/index";
@import "radios/index";
@import "select/index";
diff --git a/packages/govuk-frontend/dist/govuk/components/input/fixtures.json b/packages/govuk-frontend/dist/govuk/components/input/fixtures.json
index b7dbefda6..b30288cf5 100644
--- a/packages/govuk-frontend/dist/govuk/components/input/fixtures.json
+++ b/packages/govuk-frontend/dist/govuk/components/input/fixtures.json
@@ -274,6 +274,22 @@
"previewLayoutModifiers": [],
"html": "<div class=\"govuk-form-group\">\n <label class=\"govuk-label\" for=\"input-with-spellcheck-disabled\">\n Spellcheck is disabled\n </label>\n <input class=\"govuk-input\" id=\"input-with-spellcheck-disabled\" name=\"spellcheck\" type=\"text\" spellcheck=\"false\">\n</div>"
},
+ {
+ "name": "with autocapitalize turned off",
+ "options": {
+ "label": {
+ "text": "Autocapitalize is turned off"
+ },
+ "id": "input-with-autocapitalize-off",
+ "name": "autocapitalize",
+ "type": "text",
+ "autocapitalize": "none"
+ },
+ "hidden": false,
+ "description": "",
+ "previewLayoutModifiers": [],
+ "html": "<div class=\"govuk-form-group\">\n <label class=\"govuk-label\" for=\"input-with-autocapitalize-off\">\n Autocapitalize is turned off\n </label>\n <input class=\"govuk-input\" id=\"input-with-autocapitalize-off\" name=\"autocapitalize\" type=\"text\" autocapitalize=\"none\">\n</div>"
+ },
{
"name": "with prefix",
"options": {
@@ -719,6 +735,32 @@
"description": "",
"previewLayoutModifiers": [],
"html": "<div class=\"govuk-form-group\">\n <label class=\"govuk-label\" for=\"input-with-suffix\">\n Weight, in kilograms\n </label>\n <div class=\"govuk-input__wrapper\">\n <input class=\"govuk-input\" id=\"input-with-suffix\" name=\"weight\" type=\"text\">\n <div class=\"govuk-input__suffix\" aria-hidden=\"true\" data-attribute=\"value\"><span>kg</span></div>\n </div>\n</div>"
+ },
+ {
+ "name": "with customised input wrapper",
+ "options": {
+ "label": {
+ "text": "Cost per item, in pounds"
+ },
+ "id": "input-with-customised-input-wrapper",
+ "name": "cost",
+ "inputWrapper": {
+ "classes": "app-input-wrapper--custom-modifier",
+ "attributes": {
+ "data-attribute": "value"
+ }
+ },
+ "prefix": {
+ "text": "£"
+ },
+ "suffix": {
+ "text": "per item"
+ }
+ },
+ "hidden": true,
+ "description": "",
+ "previewLayoutModifiers": [],
+ "html": "<div class=\"govuk-form-group\">\n <label class=\"govuk-label\" for=\"input-with-customised-input-wrapper\">\n Cost per item, in pounds\n </label>\n <div class=\"govuk-input__wrapper app-input-wrapper--custom-modifier\" data-attribute=\"value\">\n <div class=\"govuk-input__prefix\" aria-hidden=\"true\">£</div>\n <input class=\"govuk-input\" id=\"input-with-customised-input-wrapper\" name=\"cost\" type=\"text\">\n <div class=\"govuk-input__suffix\" aria-hidden=\"true\">per item</div>\n </div>\n</div>"
}
]
}
diff --git a/packages/govuk-frontend/dist/govuk/components/input/macro-options.json b/packages/govuk-frontend/dist/govuk/components/input/macro-options.json
index 1d7e6b2f1..c76a2c90f 100644
--- a/packages/govuk-frontend/dist/govuk/components/input/macro-options.json
+++ b/packages/govuk-frontend/dist/govuk/components/input/macro-options.json
@@ -210,6 +210,32 @@
"required": false,
"description": "Optional field to enable or disable the `spellcheck` attribute on the input."
},
+ {
+ "name": "autocapitalize",
+ "type": "string",
+ "required": false,
+ "description": "Optional field to enable or disable autocapitalisation of user input. See [autocapitalization](https://html.spec.whatwg.org/multipage/interaction.html#autocapitalization) for a full list of values that can be used."
+ },
+ {
+ "name": "inputWrapper",
+ "type": "object",
+ "required": false,
+ "description": "If any of `prefix`, `suffix`, `formGroup.beforeInput` or `formGroup.afterInput` have a value, a wrapping element is added around the input and inserted content. This object allows you to customise that wrapping element.",
+ "params": [
+ {
+ "name": "classes",
+ "type": "string",
+ "required": false,
+ "description": "Classes to add to the wrapping element."
+ },
+ {
+ "name": "attributes",
+ "type": "object",
+ "required": false,
+ "description": "HTML attributes (for example data attributes) to add to the wrapping element."
+ }
+ ]
+ },
{
"name": "attributes",
"type": "object",
diff --git a/packages/govuk-frontend/dist/govuk/components/password-input/_index.scss b/packages/govuk-frontend/dist/govuk/components/password-input/_index.scss
new file mode 100644
index 000000000..0fa68d8b4
--- /dev/null
+++ b/packages/govuk-frontend/dist/govuk/components/password-input/_index.scss
@@ -0,0 +1,55 @@
+@import "../button/index";
+@import "../input/index";
+
+@include govuk-exports("govuk/component/password-input") {
+ .govuk-password-input__wrapper {
+ // This element inherits styles from .govuk-input__wrapper, including:
+ // - being display: block with contents in a stacked column below the mobile breakpoint
+ // - being display: flex above the mobile breakpoint
+
+ @include govuk-media-query($from: mobile) {
+ flex-direction: row;
+
+ // The default of `stretch` makes the toggle button appear taller than the input, due to using
+ // box-shadow, which we don't particularly want in this situation
+ align-items: flex-start;
+ }
+ }
+
+ .govuk-password-input__input {
+ // IE 11 and Microsoft Edge comes with its own password reveal function. We want to hide it,
+ // so that there aren't two controls presented to the user that do the same thing but aren't in
+ // sync with one another. This doesn't affect the function that allows Edge users to toggle
+ // password visibility by pressing Alt+F8, which cannot be programatically disabled.
+ &::-ms-reveal {
+ display: none;
+ }
+ }
+
+ .govuk-password-input__toggle {
+ // Add margin to the top so that the button doesn't obscure the input's focus style
+ margin-top: govuk-spacing(1);
+
+ // Remove default margin-bottom from button
+ margin-bottom: 0;
+
+ // Hide the button by default, JS removes this attribute
+ &[hidden] {
+ display: none;
+ }
+
+ @include govuk-media-query($from: mobile) {
+ // Buttons are normally 100% width on this breakpoint, but we don't want that in this case
+ width: auto;
+ flex-grow: 1;
+ flex-shrink: 0;
+ flex-basis: 5em;
+
+ // Move the spacing from top to the left
+ margin-top: 0;
+ margin-left: govuk-spacing(1);
+ }
+ }
+}
+
+/*# sourceMappingURL=_index.scss.map */
diff --git a/packages/govuk-frontend/dist/govuk/components/password-input/_password-input.scss b/packages/govuk-frontend/dist/govuk/components/password-input/_password-input.scss
new file mode 100644
index 000000000..189d8057b
--- /dev/null
+++ b/packages/govuk-frontend/dist/govuk/components/password-input/_password-input.scss
@@ -0,0 +1,4 @@
+@import "../../base";
+@import "./index";
+
+/*# sourceMappingURL=_password-input.scss.map */
diff --git a/packages/govuk-frontend/dist/govuk/components/password-input/fixtures.json b/packages/govuk-frontend/dist/govuk/components/password-input/fixtures.json
new file mode 100644
index 000000000..5c14c2ad4
--- /dev/null
+++ b/packages/govuk-frontend/dist/govuk/components/password-input/fixtures.json
@@ -0,0 +1,170 @@
+{
+ "component": "password-input",
+ "fixtures": [
+ {
+ "name": "default",
+ "options": {
+ "label": {
+ "text": "Password"
+ },
+ "id": "password-input",
+ "name": "password"
+ },
+ "hidden": false,
+ "description": "",
+ "previewLayoutModifiers": [],
+ "html": "<div class=\"govuk-form-group govuk-password-input\" data-module=\"govuk-password-input\">\n <label class=\"govuk-label\" for=\"password-input\">\n Password\n </label>\n <div class=\"govuk-input__wrapper govuk-password-input__wrapper\">\n <input class=\"govuk-input govuk-password-input__input govuk-js-password-input-input\" id=\"password-input\" name=\"password\" type=\"password\" spellcheck=\"false\" autocomplete=\"current-password\" autocapitalize=\"none\">\n <button type=\"button\" class=\"govuk-button govuk-button--secondary govuk-password-input__toggle govuk-js-password-input-toggle\" data-module=\"govuk-button\" aria-controls=\"password-input\" aria-label=\"Show password\" hidden>\n Show\n </button>\n </div>\n</div>"
+ },
+ {
+ "name": "with hint text",
+ "options": {
+ "label": {
+ "text": "Password"
+ },
+ "hint": {
+ "text": "It probably has some letters, numbers and maybe even some symbols in it."
+ },
+ "id": "password-input-with-hint-text",
+ "name": "test-name-2"
+ },
+ "hidden": false,
+ "description": "",
+ "previewLayoutModifiers": [],
+ "html": "<div class=\"govuk-form-group govuk-password-input\" data-module=\"govuk-password-input\">\n <label class=\"govuk-label\" for=\"password-input-with-hint-text\">\n Password\n </label>\n <div id=\"password-input-with-hint-text-hint\" class=\"govuk-hint\">\n It probably has some letters, numbers and maybe even some symbols in it.\n </div>\n <div class=\"govuk-input__wrapper govuk-password-input__wrapper\">\n <input class=\"govuk-input govuk-password-input__input govuk-js-password-input-input\" id=\"password-input-with-hint-text\" name=\"test-name-2\" type=\"password\" spellcheck=\"false\" aria-describedby=\"password-input-with-hint-text-hint\" autocomplete=\"current-password\" autocapitalize=\"none\">\n <button type=\"button\" class=\"govuk-button govuk-button--secondary govuk-password-input__toggle govuk-js-password-input-toggle\" data-module=\"govuk-button\" aria-controls=\"password-input-with-hint-text\" aria-label=\"Show password\" hidden>\n Show\n </button>\n </div>\n</div>"
+ },
+ {
+ "name": "with error message",
+ "options": {
+ "label": {
+ "text": "Password"
+ },
+ "hint": {
+ "text": "It probably has some letters, numbers and maybe even some symbols in it."
+ },
+ "id": "password-input-with-error-message",
+ "name": "test-name-3",
+ "errorMessage": {
+ "text": "Error message goes here"
+ }
+ },
+ "hidden": false,
+ "description": "",
+ "previewLayoutModifiers": [],
+ "html": "<div class=\"govuk-form-group govuk-form-group--error govuk-password-input\" data-module=\"govuk-password-input\">\n <label class=\"govuk-label\" for=\"password-input-with-error-message\">\n Password\n </label>\n <div id=\"password-input-with-error-message-hint\" class=\"govuk-hint\">\n It probably has some letters, numbers and maybe even some symbols in it.\n </div>\n <p id=\"password-input-with-error-message-error\" class=\"govuk-error-message\">\n <span class=\"govuk-visually-hidden\">Error:</span> Error message goes here\n </p>\n <div class=\"govuk-input__wrapper govuk-password-input__wrapper\">\n <input class=\"govuk-input govuk-password-input__input govuk-js-password-input-input govuk-input--error\" id=\"password-input-with-error-message\" name=\"test-name-3\" type=\"password\" spellcheck=\"false\" aria-describedby=\"password-input-with-error-message-hint password-input-with-error-message-error\" autocomplete=\"current-password\" autocapitalize=\"none\">\n <button type=\"button\" class=\"govuk-button govuk-button--secondary govuk-password-input__toggle govuk-js-password-input-toggle\" data-module=\"govuk-button\" aria-controls=\"password-input-with-error-message\" aria-label=\"Show password\" hidden>\n Show\n </button>\n </div>\n</div>"
+ },
+ {
+ "name": "with label as page heading",
+ "options": {
+ "label": {
+ "text": "Password",
+ "classes": "govuk-label--l",
+ "isPageHeading": true
+ },
+ "id": "password-input-with-page-heading",
+ "name": "test-name"
+ },
+ "hidden": false,
+ "description": "",
+ "previewLayoutModifiers": [],
+ "html": "<div class=\"govuk-form-group govuk-password-input\" data-module=\"govuk-password-input\">\n <h1 class=\"govuk-label-wrapper\">\n <label class=\"govuk-label govuk-label--l\" for=\"password-input-with-page-heading\">\n Password\n </label>\n </h1>\n <div class=\"govuk-input__wrapper govuk-password-input__wrapper\">\n <input class=\"govuk-input govuk-password-input__input govuk-js-password-input-input\" id=\"password-input-with-page-heading\" name=\"test-name\" type=\"password\" spellcheck=\"false\" autocomplete=\"current-password\" autocapitalize=\"none\">\n <button type=\"button\" class=\"govuk-button govuk-button--secondary govuk-password-input__toggle govuk-js-password-input-toggle\" data-module=\"govuk-button\" aria-controls=\"password-input-with-page-heading\" aria-label=\"Show password\" hidden>\n Show\n </button>\n </div>\n</div>"
+ },
+ {
+ "name": "with new-password autocomplete",
+ "options": {
+ "label": {
+ "text": "Password"
+ },
+ "autocomplete": "new-password",
+ "id": "password-input-new-password",
+ "name": "password"
+ },
+ "hidden": false,
+ "description": "Browsers and password managers should prompt to generate a password.",
+ "previewLayoutModifiers": [],
+ "html": "<div class=\"govuk-form-group govuk-password-input\" data-module=\"govuk-password-input\">\n <label class=\"govuk-label\" for=\"password-input-new-password\">\n Password\n </label>\n <div class=\"govuk-input__wrapper govuk-password-input__wrapper\">\n <input class=\"govuk-input govuk-password-input__input govuk-js-password-input-input\" id=\"password-input-new-password\" name=\"password\" type=\"password\" spellcheck=\"false\" autocomplete=\"new-password\" autocapitalize=\"none\">\n <button type=\"button\" class=\"govuk-button govuk-button--secondary govuk-password-input__toggle govuk-js-password-input-toggle\" data-module=\"govuk-button\" aria-controls=\"password-input-new-password\" aria-label=\"Show password\" hidden>\n Show\n </button>\n </div>\n</div>"
+ },
+ {
+ "name": "with translations",
+ "options": {
+ "label": {
+ "text": "Cyfrinair"
+ },
+ "id": "password-translated",
+ "name": "password-translated",
+ "showPasswordText": "Datguddia",
+ "hidePasswordText": "Cuddio",
+ "showPasswordAriaLabelText": "Datgelu cyfrinair",
+ "hidePasswordAriaLabelText": "Cuddio cyfrinair",
+ "passwordShownAnnouncementText": "Mae eich cyfrinair yn weladwy.",
+ "passwordHiddenAnnouncementText": "Mae eich cyfrinair wedi'i guddio."
+ },
+ "hidden": false,
+ "description": "",
+ "previewLayoutModifiers": [],
+ "html": "<div class=\"govuk-form-group govuk-password-input\" data-module=\"govuk-password-input\" data-i18n.show-password=\"Datguddia\" data-i18n.hide-password=\"Cuddio\" data-i18n.show-password-aria-label=\"Datgelu cyfrinair\" data-i18n.hide-password-aria-label=\"Cuddio cyfrinair\" data-i18n.password-shown-announcement=\"Mae eich cyfrinair yn weladwy.\" data-i18n.password-hidden-announcement=\"Mae eich cyfrinair wedi'i guddio.\">\n <label class=\"govuk-label\" for=\"password-translated\">\n Cyfrinair\n </label>\n <div class=\"govuk-input__wrapper govuk-password-input__wrapper\">\n <input class=\"govuk-input govuk-password-input__input govuk-js-password-input-input\" id=\"password-translated\" name=\"password-translated\" type=\"password\" spellcheck=\"false\" autocomplete=\"current-password\" autocapitalize=\"none\">\n <button type=\"button\" class=\"govuk-button govuk-button--secondary govuk-password-input__toggle govuk-js-password-input-toggle\" data-module=\"govuk-button\" aria-controls=\"password-translated\" aria-label=\"Datgelu cyfrinair\" hidden>\n Datguddia\n </button>\n </div>\n</div>"
+ },
+ {
+ "name": "classes",
+ "options": {
+ "id": "with-classes",
+ "name": "with-classes",
+ "label": {
+ "text": "With classes"
+ },
+ "classes": "app-input--custom-modifier"
+ },
+ "hidden": true,
+ "description": "",
+ "previewLayoutModifiers": [],
+ "html": "<div class=\"govuk-form-group govuk-password-input\" data-module=\"govuk-password-input\">\n <label class=\"govuk-label\" for=\"with-classes\">\n With classes\n </label>\n <div class=\"govuk-input__wrapper govuk-password-input__wrapper\">\n <input class=\"govuk-input govuk-password-input__input govuk-js-password-input-input app-input--custom-modifier\" id=\"with-classes\" name=\"with-classes\" type=\"password\" spellcheck=\"false\" autocomplete=\"current-password\" autocapitalize=\"none\">\n <button type=\"button\" class=\"govuk-button govuk-button--secondary govuk-password-input__toggle govuk-js-password-input-toggle\" data-module=\"govuk-button\" aria-controls=\"with-classes\" aria-label=\"Show password\" hidden>\n Show\n </button>\n </div>\n</div>"
+ },
+ {
+ "name": "value",
+ "options": {
+ "id": "with-value",
+ "name": "with-value",
+ "label": {
+ "text": "With value"
+ },
+ "value": "Hunter2"
+ },
+ "hidden": true,
+ "description": "",
+ "previewLayoutModifiers": [],
+ "html": "<div class=\"govuk-form-group govuk-password-input\" data-module=\"govuk-password-input\">\n <label class=\"govuk-label\" for=\"with-value\">\n With value\n </label>\n <div class=\"govuk-input__wrapper govuk-password-input__wrapper\">\n <input class=\"govuk-input govuk-password-input__input govuk-js-password-input-input\" id=\"with-value\" name=\"with-value\" type=\"password\" spellcheck=\"false\" value=\"Hunter2\" autocomplete=\"current-password\" autocapitalize=\"none\">\n <button type=\"button\" class=\"govuk-button govuk-button--secondary govuk-password-input__toggle govuk-js-password-input-toggle\" data-module=\"govuk-button\" aria-controls=\"with-value\" aria-label=\"Show password\" hidden>\n Show\n </button>\n </div>\n</div>"
+ },
+ {
+ "name": "attributes",
+ "options": {
+ "id": "with-attributes",
+ "name": "with-attributes",
+ "label": {
+ "text": "With attributes"
+ },
+ "attributes": {
+ "data-attribute": "value",
+ "data-another": "ok"
+ }
+ },
+ "hidden": true,
+ "description": "",
+ "previewLayoutModifiers": [],
+ "html": "<div class=\"govuk-form-group govuk-password-input\" data-module=\"govuk-password-input\">\n <label class=\"govuk-label\" for=\"with-attributes\">\n With attributes\n </label>\n <div class=\"govuk-input__wrapper govuk-password-input__wrapper\">\n <input class=\"govuk-input govuk-password-input__input govuk-js-password-input-input\" id=\"with-attributes\" name=\"with-attributes\" type=\"password\" spellcheck=\"false\" autocomplete=\"current-password\" autocapitalize=\"none\" data-attribute=\"value\" data-another=\"ok\">\n <button type=\"button\" class=\"govuk-button govuk-button--secondary govuk-password-input__toggle govuk-js-password-input-toggle\" data-module=\"govuk-button\" aria-controls=\"with-attributes\" aria-label=\"Show password\" hidden>\n Show\n </button>\n </div>\n</div>"
+ },
+ {
+ "name": "with describedBy",
+ "options": {
+ "id": "with-describedby",
+ "name": "with-describedby",
+ "label": {
+ "text": "With describedBy"
+ },
+ "describedBy": "test-target-element"
+ },
+ "hidden": true,
+ "description": "",
+ "previewLayoutModifiers": [],
+ "html": "<div class=\"govuk-form-group govuk-password-input\" data-module=\"govuk-password-input\">\n <label class=\"govuk-label\" for=\"with-describedby\">\n With describedBy\n </label>\n <div class=\"govuk-input__wrapper govuk-password-input__wrapper\">\n <input class=\"govuk-input govuk-password-input__input govuk-js-password-input-input\" id=\"with-describedby\" name=\"with-describedby\" type=\"password\" spellcheck=\"false\" aria-describedby=\"test-target-element\" autocomplete=\"current-password\" autocapitalize=\"none\">\n <button type=\"button\" class=\"govuk-button govuk-button--secondary govuk-password-input__toggle govuk-js-password-input-toggle\" data-module=\"govuk-button\" aria-controls=\"with-describedby\" aria-label=\"Show password\" hidden>\n Show\n </button>\n </div>\n</div>"
+ }
+ ]
+}
diff --git a/packages/govuk-frontend/dist/govuk/components/password-input/macro-options.json b/packages/govuk-frontend/dist/govuk/components/password-input/macro-options.json
new file mode 100644
index 000000000..4cf0d6a7e
--- /dev/null
+++ b/packages/govuk-frontend/dist/govuk/components/password-input/macro-options.json
@@ -0,0 +1,181 @@
+[
+ {
+ "name": "id",
+ "type": "string",
+ "required": true,
+ "description": "The ID of the input."
+ },
+ {
+ "name": "name",
+ "type": "string",
+ "required": true,
+ "description": "The name of the input, which is submitted with the form data."
+ },
+ {
+ "name": "value",
+ "type": "string",
+ "required": false,
+ "description": "Optional initial value of the input."
+ },
+ {
+ "name": "disabled",
+ "type": "boolean",
+ "required": false,
+ "description": "If `true`, input will be disabled."
+ },
+ {
+ "name": "describedBy",
+ "type": "string",
+ "required": false,
+ "description": "One or more element IDs to add to the `aria-describedby` attribute, used to provide additional descriptive information for screenreader users."
+ },
+ {
+ "name": "label",
+ "type": "object",
+ "required": true,
+ "description": "The label used by the text input component.",
+ "isComponent": true
+ },
+ {
+ "name": "hint",
+ "type": "object",
+ "required": false,
+ "description": "Can be used to add a hint to a text input component.",
+ "isComponent": true
+ },
+ {
+ "name": "errorMessage",
+ "type": "object",
+ "required": false,
+ "description": "Can be used to add an error message to the text input component. The error message component will not display if you use a falsy value for `errorMessage`, for example `false` or `null`.",
+ "isComponent": true
+ },
+ {
+ "name": "formGroup",
+ "type": "object",
+ "required": false,
+ "description": "Additional options for the form group containing the text input component.",
+ "params": [
+ {
+ "name": "classes",
+ "type": "string",
+ "required": false,
+ "description": "Classes to add to the form group (for example to show error state for the whole group)."
+ },
+ {
+ "name": "attributes",
+ "type": "object",
+ "required": false,
+ "description": "HTML attributes (for example data attributes) to add to the form group."
+ },
+ {
+ "name": "beforeInput",
+ "type": "object",
+ "required": false,
+ "description": "Content to add before the input used by the text input component.",
+ "params": [
+ {
+ "name": "text",
+ "type": "string",
+ "required": true,
+ "description": "Text to add before the input. If `html` is provided, the `text` option will be ignored."
+ },
+ {
+ "name": "html",
+ "type": "string",
+ "required": true,
+ "description": "HTML to add before the input. If `html` is provided, the `text` option will be ignored."
+ }
+ ]
+ },
+ {
+ "name": "afterInput",
+ "type": "object",
+ "required": false,
+ "description": "Content to add after the input used by the text input component.",
+ "params": [
+ {
+ "name": "text",
+ "type": "string",
+ "required": true,
+ "description": "Text to add after the input. If `html` is provided, the `text` option will be ignored."
+ },
+ {
+ "name": "html",
+ "type": "string",
+ "required": true,
+ "description": "HTML to add after the input. If `html` is provided, the `text` option will be ignored."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "classes",
+ "type": "string",
+ "required": false,
+ "description": "Classes to add to the input."
+ },
+ {
+ "name": "autocomplete",
+ "type": "string",
+ "required": false,
+ "description": "Attribute to [identify input purpose](https://www.w3.org/WAI/WCAG21/Understanding/identify-input-purpose.html). See [autofill](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill) for full list of values that can be used. Default is `\"current-password\"`."
+ },
+ {
+ "name": "attributes",
+ "type": "object",
+ "required": false,
+ "description": "HTML attributes (for example data attributes) to add to the input."
+ },
+ {
+ "name": "showPasswordText",
+ "type": "string",
+ "required": false,
+ "description": "Button text when the password is hidden. Defaults to `\"Show\"`."
+ },
+ {
+ "name": "hidePasswordText",
+ "type": "string",
+ "required": false,
+ "description": "Button text when the password is visible. Defaults to `\"Hide\"`."
+ },
+ {
+ "name": "showPasswordAriaLabelText",
+ "type": "string",
+ "required": false,
+ "description": "Button text exposed to assistive technologies, like screen readers, when the password is hidden. Defaults to `\"Show password\"`."
+ },
+ {
+ "name": "hidePasswordAriaLabelText",
+ "type": "string",
+ "required": false,
+ "description": "Button text exposed to assistive technologies, like screen readers, when the password is visible. Defaults to `\"Hide password\"`."
+ },
+ {
+ "name": "passwordShownAnnouncementText",
+ "type": "string",
+ "required": false,
+ "description": "Announcement made to screen reader users when their password has become visible in plain text. Defaults to `\"Your password is visible\"`."
+ },
+ {
+ "name": "passwordHiddenAnnouncementText",
+ "type": "string",
+ "required": false,
+ "description": "Announcement made to screen reader users when their password has been obscured and is not visible. Defaults to `\"Your password is hidden\"`."
+ },
+ {
+ "name": "button",
+ "type": "object",
+ "required": false,
+ "description": "Optional object allowing customisation of the toggle button.",
+ "params": [
+ {
+ "name": "classes",
+ "type": "string",
+ "required": false,
+ "description": "Classes to add to the button."
+ }
+ ]
+ }
+]
diff --git a/packages/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.js b/packages/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.js
new file mode 100644
index 000000000..fbc397bb2
--- /dev/null
+++ b/packages/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.js
@@ -0,0 +1,528 @@
+(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 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);
+ }
+
+ /**
+ * 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
+ */
+
+ 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 {
+ componentName,
+ identifier,
+ element,
+ expectedType
+ } = messageOrOptions;
+ message = `${componentName}: ${identifier}`;
+ message += element ? ` is not of type ${expectedType != null ? expectedType : 'HTMLElement'}` : ' not found';
+ }
+ super(message);
+ this.name = 'ElementError';
+ }
+ }
+
+ class GOVUKFrontendComponent {
+ constructor() {
+ this.checkSupport();
+ }
+ checkSupport() {
+ if (!isSupported()) {
+ throw new SupportError();
+ }
+ }
+ }
+
+ 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';
+ }
+ };
+
+ /**
+ * Password input component
+ *
+ * @preserve
+ */
+ class PasswordInput extends GOVUKFrontendComponent {
+ /**
+ * @param {Element | null} $module - HTML element to use for password input
+ * @param {PasswordInputConfig} [config] - Password input config
+ */
+ constructor($module, config = {}) {
+ super();
+ this.$module = void 0;
+ this.config = void 0;
+ this.i18n = void 0;
+ this.$input = void 0;
+ this.$showHideButton = void 0;
+ this.$screenReaderStatusMessage = void 0;
+ if (!($module instanceof HTMLElement)) {
+ throw new ElementError({
+ componentName: 'Password input',
+ element: $module,
+ identifier: 'Root element (`$module`)'
+ });
+ }
+ const $input = $module.querySelector('.govuk-js-password-input-input');
+ if (!($input instanceof HTMLInputElement)) {
+ throw new ElementError({
+ componentName: 'Password input',
+ element: $input,
+ expectedType: 'HTMLInputElement',
+ identifier: 'Form field (`.govuk-js-password-input-input`)'
+ });
+ }
+ if ($input.type !== 'password') {
+ throw new ElementError('Password input: Form field (`.govuk-js-password-input-input`) must be of type `password`.');
+ }
+ const $showHideButton = $module.querySelector('.govuk-js-password-input-toggle');
+ if (!($showHideButton instanceof HTMLButtonElement)) {
+ throw new ElementError({
+ componentName: 'Password input',
+ element: $showHideButton,
+ expectedType: 'HTMLButtonElement',
+ identifier: 'Button (`.govuk-js-password-input-toggle`)'
+ });
+ }
+ if ($showHideButton.type !== 'button') {
+ throw new ElementError('Password input: Button (`.govuk-js-password-input-toggle`) must be of type `button`.');
+ }
+ this.$module = $module;
+ this.$input = $input;
+ this.$showHideButton = $showHideButton;
+ this.config = mergeConfigs(PasswordInput.defaults, config, normaliseDataset(PasswordInput, $module.dataset));
+ this.i18n = new I18n(this.config.i18n, {
+ locale: closestAttributeValue($module, 'lang')
+ });
+ this.$showHideButton.removeAttribute('hidden');
+ const $screenReaderStatusMessage = document.createElement('div');
+ $screenReaderStatusMessage.className = 'govuk-password-input__sr-status govuk-visually-hidden';
+ $screenReaderStatusMessage.setAttribute('aria-live', 'polite');
+ this.$screenReaderStatusMessage = $screenReaderStatusMessage;
+ this.$input.insertAdjacentElement('afterend', $screenReaderStatusMessage);
+ this.$showHideButton.addEventListener('click', this.toggle.bind(this));
+ if (this.$input.form) {
+ this.$input.form.addEventListener('submit', () => this.hide());
+ }
+ window.addEventListener('pageshow', event => {
+ if (event.persisted && this.$input.type !== 'password') {
+ this.hide();
+ }
+ });
+ this.hide();
+ }
+ toggle(event) {
+ event.preventDefault();
+ if (this.$input.type === 'password') {
+ this.show();
+ return;
+ }
+ this.hide();
+ }
+ show() {
+ this.setType('text');
+ }
+ hide() {
+ this.setType('password');
+ }
+ setType(type) {
+ if (type === this.$input.type) {
+ return;
+ }
+ this.$input.setAttribute('type', type);
+ const isHidden = type === 'password';
+ const prefixButton = isHidden ? 'show' : 'hide';
+ const prefixStatus = isHidden ? 'passwordHidden' : 'passwordShown';
+ this.$showHideButton.innerHTML = this.i18n.t(`${prefixButton}Password`);
+ this.$showHideButton.setAttribute('aria-label', this.i18n.t(`${prefixButton}PasswordAriaLabel`));
+ this.$screenReaderStatusMessage.innerText = this.i18n.t(`${prefixStatus}Announcement`);
+ }
+ }
+
+ /**
+ * Password input config
+ *
+ * @typedef {object} PasswordInputConfig
+ * @property {PasswordInputTranslations} [i18n=PasswordInput.defaults.i18n] - Password input translations
+ */
+
+ /**
+ * Password input translations
+ *
+ * @see {@link PasswordInput.defaults.i18n}
+ * @typedef {object} PasswordInputTranslations
+ *
+ * Messages displayed to the user indicating the state of the show/hide toggle.
+ * @property {string} [showPassword] - Visible text of the button when the
+ * password is currently hidden. HTML is acceptable.
+ * @property {string} [hidePassword] - Visible text of the button when the
+ * password is currently visible. HTML is acceptable.
+ * @property {string} [showPasswordAriaLabel] - aria-label of the button when
+ * the password is currently hidden. Plain text only.
+ * @property {string} [hidePasswordAriaLabel] - aria-label of the button when
+ * the password is currently visible. Plain text only.
+ * @property {string} [passwordShownAnnouncement] - Screen reader
+ * announcement to make when the password has just become visible.
+ * Plain text only.
+ * @property {string} [passwordHiddenAnnouncement] - Screen reader
+ * announcement to make when the password has just been hidden.
+ * Plain text only.
+ */
+
+ /**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ * @typedef {import('../../i18n.mjs').TranslationPluralForms} TranslationPluralForms
+ */
+ PasswordInput.moduleName = 'govuk-password-input';
+ PasswordInput.defaults = Object.freeze({
+ i18n: {
+ showPassword: 'Show',
+ hidePassword: 'Hide',
+ showPasswordAriaLabel: 'Show password',
+ hidePasswordAriaLabel: 'Hide password',
+ passwordShownAnnouncement: 'Your password is visible',
+ passwordHiddenAnnouncement: 'Your password is hidden'
+ }
+ });
+ PasswordInput.schema = Object.freeze({
+ properties: {
+ i18n: {
+ type: 'object'
+ }
+ }
+ });
+
+ exports.PasswordInput = PasswordInput;
+
+}));
+//# sourceMappingURL=password-input.bundle.js.map
diff --git a/packages/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.mjs
new file mode 100644
index 000000000..f6eccf100
--- /dev/null
+++ b/packages/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.mjs
@@ -0,0 +1,520 @@
+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 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);
+}
+
+/**
+ * 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
+ */
+
+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 {
+ componentName,
+ identifier,
+ element,
+ expectedType
+ } = messageOrOptions;
+ message = `${componentName}: ${identifier}`;
+ message += element ? ` is not of type ${expectedType != null ? expectedType : 'HTMLElement'}` : ' not found';
+ }
+ super(message);
+ this.name = 'ElementError';
+ }
+}
+
+class GOVUKFrontendComponent {
+ constructor() {
+ this.checkSupport();
+ }
+ checkSupport() {
+ if (!isSupported()) {
+ throw new SupportError();
+ }
+ }
+}
+
+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';
+ }
+};
+
+/**
+ * Password input component
+ *
+ * @preserve
+ */
+class PasswordInput extends GOVUKFrontendComponent {
+ /**
+ * @param {Element | null} $module - HTML element to use for password input
+ * @param {PasswordInputConfig} [config] - Password input config
+ */
+ constructor($module, config = {}) {
+ super();
+ this.$module = void 0;
+ this.config = void 0;
+ this.i18n = void 0;
+ this.$input = void 0;
+ this.$showHideButton = void 0;
+ this.$screenReaderStatusMessage = void 0;
+ if (!($module instanceof HTMLElement)) {
+ throw new ElementError({
+ componentName: 'Password input',
+ element: $module,
+ identifier: 'Root element (`$module`)'
+ });
+ }
+ const $input = $module.querySelector('.govuk-js-password-input-input');
+ if (!($input instanceof HTMLInputElement)) {
+ throw new ElementError({
+ componentName: 'Password input',
+ element: $input,
+ expectedType: 'HTMLInputElement',
+ identifier: 'Form field (`.govuk-js-password-input-input`)'
+ });
+ }
+ if ($input.type !== 'password') {
+ throw new ElementError('Password input: Form field (`.govuk-js-password-input-input`) must be of type `password`.');
+ }
+ const $showHideButton = $module.querySelector('.govuk-js-password-input-toggle');
+ if (!($showHideButton instanceof HTMLButtonElement)) {
+ throw new ElementError({
+ componentName: 'Password input',
+ element: $showHideButton,
+ expectedType: 'HTMLButtonElement',
+ identifier: 'Button (`.govuk-js-password-input-toggle`)'
+ });
+ }
+ if ($showHideButton.type !== 'button') {
+ throw new ElementError('Password input: Button (`.govuk-js-password-input-toggle`) must be of type `button`.');
+ }
+ this.$module = $module;
+ this.$input = $input;
+ this.$showHideButton = $showHideButton;
+ this.config = mergeConfigs(PasswordInput.defaults, config, normaliseDataset(PasswordInput, $module.dataset));
+ this.i18n = new I18n(this.config.i18n, {
+ locale: closestAttributeValue($module, 'lang')
+ });
+ this.$showHideButton.removeAttribute('hidden');
+ const $screenReaderStatusMessage = document.createElement('div');
+ $screenReaderStatusMessage.className = 'govuk-password-input__sr-status govuk-visually-hidden';
+ $screenReaderStatusMessage.setAttribute('aria-live', 'polite');
+ this.$screenReaderStatusMessage = $screenReaderStatusMessage;
+ this.$input.insertAdjacentElement('afterend', $screenReaderStatusMessage);
+ this.$showHideButton.addEventListener('click', this.toggle.bind(this));
+ if (this.$input.form) {
+ this.$input.form.addEventListener('submit', () => this.hide());
+ }
+ window.addEventListener('pageshow', event => {
+ if (event.persisted && this.$input.type !== 'password') {
+ this.hide();
+ }
+ });
+ this.hide();
+ }
+ toggle(event) {
+ event.preventDefault();
+ if (this.$input.type === 'password') {
+ this.show();
+ return;
+ }
+ this.hide();
+ }
+ show() {
+ this.setType('text');
+ }
+ hide() {
+ this.setType('password');
+ }
+ setType(type) {
+ if (type === this.$input.type) {
+ return;
+ }
+ this.$input.setAttribute('type', type);
+ const isHidden = type === 'password';
+ const prefixButton = isHidden ? 'show' : 'hide';
+ const prefixStatus = isHidden ? 'passwordHidden' : 'passwordShown';
+ this.$showHideButton.innerHTML = this.i18n.t(`${prefixButton}Password`);
+ this.$showHideButton.setAttribute('aria-label', this.i18n.t(`${prefixButton}PasswordAriaLabel`));
+ this.$screenReaderStatusMessage.innerText = this.i18n.t(`${prefixStatus}Announcement`);
+ }
+}
+
+/**
+ * Password input config
+ *
+ * @typedef {object} PasswordInputConfig
+ * @property {PasswordInputTranslations} [i18n=PasswordInput.defaults.i18n] - Password input translations
+ */
+
+/**
+ * Password input translations
+ *
+ * @see {@link PasswordInput.defaults.i18n}
+ * @typedef {object} PasswordInputTranslations
+ *
+ * Messages displayed to the user indicating the state of the show/hide toggle.
+ * @property {string} [showPassword] - Visible text of the button when the
+ * password is currently hidden. HTML is acceptable.
+ * @property {string} [hidePassword] - Visible text of the button when the
+ * password is currently visible. HTML is acceptable.
+ * @property {string} [showPasswordAriaLabel] - aria-label of the button when
+ * the password is currently hidden. Plain text only.
+ * @property {string} [hidePasswordAriaLabel] - aria-label of the button when
+ * the password is currently visible. Plain text only.
+ * @property {string} [passwordShownAnnouncement] - Screen reader
+ * announcement to make when the password has just become visible.
+ * Plain text only.
+ * @property {string} [passwordHiddenAnnouncement] - Screen reader
+ * announcement to make when the password has just been hidden.
+ * Plain text only.
+ */
+
+/**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ * @typedef {import('../../i18n.mjs').TranslationPluralForms} TranslationPluralForms
+ */
+PasswordInput.moduleName = 'govuk-password-input';
+PasswordInput.defaults = Object.freeze({
+ i18n: {
+ showPassword: 'Show',
+ hidePassword: 'Hide',
+ showPasswordAriaLabel: 'Show password',
+ hidePasswordAriaLabel: 'Hide password',
+ passwordShownAnnouncement: 'Your password is visible',
+ passwordHiddenAnnouncement: 'Your password is hidden'
+ }
+});
+PasswordInput.schema = Object.freeze({
+ properties: {
+ i18n: {
+ type: 'object'
+ }
+ }
+});
+
+export { PasswordInput };
+//# sourceMappingURL=password-input.bundle.mjs.map
diff --git a/packages/govuk-frontend/dist/govuk/components/password-input/password-input.mjs b/packages/govuk-frontend/dist/govuk/components/password-input/password-input.mjs
new file mode 100644
index 000000000..f4e133fc3
--- /dev/null
+++ b/packages/govuk-frontend/dist/govuk/components/password-input/password-input.mjs
@@ -0,0 +1,163 @@
+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';
+
+/**
+ * Password input component
+ *
+ * @preserve
+ */
+class PasswordInput extends GOVUKFrontendComponent {
+ /**
+ * @param {Element | null} $module - HTML element to use for password input
+ * @param {PasswordInputConfig} [config] - Password input config
+ */
+ constructor($module, config = {}) {
+ super();
+ this.$module = void 0;
+ this.config = void 0;
+ this.i18n = void 0;
+ this.$input = void 0;
+ this.$showHideButton = void 0;
+ this.$screenReaderStatusMessage = void 0;
+ if (!($module instanceof HTMLElement)) {
+ throw new ElementError({
+ componentName: 'Password input',
+ element: $module,
+ identifier: 'Root element (`$module`)'
+ });
+ }
+ const $input = $module.querySelector('.govuk-js-password-input-input');
+ if (!($input instanceof HTMLInputElement)) {
+ throw new ElementError({
+ componentName: 'Password input',
+ element: $input,
+ expectedType: 'HTMLInputElement',
+ identifier: 'Form field (`.govuk-js-password-input-input`)'
+ });
+ }
+ if ($input.type !== 'password') {
+ throw new ElementError('Password input: Form field (`.govuk-js-password-input-input`) must be of type `password`.');
+ }
+ const $showHideButton = $module.querySelector('.govuk-js-password-input-toggle');
+ if (!($showHideButton instanceof HTMLButtonElement)) {
+ throw new ElementError({
+ componentName: 'Password input',
+ element: $showHideButton,
+ expectedType: 'HTMLButtonElement',
+ identifier: 'Button (`.govuk-js-password-input-toggle`)'
+ });
+ }
+ if ($showHideButton.type !== 'button') {
+ throw new ElementError('Password input: Button (`.govuk-js-password-input-toggle`) must be of type `button`.');
+ }
+ this.$module = $module;
+ this.$input = $input;
+ this.$showHideButton = $showHideButton;
+ this.config = mergeConfigs(PasswordInput.defaults, config, normaliseDataset(PasswordInput, $module.dataset));
+ this.i18n = new I18n(this.config.i18n, {
+ locale: closestAttributeValue($module, 'lang')
+ });
+ this.$showHideButton.removeAttribute('hidden');
+ const $screenReaderStatusMessage = document.createElement('div');
+ $screenReaderStatusMessage.className = 'govuk-password-input__sr-status govuk-visually-hidden';
+ $screenReaderStatusMessage.setAttribute('aria-live', 'polite');
+ this.$screenReaderStatusMessage = $screenReaderStatusMessage;
+ this.$input.insertAdjacentElement('afterend', $screenReaderStatusMessage);
+ this.$showHideButton.addEventListener('click', this.toggle.bind(this));
+ if (this.$input.form) {
+ this.$input.form.addEventListener('submit', () => this.hide());
+ }
+ window.addEventListener('pageshow', event => {
+ if (event.persisted && this.$input.type !== 'password') {
+ this.hide();
+ }
+ });
+ this.hide();
+ }
+ toggle(event) {
+ event.preventDefault();
+ if (this.$input.type === 'password') {
+ this.show();
+ return;
+ }
+ this.hide();
+ }
+ show() {
+ this.setType('text');
+ }
+ hide() {
+ this.setType('password');
+ }
+ setType(type) {
+ if (type === this.$input.type) {
+ return;
+ }
+ this.$input.setAttribute('type', type);
+ const isHidden = type === 'password';
+ const prefixButton = isHidden ? 'show' : 'hide';
+ const prefixStatus = isHidden ? 'passwordHidden' : 'passwordShown';
+ this.$showHideButton.innerHTML = this.i18n.t(`${prefixButton}Password`);
+ this.$showHideButton.setAttribute('aria-label', this.i18n.t(`${prefixButton}PasswordAriaLabel`));
+ this.$screenReaderStatusMessage.innerText = this.i18n.t(`${prefixStatus}Announcement`);
+ }
+}
+
+/**
+ * Password input config
+ *
+ * @typedef {object} PasswordInputConfig
+ * @property {PasswordInputTranslations} [i18n=PasswordInput.defaults.i18n] - Password input translations
+ */
+
+/**
+ * Password input translations
+ *
+ * @see {@link PasswordInput.defaults.i18n}
+ * @typedef {object} PasswordInputTranslations
+ *
+ * Messages displayed to the user indicating the state of the show/hide toggle.
+ * @property {string} [showPassword] - Visible text of the button when the
+ * password is currently hidden. HTML is acceptable.
+ * @property {string} [hidePassword] - Visible text of the button when the
+ * password is currently visible. HTML is acceptable.
+ * @property {string} [showPasswordAriaLabel] - aria-label of the button when
+ * the password is currently hidden. Plain text only.
+ * @property {string} [hidePasswordAriaLabel] - aria-label of the button when
+ * the password is currently visible. Plain text only.
+ * @property {string} [passwordShownAnnouncement] - Screen reader
+ * announcement to make when the password has just become visible.
+ * Plain text only.
+ * @property {string} [passwordHiddenAnnouncement] - Screen reader
+ * announcement to make when the password has just been hidden.
+ * Plain text only.
+ */
+
+/**
+ * @typedef {import('../../common/index.mjs').Schema} Schema
+ * @typedef {import('../../i18n.mjs').TranslationPluralForms} TranslationPluralForms
+ */
+PasswordInput.moduleName = 'govuk-password-input';
+PasswordInput.defaults = Object.freeze({
+ i18n: {
+ showPassword: 'Show',
+ hidePassword: 'Hide',
+ showPasswordAriaLabel: 'Show password',
+ hidePasswordAriaLabel: 'Hide password',
+ passwordShownAnnouncement: 'Your password is visible',
+ passwordHiddenAnnouncement: 'Your password is hidden'
+ }
+});
+PasswordInput.schema = Object.freeze({
+ properties: {
+ i18n: {
+ type: 'object'
+ }
+ }
+});
+
+export { PasswordInput };
+//# sourceMappingURL=password-input.mjs.map
Action run for 2b5d976 |
5ef295a
to
91c2b65
Compare
91c2b65
to
7aa3289
Compare
Align with the style changes in #4843.
Co-authored-by: Romaric Pascal <romaric.pascal@digital.cabinet-office.gov.uk>
Testing a hypothesis that VoiceOver failing to read the aria-live consistently may be due to it only being added when a status is set for the first time. If this is the case, then potentially it's also unnecessary to use assertive over polite.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking sharp. Done some browser testing and screen reader testing. All looking good to me. Code looking good as well. I've left a single very pithy comment that's far from critical.
I haven't looked at the puppeteer tests very much as it looks like this is being handled in #4763.
packages/govuk-frontend/src/govuk/components/input/template.test.js
Outdated
Show resolved
Hide resolved
…uppeteer-tests Add Puppeteer tests for Password input
Anika has concluded testing 80d33b4 and found that it indeed resolves the issues with VoiceOver status announcements without negatively affecting other screen readers. Reverting when the I would consider this issue to be secondary to ensuring that announcements occur consistently on the 'happy path' in VoiceOver, however, so the reversion feels sensible. With that, I think this is mergeable! 🎉 |
Prototypical password input component for #4225.
See it in the review app.
Changes
password
type upon form submission has been removed. We couldn't find any evidence that this was being used by any live service and it doesn't serve a clear user need, so we've chosen to omit it unless a need arises.all.mjs
to export the Password input and automatically use it ifdata-module="govuk-password-input"
is found on a page.Changes to other components
autocapitalize
attribute and parameter added.inputWrapper
parameter that allows for setting classes and attributes on thegovuk-input__wrapper
element.Changes to the review app
current-password
autocomplete value.new-password
autocomplete value.Thoughts
type
is hardcoded topassword
in the initial HTML, as I don't envisage this practically being anything else.autocomplete
attribute. As such, I've made it a required attribute that defaults tocurrent-password
.spellcheck
andautocapitalize
attributes are both set to off/disabled. This is to prevent the user's device attempting to 'correct' the user's input if they are typing a password whilst the input is displaying as plain text.pattern
,inputmode
and prefix/suffixes—are not accessible via the Password input. I think these have limited application when it comes to passwords and could only be used in ways we would typically advise against (e.g. client-side validation, number-only passwords).