diff --git a/src/uix/morfo/components/feed.ts b/src/uix/morfo/components/feed.ts index 8b63cbc00..77b171827 100644 --- a/src/uix/morfo/components/feed.ts +++ b/src/uix/morfo/components/feed.ts @@ -18,13 +18,19 @@ export const feedMorfo = { defaultElement: 'section', role: 'feed', optional: false, - data: [{ attr: 'data-busy', severity: 'optional' }], + data: [{ attr: 'data-busy', value: v.propRef('busy'), severity: 'optional' }], aria: [ { attr: 'aria-label', - value: v.translationRef('#?components.feed.label|Feed'), + value: v.propRef('ariaLabel'), severity: 'recommended' }, + { + attr: 'aria-labelledby', + value: v.propRef('ariaLabelledby'), + condition: { when: 'prop-truthy', prop: 'ariaLabelledby' }, + severity: 'optional' + }, { attr: 'aria-busy', value: v.literal('true'), @@ -48,8 +54,8 @@ export const feedMorfo = { role: 'article', optional: false, data: [ - { attr: 'data-posinset' }, - { attr: 'data-level' } + { attr: 'data-posinset', value: v.propRef('posinset'), emit: 'value' }, + { attr: 'data-level', value: v.propRef('level'), emit: 'value' } ], aria: [ { attr: 'aria-posinset', value: v.propRef('posinset') }, @@ -96,7 +102,7 @@ export const feedMorfo = { defaultElement: 'div', role: 'feed', optional: true, - data: [{ attr: 'data-level' }], + data: [{ attr: 'data-level', value: v.propRef('level'), emit: 'value' }], aria: [ { attr: 'aria-labelledby', @@ -112,7 +118,9 @@ export const feedMorfo = { kind: 'public', defaultElement: 'div', optional: true, - data: [{ attr: 'data-intersecting', severity: 'optional' }], + data: [ + { attr: 'data-intersecting', value: v.propRef('intersecting'), severity: 'optional' } + ], aria: [{ attr: 'aria-hidden', value: v.literal('true') }] } ] diff --git a/src/uix/soma/components/feed/feed-provider.svelte.ts b/src/uix/soma/components/feed/feed-provider.svelte.ts index fe20fad22..5ea7b1de2 100644 --- a/src/uix/soma/components/feed/feed-provider.svelte.ts +++ b/src/uix/soma/components/feed/feed-provider.svelte.ts @@ -1,6 +1,5 @@ import { context, type WithRefOpts } from '../../provider'; import { createAttrs } from '$uix/morfo'; -import { boolToEmptyStrOrUndef } from '$adom'; import { readableActive, state, type Active, type ActiveProps } from '$libs/reactive'; import type { SomaKeyboardEvent } from '../../types'; import { KEYS } from '../../keyboard'; @@ -45,12 +44,19 @@ export class FeedProvider { private constructor(opts: FeedOpts) { this.opts = opts; this.soma = Soma.require(); - this.runtime = this.soma.runtime(feedMorfo, {}); + this.runtime = this.soma.runtime(feedMorfo, { + props: { + busy: () => this.opts.busy.current, + ariaLabel: () => this.resolvedAriaLabel.current, + ariaLabelledby: () => this.opts.ariaLabelledby.current + } + }); this.runtimePart = this.runtime.part('provider', { id: opts.id, ref: opts.ref, owner: this, - context: FeedProvider.ctx + context: FeedProvider.ctx, + syncAttrs: true }); } @@ -132,12 +138,7 @@ export class FeedProvider { readonly props = $derived.by(() => this.runtimePart.assert({ - ...this.runtimePart.props, - role: 'feed' as const, - 'aria-label': this.resolvedAriaLabel.current, - 'aria-labelledby': this.opts.ariaLabelledby.current, - 'aria-busy': this.opts.busy.current ? true : undefined, - 'data-busy': boolToEmptyStrOrUndef(this.opts.busy.current) + ...this.runtimePart.props } as const) ); } @@ -176,17 +177,21 @@ export class FeedThreadProvider { id: opts.id, ref: opts.ref, owner: this, - context: FeedThreadProvider.ctx + context: FeedThreadProvider.ctx, + props: { + level: () => this.level + }, + parts: { + 'article-title': () => this.parentArticle.titleId.current || undefined + }, + syncAttrs: true }); this.level = this.parentArticle.level + 1; } readonly props = $derived.by(() => this.runtimePart.assert({ - ...this.runtimePart.props, - role: 'feed' as const, - 'aria-labelledby': this.parentArticle.titleId.current || undefined, - 'data-level': this.level + ...this.runtimePart.props } as const) ); } @@ -227,7 +232,17 @@ export class FeedArticleProvider { id: opts.id, ref: opts.ref, owner: this, - context: FeedArticleProvider.ctx + context: FeedArticleProvider.ctx, + props: { + posinset: () => this.resolvedPosinset, + setsize: () => this.resolvedSetsize, + level: () => this.level + }, + parts: { + 'article-title': () => this.titleId.current || undefined, + 'article-description': () => this.descriptionId.current || undefined + }, + syncAttrs: true }); this.level = this.thread ? this.thread.level : 1; } @@ -275,21 +290,13 @@ export class FeedArticleProvider { this.provider.handleArticleKeydown(e as SomaKeyboardEvent); }; - readonly props = $derived.by(() => { - const posinset = this.resolvedPosinset; - return this.runtimePart.assert({ + readonly props = $derived.by(() => + this.runtimePart.assert({ ...this.runtimePart.props, - role: 'article' as const, tabindex: 0, - 'aria-posinset': posinset, - 'aria-setsize': this.resolvedSetsize, - 'aria-labelledby': this.titleId.current || undefined, - 'aria-describedby': this.descriptionId.current || undefined, - 'data-posinset': posinset, - 'data-level': this.level, onkeydown: this.onkeydown - } as const); - }); + } as const) + ); } // ── ArticleTitle ─────────────────────────────────────────────────────────── @@ -311,7 +318,11 @@ export class FeedArticleTitleProvider { this.runtimePart = this.article.provider.runtime.part('article-title', { id: opts.id, ref: opts.ref, - owner: this + owner: this, + props: { + level: () => this.resolvedLevel + }, + syncAttrs: true }); // A30: direct assignment. this.article.titleId.current = opts.id.current; @@ -330,9 +341,7 @@ export class FeedArticleTitleProvider { readonly props = $derived.by(() => this.runtimePart.assert({ - ...this.runtimePart.props, - role: 'heading' as const, - 'aria-level': this.resolvedLevel + ...this.runtimePart.props } as const) ); } @@ -404,7 +413,15 @@ export class FeedSentinelProvider { this.soma = Soma.require(); this.feed = FeedProvider.get(); this.runtime = this.feed?.runtime ?? this.soma.runtime(feedMorfo, {}); - this.runtimePart = this.runtime.part('sentinel', { id: opts.id, ref: opts.ref, owner: this }); + this.runtimePart = this.runtime.part('sentinel', { + id: opts.id, + ref: opts.ref, + owner: this, + props: { + intersecting: () => this.intersecting + }, + syncAttrs: true + }); $effect(() => { const el = opts.ref.current; @@ -438,9 +455,7 @@ export class FeedSentinelProvider { readonly props = $derived.by(() => this.runtimePart.assert({ - ...this.runtimePart.props, - 'aria-hidden': true as const, - 'data-intersecting': boolToEmptyStrOrUndef(this.intersecting) + ...this.runtimePart.props } as const) ); }