Settings/NLDD: what was evaluated and not made, and when it expires #208

Closed
opened 2026-07-26 21:23:55 +00:00 by robbertbos · 1 comment
Owner

While putting Settings on NLDD's form system (#201), a set of further changes
was evaluated and deliberately not made. This records what was already tried
and why, so the same proposals do not get re-investigated from scratch.

Every claim below is pinned to a version. The original list was checked
against @nldd/design-system 0.8.70. NLDD changes behaviour inside patch
releases, so a rejection here is only as good as its date - and four of them
had already expired by 0.8.79. Re-verify before treating any of this as
settled.

Last verified against 0.8.79, 2026-08-09.

Done

Landed in #272 (merged):

  • Field-level error association in AccountPanel - but not as proposed here.
    See "Corrected" below.
  • The ImportPanel recovery-code field -> nldd-form-field +
    nldd-password-field, with the decryption failure routed onto it.
  • MattermostServerFields.vue extracted. Only the base-URL and label fields:
    they are byte-identical between the two forms, while the token field differs
    in label, placeholder and its onboarding hint.
  • The nldd-title :size drift between the two modals. It was a symptom, not a
    design question: both dialogs sat outside the shared shell. Settled by shape
    rather than by consistency - the delete confirm carries a form and moved to
    Modal.vue; the disconnect confirm is a question with buttons and stays on
    NLDD's own nldd-modal-dialog, wrapped as ConfirmDialog.vue so it
    registers with the overlay stack. Without that registration the single-key
    shortcuts stayed live behind it, because its <dialog> sits in a shadow root
    that document.querySelector cannot reach.
  • A visible error when the typed e-mail does not match, on blur.
  • The Thema control, and Sync-interval with it. Not nldd-segmented-control:
    nldd-radio-button-group, which the same panel already used for the
    browser/desktop choice. Sync-interval also lost its 10- and 30-minute steps,
    so both groups are three options. The retention fields keep dropdowns - more
    options, longer labels.

Corrected

Routing 422 to the Token field was wrong. The endpoint answered 422 for
three unrelated causes: Pydantic validation (token shape, and the SSRF guard
on base_url), an invalid token, and an unreachable server. Pasting
http://10.0.0.5/chat reported "Token is ongeldig" on a token that was fine,
and attaching that to the Token field would have made it worse. #272 splits the
statuses first (unreachable is now 502) and routes on the validation body's
loc.

The objection to the original formErrorMessage still stands and is why the
replacement returns a target rather than a message: an error with no status
is panel-level, never nothing.

Expired - do not trust these without re-checking

  • "Clicking an nldd-form-field label focuses nothing." Fixed for
    nldd-text-field and nldd-password-field, which now delegate focus. Still
    true for nldd-dropdown, nldd-radio-button-group and
    nldd-checkbox-field, which implement neither focus() nor
    delegatesFocus. Measured on 0.8.79, with a positive control:

    ff-text   activeElement = nldd-text-field > input#nldd-field-input-…
    ff-pass   activeElement = nldd-password-field > input#nldd-field-…
    ff-drop   activeElement = body
    ff-radio  activeElement = body
    ff-check  activeElement = body
    
  • "nldd-form-actions outside an nldd-form contributes only a label-column
    spacer."
    Never accurate. The spacer is conditional on label-alignment and
    a container of at least 641px; outside a form it contributes nothing.

  • "nldd-checkbox-field has a hard 44px row." It is a token-driven
    min-height (--semantics-controls-md-min-size), overridable, and it grows
    with a wrapping label.

  • "nldd-banner is for persistent, page-level feedback." The doc comment
    still says so, but 0.8.78 added size="sm", documented for "a notice that has
    to sit inside a pane or above a toolbar". The rejection for the per-server
    warning row is weaker than it was. Not acted on.

Still rejected on mechanism, verified at 0.8.79

  • nldd-form anywhere. Export, Preferences, Import and the delete dialog
    have no submit. It also auto-migrates direct children into an inner <form>
    via a MutationObserver (there is an opt-out: supply your own <form>).
    --semantics-forms-gap-tight still exists and still buys the rhythm.
  • nldd-form-section. Still renders its title as a <legend>: a group
    label that heading navigation skips. nldd-title stays the home for panel
    headings.
  • A shared SettingsAlert.vue would re-implement nldd-banner, which sets
    role and aria-atomic itself. Note the split is critical versus everything
    else, not warning versus critical: warning gets role="status" alongside
    neutral, accent and success.
  • nldd-checkbox-field on the diff-table row. Its label is still a plain
    string with no slot and no parts, so it would duplicate or destroy the visible
    label in the neighbouring <td>.
  • The import drop zone. _findInput() still returns the first non-helper
    child, so it would treat the <label> wrapper as the input and put the id,
    the aria-label and the aria-describedby on it. 0.8.79 still ships no
    file-upload component.
  • A hint on the Sync-interval dropdown. nldd-dropdown still has no
    inputId, so nldd-form-field writes aria-describedby onto the role-less
    host and it never reaches the slotted <select>. This is also why the
    aria-label on that <select> is load-bearing.
  • nldd-form-field around AccountDeletePanel's two consent checkboxes.
    Only the first would be treated as "the input", and a plain <label> is
    weaker than the <fieldset> plus visually-hidden <legend> already there.
  • nldd-switch-field for the browser/desktop-app choice - two named
    destinations, not an on/off state. It is a single boolean checked.

Settled by measurement

  • nldd-table for the diff table. Now evaluated. It fits mechanically -
    nldd-cell takes arbitrary content, and the design system itself suggests a
    checkbox column - but the family contains no <table>, <th scope> or
    <caption> anywhere: it is a CSS grid with role="table",
    role="columnheader" and role="cell". The current markup is a native table
    with real <th scope="col">, so this would trade semantics for roles.
    Rejected.

  • The <output aria-label="Herstelcode, 44 karakters"> in RecoveryKeyModal.
    Did not need a screen reader after all - Chromium's accessibility tree answers
    it:

    status name="Herstelcode, 44 karakters"
      StaticText name="aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789-_aBcDe="
    

    The label becomes the accessible name; the key stays exposed as a text child.
    The feared substitution does not happen. No change.

  • nldd-page inside nldd-window for the modal title bar. nldd-window's
    own docs say it has no header and point consumers at nldd-page with a
    sticky header, so Modal.vue's hand-written header looks like a deviation.
    Tried on 2026-08-09; it costs two things. Measured with a dialog open, the
    flattened tree carries two visible main landmarks:

    doc > main                    ← the route
    doc > nldd-page#shadow > main ← nldd-page
    

    That is the landmark-one-main violation tests/e2e/landmarks.spec.ts
    guards app-wide. The scroll region also moves into nldd-page's shadow root,
    so Modal.vue's scroll-padding-block stops applying and nothing in the app
    can reach it - long-message-thread-scroll.spec.ts fails on exactly that.
    The guidance is written for a window used as a window, not for a dialog
    layered over a route that already has a <main>. Rejected, with the
    reasoning in Modal.vue's docblock.

Still open

Nothing. The last item - a retry after a failed Voorkeuren save - is handled in
#275, which gives both auto-saving panels an "Opnieuw proberen" button.
Automatisch opruimen turned out to show no failure at all, which was not in this
list; it gets the same notice there.

Upstream

Reported as MinBZK/storybook#188. Wider than this list had it: on
0.8.79 the label click does nothing for ten components, not three -
dropdown, checkbox, checkbox-field, radio-button, radio-button-field,
radio-button-group, segmented-control, stepper, toggle-button and
toggle-button-group. _focusInput calls .focus() on the slotted host, which
only lands where the component has delegatesFocus or its own focus().

The argument that carries it is the asymmetry inside the design system:
nldd-switch-field has a focus() override and works, while
nldd-checkbox-field and nldd-radio-button-field - the same kind of control -
do not.

Verified in a blank page loading only the package, with real mouse clicks and a
per-component control. Worth knowing for us: the components' own labels do work,
so a user loses only the field-level label click.

Revisit when

The NLDD minor or patch version changes. Everything above is a claim about
component internals, and this list has already been wrong once for exactly that
reason. frontend/node_modules/@nldd/design-system/dist/components/ is the
source of truth; the skill's changelog says what moved.

While putting Settings on NLDD's form system (#201), a set of further changes was evaluated and deliberately not made. This records what was already tried and why, so the same proposals do not get re-investigated from scratch. **Every claim below is pinned to a version.** The original list was checked against `@nldd/design-system` **0.8.70**. NLDD changes behaviour inside patch releases, so a rejection here is only as good as its date - and four of them had already expired by 0.8.79. Re-verify before treating any of this as settled. *Last verified against 0.8.79, 2026-08-09.* ## Done Landed in #272 (merged): - Field-level error association in AccountPanel - **but not as proposed here**. See "Corrected" below. - The ImportPanel recovery-code field -> `nldd-form-field` + `nldd-password-field`, with the decryption failure routed onto it. - `MattermostServerFields.vue` extracted. Only the base-URL and label fields: they are byte-identical between the two forms, while the token field differs in label, placeholder and its onboarding hint. - The `nldd-title :size` drift between the two modals. It was a symptom, not a design question: both dialogs sat outside the shared shell. Settled by shape rather than by consistency - the delete confirm carries a form and moved to `Modal.vue`; the disconnect confirm is a question with buttons and stays on NLDD's own `nldd-modal-dialog`, wrapped as `ConfirmDialog.vue` so it registers with the overlay stack. Without that registration the single-key shortcuts stayed live behind it, because its `<dialog>` sits in a shadow root that `document.querySelector` cannot reach. - A visible error when the typed e-mail does not match, on blur. - The Thema control, and Sync-interval with it. Not `nldd-segmented-control`: `nldd-radio-button-group`, which the same panel already used for the browser/desktop choice. Sync-interval also lost its 10- and 30-minute steps, so both groups are three options. The retention fields keep dropdowns - more options, longer labels. ## Corrected **Routing 422 to the Token field was wrong.** The endpoint answered 422 for three unrelated causes: Pydantic validation (token shape, *and* the SSRF guard on `base_url`), an invalid token, and an unreachable server. Pasting `http://10.0.0.5/chat` reported "Token is ongeldig" on a token that was fine, and attaching that to the Token field would have made it worse. #272 splits the statuses first (unreachable is now 502) and routes on the validation body's `loc`. The objection to the original `formErrorMessage` still stands and is why the replacement returns a *target* rather than a message: an error with no status is panel-level, never nothing. ## Expired - do not trust these without re-checking - **"Clicking an `nldd-form-field` label focuses nothing."** Fixed for `nldd-text-field` and `nldd-password-field`, which now delegate focus. Still true for `nldd-dropdown`, `nldd-radio-button-group` and `nldd-checkbox-field`, which implement neither `focus()` nor `delegatesFocus`. Measured on 0.8.79, with a positive control: ``` ff-text activeElement = nldd-text-field > input#nldd-field-input-… ff-pass activeElement = nldd-password-field > input#nldd-field-… ff-drop activeElement = body ff-radio activeElement = body ff-check activeElement = body ``` - **"`nldd-form-actions` outside an `nldd-form` contributes only a label-column spacer."** Never accurate. The spacer is conditional on `label-alignment` and a container of at least 641px; outside a form it contributes nothing. - **"`nldd-checkbox-field` has a hard 44px row."** It is a token-driven `min-height` (`--semantics-controls-md-min-size`), overridable, and it grows with a wrapping label. - **"`nldd-banner` is for persistent, page-level feedback."** The doc comment still says so, but 0.8.78 added `size="sm"`, documented for "a notice that has to sit inside a pane or above a toolbar". The rejection for the per-server warning row is weaker than it was. Not acted on. ## Still rejected on mechanism, verified at 0.8.79 - **`nldd-form` anywhere.** Export, Preferences, Import and the delete dialog have no submit. It also auto-migrates direct children into an inner `<form>` via a `MutationObserver` (there is an opt-out: supply your own `<form>`). `--semantics-forms-gap-tight` still exists and still buys the rhythm. - **`nldd-form-section`.** Still renders its title as a `<legend>`: a group label that heading navigation skips. `nldd-title` stays the home for panel headings. - **A shared `SettingsAlert.vue`** would re-implement `nldd-banner`, which sets role and `aria-atomic` itself. Note the split is critical versus everything else, not warning versus critical: `warning` gets `role="status"` alongside neutral, accent and success. - **`nldd-checkbox-field` on the diff-table row.** Its label is still a plain string with no slot and no parts, so it would duplicate or destroy the visible label in the neighbouring `<td>`. - **The import drop zone.** `_findInput()` still returns the first non-helper child, so it would treat the `<label>` wrapper as the input and put the id, the `aria-label` and the `aria-describedby` on it. 0.8.79 still ships no file-upload component. - **A hint on the Sync-interval dropdown.** `nldd-dropdown` still has no `inputId`, so `nldd-form-field` writes `aria-describedby` onto the role-less host and it never reaches the slotted `<select>`. This is also why the `aria-label` on that `<select>` is load-bearing. - **`nldd-form-field` around AccountDeletePanel's two consent checkboxes.** Only the first would be treated as "the input", and a plain `<label>` is weaker than the `<fieldset>` plus visually-hidden `<legend>` already there. - **`nldd-switch-field` for the browser/desktop-app choice** - two named destinations, not an on/off state. It is a single boolean `checked`. ## Settled by measurement - **`nldd-table` for the diff table.** Now evaluated. It fits mechanically - `nldd-cell` takes arbitrary content, and the design system itself suggests a checkbox column - but the family contains no `<table>`, `<th scope>` or `<caption>` anywhere: it is a CSS grid with `role="table"`, `role="columnheader"` and `role="cell"`. The current markup is a native table with real `<th scope="col">`, so this would trade semantics for roles. **Rejected.** - **The `<output aria-label="Herstelcode, 44 karakters">` in RecoveryKeyModal.** Did not need a screen reader after all - Chromium's accessibility tree answers it: ``` status name="Herstelcode, 44 karakters" StaticText name="aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789-_aBcDe=" ``` The label becomes the accessible name; the key stays exposed as a text child. The feared substitution does not happen. **No change.** - **`nldd-page` inside `nldd-window` for the modal title bar.** `nldd-window`'s own docs say it has no header and point consumers at `nldd-page` with a sticky header, so `Modal.vue`'s hand-written header looks like a deviation. Tried on 2026-08-09; it costs two things. Measured with a dialog open, the flattened tree carries two visible main landmarks: ``` doc > main ← the route doc > nldd-page#shadow > main ← nldd-page ``` That is the `landmark-one-main` violation `tests/e2e/landmarks.spec.ts` guards app-wide. The scroll region also moves into `nldd-page`'s shadow root, so `Modal.vue`'s `scroll-padding-block` stops applying and nothing in the app can reach it - `long-message-thread-scroll.spec.ts` fails on exactly that. The guidance is written for a window used as a window, not for a dialog layered over a route that already has a `<main>`. **Rejected**, with the reasoning in `Modal.vue`'s docblock. ## Still open Nothing. The last item - a retry after a failed Voorkeuren save - is handled in #275, which gives both auto-saving panels an "Opnieuw proberen" button. Automatisch opruimen turned out to show no failure at all, which was not in this list; it gets the same notice there. ## Upstream Reported as [MinBZK/storybook#188][upstream]. Wider than this list had it: on 0.8.79 the label click does nothing for **ten** components, not three - dropdown, checkbox, checkbox-field, radio-button, radio-button-field, radio-button-group, segmented-control, stepper, toggle-button and toggle-button-group. `_focusInput` calls `.focus()` on the slotted host, which only lands where the component has `delegatesFocus` or its own `focus()`. The argument that carries it is the asymmetry inside the design system: `nldd-switch-field` has a `focus()` override and works, while `nldd-checkbox-field` and `nldd-radio-button-field` - the same kind of control - do not. Verified in a blank page loading only the package, with real mouse clicks and a per-component control. Worth knowing for us: the components' own labels do work, so a user loses only the field-level label click. [upstream]: https://github.com/MinBZK/storybook/issues/188 ## Revisit when The NLDD minor or patch version changes. Everything above is a claim about component internals, and this list has already been wrong once for exactly that reason. `frontend/node_modules/@nldd/design-system/dist/components/` is the source of truth; the skill's changelog says what moved.
robbertbos changed title from Settings/NLDD: 22 changes evaluated and deliberately not made to Settings/NLDD: what was evaluated and not made, and when it expires 2026-08-08 22:12:03 +00:00
Author
Owner

Closing: every item is resolved, rejected with a verified reason, or filed elsewhere.

  • The changes landed in #272 (merged).
  • The last open item, a retry after a failed settings save, is in #275. That PR is not merged yet - reopen this if it does not land.
  • The upstream half is MinBZK/storybook#188.

The body stays as the record it was meant to be: what was tried, what was rejected and why, and which four claims had already expired between 0.8.70 and 0.8.79. That last part is the lesson - a rejection is only as good as the version it was measured against, so re-verify before treating any of it as settled.

The reasoning that is expensive to rediscover also sits next to the code: Modal.vue`s docblock carries why nldd-page is not used for the dialog title bar, with the landmark measurement.

Closing: every item is resolved, rejected with a verified reason, or filed elsewhere. - The changes landed in #272 (merged). - The last open item, a retry after a failed settings save, is in #275. That PR is not merged yet - reopen this if it does not land. - The upstream half is MinBZK/storybook#188. The body stays as the record it was meant to be: what was tried, what was rejected and why, and which four claims had already expired between 0.8.70 and 0.8.79. That last part is the lesson - a rejection is only as good as the version it was measured against, so re-verify before treating any of it as settled. The reasoning that is expensive to rediscover also sits next to the code: Modal.vue`s docblock carries why nldd-page is not used for the dialog title bar, with the landmark measurement.
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
robbertbos/waggle#208
No description provided.