password: Add password limit indicator to form.

Adds a password limit indicator to the password field in both the
registration form and the password reset form. This indicator displays
the remaining number of characters allowed, helping users comply with
the maximum password length requirement.

Fixes: #27922.
This commit is contained in:
Maneesh Shukla 2024-10-26 14:15:40 +05:30
parent e0bd3713cc
commit f28ec726f6
5 changed files with 90 additions and 7 deletions

View File

@ -143,12 +143,14 @@ Form is validated both client-side using jquery-validation (see signup.js) and s
</div> </div>
{% elif password_required %} {% elif password_required %}
<div class="input-box password-div"> <div class="input-box password-div">
<div class="password-label-container">
<span><label for="id_password">{{ _('Password') }}</label></span>
<span id="password-limit-indicator" class="limit-indicator" data-tippy-content="{{ _('Maximum password length: 100 characters') }}" data-tippy-placement="top"></span>
</div>
<input id="id_password" class="required" type="password" name="password" autocomplete="new-password" <input id="id_password" class="required" type="password" name="password" autocomplete="new-password"
value="{% if form.password.value() %}{{ form.password.value() }}{% endif %}" value="{% if form.password.value() %}{{ form.password.value() }}{% endif %}"
maxlength="{{ MAX_PASSWORD_LENGTH }}"
data-min-length="{{password_min_length}}" data-min-length="{{password_min_length}}"
data-min-guesses="{{password_min_guesses}}" required /> data-min-guesses="{{password_min_guesses}}" required />
<label for="id_password" class="inline-block">{{ _('Password') }}</label>
<i class="fa fa-eye-slash password_visibility_toggle" role="button" tabindex="0"></i> <i class="fa fa-eye-slash password_visibility_toggle" role="button" tabindex="0"></i>
{% if full_name %} {% if full_name %}
<span class="help-inline"> <span class="help-inline">
@ -252,7 +254,7 @@ Form is validated both client-side using jquery-validation (see signup.js) and s
</div> </div>
{% endif %} {% endif %}
<div class="register-button-box"> <div class="register-button-box">
<button class="register-button" type="submit"> <button class="register-button" id="signup-button" type="submit">
<span>{{ _('Sign up') }}</span> <span>{{ _('Sign up') }}</span>
<object class="loader" type="image/svg+xml" data="{{ static('images/loading/loader-white.svg') }}"></object> <object class="loader" type="image/svg+xml" data="{{ static('images/loading/loader-white.svg') }}"></object>
</button> </button>

View File

@ -28,10 +28,12 @@
</div> </div>
<div class="input-box password-div"> <div class="input-box password-div">
<label for="id_new_password1" class="">{{ _('Password') }}</label> <div class="password-label-container">
<span><label for="id_new_password1" class="">{{ _('Password') }}</label></span>
<span id="reset-password-limit-indicator" class="limit-indicator" data-tippy-content="{{ _('Maximum password length: 100 characters') }}" data-tippy-placement="top"></span>
</div>
<input id="id_new_password1" class="required" type="password" name="new_password1" autocomplete="new-password" <input id="id_new_password1" class="required" type="password" name="new_password1" autocomplete="new-password"
value="{% if form.new_password1.value() %}{{ form.new_password1.value() }}{% endif %}" value="{% if form.new_password1.value() %}{{ form.new_password1.value() }}{% endif %}"
maxlength="100"
data-min-length="{{password_min_length}}" data-min-length="{{password_min_length}}"
data-min-guesses="{{password_min_guesses}}" autofocus required /> data-min-guesses="{{password_min_guesses}}" autofocus required />
<i class="fa fa-eye-slash password_visibility_toggle" role="button" tabindex="0"></i> <i class="fa fa-eye-slash password_visibility_toggle" role="button" tabindex="0"></i>
@ -52,7 +54,7 @@
<label for="id_new_password2" class="">{{ _('Confirm password') }}</label> <label for="id_new_password2" class="">{{ _('Confirm password') }}</label>
<input id="id_new_password2" class="required" type="password" name="new_password2" autocomplete="off" <input id="id_new_password2" class="required" type="password" name="new_password2" autocomplete="off"
value="{% if form.new_password2.value() %}{{ form.new_password2.value() }}{% endif %}" value="{% if form.new_password2.value() %}{{ form.new_password2.value() }}{% endif %}"
maxlength="100" required /> required />
<i class="fa fa-eye-slash password_visibility_toggle" role="button" tabindex="0"></i> <i class="fa fa-eye-slash password_visibility_toggle" role="button" tabindex="0"></i>
{% if form.new_password2.errors %} {% if form.new_password2.errors %}
{% for error in form.new_password2.errors %} {% for error in form.new_password2.errors %}
@ -63,7 +65,7 @@
<div class="input-box m-t-30"> <div class="input-box m-t-30">
<div class="centered-button"> <div class="centered-button">
<button type="submit" class="" value="Submit">Submit</button> <button type="submit" id="reset-button" class="" value="Submit">Submit</button>
</div> </div>
</div> </div>
</form> </form>

View File

@ -6,3 +6,4 @@ import "../portico/tippyjs.ts";
import "../../third/bootstrap/css/bootstrap.portico.css"; import "../../third/bootstrap/css/bootstrap.portico.css";
import "../../styles/portico/portico_styles.css"; import "../../styles/portico/portico_styles.css";
import "tippy.js/dist/tippy.css"; import "tippy.js/dist/tippy.css";
import "../../styles/app_variables.css";

View File

@ -2,6 +2,7 @@ import $ from "jquery";
import assert from "minimalistic-assert"; import assert from "minimalistic-assert";
import {z} from "zod"; import {z} from "zod";
import render_compose_limit_indicator from "../../templates/compose_limit_indicator.hbs";
import * as common from "../common.ts"; import * as common from "../common.ts";
import {$t} from "../i18n.ts"; import {$t} from "../i18n.ts";
import {password_quality, password_warning} from "../password_quality.ts"; import {password_quality, password_warning} from "../password_quality.ts";
@ -342,3 +343,49 @@ $(() => {
} }
}); });
}); });
const $password_elem: JQuery<HTMLInputElement> = $("input#id_password");
const $new_password1_elem: JQuery<HTMLInputElement> = $("input#id_new_password1");
if ($password_elem.length) {
$password_elem.on("input", () => {
check_overflow_password($password_elem);
});
}
if ($new_password1_elem.length) {
$new_password1_elem.on("input", () => {
check_overflow_password($new_password1_elem);
});
}
export function check_overflow_password($password_elem: JQuery<HTMLInputElement>): void {
const password = $password_elem.val() ?? "";
const max_length = 100;
const remaining_characters = max_length - password.length;
const $indicator = $password_elem.closest(".input-box").find(".limit-indicator");
const $button = $password_elem.closest("form").find("button[type='submit']");
const password_too_long = password.length > max_length;
$button.prop("disabled", password_too_long);
// Update the limit indicator
if (password_too_long) {
$indicator.addClass("over_limit");
$indicator.html(
render_compose_limit_indicator({
remaining_characters,
}),
);
} else if (remaining_characters <= 20) {
$indicator.removeClass("over_limit");
$indicator.html(
render_compose_limit_indicator({
remaining_characters,
}),
);
} else {
$indicator.text("");
}
}

View File

@ -1461,3 +1461,34 @@ button#register_auth_button_gitlab {
margin-top: 5px; margin-top: 5px;
display: none; display: none;
} }
#signup-button,
#reset-button {
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
#reset-password-limit-indicator,
#password-limit-indicator {
&:not(:empty) {
font-size: 16px;
color: var(--color-limit-indicator);
cursor: pointer;
height: 0;
margin-right: 5px;
z-index: 10;
}
&.over_limit {
color: var(--color-limit-indicator-over-limit);
font-weight: bold;
}
}
.password-label-container {
display: flex;
align-items: center;
justify-content: space-between;
}