From 712fc8587f6f1e4915dd4d1180cb9cbe5406076e Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 12 Jun 2026 13:59:35 +0200 Subject: [PATCH] refactor(soma): de-dup attrs in dropdown-menu + context-menu via renderProps (batch 3 tail) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Final 2 components of the rollout (words excluded per request). Both menu siblings now source role/aria/data from the morfo via renderProps(). Audit total 213 → 158; both at 0 residual hits. Unified the siblings' aria-boolean handling: aria-expanded/aria-checked (morfo stateRef → aria-boolean string) resolve through renderProps, not a soma boolean override. For checkbox/radio aria-checked the per-part `checked` state source feeds the COMPUTED value — dropdown-menu's effectiveChecked (group membership) and isChecked (value === group value) — so renderProps emits the correct string. The migration agent had left dropdown-menu's aria-checked as a boolean override reading effectiveChecked; removing it without registering the state regressed a checked item to aria-checked="false" — fixed by registering the computed source. Tests assert the string aria-boolean shape. Co-Authored-By: Claude Opus 4.8 --- .../context-menu-provider.svelte.test.ts | 6 +- .../context-menu-provider.svelte.ts | 106 +++++++------- .../dropdown-menu-provider.svelte.test.ts | 6 +- .../dropdown-menu-provider.svelte.ts | 133 ++++++++++++------ src/uix/soma/soma-attr-audit.test.ts | 6 +- 5 files changed, 156 insertions(+), 101 deletions(-) diff --git a/src/uix/soma/components/context-menu/context-menu-provider.svelte.test.ts b/src/uix/soma/components/context-menu/context-menu-provider.svelte.test.ts index f2e5850b6..dae3c3721 100644 --- a/src/uix/soma/components/context-menu/context-menu-provider.svelte.test.ts +++ b/src/uix/soma/components/context-menu/context-menu-provider.svelte.test.ts @@ -144,7 +144,7 @@ describe('ContextMenuProvider', () => { expect(result.trigger.props).toMatchObject({ 'aria-haspopup': 'menu', - 'aria-expanded': false, + 'aria-expanded': 'false', 'aria-controls': 'context-menu-content', 'data-state': 'closed' }); @@ -264,7 +264,7 @@ describe('ContextMenuProvider', () => { expect(result.checkboxItem.props).toMatchObject({ role: 'menuitemcheckbox', - 'aria-checked': false, + 'aria-checked': 'false', 'data-state': 'unchecked' }); result.checkboxItem.onclick({} as never); @@ -314,7 +314,7 @@ describe('ContextMenuProvider', () => { expect(result.trigger.props).toMatchObject({ role: 'menuitem', 'aria-haspopup': 'menu', - 'aria-expanded': true, + 'aria-expanded': 'true', 'aria-controls': 'context-menu-sub-content', 'data-state': 'open', 'data-text-value': 'More' diff --git a/src/uix/soma/components/context-menu/context-menu-provider.svelte.ts b/src/uix/soma/components/context-menu/context-menu-provider.svelte.ts index ce7d26be4..705062c91 100644 --- a/src/uix/soma/components/context-menu/context-menu-provider.svelte.ts +++ b/src/uix/soma/components/context-menu/context-menu-provider.svelte.ts @@ -170,7 +170,11 @@ export class ContextMenuTriggerProvider { this.runtimePart = this.provider.runtime.part('trigger', { id: opts.id, ref: opts.ref, - owner: this + owner: this, + // Per-part sources so renderProps resolves the morfo's state-/part- + // bound aria: aria-expanded ← open, aria-controls ← content id. + states: { open: () => this.provider.opts.open.current }, + parts: { content: () => this.provider.contentId.current } }); this.provider.triggerId.current = opts.id.current; } @@ -185,10 +189,10 @@ export class ContextMenuTriggerProvider { readonly props = $derived.by(() => this.runtimePart.assert({ - ...this.runtimePart.props, - 'aria-haspopup': 'menu' as const, - 'aria-expanded': this.provider.opts.open.current, - 'aria-controls': this.provider.contentId.current || undefined, + // Morfo-declared: aria-haspopup, aria-expanded, aria-controls. + ...this.runtimePart.renderProps(), + // Soma-owned: data-state (morfo declares it without a value) + the + // native disabled marker + the handler. 'data-state': getDataOpenClosed(this.provider.opts.open.current), 'data-disabled': boolToEmptyStrOrUndef(this.opts.disabled.current), oncontextmenu: this.oncontextmenu @@ -249,6 +253,8 @@ export class ContextMenuContentProvider { id: opts.id, ref: opts.ref, owner: this, + // aria-labelledby ← trigger id (renderProps resolves the partRef). + parts: { trigger: () => this.provider.triggerId.current }, onRefChange: (el) => { this.provider.setContentRef(el); } @@ -366,10 +372,9 @@ export class ContextMenuContentProvider { readonly props = $derived.by(() => this.runtimePart.assert({ - ...this.runtimePart.props, - role: 'menu' as const, - 'aria-labelledby': this.provider.triggerId.current || undefined, - 'aria-orientation': 'vertical' as const, + // Morfo-declared: role=menu, aria-labelledby, aria-orientation. + ...this.runtimePart.renderProps(), + // Soma-owned data-state (morfo declares it without a value) + layers. 'data-state': getDataOpenClosed(this.provider.opts.open.current), ...this.floating.props, ...this.provider.contentPresence.transitionAttrs, @@ -436,7 +441,9 @@ export class ContextMenuItemProvider { this.runtimePart = this.provider.runtime.part('item', { id: opts.id, ref: opts.ref, - owner: this + owner: this, + // aria-disabled is morfo-declared (literal 'true' gated on prop-truthy). + props: { disabled: () => this.opts.disabled.current } }); } @@ -465,10 +472,10 @@ export class ContextMenuItemProvider { readonly props = $derived.by(() => this.runtimePart.assert({ - ...this.runtimePart.props, - role: 'menuitem' as const, + // Morfo-declared: role=menuitem, aria-disabled. + ...this.runtimePart.renderProps(), + // Soma-owned: tabindex + the no-value data-* markers + handlers. tabindex: this.opts.disabled.current ? undefined : -1, - 'aria-disabled': this.opts.disabled.current || undefined, 'data-disabled': boolToEmptyStrOrUndef(this.opts.disabled.current), 'data-text-value': this.opts.textValue.current || undefined, onclick: this.onclick, @@ -505,16 +512,15 @@ export class ContextMenuGroupProvider { id: opts.id, ref: opts.ref, owner: this, - context: ContextMenuGroupProvider.ctx + context: ContextMenuGroupProvider.ctx, + // aria-labelledby ← group-heading id (renderProps resolves the partRef). + parts: { 'group-heading': () => this.headingId.current } }); } readonly props = $derived.by(() => - this.runtimePart.assert({ - ...this.runtimePart.props, - role: 'group' as const, - 'aria-labelledby': this.headingId.current || undefined - } as const) + // Morfo-declared: role=group, aria-labelledby. + this.runtimePart.assert({ ...this.runtimePart.renderProps() } as const) ); } @@ -570,11 +576,8 @@ export class ContextMenuSeparatorProvider { } readonly props = $derived.by(() => - this.runtimePart.assert({ - ...this.runtimePart.props, - role: 'separator' as const, - 'aria-orientation': 'horizontal' as const - } as const) + // Morfo-declared: role=separator, aria-orientation=horizontal. + this.runtimePart.assert({ ...this.runtimePart.renderProps() } as const) ); } @@ -606,7 +609,10 @@ export class ContextMenuCheckboxItemProvider { this.runtimePart = this.provider.runtime.part('checkbox-item', { id: opts.id, ref: opts.ref, - owner: this + owner: this, + // aria-checked ← checked state, aria-disabled ← disabled prop. + states: { checked: () => this.opts.checked.current }, + props: { disabled: () => this.opts.disabled.current } }); } @@ -631,11 +637,10 @@ export class ContextMenuCheckboxItemProvider { readonly props = $derived.by(() => this.runtimePart.assert({ - ...this.runtimePart.props, - role: 'menuitemcheckbox' as const, - 'aria-checked': this.opts.checked.current, + // Morfo-declared: role=menuitemcheckbox, aria-checked, aria-disabled. + ...this.runtimePart.renderProps(), + // Soma-owned: tabindex + the no-value data-* markers + handlers. tabindex: this.opts.disabled.current ? undefined : -1, - 'aria-disabled': this.opts.disabled.current || undefined, 'data-state': this.opts.checked.current ? 'checked' : 'unchecked', 'data-disabled': boolToEmptyStrOrUndef(this.opts.disabled.current), 'data-text-value': this.opts.textValue.current || undefined, @@ -676,10 +681,8 @@ export class ContextMenuRadioGroupProvider { } readonly props = $derived.by(() => - this.runtimePart.assert({ - ...this.runtimePart.props, - role: 'group' as const - } as const) + // Morfo-declared: role=group. + this.runtimePart.assert({ ...this.runtimePart.renderProps() } as const) ); } @@ -712,7 +715,10 @@ export class ContextMenuRadioItemProvider { this.runtimePart = this.provider.runtime.part('radio-item', { id: opts.id, ref: opts.ref, - owner: this + owner: this, + // aria-checked ← checked state (computed), aria-disabled ← disabled. + states: { checked: () => this.isChecked }, + props: { disabled: () => this.opts.disabled.current } }); this.radioGroup = ContextMenuRadioGroupProvider.require(); } @@ -741,11 +747,10 @@ export class ContextMenuRadioItemProvider { readonly props = $derived.by(() => this.runtimePart.assert({ - ...this.runtimePart.props, - role: 'menuitemradio' as const, - 'aria-checked': this.isChecked, + // Morfo-declared: role=menuitemradio, aria-checked, aria-disabled. + ...this.runtimePart.renderProps(), + // Soma-owned: tabindex + the no-value data-* markers + handlers. tabindex: this.opts.disabled.current ? undefined : -1, - 'aria-disabled': this.opts.disabled.current || undefined, 'data-state': this.isChecked ? 'checked' : 'unchecked', 'data-disabled': boolToEmptyStrOrUndef(this.opts.disabled.current), 'data-text-value': this.opts.textValue.current || undefined, @@ -855,6 +860,12 @@ export class ContextMenuSubTriggerProvider { id: opts.id, ref: opts.ref, owner: this, + // renderProps resolves the morfo's state-/part-/prop-bound aria: + // aria-expanded ← open, aria-controls ← sub-content id, + // aria-disabled ← disabled prop. + states: { open: () => this.sub.opts.open.current }, + parts: { 'sub-content': () => this.sub.contentId.current }, + props: { disabled: () => this.opts.disabled.current }, onRefChange: (el) => { this.sub.subTriggerRef.current = el; } @@ -921,13 +932,11 @@ export class ContextMenuSubTriggerProvider { readonly props = $derived.by(() => this.runtimePart.assert({ - ...this.runtimePart.props, - role: 'menuitem' as const, - 'aria-haspopup': 'menu' as const, - 'aria-expanded': this.sub.opts.open.current, - 'aria-controls': this.sub.contentId.current || undefined, + // Morfo-declared: role=menuitem, aria-haspopup, aria-expanded, + // aria-controls, aria-disabled. + ...this.runtimePart.renderProps(), + // Soma-owned: tabindex + the no-value data-* markers + handlers. tabindex: this.opts.disabled.current ? undefined : -1, - 'aria-disabled': this.opts.disabled.current || undefined, 'data-state': getDataOpenClosed(this.sub.opts.open.current), 'data-disabled': boolToEmptyStrOrUndef(this.opts.disabled.current), 'data-text-value': this.opts.textValue.current || undefined, @@ -974,6 +983,8 @@ export class ContextMenuSubContentProvider { id: opts.id, ref: opts.ref, owner: this, + // aria-labelledby ← sub-trigger id (renderProps resolves the partRef). + parts: { 'sub-trigger': () => this.sub.subTriggerId.current }, onRefChange: (el) => { this.sub.setContentRef(el); } @@ -1053,10 +1064,9 @@ export class ContextMenuSubContentProvider { readonly props = $derived.by(() => this.runtimePart.assert({ - ...this.runtimePart.props, - role: 'menu' as const, - 'aria-labelledby': this.sub.subTriggerId.current || undefined, - 'aria-orientation': 'vertical' as const, + // Morfo-declared: role=menu, aria-labelledby, aria-orientation. + ...this.runtimePart.renderProps(), + // Soma-owned data-state (morfo declares it without a value) + layers. 'data-state': getDataOpenClosed(this.sub.opts.open.current), ...this.floating.props, ...this.sub.contentPresence.transitionAttrs, diff --git a/src/uix/soma/components/dropdown-menu/dropdown-menu-provider.svelte.test.ts b/src/uix/soma/components/dropdown-menu/dropdown-menu-provider.svelte.test.ts index 19b15fcb9..5623ec86b 100644 --- a/src/uix/soma/components/dropdown-menu/dropdown-menu-provider.svelte.test.ts +++ b/src/uix/soma/components/dropdown-menu/dropdown-menu-provider.svelte.test.ts @@ -141,7 +141,7 @@ describe('MenuProvider', () => { expect(result.trigger.props).toMatchObject({ type: 'button', 'aria-haspopup': 'menu', - 'aria-expanded': false, + 'aria-expanded': 'false', 'aria-controls': 'dropdown-menu-content', 'data-state': 'closed' }); @@ -266,7 +266,7 @@ describe('MenuProvider', () => { expect(result.checkboxItem.effectiveChecked).toBe(true); expect(result.checkboxItem.props).toMatchObject({ role: 'menuitemcheckbox', - 'aria-checked': true, + 'aria-checked': 'true', 'data-state': 'checked' }); result.checkboxItem.onclick({} as never); @@ -315,7 +315,7 @@ describe('MenuProvider', () => { expect(result.trigger.props).toMatchObject({ role: 'menuitem', 'aria-haspopup': 'menu', - 'aria-expanded': true, + 'aria-expanded': 'true', 'aria-controls': 'dropdown-menu-sub-content', 'data-state': 'open', 'data-text-value': 'More' diff --git a/src/uix/soma/components/dropdown-menu/dropdown-menu-provider.svelte.ts b/src/uix/soma/components/dropdown-menu/dropdown-menu-provider.svelte.ts index 8bc90a68f..afe3e4964 100644 --- a/src/uix/soma/components/dropdown-menu/dropdown-menu-provider.svelte.ts +++ b/src/uix/soma/components/dropdown-menu/dropdown-menu-provider.svelte.ts @@ -178,7 +178,13 @@ export class MenuTriggerProvider { this.runtimePart = this.provider.runtime.part('trigger', { id: opts.id, ref: opts.ref, - owner: this + owner: this, + // Per-part sources for the morfo's value-bound attrs: aria-expanded + // (stateRef 'open') and aria-controls (partRef 'content', emitted only + // when content is present). With these, `renderProps()` resolves them + // from the morfo instead of the provider re-declaring them. + states: { open: () => this.provider.opts.open.current }, + parts: { content: () => this.provider.contentId.current || undefined } }); this.provider.triggerId.current = opts.id.current; @@ -204,11 +210,11 @@ export class MenuTriggerProvider { readonly props = $derived.by(() => this.runtimePart.assert({ - ...this.runtimePart.props, - type: 'button' as const, - 'aria-haspopup': 'menu' as const, - 'aria-expanded': this.provider.opts.open.current, - 'aria-controls': this.provider.contentId.current || undefined, + // Morfo-declared (type, aria-haspopup, aria-controls) resolved against + // this part's registered sources — no longer re-declared here. + ...this.runtimePart.renderProps(), + // aria-expanded resolves from renderProps (morfo stateRef → string); + // data-state has no morfo value source so soma derives it. 'data-state': getDataOpenClosed(this.provider.opts.open.current), onclick: this.onclick, onkeydown: this.onkeydown @@ -271,7 +277,10 @@ export class MenuContentProvider { owner: this, onRefChange: (el) => { this.provider.setContentRef(el); - } + }, + // aria-labelledby (partRef 'trigger', emitted only when the trigger is + // present) resolves from this source via renderProps. + parts: { trigger: () => this.provider.triggerId.current || undefined } }); this.provider.contentId.current = opts.id.current; @@ -397,10 +406,10 @@ export class MenuContentProvider { readonly props = $derived.by(() => this.runtimePart.assert({ - ...this.runtimePart.props, - role: 'menu' as const, - 'aria-labelledby': this.provider.triggerId.current || undefined, - 'aria-orientation': 'vertical' as const, + // Morfo-declared (role, aria-labelledby, aria-orientation) resolved + // against this part's registered sources. + ...this.runtimePart.renderProps(), + // data-state has no morfo value source — soma derives it. 'data-state': getDataOpenClosed(this.provider.opts.open.current), ...this.floating.props, ...this.provider.contentPresence.transitionAttrs, @@ -467,7 +476,10 @@ export class MenuItemProvider { this.runtimePart = this.provider.runtime.part('item', { id: opts.id, ref: opts.ref, - owner: this + owner: this, + // aria-disabled (literal 'true', emitted only when `disabled` is + // truthy) resolves from this source via renderProps. + props: { disabled: () => this.opts.disabled.current } }); } @@ -498,13 +510,16 @@ export class MenuItemProvider { readonly props = $derived.by(() => this.runtimePart.assert({ - ...this.runtimePart.props, - role: 'menuitem' as const, + // Morfo-declared (role, aria-disabled) resolved against this part's + // registered sources. + ...this.runtimePart.renderProps(), + // Soma owns: tabindex (native, computed from disabled + + // accessibleWhenDisabled), data-disabled/data-text-value (morfo + // declares them without a value source), handlers. tabindex: this.opts.disabled.current && !this.provider.opts.accessibleWhenDisabled.current ? undefined : -1, - 'aria-disabled': this.opts.disabled.current || undefined, 'data-disabled': boolToEmptyStrOrUndef(this.opts.disabled.current), 'data-text-value': this.opts.textValue.current || undefined, onclick: this.onclick, @@ -541,15 +556,18 @@ export class MenuGroupProvider { id: opts.id, ref: opts.ref, owner: this, - context: MenuGroupProvider.ctx + context: MenuGroupProvider.ctx, + // aria-labelledby (partRef 'group-heading', emitted only when the + // heading is present) resolves from this source via renderProps. + parts: { 'group-heading': () => this.headingId.current || undefined } }); } readonly props = $derived.by(() => this.runtimePart.assert({ - ...this.runtimePart.props, - role: 'group' as const, - 'aria-labelledby': this.headingId.current || undefined + // Morfo-declared (role, aria-labelledby) resolved against this part's + // registered sources. + ...this.runtimePart.renderProps() } as const) ); } @@ -608,9 +626,8 @@ export class MenuSeparatorProvider { readonly props = $derived.by(() => this.runtimePart.assert({ - ...this.runtimePart.props, - role: 'separator' as const, - 'aria-orientation': 'horizontal' as const + // Morfo-declared (role, aria-orientation) — both literals. + ...this.runtimePart.renderProps() } as const) ); } @@ -680,7 +697,12 @@ export class MenuCheckboxItemProvider { this.runtimePart = this.provider.runtime.part('checkbox-item', { id: opts.id, ref: opts.ref, - owner: this + owner: this, + // Per-part sources: aria-checked (stateRef 'checked') resolves from the + // EFFECTIVE checked state (group membership when grouped), aria-disabled + // (literal 'true' when `disabled` truthy) — both via renderProps. + states: { checked: () => this.effectiveChecked }, + props: { disabled: () => this.opts.disabled.current } }); this.group = MenuCheckboxGroupProvider.get(); } @@ -718,14 +740,16 @@ export class MenuCheckboxItemProvider { readonly props = $derived.by(() => this.runtimePart.assert({ - ...this.runtimePart.props, - role: 'menuitemcheckbox' as const, - 'aria-checked': this.effectiveChecked, + // Morfo-declared (role, aria-disabled) resolved against this part's + // registered sources. + ...this.runtimePart.renderProps(), + // aria-checked resolves from renderProps (morfo stateRef → string). + // Soma owns: data-state (no morfo value source), tabindex/ + // data-disabled/data-text-value (native or no-value), handlers. tabindex: this.opts.disabled.current && !this.provider.opts.accessibleWhenDisabled.current ? undefined : -1, - 'aria-disabled': this.opts.disabled.current || undefined, 'data-state': this.effectiveChecked ? 'checked' : 'unchecked', 'data-disabled': boolToEmptyStrOrUndef(this.opts.disabled.current), 'data-text-value': this.opts.textValue.current || undefined, @@ -767,8 +791,8 @@ export class MenuRadioGroupProvider { readonly props = $derived.by(() => this.runtimePart.assert({ - ...this.runtimePart.props, - role: 'group' as const + // Morfo-declared (role) — literal. + ...this.runtimePart.renderProps() } as const) ); } @@ -802,7 +826,12 @@ export class MenuRadioItemProvider { this.runtimePart = this.provider.runtime.part('radio-item', { id: opts.id, ref: opts.ref, - owner: this + owner: this, + // Per-part sources: aria-checked (stateRef 'checked') resolves from the + // computed isChecked (value === group value), aria-disabled (literal + // 'true' when `disabled` truthy) — both via renderProps. + states: { checked: () => this.isChecked }, + props: { disabled: () => this.opts.disabled.current } }); this.radioGroup = MenuRadioGroupProvider.require(); } @@ -831,14 +860,16 @@ export class MenuRadioItemProvider { readonly props = $derived.by(() => this.runtimePart.assert({ - ...this.runtimePart.props, - role: 'menuitemradio' as const, - 'aria-checked': this.isChecked, + // Morfo-declared (role, aria-disabled) resolved against this part's + // registered sources. + ...this.runtimePart.renderProps(), + // aria-checked resolves from renderProps (morfo stateRef → string). + // Soma owns: data-state (no morfo value source), tabindex/ + // data-disabled/data-text-value (native or no-value), handlers. tabindex: this.opts.disabled.current && !this.provider.opts.accessibleWhenDisabled.current ? undefined : -1, - 'aria-disabled': this.opts.disabled.current || undefined, 'data-state': this.isChecked ? 'checked' : 'unchecked', 'data-disabled': boolToEmptyStrOrUndef(this.opts.disabled.current), 'data-text-value': this.opts.textValue.current || undefined, @@ -939,7 +970,14 @@ export class MenuSubTriggerProvider { owner: this, onRefChange: (el) => { this.sub.subTriggerRef.current = el; - } + }, + // Per-part sources for the morfo's value-bound attrs: aria-controls + // (partRef 'sub-content', emitted only when present) and aria-disabled + // (literal 'true' when `disabled` truthy). aria-expanded stays a soma + // override (boolean, vs the morfo's stringified stateRef). + states: { open: () => this.sub.opts.open.current }, + parts: { 'sub-content': () => this.sub.contentId.current || undefined }, + props: { disabled: () => this.opts.disabled.current } }); this.sub.subTriggerId.current = opts.id.current; @@ -1015,16 +1053,16 @@ export class MenuSubTriggerProvider { readonly props = $derived.by(() => this.runtimePart.assert({ - ...this.runtimePart.props, - role: 'menuitem' as const, - 'aria-haspopup': 'menu' as const, - 'aria-expanded': this.sub.opts.open.current, - 'aria-controls': this.sub.contentId.current || undefined, + // Morfo-declared (role, aria-haspopup, aria-controls, aria-disabled) + // resolved against this part's registered sources. + ...this.runtimePart.renderProps(), + // aria-expanded resolves from renderProps (morfo stateRef → string). + // Soma owns: data-state (no morfo value source), tabindex/ + // data-disabled/data-text-value (native or no-value), handlers. tabindex: this.opts.disabled.current && !this.sub.provider.opts.accessibleWhenDisabled.current ? undefined : -1, - 'aria-disabled': this.opts.disabled.current || undefined, 'data-state': getDataOpenClosed(this.sub.opts.open.current), 'data-disabled': boolToEmptyStrOrUndef(this.opts.disabled.current), 'data-text-value': this.opts.textValue.current || undefined, @@ -1073,7 +1111,10 @@ export class MenuSubContentProvider { owner: this, onRefChange: (el) => { this.sub.setContentRef(el); - } + }, + // aria-labelledby (partRef 'sub-trigger', emitted only when present) + // resolves from this source via renderProps. + parts: { 'sub-trigger': () => this.sub.subTriggerId.current || undefined } }); this.sub.contentId.current = opts.id.current; @@ -1152,10 +1193,10 @@ export class MenuSubContentProvider { readonly props = $derived.by(() => this.runtimePart.assert({ - ...this.runtimePart.props, - role: 'menu' as const, - 'aria-labelledby': this.sub.subTriggerId.current || undefined, - 'aria-orientation': 'vertical' as const, + // Morfo-declared (role, aria-labelledby, aria-orientation) resolved + // against this part's registered sources. + ...this.runtimePart.renderProps(), + // data-state has no morfo value source — soma derives it. 'data-state': getDataOpenClosed(this.sub.opts.open.current), ...this.floating.props, ...this.sub.contentPresence.transitionAttrs, diff --git a/src/uix/soma/soma-attr-audit.test.ts b/src/uix/soma/soma-attr-audit.test.ts index b79abfcb9..eb2ba3277 100644 --- a/src/uix/soma/soma-attr-audit.test.ts +++ b/src/uix/soma/soma-attr-audit.test.ts @@ -67,7 +67,11 @@ const MIGRATED_CEILINGS: Record = { table: 8, // per-cell/row aria-rowindex/colindex/sort/selected (dynamic) editable: 4, // computed trigger aria-labels combobox: 0, - command: 4 // aria-activedescendant/controls (dynamic) + computed labels + command: 4, // aria-activedescendant/controls (dynamic) + computed labels + // Menu siblings — aria-expanded/aria-checked come from the morfo's stateRef + // (aria-boolean string) via renderProps; tests assert the string. 0 residue. + 'dropdown-menu': 0, + 'context-menu': 0 }; describe('soma↔morfo attr de-dup guard', () => {