fix(uix): audit Phase 2 — 3 more isolated HIGH findings

- select: drop the dead `focus` block from the morfo. Select uses VIRTUAL focus
  (focus stays on the trigger via aria-activedescendant; items are never
  DOM-focused), so the declared `trap: true` / `initial: 'first-focusable'` was
  unimplemented and contradicted the behavior. Matches Combobox (no focus
  block). [select-002]
- dialog: stop double-writing the Content `role`. The morfo declared
  `role: 'dialog'` (literal) AND the provider set `role = variant` — syncAttrs'
  dom.apply (which runs after Svelte render) overwrote the provider's
  `alertdialog` back to `dialog`, so AlertDialog rendered the wrong a11y role.
  Remove `role` from the morfo (it's variant-dependent, which a literal can't
  express) → the provider is the sole authority. Also drop the dead
  `aria-roledescription: undefined` (syncAttrs wrote the morfo value anyway).
  Browser-verified: alertdialog content -> role='alertdialog'. [dialog-001]
- collapsible: `collapse` event -> `sequence: 'post'`. The provider sets
  `open = false` in the runtime HANDLER, so `pre` deferred the close behind the
  full perceptual hold — measured 250ms dead delay before the content closed
  (the Checkbox-lag class). With `post` the content closes immediately; the CSS
  exit animation is keyed on data-state. Browser-measured: 250ms -> 1ms. [collapsible-NEW-001]

Verified: dialog/alert-dialog/select/collapsible provider tests green,
morfo:check passes for all four (the 5 unrelated fails — chronos/combobox/fab/
menu-dial/words — are pre-existing and untouched), svelte-check 0 errors in
touched files, browser-verified dialog role + collapsible latency.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
active-uix
dev 3 months ago
parent 98954a7c35
commit db33f66cdf

@ -45,7 +45,12 @@ export const collapsibleMorfo = {
family: 'emerge',
verb: 'collapse',
target: v.partRef('content'),
sequence: 'pre'
// `post` (not `pre`): the provider sets `open = false` inside the
// runtime HANDLER, so `pre` deferred the close behind the full
// perceptual hold (~250ms measured dead delay — the Checkbox-lag
// class, collapsible-NEW-001). With `post` the content closes
// immediately; the CSS exit animation is keyed on `data-state`.
sequence: 'post'
}
}
],

@ -147,7 +147,12 @@ export const dialogMorfo = {
archetype: 'content',
kind: 'public',
defaultElement: 'div',
role: 'dialog',
// `role` is intentionally NOT declared here: it is variant-dependent
// (`dialog` vs `alertdialog`), which the morfo's literal `role` can't
// express. The DialogContentProvider sets it from `opts.variant` as the
// SOLE authority — declaring `role: 'dialog'` here too would let
// syncAttrs' `dom.apply` (which runs after Svelte render) overwrite the
// provider's `alertdialog` back to `dialog` (the a11y race, dialog-001).
optional: false,
supportsNesting: true,
states: ['open', 'closed'],

@ -57,13 +57,11 @@ export const selectMorfo = {
}
],
focus: {
initial: 'first-focusable',
trap: true,
return: 'trigger',
restore: true
},
// No `focus` block: Select uses VIRTUAL focus — focus stays on the Trigger
// (aria-activedescendant), items are never DOM-focused, so there is no
// content focus-trap to declare (a `trap: true` here was dead + contradicted
// the implementation). Focus return to the trigger on close is done
// imperatively in the provider (`handleClose`). Matches Combobox.
parts: [
{
name: 'Provider',

@ -459,8 +459,9 @@ export class DialogContentProvider {
readonly props = $derived.by(() => ({
...this.runtimePart.props,
// `role` is provider-owned (variant-dependent) — the morfo no longer
// declares it, so syncAttrs can't overwrite this back to 'dialog' (dialog-001).
role: this.provider.opts.variant.current,
'aria-roledescription': undefined,
'data-nested': this.provider.isNested ? '' : undefined,
'data-nested-open': this.provider.hasNestedOpen ? '' : undefined,
style: {

Loading…
Cancel
Save

Powered by TurnKey Linux.