mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(media-store): add fullscreen state mediator with shadow DOM support
Implement comprehensive fullscreen state management: - Cross-browser fullscreen API support (webkit, moz, ms prefixes) - Advanced shadow DOM traversal for nested web components - Support for container-based fullscreen targeting - Event-driven state updates with stateOwnersUpdateHandlers - Fallback support for older Safari versions using composed node traversal The implementation handles complex scenarios where fullscreen elements are nested within shadow DOM boundaries, ensuring accurate state detection in modern web component architectures. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
committed by
Christian Pillsbury
co-authored by
Claude
parent
29ed5e3759
commit
7f243c22d8
@@ -46,6 +46,7 @@ export type StateMediator = {
|
||||
currentTime: FacadeProp<HTMLMediaElement['currentTime']>;
|
||||
duration: ReadonlyFacadeProp<HTMLMediaElement['duration']>;
|
||||
seekable: ReadonlyFacadeProp<[number, number] | undefined>;
|
||||
fullscreen: FacadeProp<boolean>;
|
||||
};
|
||||
|
||||
export function createMediaStore({
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { playable } from './state-mediators/playable';
|
||||
import { audible } from './state-mediators/audible';
|
||||
import { temporal } from './state-mediators/temporal';
|
||||
import { fullscreenable } from './state-mediators/fullscreenable';
|
||||
import { createMediaStore as factory } from './factory';
|
||||
// Example of default media store with default state mediator definitions. (CJP)
|
||||
// NOTE: We can also change the API to take an array of stateMediators (or either/both) (CJP)
|
||||
const stateMediator = { ...playable, ...audible, ...temporal };
|
||||
const stateMediator = { ...playable, ...audible, ...temporal, ...fullscreenable };
|
||||
type Params = Partial<Parameters<typeof factory>[0]>;
|
||||
export const createMediaStore = (params: Params = {}) =>
|
||||
factory({ stateMediator, ...params });
|
||||
@@ -0,0 +1,150 @@
|
||||
// Utility function to check if a root node contains a child node across shadow DOM boundaries
|
||||
const containsComposedNode = (rootNode: Node, childNode: Node): boolean => {
|
||||
if (!rootNode || !childNode) return false;
|
||||
if (rootNode?.contains(childNode)) return true;
|
||||
const childRootNode = childNode.getRootNode();
|
||||
if (childRootNode && 'host' in childRootNode && childRootNode.host) {
|
||||
return containsComposedNode(rootNode, childRootNode.host as Node);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/** @TODO This is implemented for web/browser only! We will need an alternative state mediator model for e.g. React Native. (CJP) */
|
||||
export const fullscreenable = {
|
||||
fullscreen: {
|
||||
get(stateOwners: any) {
|
||||
const { container } = stateOwners;
|
||||
if (!container || !globalThis?.document) return false;
|
||||
|
||||
const doc = globalThis.document;
|
||||
const currentFullscreenElement =
|
||||
doc.fullscreenElement ||
|
||||
(doc as any).webkitFullscreenElement ||
|
||||
(doc as any).mozFullScreenElement ||
|
||||
(doc as any).msFullscreenElement;
|
||||
|
||||
if (!currentFullscreenElement) return false;
|
||||
|
||||
// If document.fullscreenElement is the container, we're definitely in fullscreen
|
||||
if (currentFullscreenElement === container) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if container is contained within the fullscreen element
|
||||
if (currentFullscreenElement.contains?.(container)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle web components with shadow DOM - traverse shadow DOM layers
|
||||
// In this case (most modern browsers), the fullscreenElement may be
|
||||
// a web component that contains our container within shadow DOM layers
|
||||
if (currentFullscreenElement.localName?.includes('-')) {
|
||||
let currentRoot = currentFullscreenElement.shadowRoot;
|
||||
|
||||
// Check if ShadowRoot supports fullscreenElement (Safari < 16.4 workaround)
|
||||
const fullscreenElementKey =
|
||||
'fullscreenElement' in doc
|
||||
? 'fullscreenElement'
|
||||
: 'webkitFullscreenElement' in doc
|
||||
? 'webkitFullscreenElement'
|
||||
: undefined;
|
||||
|
||||
if (
|
||||
fullscreenElementKey &&
|
||||
!(fullscreenElementKey in (currentRoot || {}))
|
||||
) {
|
||||
// For older Safari versions, use composed node containment check
|
||||
return containsComposedNode(currentFullscreenElement, container);
|
||||
}
|
||||
|
||||
// Traverse shadow DOM layers looking for our container
|
||||
while (fullscreenElementKey && currentRoot?.[fullscreenElementKey]) {
|
||||
if (currentRoot[fullscreenElementKey] === container) return true;
|
||||
if (currentRoot[fullscreenElementKey]?.contains?.(container))
|
||||
return true;
|
||||
currentRoot = currentRoot[fullscreenElementKey]?.shadowRoot;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
set(value: boolean, stateOwners: any) {
|
||||
const { container } = stateOwners;
|
||||
if (!container || !globalThis?.document) return;
|
||||
|
||||
try {
|
||||
if (value) {
|
||||
// Enter fullscreen
|
||||
if (container.requestFullscreen) {
|
||||
container.requestFullscreen();
|
||||
} else if (container.webkitRequestFullscreen) {
|
||||
// Safari support
|
||||
container.webkitRequestFullscreen();
|
||||
} else if (container.mozRequestFullScreen) {
|
||||
// Firefox support
|
||||
container.mozRequestFullScreen();
|
||||
} else if (container.msRequestFullscreen) {
|
||||
// IE/Edge support
|
||||
container.msRequestFullscreen();
|
||||
}
|
||||
} else {
|
||||
// Exit fullscreen
|
||||
const doc = globalThis.document as any;
|
||||
if (doc.exitFullscreen) {
|
||||
doc.exitFullscreen();
|
||||
} else if (doc.webkitExitFullscreen) {
|
||||
// Safari support
|
||||
doc.webkitExitFullscreen();
|
||||
} else if (doc.mozCancelFullScreen) {
|
||||
// Firefox support
|
||||
doc.mozCancelFullScreen();
|
||||
} else if (doc.msExitFullscreen) {
|
||||
// IE/Edge support
|
||||
doc.msExitFullscreen();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Gracefully handle fullscreen API errors (e.g., user interaction required)
|
||||
console.warn('Fullscreen operation failed:', error);
|
||||
}
|
||||
},
|
||||
stateOwnersUpdateHandlers: [
|
||||
(handler: (value?: boolean) => void, _stateOwners: any) => {
|
||||
if (!globalThis?.document) return;
|
||||
|
||||
const eventHandler = () => handler();
|
||||
const events = [
|
||||
'fullscreenchange',
|
||||
'webkitfullscreenchange',
|
||||
'mozfullscreenchange',
|
||||
'MSFullscreenChange',
|
||||
];
|
||||
|
||||
events.forEach((event) => {
|
||||
globalThis.document.addEventListener(event, eventHandler);
|
||||
});
|
||||
|
||||
return () => {
|
||||
events.forEach((event) => {
|
||||
globalThis.document.removeEventListener(event, eventHandler);
|
||||
});
|
||||
};
|
||||
},
|
||||
],
|
||||
actions: {
|
||||
/** Toggle fullscreen state or explicitly enter/exit based on detail */
|
||||
fullscreenrequest: (
|
||||
{ detail }: Pick<CustomEvent<any>, 'detail'> = { detail: undefined },
|
||||
) => {
|
||||
// If detail is provided, use it; otherwise toggle current state
|
||||
if (typeof detail === 'boolean') {
|
||||
return detail;
|
||||
}
|
||||
|
||||
// Toggle behavior: check current fullscreen state
|
||||
const fullscreenEl = globalThis?.document?.fullscreenElement;
|
||||
return !fullscreenEl;
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export { audible } from './audible';
|
||||
export { playable } from './playable';
|
||||
export { temporal } from './temporal';
|
||||
export { fullscreenable } from './fullscreenable';
|
||||
Reference in New Issue
Block a user