diff --git a/src/components/menu/Menu.test.ts b/src/components/menu/Menu.test.ts
index a7fdd8a3f..51cd4dbb7 100644
--- a/src/components/menu/Menu.test.ts
+++ b/src/components/menu/Menu.test.ts
@@ -28,8 +28,8 @@ describe('solid-ui-menu syncItems', () => {
await menu.updateComplete
// Children should be assigned slot attributes
- expect(child1.getAttribute('slot')).toBe('menu-item-0')
- expect(child2.getAttribute('slot')).toBe('menu-item-1')
+ expect(child1.getAttribute('slot')).toBe('menu-item-index-0')
+ expect(child2.getAttribute('slot')).toBe('menu-item-index-1')
// Trigger should keep its slot
const trigger = menu.querySelector('[slot="trigger"]')
@@ -49,7 +49,7 @@ describe('solid-ui-menu syncItems', () => {
await menu.updateComplete
// First sync assigned slots
- expect(child1.getAttribute('slot')).toBe('menu-item-0')
+ expect(child1.getAttribute('slot')).toBe('menu-item-index-0')
// Trigger a re-sync by adding another child (MutationObserver)
const child2 = document.createElement('div')
@@ -58,8 +58,8 @@ describe('solid-ui-menu syncItems', () => {
await menu.updateComplete
// Both children should still have valid slot assignments
- expect(child1.getAttribute('slot')).toBe('menu-item-0')
- expect(child2.getAttribute('slot')).toBe('menu-item-1')
+ expect(child1.getAttribute('slot')).toBe('menu-item-index-0')
+ expect(child2.getAttribute('slot')).toBe('menu-item-index-1')
document.body.removeChild(menu)
})
@@ -73,7 +73,7 @@ describe('solid-ui-menu syncItems', () => {
menu.appendChild(child)
await menu.updateComplete
- expect(child.getAttribute('slot')).toBe('menu-item-0')
+ expect(child.getAttribute('slot')).toBe('menu-item-index-0')
// Trigger should not have been re-assigned
const trigger = menu.querySelector('[slot="trigger"]')
@@ -81,4 +81,52 @@ describe('solid-ui-menu syncItems', () => {
document.body.removeChild(menu)
})
+
+ it('keeps keyed and positional slots distinct', async () => {
+ const menu = createMenu()
+
+ const keyedChild = document.createElement('div')
+ keyedChild.dataset.menuKey = '1'
+ menu.appendChild(keyedChild)
+
+ const positionalChild = document.createElement('div')
+ menu.appendChild(positionalChild)
+
+ document.body.appendChild(menu)
+ await menu.updateComplete
+
+ expect(keyedChild.getAttribute('slot')).toBe('menu-item-key-1')
+ expect(positionalChild.getAttribute('slot')).toBe('menu-item-index-1')
+
+ document.body.removeChild(menu)
+ })
+
+ it('moves keyed dropdown items instead of recreating them when actions reorder', async () => {
+ const menu = createMenu()
+ const first = document.createElement('div')
+ first.dataset.menuKey = 'first'
+ const second = document.createElement('div')
+ second.dataset.menuKey = 'second'
+ menu.append(first, second)
+
+ document.body.appendChild(menu)
+ await menu.updateComplete
+
+ const firstWrapper = Array.from(menu.shadowRoot?.querySelectorAll('wa-dropdown-item') || [])
+ .find(item => item.children[0]?.getAttribute('name') === 'menu-item-key-first')
+ const secondWrapper = Array.from(menu.shadowRoot?.querySelectorAll('wa-dropdown-item') || [])
+ .find(item => item.children[0]?.getAttribute('name') === 'menu-item-key-second')
+ expect(firstWrapper).toBeTruthy()
+ expect(secondWrapper).toBeTruthy()
+
+ menu.insertBefore(second, first)
+ await new Promise(resolve => setTimeout(resolve, 0))
+ await menu.updateComplete
+
+ const reorderedWrappers = Array.from(menu.shadowRoot?.querySelectorAll('wa-dropdown-item') || [])
+ expect(reorderedWrappers[0]).toBe(secondWrapper)
+ expect(reorderedWrappers[1]).toBe(firstWrapper)
+
+ document.body.removeChild(menu)
+ })
})
diff --git a/src/components/menu/Menu.ts b/src/components/menu/Menu.ts
index ce479b9d2..d43c5a692 100644
--- a/src/components/menu/Menu.ts
+++ b/src/components/menu/Menu.ts
@@ -1,6 +1,7 @@
import { customElement, WebComponent } from '@/lib/components'
import { html } from 'lit'
import { property, query, state } from 'lit/decorators.js'
+import { repeat } from 'lit/directives/repeat.js'
import type WaDropdown from '@awesome.me/webawesome/dist/components/dropdown/dropdown.js'
import type WaDropdownItem from '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js'
import type { WaDropdownSelectEvent } from '@awesome.me/webawesome'
@@ -26,6 +27,7 @@ export default class Menu extends WebComponent {
@state()
private accessor items: { slot: string; }[] = []
+ private slotToRestoreFocus: string | undefined
private observer: MutationObserver = new MutationObserver(() => this.syncItems())
connectedCallback () {
@@ -50,17 +52,22 @@ export default class Menu extends WebComponent {
>
- ${this.items.map(
- (item) =>
- html`
-
- `
+ ${repeat(
+ this.items,
+ item => item.slot,
+ item => html`
+
+ `
)}
`
}
private syncItems (): void {
+ const activeElement = this.shadowRoot?.activeElement as WaDropdownItem | null
+ const activeSlot = activeElement?.children[0]?.getAttribute('name')
+ if (activeSlot) this.slotToRestoreFocus = activeSlot
+
// Exclude only the trigger element; include all other children
// regardless of whether they already have a slot assigned.
const items = Array.from(this.children).filter(
@@ -68,7 +75,10 @@ export default class Menu extends WebComponent {
)
this.items = items.map((item, index) => {
- const slotName = `menu-item-${index}`
+ const key = item.getAttribute('data-menu-key')
+ const slotName = key
+ ? `menu-item-key-${encodeURIComponent(key)}`
+ : `menu-item-index-${index}`
if (item.getAttribute('slot') !== slotName) {
item.setAttribute('slot', slotName)
@@ -78,6 +88,16 @@ export default class Menu extends WebComponent {
})
}
+ protected updated (): void {
+ if (!this.slotToRestoreFocus) return
+
+ const slotToRestore = this.slotToRestoreFocus
+ this.slotToRestoreFocus = undefined
+ const item = Array.from(this.shadowRoot?.querySelectorAll('wa-dropdown-item') || [])
+ .find(candidate => candidate.children[0]?.getAttribute('name') === slotToRestore)
+ item?.focus({ preventScroll: true })
+ }
+
private onItemClick (event: Event) {
const waItem = event.currentTarget as WaDropdownItem
diff --git a/src/components/resource-actions-menu/ResourceActionsMenu.ts b/src/components/resource-actions-menu/ResourceActionsMenu.ts
index 89e502f59..5b14cc477 100644
--- a/src/components/resource-actions-menu/ResourceActionsMenu.ts
+++ b/src/components/resource-actions-menu/ResourceActionsMenu.ts
@@ -1,6 +1,7 @@
import { customElement, WebComponent } from '@/lib/components'
import { property } from 'lit/decorators.js'
import { html, nothing } from 'lit'
+import { keyed } from 'lit/directives/keyed.js'
import '@/components/button'
import '@/components/menu'
import '@/components/menu-item'
@@ -13,6 +14,7 @@ import styles from './ResourceActionsMenu.styles.css'
import { isContainerSubject } from '@/utils/podUtils'
type ResourceActionMenuItem = {
+ key?: string
label: string
icon?: HTMLElement | string
action?: (event: Event) => void
@@ -68,14 +70,17 @@ export default class ResourceActionsMenu extends WebComponent {
More options
- ${actionableMenuItems.map(item => html`
- item.action(event)}>
+ ${actionableMenuItems.map(item => keyed(item.key ?? item.label, html`
+ item.action(event)}
+ >
${typeof item.icon === 'string'
? html`
`
: item.icon ?? html``}
${item.label}
- `)}
+ `))}
${canEdit
? html`