@ -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 ,