Skip to content

Commit 50c0ba5

Browse files
authored
Merge pull request #37 from ProLoser/fix/dialog-outside-click-handler
Refine dialog outside click detection and remove redundant props
2 parents fd3c70a + ffbf137 commit 50c0ba5

File tree

5 files changed

+181
-53
lines changed

5 files changed

+181
-53
lines changed

src/Dialogues/DialogContext.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React, { createContext } from 'react';
2+
import type { ModalState } from '../Types';
3+
4+
// Define ModalState based on its usage in src/index.tsx
5+
// It can be 'friends', 'profile', 'chat', or false, or other string values.
6+
// export type ModalState = 'friends' | 'profile' | 'chat' | string | boolean;
7+
8+
export interface DialogContextType {
9+
dialogState: ModalState;
10+
toggleDialog: (newState: ModalState) => void;
11+
lastDialogState: ModalState;
12+
}
13+
14+
// Provide sensible defaults or undefined if consumers should always expect a provider
15+
const DialogContext = createContext<DialogContextType | undefined>(undefined);
16+
17+
export default DialogContext;

src/Dialogues/Friends.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// Import FirebaseAuth and firebase.
2-
import { useState, useCallback, useRef, ReactNode, useEffect } from 'react';
2+
import { useState, useCallback, useRef, ReactNode, useEffect, useContext } from 'react'; // Added useContext
33
import type { ChangeEventHandler } from 'react';
44
import { formatDistance } from 'date-fns';
5+
import DialogContext, { DialogContextType } from './DialogContext'; // Added DialogContext
56
import firebase from 'firebase/compat/app';
67
import 'firebase/compat/auth';
78
import 'firebase/compat/database';
@@ -11,7 +12,14 @@ import './Friends.css'
1112
import ToggleFullscreen from '../ToggleFullscreen';
1213
type Users = { [key: string]: UserData }
1314

14-
export default function Friends({ authUser, toggle, load, reset }) {
15+
export default function Friends({ authUser, load, reset }) { // Removed toggle from props
16+
const context = useContext(DialogContext);
17+
if (!context) {
18+
console.error('DialogContext not found in Friends component');
19+
return null;
20+
}
21+
const { toggleDialog } = context; // Destructure toggleDialog from context
22+
1523
const searchRef = useRef<HTMLInputElement>(null);
1624
const [users, setUsers] = useState<Users>({});
1725
const [isExpanded, setIsExpanded] = useState(false);
@@ -63,7 +71,7 @@ export default function Friends({ authUser, toggle, load, reset }) {
6371
const NOW = new Date()
6472

6573
const row = (user: UserData, match?: Match) =>
66-
<li key={user.uid} onPointerUp={() => { load(user.uid, authUser.key); toggle() }}>
74+
<li key={user.uid} onPointerUp={() => { load(user.uid, authUser.key); toggleDialog(false); }}> {/* Use toggleDialog(false) */}
6775
<Avatar user={user} />
6876
<div>
6977
<h3>{user.name}</h3>
@@ -127,7 +135,7 @@ export default function Friends({ authUser, toggle, load, reset }) {
127135
</li>
128136
: null}
129137
<li>
130-
<a onPointerUp={() => toggle('profile')}>
138+
<a onPointerUp={() => toggleDialog('profile')}> {/* Use toggleDialog('profile') */}
131139
<span className="material-icons notranslate">manage_accounts</span>
132140
Edit Profile
133141
</a>

src/Dialogues/Profile.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// Import FirebaseAuth and firebase.
2-
import { useState, useCallback, ChangeEvent, useEffect } from 'react';
2+
import React, { useState, useCallback, ChangeEvent, useEffect, useContext } from 'react'; // Added React and useContext
33
import firebase from 'firebase/compat/app';
44
import 'firebase/compat/auth';
5+
import DialogContext, { DialogContextType } from './DialogContext'; // Added DialogContext
56
import 'firebase/compat/database';
67
import Avatar from '../Avatar';
78
import type { UserData } from '../Types';
@@ -110,7 +111,14 @@ export const LANGUAGES = ["af", "af-NA", "af-ZA", "agq", "agq-CM", "ak", "ak-GH"
110111
"zh-Hant-TW", "zu", "zu-ZA"];
111112

112113

113-
export default function Profile({ authUser, toggle }) {
114+
export default function Profile({ authUser }) { // Removed toggle from props
115+
const context = useContext(DialogContext);
116+
if (!context) {
117+
console.error('DialogContext not found in Profile component');
118+
return null;
119+
}
120+
const { toggleDialog } = context; // Destructure toggleDialog from context
121+
114122
const [editing, setEditing] = useState<UserData>(authUser?.val() || { uid: '', name: '', language: '', photoURL: '' });
115123
const [currentNotificationPermission, setCurrentNotificationPermission] = useState(Notification.permission);
116124

@@ -120,8 +128,8 @@ export default function Profile({ authUser, toggle }) {
120128
const userRef = firebase.database().ref(`users/${authUser!.key}`);
121129
userRef.set(editing);
122130
console.log('Saved', editing);
123-
toggle('friends')
124-
}, [editing, authUser]);
131+
toggleDialog('friends'); // Use toggleDialog
132+
}, [editing, authUser, toggleDialog]); // Added toggleDialog to dependencies
125133

126134
const generateOnChange = (key: string) => (event: ChangeEvent<HTMLInputElement>) => {
127135
setEditing(editing => ({ ...editing, [key]: event.target.value }));
@@ -140,7 +148,7 @@ export default function Profile({ authUser, toggle }) {
140148
<form onSubmit={save}>
141149
<header>
142150
<h1>
143-
<a onPointerUp={() => toggle('friends')}>
151+
<a onPointerUp={() => toggleDialog('friends')}> {/* Use toggleDialog */}
144152
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="icon icon-tabler icons-tabler-outline icon-tabler-x"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M18 6l-12 12" /><path d="M6 6l12 12" /></svg>
145153
</a>
146154
Edit Profile

src/Dialogues/index.tsx

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import React, { useContext, useRef, useEffect } from 'react';
2+
import DialogContext, { DialogContextType } from './DialogContext';
3+
import Friends from './Friends';
4+
import Chat from './Chat';
5+
import Profile from './Profile';
6+
import Login from './Login';
7+
import type { UserData, SnapshotOrNullType } from '../Types'; // Assuming these types are needed from App.tsx
8+
9+
// Props that DialogContainer will receive from App.tsx
10+
// These are the props that were originally passed to the individual dialog components from App.tsx
11+
interface DialogContainerProps {
12+
user: SnapshotOrNullType; // From App's state
13+
friendData: UserData | null | undefined; // From App's state (derived from `friend` snapshot)
14+
load: (friendId?: string, authUser?: string) => void; // From App's methods
15+
reset: () => void; // From App's methods
16+
chats: SnapshotOrNullType; // From App's state
17+
// Add any other props that individual dialogs might need from App.tsx
18+
}
19+
20+
const DialogContainer: React.FC<DialogContainerProps> = ({
21+
user,
22+
friendData,
23+
load,
24+
reset,
25+
chats,
26+
}) => {
27+
const context = useContext(DialogContext);
28+
29+
if (!context) {
30+
// This should not happen if the provider is set up correctly in App.tsx
31+
console.error('DialogContext not found. Make sure DialogProvider is wrapping this component.');
32+
return null;
33+
}
34+
35+
const { dialogState, toggleDialog } = context;
36+
const dialogRef = useRef<HTMLDialogElement>(null);
37+
38+
// Determine if the dialog should be open
39+
// This combines the logic from the original <dialog open={(friendData&&!user)||!!state}>
40+
const isOpen = (friendData && !user) || !!dialogState;
41+
42+
// Effect for managing "click outside to close"
43+
useEffect(() => {
44+
const handleClickOutside = (event: MouseEvent) => {
45+
// If the dialog is shown and the click is outside its content
46+
if (dialogRef.current && !dialogRef.current.contains(event.target as Node)) {
47+
toggleDialog(false);
48+
}
49+
};
50+
51+
if (isOpen) {
52+
document.addEventListener('mousedown', handleClickOutside);
53+
}
54+
55+
return () => {
56+
document.removeEventListener('mousedown', handleClickOutside);
57+
};
58+
}, [isOpen, toggleDialog]); // Dependencies for the click outside effect
59+
60+
// Effect for calling showModal and close on the dialog element
61+
useEffect(() => {
62+
if (dialogRef.current) {
63+
if (isOpen) {
64+
if (!dialogRef.current.open) {
65+
dialogRef.current.showModal();
66+
}
67+
} else {
68+
if (dialogRef.current.open) {
69+
dialogRef.current.close();
70+
}
71+
}
72+
}
73+
}, [isOpen]);
74+
75+
if (!isOpen) {
76+
return null; // Don't render the dialog if it shouldn't be open
77+
}
78+
79+
return (
80+
<dialog ref={dialogRef} onCancel={() => toggleDialog(false)}>
81+
{user ? (
82+
dialogState === 'friends' ? (
83+
<Friends authUser={user} load={load} reset={reset} />
84+
) : dialogState === 'profile' ? (
85+
<Profile authUser={user} />
86+
) : dialogState === 'chat' ? (
87+
<Chat chats={chats} user={user} />
88+
) : null
89+
) : (
90+
<Login reset={reset} friend={friendData} load={load} />
91+
)}
92+
</dialog>
93+
);
94+
};
95+
96+
export default DialogContainer;

src/index.tsx

Lines changed: 43 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ import ReactDOM from 'react-dom/client'
44
// import { initializeApp } from 'firebase/app';
55
import type { Match, Move, GameType, SnapshotOrNullType, UserData, ModalState } from "./Types";
66
import Avatar from "./Avatar";
7-
import Friends from "./Dialogues/Friends";
8-
import Chat from "./Dialogues/Chat";
9-
import Profile from "./Dialogues/Profile";
10-
import Login from "./Dialogues/Login";
7+
// Dialog components are now rendered by DialogContainer
8+
// import Friends from "./Dialogues/Friends";
9+
// import Chat from "./Dialogues/Chat";
10+
// import Profile from "./Dialogues/Profile";
11+
// import Login from "./Dialogues/Login";
12+
import DialogContext from './Dialogues/DialogContext';
13+
import DialogContainer from './Dialogues';
1114
import Dice from './Board/Dice';
1215
import Point from './Board/Point';
1316
import Piece from './Board/Piece';
@@ -30,33 +33,32 @@ export function App() {
3033
const [game, setGame] = useState<GameType>(newGame);
3134
const [user, setUser] = useState<SnapshotOrNullType>(null);
3235
const [hasAttemptedNotificationPermission, setHasAttemptedNotificationPermission] = useState(false);
33-
const [state, setState] = useState<ModalState>(false);
34-
const [lastState, setLastState] = useState<ModalState>('friends');
36+
const [dialogState, setDialogState] = useState<ModalState>(false); // Renamed state to dialogState
37+
const [lastDialogState, setLastDialogState] = useState<ModalState>('friends'); // Renamed lastState to lastDialogState
3538
const [match, setMatch] = useState<Match | null>(null);
3639
const [chats, setChats] = useState<SnapshotOrNullType>(null);
3740
const [friend, setFriend] = useState<SnapshotOrNullType>(null);
3841
const [selected, setSelected] = useState<number | null>(null);
3942

40-
const toggle = useCallback((newState: ModalState) => {
41-
setState(prevState => {
42-
if (typeof newState === 'string') { // Open
43-
if (prevState) setLastState(prevState);
44-
return newState
45-
} else if (newState == true) { // Back
46-
setLastState(lastState => {
47-
newState = lastState;
48-
return prevState;
49-
});
43+
const toggleDialog = useCallback((newState: ModalState) => { // Renamed toggle to toggleDialog
44+
setDialogState(prevState => {
45+
if (typeof newState === 'string') { // Open specific dialog
46+
if (prevState) setLastDialogState(prevState);
5047
return newState;
51-
} else if (newState === false) { // Close
52-
if (prevState) setLastState(prevState);
48+
} else if (newState === true) { // Back button: Go to lastDialogState
49+
const actualNewState = lastDialogState;
50+
setLastDialogState(prevState || false); // Save current state (or false if none) as the new last state
51+
return actualNewState;
52+
} else if (newState === false) { // Close dialog
53+
if (prevState) setLastDialogState(prevState);
5354
return false;
54-
} else { // Toggle
55-
setLastState(prevState);
56-
return prevState === 'friends' ? false : 'friends';
55+
} else { // Toggle friends or close (original generic toggle)
56+
const nextState = prevState === 'friends' ? false : 'friends';
57+
if (prevState) setLastDialogState(prevState);
58+
return nextState;
5759
}
5860
});
59-
}, []);
61+
}, [lastDialogState]); // Added lastDialogState to dependency array
6062

6163
const load = useCallback(async (friendId: string = '', authUser?: string) => {
6264
if (friendId === 'PeaceInTheMiddleEast') return;
@@ -99,19 +101,19 @@ export function App() {
99101
database.ref(`matches/${friendId}/${authUser}`).set(data);
100102
setMatch(data);
101103
}
102-
toggle(false)
103-
}, [toggle]);
104+
toggleDialog(false) // Use renamed function
105+
}, [toggleDialog, database]); // Added database to dependency array as it's used inside
104106

105107
const reset = useCallback(() => {
106108
if (confirm('Are you sure you want to reset the match?')) {
107109
console.log('Resetting', match?.game);
108110
let data = newGame()
109111
if (match?.game)
110-
database.ref(`games/${match?.game}`).set(data);
112+
database.ref(`games/${match?.game}`).set(data); // database is used here
111113
setGame(data);
112-
toggle(false)
114+
toggleDialog(false) // Use renamed function
113115
}
114-
}, [match?.game, toggle])
116+
}, [match?.game, toggleDialog, database]); // Added database to dependency array
115117

116118

117119
useEffect(() => {
@@ -152,7 +154,7 @@ export function App() {
152154

153155
useEffect(() => {
154156
const requestPermission = async () => {
155-
if (state === 'friends' && user && Notification.permission === 'default' && !hasAttemptedNotificationPermission) {
157+
if (dialogState === 'friends' && user && Notification.permission === 'default' && !hasAttemptedNotificationPermission) { // Use renamed state
156158
console.log("Friends modal opened, attempting notification permission request via useEffect..."); // Dev log
157159
try {
158160
await saveMessagingDeviceToken();
@@ -165,7 +167,7 @@ export function App() {
165167
};
166168

167169
requestPermission();
168-
}, [state, user, hasAttemptedNotificationPermission]);
170+
}, [dialogState, user, hasAttemptedNotificationPermission]); // Use renamed state
169171

170172
// Subscribe to match
171173
useEffect(() => {
@@ -270,24 +272,21 @@ export function App() {
270272
const friendData = friend?.val();
271273

272274
return (
273-
<>
274-
<dialog open={(friendData&&!user)||!!state}>
275-
{user
276-
? state === 'friends'
277-
? <Friends authUser={user} load={load} toggle={toggle} reset={reset} />
278-
: state === 'profile'
279-
? <Profile authUser={user} toggle={toggle} />
280-
: state === 'chat'
281-
? <Chat chats={chats} user={user} />
282-
: null
283-
: <Login reset={reset} friend={friendData} load={load} />}
284-
</dialog>
275+
<DialogContext.Provider value={{ dialogState, toggleDialog, lastDialogState }}>
276+
{/* Old dialog element removed */}
277+
<DialogContainer
278+
user={user}
279+
friendData={friendData}
280+
load={load}
281+
reset={reset}
282+
chats={chats}
283+
/>
285284

286285
<div id="board">
287-
<div id="toolbar" onPointerUp={toggle}>
286+
<div id="toolbar" onPointerUp={() => toggleDialog(null)}>
288287
{friendData
289288
? <Avatar user={friendData} />
290-
: <a className={`material-icons notranslate ${state && 'active' || ''}`}>account_circle</a>}
289+
: <a className={`material-icons notranslate ${dialogState && 'active' || ''}`}>account_circle</a>} {/* Use renamed state */}
291290
<h2>{friendData?.name ?? 'Local'}</h2>
292291
</div>
293292

@@ -317,6 +316,6 @@ export function App() {
317316
<Point key={index} pieces={pieces} move={move} position={index} selected={selected} onSelect={onSelect} />
318317
)}
319318
</div >
320-
</>
319+
</DialogContext.Provider>
321320
);
322321
}

0 commit comments

Comments
 (0)