fix(core): update trigger aria-expanded on close (#1644)

This commit is contained in:
Sam Potts
2026-06-02 17:07:20 +10:00
committed by GitHub
parent e9a619e03a
commit 2f5e23e100
6 changed files with 70 additions and 46 deletions
+25 -23
View File
@@ -21,7 +21,7 @@ const menuContentPlacementClass = [
const menuContentClass = [
menuContentSurfaceClass,
'transition-[opacity,scale,translate,filter] duration-150',
'transition-[opacity,scale,translate,filter] duration-150 ease-in-out',
menuContentPlacementClass,
].join(' ');
@@ -68,7 +68,7 @@ const menuNavPopupClass = [
'group relative',
menuNavSurfaceClass,
'w-(--media-menu-width) h-(--media-menu-height)',
'transition-[opacity,scale,translate,filter,width,height] duration-300 ease-in-out',
'transition-[opacity,scale,translate,filter,width,height] duration-150 ease-in-out',
menuContentPlacementClass,
].join(' ');
@@ -87,7 +87,7 @@ root.innerHTML = `
<div class="flex gap-4 flex-wrap justify-center">
<!-- Radio group -->
<div class="bg-white border border-slate-200 rounded-xl p-6 flex flex-col items-start gap-3.5 min-w-[200px] shadow-[0_1px_3px_0_rgb(0_0_0/.05)]">
<div class="bg-white ring-1 ring-slate-700/10 rounded-xl p-6 flex flex-col items-start gap-3.5 min-w-[200px] shadow-sm">
<span class="text-xs font-medium text-slate-500 uppercase tracking-widest">Radio group</span>
<button
commandfor="quality-menu"
@@ -97,12 +97,12 @@ root.innerHTML = `
<svg class="w-3.5 h-3.5 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<media-menu id="quality-menu" class="${menuContentClass}">
<media-menu-label class="${menuLabelClass}">Resolution</media-menu-label>
<media-menu-radio-group id="quality-group" value="auto">
<media-menu-radio-item value="auto" class="${radioItemClass}">Auto</media-menu-radio-item>
<media-menu-radio-item value="1080p" class="${radioItemClass}">1080p</media-menu-radio-item>
<media-menu-radio-item value="720p" class="${radioItemClass}">720p</media-menu-radio-item>
<media-menu-radio-item value="480p" class="${radioItemClass}">480p</media-menu-radio-item>
<media-menu-label class="${menuLabelClass}">Resolution</media-menu-label>
<media-menu-radio-item value="auto" class="${radioItemClass}">Auto</media-menu-radio-item>
<media-menu-radio-item value="1080p" class="${radioItemClass}">1080p</media-menu-radio-item>
<media-menu-radio-item value="720p" class="${radioItemClass}">720p</media-menu-radio-item>
<media-menu-radio-item value="480p" class="${radioItemClass}">480p</media-menu-radio-item>
<media-menu-radio-item value="360p" disabled class="${radioItemClass}">360p (unavailable)</media-menu-radio-item>
</media-menu-radio-group>
</media-menu>
@@ -110,7 +110,7 @@ root.innerHTML = `
</div>
<!-- Mixed items -->
<div class="bg-white border border-slate-200 rounded-xl p-6 flex flex-col items-start gap-3.5 min-w-[200px] shadow-[0_1px_3px_0_rgb(0_0_0/.05)]">
<div class="bg-white ring-1 ring-slate-700/10 rounded-xl p-6 flex flex-col items-start gap-3.5 min-w-[200px] shadow-sm">
<span class="text-xs font-medium text-slate-500 uppercase tracking-widest">Mixed items</span>
<button
commandfor="settings-menu"
@@ -120,18 +120,20 @@ root.innerHTML = `
<svg class="w-3.5 h-3.5 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<media-menu id="settings-menu" class="${menuContentClass}">
<media-menu-label class="${menuLabelClass}">Playback</media-menu-label>
<media-menu-checkbox-item id="loop-item" class="${checkboxItemClass}">Loop</media-menu-checkbox-item>
<media-menu-checkbox-item id="autoplay-item" class="${checkboxItemClass}">Autoplay</media-menu-checkbox-item>
<media-menu-group>
<media-menu-label class="${menuLabelClass}">Playback</media-menu-label>
<media-menu-checkbox-item id="loop-item" class="${checkboxItemClass}">Loop</media-menu-checkbox-item>
<media-menu-checkbox-item id="autoplay-item" class="${checkboxItemClass}">Autoplay</media-menu-checkbox-item>
</media-menu-group>
<media-menu-separator class="${menuSeparatorClass}"></media-menu-separator>
<media-menu-item id="copy-item" class="${menuItemClass}">Copy link</media-menu-item>
<media-menu-item id="copy-item" class="${menuItemClass}">Copy link</media-menu-item>
<media-menu-item id="report-item" class="${menuItemClass}">Report issue</media-menu-item>
</media-menu>
<p class="text-[0.8125rem] text-slate-500">Loop: <strong id="loop-output" class="text-slate-900 font-medium">off</strong></p>
</div>
<!-- Submenu navigation -->
<div class="bg-white border border-slate-200 rounded-xl p-6 flex flex-col items-start gap-3.5 min-w-[200px] shadow-[0_1px_3px_0_rgb(0_0_0/.05)]">
<div class="bg-white ring-1 ring-slate-700/10 rounded-xl p-6 flex flex-col items-start gap-3.5 min-w-[200px] shadow-sm">
<span class="text-xs font-medium text-slate-500 uppercase tracking-widest">Submenu</span>
<button
commandfor="nav-menu"
@@ -173,11 +175,11 @@ root.innerHTML = `
<svg class="w-3.5 h-3.5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
Quality
</media-menu-back>
<media-menu-radio-group id="nav-quality-group" value="auto">
<media-menu-radio-item value="auto" class="${radioItemClass}">Auto</media-menu-radio-item>
<media-menu-radio-group id="nav-quality-group" aria-label="Resolution" value="auto">
<media-menu-radio-item value="auto" class="${radioItemClass}">Auto</media-menu-radio-item>
<media-menu-radio-item value="1080p" class="${radioItemClass}">1080p</media-menu-radio-item>
<media-menu-radio-item value="720p" class="${radioItemClass}">720p</media-menu-radio-item>
<media-menu-radio-item value="480p" class="${radioItemClass}">480p</media-menu-radio-item>
<media-menu-radio-item value="720p" class="${radioItemClass}">720p</media-menu-radio-item>
<media-menu-radio-item value="480p" class="${radioItemClass}">480p</media-menu-radio-item>
</media-menu-radio-group>
</media-menu>
@@ -186,13 +188,13 @@ root.innerHTML = `
<svg class="w-3.5 h-3.5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
Speed
</media-menu-back>
<media-menu-radio-group id="nav-speed-group" value="1">
<media-menu-radio-item value="0.5" class="${radioItemClass}">0.5x</media-menu-radio-item>
<media-menu-radio-group id="nav-speed-group" aria-label="Speed" value="1">
<media-menu-radio-item value="0.5" class="${radioItemClass}">0.5x</media-menu-radio-item>
<media-menu-radio-item value="0.75" class="${radioItemClass}">0.75x</media-menu-radio-item>
<media-menu-radio-item value="1" class="${radioItemClass}">Normal</media-menu-radio-item>
<media-menu-radio-item value="1" class="${radioItemClass}">Normal</media-menu-radio-item>
<media-menu-radio-item value="1.25" class="${radioItemClass}">1.25x</media-menu-radio-item>
<media-menu-radio-item value="1.5" class="${radioItemClass}">1.5x</media-menu-radio-item>
<media-menu-radio-item value="2" class="${radioItemClass}">2x</media-menu-radio-item>
<media-menu-radio-item value="1.5" class="${radioItemClass}">1.5x</media-menu-radio-item>
<media-menu-radio-item value="2" class="${radioItemClass}">2x</media-menu-radio-item>
</media-menu-radio-group>
</media-menu>
</media-menu>
+23 -21
View File
@@ -15,15 +15,15 @@ const menuNavSurfaceClass =
'bg-white border-none ring-1 ring-black/10 shadow-sm rounded-md min-w-[10rem] overflow-hidden outline-none';
const menuContentPlacementClass = [
'data-side=bottom:origin-top data-side=top:origin-bottom',
'data-side=left:origin-right data-side=right:origin-left',
'data-[side=bottom]:origin-top data-[side=top]:origin-bottom',
'data-[side=left]:origin-right data-[side=right]:origin-left',
'data-starting-style:opacity-0 data-starting-style:scale-95 data-starting-style:-translate-y-1 data-starting-style:blur-sm',
'data-ending-style:opacity-0 data-ending-style:scale-95 data-ending-style:-translate-y-1 data-ending-style:blur-sm',
].join(' ');
const menuContentClass = [
menuContentSurfaceClass,
'transition-[opacity,scale,translate,filter] duration-150',
'transition-[opacity,scale,translate,filter] duration-150 ease-in-out',
menuContentPlacementClass,
].join(' ');
@@ -70,19 +70,19 @@ const menuNavPopupClass = [
'group relative',
menuNavSurfaceClass,
'w-(--media-menu-width) h-(--media-menu-height)',
'transition-[opacity,scale,translate,filter,width,height] duration-300 ease-in-out',
'transition-[opacity,scale,translate,filter,width,height] duration-150 ease-in-out',
menuContentPlacementClass,
].join(' ');
// ── Indicators ────────────────────────────────────────────────────────────────
function RadioDot() {
return <span className="absolute left-[0.5625rem] top-1/2 -translate-y-1/2 w-2 h-2 rounded-full bg-current" />;
return <span className="absolute left-2.25 top-1/2 -translate-y-1/2 w-2 h-2 rounded-full bg-current" />;
}
function Checkmark() {
return (
<span className="absolute left-[0.4375rem] top-1/2 -translate-y-1/2 w-3 h-3 flex items-center justify-center">
<span className="absolute left-1.75 top-1/2 -translate-y-1/2 w-3 h-3 flex items-center justify-center">
<svg
aria-hidden="true"
viewBox="0 0 24 24"
@@ -174,13 +174,13 @@ function App() {
<div className="flex gap-4 flex-wrap justify-center">
{/* Radio group */}
<div className="bg-white border border-slate-200 rounded-xl p-6 flex flex-col items-start gap-3.5 min-w-[200px] shadow-[0_1px_3px_0_rgb(0_0_0/.05)]">
<div className="bg-white ring-1 ring-slate-700/10 rounded-xl p-6 flex flex-col items-start gap-3.5 min-w-[200px] shadow-sm">
<span className="text-xs font-medium text-slate-500 uppercase tracking-widest">Radio group</span>
<Menu.Root>
<TriggerButton>Quality</TriggerButton>
<Menu.Content className={menuContentClass}>
<Menu.Label className={menuLabelClass}>Resolution</Menu.Label>
<Menu.RadioGroup value={quality} onValueChange={setQuality}>
<Menu.Label className={menuLabelClass}>Resolution</Menu.Label>
{['auto', '1080p', '720p', '480p'].map((value) => (
<Menu.RadioItem key={value} value={value} className={radioItemClass}>
{quality === value && <RadioDot />}
@@ -199,20 +199,22 @@ function App() {
</div>
{/* Mixed items */}
<div className="bg-white border border-slate-200 rounded-xl p-6 flex flex-col items-start gap-3.5 min-w-[200px] shadow-[0_1px_3px_0_rgb(0_0_0/.05)]">
<div className="bg-white ring-1 ring-slate-700/10 rounded-xl p-6 flex flex-col items-start gap-3.5 min-w-[200px] shadow-sm">
<span className="text-xs font-medium text-slate-500 uppercase tracking-widest">Mixed items</span>
<Menu.Root>
<TriggerButton>Settings</TriggerButton>
<Menu.Content className={menuContentClass}>
<Menu.Label className={menuLabelClass}>Playback</Menu.Label>
<Menu.CheckboxItem checked={loop} onCheckedChange={setLoop} className={checkboxItemClass}>
{loop && <Checkmark />}
Loop
</Menu.CheckboxItem>
<Menu.CheckboxItem checked={autoplay} onCheckedChange={setAutoplay} className={checkboxItemClass}>
{autoplay && <Checkmark />}
Autoplay
</Menu.CheckboxItem>
<Menu.Group>
<Menu.Label className={menuLabelClass}>Playback</Menu.Label>
<Menu.CheckboxItem checked={loop} onCheckedChange={setLoop} className={checkboxItemClass}>
{loop && <Checkmark />}
Loop
</Menu.CheckboxItem>
<Menu.CheckboxItem checked={autoplay} onCheckedChange={setAutoplay} className={checkboxItemClass}>
{autoplay && <Checkmark />}
Autoplay
</Menu.CheckboxItem>
</Menu.Group>
<Menu.Separator className={menuSeparatorClass} />
<Menu.Item onSelect={() => console.log('copy link')} className={menuItemClass}>
Copy link
@@ -228,7 +230,7 @@ function App() {
</div>
{/* Submenu navigation */}
<div className="bg-white border border-slate-200 rounded-xl p-6 flex flex-col items-start gap-3.5 min-w-[200px] shadow-[0_1px_3px_0_rgb(0_0_0/.05)]">
<div className="bg-white ring-1 ring-slate-700/10 rounded-xl p-6 flex flex-col items-start gap-3.5 min-w-[200px] shadow-sm">
<span className="text-xs font-medium text-slate-500 uppercase tracking-widest">Submenu</span>
<Menu.Root>
<TriggerButton>Settings</TriggerButton>
@@ -248,7 +250,7 @@ function App() {
<ChevronLeft />
Quality
</Menu.Back>
<Menu.RadioGroup value={quality} onValueChange={setQuality}>
<Menu.RadioGroup aria-label="Resolution" value={quality} onValueChange={setQuality}>
{['auto', '1080p', '720p', '480p'].map((v) => (
<Menu.RadioItem key={v} value={v} className={radioItemClass}>
{quality === v && <RadioDot />}
@@ -273,7 +275,7 @@ function App() {
<ChevronLeft />
Speed
</Menu.Back>
<Menu.RadioGroup value={speed} onValueChange={setSpeed}>
<Menu.RadioGroup aria-label="Speed" value={speed} onValueChange={setSpeed}>
{[
{ value: '0.5', label: '0.5x' },
{ value: '0.75', label: '0.75x' },
+1 -1
View File
@@ -83,7 +83,7 @@ export class MenuCore {
getTriggerAttrs(state: MenuState, contentId?: string) {
return {
'aria-haspopup': 'menu' as const,
'aria-expanded': state.open ? 'true' : 'false',
'aria-expanded': state.open && state.status !== 'ending' ? 'true' : 'false',
'aria-controls': contentId,
};
}
@@ -114,6 +114,16 @@ describe('MenuCore', () => {
expect(attrs['aria-expanded']).toBe('true');
});
it('returns aria-expanded false when closing', () => {
const core = new MenuCore();
core.setInput(createInput({ active: true, status: 'ending' }));
const state = core.getState();
const attrs = core.getTriggerAttrs(state);
expect(state.open).toBe(true);
expect(attrs['aria-expanded']).toBe('false');
});
it('sets aria-controls when contentId is provided', () => {
const core = new MenuCore();
core.setInput(createInput());
@@ -94,7 +94,7 @@ export class PopoverCore {
getTriggerAttrs(state: PopoverState, popupId?: string) {
return {
'aria-expanded': state.open ? 'true' : 'false',
'aria-expanded': state.open && state.status !== 'ending' ? 'true' : 'false',
'aria-haspopup': 'dialog',
'aria-controls': popupId,
};
@@ -69,6 +69,16 @@ describe('PopoverCore', () => {
expect(attrs['aria-expanded']).toBe('true');
});
it('returns aria-expanded false when closing', () => {
const core = new PopoverCore();
core.setInput({ active: true, status: 'ending' });
const state = core.getState();
const attrs = core.getTriggerAttrs(state);
expect(state.open).toBe(true);
expect(attrs['aria-expanded']).toBe('false');
});
it('includes aria-controls when popupId is provided', () => {
const core = new PopoverCore();
core.setInput(OPEN);