Skip to content

Commit 6ba5766

Browse files
Yeray Diaz Diazdi
Yeray Diaz Diaz
authored andcommitted
Added password strength gauge in registration form. (#2542)
* Added vendor JS gulp step to include zxcvbn.js Include it in the register template Added password strength gauge in registration form. * Added JS validation via tooltips. Ignore vendor scripts in lint. * Include `window.scroll` values in tooltip placement * Move SCSS block to its own file. Adapt to naming convention. Use existing tooltips. * Always show 'Confirm Password' label * Fix password-strench documentation * Split help text into two paragraphs * Fix typo * Add screenreader feedback
1 parent 7829480 commit 6ba5766

File tree

9 files changed

+208
-7
lines changed

9 files changed

+208
-7
lines changed

Gulpfile.babel.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,11 @@ gulp.task("dist:images", () => {
156156
.pipe(gulp.dest(path.join(distPath, "images")));
157157
});
158158

159+
gulp.task("dist:vendor", () => {
160+
return gulp.src(path.join(staticPrefix, "js", "vendor", "**", "*"))
161+
.pipe(gulp.dest(path.join(distPath, "js", "vendor")));
162+
});
163+
159164

160165
gulp.task("dist:manifest", () => {
161166
let paths = [
@@ -258,7 +263,7 @@ gulp.task("dist", (cb) => {
258263
// any previously built files.
259264
"clean",
260265
// Build all of our static assets.
261-
["dist:font-awesome", "dist:css", "dist:js"],
266+
["dist:font-awesome", "dist:css", "dist:js", "dist:vendor"],
262267
// We have this here, instead of in the list above even though there is no
263268
// ordering dependency so that all of it's output shows up together which
264269
// makes it easier to read.

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,11 @@ lint: .state/env/pyvenv.cfg
106106
$(BINDIR)/html_lint.py --printfilename --disable=optional_tag,names,protocol,extra_whitespace `find ./warehouse/templates -path ./warehouse/templates/legacy -prune -o -name '*.html' -print`
107107
ifneq ($(TRAVIS), false)
108108
# We're on Travis, so we can lint static files locally
109-
./node_modules/.bin/eslint 'warehouse/static/js/**' '**.js'
109+
./node_modules/.bin/eslint 'warehouse/static/js/**' '**.js' --ignore-pattern 'warehouse/static/js/vendor/**'
110110
./node_modules/.bin/sass-lint --verbose
111111
else
112112
# We're not on Travis, so we should lint static files inside the static container
113-
docker-compose run --rm static ./node_modules/.bin/eslint 'warehouse/static/js/**' '**.js'
113+
docker-compose run --rm static ./node_modules/.bin/eslint 'warehouse/static/js/**' '**.js' --ignore-pattern 'warehouse/static/js/vendor/**'
114114
docker-compose run --rm static ./node_modules/.bin/sass-lint --verbose
115115
endif
116116

warehouse/static/js/vendor/zxcvbn.js

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

warehouse/static/js/warehouse/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ docReady(Analytics);
6565

6666
// Handle the JS based automatic form submission.
6767
docReady(formUtils.submitTriggers);
68+
docReady(formUtils.registerFormValidation);
6869

6970
docReady(Statuspage);
7071

warehouse/static/js/warehouse/utils/forms.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,96 @@ export function submitTriggers() {
4949
);
5050
}
5151
}
52+
53+
/* global zxcvbn */
54+
55+
const tooltipClasses = ["tooltipped", "tooltipped-s", "tooltipped-immediate"];
56+
57+
const passwordStrengthValidator = (value) => {
58+
const zxcvbnResult = zxcvbn(value);
59+
return zxcvbnResult.score < 2 ?
60+
zxcvbnResult.feedback.suggestions.join(" ") : null;
61+
};
62+
63+
const fieldRequiredValidator = (value) => {
64+
return value === ""?
65+
"Please fill out this field" : null;
66+
};
67+
68+
const checkPasswordStrength = (event) => {
69+
let result = document.querySelector(".password-strength__gauge");
70+
if (event.target.value === "") {
71+
result.setAttribute("class", "password-strength__gauge");
72+
// Feedback for screen readers
73+
result.querySelector(".sr-only").innerHTML = "Password field is empty";
74+
} else {
75+
// following recommendations on the zxcvbn JS docs
76+
// the zxcvbn function is available by loading `vendor/zxcvbn.js`
77+
// in the register page template only
78+
let zxcvbnResult = zxcvbn(event.target.value);
79+
result.setAttribute("class", `password-strength__gauge password-strength__gauge--${zxcvbnResult.score}`);
80+
81+
// Feedback for screen readers
82+
result.querySelector(".sr-only").innerHTML = zxcvbnResult.feedback.suggestions.join(" ") || "Password is strong";
83+
}
84+
};
85+
86+
const setupPasswordStrengthGauge = () => {
87+
let password = document.querySelector("#new_password");
88+
if (password === null) return;
89+
password.addEventListener(
90+
"input",
91+
checkPasswordStrength,
92+
false
93+
);
94+
};
95+
96+
const attachTooltip = (field, message) => {
97+
let parentNode = field.parentNode;
98+
parentNode.classList.add.apply(parentNode.classList, tooltipClasses);
99+
parentNode.setAttribute("aria-label", message);
100+
};
101+
102+
const removeTooltips = () => {
103+
let tooltippedNodes = document.querySelectorAll(".tooltipped");
104+
for (let tooltippedNode of tooltippedNodes) {
105+
tooltippedNode.classList.remove.apply(tooltippedNode.classList, tooltipClasses);
106+
tooltippedNode.removeAttribute("aria-label");
107+
}
108+
};
109+
110+
const validateForm = (event) => {
111+
removeTooltips();
112+
let inputFields = document.querySelectorAll("input[required='required']");
113+
for (let inputField of inputFields) {
114+
let requiredMessage = fieldRequiredValidator(inputField.value);
115+
if (requiredMessage !== null) {
116+
attachTooltip(inputField, requiredMessage);
117+
event.preventDefault();
118+
return false;
119+
}
120+
}
121+
122+
let password = document.querySelector("#new_password");
123+
let passwordConfirm = document.querySelector("#password_confirm");
124+
if (password.value !== passwordConfirm.value) {
125+
let message = "Passwords do not match";
126+
attachTooltip(password, message);
127+
event.preventDefault();
128+
return false;
129+
}
130+
131+
let passwordStrengthMessage = passwordStrengthValidator(password.value);
132+
if (passwordStrengthMessage !== null) {
133+
attachTooltip(password, passwordStrengthMessage);
134+
event.preventDefault();
135+
return false;
136+
}
137+
};
138+
139+
export function registerFormValidation() {
140+
if (document.querySelector("#password_confirm") === null) return;
141+
setupPasswordStrengthGauge();
142+
const submitButton = document.querySelector("#content input[type='submit']");
143+
submitButton.addEventListener("click", validateForm, false);
144+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*!
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
15+
/*
16+
A password strength indicator:
17+
18+
<span class="password-strength">
19+
<span class="password-strength__gauge"></span>
20+
</span>
21+
*/
22+
23+
.password-strength {
24+
display: inline-block;
25+
width: 100%;
26+
height: 0.8em;
27+
border: 1px solid $border-color;
28+
29+
.password-strength__gauge {
30+
width: 0%;
31+
height: 100%;
32+
display: block;
33+
34+
&--0 {
35+
width: 20%;
36+
background-color: red;
37+
}
38+
39+
&--1 {
40+
width: 40%;
41+
background-color: orange;
42+
}
43+
44+
&--2 {
45+
width: 60%;
46+
background-color: yellow;
47+
}
48+
49+
&--3 {
50+
width: 80%;
51+
background-color: #508000;
52+
}
53+
54+
&--4 {
55+
width: 100%;
56+
background-color: green;
57+
}
58+
}
59+
}

warehouse/static/sass/blocks/_tooltip.scss

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ $tooltip-duration: 0.1s !default;
6464
// This will indicate when we'll activate the tooltip
6565
.tooltipped:hover,
6666
.tooltipped:active,
67-
.tooltipped:focus {
67+
.tooltipped:focus,
68+
.tooltipped-immediate {
6869
&::before,
6970
&::after {
7071
display: inline-block;

warehouse/static/sass/warehouse.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
@import "blocks/package-description";
9292
@import "blocks/package-header";
9393
@import "blocks/package-snippet";
94+
@import "blocks/password-strength";
9495
@import "blocks/project-description";
9596
@import "blocks/release";
9697
@import "blocks/release-timeline";

warehouse/templates/accounts/register.html

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ <h1 class="page-title">Create an account on PyPI</h1>
3636

3737
<div class="form-group">
3838
<label for="full_name" class="form-group__label">Name</label>
39-
{{ form.full_name(placeholder="Your name", class_="form-group__input") }}
39+
{{ form.full_name(placeholder="Your name", required="required", class_="form-group__input") }}
4040
{% if form.full_name.errors %}
4141
<ul class="form-errors">
4242
{% for error in form.full_name.errors %}
@@ -77,10 +77,20 @@ <h1 class="page-title">Create an account on PyPI</h1>
7777
<input id="show-password" type="checkbox">&nbsp;Show passwords
7878
</label>
7979
</div>
80-
{{ form.new_password(placeholder="Select a password", required="required", class_="form-group__input", autocomplete="new-password") }}
80+
<div>
81+
{{ form.new_password(placeholder="Select a password", required="required", class_="form-group__input", autocomplete="new-password") }}
82+
</div>
8183
<p class="form-group__help-text">
8284
Choose a strong password that contains letters (uppercase and lowercase), numbers and special characters. Avoid common words or repetition.
8385
</p>
86+
<p class="form-group__help-text">
87+
<strong>Password strength:</strong>
88+
<span class="password-strength">
89+
<span class="password-strength__gauge">
90+
<span class="sr-only">Password field is empty</span>
91+
</span>
92+
</span>
93+
</p>
8494
{% if form.new_password.errors %}
8595
<ul class="form-errors">
8696
{% for error in form.new_password.errors %}
@@ -91,7 +101,7 @@ <h1 class="page-title">Create an account on PyPI</h1>
91101
</div>
92102

93103
<div class="form-group">
94-
<label for="password_confirm" class="sr-only">Confirm Password</label>
104+
<label for="password_confirm" class="form-group__label">Confirm Password</label>
95105
{{ form.password_confirm(placeholder="Confirm password", required="required", class_="form-group__input") }}
96106
{% if form.password_confirm.errors %}
97107
<ul class="form-errors">
@@ -114,4 +124,7 @@ <h1 class="page-title">Create an account on PyPI</h1>
114124

115125
{% block extra_js %}
116126
{{ recaptcha_src(request) }}
127+
<script async
128+
src="{{ request.static_path('warehouse:static/dist/js/vendor/zxcvbn.js') }}">
129+
</script>
117130
{% endblock %}

0 commit comments

Comments
 (0)