"use client"; import { useId, type ReactNode, type Ref } from "react"; import { cx } from "../../utils"; import { FoxCheckbox } from "../fox-checkbox"; import { FoxCheckboxGroup } from "../fox-checkbox-group"; import { FoxRadio } from "../fox-radio"; import { FoxRadioGroup } from "../fox-radio-group"; export type FoxChipSelectionMode = "single" | "multiple"; /** 칩 목록의 항목 하나. 그리는 일은 `FoxCheckbox`·`FoxRadio`가 한다. */ export interface FoxChipItem { value: string; label: ReactNode; /** 이 항목만 고를 수 없게 한다. */ disabled?: boolean; } export interface FoxChipSelectOptionProps { /** 목록을 여는 요소의 `aria-controls`가 가리킬 값. */ id?: string; /** 목록에 이름을 주는 요소의 id — 보통 칩 자신이다. */ labelledBy?: string; /** * 하나만 고를지 여러 개를 고를지. 항목의 모양이 여기서 갈린다 — * `single`은 라디오 목록, `multiple`은 체크박스 목록이다. */ selectionMode?: FoxChipSelectionMode; options: FoxChipItem[]; /** * 라디오 묶음의 `name`. `single`에서만 쓰이고, 넘기지 않으면 자동으로 만든다. * * 같은 화면에 칩 목록이 둘 이상 열릴 일은 없지만, `name`이 겹치면 브라우저가 한 묶음으로 * 보고 서로를 끄기 때문에 자동 생성이라도 반드시 있어야 한다. */ name?: string; /** 지금 켜진 값들. `single`이면 길이가 0 또는 1이다 — 두 계열의 상태를 한 모양으로 받는다. */ values?: string[]; /** 항목을 켜거나 끌 때 호출된다. 무엇을 어떻게 반영할지는 받는 쪽이 정한다. */ onSelect?: (value: string) => void; /** * 모바일에만 보이는 확인 버튼의 글자. 버튼 자체는 언제나 렌더되고 **CSS가 데스크톱에서 * 숨긴다** — 조건부 렌더로 하면 창 폭이 바뀔 때마다 DOM이 들락거려서, 열린 채로 화면을 * 돌리면 포커스가 날아간다. */ confirmLabel?: string; /** 확인 버튼을 눌렀을 때. 보통 목록을 닫는다. */ onConfirm?: () => void; /** * 항목(체크박스·라디오)의 크기. 여는 칩의 크기를 그대로 받는다 — 두 컴포넌트의 크기 * 이름이 같고 글자 크기도 같은 값이라(lg 17px, md 15px) 그대로 이어진다. 칩만 크고 * 목록은 작으면 같은 필터인데 두 벌처럼 보인다. */ size?: "lg" | "md"; /** 배치 조정용. 모양이 달라야 하면 여기 말고 모디파이어를 추가한다. */ className?: string; ref?: Ref; } /** * @fox 칩의 `select`가 열렸을 때 뜨는 목록. 배치만 책임지고 상태를 갖지 않는다 — * 열림 여부·고른 값은 전부 목록을 여는 칩이 소유한다. * * `FoxSelectOption`과 **다른 컴포넌트다.** 저쪽은 `role="listbox"` + `role="option"`이고 * 항목이 글자 한 줄이지만, 이쪽은 시안이 **체크박스·라디오 목록**이라 이미 있는 * `FoxCheckboxGroup`·`FoxRadioGroup`을 그대로 쓴다. 네이티브 입력이라 단일 선택의 * 화살표 이동·폼 전송을 브라우저가 처리하고, 항목 모양이 다른 화면의 체크박스와 어긋나지 * 않는다. * * 항목이 진짜 입력이라 `FoxSelectOption`처럼 `mousedown`을 막지 않는다 — 눌러서 그 입력에 * 포커스가 가는 것이 맞다. 목록이 닫히지 않는 이유는 포커스가 여는 쪽과 **같은 조상 안**에 * 머물러서다(칩의 focusout 처리가 그것을 본다). * * ⚠️ 스스로 위치를 잡지 못한다 — `position: absolute`라 위치 기준이 되는 조상이 필요하다. * `FoxChip` 안에서는 `fox-chip-select`가 그 역할을 한다. * * ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"` * (또는 개별 파티셜)로 한 번 불러와야 한다. */ export function FoxChipSelectOption({ id, labelledBy, selectionMode = "single", options, name, values = [], onSelect, confirmLabel = "확인", onConfirm, size = "md", className, ref, }: FoxChipSelectOptionProps) { const generatedName = useId(); const groupName = name ?? generatedName; const isMultiple = selectionMode === "multiple"; return (
{isMultiple ? ( {options.map((option) => ( onSelect?.(option.value)} /> ))} ) : ( {options.map((option) => ( onSelect?.(option.value)} /> ))} )}
{/* 확인 버튼은 **다중 선택에만** 있다. 이 버튼이 하는 일은 "고르기를 마쳤다"고 알려 목록을 닫는 것인데, 단일 선택은 하나 고르는 순간 목록이 닫히므로 버튼이 할 일이 없다 — 남겨 두면 눌러도 아무 일이 없는 버튼이 모바일 화면 아래를 차지한다. 값이 반영되는 시점은 플랫폼과 무관하게 같다(고르는 즉시). 모바일에서 달라지는 것은 **닫는 수단이 하나 더 생기는 것**뿐이고, 데스크톱에서는 CSS가 이 버튼을 숨긴다. */} {isMultiple ? ( ) : null}
); }