Skip to content

Commit a801566

Browse files
committed
Added custom selector
1 parent 32d5c1f commit a801566

File tree

5 files changed

+218
-46
lines changed

5 files changed

+218
-46
lines changed

src/components/LanguageSelector.tsx

+75-20
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,39 @@
1+
import React, { useState, useRef } from "react";
12
import { useAppContext } from "../contexts/AppContext";
23
import { useLanguages } from "../hooks/useLanguages";
4+
import { LanguageType } from "../types";
5+
6+
// Inspired by https://blog.logrocket.com/creating-custom-select-dropdown-css/
37

48
const LanguageSelector = () => {
5-
const { language, setLanguage, setCategory } = useAppContext();
9+
const { language, setLanguage } = useAppContext();
610
const { fetchedLanguages, loading, error } = useLanguages();
711

8-
const handleLanguageChange = (
9-
event: React.ChangeEvent<HTMLSelectElement>
10-
) => {
11-
const selectedLanguage = fetchedLanguages.find(
12-
(language) => language.lang === event.target.value
12+
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
13+
const [selectedLanguage, setSelectedLanguage] =
14+
useState<LanguageType>(language);
15+
const dropdownRef = useRef<HTMLDivElement>(null);
16+
17+
const handleLanguageChange = (langObj: LanguageType) => {
18+
const selected = fetchedLanguages.find(
19+
(item) => item.lang === langObj.lang
1320
);
14-
if (selectedLanguage) {
15-
setLanguage(selectedLanguage);
16-
setCategory("");
21+
if (selected) {
22+
setSelectedLanguage(selected);
23+
setLanguage(selected);
24+
setIsDropdownOpen(false);
25+
}
26+
};
27+
28+
const toggleDropdown = () => {
29+
setIsDropdownOpen((prev) => !prev);
30+
};
31+
32+
const handleKeyDown = (event: React.KeyboardEvent, lang: LanguageType) => {
33+
if (event.key === "Enter") {
34+
handleLanguageChange(lang);
35+
} else if (event.key === "Escape") {
36+
setIsDropdownOpen(false);
1737
}
1838
};
1939

@@ -26,18 +46,53 @@ const LanguageSelector = () => {
2646
}
2747

2848
return (
29-
<select
30-
id="languages"
31-
className="language-selector"
32-
onChange={handleLanguageChange}
33-
value={language?.lang || "CSS"}
49+
<div
50+
className={`selector ${isDropdownOpen ? "selector--open" : ""}`}
51+
ref={dropdownRef}
3452
>
35-
{fetchedLanguages.map((language, idx) => (
36-
<option key={idx} value={language.lang}>
37-
{language.lang}
38-
</option>
39-
))}
40-
</select>
53+
<button
54+
className="selector__button"
55+
aria-label="select button"
56+
aria-haspopup="listbox"
57+
aria-expanded={isDropdownOpen}
58+
onClick={toggleDropdown}
59+
>
60+
<div className="selector__value">
61+
<img src={selectedLanguage.icon} alt="" />
62+
<span>{selectedLanguage.lang || "Select a language"}</span>
63+
</div>
64+
<span className="selector__arrow"></span>
65+
</button>
66+
{isDropdownOpen && (
67+
<ul className="selector__dropdown" role="listbox">
68+
{fetchedLanguages.map((lang) => (
69+
<li
70+
key={lang.lang}
71+
role="option"
72+
tabIndex={0}
73+
onClick={() => handleLanguageChange(lang)}
74+
onKeyDown={(e) => handleKeyDown(e, lang)}
75+
className={`selector__item ${
76+
selectedLanguage.lang === lang.lang ? "selected" : ""
77+
}`}
78+
>
79+
<input
80+
type="radio"
81+
id={`selector-for-${lang.lang}`}
82+
name="language"
83+
value={lang.lang}
84+
checked={selectedLanguage === lang}
85+
readOnly
86+
/>
87+
<label htmlFor={`selector-for-${lang.lang}`}>
88+
<img src={lang.icon} alt="" />
89+
<span>{lang.lang}</span>
90+
</label>
91+
</li>
92+
))}
93+
</ul>
94+
)}
95+
</div>
4196
);
4297
};
4398

src/contexts/AppContext.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { AppState, LanguageType, SnippetType } from "../types";
33

44
// tokens
55
const defaultLanguage: LanguageType = {
6-
lang: "CSS",
7-
icon: "/icons/css.svg",
6+
lang: "JavaScript",
7+
icon: "/icons/javascript.svg",
88
};
99

1010
// TODO: add custom loading and error handling

src/layouts/Banner.tsx

+5-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ const Banner = () => {
55
Made to save your <span className="text-highlight">time.</span>
66
</h1>
77
<p>
8-
Find the necessary snippet in seconds, across multiple languages. Just
9-
search and copy!
8+
Find code snippets in seconds, across multiple languages. Just{" "}
9+
<s>
10+
<abbr title="Under construction :)">search</abbr>
11+
</s>{" "}
12+
and copy!
1013
</p>
1114
</div>
1215
);

src/layouts/Footer.tsx

+5-15
Original file line numberDiff line numberDiff line change
@@ -9,30 +9,20 @@ const Footer = () => {
99
<a href="/" className="styled-link">
1010
QuickSnip
1111
</a>{" "}
12-
is your go-to collection of handy code snippets, making repetitive
13-
tasks easier and faster for developers across different programming
14-
languages.
12+
is an open-source project that categorizes handy code snippets
13+
across various programming languages.
1514
</p>
1615
<p>
17-
Built by{" "}
18-
<a
19-
href="https://github.com/dostonnabotov"
20-
target="_blank"
21-
rel="noopener noreferrer"
22-
className="styled-link"
23-
>
24-
Technophile
25-
</a>
26-
, and powered by awesome{" "}
16+
Built with love and powered by an{" "}
2717
<a
2818
href="https://github.com/dostonnabotov/quicksnip"
2919
target="_blank"
3020
rel="noopener noreferrer"
3121
className="styled-link"
3222
>
33-
community
23+
awesome community
3424
</a>
35-
.
25+
. 🚀
3626
</p>
3727
</div>
3828
<nav className="footer__nav">

src/styles/main.css

+131-7
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,11 @@ ol:where([role="list"]) {
136136
margin: 0;
137137
}
138138

139+
abbr {
140+
text-decoration: none;
141+
cursor: help;
142+
}
143+
139144
/*------------------------------------*\
140145
#UTILS
141146
\*------------------------------------*/
@@ -195,6 +200,7 @@ ol:where([role="list"]) {
195200
/*------------------------------------*\
196201
#COMPONENTS
197202
\*------------------------------------*/
203+
/* button */
198204
.button {
199205
display: inline-flex;
200206
min-height: 3rem;
@@ -208,6 +214,11 @@ ol:where([role="list"]) {
208214
cursor: pointer;
209215
line-height: 1.1;
210216
text-decoration: none;
217+
transition: transform 200ms ease;
218+
}
219+
220+
.button:is(:hover, :focus-visible) {
221+
border-color: var(--clr-accent);
211222
}
212223

213224
.button--icon {
@@ -220,6 +231,7 @@ ol:where([role="list"]) {
220231
padding: 0.5em;
221232
}
222233

234+
/* search field */
223235
.search-field {
224236
display: inline-flex;
225237
align-items: center;
@@ -228,6 +240,10 @@ ol:where([role="list"]) {
228240
border: 1px solid var(--border-color);
229241
border-radius: var(--br-md);
230242
padding: 0.75em 1.125em;
243+
244+
&:is(:hover, :focus-within) {
245+
border-color: var(--clr-accent);
246+
}
231247
}
232248

233249
.search-field > input {
@@ -239,6 +255,112 @@ ol:where([role="list"]) {
239255
}
240256
}
241257

258+
/* custom selector */
259+
.selector {
260+
position: relative;
261+
width: 100%;
262+
}
263+
264+
.selector__button {
265+
width: 100%;
266+
font-size: var(--fs-500);
267+
font-weight: var(--fw-bold);
268+
padding: 0.5em 1em;
269+
background-color: transparent;
270+
border: 1px solid var(--border-color);
271+
border-radius: var(--br-md);
272+
cursor: pointer;
273+
274+
display: grid;
275+
grid-template-columns: 1fr auto;
276+
align-items: center;
277+
}
278+
279+
.selector__value {
280+
display: flex;
281+
gap: 0.5em;
282+
align-items: center;
283+
}
284+
285+
.selector__value img {
286+
width: 30px;
287+
}
288+
289+
.selector__arrow {
290+
border-left: 7px solid transparent;
291+
border-right: 7px solid transparent;
292+
border-top: 7px solid var(--text-primary);
293+
transition: transform 100ms ease;
294+
}
295+
296+
.selector--open .selector__arrow {
297+
transform: rotate(180deg);
298+
}
299+
300+
.selector__dropdown {
301+
display: grid;
302+
gap: 0.25rem;
303+
304+
position: absolute;
305+
width: 100%;
306+
max-height: 15rem;
307+
overflow-y: auto;
308+
309+
background-color: var(--bg-secondary);
310+
border: 1px solid var(--border-color);
311+
border-radius: var(--br-md);
312+
margin-top: 0.5rem;
313+
padding: 0.5rem;
314+
font-size: var(--fs-400);
315+
list-style: none;
316+
317+
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
318+
}
319+
320+
.selector__dropdown:focus-within {
321+
border-color: var(--clr-accent);
322+
}
323+
324+
.selector__item {
325+
position: relative;
326+
cursor: pointer;
327+
display: flex;
328+
gap: 1rem;
329+
align-items: center;
330+
border-radius: var(--br-md);
331+
}
332+
333+
.selector__item label {
334+
width: 100%;
335+
padding: 0.25em 0.75em;
336+
cursor: pointer;
337+
border-radius: var(--br-md);
338+
display: flex;
339+
gap: 1em;
340+
align-items: center;
341+
color: var(--text-primary);
342+
}
343+
344+
.selector__item label img {
345+
width: 35px;
346+
}
347+
348+
.selector__item:hover {
349+
background-image: var(--gradient-secondary);
350+
}
351+
352+
.selector__item.selected label {
353+
background-color: var(--clr-accent);
354+
color: var(--text-dark);
355+
font-weight: var(--fw-bold);
356+
}
357+
358+
.selector__item input[type="radio"] {
359+
position: absolute;
360+
left: 0;
361+
opacity: 0;
362+
}
363+
242364
.logo {
243365
display: inline-flex;
244366
gap: 0.25em;
@@ -294,18 +416,20 @@ ol:where([role="list"]) {
294416
/*------------------------------------*\
295417
#SIDEBAR
296418
\*------------------------------------*/
297-
.language-selector {
419+
/* .language-selector {
298420
background-color: transparent;
299421
cursor: pointer;
300-
border: 0;
301-
/* padding-block: 0.5em; */
422+
border: 1px solid transparent;
423+
padding-block: 0.5em;
302424
font-weight: var(--fw-bold);
303425
font-size: var(--fs-600);
304-
}
426+
border-radius: var(--br-md);
427+
appearance: none; /* Removes default arrow */
305428

306-
.language-selector option {
307-
font-size: var(--fs-400);
308-
}
429+
/* &:focus-within {
430+
border-color: var(--clr-accent);
431+
}
432+
} */
309433

310434
.categories {
311435
display: grid;

0 commit comments

Comments
 (0)