fix(packages): handle menu child mutations (#1739)

This commit is contained in:
Sam Potts
2026-06-30 10:20:07 +10:00
committed by GitHub
parent a6d30a9e61
commit 9ab7adefb7
7 changed files with 163 additions and 2 deletions
+1 -1
View File
@@ -252,7 +252,7 @@ export class HlsJsMedia extends HTMLVideoElementHost implements HlsMediaProps {
return {
...this.config.hlsJs,
preferPlayback: this.config.preferPlayback,
contentType: this.config.contentType,
contentType: this.config.contentType ?? inferContentType(this.#src),
};
}
@@ -3,7 +3,7 @@ import { MediaError } from '../../../../core/media/media-error';
import type { RemotePlaybackLike } from '../../../../core/media/types';
import { addComponent, type Component } from '../../media-host';
import { NativeHlsMedia } from '../../native-hls';
import { ContentTypes, HlsJsMedia } from '../index';
import { ContentTypes, Hls, HlsJsMedia } from '../index';
afterEach(() => {
document.body.innerHTML = '';
@@ -162,6 +162,26 @@ describe('HlsJsMedia', () => {
expect(handler).not.toHaveBeenCalled();
});
it('recreates the engine when inferred content type changes', () => {
vi.spyOn(Hls, 'isSupported').mockReturnValue(true);
const video = document.createElement('video');
document.body.appendChild(video);
const media = new HlsJsMedia();
media.attach(video);
media.src = 'https://example.com/video.mp4';
media.load();
expect(media.engine).toBeNull();
media.src = 'https://example.com/video.m3u8';
media.load();
expect(media.engine).not.toBeNull();
});
it('resets free-form config when a new object is assigned', () => {
const { media } = setup();
@@ -44,6 +44,7 @@ const MENU_VIEW_ACTIVE_STATE = 'active';
const MENU_VIEW_INACTIVE_STATE = 'inactive';
const MENU_ROOT_VIEW_ATTR = 'data-menu-root-view';
const MENU_VIEWPORT_ATTR = 'data-menu-viewport';
const MENU_VIEW_LAYOUT_ATTRS = ['data-availability'];
const MENU_WIDTH_VAR = '--media-menu-width';
const MENU_HEIGHT_VAR = '--media-menu-height';
const MENU_VIEW_MEASURE_STYLE_PROPERTIES = [
@@ -273,6 +274,34 @@ export function syncMenuViewRoot(
setViewportSize(content, size);
}
export function observeMenuViewContent(content: HTMLElement, onChange: () => void): () => void {
if (typeof MutationObserver === 'undefined') return () => {};
let rafId = 0;
function scheduleChange(): void {
cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(() => {
rafId = 0;
onChange();
});
}
const observer = new MutationObserver(scheduleChange);
observer.observe(content, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: MENU_VIEW_LAYOUT_ATTRS,
});
return () => {
cancelAnimationFrame(rafId);
observer.disconnect();
};
}
export function syncMenuViewTransition(
content: HTMLElement | null,
view: HTMLElement | null,
@@ -4,6 +4,7 @@ import {
getMenuRootViewAttrs,
getMenuViewportAttrs,
getMenuViewportElement,
observeMenuViewContent,
syncMenuViewRoot,
syncMenuViewTransition,
} from '../menu-viewport-transition';
@@ -38,6 +39,15 @@ function createRect(width: number, height: number): DOMRect {
} as DOMRect;
}
function nextFrame(): Promise<void> {
return new Promise((resolve) => requestAnimationFrame(() => resolve()));
}
async function waitForMutationFrame(): Promise<void> {
await Promise.resolve();
await nextFrame();
}
function mockMenuViewSize(
element: HTMLElement,
{
@@ -125,6 +135,29 @@ describe('menu-viewport-transition', () => {
expect(getMenuViewportElement(content)).toBe(content);
});
it('observes child and availability changes in menu content', async () => {
const content = addElement();
const item = document.createElement('div');
const onChange = vi.fn();
const cleanup = observeMenuViewContent(content, onChange);
content.append(item);
await waitForMutationFrame();
expect(onChange).toHaveBeenCalledTimes(1);
item.setAttribute('data-availability', 'available');
await waitForMutationFrame();
expect(onChange).toHaveBeenCalledTimes(2);
cleanup();
item.setAttribute('data-availability', 'unavailable');
await waitForMutationFrame();
expect(onChange).toHaveBeenCalledTimes(2);
});
it('toggles menu viewport data attributes for active and exiting phases', () => {
const content = addElement();
const rootView = document.createElement('div');