Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion src/sentry/templates/sentry/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,19 @@ <h3>{% trans "Sign in to continue" %}</h3>
{{ login_form|as_crispy_errors }}

{% for field in login_form %}
{{ field|as_crispy_field }}
<div class="login-form-field{% if field.name == 'password' %} password-field{% endif %}">
{{ field|as_crispy_field }}
{% if field.name == 'password' %}
<button
type="button"
class="password-visibility-toggle"
aria-label="Show password"
aria-pressed="false"
>
<span class="icon-eye"></span>
</button>
{% endif %}
</div>
{% endfor %}

<div class="auth-footer m-t-1">
Expand Down Expand Up @@ -165,6 +177,42 @@ <h3>{% trans "Sign in to continue" %}</h3>
document.cookie = `sentry_react_auth=1; Path=/auth/; SameSite=None; Secure${domainAttribute}`;
window.location.reload();
};

(function() {
function bindPasswordVisibilityToggles() {
document
.querySelectorAll('.password-visibility-toggle:not([data-bound])')
.forEach(function(button) {
button.setAttribute('data-bound', 'true');
button.addEventListener('click', function() {
var container = button.closest('.password-field');
if (!container) {
return;
}
var input = container.querySelector('input');
if (!input) {
return;
}
var isHidden = input.type === 'password';
input.type = isHidden ? 'text' : 'password';
// "pressed" means the password is currently visible. isHidden is
// the pre-toggle state, so after flipping to visible it is true.
button.setAttribute('aria-pressed', String(isHidden));
var label = button.getAttribute('aria-label');
button.setAttribute(
'aria-label',
label === 'Show password' ? 'Hide password' : 'Show password'
);
});
});
}

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bindPasswordVisibilityToggles);
} else {
bindPasswordVisibilityToggles();
}
})();
</script>
{% endscript %}
{% endblock %}
4 changes: 3 additions & 1 deletion src/sentry/web/forms/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ class AuthenticationForm(forms.Form):
)
password = forms.CharField(
label=_("Password"),
widget=forms.PasswordInput(attrs={"placeholder": _("password"), "tabindex": 2}),
widget=forms.PasswordInput(
attrs={"placeholder": _("password"), "tabindex": 2, "class": "password-input"}
),
)

error_messages = {
Expand Down
4 changes: 4 additions & 0 deletions static/less/fonts.less
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,10 @@
content: '\e611';
}

.icon-eye:before {
content: '\e60d';
}

// Used by plugins in getsentry
.icon-checkmark:before {
content: '\e60a';
Expand Down
33 changes: 33 additions & 0 deletions static/less/layout.less
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,39 @@ body.auth {
flex: 1;
}

.login-form-field {
&.password-field {
position: relative;

.password-visibility-toggle {
position: absolute;
top: 34px;
right: 10px;
z-index: 3;
padding: 0;
border: 0;
background: none;
color: @gray;
cursor: pointer;
line-height: 1;
font-size: 16px;

&:hover,
&:focus {
color: @gray-dark;
}

.icon-eye {
font-size: 16px;
}
}

input.password-input {
padding-right: 34px;
}
}
}

.auth-provider-column {
width: 46%;
padding-left: 30px;
Expand Down
12 changes: 12 additions & 0 deletions tests/sentry/web/frontend/test_auth_login.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@ def test_renders_correct_template(self) -> None:
assert resp.status_code == 200
self.assertTemplateUsed("sentry/login.html")

def test_login_page_includes_password_visibility_toggle(self) -> None:
resp = self.client.get(self.path)

assert resp.status_code == 200
content = resp.content.decode()
# The password field is wrapped in a container with a toggle button
assert 'class="login-form-field password-field"' in content
assert 'class="password-visibility-toggle"' in content
assert 'aria-label="Show password"' in content
# The client-side behavior for the toggle is present
assert "bindPasswordVisibilityToggles" in content

def test_renders_legacy_login_banner(self) -> None:
banner = 'Banner message <a href="https://example.com">Learn more</a>.'
with mock.patch.object(
Expand Down
Loading