Make the OTP boxes focus the input when tapped
The overlaid OTP input has pointer-events: none so clicks land on the decorative boxes, but that also means a tap never reached the real input on a touchscreen (desktop got away with it via autofocus/Tab). Wrapping the boxes in a <label for> restores focus-on-click without adding a DOM child that would throw off the otp box count.
This commit is contained in:
@@ -276,6 +276,20 @@ class TwoFactorPageTests(TestCase):
|
|||||||
|
|
||||||
self.assertEqual(otp.count("<span></span>"), 6)
|
self.assertEqual(otp.count("<span></span>"), 6)
|
||||||
|
|
||||||
|
def test_the_boxes_are_wrapped_in_a_label_so_tapping_focuses_the_input(self):
|
||||||
|
# daisyUI's overlaid otp input carries `pointer-events: none` (so clicks land on the
|
||||||
|
# boxes, not a naked input) — which also means a tap on the boxes never reaches the
|
||||||
|
# input directly. A <label for> is what closes that gap: browsers focus a labelled
|
||||||
|
# control on click regardless of the control's own pointer-events. Without this
|
||||||
|
# wrapper the field cannot be entered on a touchscreen, which has no Tab key to fall
|
||||||
|
# back on.
|
||||||
|
html = self.response.content.decode()
|
||||||
|
label_start = html.index('<label class="contents"')
|
||||||
|
otp_start = html.index('class="otp otp-lg"')
|
||||||
|
|
||||||
|
self.assertLess(label_start, otp_start)
|
||||||
|
self.assertIn('for="id_code"', html[label_start : label_start + 60])
|
||||||
|
|
||||||
def test_the_otp_field_has_no_placeholder(self):
|
def test_the_otp_field_has_no_placeholder(self):
|
||||||
# allauth sets placeholder="Code"; inside the boxes it reads as a typed-in code.
|
# allauth sets placeholder="Code"; inside the boxes it reads as a typed-in code.
|
||||||
self.assertNotContains(self.response, 'placeholder="Code"')
|
self.assertNotContains(self.response, 'placeholder="Code"')
|
||||||
|
|||||||
@@ -52,9 +52,20 @@
|
|||||||
|
|
||||||
For the same reason the sr-only label lives outside the container: another
|
For the same reason the sr-only label lives outside the container: another
|
||||||
child would throw the count off again.
|
child would throw the count off again.
|
||||||
|
|
||||||
|
The overlaid input carries daisyUI's own `pointer-events: none` (it exists so
|
||||||
|
clicks land on the boxes, not a naked input) — which also means a tap/click on
|
||||||
|
the boxes never reaches the input directly. A <label for> is what actually
|
||||||
|
closes that gap: browsers focus a labelled control on click regardless of the
|
||||||
|
control's own pointer-events, so wrapping the boxes in one (without adding a
|
||||||
|
child *inside* the otp container, which would break the nth-child count above)
|
||||||
|
makes the whole visible field tappable. Without it the field is a decorative
|
||||||
|
box nobody can enter a code into — invisible on desktop, where autofocus or Tab
|
||||||
|
gets you there anyway, but a dead end on mobile, which has neither.
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
<label class="sr-only" for="{{ field.id_for_label }}">{{ field.label }}</label>
|
<label class="sr-only" for="{{ field.id_for_label }}">{{ field.label }}</label>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
|
<label class="contents" for="{{ field.id_for_label }}">
|
||||||
<div class="otp otp-lg" data-otp>
|
<div class="otp otp-lg" data-otp>
|
||||||
<span></span>
|
<span></span>
|
||||||
<span></span>
|
<span></span>
|
||||||
@@ -64,6 +75,7 @@
|
|||||||
<span></span>
|
<span></span>
|
||||||
{{ field|as_otp }}
|
{{ field|as_otp }}
|
||||||
</div>
|
</div>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{% elif icon %}
|
{% elif icon %}
|
||||||
<label class="input flex w-full items-center gap-2 {% if field.errors %}input-error{% endif %}" for="{{ field.id_for_label }}">
|
<label class="input flex w-full items-center gap-2 {% if field.errors %}input-error{% endif %}" for="{{ field.id_for_label }}">
|
||||||
|
|||||||
Reference in New Issue
Block a user