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

1211 add select component for react hook form #1213

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
198 changes: 198 additions & 0 deletions packages/web/src/common/components/Select/Form/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { useEffect, useRef, useState } from "react";

import isPropValid from "@emotion/is-prop-valid";
import styled, { css } from "styled-components";

import FormError from "@sparcs-clubs/web/common/components/FormError";
import Label from "@sparcs-clubs/web/common/components/FormLabel";
import Icon from "@sparcs-clubs/web/common/components/Icon";

import NoOption from "../_atomic/NoOption";
import Dropdown from "../Dropdown";
import SelectOption from "../SelectOption";

export interface SelectItem<T> {
label: string;
value: T;
selectable?: boolean;
}

interface SelectProps<T> {
items: SelectItem<T>[];
label?: string;
errorMessage?: string;
disabled?: boolean;
value: T;
onChange?: (value: T) => void;
placeholder?: string;
}

const SelectInner = styled.div`
gap: 4px;
position: relative;
`;

const disabledStyle = css`
background-color: ${({ theme }) => theme.colors.GRAY[100]};
border-color: ${({ theme }) => theme.colors.GRAY[200]};
color: ${({ theme }) => theme.colors.GRAY[300]};
pointer-events: none;
`;

const StyledSelect = styled.div.withConfig({
shouldForwardProp: prop => isPropValid(prop),
})<{ hasError?: boolean; disabled?: boolean; isOpen?: boolean }>`
width: 100%;
padding: 8px 12px;
outline: none;
cursor: pointer;
background-color: ${({ theme }) => theme.colors.WHITE};
border: 1px solid
${({ theme, hasError, isOpen }) => {
if (isOpen) return theme.colors.PRIMARY;
return hasError ? theme.colors.RED[600] : theme.colors.GRAY[200];
}};
border-radius: 4px;
font-family: ${({ theme }) => theme.fonts.FAMILY.PRETENDARD};
font-size: 16px;
line-height: 20px;
font-weight: ${({ theme }) => theme.fonts.WEIGHT.REGULAR};

&:focus,
&:hover:not(:focus) {
border-color: ${({ theme, isOpen }) =>
isOpen ? theme.colors.PRIMARY : theme.colors.GRAY[300]};
}

${({ disabled }) => disabled && disabledStyle}
`;

const IconWrapper = styled.div`
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
`;

const SelectWrapper = styled.div`
width: 100%;
flex-direction: column;
display: flex;
gap: 4px;
`;

const SelectValue = styled.span.withConfig({
shouldForwardProp: prop => isPropValid(prop),
})<{ isSelected: boolean; disabled: boolean }>`
color: ${({ theme, isSelected, disabled }) => {
if (disabled) {
return theme.colors.GRAY[300];
}
if (isSelected) {
return theme.colors.BLACK;
}
return theme.colors.GRAY[200];
}};
`;

const FormSelect = <T,>({
items,
errorMessage = "",
label = "",
disabled = false,
value,
onChange = () => {},
placeholder = "항목을 선택해주세요",
}: SelectProps<T>) => {
const [isOpen, setIsOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (
containerRef.current &&
!containerRef.current.contains(event.target as Node)
) {
if (isOpen) {
setIsOpen(false);
}
}
}

document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [containerRef, isOpen, items.length, value]);

const handleSelectClick = () => {
if (!disabled) {
setIsOpen(!isOpen);
}
};

const handleOptionClick = (item: SelectItem<T>) => {
if (item.selectable || item.selectable === undefined) {
onChange(item.value);
setIsOpen(false);
}
};

const selectedLabel =
items.find(item => item.value === value)?.label || placeholder;

return (
<SelectWrapper>
{label && <Label>{label}</Label>}
<SelectWrapper>
<SelectInner ref={containerRef}>
<StyledSelect
hasError={errorMessage.length > 0}
disabled={disabled}
onClick={handleSelectClick}
isOpen={isOpen}
>
<SelectValue
isSelected={value != null && value !== ""}
disabled={disabled}
>
{selectedLabel}
</SelectValue>
<IconWrapper>
{isOpen ? (
<Icon type="keyboard_arrow_up" size={20} />
) : (
<Icon type="keyboard_arrow_down" size={20} />
)}
</IconWrapper>
</StyledSelect>
{isOpen && (
<Dropdown marginTop={4}>
{items.length > 0 ? (
items.map(item => (
<SelectOption
key={item.value as string}
selectable={
item.selectable || item.selectable === undefined
}
onClick={() => handleOptionClick(item)}
>
{item.label}
</SelectOption>
))
) : (
<NoOption>항목이 존재하지 않습니다</NoOption>
)}
</Dropdown>
)}
</SelectInner>
{errorMessage.length > 0 && <FormError>{errorMessage}</FormError>}
</SelectWrapper>
</SelectWrapper>
);
};

export default FormSelect;
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import TextInput from "@sparcs-clubs/web/common/components/Forms/TextInput";
import SectionTitle from "@sparcs-clubs/web/common/components/SectionTitle";
import Select from "@sparcs-clubs/web/common/components/Select";

import FormSelect from "@sparcs-clubs/web/common/components/Select/Form";

import { meetingEnumToText } from "../constants/getEnumType";

interface MeetingInformationFrameProps {
Expand Down Expand Up @@ -95,7 +97,7 @@ const MeetingInformationFrame: React.FC<MeetingInformationFrameProps> = ({
validate: value => typeof value === "boolean",
}}
renderItem={props => (
<Select
<FormSelect
{...props}
label="정기회의 여부"
placeholder="정기회의 여부를 선택해주세요"
Expand All @@ -104,7 +106,6 @@ const MeetingInformationFrame: React.FC<MeetingInformationFrameProps> = ({
{ label: "비정기회의", value: false },
]}
disabled={isDivisionMeeting}
isRequired={false} // TODO. select 컴포넌트 hookform에서 사용하게 바꾼 후 삭제
/>
)}
/>
Expand Down