From 28d0dd151a97263a245a872e620cde4bf5472fe5 Mon Sep 17 00:00:00 2001 From: Sharon Stratsianis Date: Mon, 26 Oct 2026 16:41:35 +1100 Subject: [PATCH 1/2] fix the menu, preserve action identity when lazy items reorder --- package-lock.json | 28 ++++----- src/components/menu/Menu.test.ts | 60 +++++++++++++++++-- src/components/menu/Menu.ts | 32 ++++++++-- .../ResourceActionsMenu.ts | 11 +++- 4 files changed, 102 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index 538c41d44..651ee9d0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3911,12 +3911,13 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.155.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.155.0.tgz", - "integrity": "sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==", + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", + "dev": true, "license": "MIT", "funding": { - "url": "https://github.com/sponsors/oxc-project" + "url": "https://github.com/sponsors/Boshen" } }, "node_modules/@oxc-resolver/binding-android-arm-eabi": { @@ -11662,16 +11663,6 @@ "@oxc-parser/binding-win32-x64-msvc": "0.127.0" } }, - "node_modules/oxc-parser/node_modules/@oxc-project/types": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", - "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, "node_modules/oxc-resolver": { "version": "11.24.2", "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.24.2.tgz", @@ -12479,6 +12470,15 @@ "@rolldown/binding-win32-x64-msvc": "1.2.15" } }, + "node_modules/rolldown/node_modules/@oxc-project/types": { + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.155.0.tgz", + "integrity": "sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", 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` From 27cd9f574ff4716554dfd392c2193787f03a98d5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 26 Oct 2026 08:04:38 +0000 Subject: [PATCH 2/2] chore: update solidos dependencies (dev: solid-logic@6.0.1-4 pane-registry@5.0.2-1) (latest: rdflib@2.4.1) --- package-lock.json | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 651ee9d0d..538c41d44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3911,13 +3911,12 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", - "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", - "dev": true, + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.155.0.tgz", + "integrity": "sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==", "license": "MIT", "funding": { - "url": "https://github.com/sponsors/Boshen" + "url": "https://github.com/sponsors/oxc-project" } }, "node_modules/@oxc-resolver/binding-android-arm-eabi": { @@ -11663,6 +11662,16 @@ "@oxc-parser/binding-win32-x64-msvc": "0.127.0" } }, + "node_modules/oxc-parser/node_modules/@oxc-project/types": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/oxc-resolver": { "version": "11.24.2", "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.24.2.tgz", @@ -12470,15 +12479,6 @@ "@rolldown/binding-win32-x64-msvc": "1.2.15" } }, - "node_modules/rolldown/node_modules/@oxc-project/types": { - "version": "0.155.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.155.0.tgz", - "integrity": "sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/oxc-project" - } - }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz",