diff --git a/packages/core/src/dom/ui/menu/menu-viewport-transition.ts b/packages/core/src/dom/ui/menu/menu-viewport-transition.ts index 4e82ea69..35ab952f 100644 --- a/packages/core/src/dom/ui/menu/menu-viewport-transition.ts +++ b/packages/core/src/dom/ui/menu/menu-viewport-transition.ts @@ -335,12 +335,24 @@ export function observeMenuViewContent(content: HTMLElement, onChange: () => voi }); } - const observer = new MutationObserver(scheduleChange); + const observer = new MutationObserver((records) => { + const changed = records.some((record) => { + if (record.type !== 'attributes') return true; + + const name = record.attributeName; + if (!name) return true; + + return record.oldValue !== (record.target as Element).getAttribute(name); + }); + + if (changed) scheduleChange(); + }); observer.observe(content, { childList: true, subtree: true, attributes: true, + attributeOldValue: true, attributeFilter: MENU_VIEW_LAYOUT_ATTRS, }); diff --git a/packages/core/src/dom/ui/menu/tests/menu-viewport-transition.test.ts b/packages/core/src/dom/ui/menu/tests/menu-viewport-transition.test.ts index 09a5ff2f..7136306f 100644 --- a/packages/core/src/dom/ui/menu/tests/menu-viewport-transition.test.ts +++ b/packages/core/src/dom/ui/menu/tests/menu-viewport-transition.test.ts @@ -172,11 +172,21 @@ describe('menu-viewport-transition', () => { expect(onChange).toHaveBeenCalledTimes(2); - cleanup(); - item.setAttribute('data-availability', 'unavailable'); + item.setAttribute('data-availability', 'available'); await waitForMutationFrame(); expect(onChange).toHaveBeenCalledTimes(2); + + item.setAttribute('data-availability', 'unavailable'); + await waitForMutationFrame(); + + expect(onChange).toHaveBeenCalledTimes(3); + + cleanup(); + item.setAttribute('data-availability', 'available'); + await waitForMutationFrame(); + + expect(onChange).toHaveBeenCalledTimes(3); }); it('toggles menu viewport data attributes for active and exiting phases', () => { diff --git a/packages/html/src/ui/menu/menu-item-value-element.ts b/packages/html/src/ui/menu/menu-item-value-element.ts index 0e92d588..877d8c99 100644 --- a/packages/html/src/ui/menu/menu-item-value-element.ts +++ b/packages/html/src/ui/menu/menu-item-value-element.ts @@ -17,6 +17,9 @@ export class MenuItemValueElement extends MediaElement { protected override update(_changed: PropertyValues): void { super.update(_changed); - this.textContent = this.#ctx.value?.label ?? ''; + const label = this.#ctx.value?.label ?? ''; + if (this.textContent !== label) { + this.textContent = label; + } } } diff --git a/packages/html/src/ui/menu/tests/menu-item-value-element.test.ts b/packages/html/src/ui/menu/tests/menu-item-value-element.test.ts index 78e9a601..d457cd49 100644 --- a/packages/html/src/ui/menu/tests/menu-item-value-element.test.ts +++ b/packages/html/src/ui/menu/tests/menu-item-value-element.test.ts @@ -172,6 +172,26 @@ describe('MenuItemValueElement', () => { }); }); + it('does not rewrite an unchanged label', async () => { + const { value } = setup(createPlaybackRateStore({ playbackRate: 1.5 }), 'playback-rate'); + + await value.updateComplete; + await waitForAssertion(() => { + expect(value.textContent).toBe('1.5×'); + }); + + const mutations: MutationRecord[] = []; + const observer = new MutationObserver((records) => mutations.push(...records)); + observer.observe(value, { childList: true, characterData: true, subtree: true }); + + value.requestUpdate(); + await value.updateComplete; + await nextFrame(); + observer.disconnect(); + + expect(mutations).toHaveLength(0); + }); + it('renders Off when captions are disabled', async () => { const { value } = setup( createTextTrackStore({