Put icons in the login fields and drop their labels
The entrance forms lose their visible field labels and gain an icon inside each field (mail, lock), and allauth's "Forgot your password?" link -- which is the password field's help_text -- is spaced away from the input instead of sitting flush against it. Three things this depends on: - allauth already passes `unlabeled=True` on the entrance forms and already sets a placeholder on every field there, so the visible label was redundant. The label is still emitted sr-only: a placeholder is not a label, and it vanishes as soon as you type. - daisyUI's icon-in-field layout puts the `input` class on the *wrapping label*, so the input itself must carry only `grow` -- hence the optional css override on the daisy filter. `input` on both draws a box inside a box, and the error state belongs on the wrapper for the same reason. - The help text now carries id="<auto_id>_helptext". Django points the input's aria-describedby at exactly that id, so without it the reference dangled and a screen reader never announced the password-reset link. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -46,16 +46,40 @@ def as_alert(message):
|
||||
return {"icon": icon, "title": message.extra_tags or title, "body": message.message, "css": css}
|
||||
|
||||
|
||||
@register.filter
|
||||
def daisy(field):
|
||||
"""Render a bound form field with the right daisyUI classes."""
|
||||
widget = field.field.widget
|
||||
css = next((css for widget_type, css in WIDGET_CLASSES if isinstance(widget, widget_type)), DEFAULT_WIDGET_CLASS)
|
||||
#: Icon shown inside the field, by form field name. Anything unlisted gets none.
|
||||
FIELD_ICONS = {
|
||||
"login": "mail",
|
||||
"email": "mail",
|
||||
"email2": "mail",
|
||||
"oldpassword": "lock-keyhole",
|
||||
"password": "lock-keyhole",
|
||||
"password1": "lock-keyhole",
|
||||
"password2": "lock-keyhole",
|
||||
"code": "shield-check",
|
||||
}
|
||||
|
||||
classes = [widget.attrs.get("class", ""), css]
|
||||
if field.errors:
|
||||
classes.append(f"{css.split()[0]}-error")
|
||||
|
||||
@register.filter
|
||||
def field_icon(field):
|
||||
return FIELD_ICONS.get(field.name, "")
|
||||
|
||||
|
||||
@register.filter
|
||||
def daisy(field, css=None):
|
||||
"""Render a bound form field with the right daisyUI classes.
|
||||
|
||||
Pass ``css`` to override them — the icon-in-field layout wraps the input in a
|
||||
``label.input``, and there the input itself must NOT carry the ``input`` class
|
||||
(daisyUI styles the wrapper instead), so it is rendered with ``grow``. The error
|
||||
state then belongs on the wrapper too, which is why an override skips it here.
|
||||
"""
|
||||
widget = field.field.widget
|
||||
|
||||
if css is None:
|
||||
css = next((css for widget_type, css in WIDGET_CLASSES if isinstance(widget, widget_type)), DEFAULT_WIDGET_CLASS)
|
||||
if field.errors:
|
||||
css = f"{css} {css.split()[0]}-error"
|
||||
|
||||
attrs = dict(widget.attrs)
|
||||
attrs["class"] = " ".join(part for part in classes if part)
|
||||
attrs["class"] = " ".join(part for part in [widget.attrs.get("class", ""), css] if part)
|
||||
return field.as_widget(attrs=attrs)
|
||||
|
||||
@@ -19,7 +19,7 @@ from teams.models import Position, Team, TeamMembership
|
||||
from .services.admins import grant_club_admin
|
||||
from .services.platform_admins import PlatformAdminError, is_last_superuser, set_platform_access
|
||||
from .services.statistics import club_statistics, clubs_with_totals, platform_totals
|
||||
from .templatetags.ui import as_alert, daisy
|
||||
from .templatetags.ui import as_alert, daisy, field_icon
|
||||
|
||||
User = get_user_model()
|
||||
Flag = get_waffle_flag_model()
|
||||
@@ -455,3 +455,61 @@ class MessageRenderingTests(ControlPanelTestBase):
|
||||
self.assertContains(response, "alert alert-soft alert-warning")
|
||||
self.assertContains(response, '<div class="font-bold">Careful</div>', html=False)
|
||||
self.assertContains(response, "<svg") # the lucide icon
|
||||
|
||||
|
||||
class FieldRenderingTests(TestCase):
|
||||
def field(self, form_field, name="email", errors=False):
|
||||
class Form(forms.Form):
|
||||
pass
|
||||
|
||||
Form.base_fields[name] = form_field
|
||||
form = Form(data={} if errors else None)
|
||||
if errors:
|
||||
form.full_clean()
|
||||
return form[name]
|
||||
|
||||
def test_known_fields_get_an_icon_and_others_do_not(self):
|
||||
self.assertEqual(field_icon(self.field(forms.EmailField(), "email")), "mail")
|
||||
self.assertEqual(field_icon(self.field(forms.CharField(), "password")), "lock-keyhole")
|
||||
self.assertEqual(field_icon(self.field(forms.CharField(), "note")), "")
|
||||
|
||||
def test_the_default_rendering_styles_the_input_itself(self):
|
||||
self.assertIn('class="input input-bordered w-full"', str(daisy(self.field(forms.CharField()))))
|
||||
|
||||
def test_an_override_replaces_the_daisy_classes(self):
|
||||
# The icon layout puts `input` on the wrapping label, so the input must not
|
||||
# carry it too — that would draw a box inside a box.
|
||||
rendered = str(daisy(self.field(forms.CharField()), "grow"))
|
||||
|
||||
self.assertIn('class="grow"', rendered)
|
||||
self.assertNotIn("input-bordered", rendered)
|
||||
|
||||
def test_an_override_leaves_the_error_state_to_the_wrapper(self):
|
||||
rendered = str(daisy(self.field(forms.CharField(required=True), errors=True), "grow"))
|
||||
|
||||
self.assertNotIn("grow-error", rendered)
|
||||
|
||||
def test_the_default_rendering_marks_errors_on_the_input(self):
|
||||
rendered = str(daisy(self.field(forms.CharField(required=True), errors=True)))
|
||||
|
||||
self.assertIn("input-error", rendered)
|
||||
|
||||
|
||||
class LoginFormRenderingTests(TestCase):
|
||||
def setUp(self):
|
||||
self.response = self.client.get(reverse("account_login"))
|
||||
|
||||
def test_the_fields_carry_an_icon_and_no_visible_label(self):
|
||||
self.assertContains(self.response, 'class="sr-only">Email</span>')
|
||||
self.assertContains(self.response, 'class="sr-only">Password</span>')
|
||||
self.assertNotContains(self.response, '<span class="label-text">Email</span>')
|
||||
self.assertContains(self.response, 'placeholder="Email address"')
|
||||
|
||||
def test_the_checkbox_keeps_its_visible_label(self):
|
||||
self.assertContains(self.response, '<span class="label-text">Remember Me</span>')
|
||||
|
||||
def test_the_password_reset_link_is_spaced_and_addressable(self):
|
||||
# The input's aria-describedby points here; without the id it dangles.
|
||||
self.assertContains(self.response, 'id="id_password_helptext"')
|
||||
self.assertContains(self.response, "mt-3")
|
||||
self.assertContains(self.response, "Forgot your password?")
|
||||
|
||||
Reference in New Issue
Block a user