Skip to content

Commit

Permalink
feat: Extract custom dropdown to reusable component, fix styles, matc…
Browse files Browse the repository at this point in the history
…h zeplin specs, use it in Sim tab [PT-188162567]
  • Loading branch information
pjanik committed Aug 26, 2024
1 parent 0bcfd10 commit 8142aa9
Show file tree
Hide file tree
Showing 17 changed files with 315 additions and 177 deletions.
60 changes: 0 additions & 60 deletions src/assets/scss/location-picker.scss

This file was deleted.

File renamed without changes.
2 changes: 1 addition & 1 deletion src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { AboutTab } from "./about-tab";
import { Header } from "./header";
import { locationsEqual } from "../utils/daylight-utils";

import "../assets/scss/App.scss";
import "./App.scss";

const debouncedUpdateRowSelectionInCodap = debounce((
latitude: string,
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion src/components/about-tab.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react";

import "../assets/scss/about-tab.scss";
import "./about-tab.scss";

export const AboutTab: React.FC = () => {
return (
Expand Down
114 changes: 114 additions & 0 deletions src/components/dropdown.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
@import "./vars.scss";

.day-length-dropdown-container {
box-sizing: border-box;
font-size: 12px;

.day-length-dropdown-label {
position: relative;
margin-top: 6px;
margin-bottom: 6px;
}

&.inline {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;

.day-length-dropdown-label {
margin-right: 6px;
}
}

.day-length-dropdown {
display: flex;
position: relative;
justify-content: space-between;

.left-icon {
position: absolute;
left: 6px;
top: 6px;
z-index: 1;
}

.dropdown-main-button, input {
font-family: "Montserrat", sans-serif;
padding: 5px;
height: $input-height;
width: 100%;
border-radius: $input-border-radius;
box-sizing: border-box;
border: $input-border;
outline: none;
text-align: left;
}

.dropdown-main-button-container {
width: 100%;
background-color: white;
border-radius: $input-border-radius;
box-sizing: border-box;
}

.dropdown-main-button {
font-size: 12px;
font-weight: 600;
cursor: pointer;
&:hover {
background-color: $codap-teal-lightest;
}
&:active {
background-color: $codap-teal-active;
}
}

input {
padding-left: 28px;

&:hover {
background-color: $codap-teal-lightest-2;
}
&:focus {
background-color: $codap-teal-lightest;
}
}

.day-length-dropdown-options {
$padding: 4px;
z-index: 1;
position: absolute;
background-color: white;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.5);
width: calc(100% - 2 * $padding);
top: 32px;
left: 0;
margin: 0;
padding: $padding;

button {
font-family: "Montserrat", sans-serif;
border: none;
background-color: white;
display: flex;
align-items: center;
width: 100%;
height: 24px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;

cursor: pointer;
transition: background-color 0.3s;

&.focused {
background-color: $codap-teal-lightest;
}
&:active {
background-color: $codap-teal-active;
}
}
}
}
}
143 changes: 143 additions & 0 deletions src/components/dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import React, { useState, useEffect } from "react";
import { clsx } from "clsx";

import "./dropdown.scss";

interface IOption {
name: string;
value?: string;
}

interface IDropdownProps<T extends IOption> {
value: string;
options: T[];
onSelect: (place: T) => void;
onSearchChange?: (value: string) => void;
label: string;
inputPlaceholder?: string;
icon?: React.ReactNode;
inline?: boolean;
width?: string;
}

export const Dropdown = <T extends IOption>({
value,
options,
onSelect,
onSearchChange,
label,
inputPlaceholder,
icon,
inline,
width
}: IDropdownProps<T>) => {
const [showOptions, setShowOptions] = useState(false);
const [focusedOptionIndex, setFocusedOptionIndex] = useState(-1);

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (event.target instanceof HTMLElement) {
if (!event.target.closest(".day-length-dropdown")) {
setShowOptions(false);
}
}
};

document.addEventListener("click", handleClickOutside);
return () => {
document.removeEventListener("click", handleClickOutside);
};
}, []);

const handleTextInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newValue = event.target.value;
onSearchChange?.(newValue);
setShowOptions(newValue.length > 3);
};

const handleMainButtonClick = () => {
setShowOptions(prevState => !prevState);
}

const handleOptionSelect = (option: T) => {
onSelect(option);
setShowOptions(false);
};

const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (!showOptions) return;

switch (event.key) {
case "ArrowDown":
event.preventDefault();
setFocusedOptionIndex(prevIndex =>
prevIndex < options.length - 1 ? prevIndex + 1 : prevIndex
);
break;
case "ArrowUp":
event.preventDefault();
setFocusedOptionIndex(prevIndex => (prevIndex > 0 ? prevIndex - 1 : prevIndex));
break;
case "Enter":
event.preventDefault();
if (focusedOptionIndex >= 0 && focusedOptionIndex < options.length) {
handleOptionSelect(options[focusedOptionIndex]);
}
break;
case "Escape":
setShowOptions(false);
setFocusedOptionIndex(-1);
break;
}
};

return (
<div className={clsx("day-length-dropdown-container", { inline })}>
<div className="day-length-dropdown-label">
<label>{label}</label>
</div>
<div className="day-length-dropdown" style={{ width }}>
{
onSearchChange ? (
<input
type="text"
placeholder={inputPlaceholder}
value={value}
onChange={handleTextInputChange}
onKeyDown={handleKeyDown}
/>
) : (
<div className="dropdown-main-button-container">
<button className="dropdown-main-button" onClick={handleMainButtonClick}>{value}</button>
</div>
)
}
<div className="left-icon">
{icon}
</div>
{
showOptions && options.length > 0 && (
<div className="day-length-dropdown-options">
{
options.map((option, index) => (
<button
key={index}
onClick={() => handleOptionSelect(option)}
onMouseEnter={() => setFocusedOptionIndex(index)}
onMouseLeave={() => setFocusedOptionIndex(-1)}
className={clsx({
focused: focusedOptionIndex === index ? "focused" : ""
})
}
>
{option.name}
</button>
))
}
</div>
)
}
</div>
</div>
);
};
File renamed without changes.
2 changes: 1 addition & 1 deletion src/components/header.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from "react";
import { TabName } from "../types";

import "../assets/scss/header.scss";
import "./header.scss";

interface IHeaderProps {
activeTab: TabName;
Expand Down
Loading

0 comments on commit 8142aa9

Please sign in to comment.