fix(morfo,soma,eidos): el caption-button suelta aria-pressed cuando es disparador de MENU

Dentro del CaptionFloat el click abre la lista (la costura anula el toggle) y
el mismo elemento gana aria-haspopup/aria-expanded del DropdownMenu — pero el
morfo le estampaba aria-pressed incondicional: un elemento anunciandose como
toggle pulsado Y boton de menu a la vez, con lo pulsado describiendo un
control que ese click ya no ejerce.

Firmada la (b) de las tres opciones del handoff: condicion declarada en el
morfo (prop-falsy menuTrigger — el patron fieldLabelled, morfo.md Step 4), el
provider publica la fuente por parte y el CaptionFloat pasa menuTrigger. Las
alternativas: (a) shipear el anuncio contradictorio en el buque insignia; (c)
una parte 26 que contradice la doctrina escrita de la propia composicion («the
trigger stays the real CaptionButton») y engorda el morfo mayor del catalogo.

aria-keyshortcuts 'c' se queda: la tecla alterna los subtitulos desde
cualquier foco, el anuncio es verdad. pip/fullscreen no se tocan — nadie los
compone como disparadores de menu.

Test visto fallar: compuesto como disparador el attr queda genuinamente
AUSENTE (no presente-undefined); solo, sigue anunciando su estado. player
18/18 · barrido server 923/6 (los 6 ajenos de contracts).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-dir-prefs
dev 2 months ago
parent cf96a4852f
commit 7daac29870

@ -68,6 +68,7 @@
{#snippet child({ props })}
<CaptionButton
{...props}
menuTrigger={true}
onclick={(e: MouseEvent) => {
(props as { onclick?: (e: MouseEvent) => void }).onclick?.(e);
// Opening the list IS the click's whole job here.

@ -554,7 +554,19 @@ export const mediaPlayerMorfo = {
],
aria: [
{ attr: 'type', value: v.literal('button') },
{ attr: 'aria-pressed', value: v.stateRef('on') },
{
attr: 'aria-pressed',
value: v.stateRef('on'),
// Composed as the CaptionFloat's LIST trigger, the click opens a
// menu instead of toggling (the seam suppresses the handler) —
// announcing «pressed» next to the menu's aria-haspopup would
// state a control that click no longer exercises. The provider
// publishes the virtual prop (2-of-3: computed state lives
// there); same declared-suppression shape as `fieldLabelled`
// (morfo.md Step 4). `aria-keyshortcuts` stays: `c` toggles the
// captions wherever the focus sits, so it remains true.
condition: { when: 'prop-falsy', prop: 'menuTrigger' }
},
{ attr: 'aria-label', value: v.propRef('label'), severity: 'recommended' },
{ attr: 'aria-keyshortcuts', value: v.literal('c') }
]

@ -1,4 +1,5 @@
<script lang="ts">
import { readableActive } from '$libs/reactive';
import { partOpts } from '../../../provider';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
@ -10,18 +11,20 @@
let {
ref = $bindable(null),
id = createId(uid, 'media-player-caption-button'),
menuTrigger,
children,
child,
...restProps
}: MediaPlayerCaptionButtonProps = $props();
const state = MediaPlayerCaptionButtonProvider.create(
partOpts(
const state = MediaPlayerCaptionButtonProvider.create({
...partOpts(
() => id,
() => ref,
(v) => (ref = v)
)
);
),
menuTrigger: readableActive(() => menuTrigger)
});
const mergedProps = $derived(mergeProps(restProps, state.props));
</script>

@ -14,6 +14,7 @@ import {
MediaPlayerProvider,
MediaPlayerPlayButtonProvider,
MediaPlayerMuteButtonProvider,
MediaPlayerCaptionButtonProvider,
MediaPlayerTimeProvider,
MediaPlayerRestartButtonProvider,
MediaPlayerSkipButtonProvider
@ -433,6 +434,40 @@ describe('MediaPlayerProvider', () => {
dom.dispose();
});
it('the caption button sheds aria-pressed when composed as a MENU trigger', () => {
const { dom } = installSomaHarness();
const root = document.createElement('div');
const fake = fakeEngine();
const opts = mediaPlayerOpts(root, () => fake.engine);
const { result, cleanup } = withEffectRoot(() => {
const provider = MediaPlayerProvider.create(opts);
vi.spyOn(MediaPlayerProvider, 'require').mockReturnValue(provider);
const plain = MediaPlayerCaptionButtonProvider.create({
id: state('cap-plain'),
ref: state<HTMLElement | null>(document.createElement('button')),
menuTrigger: state<boolean | undefined>(undefined)
});
const menuTrigger = MediaPlayerCaptionButtonProvider.create({
id: state('cap-menu'),
ref: state<HTMLElement | null>(document.createElement('button')),
menuTrigger: state<boolean | undefined>(true)
});
return { provider, plain, menuTrigger };
});
// On its own the control IS the captions toggle: its state is announced.
expect(result.plain.props).toMatchObject({ 'aria-pressed': 'false' });
// Composed as the CaptionFloat's trigger, the click opens a list instead
// (the seam suppresses the toggle): announcing «pressed» next to the
// menu's aria-haspopup would state a control the click no longer
// exercises. Genuinely ABSENT, not present-and-undefined.
expect(result.menuTrigger.props).not.toHaveProperty('aria-pressed');
cleanup();
dom.dispose();
});
it('drives PlayButton and MuteButton state + dynamic labels', () => {
const { dom } = installSomaHarness();
const root = document.createElement('div');

@ -1444,7 +1444,8 @@ export class MediaPlayerPipButtonProvider {
// ── Caption button ──────────────────────────────────────────────────────────────
interface MediaPlayerCaptionButtonOpts extends WithRefOpts {}
interface MediaPlayerCaptionButtonOpts
extends WithRefOpts, ActiveProps<{ menuTrigger: boolean | undefined }> {}
export class MediaPlayerCaptionButtonProvider {
readonly opts: MediaPlayerCaptionButtonOpts;
@ -1461,9 +1462,10 @@ export class MediaPlayerCaptionButtonProvider {
id: opts.id,
ref: opts.ref,
owner: this,
// Sources so renderProps resolves aria-label (propRef('label')) and the
// on-state pair (data-state + aria-pressed via stateRef('on')).
props: { label: () => this.label },
// Sources so renderProps resolves aria-label (propRef('label')), the
// on-state pair (data-state + aria-pressed via stateRef('on')) and the
// prop-falsy gate that sheds aria-pressed on a menu-trigger composition.
props: { label: () => this.label, menuTrigger: () => this.opts.menuTrigger.current },
states: { on: () => this.provider.captionsOn }
});
}

@ -145,7 +145,17 @@ export type MediaPlayerFullscreenButtonProps = WithChild<{ id?: string }> &
export type MediaPlayerPipButtonProps = WithChild<{ id?: string }> &
Without<PrimitiveButtonAttributes, {}>;
export type MediaPlayerCaptionButtonProps = WithChild<{ id?: string }> &
export type MediaPlayerCaptionButtonProps = WithChild<{
id?: string;
/**
* Composed as the trigger of a caption LIST (`CaptionFloat` sets this): the
* click opens the menu instead of toggling, so the morfo suppresses
* `aria-pressed` — the menu's `aria-haspopup` / `aria-expanded` carry the
* announcement. Leave unset when the button stands alone as the toggle.
* @default undefined
*/
menuTrigger?: boolean;
}> &
Without<PrimitiveButtonAttributes, {}>;
/**

Loading…
Cancel
Save

Powered by TurnKey Linux.