1+ import { useSetAtom } from "jotai" ;
12import { type RefObject , useCallback , useEffect , useState } from "react" ;
3+ import { useTranslation } from "../../../lib/i18n" ;
4+ import { pushToastAtom } from "../../notifications/atoms" ;
25
36export interface WorkspaceFullscreenController {
47 supported : boolean ;
@@ -8,65 +11,153 @@ export interface WorkspaceFullscreenController {
811 toggleFullscreen : ( ) => Promise < void > ;
912}
1013
11- function canEnterFullscreen (
12- target : HTMLElement | null
13- ) : target is HTMLElement & { requestFullscreen : NonNullable < HTMLElement [ "requestFullscreen" ] > } {
14- return Boolean (
15- document . fullscreenEnabled &&
16- target &&
17- typeof target . requestFullscreen === "function" &&
18- typeof document . exitFullscreen === "function"
19- ) ;
14+ type FullscreenMethod = ( ) => Promise < void > | void ;
15+
16+ type FullscreenDocument = Document & {
17+ webkitCurrentFullScreenElement ?: Element | null ;
18+ webkitExitFullscreen ?: FullscreenMethod ;
19+ webkitCancelFullScreen ?: FullscreenMethod ;
20+ } ;
21+
22+ type FullscreenTarget = HTMLElement & {
23+ requestFullscreen ?: FullscreenMethod ;
24+ webkitRequestFullscreen ?: FullscreenMethod ;
25+ webkitRequestFullScreen ?: FullscreenMethod ;
26+ } ;
27+
28+ function getFullscreenElement ( doc : Document ) : Element | null {
29+ const fullscreenDocument = doc as FullscreenDocument ;
30+ return doc . fullscreenElement ?? fullscreenDocument . webkitCurrentFullScreenElement ?? null ;
31+ }
32+
33+ function getRequestFullscreenMethod ( target : HTMLElement | null ) : FullscreenMethod | null {
34+ if ( ! target ) {
35+ return null ;
36+ }
37+
38+ const fullscreenTarget = target as FullscreenTarget ;
39+
40+ if ( typeof fullscreenTarget . requestFullscreen === "function" ) {
41+ return fullscreenTarget . requestFullscreen . bind ( fullscreenTarget ) ;
42+ }
43+
44+ if ( typeof fullscreenTarget . webkitRequestFullscreen === "function" ) {
45+ return fullscreenTarget . webkitRequestFullscreen . bind ( fullscreenTarget ) ;
46+ }
47+
48+ if ( typeof fullscreenTarget . webkitRequestFullScreen === "function" ) {
49+ return fullscreenTarget . webkitRequestFullScreen . bind ( fullscreenTarget ) ;
50+ }
51+
52+ return null ;
53+ }
54+
55+ function getExitFullscreenMethod ( doc : Document ) : FullscreenMethod | null {
56+ const fullscreenDocument = doc as FullscreenDocument ;
57+
58+ if ( typeof doc . exitFullscreen === "function" ) {
59+ return doc . exitFullscreen . bind ( doc ) ;
60+ }
61+
62+ if ( typeof fullscreenDocument . webkitExitFullscreen === "function" ) {
63+ return fullscreenDocument . webkitExitFullscreen . bind ( doc ) ;
64+ }
65+
66+ if ( typeof fullscreenDocument . webkitCancelFullScreen === "function" ) {
67+ return fullscreenDocument . webkitCancelFullScreen . bind ( doc ) ;
68+ }
69+
70+ return null ;
71+ }
72+
73+ function canUseFullscreen ( target : HTMLElement | null ) : boolean {
74+ return Boolean ( getRequestFullscreenMethod ( target ) && getExitFullscreenMethod ( document ) ) ;
75+ }
76+
77+ function getErrorMessage ( error : unknown ) : string | null {
78+ return error instanceof Error && error . message . trim ( ) . length > 0 ? error . message : null ;
2079}
2180
2281export function useWorkspaceFullscreen (
2382 targetRef : RefObject < HTMLElement | null >
2483) : WorkspaceFullscreenController {
84+ const t = useTranslation ( ) ;
85+ const pushToast = useSetAtom ( pushToastAtom ) ;
2586 const [ supported , setSupported ] = useState ( false ) ;
2687 const [ isFullscreen , setIsFullscreen ] = useState ( false ) ;
2788
89+ const showUnsupportedToast = useCallback ( ( ) => {
90+ pushToast ( {
91+ kind : "info" ,
92+ title : t ( "workspace.fullscreen.unsupported_title" ) ,
93+ body : t ( "workspace.fullscreen.unsupported_body" ) ,
94+ } ) ;
95+ } , [ pushToast , t ] ) ;
96+
2897 useEffect ( ( ) => {
2998 const syncState = ( ) => {
3099 const target = targetRef . current ;
31- setSupported ( canEnterFullscreen ( target ) ) ;
32- setIsFullscreen ( Boolean ( target && document . fullscreenElement === target ) ) ;
100+ setSupported ( canUseFullscreen ( target ) ) ;
101+ setIsFullscreen ( Boolean ( target && getFullscreenElement ( document ) === target ) ) ;
33102 } ;
34103
35104 syncState ( ) ;
36105 document . addEventListener ( "fullscreenchange" , syncState ) ;
106+ document . addEventListener ( "webkitfullscreenchange" , syncState as EventListener ) ;
37107
38108 return ( ) => {
39109 document . removeEventListener ( "fullscreenchange" , syncState ) ;
110+ document . removeEventListener ( "webkitfullscreenchange" , syncState as EventListener ) ;
40111 } ;
41112 } , [ targetRef ] ) ;
42113
43114 const enterFullscreen = useCallback ( async ( ) => {
44115 const target = targetRef . current ;
45- if ( ! canEnterFullscreen ( target ) ) {
116+ const requestFullscreen = getRequestFullscreenMethod ( target ) ;
117+
118+ if ( ! requestFullscreen || ! getExitFullscreenMethod ( document ) ) {
119+ showUnsupportedToast ( ) ;
46120 return ;
47121 }
48122
49123 try {
50- await target . requestFullscreen ( ) ;
124+ await Promise . resolve ( requestFullscreen ( ) ) ;
51125 } catch ( error ) {
52126 console . warn ( "Failed to enter fullscreen" , error ) ;
127+ pushToast ( {
128+ kind : "warning" ,
129+ title : t ( "workspace.fullscreen.enter_failed_title" ) ,
130+ body : getErrorMessage ( error ) ?? t ( "workspace.fullscreen.enter_failed_body" ) ,
131+ } ) ;
53132 }
54- } , [ targetRef ] ) ;
133+ } , [ pushToast , showUnsupportedToast , t , targetRef ] ) ;
55134
56135 const exitFullscreen = useCallback ( async ( ) => {
57- if ( typeof document . exitFullscreen !== "function" || ! document . fullscreenElement ) {
136+ const exitFullscreenMethod = getExitFullscreenMethod ( document ) ;
137+
138+ if ( ! getFullscreenElement ( document ) ) {
139+ return ;
140+ }
141+
142+ if ( ! exitFullscreenMethod ) {
143+ showUnsupportedToast ( ) ;
58144 return ;
59145 }
60146
61147 try {
62- await document . exitFullscreen ( ) ;
148+ await Promise . resolve ( exitFullscreenMethod ( ) ) ;
63149 } catch ( error ) {
64150 console . warn ( "Failed to exit fullscreen" , error ) ;
151+ pushToast ( {
152+ kind : "warning" ,
153+ title : t ( "workspace.fullscreen.exit_failed_title" ) ,
154+ body : getErrorMessage ( error ) ?? t ( "workspace.fullscreen.exit_failed_body" ) ,
155+ } ) ;
65156 }
66- } , [ ] ) ;
157+ } , [ pushToast , showUnsupportedToast , t ] ) ;
67158
68159 const toggleFullscreen = useCallback ( async ( ) => {
69- if ( targetRef . current && document . fullscreenElement === targetRef . current ) {
160+ if ( targetRef . current && getFullscreenElement ( document ) === targetRef . current ) {
70161 await exitFullscreen ( ) ;
71162 return ;
72163 }
0 commit comments