Telephone and web address rows: say what it is first, then the value #213

Open
opened 2026-09-15 19:39:59 +00:00 by tiagoagueda · 0 comments
Owner

In the rows for telephone numbers, social profiles, code repositories and websites, the fields should come in the order a person decides them: first what the entry is, then the value itself. Today every one of these rows puts the value first and describes it afterwards.

Telephone numbers:

  1. Kind (Mobile, Work, Home, Switchboard, Fax, Other)
  2. Name, shown only when Kind is Other
  3. Country
  4. Number

Social profiles, code repositories and websites:

  1. Name (optional: "LinkedIn", "Codeberg", "my blog")
  2. Address

Then, in both kinds of row, the Primary / Gets me back in / Remove controls.

What it is today

Telephone numbers

src/postulo/templates/partials/phone_numbers.html, included by Your details (accounts/profile.html:121) and the contact form (jobs/contact_form.html:38) when Several telephone numbers is on:

  • Line 1 (:33-36): Phone, which is the PhoneField widget (partials/phone_widget.html): the country select with its flag, then the number box.
  • Line 2 (:37-83): a three-column grid holding Kind, Name, if Other, and the Primary / Gets me back in / Remove controls.

The form declares fields = ("kind", "label", "number") (core/phone_numbers.py:297), but the template draws them in its own order. Name, if Other is always visible, even though it only means something for Other. The server already requires it in that case (PhoneNumberForm.clean, :313-317), and nothing requires or uses it otherwise.

Social profiles, code repositories and websites

src/postulo/templates/partials/web_links.html (#189) draws one block per kind, each only while its feature is on, on Your details and on the contact form:

  • Line 1 (:26-29): Address (url), full width.
  • Line 2 (:30-47): a two-column grid with Name (label) and the Primary / Remove controls.

These rows have no kind field: each block is a formset of one kind, and the block's legend says which (core/web_links.py:60-124, WebLinkForm.fields = ("label", "url") at :295). Name is optional and always meaningful (left blank, the address's host is shown instead, per the note at :55-60 and the model's help text at core/models.py:537), so nothing about it is conditional. The change is only the order: Name, then Address on its own full-width line, then the controls.

Why it is the way it is

A comment in phone_numbers.html (:28-32) says the number was given its own line deliberately. Squeezed into a column beside two others, the fixed 10rem country select left the number box a few pixels wide, which is unusable and a WCAG 2.2 2.5.8 target-size failure that axe caught. The new order must not bring that back: country and number stay together as one full-width control (they are one widget already, country first), below kind and name, not squeezed beside them.

web_links.html (:23-25) gives the same reason for the address: it is the long thing in the row. That still holds, so the address keeps its own full-width line, now below the name.

What a fix has to settle

  • Order in phone_numbers.html: a line for Kind (and Name when shown), then the phone widget on its own line, then Primary / recovery / Remove. The DOM order is the reading and tab order, so no order-* utilities.
  • Order in web_links.html: Name on one line, then Address on its own full-width line, then Primary / Remove. There is no kind to add and nothing to show or hide.
  • One arrangement for both. The two partials should end up the same shape — the description, the value on its own line, the controls — so a page with both kinds of row reads consistently.
  • Showing Name only for Other, without breaking scripts-off use:
    • The server renders the field hidden unless the row's kind is already Other, either on load or after a failed submit where the person chose Other.
    • app.js shows and hides it as Kind changes, delegated from the document like the rest of the file (the CSP forbids inline handlers), and moves focus nowhere.
    • With scripts blocked the field must still be reachable, or a person choosing Other cannot give the name the server then demands. Either leave it visible when the script has not run (the script hides it, the template does not), or give a <noscript> fallback. The first is simpler and matches how the dragging code sets draggable only from script.
    • Hidden means hidden, so it is out of the accessibility tree too. When it appears, it follows Kind in the reading order, so a screen reader meets it next without any announcement.
  • The label can then drop "if Other" and read Name, since it only appears when that is true. Keep the model's verbose_name and help text in step (core/models.py:180-183); that is a translation change in every catalogue.
  • A value typed under Other and then abandoned (kind changed away from Other): decide whether it is cleared on save or kept silently. Clearing it matches what the field means.
  • Layout on a phone: everything stacks in the new order, and nothing sits beside the phone widget below sm:.
  • The single boxes are unaffected: the one telephone field while Several telephone numbers is off (accounts/forms.py:301-312) and the one URL box per kind while its Several … feature is off (core/web_links.py:248-267) have no kind or name to reorder.

The same shape elsewhere

These rows have the same Kind + Name, if Other pair:

  • postal addresses (partials/postal_addresses.html:63);
  • the identifier rows on Your details (accounts/profile.html:153);
  • company identifiers (jobs/company_form.html:80).

The show-only-for-Other behaviour belongs in one place in app.js (e.g. a data-shows-for-kind="other" attribute on the name field's wrapper) so they can adopt it. Whether they also change order should be decided here and filed separately, not done quietly as part of this.

Checks

  • Tests:
    • a telephone row rendered with kind Other shows the name field, and a row with any other kind renders it with hidden; a failed submit with Other and no name shows it with its error;
    • in both partials, the description's field comes before the value's field in the rendered HTML (kind before number; name before address).
  • Browser suite: tests/e2e/test_accessibility.py visits Your details and the contact form with all three link features on; keep axe clean at phone width, and add a step that picks Other and finds the field.
  • CSS: npm run build:css if new classes appear.
  • Translations: scripts/messages.py extract for the changed label.
In the rows for telephone numbers, social profiles, code repositories and websites, the fields should come in the order a person decides them: first what the entry *is*, then the value itself. Today every one of these rows puts the value first and describes it afterwards. **Telephone numbers:** 1. **Kind** (Mobile, Work, Home, Switchboard, Fax, Other) 2. **Name**, shown **only when Kind is *Other*** 3. **Country** 4. **Number** **Social profiles, code repositories and websites:** 1. **Name** (optional: "LinkedIn", "Codeberg", "my blog") 2. **Address** Then, in both kinds of row, the *Primary* / *Gets me back in* / *Remove* controls. ## What it is today ### Telephone numbers `src/postulo/templates/partials/phone_numbers.html`, included by *Your details* (`accounts/profile.html:121`) and the contact form (`jobs/contact_form.html:38`) when *Several telephone numbers* is on: - **Line 1 (`:33-36`):** *Phone*, which is the `PhoneField` widget (`partials/phone_widget.html`): the country select with its flag, then the number box. - **Line 2 (`:37-83`):** a three-column grid holding *Kind*, *Name, if Other*, and the Primary / *Gets me back in* / Remove controls. The form declares `fields = ("kind", "label", "number")` (`core/phone_numbers.py:297`), but the template draws them in its own order. *Name, if Other* is always visible, even though it only means something for *Other*. The server already requires it in that case (`PhoneNumberForm.clean`, `:313-317`), and nothing requires or uses it otherwise. ### Social profiles, code repositories and websites `src/postulo/templates/partials/web_links.html` (#189) draws one block per kind, each only while its feature is on, on *Your details* and on the contact form: - **Line 1 (`:26-29`):** *Address* (`url`), full width. - **Line 2 (`:30-47`):** a two-column grid with *Name* (`label`) and the Primary / Remove controls. These rows have **no kind field**: each block is a formset of one kind, and the block's legend says which (`core/web_links.py:60-124`, `WebLinkForm.fields = ("label", "url")` at `:295`). *Name* is optional and always meaningful (left blank, the address's host is shown instead, per the note at `:55-60` and the model's help text at `core/models.py:537`), so nothing about it is conditional. The change is only the order: *Name*, then *Address* on its own full-width line, then the controls. ## Why it is the way it is A comment in `phone_numbers.html` (`:28-32`) says the number was given its own line deliberately. Squeezed into a column beside two others, the fixed 10rem country select left the number box a few pixels wide, which is unusable and a WCAG 2.2 2.5.8 target-size failure that axe caught. The new order must not bring that back: country and number stay together as one full-width control (they are one widget already, country first), below kind and name, not squeezed beside them. `web_links.html` (`:23-25`) gives the same reason for the address: it is the long thing in the row. That still holds, so the address keeps its own full-width line, now below the name. ## What a fix has to settle - **Order in `phone_numbers.html`:** a line for *Kind* (and *Name* when shown), then the phone widget on its own line, then Primary / recovery / Remove. The DOM order is the reading and tab order, so no `order-*` utilities. - **Order in `web_links.html`:** *Name* on one line, then *Address* on its own full-width line, then Primary / Remove. There is no kind to add and nothing to show or hide. - **One arrangement for both.** The two partials should end up the same shape — the description, the value on its own line, the controls — so a page with both kinds of row reads consistently. - **Showing Name only for Other, without breaking scripts-off use:** - The server renders the field hidden unless the row's kind is already *Other*, either on load or after a failed submit where the person chose *Other*. - `app.js` shows and hides it as *Kind* changes, delegated from the document like the rest of the file (the CSP forbids inline handlers), and moves focus nowhere. - **With scripts blocked the field must still be reachable,** or a person choosing *Other* cannot give the name the server then demands. Either leave it visible when the script has not run (the script hides it, the template does not), or give a `<noscript>` fallback. The first is simpler and matches how the dragging code sets `draggable` only from script. - Hidden means `hidden`, so it is out of the accessibility tree too. When it appears, it follows *Kind* in the reading order, so a screen reader meets it next without any announcement. - **The label** can then drop "if Other" and read *Name*, since it only appears when that is true. Keep the model's `verbose_name` and help text in step (`core/models.py:180-183`); that is a translation change in every catalogue. - **A value typed under Other and then abandoned** (kind changed away from *Other*): decide whether it is cleared on save or kept silently. Clearing it matches what the field means. - **Layout on a phone:** everything stacks in the new order, and nothing sits beside the phone widget below `sm:`. - **The single boxes** are unaffected: the one telephone field while *Several telephone numbers* is off (`accounts/forms.py:301-312`) and the one URL box per kind while its *Several …* feature is off (`core/web_links.py:248-267`) have no kind or name to reorder. ## The same shape elsewhere These rows have the same *Kind* + *Name, if Other* pair: - postal addresses (`partials/postal_addresses.html:63`); - the identifier rows on *Your details* (`accounts/profile.html:153`); - company identifiers (`jobs/company_form.html:80`). The show-only-for-Other behaviour belongs in one place in `app.js` (e.g. a `data-shows-for-kind="other"` attribute on the name field's wrapper) so they can adopt it. Whether they also change order should be decided here and filed separately, not done quietly as part of this. ## Checks - **Tests:** - a telephone row rendered with kind *Other* shows the name field, and a row with any other kind renders it with `hidden`; a failed submit with *Other* and no name shows it with its error; - in both partials, the description's field comes before the value's field in the rendered HTML (kind before number; name before address). - **Browser suite:** `tests/e2e/test_accessibility.py` visits *Your details* and the contact form with all three link features on; keep axe clean at phone width, and add a step that picks *Other* and finds the field. - **CSS:** `npm run build:css` if new classes appear. - **Translations:** `scripts/messages.py extract` for the changed label.
tiagoagueda changed title from Telephone number rows: kind first, then a name only for Other, then country and number to Telephone and web address rows: say what it is first, then the value 2026-09-15 19:42:18 +00:00
tiagoagueda added this to the 0.6.0 milestone 2026-09-15 21:33:34 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Postulo/postulo#213
No description provided.