Skip to content

Commit 4a1d4eb

Browse files
committed
fix: custom dropdown usage
1 parent 7947cac commit 4a1d4eb

File tree

8 files changed

+198
-163
lines changed

8 files changed

+198
-163
lines changed

src/components/CustomDropdown.js

Lines changed: 67 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
import React, { useState, useRef, useEffect } from 'react';
2+
import ReactDOM from 'react-dom'; // Import ReactDOM
23
import { CaretDown, Check } from '@phosphor-icons/react';
34
import { motion, AnimatePresence } from 'framer-motion';
45

56
const CustomDropdown = ({ options, value, onChange, icon: Icon, label }) => {
67
const [isOpen, setIsOpen] = useState(false);
7-
const dropdownRef = useRef(null);
8+
const dropdownRef = useRef(null); // Ref for the button
9+
const menuRef = useRef(null); // Ref for the dropdown menu
10+
const [dropdownMenuPosition, setDropdownMenuPosition] = useState({});
811

912
useEffect(() => {
1013
const handleClickOutside = (event) => {
11-
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
14+
const isClickInsideButton =
15+
dropdownRef.current && dropdownRef.current.contains(event.target);
16+
const isClickInsideMenu =
17+
menuRef.current && menuRef.current.contains(event.target);
18+
19+
if (!isClickInsideButton && !isClickInsideMenu) {
1220
setIsOpen(false);
1321
}
1422
};
@@ -19,17 +27,72 @@ const CustomDropdown = ({ options, value, onChange, icon: Icon, label }) => {
1927
};
2028
}, []);
2129

30+
useEffect(() => {
31+
if (isOpen && dropdownRef.current) {
32+
const rect = dropdownRef.current.getBoundingClientRect();
33+
setDropdownMenuPosition({
34+
top: rect.bottom + window.scrollY + 8, // 8px for mt-2
35+
left: rect.left + window.scrollX,
36+
width: rect.width,
37+
});
38+
}
39+
}, [isOpen]);
40+
2241
const handleSelect = (optionValue) => {
2342
onChange(optionValue);
2443
setIsOpen(false);
2544
};
2645

2746
const selectedOption = options.find((opt) => opt.value === value);
2847

48+
// Render the dropdown menu (options list) using a Portal
49+
const renderDropdownMenu = () => {
50+
if (!isOpen) return null;
51+
52+
return ReactDOM.createPortal(
53+
<motion.div
54+
ref={menuRef}
55+
initial={{ opacity: 0, y: -10, scale: 0.95 }}
56+
animate={{ opacity: 1, y: 0, scale: 1 }}
57+
exit={{ opacity: 0, y: -10, scale: 0.95 }}
58+
transition={{ duration: 0.1 }}
59+
className="bg-gray-800 border border-gray-700 rounded-md shadow-lg z-50 origin-top-left max-h-80 overflow-y-auto" // Added max-h-80 and overflow-y-auto
60+
style={{
61+
position: 'absolute',
62+
top: dropdownMenuPosition.top,
63+
left: dropdownMenuPosition.left,
64+
minWidth: dropdownMenuPosition.width, // Set minWidth to button width
65+
width: 'max-content', // Allow content to determine width, but respect minWidth
66+
}}
67+
>
68+
<div className="py-1">
69+
{options.map((option) => (
70+
<button
71+
key={option.value}
72+
onClick={() => handleSelect(option.value)}
73+
className={`flex items-center justify-between w-full px-4 py-2 text-sm text-left transition-colors ${
74+
value === option.value
75+
? 'bg-primary-500/10 text-primary-400'
76+
: 'text-gray-300 hover:bg-gray-700 hover:text-white'
77+
}`}
78+
>
79+
<span>{option.label}</span>
80+
{value === option.value && (
81+
<Check size={16} className="text-primary-400" />
82+
)}
83+
</button>
84+
))}
85+
</div>
86+
</motion.div>,
87+
document.body,
88+
);
89+
};
90+
2991
return (
30-
<div className="relative inline-block text-left" ref={dropdownRef}>
92+
<div className="relative inline-block text-left">
3193
<button
3294
type="button"
95+
ref={dropdownRef} // Attach ref to the button
3396
onClick={() => setIsOpen(!isOpen)}
3497
className="flex items-center gap-2 px-4 py-2 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded-md text-sm font-medium text-gray-200 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-primary-500"
3598
>
@@ -41,36 +104,7 @@ const CustomDropdown = ({ options, value, onChange, icon: Icon, label }) => {
41104
/>
42105
</button>
43106

44-
<AnimatePresence>
45-
{isOpen && (
46-
<motion.div
47-
initial={{ opacity: 0, y: -10, scale: 0.95 }}
48-
animate={{ opacity: 1, y: 0, scale: 1 }}
49-
exit={{ opacity: 0, y: -10, scale: 0.95 }}
50-
transition={{ duration: 0.1 }}
51-
className="absolute right-0 mt-2 w-48 bg-gray-800 border border-gray-700 rounded-md shadow-lg z-50 origin-top-right"
52-
>
53-
<div className="py-1">
54-
{options.map((option) => (
55-
<button
56-
key={option.value}
57-
onClick={() => handleSelect(option.value)}
58-
className={`flex items-center justify-between w-full px-4 py-2 text-sm text-left transition-colors ${
59-
value === option.value
60-
? 'bg-primary-500/10 text-primary-400'
61-
: 'text-gray-300 hover:bg-gray-700 hover:text-white'
62-
}`}
63-
>
64-
<span>{option.label}</span>
65-
{value === option.value && (
66-
<Check size={16} className="text-primary-400" />
67-
)}
68-
</button>
69-
))}
70-
</div>
71-
</motion.div>
72-
)}
73-
</AnimatePresence>
107+
{renderDropdownMenu()}
74108
</div>
75109
);
76110
};

src/pages/apps/CronJobGeneratorPage.js

Lines changed: 49 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { useState, useCallback, useEffect } from 'react';
22
import { Link } from 'react-router-dom';
33
import { ArrowLeftIcon, CopySimple } from '@phosphor-icons/react';
44
import useSeo from '../../hooks/useSeo';
5+
import CustomDropdown from '../../components/CustomDropdown';
56

67
const CronJobGeneratorPage = () => {
78
useSeo({
@@ -107,16 +108,12 @@ const CronJobGeneratorPage = () => {
107108
});
108109
};
109110

110-
const renderOptions = (start, end) => {
111-
const options = ['*'];
111+
const getNumericOptions = (start, end) => {
112+
const options = [{ label: '*', value: '*' }];
112113
for (let i = start; i <= end; i++) {
113-
options.push(i.toString());
114+
options.push({ label: i.toString(), value: i.toString() });
114115
}
115-
return options.map((opt) => (
116-
<option key={opt} value={opt}>
117-
{opt}
118-
</option>
119-
));
116+
return options;
120117
};
121118

122119
const monthNames = [
@@ -136,6 +133,20 @@ const CronJobGeneratorPage = () => {
136133
];
137134
const dayOfWeekNames = ['*', 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
138135

136+
const getMonthOptions = () => {
137+
return monthNames.map((name, index) => ({
138+
label: name,
139+
value: index === 0 ? '*' : index.toString(),
140+
}));
141+
};
142+
143+
const getDayOfWeekOptions = () => {
144+
return dayOfWeekNames.map((name, index) => ({
145+
label: name,
146+
value: index === 0 ? '*' : index.toString(),
147+
}));
148+
};
149+
139150
const inputStyle = `mt-1 block w-full p-2 border rounded-md bg-gray-700 text-white focus:ring-blue-500 focus:border-blue-500 border-gray-600`;
140151
const selectStyle = `mt-1 block w-full p-2 border border-gray-600 rounded-md bg-gray-700 text-white focus:ring-blue-500 focus:border-blue-500`;
141152
const buttonStyle = `px-6 py-2 rounded-md text-lg font-arvo font-normal transition-colors duration-300 ease-in-out border bg-tb text-app border-app-alpha-50 hover:bg-app/15`;
@@ -188,72 +199,59 @@ const CronJobGeneratorPage = () => {
188199
</h2>
189200
<div className="grid grid-cols-1 md:grid-cols-5 gap-4 mb-4">
190201
<div>
191-
<label className="block text-sm font-medium text-gray-300">
202+
<label className="block text-sm font-medium text-gray-300 mb-1">
192203
Minute
193204
</label>
194-
<select
205+
<CustomDropdown
206+
options={getNumericOptions(0, 59)}
195207
value={minute}
196-
onChange={(e) => setMinute(e.target.value)}
197-
className={selectStyle}
198-
>
199-
{renderOptions(0, 59)}
200-
</select>
208+
onChange={setMinute}
209+
label="Minute"
210+
/>
201211
</div>
202212
<div>
203-
<label className="block text-sm font-medium text-gray-300">
213+
<label className="block text-sm font-medium text-gray-300 mb-1">
204214
Hour
205215
</label>
206-
<select
216+
<CustomDropdown
217+
options={getNumericOptions(0, 23)}
207218
value={hour}
208-
onChange={(e) => setHour(e.target.value)}
209-
className={selectStyle}
210-
>
211-
{renderOptions(0, 23)}
212-
</select>
219+
onChange={setHour}
220+
label="Hour"
221+
/>
213222
</div>
214223
<div>
215-
<label className="block text-sm font-medium text-gray-300">
224+
<label className="block text-sm font-medium text-gray-300 mb-1">
216225
Day of Month
217226
</label>
218-
<select
227+
<CustomDropdown
228+
options={getNumericOptions(1, 31)}
219229
value={dayOfMonth}
220-
onChange={(e) => setDayOfMonth(e.target.value)}
221-
className={selectStyle}
222-
>
223-
{renderOptions(1, 31)}
224-
</select>
230+
onChange={setDayOfMonth}
231+
label="Day"
232+
/>
225233
</div>
226234
<div>
227-
<label className="block text-sm font-medium text-gray-300">
235+
<label className="block text-sm font-medium text-gray-300 mb-1">
228236
Month
229237
</label>
230-
<select
238+
<CustomDropdown
239+
options={getMonthOptions()}
231240
value={month}
232-
onChange={(e) => setMonth(e.target.value)}
233-
className={selectStyle}
234-
>
235-
{monthNames.map((name, index) => (
236-
<option key={name} value={index === 0 ? '*' : index}>
237-
{name}
238-
</option>
239-
))}
240-
</select>
241+
onChange={setMonth}
242+
label="Month"
243+
/>
241244
</div>
242245
<div>
243-
<label className="block text-sm font-medium text-gray-300">
246+
<label className="block text-sm font-medium text-gray-300 mb-1">
244247
Day of Week
245248
</label>
246-
<select
249+
<CustomDropdown
250+
options={getDayOfWeekOptions()}
247251
value={dayOfWeek}
248-
onChange={(e) => setDayOfWeek(e.target.value)}
249-
className={selectStyle}
250-
>
251-
{dayOfWeekNames.map((name, index) => (
252-
<option key={name} value={index === 0 ? '*' : index}>
253-
{name}
254-
</option>
255-
))}
256-
</select>
252+
onChange={setDayOfWeek}
253+
label="Weekday"
254+
/>
257255
</div>
258256
</div>
259257
<div className="flex items-center justify-between bg-gray-800 p-3 rounded-md">

src/pages/apps/CssUnitConverterPage.js

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
33
import { ArrowLeftIcon } from '@phosphor-icons/react';
44
import colors from '../../config/colors';
55
import useSeo from '../../hooks/useSeo';
6+
import CustomDropdown from '../../components/CustomDropdown';
67

78
const CssUnitConverterPage = () => {
89
useSeo({
@@ -148,19 +149,21 @@ const CssUnitConverterPage = () => {
148149
>
149150
Unit
150151
</label>
151-
<select
152-
id="inputUnit"
153-
className="mt-1 block w-full p-2 border border-gray-600 rounded-md bg-gray-700 text-white focus:ring-blue-500 focus:border-blue-500"
154-
value={inputUnit}
155-
onChange={(e) => setInputUnit(e.target.value)}
156-
>
157-
<option value="px">px</option>
158-
<option value="em">em</option>
159-
<option value="rem">rem</option>
160-
<option value="vw">vw</option>
161-
<option value="vh">vh</option>
162-
<option value="percent">%</option>
163-
</select>
152+
<div className="mt-1">
153+
<CustomDropdown
154+
options={[
155+
{ label: 'px', value: 'px' },
156+
{ label: 'em', value: 'em' },
157+
{ label: 'rem', value: 'rem' },
158+
{ label: 'vw', value: 'vw' },
159+
{ label: 'vh', value: 'vh' },
160+
{ label: '%', value: 'percent' },
161+
]}
162+
value={inputUnit}
163+
onChange={setInputUnit}
164+
label="Unit"
165+
/>
166+
</div>
164167
</div>
165168
<div>
166169
<label

src/pages/apps/DiceRollerPage.js

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useToast } from '../../hooks/useToast';
66
import Dice from '../../components/Dice';
77
import '../../styles/DiceRollerPage.css'; // Import the CSS for animations
88
import useSeo from '../../hooks/useSeo';
9+
import CustomDropdown from '../../components/CustomDropdown';
910

1011
const DiceRollerPage = () => {
1112
useSeo({
@@ -123,20 +124,20 @@ const DiceRollerPage = () => {
123124
>
124125
Dice Type (dX)
125126
</label>
126-
<select
127-
id="diceType"
128-
className="mt-1 block w-full p-2 border border-gray-600 rounded-md bg-gray-700 text-white focus:ring-blue-500 focus:border-blue-500"
127+
<CustomDropdown
128+
options={[
129+
{ label: 'd4', value: 4 },
130+
{ label: 'd6', value: 6 },
131+
{ label: 'd8', value: 8 },
132+
{ label: 'd10', value: 10 },
133+
{ label: 'd12', value: 12 },
134+
{ label: 'd20', value: 20 },
135+
{ label: 'd100', value: 100 },
136+
]}
129137
value={diceType}
130-
onChange={(e) => setDiceType(Number(e.target.value))}
131-
>
132-
<option value={4}>d4</option>
133-
<option value={6}>d6</option>
134-
<option value={8}>d8</option>
135-
<option value={10}>d10</option>
136-
<option value={12}>d12</option>
137-
<option value={20}>d20</option>
138-
<option value={100}>d100</option>
139-
</select>
138+
onChange={setDiceType}
139+
label="Dice Type"
140+
/>
140141
</div>
141142
<div className="flex-1">
142143
<label

0 commit comments

Comments
 (0)