이번엔 Select Box를 구현해보려고 한다. 중요한 사실은~ <select>
객체는 스타일링이 가능하지만 드롭다운 되는 <option>
객체는 스타일링이 불가능하다. 그러므로 select + option 조합이 아니라 다른 방법으로 구현을 해야 한다. 여기에서는 div와 ul, li를 사용하였다.
type InputSelectProp = {
disabled?: boolean;
name?: string;
options?: Array<{
[index: number]: string;
name: string;
value: string;
}>;
onChange: (value: string) => void;
};
const InputSelect = ({ disabled, options, onChange }: InputSelectProp) => {
const [isOpened, setIsOpened] = useState(false);
const [selectedId, setSelectedId] = useState(-1);
const selectShape =
"caption-bold border-m1 " +
"tablet:border-t1 desktop:border-d1 " +
(isOpened || selectedId !== -1 ? "border-black " : "border-lightgray ") +
(selectedId === -1 ? "text-gray " : "text-black ") +
(disabled ? "bg-extralight border-lightgray text-lightgray " : "");
const selectLayout =
"flex justify-between items-center px-m16 py-m12 " +
"tablet:px-t16 desktop:px-d16 tablet:py-t16 desktop:py-d16 ";
const selectState = "cursor-pointer ";
const selectStyle = selectShape + selectLayout + selectState;
const optionContainerShape = "w-full border border-black divide-y ";
const optionContainerLayout =
"absolute left-0 top-full mt-m8 " + "tablet:mt-t8 desktop:mt-d8 ";
const optionContainerState = "";
const optionContainerStyle =
optionContainerShape + optionContainerLayout + optionContainerState;
const optionShape = "caption-bold bg-white ";
const optionLayout = "p-m16 " + "tablet:p-t16 desktop:p-d16 ";
const optionState = "cursor-pointer ";
const optionStyle = optionShape + optionLayout + optionState;
const handleClick = () => {
if (disabled) return;
setIsOpened(!isOpened);
};
const handleOptionClick = (index: number) => {
setSelectedId(index);
setIsOpened(false);
onChange(options[index].value);
};
return (
<div className="relative w-full">
<div className={selectStyle} onClick={handleClick}>
<p>{selectedId === -1 ? "-Select-" : options[selectedId].name}</p>
<ArrowIcon
className={`h-m24 w-m24 ${
disabled ? "fill-lightgray " : "fill-black "
} tablet:h-t24 tablet:w-t24 desktop:h-d24 desktop:w-d24 ${
isOpened ? "rotate-180" : "rotate-0"
}`}
/>
</div>
{isOpened && (
<ul className={optionContainerStyle}>
{options &&
options.map(({ name, value }, index) => (
<li
key={value}
className={
optionStyle +
(index === selectedId ? "bg-gray text-white" : "")
}
onClick={() => handleOptionClick(index)}
>
{name}
</li>
))}
</ul>
)}
</div>
);
};
function App() {
const [value, setValue] = useState("");
return (
<div className="mx-auto mt-6 flex h-screen w-[400px] flex-col place-items-center">
<p>현재 선택된 value: {value}</p>
<InputSelect
// disabled
name="select"
options={[
{ name: "Option A", value: "option-a" },
{ name: "Option B", value: "option-b" },
{ name: "Option C", value: "option-c" },
{ name: "Option D", value: "option-d" },
]}
onChange={(value) => setValue(value)}
/>
</div>
);
만들면서,, 디자이너분이 꽤나 많이 헤맸다는 느낌을 많이 받았다(ㅋㅋ) SelectBox의 예외 케이스에 대한 스펙이 없었기 때문. 옵션이 Scrollable한 건지, 아래에 화면이 부족하면 위로 뜨게 할 건지 아님 끝에 맞춰서 보여줄지, 선택한 상태에서 또 열면 border가 그대로 블랙인인 건지 어떤 건지!
물론 만들어진 템플릿을 내가 다운 받아서 구현하는 거라 어느 정도 한계가 있겠지만, 만약 실제로 실무였다면 디자이너분과 이런 저런 예외 케이스들에 대한 이야기를 나누는 것이 중요하겠다는 걸 느꼈다. (당연) 내가 당연히 생각하는 예외 케이스가 모두에게 당연한 건 아니니까!
tailwind는 저렇게 다이나믹한 스타일에 대해서 좀 불편함이 많은 것 같다. styled-components나 emotion 같은 건 변수로 줘서 해결하면 되는데,,, 흐으음. 점점 편리함 원툴로 가는 tailwind... 그 편리함이 넘사긴 하지만...
처음에 설계는 라디오 버튼처럼 Group과 Item을 나누려고 했다. 저런 식으로 prop에 item array를 넘기는 것보다 직접 상위 객체에서 자식에게 넘겨주는 형태가 괜찮지 않나~? 싶은 생각이었다. 그런데 어쨌든 자식과 값을 공유해야 하는 부분이 있어서 이번엔 합쳐서 구현해보았다. 이게 나은 것 같기도 하고... 흠! 모르겠다!
자꾸 까먹는다(ㅋㅋ) Array나 Object에 direct key로 접근하게 되면 TS가 오류를 내뿜는다. 관련 링크
options?: Array<{
[index: number]: string;
name: string;
value: string;
}>;
잊지말고 추가해주기!
Bring natural elegance to your space with https://www.floresamerica.com/floristeria-bogota. Our handcrafted bouquets, made from Colombia’s freshest flowers, are perfect for brightening any day or special event. Experience the joy and beauty of our meticulously designed floral arrangements.