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
08-14
08-18
08-18
08-14
08-18
08-18
08-18
File name
Commit message
Commit date
"use client";
import type { ChangeEvent, ComponentPropsWithRef, ReactNode } from "react";
import { cx } from "../../utils";
export type FoxToggleSwitchSize = "lg" | "md";
/**
* 네이티브 `<input type="checkbox">`가 받는 것을 그대로 넘겨받는다 — name·value·checked·
* defaultChecked·disabled·required·form 등. 의미가 겹치는 것만 걷어내고 아래에서 다시 정의한다.
* `role`도 걷어낸다 — 스위치로 읽히는 것이 이 컴포넌트의 정체라 호출부가 바꿀 값이 아니다.
*/
type NativeCheckboxProps = Omit<
ComponentPropsWithRef<"input">,
"type" | "role" | "size" | "className" | "children" | "hidden" | "onChange"
>;
export interface FoxToggleSwitchProps extends NativeCheckboxProps {
size?: FoxToggleSwitchSize;
/**
* 스위치 옆 글자. 넘기지 않으면 영역을 렌더링하지 않는다.
*
* ⚠️ 생략하면 스위치를 읽어 줄 이름이 없어진다 — 그때는 `aria-label`을 함께 넘긴다
* (네이티브 속성이라 그대로 전달된다).
*/
label?: ReactNode;
/**
* 켜지거나 꺼질 때 부른다. 켜짐 여부가 첫 인자다 — 네이티브 `onChange`를 `Omit`으로
* 걷어내고 그 자리를 대신한다(`FoxCheckbox`와 같은 방식).
*/
onChange?: (checked: boolean, event: ChangeEvent<HTMLInputElement>) => void;
/** 참이면 렌더하지 않는다(DOM에 남지 않는다). */
hidden?: boolean;
/** 배치 조정용. */
className?: string;
}
const SIZE_CLASS: Record<FoxToggleSwitchSize, string> = {
lg: "fox-toggle-switch--lg",
md: "fox-toggle-switch--md",
};
/**
* @fox 토글 스위치. 켜짐 여부는 네이티브 `<input type="checkbox">`가 소유한다.
*
* 체크박스와 마크업은 같지만 `role="switch"`를 얹어 "선택됨"이 아니라 "켜짐/꺼짐"으로 읽히게
* 한다. 폼에도 체크박스처럼 참여한다 — 켜져 있으면 `value`가 함께 전송된다.
*
* 루트가 `<label>`이라 글자를 눌러도 바뀐다. 동그라미는 꺼짐이면 왼쪽, 켜짐이면 오른쪽이고
* X·체크 표시를 둘 다 그려 두고 CSS가 골라 보여 준다 — 켜졌을 때만 넣으면 비제어 입력에서
* React가 상태를 몰라 어긋난다.
*
* ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"`
* (또는 개별 파티셜)로 한 번 불러와야 한다.
*/
export function FoxToggleSwitch({
size = "lg",
label,
onChange,
hidden = false,
className,
...rest
}: FoxToggleSwitchProps) {
if (hidden) {
return null;
}
// 끌 때도 change가 뛴다. 그래서 켜짐 여부를 그대로 넘긴다.
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
onChange?.(event.target.checked, event);
};
return (
<label className={cx("fox-toggle-switch", SIZE_CLASS[size], className)}>
{/* 형제 선택자(`~`)가 뒤따르는 트랙과 글자에 닿아야 하므로 맨 앞에 둔다. */}
<input
{...rest}
type="checkbox"
role="switch"
className="fox-toggle-switch__input"
onChange={handleChange}
/>
<span className="fox-toggle-switch__track">
<span className="fox-toggle-switch__thumb">
<XMark />
<CheckMark />
</span>
</span>
{label === undefined ? null : (
<span className="fox-toggle-switch__label">{label}</span>
)}
</label>
);
}
// 시안 아이콘 둘. `fill`을 시안의 색 리터럴(#6D7882·#256EF4·#CDD1D5) 대신 `currentColor`로
// 두면 트랙 규칙이 정한 색을 그대로 물려받는다 — 시안에서 표시 색은 언제나 트랙 배경과 같은
// 값이라, 상태가 셋(꺼짐·켜짐·비활성)이어도 SVG는 한 벌로 끝난다.
// `width`·`height`를 쓰지 않는다 — 크기는 CSS가 정한다.
// 꺼짐 표시.
function XMark() {
return (
<svg
className="fox-toggle-switch__mark-off"
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
>
<path
d="M13.0306 11.9694C13.1715 12.1103 13.2507 12.3014 13.2507 12.5006C13.2507 12.6999 13.1715 12.891 13.0306 13.0319C12.8897 13.1728 12.6986 13.2519 12.4994 13.2519C12.3001 13.2519 12.109 13.1728 11.9681 13.0319L8 9.06249L4.03063 13.0306C3.88973 13.1715 3.69864 13.2507 3.49938 13.2507C3.30012 13.2507 3.10902 13.1715 2.96813 13.0306C2.82723 12.8897 2.74808 12.6986 2.74808 12.4994C2.74808 12.3001 2.82723 12.109 2.96813 11.9681L6.9375 7.99999L2.96938 4.03061C2.82848 3.88972 2.74933 3.69862 2.74933 3.49936C2.74933 3.30011 2.82848 3.10901 2.96938 2.96811C3.11027 2.82722 3.30137 2.74806 3.50063 2.74806C3.69989 2.74806 3.89098 2.82722 4.03188 2.96811L8 6.93749L11.9694 2.96749C12.1103 2.82659 12.3014 2.74744 12.5006 2.74744C12.6999 2.74744 12.891 2.82659 13.0319 2.96749C13.1728 3.10838 13.2519 3.29948 13.2519 3.49874C13.2519 3.69799 13.1728 3.88909 13.0319 4.02999L9.0625 7.99999L13.0306 11.9694Z"
fill="currentColor"
/>
</svg>
);
}
// 켜짐 표시.
function CheckMark() {
return (
<svg
className="fox-toggle-switch__mark-on"
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
>
<path
d="M14.5306 5.03057L6.53063 13.0306C6.46095 13.1005 6.37816 13.156 6.28699 13.1938C6.19583 13.2317 6.09809 13.2512 5.99938 13.2512C5.90067 13.2512 5.80293 13.2317 5.71176 13.1938C5.6206 13.156 5.53781 13.1005 5.46813 13.0306L1.96813 9.53057C1.89836 9.4608 1.84302 9.37798 1.80527 9.28683C1.76751 9.19568 1.74808 9.09798 1.74808 8.99932C1.74808 8.90066 1.76751 8.80296 1.80527 8.71181C1.84302 8.62066 1.89836 8.53783 1.96813 8.46807C2.03789 8.3983 2.12072 8.34296 2.21187 8.30521C2.30302 8.26745 2.40072 8.24802 2.49938 8.24802C2.59804 8.24802 2.69574 8.26745 2.78689 8.30521C2.87804 8.34296 2.96086 8.3983 3.03063 8.46807L6 11.4374L13.4694 3.96932C13.6103 3.82842 13.8014 3.74927 14.0006 3.74927C14.1999 3.74927 14.391 3.82842 14.5319 3.96932C14.6728 4.11021 14.7519 4.30131 14.7519 4.50057C14.7519 4.69983 14.6728 4.89092 14.5319 5.03182L14.5306 5.03057Z"
fill="currentColor"
/>
</svg>
);
}