Skip to main content

max / makenotwork

mnw-server: fix discover filter aria state and header affordances The filter sidebar's htmx:beforeRequest handler toggled only the is-selected class, so the server-rendered aria-selected went stale on the first click and then inverted: the selected item reported false while the deselected "All" still reported true. These are role=option in a role=listbox, so assistive tech reads the attribute rather than the class and the listbox announced the opposite of the screen. Set the attribute alongside the class. The column headers were gated on mode but never on view, so the default grid view rendered a five-column table header above a card grid, labelling nothing. Toggle them from applyView, which also covers the htmx:afterSwap re-apply so the header cannot drift out of sync after a filter swap. The headers also carried cursor: pointer plus a hover response while nothing bound them, inviting a click that did nothing. Sorting exists via the sort select, which made the invitation more plausible. Both rules were global, so feed.html's equally unsortable headers are fixed too. Found and verified in a real browser against testnot.work. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-16 15:01 UTC
Commit: dd83799d7e87b0576aab24df71a2d022edd60fc6
Parent: 202af2d
2 files changed, +15 insertions, -4 deletions
@@ -3,12 +3,18 @@
3 3 // search/sort submissions include it.
4 4 document.body.addEventListener('htmx:beforeRequest', function(evt) {
5 5 if (evt.detail.elt.classList.contains('filter-item')) {
6 - // Only deactivate siblings in the same filter section
6 + // Only deactivate siblings in the same filter section.
7 + // aria-selected must track is-selected: these are role="option" in a
8 + // role="listbox", so assistive tech reads the attribute, not the class.
7 9 var section = evt.detail.elt.closest('.filter-list');
8 10 if (section) {
9 - section.querySelectorAll('.filter-item').forEach(function(i) { i.classList.remove('is-selected'); });
11 + section.querySelectorAll('.filter-item').forEach(function(i) {
12 + i.classList.remove('is-selected');
13 + i.setAttribute('aria-selected', 'false');
14 + });
10 15 }
11 16 evt.detail.elt.classList.add('is-selected');
17 + evt.detail.elt.setAttribute('aria-selected', 'true');
12 18
13 19 var hxVals = JSON.parse(evt.detail.elt.getAttribute('hx-vals') || '{}');
14 20 if ('item_type' in hxVals) {
@@ -40,6 +46,11 @@
40 46 document.querySelectorAll('.view-btn').forEach(function(btn) {
41 47 btn.classList.toggle('is-selected', btn.dataset.view === view);
42 48 });
49 + // The column headers label the list layout; in grid view they label nothing.
50 + var header = document.querySelector('.table-header');
51 + if (header) {
52 + header.classList.toggle('is-hidden', view !== 'list');
53 + }
43 54 }
44 55
45 56 // Load saved preference on page load
@@ -7480,8 +7480,8 @@ textarea:focus-visible {
7480 7480 .sort-select { background: var(--surface-page); border: none; padding: 0.3rem 0.4rem; font-size: 0.75rem; font-family: inherit; cursor: pointer; width: auto; flex-shrink: 0; }
7481 7481 .table-header { display: grid; grid-template-columns: 50px 1fr 100px 70px 70px; gap: 0.5rem; padding: 0.5rem 0.75rem; background: var(--surface-overlay); font-size: 0.75rem; opacity: 0.7; text-transform: uppercase; letter-spacing: 0.03em; }
7482 7482 .table-header.projects-header { grid-template-columns: 1fr 100px 80px 70px; }
7483 - .table-header span { cursor: pointer; }
7484 - .table-header span:hover { opacity: 0.6; }
7483 + /* Column headers describe the list layout only; hidden when the grid view is active. */
7484 + .table-header.is-hidden { display: none; }
7485 7485 .col-right { text-align: right; }
7486 7486 .results-table { border: 1px solid var(--border); border-top: none; }
7487 7487 .table-row { display: grid; grid-template-columns: 50px 1fr 100px 70px 70px; gap: 0.5rem; padding: 0.4rem 0.75rem; align-items: center; text-decoration: none; color: var(--content); font-size: 0.85rem; border-bottom: 1px solid var(--border); transition: background 0.1s ease; }