Skip to content

Commit 671de51

Browse files
committed
fix: I've implemented a React Portal to render the captcha modal outside the normal DOM hierarchy
1 parent 6f61e78 commit 671de51

1 file changed

Lines changed: 84 additions & 80 deletions

File tree

src/HideMe.tsx

Lines changed: 84 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
"use client";
1+
'use client';
22

3-
import React, { useState, useCallback, useMemo } from 'react';
43
import classNames from 'classnames';
4+
import React, { useCallback, useMemo, useState } from 'react';
5+
import { createPortal } from 'react-dom';
6+
import './styles.css';
57
import { HideMeProps } from './types';
68
import { generateMathProblem } from './utils/captcha';
7-
import './styles.css';
89

910
export const HideMe: React.FC<HideMeProps> = ({
1011
children,
@@ -43,7 +44,7 @@ export const HideMe: React.FC<HideMeProps> = ({
4344
setShowCaptcha(false);
4445
} else {
4546
// Wrong answer, generate new problem
46-
setProblemKey(prev => prev + 1);
47+
setProblemKey((prev) => prev + 1);
4748
setUserAnswer('');
4849
}
4950
}, [mathProblem, userAnswer]);
@@ -53,13 +54,16 @@ export const HideMe: React.FC<HideMeProps> = ({
5354
setUserAnswer('');
5455
}, []);
5556

56-
const handleKeyDown = useCallback((event: React.KeyboardEvent) => {
57-
if (event.key === 'Enter' || event.key === ' ') {
58-
event.preventDefault();
59-
if (isRevealed) return;
60-
handleReveal();
61-
}
62-
}, [isRevealed, handleReveal]);
57+
const handleKeyDown = useCallback(
58+
(event: React.KeyboardEvent) => {
59+
if (event.key === 'Enter' || event.key === ' ') {
60+
event.preventDefault();
61+
if (isRevealed) return;
62+
handleReveal();
63+
}
64+
},
65+
[isRevealed, handleReveal]
66+
);
6367

6468
// Don't render anything if no children
6569
if (!children) {
@@ -69,66 +73,65 @@ export const HideMe: React.FC<HideMeProps> = ({
6973
// If revealed, just show the content
7074
if (isRevealed) {
7175
return (
72-
<span
73-
className={classNames('hide-me', 'hide-me--revealed', className)}
74-
style={style}
75-
>
76+
<span className={classNames('hide-me', 'hide-me--revealed', className)} style={style}>
7677
{children}
7778
</span>
7879
);
7980
}
8081

81-
// Show CAPTCHA modal
82-
if (showCaptcha && mathProblem) {
83-
return (
84-
<div className="hide-me-captcha-overlay">
85-
<div className="hide-me-captcha-modal">
86-
<div className="hide-me-captcha-content">
87-
<div className="hide-me-captcha-title">Solve this math problem to reveal content:</div>
88-
<div className="hide-me-captcha-question">
89-
What is {mathProblem.question}?
90-
</div>
91-
<div className="hide-me-captcha-input-group">
92-
<input
93-
type="number"
94-
value={userAnswer}
95-
onChange={(e) => setUserAnswer(e.target.value)}
96-
placeholder="Your answer"
97-
className="hide-me-captcha-input"
98-
autoFocus
99-
onKeyDown={(e) => {
100-
if (e.key === 'Enter') {
101-
handleCaptchaSubmit();
102-
}
103-
}}
104-
/>
105-
<div className="hide-me-captcha-buttons">
106-
<button
107-
type="button"
108-
onClick={handleCaptchaSubmit}
109-
className="hide-me-captcha-submit"
110-
>
111-
Submit
112-
</button>
113-
<button
114-
type="button"
115-
onClick={handleCaptchaCancel}
116-
className="hide-me-captcha-cancel"
117-
>
118-
Cancel
119-
</button>
82+
// Render CAPTCHA modal using portal (to avoid nesting issues with <p> tags)
83+
const captchaModal =
84+
showCaptcha && mathProblem && typeof document !== 'undefined'
85+
? createPortal(
86+
<div className="hide-me-captcha-overlay">
87+
<div className="hide-me-captcha-modal">
88+
<div className="hide-me-captcha-content">
89+
<div className="hide-me-captcha-title">
90+
Solve this math problem to reveal content:
91+
</div>
92+
<div className="hide-me-captcha-question">What is {mathProblem.question}?</div>
93+
<div className="hide-me-captcha-input-group">
94+
<input
95+
type="number"
96+
value={userAnswer}
97+
onChange={(e) => setUserAnswer(e.target.value)}
98+
placeholder="Your answer"
99+
className="hide-me-captcha-input"
100+
autoFocus
101+
onKeyDown={(e) => {
102+
if (e.key === 'Enter') {
103+
handleCaptchaSubmit();
104+
}
105+
}}
106+
/>
107+
<div className="hide-me-captcha-buttons">
108+
<button
109+
type="button"
110+
onClick={handleCaptchaSubmit}
111+
className="hide-me-captcha-submit"
112+
>
113+
Submit
114+
</button>
115+
<button
116+
type="button"
117+
onClick={handleCaptchaCancel}
118+
className="hide-me-captcha-cancel"
119+
>
120+
Cancel
121+
</button>
122+
</div>
123+
</div>
120124
</div>
121125
</div>
122-
</div>
123-
</div>
124-
</div>
125-
);
126-
}
126+
</div>,
127+
document.body
128+
)
129+
: null;
127130

128131
// Build CSS classes using classNames
129132
const classes = classNames(
130-
'hide-me',
131-
blackOut ? 'hide-me--blackout' : (mode === 'blur' ? 'hide-me--blur' : 'hide-me--captcha'),
133+
'hide-me',
134+
blackOut ? 'hide-me--blackout' : mode === 'blur' ? 'hide-me--blur' : 'hide-me--captcha',
132135
className
133136
);
134137

@@ -138,27 +141,28 @@ export const HideMe: React.FC<HideMeProps> = ({
138141
'--hide-me-blur-amount': `${blurAmount}px`,
139142
};
140143

141-
const ariaLabel = blackOut
144+
const ariaLabel = blackOut
142145
? 'Blacked out content. Click to reveal.'
143-
: mode === 'captcha'
144-
? 'Hidden content. Click to solve CAPTCHA and reveal.'
145-
: 'Hidden content. Click to reveal.';
146+
: mode === 'captcha'
147+
? 'Hidden content. Click to solve CAPTCHA and reveal.'
148+
: 'Hidden content. Click to reveal.';
146149

147150
return (
148-
<button
149-
type="button"
150-
className={classes}
151-
style={inlineStyles}
152-
onClick={handleReveal}
153-
onKeyDown={handleKeyDown}
154-
aria-label={ariaLabel}
155-
aria-expanded={isRevealed}
156-
tabIndex={0}
157-
>
158-
<span className="sr-only">
159-
{ariaLabel}
160-
</span>
161-
{blackOut ? '████████' : (mode === 'captcha' ? '••••••••' : children)}
162-
</button>
151+
<>
152+
<button
153+
type="button"
154+
className={classes}
155+
style={inlineStyles}
156+
onClick={handleReveal}
157+
onKeyDown={handleKeyDown}
158+
aria-label={ariaLabel}
159+
aria-expanded={isRevealed}
160+
tabIndex={0}
161+
>
162+
<span className="sr-only">{ariaLabel}</span>
163+
{blackOut ? '████████' : mode === 'captcha' ? '••••••••' : children}
164+
</button>
165+
{captchaModal}
166+
</>
163167
);
164168
};

0 commit comments

Comments
 (0)