Put the otp input after its boxes

A seventh box appeared when the field took focus.

daisyUI positions each otp box with nth-child, which counts *every* child of the
container, not just the spans. With the input as the first child, all six boxes
shifted one stride right and the container matched :has(>span:nth-child(7)), so
it grew to seven strides wide. The phantom box was the ::after active-box marker
-- transparent until :focus-within gives it an outline -- sitting in the empty
stride that the off-by-one had opened up.

The input goes last, which is also how daisyUI's own examples order it. A test
counts the boxes ahead of the input so this cannot come back.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 23:16:35 +02:00
parent fa213d95ee
commit add1ee87ae
2 changed files with 24 additions and 9 deletions

View File

@@ -265,6 +265,15 @@ class TwoFactorPageTests(TestCase):
self.assertContains(self.response, 'name="code"')
self.assertContains(self.response, "otp otp-lg")
def test_the_input_comes_after_the_boxes(self):
# daisyUI places each box with nth-child, which counts every child. With the input
# first, all six boxes shift a stride right, the container grows to seven strides
# and the ::after focus marker appears as a phantom seventh box.
html = self.response.content.decode()
otp = html[html.index('class="otp otp-lg"') : html.index('name="code"')]
self.assertEqual(otp.count("<span></span>"), 6)
def test_the_otp_field_has_no_placeholder(self):
# allauth sets placeholder="Code"; inside the boxes it reads as a typed-in code.
self.assertNotContains(self.response, 'placeholder="Code"')