Skip to content
Merged
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
26 changes: 26 additions & 0 deletions docs/source/markup.rst
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,32 @@ uses XML-friendly "-"-separated attribute names in markup.
</fieldset>
</form:with>

**Targeting Radio Buttons and Checkboxes:**

When working with radio buttons or checkboxes, each input element with a
different ``value=`` attribute gets a unique ``id`` by appending the value
to the base id (e.g., ``f_choice_a``, ``f_choice_b``). To generate a
label that targets a specific radio button or checkbox, provide a
``value=`` attribute on the label tag:

.. doctest:: transforms2

>>> from flatland import String
>>> from flatland.out.markup import Generator
>>> html = Generator(auto_domid=True, auto_for=True)
>>> choice = String.named('choice')('b')
>>> print(html.label(choice, value='a', contents='Option A'))
<label for="f_choice_a">Option A</label>
>>> print(html.input(choice, type='radio', value='a'))
<input type="radio" name="choice" value="a" id="f_choice_a" />
>>> print(html.label(choice, value='b', contents='Option B'))
<label for="f_choice_b">Option B</label>
>>> print(html.input(choice, type='radio', value='b'))
<input type="radio" name="choice" value="b" checked="checked" id="f_choice_b" />

The ``value=`` attribute is automatically removed from the rendered label
tag and is only used to generate the correct ``for=`` attribute.


.. describe:: auto-tabindex

Expand Down
28 changes: 17 additions & 11 deletions src/flatland/out/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,15 +201,15 @@ def transform_domid(tagname, attributes, contents, context, bind):
@defaults({"auto_for": False})
def transform_for(tagname, attributes, contents, context, bind):
proceed, forced = _pop_toggle("auto_for", attributes, context)
if not proceed or bind is None:
return contents

current = attributes.get("for")
if forced or current is None and tagname in _auto_tags["for"]:
raw_id = _generate_raw_domid(tagname, attributes, bind)
if raw_id:
fmt = context["domid_format"]
attributes["for"] = fmt % raw_id
if proceed and bind is not None:
current = attributes.get("for")
if forced or current is None and tagname in _auto_tags["for"]:
raw_id = _generate_raw_domid(tagname, attributes, bind)
if raw_id:
fmt = context["domid_format"]
attributes["for"] = fmt % raw_id
if tagname == "label":
attributes.pop("value", None)
return contents


Expand Down Expand Up @@ -282,11 +282,17 @@ def _generate_raw_domid(tagname, attributes, bind):
if not basis:
return

suffix = None
# add the value="" to CHECKBOX and RADIO to produce a unique ID
if tagname == "input" and attributes.get("type") in ("checkbox", "radio"):
suffix = _sanitize_domid_suffix(attributes.get("value", ""))
if suffix:
basis += "_" + suffix
# when the value attribute is supplied for a LABEL, add it to domid.
# this is used to produce LABELs with matching for="..." attribute values
# corresponding to input checkbox/radio IDs.
if tagname == "label":
suffix = _sanitize_domid_suffix(attributes.get("value", ""))
if suffix:
basis += "_" + suffix
return basis


Expand Down
155 changes: 155 additions & 0 deletions tests/markup/test_label_for_radio_checkbox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
"""Tests for label 'for' attribute generation with radio/checkbox inputs."""
from flatland import String
from flatland.out.markup import Generator

import pytest


@pytest.fixture
def schema():
return String.named("choice")


@pytest.fixture
def html():
return Generator(markup="html", auto_domid=True, auto_for=True)


@pytest.fixture
def xml():
return Generator(markup="xml", auto_domid=True, auto_for=True)


def test_complete_radio_form_example(schema, html):
"""Test a complete example form."""
el = schema("b")

# Generate label for option 'a'
label_a = html.label(el, value="a", contents="Option A")
assert 'for="f_choice_a"' in label_a
assert 'Option A' in label_a
assert 'value="a"' not in label_a

# Generate radio input for option 'a'
input_a = html.input(el, type="radio", value="a")
assert 'id="f_choice_a"' in input_a
assert 'name="choice"' in input_a
assert 'type="radio"' in input_a
assert 'checked' not in input_a
assert 'value="a"' in input_a

# Generate label for option 'b'
label_b = html.label(el, value="b", contents="Option B")
assert 'for="f_choice_b"' in label_b
assert 'Option B' in label_b
assert 'value="b"' not in label_b

# Generate radio input for option 'b' (should be checked)
input_b = html.input(el, type="radio", value="b")
assert 'id="f_choice_b"' in input_b
assert 'name="choice"' in input_b
assert 'type="radio"' in input_b
assert 'checked="checked"' in input_b
assert 'value="b"' in input_b


def test_label_without_value_backward_compatibility(schema, html):
"""Test that labels without a value attribute still work as before."""
el = schema("b")

# Label without value should generate for="f_choice" (no suffix)
label = html.label(el)
assert 'for="f_choice"' in label
assert 'value=' not in label


def test_label_with_empty_value(schema, html):
"""Test that label with empty value doesn't add suffix."""
el = schema("test")

# Label with empty value should not add suffix
label = html.label(el, value="")
assert 'for="f_choice"' in label
assert 'value=' not in label


def test_label_value_sanitization(schema, html):
"""Test that label value is sanitized like input values."""
el = schema("test")

# Value with special characters should be sanitized
label = html.label(el, value="option-1")
assert 'for="f_choice_option-1"' in label

# Value with invalid characters should be sanitized
label2 = html.label(el, value="option@#$1")
# Invalid characters should be removed, leaving "option1"
assert 'for="f_choice_option1"' in label2


def test_label_xml_format(schema, xml):
"""Test that the label value attribute works with XML format."""
el = schema("b")

label = xml.label(el, value="a")
assert 'for="f_choice_a"' in label
assert 'value=' not in label

# Label is not a void element, so it has a closing tag
assert '</label>' in label


def test_label_explicit_for_not_overridden(schema, html):
"""Test that explicitly provided 'for' attribute is not overridden."""
el = schema("b")

# Explicit 'for' should take precedence
label = html.label(el, value="a", **{"for": "custom_id"})
assert 'for="custom_id"' in label
# Value should still be removed from attributes
assert 'value=' not in label


def test_label_auto_for_disabled(schema):
"""Test that the value attribute has no effect when auto_for is disabled."""
html = Generator(markup="html", auto_domid=True, auto_for=False)
el = schema("b")

# With auto_for=False, no 'for' attribute should be generated
label = html.label(el, value="a")
assert 'for=' not in label
# Value should still be removed
assert 'value=' not in label


def test_label_with_contents_and_value(schema, html):
"""Test that the label can have both contents and value attribute."""
el = schema("b")

label = html.label(el, value="a", contents="Option A")
assert 'for="f_choice_a"' in label
assert 'Option A' in label
assert 'value=' not in label


def test_label_value_with_numbers(schema, html):
"""Test that numeric values work correctly."""
el = schema("1")

label = html.label(el, value="1")
assert 'for="f_choice_1"' in label

label2 = html.label(el, value="123")
assert 'for="f_choice_123"' in label2


def test_label_forced_auto_for_with_value(schema):
"""Test that forced auto_for works with a value attribute."""
html = Generator(markup="html", auto_domid=True, auto_for=False)
el = schema("b")

# Force auto_for with auto_for="on"
label = html.label(el, value="a", auto_for="on")
assert 'for="f_choice_a"' in label
assert 'value=' not in label
assert 'auto_for=' not in label