Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

YH-474: add dropdown #321

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/shared/assets/icons/ArrowSelect.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/shared/ui/Dropdown/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Dropdown } from './ui/Dropdown/Dropdown';
6 changes: 6 additions & 0 deletions src/shared/ui/Dropdown/model/DropdownTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface DropdownOption {
label: string;
value: string | number;
}

export type Size = 'S' | 'L';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DropdownSize

18 changes: 18 additions & 0 deletions src/shared/ui/Dropdown/ui/Dropdown/Dropdown.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.dropdown {
position: relative;
display: inline-flex;
}

.suffix {
flex-shrink: 0;
width: 20px;
height: 20px;
fill: var(--icon-fill);
transition: transform 0.3s ease;
pointer-events: none;
}

.suffix.active {
background-color: transparent;
transform: rotate(180deg);
}
98 changes: 98 additions & 0 deletions src/shared/ui/Dropdown/ui/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React, { useState, useRef, useEffect } from 'react';

import Arrow from '@/shared/assets/icons/ArrowSelect.svg';
import { Input } from '@/shared/ui/Input';

import { DropdownOption, Size } from '../../model/DropdownTypes';
import DropdownMenu from '../DropdownMenu/DropdownMenu';

import styles from './Dropdown.module.css';

interface DropdownProps
extends Omit<React.HTMLProps<HTMLDivElement>, 'prefix' | 'size' | 'onChange'> {
label: string;
disabled?: boolean;
options: DropdownOption[];
prefix?: React.ReactNode;
suffix?: React.ReactNode;
size?: Size;
error?: boolean;
onChange?: (value: string | number) => void;
}

export const Dropdown: React.FC<DropdownProps> = ({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FC мы не используем

label,
disabled = false,
options,
prefix,
suffix,
size = 'L',
error = false,
onChange,
...props
}) => {
const [isOpen, setIsOpen] = useState(false);
const inputRef = useRef<HTMLInputElement | null>(null);
const dropdownRef = useRef<HTMLDivElement | null>(null);

useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);

const toggleDropdown = () => setIsOpen((prev) => !prev);

const handleKeyDown = (event: React.KeyboardEvent) => {
if (event.key === 'Escape') {
setIsOpen(false);
}
};

return (
<div>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

пустой див нужен?

<div
className={styles.dropdown}
ref={dropdownRef}
tabIndex={0}
role="button"
onClick={toggleDropdown}
onKeyDown={handleKeyDown}
{...props}
>
<Input
ref={inputRef}
variant="dropdown"
size={size}
placeholder={label}
prefix={prefix}
suffix={
suffix || (
<Arrow
className={`${isOpen ? `${styles.suffix} ${styles.active}` : styles.suffix}`}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Тут используй classNames

classNames(styles.suffix, { [styles.active]: isOpen })

/>
)
}
error={error}
disabled={disabled}
/>

{isOpen && (
<DropdownMenu
size={size}
setIsOpen={setIsOpen}
onChange={onChange}
options={options}
inputRef={inputRef}
/>
)}
</div>
</div>
);
};

Dropdown.displayName = 'Dropdown';
53 changes: 53 additions & 0 deletions src/shared/ui/Dropdown/ui/DropdownMenu/DropdownMenu.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
.dropdown-content {
position: absolute;
right: 0;
left: 0;
z-index: 1;
display: flex;
flex-direction: column;
justify-content: flex-start;
max-height: 276px;
overflow-y: auto;
background: var(--background-block);
direction: rtl;
transition: transform 0.2s ease;

&::-webkit-scrollbar {
width: 2px;
}

&::-webkit-scrollbar-thumb {
background: var(--background-primary);
}

&::-webkit-scrollbar-track {
background: transparent;
}
}

.option-s {
width: 328px;
height: 184px;
}

.option-l {
width: 408px;
height: 276px;
}

.dropdown-position-bottom {
top: 100%;
transform: translateY(8px);
}

.dropdown-position-top {
top: auto;
bottom: 100%;
transform: translateY(-8px);
}






75 changes: 75 additions & 0 deletions src/shared/ui/Dropdown/ui/DropdownMenu/DropdownMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import classNames from 'classnames';
import { forwardRef, useState } from 'react';

import { DropdownOption, Size } from '../../model/DropdownTypes';
import DropdownOptions from '../DropdownOptions/DropdownOptions';

import styles from './DropdownMenu.module.css';

const HEIGHT_VALUE_S = 184;
const HEIGHT_VALUE_L = 276;

interface DropdownMenuProps {
size: Size;
setIsOpen: (isOpen: boolean) => void;
onChange?: (value: string | number) => void;
options: DropdownOption[];
inputRef: React.RefObject<HTMLInputElement>;
}

const DropdownMenu = forwardRef<HTMLUListElement, DropdownMenuProps>(
({ size, setIsOpen, onChange, options, inputRef }, ref) => {
const dropdownStyle = classNames(
styles['dropdown-content'],
size === 'S' ? styles['option-s'] : styles['option-l'],
);

const [selectedOption, setSelectedOption] = useState<DropdownOption | null>(null);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

тут лучше максимально наверх выносить все
у дропдавна не должно быть внутренних стейтов, все хэндлеры и стейты приходят извне


const handleOptionClick = (option: DropdownOption) => {
setSelectedOption(option);
if (onChange) {
onChange(option.value);
}
setIsOpen(false);
};

const dropdownPosition = () => {
if (inputRef.current) {
const rect = inputRef.current.getBoundingClientRect();
const headerRect = document.querySelector('header')?.getBoundingClientRect();
const spaceAbove = headerRect ? rect.top - headerRect.bottom : 0;
const spaceBelow = window.innerHeight - rect.bottom;
const heightValue = size === 'S' ? HEIGHT_VALUE_S : HEIGHT_VALUE_L;

if (spaceAbove >= heightValue) {
return styles['dropdown-position-top'];
} else if (spaceBelow >= heightValue) {
return styles['dropdown-position-bottom'];
} else {
return styles['dropdown-position-bottom'];
}
}
return styles['dropdown-position-bottom'];
};

const positionStyle = dropdownPosition();

return (
<ul ref={ref} className={`${dropdownStyle} ${positionStyle}`} role="listbox">
{options.map((option) => (
<DropdownOptions
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Option - единственное число, убрать s
DropdownOption

key={option.value}
option={option}
onClick={handleOptionClick}
isSelected={selectedOption?.value === option.value}
/>
))}
</ul>
);
},
);

DropdownMenu.displayName = 'DropdownMenu';

export default DropdownMenu;
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.dropdown-option {
display: flex;
justify-content: end;
padding: 12px 24px;
color: var(--text-color-bright);
transition: background-color 0.2s ease;
cursor: pointer;


&:hover{
background-color: var(--background-app);
color: var(--background-primary);
}

&:active{
background-color: var(--background-app);
}

&:focus-within{
background-color: var(--background-app);
color: var(--background-primary);
}
}
35 changes: 35 additions & 0 deletions src/shared/ui/Dropdown/ui/DropdownOptions/DropdownOptions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';

import { DropdownOption } from '../../model/DropdownTypes';

import styles from './DropdownOptions.module.css';

interface DropdownOptionsProps {
option: DropdownOption;
onClick: (value: DropdownOption) => void;
isSelected: boolean;
}

const DropdownOptions: React.FC<DropdownOptionsProps> = ({ option, onClick, isSelected }) => {
const handleKeyDown = (event: React.KeyboardEvent) => {
if (event.key === 'Enter' || event.key === ' ') {
onClick(option);
}
};

return (
<li
key={option.value}
className={styles['dropdown-option']}
onClick={() => onClick(option)}
onKeyDown={handleKeyDown}
tabIndex={0}
role="option"
aria-selected={isSelected}
>
{option.label}
</li>
);
};

export default DropdownOptions;
2 changes: 2 additions & 0 deletions src/shared/ui/Input/model/types/InputTypes.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export type Size = 'L' | 'S';

export type Variant = 'input' | 'dropdown';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

К обоим типам добавь вначале Input

35 changes: 19 additions & 16 deletions src/shared/ui/Input/ui/Input.module.css
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
.input {
flex-grow: 1;
align-self: center;
padding: 12px 24px 12px 0;
box-sizing: border-box;
outline: none;
border: none;
border-radius: inherit;
background: transparent;
color: var(--text-color-bright);
line-height: 120%;
}

.wrapper {
position: relative;
display: inline-flex;
align-items: center;
width: 100%;
height: 48px;
border: 1px solid var(--background-button);
border-radius: 68px;
background-color: var(--background-block);
Expand Down Expand Up @@ -33,26 +45,18 @@
}
}


.input {
flex-grow: 1;
align-self: center;
padding: 12px 24px 12px 0;
box-sizing: border-box;
outline: none;
border: none;
border-radius: inherit;
background: transparent;
color: var(--text-color-bright);
line-height: 120%;
.dropdown {
cursor: pointer;
}

.wrapper-s {
width: 328px;
height: 48px;
}

.wrapper-l {
width: 408px;
width: 408px;
height: 48px;
}

.wrapper-disabled {
Expand Down Expand Up @@ -82,7 +86,6 @@
}
}


.input-prefix {
display: flex;
flex-shrink: 0;
Expand All @@ -107,4 +110,4 @@
height: 18px;
fill: var(--icon-fill);
pointer-events: none;
}
}
Loading