diff --git a/apps/web/src/components/PreviewModal.tsx b/apps/web/src/components/PreviewModal.tsx index 161190739..14dcafd37 100644 --- a/apps/web/src/components/PreviewModal.tsx +++ b/apps/web/src/components/PreviewModal.tsx @@ -72,6 +72,22 @@ export function PreviewModal({ return () => document.removeEventListener('keydown', onKey); }, [onClose, fullscreen]); + // Mirror native fullscreen state into React. Without this, a user in + // browser fullscreen has to press Esc twice: the first Esc exits the + // native fullscreen element (consumed by the browser; in some browsers no + // keydown is delivered) while our `fullscreen` state stays true and the + // overlay keeps its `ds-modal-fullscreen` class. Listening to + // fullscreenchange lets one Esc dismiss both layers in lock-step. + useEffect(() => { + const onFsChange = () => { + if (!document.fullscreenElement) { + setFullscreen(false); + } + }; + document.addEventListener('fullscreenchange', onFsChange); + return () => document.removeEventListener('fullscreenchange', onFsChange); + }, []); + // Close share popover on outside click / Escape. useEffect(() => { if (!shareOpen) return; diff --git a/e2e/tests/preview-modal-fullscreen.test.tsx b/e2e/tests/preview-modal-fullscreen.test.tsx new file mode 100644 index 000000000..54927109a --- /dev/null +++ b/e2e/tests/preview-modal-fullscreen.test.tsx @@ -0,0 +1,109 @@ +import { act, cleanup, fireEvent, render } from '@testing-library/react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { PreviewModal } from '../../apps/web/src/components/PreviewModal'; + +// Regression coverage for nexu-io/open-design#141: pressing Esc in fullscreen +// used to require two presses because the browser exits its native fullscreen +// element on the first press without delivering a keydown to JS, leaving the +// React `fullscreen` state stuck on. The fix listens to fullscreenchange and +// mirrors the native state into React. + +const baseProps = { + title: 'Sample', + views: [{ id: 'main', label: 'Main', html: '
hi
' }], + exportTitleFor: (id: string) => id, +}; + +function dispatchFullscreenChange() { + act(() => { + document.dispatchEvent(new Event('fullscreenchange')); + }); +} + +function setNativeFullscreenElement(el: Element | null) { + Object.defineProperty(document, 'fullscreenElement', { + configurable: true, + get: () => el, + }); +} + +describe('PreviewModal fullscreen exit', () => { + afterEach(() => { + cleanup(); + setNativeFullscreenElement(null); + }); + + it('drops the fullscreen overlay when the browser exits native fullscreen', () => { + const onClose = vi.fn(); + const { container } = render( +