From e761d1fe96d7c188633128e4b0d680838eedc207 Mon Sep 17 00:00:00 2001
From: Santiago Rojas <126520289+Santirv17@users.noreply.github.com>
Date: Wed, 1 Jul 2026 17:39:35 +0200
Subject: [PATCH 1/3] fix: remove react-fullscreen-crossbrowser dependency and
implement fullscreen handling for player fullscreen mode
---
package.json | 1 -
pnpm-lock.yaml | 8 --
src/modules/player/rightPanel/SideContent.tsx | 92 +++++++++++++++----
3 files changed, 72 insertions(+), 29 deletions(-)
diff --git a/package.json b/package.json
index 4b787a967..66fd96d34 100644
--- a/package.json
+++ b/package.json
@@ -63,7 +63,6 @@
"react-dnd": "16.0.1",
"react-dnd-html5-backend": "16.0.1",
"react-dom": "19.1.1",
- "react-fullscreen-crossbrowser": "1.1.3",
"react-helmet-async": "2.0.5",
"react-hook-form": "7.54.2",
"react-i18next": "16.0.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7371e2c37..69f1a5b2c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -152,9 +152,6 @@ importers:
react-dom:
specifier: 19.1.1
version: 19.1.1(react@19.1.1)
- react-fullscreen-crossbrowser:
- specifier: 1.1.3
- version: 1.1.3
react-helmet-async:
specifier: 2.0.5
version: 2.0.5(react@19.1.1)
@@ -5714,9 +5711,6 @@ packages:
react-fast-compare@3.2.2:
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
- react-fullscreen-crossbrowser@1.1.3:
- resolution: {integrity: sha512-z1iOYRnciP+rcgH5t+TDAeFmUmBUAwK5hRkiTg0Rn7VbI0YYcgRRcIVM2hT3fDla7kcT+bXRuIEpZO+54r0P+w==}
-
react-helmet-async@2.0.5:
resolution: {integrity: sha512-rYUYHeus+i27MvFE+Jaa4WsyBKGkL6qVgbJvSBoX8mbsWoABJXdEO0bZyi0F6i+4f0NuIb8AvqPMj3iXFHkMwg==}
peerDependencies:
@@ -13126,8 +13120,6 @@ snapshots:
react-fast-compare@3.2.2: {}
- react-fullscreen-crossbrowser@1.1.3: {}
-
react-helmet-async@2.0.5(react@19.1.1):
dependencies:
invariant: 2.2.4
diff --git a/src/modules/player/rightPanel/SideContent.tsx b/src/modules/player/rightPanel/SideContent.tsx
index e87a7be31..45e10ca5d 100644
--- a/src/modules/player/rightPanel/SideContent.tsx
+++ b/src/modules/player/rightPanel/SideContent.tsx
@@ -1,10 +1,9 @@
-import type { JSX } from 'react';
-import Fullscreen from 'react-fullscreen-crossbrowser';
+import { type JSX, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { IconButton, Stack, Tooltip, styled } from '@mui/material';
-import { useParams, useSearch } from '@tanstack/react-router';
+import { useNavigate, useParams, useSearch } from '@tanstack/react-router';
import { ExpandIcon, ShrinkIcon } from 'lucide-react';
import { Chatbox } from '@/components/chatbox/Chatbox/Chatbox';
@@ -63,48 +62,104 @@ type Props = {
};
const SideContent = ({ content, item }: Props): JSX.Element | null => {
- const { rootId } = useParams({ from: '/player/$rootId/$itemId' });
+ const { itemId, rootId } = useParams({ from: '/player/$rootId/$itemId' });
+ const navigate = useNavigate();
const { isMobile } = useMobileView();
const { data: children } = hooks.useChildren(item.id, undefined, {
enabled: !!item,
});
const search = useSearch({ from: '/player/$rootId/$itemId' });
+ const { fullscreen } = search;
const {
toggleChatbox,
togglePinned,
isChatboxOpen,
isPinnedOpen,
- isFullscreen,
setIsFullscreen,
} = useLayoutContext();
const { t } = useTranslation(NS.Player);
const settings = item.settings ?? {};
- if (!rootId) {
- return null;
- }
-
const pinnedItems = children?.filter(
({ settings: s, hidden }) => s.isPinned && !hidden,
);
const pinnedCount = pinnedItems?.length ?? 0;
+ const navigateFullscreen = useCallback(
+ (fullscreenEnabled: boolean) => {
+ navigate({
+ to: '/player/$rootId/$itemId',
+ params: { itemId, rootId },
+ search: { ...search, fullscreen: fullscreenEnabled },
+ });
+ },
+ [itemId, navigate, rootId, search],
+ );
+
const toggleFullscreen = () => {
- setIsFullscreen(!isFullscreen);
+ if (fullscreen) {
+ if (document.fullscreenElement) {
+ document.exitFullscreen().catch((err) => {
+ console.error(
+ `Error attempting to exit fullscreen mode: ${err.message} (${err.name})`,
+ );
+ });
+ }
+
+ setIsFullscreen(false);
+ navigateFullscreen(false);
+ return;
+ }
+
+ document.documentElement
+ .requestFullscreen()
+ .then(() => {
+ setIsFullscreen(true);
+ navigateFullscreen(true);
+ })
+ .catch((err) => {
+ console.error(
+ `Error attempting to enable fullscreen mode: ${err.message} (${err.name})`,
+ );
+ });
};
+ useEffect(() => {
+ const syncFullscreenRoute = () => {
+ const isBrowserFullscreen = Boolean(document.fullscreenElement);
+ setIsFullscreen(isBrowserFullscreen);
+
+ if (fullscreen && !isBrowserFullscreen) {
+ navigateFullscreen(false);
+ }
+ };
+
+ syncFullscreenRoute();
+
+ window.addEventListener('fullscreenchange', syncFullscreenRoute);
+ window.addEventListener('resize', syncFullscreenRoute);
+
+ return () => {
+ window.removeEventListener('fullscreenchange', syncFullscreenRoute);
+ window.removeEventListener('resize', syncFullscreenRoute);
+ };
+ }, [fullscreen, navigateFullscreen, setIsFullscreen]);
+
+ if (!rootId) {
+ return null;
+ }
+
const displayFullscreenButton = () => {
- const { fullscreen } = search;
- if (isMobile || !fullscreen) {
+ if (isMobile) {
return null;
}
return (
{
- {isFullscreen ? : }
+ {fullscreen ? : }
);
@@ -163,10 +218,7 @@ const SideContent = ({ content, item }: Props): JSX.Element | null => {
};
return (
- setIsFullscreen(isFullscreenEnabled)}
- >
+ <>
{displayChatbox()}
{displayPinnedItems()}
@@ -178,7 +230,7 @@ const SideContent = ({ content, item }: Props): JSX.Element | null => {
{content}
-
+ >
);
};
From ad73a5e81044986d1cdc638fcca810e36fbe882f Mon Sep 17 00:00:00 2001
From: Santiago Rojas <126520289+Santirv17@users.noreply.github.com>
Date: Tue, 14 Jul 2026 17:11:05 +0200
Subject: [PATCH 2/3] refactor: simplify player fullscreen state handling
---
cypress/e2e/player/autoLogin.cy.ts | 6 +-
cypress/e2e/player/fullscreen.cy.ts | 130 ++++++++++++++++++
cypress/e2e/player/shortcut.cy.ts | 6 +-
src/modules/player/contexts/LayoutContext.tsx | 18 +--
src/modules/player/rightPanel/SideContent.tsx | 60 ++++----
5 files changed, 161 insertions(+), 59 deletions(-)
create mode 100644 cypress/e2e/player/fullscreen.cy.ts
diff --git a/cypress/e2e/player/autoLogin.cy.ts b/cypress/e2e/player/autoLogin.cy.ts
index 5f6c94a98..4358c0c40 100644
--- a/cypress/e2e/player/autoLogin.cy.ts
+++ b/cypress/e2e/player/autoLogin.cy.ts
@@ -47,7 +47,7 @@ describe('Auto Login on pseudonimized item', () => {
['1234', '"1234"', 'bobichette'].forEach((username) =>
it(`Allows auto login for ${username} on item with item login`, () => {
const search = new URLSearchParams({
- fullscreen: 'true',
+ shuffle: 'true',
});
const keepSearchString = search.toString();
search.set('username', username);
@@ -92,7 +92,7 @@ describe('Auto Login on private item', () => {
it('Fails if itemLogin is not enabled', () => {
const search = new URLSearchParams({
username: '1234',
- fullscreen: 'true',
+ shuffle: 'true',
});
const routeArgs = {
rootId: pseudonimizedItem.id,
@@ -116,7 +116,7 @@ describe('Auto Login with logged in user', () => {
it('Redirects to item page', () => {
const search = new URLSearchParams({
username: '1234',
- fullscreen: 'true',
+ shuffle: 'true',
});
const routeArgs = {
rootId: pseudonimizedItem.id,
diff --git a/cypress/e2e/player/fullscreen.cy.ts b/cypress/e2e/player/fullscreen.cy.ts
new file mode 100644
index 000000000..707a7a944
--- /dev/null
+++ b/cypress/e2e/player/fullscreen.cy.ts
@@ -0,0 +1,130 @@
+import { PackedFolderItemFactory } from '@graasp/sdk';
+
+import { ITEM_FULLSCREEN_BUTTON_ID } from '../../../src/config/selectors';
+import { buildContentPagePath } from './utils';
+
+type FullscreenState = {
+ element: Element | null;
+};
+
+const visitWithFullscreenApi = (path: string): FullscreenState => {
+ const fullscreenState: FullscreenState = { element: null };
+
+ cy.visit(path, {
+ onBeforeLoad: (win) => {
+ Object.defineProperty(win.document, 'fullscreenElement', {
+ configurable: true,
+ get: () => fullscreenState.element,
+ });
+ Object.defineProperty(win.document.documentElement, 'requestFullscreen', {
+ configurable: true,
+ value: cy.stub().callsFake(() => {
+ fullscreenState.element = win.document.documentElement;
+ win.document.dispatchEvent(new win.Event('fullscreenchange'));
+ return Promise.resolve();
+ }),
+ });
+ Object.defineProperty(win.document, 'exitFullscreen', {
+ configurable: true,
+ value: cy.stub().callsFake(() => {
+ fullscreenState.element = null;
+ win.document.dispatchEvent(new win.Event('fullscreenchange'));
+ return Promise.resolve();
+ }),
+ });
+ },
+ });
+
+ return fullscreenState;
+};
+
+const expectSearchParams = (expected: Record): void => {
+ cy.location('search').should((search) => {
+ const searchParams = new URLSearchParams(search);
+
+ Object.entries(expected).forEach(([key, value]) => {
+ expect(searchParams.get(key)).to.equal(value);
+ });
+ });
+};
+
+describe('Fullscreen', () => {
+ const item = PackedFolderItemFactory({ settings: {} });
+
+ beforeEach(() => {
+ cy.setUpApi({ items: [item] });
+ });
+
+ it('Toggles fullscreen from the player and preserves search parameters', () => {
+ visitWithFullscreenApi(
+ buildContentPagePath({
+ rootId: item.id,
+ itemId: item.id,
+ searchParams: 'shuffle=true',
+ }),
+ );
+
+ cy.get(`#${ITEM_FULLSCREEN_BUTTON_ID}`).should('be.visible').click();
+
+ expectSearchParams({ fullscreen: 'true', shuffle: 'true' });
+ cy.window()
+ .its('document.documentElement.requestFullscreen')
+ .should('have.been.calledOnce');
+
+ cy.get(`#${ITEM_FULLSCREEN_BUTTON_ID}`).click();
+
+ expectSearchParams({ fullscreen: 'false', shuffle: 'true' });
+ cy.window().its('document.exitFullscreen').should('have.been.calledOnce');
+ });
+
+ it('Returns to the normal player route after an external fullscreen exit', () => {
+ const fullscreenState = visitWithFullscreenApi(
+ buildContentPagePath({ rootId: item.id, itemId: item.id }),
+ );
+
+ cy.get(`#${ITEM_FULLSCREEN_BUTTON_ID}`).click();
+ expectSearchParams({ fullscreen: 'true' });
+
+ cy.window().then((win) => {
+ fullscreenState.element = null;
+ win.document.dispatchEvent(new win.Event('fullscreenchange'));
+ });
+
+ expectSearchParams({ fullscreen: 'false' });
+ });
+
+ it('Uses resize as a fallback when fullscreen exits externally', () => {
+ const fullscreenState = visitWithFullscreenApi(
+ buildContentPagePath({ rootId: item.id, itemId: item.id }),
+ );
+
+ cy.get(`#${ITEM_FULLSCREEN_BUTTON_ID}`).click();
+ expectSearchParams({ fullscreen: 'true' });
+
+ cy.window().then((win) => {
+ fullscreenState.element = null;
+ win.dispatchEvent(new win.Event('resize'));
+ });
+
+ expectSearchParams({ fullscreen: 'false' });
+ });
+
+ it('Corrects a fullscreen route when the browser is not fullscreen', () => {
+ cy.visit(
+ buildContentPagePath({
+ rootId: item.id,
+ itemId: item.id,
+ searchParams: 'fullscreen=true',
+ }),
+ );
+
+ expectSearchParams({ fullscreen: 'false' });
+ });
+
+ it('Hides the fullscreen button on mobile', () => {
+ cy.viewport('iphone-x');
+ cy.visit(buildContentPagePath({ rootId: item.id, itemId: item.id }));
+
+ cy.get(`#${ITEM_FULLSCREEN_BUTTON_ID}`).should('not.exist');
+ });
+});
diff --git a/cypress/e2e/player/shortcut.cy.ts b/cypress/e2e/player/shortcut.cy.ts
index 2d864cbc1..21cf63505 100644
--- a/cypress/e2e/player/shortcut.cy.ts
+++ b/cypress/e2e/player/shortcut.cy.ts
@@ -132,7 +132,7 @@ describe('Shortcuts', () => {
buildContentPagePath({
rootId: parentItem.id,
itemId: parentItem.id,
- searchParams: 'fullscreen=true',
+ searchParams: 'shuffle=true',
}),
);
@@ -143,11 +143,11 @@ describe('Shortcuts', () => {
.and('contain', parentItem.id)
.and('contain', 'fromName')
.and('contain', 'parent+item')
- .and('contain', 'fullscreen=true');
+ .and('contain', 'shuffle=true');
// go back to origin
cy.get(`#${BACK_TO_SHORTCUT_ID}`).click();
- cy.url().should('contain', parentItem.id).and('contain', 'fullscreen=true');
+ cy.url().should('contain', parentItem.id).and('contain', 'shuffle=true');
});
it('No from name does not show button', () => {
diff --git a/src/modules/player/contexts/LayoutContext.tsx b/src/modules/player/contexts/LayoutContext.tsx
index 0bdbce78c..7a4e30aae 100644
--- a/src/modules/player/contexts/LayoutContext.tsx
+++ b/src/modules/player/contexts/LayoutContext.tsx
@@ -16,8 +16,6 @@ type LayoutContextType = {
setIsPinnedOpen: Dispatch>;
isChatboxOpen: boolean;
setIsChatboxOpen: Dispatch>;
- isFullscreen: boolean;
- setIsFullscreen: Dispatch>;
toggleChatbox: () => void;
togglePinned: () => void;
};
@@ -31,10 +29,6 @@ const LayoutContext = createContext({
setIsChatboxOpen: () => {
throw new Error('No context');
},
- isFullscreen: false,
- setIsFullscreen: () => {
- throw new Error('No context');
- },
toggleChatbox: () => {
throw new Error('No context');
},
@@ -52,7 +46,6 @@ export const LayoutContextProvider = ({ children }: Props): JSX.Element => {
const [isPinnedOpen, setIsPinnedOpen] = useState(!isMobile);
const [isChatboxOpen, setIsChatboxOpen] = useState(false);
- const [isFullscreen, setIsFullscreen] = useState(false);
useEffect(() => {
// TODO: fix this issue
@@ -66,8 +59,6 @@ export const LayoutContextProvider = ({ children }: Props): JSX.Element => {
setIsPinnedOpen,
isChatboxOpen,
setIsChatboxOpen,
- isFullscreen,
- setIsFullscreen,
toggleChatbox: () => {
setIsChatboxOpen((prev) => !prev);
if (isPinnedOpen) {
@@ -83,14 +74,7 @@ export const LayoutContextProvider = ({ children }: Props): JSX.Element => {
}
},
}),
- [
- isPinnedOpen,
- setIsPinnedOpen,
- isChatboxOpen,
- setIsChatboxOpen,
- isFullscreen,
- setIsFullscreen,
- ],
+ [isPinnedOpen, setIsPinnedOpen, isChatboxOpen, setIsChatboxOpen],
);
return (
{children}
diff --git a/src/modules/player/rightPanel/SideContent.tsx b/src/modules/player/rightPanel/SideContent.tsx
index 45e10ca5d..5c8af33cd 100644
--- a/src/modules/player/rightPanel/SideContent.tsx
+++ b/src/modules/player/rightPanel/SideContent.tsx
@@ -71,13 +71,8 @@ const SideContent = ({ content, item }: Props): JSX.Element | null => {
const search = useSearch({ from: '/player/$rootId/$itemId' });
const { fullscreen } = search;
- const {
- toggleChatbox,
- togglePinned,
- isChatboxOpen,
- isPinnedOpen,
- setIsFullscreen,
- } = useLayoutContext();
+ const { toggleChatbox, togglePinned, isChatboxOpen, isPinnedOpen } =
+ useLayoutContext();
const { t } = useTranslation(NS.Player);
const settings = item.settings ?? {};
@@ -92,44 +87,37 @@ const SideContent = ({ content, item }: Props): JSX.Element | null => {
navigate({
to: '/player/$rootId/$itemId',
params: { itemId, rootId },
- search: { ...search, fullscreen: fullscreenEnabled },
+ search: (currentSearch) => ({
+ ...currentSearch,
+ fullscreen: fullscreenEnabled,
+ }),
});
},
- [itemId, navigate, rootId, search],
+ [itemId, navigate, rootId],
);
- const toggleFullscreen = () => {
- if (fullscreen) {
- if (document.fullscreenElement) {
- document.exitFullscreen().catch((err) => {
- console.error(
- `Error attempting to exit fullscreen mode: ${err.message} (${err.name})`,
- );
- });
+ const toggleFullscreen = async () => {
+ try {
+ if (fullscreen) {
+ navigateFullscreen(false);
+
+ if (document.fullscreenElement) {
+ await document.exitFullscreen();
+ }
+
+ return;
}
- setIsFullscreen(false);
- navigateFullscreen(false);
- return;
+ await document.documentElement.requestFullscreen();
+ navigateFullscreen(true);
+ } catch (error) {
+ console.error('Error attempting to toggle fullscreen mode:', error);
}
-
- document.documentElement
- .requestFullscreen()
- .then(() => {
- setIsFullscreen(true);
- navigateFullscreen(true);
- })
- .catch((err) => {
- console.error(
- `Error attempting to enable fullscreen mode: ${err.message} (${err.name})`,
- );
- });
};
useEffect(() => {
const syncFullscreenRoute = () => {
const isBrowserFullscreen = Boolean(document.fullscreenElement);
- setIsFullscreen(isBrowserFullscreen);
if (fullscreen && !isBrowserFullscreen) {
navigateFullscreen(false);
@@ -138,14 +126,14 @@ const SideContent = ({ content, item }: Props): JSX.Element | null => {
syncFullscreenRoute();
- window.addEventListener('fullscreenchange', syncFullscreenRoute);
+ document.addEventListener('fullscreenchange', syncFullscreenRoute);
window.addEventListener('resize', syncFullscreenRoute);
return () => {
- window.removeEventListener('fullscreenchange', syncFullscreenRoute);
+ document.removeEventListener('fullscreenchange', syncFullscreenRoute);
window.removeEventListener('resize', syncFullscreenRoute);
};
- }, [fullscreen, navigateFullscreen, setIsFullscreen]);
+ }, [fullscreen, navigateFullscreen]);
if (!rootId) {
return null;
From ba025ed3042a6909f7bd554254cf2a4df830e9bd Mon Sep 17 00:00:00 2001
From: Santiago Rojas <126520289+Santirv17@users.noreply.github.com>
Date: Wed, 15 Jul 2026 11:08:41 +0200
Subject: [PATCH 3/3] fix: update auto-login fullscreen expectation
---
cypress/e2e/player/autoLogin.cy.ts | 14 ++++++++------
1 file changed, 8 insertions(+), 6 deletions(-)
diff --git a/cypress/e2e/player/autoLogin.cy.ts b/cypress/e2e/player/autoLogin.cy.ts
index 4358c0c40..02ad96d3a 100644
--- a/cypress/e2e/player/autoLogin.cy.ts
+++ b/cypress/e2e/player/autoLogin.cy.ts
@@ -47,9 +47,8 @@ describe('Auto Login on pseudonimized item', () => {
['1234', '"1234"', 'bobichette'].forEach((username) =>
it(`Allows auto login for ${username} on item with item login`, () => {
const search = new URLSearchParams({
- shuffle: 'true',
+ fullscreen: 'true',
});
- const keepSearchString = search.toString();
search.set('username', username);
const routeArgs = {
rootId: pseudonimizedItem.id,
@@ -63,8 +62,11 @@ describe('Auto Login on pseudonimized item', () => {
// checks that the user was correctly redirected to the item page
const { searchParams: _, ...pathArgs } = routeArgs;
cy.location('pathname').should('equal', buildContentPagePath(pathArgs));
- // keep the search params
- cy.location('search').should('equal', `?${keepSearchString}`);
+ cy.location('search').should((searchString) => {
+ const searchParams = new URLSearchParams(searchString);
+ expect(searchParams.get('fullscreen')).to.equal('false');
+ expect(searchParams.has('username')).to.equal(false);
+ });
}),
);
it('Missing username triggers error', () => {
@@ -92,7 +94,7 @@ describe('Auto Login on private item', () => {
it('Fails if itemLogin is not enabled', () => {
const search = new URLSearchParams({
username: '1234',
- shuffle: 'true',
+ fullscreen: 'true',
});
const routeArgs = {
rootId: pseudonimizedItem.id,
@@ -116,7 +118,7 @@ describe('Auto Login with logged in user', () => {
it('Redirects to item page', () => {
const search = new URLSearchParams({
username: '1234',
- shuffle: 'true',
+ fullscreen: 'true',
});
const routeArgs = {
rootId: pseudonimizedItem.id,