File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
08-14
File name
Commit message
Commit date
08-14
"use client";
import { useRef, type ReactNode } from "react";
import { cx } from "../../utils";
import { FoxRadio, type FoxRadioSize } from "../fox-radio";
import { FoxSelect, type FoxSelectItem } from "../fox-select";
export type FoxConditionalOptionOrientation = "horizontal" | "vertical";
export interface FoxConditionalOptionProps {
/** 라디오 값. 같은 `name` 안에서 이 칸을 가리킨다. */
value: string;
label: ReactNode;
/** 라벨 아래 보조 설명. 넘기지 않으면 영역을 렌더링하지 않는다. */
description?: ReactNode;
/** 딸린 select의 목록. */
options: FoxSelectItem[];
/** 딸린 select의 값(제어). 넘기지 않으면 안에서 들고 있는다. */
detail?: string;
/** 비제어로 쓸 때 딸린 select의 초기 값. */
defaultDetail?: string;
/** 딸린 select 값이 바뀔 때 부른다. 값이 첫 인자다. */
onChange?: (detail: string) => void;
/** 이 칸이 선택될 때 부른다 — select를 골라 자동으로 켜진 경우도 포함이다. */
onCheck?: (value: string) => void;
/** 아무것도 고르지 않았을 때 select에 보여줄 문구. */
placeholder?: ReactNode;
orientation?: FoxConditionalOptionOrientation;
/** 라디오 묶음 이름. `FoxRadioGroup` 안에 넣으면 그룹이 넣어 주므로 직접 적지 않아도 된다. */
name?: string;
/** `FoxRadioGroup`이 넣어 준다. 직접 넣으면 그 값이 라디오 크기가 된다. */
size?: FoxRadioSize;
/** 라디오를 처음부터 선택된 채로 둔다(비제어). */
defaultChecked?: boolean;
/** 주면 선택 여부를 호출부가 소유한다. */
checked?: boolean;
disabled?: boolean;
/** 참이면 렌더하지 않는다(DOM에 남지 않는다). */
hidden?: boolean;
/** 배치 조정용. */
className?: string;
}
const ORIENTATION_CLASS: Record<FoxConditionalOptionOrientation, string> = {
horizontal: "fox-conditional-option--horizontal",
vertical: "fox-conditional-option--vertical",
};
/**
* @fox 후속 질문이 딸린 선택지. 라디오 한 칸과 select를 묶어 하나의 답으로 다룬다.
*
* select는 이 칸이 골라지지 않아도 **조작할 수 있다.** 값을 고르면 라디오가 자동으로 켜진다 —
* 값을 고르고도 라디오를 안 눌러 답이 비는 일이 흔해서다. 반대로 select를 잠가 두면 그 자동
* 선택 자체가 불가능해진다(`disabled`는 클릭을 먹지 않는다).
*
* 켜는 방법은 숨은 input을 실제로 클릭하는 것이다. 그래야 같은 `name`의 다른 칸이 꺼지는
* 것과 change 이벤트가 브라우저 규칙 그대로 일어난다 — `checked`를 직접 대입하면 이벤트가
* 뛰지 않아 호출부가 모른다.
*
* `FoxRadioGroup` 안에 그대로 넣을 수 있다 — 그룹이 `name`·`size`를 넣어 준다.
*
* ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"`
* (또는 개별 파티셜)로 한 번 불러와야 한다.
*/
export function FoxConditionalOption({
value,
label,
description,
options,
detail,
defaultDetail,
onChange,
onCheck,
placeholder,
orientation = "vertical",
name,
size = "lg",
defaultChecked,
checked,
disabled = false,
hidden = false,
className,
}: FoxConditionalOptionProps) {
const radioRef = useRef<HTMLInputElement>(null);
if (hidden) {
return null;
}
// 비제어일 때의 값은 `FoxSelect`가 이미 들고 있다 — 여기서 또 들면 두 벌이 어긋난다.
const handleDetail = (next: string) => {
onChange?.(next);
// 선택 여부는 DOM에서 읽는다 — 형제 칸을 고르면 이 칸이 조용히 꺼지므로 따로 들고 있으면
// 어긋난다. 이미 켜져 있으면 다시 클릭해 change를 중복으로 뛰우지 않는다.
if (radioRef.current && !radioRef.current.checked) {
radioRef.current.click();
}
};
return (
<div className={cx("fox-conditional-option", ORIENTATION_CLASS[orientation], className)}>
<FoxRadio
ref={radioRef}
name={name}
value={value}
label={label}
description={description}
size={size}
checked={checked}
defaultChecked={defaultChecked}
disabled={disabled}
onChange={onCheck}
/>
<div className="fox-conditional-option__detail">
<FoxSelect
size="md"
options={options}
value={detail}
defaultValue={defaultDetail}
placeholder={placeholder}
onValueChange={handleDetail}
disabled={disabled}
/>
</div>
</div>
);
}