"use client"; import { useEffect, useId, useRef, useState, type KeyboardEvent, type ReactNode, type Ref, } from "react"; import { cx } from "../../utils"; import { FoxSelectOption } from "../fox-select-option"; import { FoxSelectOptionItem } from "../fox-select-option-item"; export type FoxSelectSize = "sm" | "md" | "lg"; /** 옵션 하나의 데이터. 그리는 일은 `FoxSelectOptionItem`이 한다. */ export interface FoxSelectItem { value: string; label: ReactNode; /** 옵션 라벨 뒤에 붙는 아이콘. 없으면 텍스트만 보여준다. */ icon?: ReactNode; /** 위험한 선택지(시안의 `state=danger`). 글자·아이콘이 danger 색으로 바뀐다. */ danger?: boolean; } export interface FoxSelectProps { size: FoxSelectSize; options: FoxSelectItem[]; /** 주면 제어 컴포넌트가 된다 — 호출부가 소유하고, select가 스스로 바꾸지 않는다. */ value?: string; /** * 비제어로 쓸 때의 초기 값. `value`를 주면 무시된다. * * 버튼 계열과 달리 값 상태를 안에 둘 수 있게 열어 둔 이유는, 폼 한 칸을 놓기 위해 * 호출부마다 `useState`를 반복하는 비용이 크기 때문이다(사용자 승인). */ defaultValue?: string; /** 값이 바뀔 때 호출된다. 이름이 `onChange`가 아닌 이유는 DOM 이벤트가 아니기 때문이다. */ onValueChange?: (value: string) => void; /** 아무 옵션도 선택되지 않았을 때 트리거에 보여줄 문구. */ placeholder?: ReactNode; /** Figma 시안의 error 상태. */ error?: boolean; disabled?: boolean; /** * Figma 시안의 view(조회 전용) 상태. 값은 보이되 바꿀 수 없다 — 내부적으로는 * `disabled`를 걸되, disabled(입력 불가 안내)와 시각적으로 구분해 보여준다. */ readOnly?: boolean; /** 필드 위 라벨. 넘기지 않으면 라벨을 렌더링하지 않는다. */ label?: ReactNode; /** select 아래 힌트 메시지. 넘기지 않으면 힌트 영역을 렌더링하지 않는다. */ hint?: ReactNode; /** 힌트 메시지 앞에 붙는 아이콘. `hint`가 없으면 의미 없다. */ hintIcon?: ReactNode; id?: string; /** 배치 조정용. 모양이 달라야 하면 여기 말고 모디파이어를 추가한다. */ className?: string; /** 참이면 렌더하지 않는다(DOM에 남지 않는다). */ hidden?: boolean; /** 포인터가 올라오면 `true`, 벗어나면 `false`. */ onHoverChange?: (hovered: boolean) => void; /** 트리거 버튼을 가리킨다 — 호출부가 포커스를 옮길 때 쓴다. */ ref?: Ref; } const SIZE_CLASS: Record = { sm: "fox-select--sm", md: "fox-select--md", lg: "fox-select--lg", }; /** * @fox 커스텀 드롭다운. 네이티브 `