@ -14,7 +14,6 @@ import type {
} from '../../types' ;
import { KEYS } from '../../keyboard' ;
import { Soma } from '../../core/soma.svelte' ;
import { COMBOBOX_LANGS } from './langs' ;
import { Presence } from '../../layers/presence.svelte' ;
import { Dismissal , type DismissalBehavior } from '../../layers/dismissal.svelte' ;
@ -338,7 +337,12 @@ export class ComboboxInputProvider {
this . runtimePart = this . provider . runtime . part ( 'input' , {
id : opts.id ,
ref : opts.ref ,
owner : this
owner : this ,
// Per-part source for the morfo's `aria-activedescendant` (propRef
// 'highlightedId'). The component-level sources don't carry it, so
// register it here → `renderProps()` resolves it from the morfo
// instead of the provider re-declaring it.
props : { highlightedId : ( ) = > this . provider . highlightedId || undefined }
} ) ;
this . provider . inputId . current = opts . id . current ;
this . provider . inputRef = opts . ref ;
@ -426,16 +430,12 @@ export class ComboboxInputProvider {
readonly props = $derived . by ( ( ) = >
this . runtimePart . assert ( {
. . . this . runtimePart . props ,
// Morfo-declared: role, aria-haspopup/expanded/controls/activedescendant/
// required/autocomplete, data-state — resolved against this part's
// registered sources (incl. the per-part `highlightedId`).
. . . this . runtimePart . renderProps ( ) ,
// Soma-only: what the morfo can't express.
type : 'text' as const ,
role : 'combobox' as const ,
'aria-haspopup' : 'listbox' as const ,
'aria-expanded' : this . provider . opts . open . current ,
'aria-controls' : this . provider . contentId . current || undefined ,
'aria-activedescendant' : this . provider . highlightedId || undefined ,
'aria-required' : this . provider . opts . required . current || undefined ,
'aria-autocomplete' : 'list' as const ,
'data-state' : getDataOpenClosed ( this . provider . opts . open . current ) ,
'data-disabled' : boolToEmptyStrOrUndef ( this . provider . opts . disabled . current ) ,
disabled : this.provider.opts.disabled.current || undefined ,
value : this.provider.opts.inputValue.current ,
@ -476,13 +476,11 @@ export class ComboboxTriggerProvider {
readonly props = $derived . by ( ( ) = >
this . runtimePart . assert ( {
. . . this . runtimePart . props ,
type : 'button' as const ,
// Morfo-declared: type, aria-label/haspopup/expanded/controls.
. . . this . runtimePart . renderProps ( ) ,
// Soma-only: native tabindex, the data-* the morfo declares WITHOUT a
// value (soma owns the value), the native disabled attr, the handler.
tabindex : - 1 ,
'aria-label' : this . provider . soma . langs . ts ( COMBOBOX_LANGS . TOGGLE ) ,
'aria-haspopup' : 'listbox' as const ,
'aria-expanded' : this . provider . opts . open . current ,
'aria-controls' : this . provider . contentId . current || undefined ,
'data-state' : getDataOpenClosed ( this . provider . opts . open . current ) ,
'data-disabled' : boolToEmptyStrOrUndef ( this . provider . opts . disabled . current ) ,
disabled : this.provider.opts.disabled.current || undefined ,
@ -658,10 +656,10 @@ export class ComboboxContentProvider {
readonly props = $derived . by ( ( ) = >
this . runtimePart . assert ( {
. . . this . runtimePart . props ,
role : 'listbox' as const ,
'aria-labelledby' : this . provider . inputId . current || undefined ,
'aria-multiselectable' : this . provider . opts . type . current === 'multiple' || undefined ,
// Morfo-declared: role, aria-labelledby, aria-multiselectable.
. . . this . runtimePart . renderProps ( ) ,
// Soma-only: data-state (morfo declares it WITHOUT a value), the
// floating/presence/dismissal layer attrs, native tabindex + handler.
'data-state' : getDataOpenClosed ( this . provider . opts . open . current ) ,
. . . this . floating . props ,
. . . this . provider . contentPresence . transitionAttrs ,
@ -727,7 +725,12 @@ export class ComboboxItemProvider {
this . runtimePart = this . provider . runtime . part ( 'item' , {
id : opts.id ,
ref : opts.ref ,
owner : this
owner : this ,
// Per-part state source for the morfo's `aria-selected` (stateRef
// 'checked'). Each Item carries its own selection state, so it's
// registered per-instance here → `renderProps()` resolves
// aria-selected from the morfo instead of the provider re-declaring it.
states : { checked : ( ) = > this . isSelected }
} ) ;
// Maintain value → label registry without a ref-reading mount effect.
@ -760,9 +763,10 @@ export class ComboboxItemProvider {
readonly props = $derived . by ( ( ) = >
this . runtimePart . assert ( {
. . . this . runtimePart . props ,
role : 'option' as const ,
'aria-selected' : this . isSelected ,
// Morfo-declared: role, aria-selected (from the per-part `checked` state).
. . . this . runtimePart . renderProps ( ) ,
// Soma-only: native tabindex + the data-* the morfo declares WITHOUT a
// value (soma owns value/label/highlighted/disabled/state) + handlers.
tabindex : this.opts.disabled.current ? undefined : - 1 ,
'data-state' : this . isSelected ? 'checked' : 'unchecked' ,
'data-highlighted' : this . provider . highlightedId === this . opts . id . current ? '' : undefined ,
@ -804,15 +808,19 @@ export class ComboboxGroupProvider {
id : opts.id ,
ref : opts.ref ,
owner : this ,
context : ComboboxGroupProvider.ctx
context : ComboboxGroupProvider.ctx ,
// Per-part source for the morfo's `aria-labelledby` (partRef
// 'group-heading'). The heading id is per-group-instance (set by the
// nested GroupHeading), so it's registered here → `renderProps()`
// resolves aria-labelledby + its part-present condition from the morfo.
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 (from the per-part heading id).
. . . this . runtimePart . renderProps ( )
} as const )
) ;
}
@ -873,8 +881,8 @@ export class ComboboxSeparatorProvider {
readonly props = $derived . by ( ( ) = >
this . runtimePart . assert ( {
. . . this . runtimePart . props ,
role : 'separator' as const
// Morfo-declared: role. No soma-specific extras.
. . . this . runtimePart . renderProps ( )
} as const )
) ;
}
@ -934,7 +942,11 @@ export class ComboboxSelectedTagsProvider {
/** Live snapshot of `{ value, label }` for every selected entry. Drives the chip loop. */
readonly items = $derived . by ( ( ) = > this . provider . getSelectedItems ( ) ) ;
readonly props = $derived . by ( ( ) = > this . runtimePart . assert ( { . . . this . runtimePart . props } as const ) ) ;
readonly props = $derived . by ( ( ) = >
// Morfo-declared: role, aria-label (translationRef 'selected-tags'),
// data-empty (propRef 'selectedTagsEmpty'). No soma-specific extras.
this . runtimePart . assert ( { . . . this . runtimePart . renderProps ( ) } as const )
) ;
}
// ── SelectedTag ─────────────────────────────────────────────────────────────
@ -1024,8 +1036,11 @@ export class ComboboxSelectedTagRemoveProvider {
readonly props = $derived . by ( ( ) = >
this . runtimePart . assert ( {
. . . this . runtimePart . props ,
type : 'button' as const ,
// Morfo-declared: type, aria-label (translationRef 'remove' — the morfo
// declares it; previously the provider never applied it).
. . . this . runtimePart . renderProps ( ) ,
// Soma-only: data-disabled (morfo declares it WITHOUT a value), the
// native disabled attr, the handler.
'data-disabled' : boolToEmptyStrOrUndef ( this . disabled ) ,
disabled : this.disabled || undefined ,
onclick : this.onclick
@ -1062,14 +1077,12 @@ export class ComboboxClearButtonProvider {
readonly props = $derived . by ( ( ) = >
this . runtimePart . assert ( {
. . . this . runtimePart . props ,
type : 'button' as const ,
'aria-label' : this . provider . soma . langs . ts ( COMBOBOX_LANGS . CLEAR ) ,
// Morfo-declared: type, aria-label (translationRef 'clear'), data-disabled
// (propRef 'clearDisabled' = disabled || isEmpty).
. . . this . runtimePart . renderProps ( ) ,
// Soma-only: native tabindex + the native disabled attr + the handler.
tabindex : - 1 ,
disabled : this.provider.opts.disabled.current || this . provider . isEmpty || undefined ,
'data-disabled' : boolToEmptyStrOrUndef (
this . provider . opts . disabled . current || this . provider . isEmpty
) ,
onclick : this.onclick
} as const )
) ;