Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
17 changes: 12 additions & 5 deletions src/app/components/button/dropdown/FilterDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,14 @@ const FilterDropdown = ({ options, className = "", onChange, initialValue, readO
setIsOpen((prev) => !prev);
};

const handleSelect = (option: string) => {
setSelectedLabel(option);
setIsOpen(false);
onChange?.(option);
const handleSelect = (option: string | null) => {
if (option !== null) {
setSelectedLabel(option);
setIsOpen(false);
onChange?.(option);
} else {
setIsOpen(false);
}
};

return (
Expand All @@ -66,7 +70,10 @@ const FilterDropdown = ({ options, className = "", onChange, initialValue, readO
? "border border-grayscale-100 bg-white"
: "border-primary-orange-300 bg-primary-orange-50"
)}
onClick={toggleDropdown}
onClick={(e) => {
e.preventDefault();
toggleDropdown();
}}
disabled={readOnly}
>
<span className={selectedLabel === options[0] ? "text-black-100" : "text-primary-orange-300"}>
Expand Down
114 changes: 87 additions & 27 deletions src/app/components/button/dropdown/InputDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"use client";
import React, { forwardRef, useEffect, useState } from "react";
import React, { forwardRef, useEffect, useState, useRef, useCallback } from "react";
import { IoMdArrowDropdown } from "react-icons/io";
import { cn } from "@/lib/tailwindUtil";
import DropdownList from "./dropdownComponent/DropdownList";
Expand All @@ -19,26 +19,64 @@ const InputDropdown = forwardRef<HTMLInputElement, InputDropdownProps>(
const [selectedValue, setSelectedValue] = useState<string>("");
const [isCustomInput, setIsCustomInput] = useState<boolean>(false);
const { setValue, watch } = useFormContext();
const dropdownRef = useRef<HTMLDivElement>(null);

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

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

const toggleDropdown = (e: React.MouseEvent) => {
e.preventDefault();
setIsOpen((prev) => !prev);
};

const handleSelect = useCallback(
(option: string | null) => {
if (!option) {
setIsOpen(false);
return;
}

if (option === "직접 입력") {
setIsCustomInput(true);
setSelectedValue("");
setValue(name, "", { shouldDirty: true });
setIsOpen(false);
return;
}

const handleOptionClick = (option: string) => {
if (option === "직접 입력") {
setIsCustomInput(true);
setSelectedValue("");
// 동적으로 받아온 name에 값 할당 -> 훅폼에 저장
setValue(name, selectedValue, { shouldDirty: true });
} else {
setSelectedValue(option);
setIsCustomInput(false);
setValue(name, option, { shouldDirty: true });
setIsOpen(false);
}
};
},
[name, setValue]
);

const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
if (isCustomInput) {
const value = e.target.value;
setSelectedValue(value);
setValue(name, value, { shouldDirty: true });
}
},
[isCustomInput, name, setValue]
);

// 작성중인 탭으로 다시 이동했을때 이전에 저장된 훅폼 데이터 연동
useEffect(() => {
const value = watch(name); // 동적으로 필드 값 가져오기
const value = watch(name);
if (value !== undefined) {
setSelectedValue(value); // 초기값 동기화
setSelectedValue(value);
}
}, [name, watch]);

Expand All @@ -48,37 +86,59 @@ const InputDropdown = forwardRef<HTMLInputElement, InputDropdownProps>(
const errorTextStyle =
"absolute -bottom-[26px] text-[13px] text-sm font-medium leading-[22px] text-state-error lg:text-base lg:leading-[26px]";

const handleInputClick = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation();
if (!isCustomInput) {
setIsOpen(true);
}
},
[isCustomInput]
);

return (
<div className={cn("relative inline-block text-left", "w-80 lg:w-[640px]", textStyle, className, errorStyle)}>
<div
ref={dropdownRef}
className={cn("relative inline-block text-left", "w-80 lg:w-[640px]", textStyle, className, errorStyle)}
>
<div
onMouseDown={() => setIsOpen(!isOpen)}
onClick={isCustomInput ? undefined : toggleDropdown}
className={cn(
"cursor-pointer rounded-md border border-transparent bg-background-200 p-2",
"hover:border-grayscale-200 hover:bg-background-300",
"rounded-md border border-transparent bg-background-200 p-2",
!isCustomInput && "cursor-pointer hover:border-grayscale-200 hover:bg-background-300",
isOpen && "ring-1 ring-grayscale-300"
)}
>
<input
type="text"
ref={ref}
value={selectedValue}
onChange={(e) => {
if (isCustomInput) {
setSelectedValue(e.target.value);
setValue(name, e.target.value);
}
}}
onChange={handleInputChange}
onClick={handleInputClick}
readOnly={!isCustomInput}
className={cn(
"text-grayscale-700 flex w-full items-center justify-between px-4 py-2 font-medium focus:outline-none",
"cursor-pointer bg-transparent"
"flex w-full items-center justify-between px-4 py-2 font-medium focus:outline-none",
isCustomInput ? "cursor-text bg-transparent" : "cursor-pointer bg-transparent",
"text-grayscale-700"
)}
placeholder={isCustomInput ? "직접 입력하세요" : "선택"}
/>
<button type="button" className="absolute right-3 top-3.5 text-3xl">
<button
type="button"
className="absolute right-3 top-3.5 text-3xl"
onClick={(e) => {
e.stopPropagation();
if (isCustomInput) {
setIsCustomInput(false);
setSelectedValue("");
setValue(name, "", { shouldDirty: true });
}
}}
>
<IoMdArrowDropdown className={cn("transition-transform duration-200", isOpen && "rotate-180")} />
</button>
</div>
{isOpen && <DropdownList list={options} onSelect={handleOptionClick} itemStyle={textStyle} />}
{isOpen && <DropdownList list={options} onSelect={handleSelect} itemStyle={textStyle} />}
{errormessage && <p className={cn(errorTextStyle, "right-0 pr-2")}>{errormessage}</p>}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@ const DropdownItem = ({
itemStyle,
}: {
item: string;
onSelect: (item: string) => void;
onSelect: (item: string | null) => void;
itemStyle?: string;
}) => {
const handleClick = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
onSelect(item);
};

return (
<li
onClick={() => onSelect(item)}
onClick={handleClick}
className={cn(
"flex w-full cursor-pointer bg-grayscale-50 px-[10px] py-2 text-sm font-normal leading-[18px] text-black-100 hover:bg-primary-orange-50 lg:text-lg lg:leading-[26px]",
itemStyle
Expand All @@ -23,14 +29,15 @@ const DropdownItem = ({
</li>
);
};

const DropdownList = ({
list,
onSelect,
wrapperStyle,
itemStyle,
}: {
list: string[];
onSelect: (item: string) => void;
onSelect: (item: string | null) => void;
wrapperStyle?: string;
itemStyle?: string;
}) => {
Expand All @@ -39,22 +46,28 @@ const DropdownList = ({
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
onSelect("");
event.preventDefault();
onSelect(null);
}
};

document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("mousedown", handleClickOutside, { capture: true });
return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("mousedown", handleClickOutside, { capture: true });
};
}, []);
}, [onSelect]);

const handleContainerClick = (e: React.MouseEvent) => {
e.stopPropagation();
};

return (
<div
role="menu"
aria-orientation="vertical"
aria-labelledby="options-menu"
ref={dropdownRef}
onClick={handleContainerClick}
className={cn(
"absolute left-0 right-0 z-10 mt-[6px] rounded border border-grayscale-100 bg-grayscale-50 pr-[2px] pt-1",
wrapperStyle
Expand Down
92 changes: 66 additions & 26 deletions src/app/components/input/dateTimeDaypicker/TimePickerInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,55 +4,95 @@ import { IoMdArrowDropup } from "react-icons/io";
import BaseInput from "../text/BaseInput";
import { useDropdownOpen } from "@/hooks/useDropdownOpen";
import DropdownList from "../../button/dropdown/dropdownComponent/DropdownList";
import { forwardRef } from "react";
import { forwardRef, useRef, useEffect, useCallback } from "react";
import { BaseInputProps } from "@/types/textInput";
import { cn } from "@/lib/tailwindUtil";

const TimePickerInput = forwardRef<HTMLInputElement, BaseInputProps>((props, ref) => {
const { value, onChange, errormessage } = props;
const dropdownRef = useRef<HTMLDivElement>(null);
const { isOpen, handleOpenDropdown, setIsOpen } = useDropdownOpen();

const handleClickOutside = useCallback(
(event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
},
[setIsOpen]
);

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

const handleSelect = useCallback(
(time: string | null) => {
if (!time) {
setIsOpen(false);
return;
}

if (onChange) {
const event = {
target: { value: time, name: props.name },
} as React.ChangeEvent<HTMLInputElement>;
onChange(event);
}

setIsOpen(false);
},
[onChange, props.name, setIsOpen]
);

const handleTimeSelect = (time: string) => {
if (onChange) {
onChange({ target: { value: time } } as React.ChangeEvent<HTMLInputElement>);
}
handleOpenDropdown();
};
const { isOpen, handleOpenDropdown } = useDropdownOpen();
const beforeIconStyle = "text-grayscale-400 size-5 lg:size-8";
const afterIconStyle =
"text-black-400 size-6 lg:size-9 transition-all transition-transform duration-200 ease-in-out";
const afterIconStyle = "text-black-400 size-6 lg:size-9 transition-transform duration-200 ease-in-out";
const width = "w-[150px] lg:w-[210px]";

const timeOption = Array.from({ length: 24 }, (_, index) => {
const hour = index.toString().padStart(2, "0");
return `${hour}:00`;
});

const handleClick = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
handleOpenDropdown();
},
[handleOpenDropdown]
);

return (
<div onClick={handleOpenDropdown} className="relative">
<div>{isOpen}</div>
<BaseInput
ref={ref}
type="text"
readOnly={true}
variant="white"
placeholder="00:00"
value={value || ""}
size="w-[150px] h-[54px] lg:w-[210px] lg:h-[64px]"
beforeIcon={<LuClock className={beforeIconStyle} strokeWidth="1" />}
afterIcon={<IoMdArrowDropup className={`${afterIconStyle} ${isOpen ? "rotate-180" : ""}`} />}
errormessage={errormessage}
/>
<div ref={dropdownRef} className="relative">
<div onClick={handleClick}>
<BaseInput
ref={ref}
type="text"
readOnly
variant="white"
placeholder="00:00"
value={value || ""}
size="w-[150px] h-[54px] lg:w-[210px] lg:h-[64px]"
beforeIcon={<LuClock className={beforeIconStyle} strokeWidth="1" />}
afterIcon={<IoMdArrowDropup className={cn(afterIconStyle, isOpen && "rotate-180")} />}
errormessage={errormessage}
/>
</div>
{isOpen && (
<DropdownList
list={timeOption}
onSelect={handleTimeSelect}
onSelect={handleSelect}
wrapperStyle={width}
itemStyle="pl-[35px] text-base font-normal leading-[26px] lg:text-lg lg:leading-8 !important"
itemStyle="pl-[35px] text-base font-normal leading-[26px] lg:text-lg lg:leading-8"
/>
)}
</div>
);
});

TimePickerInput.displayName = "TimePickerInput";

export default TimePickerInput;
Loading
Loading