Table filters: emptying a header filter folds it shut around the box being typed in, and focus is lost #626

Closed
opened 2026-10-01 10:48:29 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading the column header template, then typing into and clearing a header filter on Companies in headless Chromium.

Checked again on 2026-10-01 against main at 0a5cad587: still present.

Each column filter is a <details> that the server renders open only while the column has a value (header.filtered). The text and number boxes are hx-preserve, so after the table swap htmx moves the old, focused box into the freshly rendered header. When the person has just emptied the box, that header is rendered closed, so the box they are typing in ends up inside a closed disclosure: it disappears, focus falls to the document body, and the next keystrokes go nowhere. Nothing in app.js reopens it.

  • Where: templates/cotton/table/head.html:110 (<details class="col-filter" data-col-filter {% if header.filtered %}open{% endif %}>); core/tables.py:152-161 (filtered is bool(self.value or self.value_from or self.value_to)); templates/cotton/table/filter.html:37-40, :62 and :71 (hx-preserve on the live boxes). The choice and date controls are not preserved, so htmx tries to refocus their replacement by id, which sits inside the closed disclosure and cannot take focus.
  • What happens: on Companies, open Name, type ac (the table narrows; the disclosure stays open because the column is now filtered), then delete both letters to try another word. 300 ms later the table swaps: the Name disclosure is closed, the box is no longer visible and document.activeElement is <body> (observed). Typing x next changes nothing: the box is still empty and no request is sent. Choosing Any in a choice filter or clearing a date has the same result.
  • Why it matters: clearing a filter to try another value is the ordinary way to use it. Everyone loses the box they were typing in; a keyboard or screen-reader user is also put back at the top of the document, which is the failure #227 set out to remove (WCAG 2.2 2.4.3, focus order, and 3.2.2, on input).
  • Fix: keep open the disclosure whose control started the request. On the server, render open also when the request is an htmx request whose HX-Trigger is one of that header's input ids (header.input_id, -min, -max, -from, -to); scripts-off rendering is unchanged. Alternatively, in app.js, note the open [data-col-filter] that contains document.activeElement on htmx:beforeSwap and reopen it on htmx:afterSwap, before htmx restores focus.

Done when

  • A browser test (beside the header-filter tests in tests/e2e/) opens a header filter, types a value, deletes it, and asserts that the disclosure is still open and the box still has focus; a second case does the same with Any in a choice filter.
  • If the server-side route is taken, a test in tests/test_tables.py asserts that an htmx request triggered by filter-name with an empty value renders the Name disclosure open.
Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading the column header template, then typing into and clearing a header filter on *Companies* in headless Chromium. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. Each column filter is a `<details>` that the server renders `open` only while the column has a value (`header.filtered`). The text and number boxes are `hx-preserve`, so after the table swap htmx moves the old, focused box into the freshly rendered header. When the person has just emptied the box, that header is rendered closed, so the box they are typing in ends up inside a closed disclosure: it disappears, focus falls to the document body, and the next keystrokes go nowhere. Nothing in `app.js` reopens it. - **Where:** `templates/cotton/table/head.html:110` (`<details class="col-filter" data-col-filter {% if header.filtered %}open{% endif %}>`); `core/tables.py:152-161` (`filtered` is `bool(self.value or self.value_from or self.value_to)`); `templates/cotton/table/filter.html:37-40`, `:62` and `:71` (`hx-preserve` on the live boxes). The choice and date controls are not preserved, so htmx tries to refocus their replacement by id, which sits inside the closed disclosure and cannot take focus. - **What happens:** on *Companies*, open *Name*, type `ac` (the table narrows; the disclosure stays open because the column is now filtered), then delete both letters to try another word. 300 ms later the table swaps: the *Name* disclosure is closed, the box is no longer visible and `document.activeElement` is `<body>` (observed). Typing `x` next changes nothing: the box is still empty and no request is sent. Choosing *Any* in a choice filter or clearing a date has the same result. - **Why it matters:** clearing a filter to try another value is the ordinary way to use it. Everyone loses the box they were typing in; a keyboard or screen-reader user is also put back at the top of the document, which is the failure #227 set out to remove (WCAG 2.2 2.4.3, focus order, and 3.2.2, on input). - **Fix:** keep open the disclosure whose control started the request. On the server, render `open` also when the request is an htmx request whose `HX-Trigger` is one of that header's input ids (`header.input_id`, `-min`, `-max`, `-from`, `-to`); scripts-off rendering is unchanged. Alternatively, in `app.js`, note the open `[data-col-filter]` that contains `document.activeElement` on `htmx:beforeSwap` and reopen it on `htmx:afterSwap`, before htmx restores focus. ## Done when - A browser test (beside the header-filter tests in `tests/e2e/`) opens a header filter, types a value, deletes it, and asserts that the disclosure is still open and the box still has focus; a second case does the same with *Any* in a choice filter. - If the server-side route is taken, a test in `tests/test_tables.py` asserts that an htmx request triggered by `filter-name` with an empty value renders the *Name* disclosure `open`.
tiagoagueda added this to the 0.5.0 milestone 2026-10-01 10:48:29 +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#626
No description provided.