Skip to content

Commit

Permalink
Merge pull request #439 from dump-hr/lovretomic/dropdown
Browse files Browse the repository at this point in the history
Dropdown Component
  • Loading branch information
lovretomic authored Dec 18, 2024
2 parents 4ace9c8 + 7abb0f2 commit 8b088b4
Show file tree
Hide file tree
Showing 10 changed files with 305 additions and 48 deletions.
1 change: 1 addition & 0 deletions apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"clsx": "^2.1.1",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
function App() {
return (
<>
<p>DUMP Days 2025 App</p>
<h1>App</h1>
</>
);
}
Expand Down
3 changes: 3 additions & 0 deletions apps/app/src/assets/icons/arrow-down-1.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
117 changes: 117 additions & 0 deletions apps/app/src/compontents/Dropdown/Dropdown.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
.wrapper {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
position: relative;

.label {
@include paragraph-14;
color: $black-50;
}

.mainButton {
@include paragraph-16;
background-color: $black-10;
outline: none;
border: none;
cursor: pointer;
width: 100%;

box-sizing: border-box;
padding: 16px;
border-radius: 4px;

display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;

transition: box-shadow 200ms;

.arrow {
transition: 200ms;
}

&.isOpen {
box-shadow: inset 0 0 0 1px $black-30;

.arrow {
rotate: 180deg;
}
}

&.isError {
box-shadow: inset 0 0 0 1px $error-light;
}
}

.errorLabel {
@include paragraph-14;
color: $error-light;
}

.optionsWrapper {
box-sizing: border-box;
padding: 16px;
box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.12);
background-color: white;
border-radius: 4px;
position: absolute;
top: 86px;
width: 100%;
z-index: 100;

.innerContainer {
display: block;
width: 100%;
box-sizing: border-box;

max-height: 310px;
overflow-y: auto;

&::-webkit-scrollbar {
width: 4px;
border-radius: 100px;
}

&::-webkit-scrollbar-track {
background: $black-10;
border-radius: 100px;
}

&::-webkit-scrollbar-thumb {
background: $primary-black;
border-radius: 100px;
}

&::-webkit-scrollbar-thumb:hover {
background: $primary-black;
}

.divider {
@include dottedBreak(rgba(23, 22, 21, 0.65));
height: 4px;
margin-bottom: 16px;
margin-top: 8px;
}

.option {
background: none;
outline: none;
border: none;
@include paragraph-16;
margin: 0;
padding: 0;
text-align: left;
cursor: pointer;
width: 100%;

&.selected {
color: $black-30;
cursor: default;
}
}
}
}
}
90 changes: 90 additions & 0 deletions apps/app/src/compontents/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { useRef, useState } from 'react';
import React from 'react';
import c from './Dropdown.module.scss';
import { DropdownOption } from './DropdownOption';
import ArrowIcon from '../../assets/icons/arrow-down-1.svg';
import clsx from 'clsx';
import { useClickOutside } from '../../hooks/useClickOutside';

type DropdownProps = {
label: string;
placeholder: string;
options: DropdownOption[];
setOption: (option: DropdownOption) => void;
selectedOption: DropdownOption | undefined;
errorLabel?: string;
showError?: boolean;
width?: string;
hasError?: boolean;
};

const Dropdown = ({
label,
placeholder,
options,
setOption,
selectedOption,
errorLabel,
width = 'auto',
hasError = false,
}: DropdownProps) => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);

const toggle = () => {
setIsOpen(!isOpen);
};

function handleOptionSelected(option: DropdownOption) {
setOption(option);
setIsOpen(false);
}

useClickOutside(dropdownRef, () => setIsOpen(false));

const widthStyle = { width: width };
const showError = (hasError || !selectedOption?.value) && !isOpen;

return (
<div className={c.wrapper} style={widthStyle} ref={dropdownRef}>
{label && <label className={c.label}>{label}</label>}

<button
className={clsx({
[c.mainButton]: true,
[c.isOpen]: isOpen,
[c.isError]: showError,
})}
onClick={toggle}>
{selectedOption?.label || placeholder}
<img className={c.arrow} src={ArrowIcon} alt='arrow' />
</button>

{showError && <div className={c.errorLabel}>{errorLabel}</div>}

{isOpen && (
<div className={c.optionsWrapper}>
<div className={c.innerContainer}>
{options.map((option, i) => (
<React.Fragment key={option.value}>
{i !== 0 && <div className={c.divider} key={i}></div>}
<button
disabled={option.value === selectedOption?.value}
className={clsx({
[c.option]: true,
[c.selected]: option.value === selectedOption?.value,
})}
key={option.value}
onClick={() => handleOptionSelected(option)}>
{option.label}
</button>
</React.Fragment>
))}
</div>
</div>
)}
</div>
);
};

export default Dropdown;
4 changes: 4 additions & 0 deletions apps/app/src/compontents/Dropdown/DropdownOption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type DropdownOption = {
value: string;
label: string;
};
3 changes: 3 additions & 0 deletions apps/app/src/compontents/Dropdown/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Dropdown from './Dropdown';

export default Dropdown;
21 changes: 21 additions & 0 deletions apps/app/src/hooks/useClickOutside.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { RefObject, useEffect } from 'react';

export const useClickOutside = (
ref: RefObject<HTMLElement>,
handleOnClickOutside: (event: MouseEvent | TouchEvent) => void,
) => {
useEffect(() => {
const listener = (event: MouseEvent | TouchEvent) => {
if (!ref.current || ref.current.contains(event.target as Node)) {
return;
}
handleOnClickOutside(event);
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
}, [ref, handleOnClickOutside]);
};
107 changes: 60 additions & 47 deletions apps/app/src/styles/_mixins.scss
Original file line number Diff line number Diff line change
@@ -1,76 +1,89 @@
@import "./fonts";
@import './fonts';

@mixin heading-1 {
font-family: NeueMachina;
font-size: 2rem;
line-height: 2.25rem;
letter-spacing: -1%;
font-family: NeueMachina;
font-size: 2rem;
line-height: 2.25rem;
letter-spacing: -1%;
}

@mixin heading-2 {
font-family: NeueMachina;
font-size: 1.5rem;
line-height: 1.75rem;
letter-spacing: -1%;
font-family: NeueMachina;
font-size: 1.5rem;
line-height: 1.75rem;
letter-spacing: -1%;
}

@mixin heading-3 {
font-family: NeueMachina;
font-size: 1rem;
line-height: 1.25rem;
letter-spacing: -1%;
font-family: NeueMachina;
font-size: 1rem;
line-height: 1.25rem;
letter-spacing: -1%;
}

@mixin label-large {
font-family: Inter;
font-weight: 700;
font-size: 1.25rem;
line-height: 1.75rem;
letter-spacing: 0%;
font-family: Inter;
font-weight: 700;
font-size: 1.25rem;
line-height: 1.75rem;
letter-spacing: 0%;
}

@mixin label-medium {
font-family: Inter;
font-weight: 600;
font-size: 1rem;
line-height: 1.375rem;
letter-spacing: 0%;
font-family: Inter;
font-weight: 600;
font-size: 1rem;
line-height: 1.375rem;
letter-spacing: 0%;
}

@mixin label-small {
font-family: Inter;
font-weight: 600;
font-size: 0.875rem;
line-height: 1.125rem;
letter-spacing: 2%;
font-family: Inter;
font-weight: 600;
font-size: 0.875rem;
line-height: 1.125rem;
letter-spacing: 2%;
}

@mixin paragraph-16 {
font-family: Inter;
font-weight: 400;
font-size: 1rem;
line-height: 1.375rem;
letter-spacing: -1%;
font-family: Inter;
font-weight: 400;
font-size: 1rem;
line-height: 1.375rem;
letter-spacing: -1%;
}

@mixin paragraph-14 {
font-family: Inter;
font-weight: 400;
font-size: 0.875rem;
line-height: 1.25rem;
letter-spacing: -1%;
font-family: Inter;
font-weight: 400;
font-size: 0.875rem;
line-height: 1.25rem;
letter-spacing: -1%;
}

@mixin tag-16 {
font-family: NeueMontrealMono;
font-size: 1rem;
line-height: 1.125rem;
letter-spacing: 0%;
font-family: NeueMontrealMono;
font-size: 1rem;
line-height: 1.125rem;
letter-spacing: 0%;
}

@mixin tag-14 {
font-family: NeueMontrealMono;
font-size: 0.875rem;
line-height: 1rem;
letter-spacing: 0%;
}
font-family: NeueMontrealMono;
font-size: 0.875rem;
line-height: 1rem;
letter-spacing: 0%;
}

@mixin dottedBreak($color: rgba(23, 22, 21, 0.3), $dot-size: 0.8px) {
width: 100%;
height: 2px;
background-image: radial-gradient(
circle,
$color $dot-size,
transparent calc($dot-size + 0.3px)
);
background-size: calc($dot-size * 10) 2px;
background-repeat: repeat-x;
background-color: transparent;
}
Loading

0 comments on commit 8b088b4

Please sign in to comment.