Skip to content

Commit

Permalink
Merge pull request #5 from No0ne003/select-component
Browse files Browse the repository at this point in the history
Select component
  • Loading branch information
No0ne003 authored Jun 8, 2024
2 parents 2ea1e00 + 5011452 commit 7e5bcac
Show file tree
Hide file tree
Showing 7 changed files with 206 additions and 6 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"build": "vite build",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
Expand Down
2 changes: 2 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const ScrollToSection = lazy(
() => import("./pages/Scroll-to-section/ScrollToSection"),
);
const WeatherApp = lazy(() => import("@/pages/Weather-app/index"));
const SelectComponent = lazy(() => import("@/pages/Select-Component/index"))

function App() {
const [cursorVariant, setCursorVariant] = useState("default");
Expand Down Expand Up @@ -134,6 +135,7 @@ function App() {
element={<ScrollToSection />}
/>
<Route path="weather-app" element={<WeatherApp />} />
<Route path="select-component" element={<SelectComponent />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
Expand Down
15 changes: 15 additions & 0 deletions src/data/projects.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export const projects = [
id: 1,
name: "Accordion",
path: "accordion",
tags: ['component']
},
{
id: 2,
Expand All @@ -14,11 +15,13 @@ export const projects = [
id: 3,
name: "Star Rating",
path: "star-rating",
tags: ['component']
},
{
id: 4,
name: "Image Slider",
path: "image-slider",
tags: ['component']
},
{
id: 5,
Expand All @@ -30,6 +33,7 @@ export const projects = [
id: 6,
name: "Tree View",
path: "tree-view",
tags: ['component']
},
{
id: 7,
Expand All @@ -41,16 +45,19 @@ export const projects = [
id: 8,
name: "Light Dark Mode",
path: "light-dark-mode",
tags: ['component']
},
{
id: 9,
name: "Custom Tabs",
path: "custom-tabs",
tags: ['component']
},
{
id: 10,
name: "Modal Popup",
path: "modal-popup",
tags: ['component']
},
{
id: 11,
Expand All @@ -62,6 +69,7 @@ export const projects = [
id: 12,
name: "Search Auto-Complete",
path: "search-auto-complete",
tags: ['component']
},
{
id: 13,
Expand All @@ -73,6 +81,7 @@ export const projects = [
id: 14,
name: "Feature Flags",
path: "feature-flags",
tags: ['component']
},
{
id: 15,
Expand Down Expand Up @@ -103,5 +112,11 @@ export const projects = [
name: 'Weather App',
path: 'weather-app',
tags: ['project']
},
{
id: 20,
name: 'Select component',
path: 'select-component',
tags: ['component']
}
];
156 changes: 156 additions & 0 deletions src/pages/Select-Component/components/Select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { cn } from "@/lib/utils";
import { useEffect, useRef, useState } from "react";

export type SelectOptions = {
label: string;
value: string | number;
};
type MultipleSelectProps = {
multiple: true;
onChange: (value: SelectOptions[]) => void;
value: SelectOptions[];
};
type SingleSelectProps = {
multiple?: false;
onChange: (value: SelectOptions | undefined) => void;
value?: SelectOptions;
};
type SelectProps = {
options: SelectOptions[];
} & (SingleSelectProps | MultipleSelectProps);

const Select = ({ multiple, value, onChange, options }: SelectProps) => {
const [isOpen, setIsOpen] = useState(false);
const [highlightedIndex, setHighlightedIndex] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);

function clearOptions() {
multiple ? onChange([]) : onChange(undefined);
}

function selectOption(option: SelectOptions) {
if (multiple) {
if (value.includes(option)) {
onChange(value.filter((o) => o !== option));
} else {
onChange([...value, option]);
}
} else {
if (option !== value) onChange(option);
}
}

function isOptionSelected(option: SelectOptions) {
return multiple ? value.includes(option) : option === value;
}

useEffect(() => {
if (isOpen) setHighlightedIndex(0);
}, [isOpen]);

useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.target != containerRef.current) return;
switch (e.code) {
case "Enter":
case "Space":
setIsOpen((prev) => !prev);
if (isOpen) selectOption(options[highlightedIndex]);
break;
case "ArrowUp":
case "ArrowDown": {
if (!isOpen) {
setIsOpen(true);
break;
}

const newValue = highlightedIndex + (e.code === "ArrowDown" ? 1 : -1);
if (newValue >= 0 && newValue < options.length) {
setHighlightedIndex(newValue);
}
break;
}
case "Escape":
setIsOpen(false);
break;
}
};
containerRef.current?.addEventListener("keydown", handler);

return () => {
containerRef.current?.removeEventListener("keydown", handler);
};
}, [isOpen, highlightedIndex, options]);

return (
<div
ref={containerRef}
onBlur={() => setIsOpen(false)}
onClick={() => setIsOpen((prev) => !prev)}
tabIndex={0}
className="relative w-[20em] min-h-[1.5em] border-[0.05em] border-solid border-border flex items-center gap-[0.5em] p-[0.5em] rounded-lg outline-none focus:border-primary"
>
<span className="grow flex gap-2 flex-wrap">
{multiple
? value.map((v) => (
<button
key={v.value}
onClick={(e) => {
e.stopPropagation();
selectOption(v);
}}
className="flex items-center border-border border-[0.05em] rounded-sm px-2 gap-1 cursor-pointer bg-none outline-none hover:bg-secondary hover:border-primary/20 focus:bg-secondary focus:border-primary/20"
>
{v.label}
<span className="text-[#777] outline-none cursor-pointer text-[1.25em] focus:text-border hover:text-border">
&times;
</span>
</button>
))
: value?.label}
</span>
<button
className="text-[#777] outline-none cursor-pointer text-[1.25em] focus:text-border hover:text-border"
onClick={(e) => {
e.stopPropagation();
clearOptions();
}}
>
&times;
</button>
<div className="bg-border self-stretch w-[0.05em]"></div>
<div className="border-[0.30em] border-transparent border-t-border translate-[0_25%] mx-1"></div>
<ul
className={cn(
"absolute hidden max-h-[15em] overflow-y-auto border-[0.05em] border-border rounded-lg w-full left-0 top-[calc(100%+0.5rem)] bg-background z-[100]",
isOpen ? "block" : null,
)}
>
{options.map((option, index) => (
<li
key={option.value}
onMouseEnter={() => setHighlightedIndex(index)}
className={cn(
"py-1 px-2 cursor-pointer",
isOptionSelected(option)
? "bg-primary text-primary-foreground"
: null,
index === highlightedIndex
? "bg-secondary text-foreground"
: null,
)}
onClick={(e) => {
e.stopPropagation();
selectOption(option);
setIsOpen(false);
}}
>
{option.label}
</li>
))}
</ul>
</div>
);
};

export default Select;
28 changes: 28 additions & 0 deletions src/pages/Select-Component/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useState } from "react";
import Select, { SelectOptions } from "./components/Select";

const options = [
{ label: "First", value: 1 },
{ label: "Second", value: 2 },
{ label: "Third", value: 3 },
{ label: "Fourth ", value: 4 },
{ label: "Fifth", value: 5 },
];

const SelectComponent = () => {
const [value1, setValue1] = useState<SelectOptions[]>([options[0]]);
const [value2, setValue2] = useState<SelectOptions | undefined>(options[0]);
return (
<div className="container flex flex-col justify-center items-center gap-10">
<Select
multiple
options={options}
value={value1}
onChange={(o) => setValue1(o)}
/>
<Select options={options} value={value2} onChange={(o) => setValue2(o)} />
</div>
);
};

export default SelectComponent;
7 changes: 3 additions & 4 deletions tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ export default {
darkMode: ["class"],
content: [
"./index.html",
"./pages/*.{js,jsx}",
"./pages/**/*.{js,jsx}",
"./components/**/*.{js,jsx}",
"./src/**/*.{js,jsx}",
"./src/pages/**/*.{js,jsx,ts,tsx}",
"./src/components/**/*.{js,jsx,ts,tsx}",
"./src/**/*.{js,jsx,ts,tsx}",
],
prefix: "",
theme: {
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "ES2020",
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
Expand Down

0 comments on commit 7e5bcac

Please sign in to comment.