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-18
08-14
08-18
08-18
08-18
08-18
File name
Commit message
Commit date
"use client";
import type { ReactNode, Ref } from "react";
import { cx } from "../../utils";
import { FoxSpinner } from "../fox-spinner";
export type FoxButtonType = "primary" | "secondary" | "default" | "error";
export type FoxButtonSize = "lg" | "md" | "sm" | "xsm";
export interface FoxButtonProps {
type?: FoxButtonType;
size: FoxButtonSize;
/** 네이티브 `type` — prop의 `type`이 시각 계열을 쓰므로 분리했다. */
htmlType?: "button" | "submit" | "reset";
/** 배치 조정용. 모양이 달라야 하면 여기 말고 `type`을 추가한다. */
className?: string;
/** `currentColor`로 그린 SVG여야 계열별 색이 적용된다. 크기는 슬롯이 정한다. */
leadingIcon?: ReactNode;
trailingIcon?: ReactNode;
label: string;
disabled?: boolean;
/** 참이면 렌더하지 않는다(DOM에 남지 않는다). */
hidden?: boolean;
/** 호출부가 소유한다 — 버튼이 스스로 켜지 않는다. */
loading?: boolean;
fullWidth?: boolean;
onAction?: () => void;
/** 포인터가 올라오면 `true`, 벗어나면 `false`. */
onHoverChange?: (hovered: boolean) => void;
ref?: Ref<HTMLButtonElement>;
}
/** `Record`로 고정해 계열을 추가하면 항목 누락이 타입 에러가 되게 한다. */
const TYPE_CLASS: Record<FoxButtonType, string> = {
primary: "fox-button--primary",
secondary: "fox-button--secondary",
default: "fox-button--default",
error: "fox-button--error",
};
const SIZE_CLASS: Record<FoxButtonSize, string> = {
lg: "fox-button--lg",
md: "fox-button--md",
sm: "fox-button--sm",
xsm: "fox-button--xsm",
};
/**
* @fox 공용 버튼. 상태를 갖지 않는다.
*
* ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"`
* (또는 개별 파티셜)로 한 번 불러와야 한다.
*/
export function FoxButton({
type = "default",
size,
htmlType = "button",
className,
leadingIcon,
trailingIcon,
label,
disabled = false,
hidden = false,
loading = false,
fullWidth = false,
onAction,
onHoverChange,
ref,
}: FoxButtonProps) {
if (hidden) {
return null;
}
// 같은 요청이 두 번 나가지 않게 로딩 중에도 막는다.
const inactive = disabled || loading;
return (
<button
ref={ref}
type={htmlType}
className={cx(
"fox-button",
TYPE_CLASS[type],
SIZE_CLASS[size],
fullWidth && "fox-button--full-width",
className
)}
disabled={inactive}
aria-busy={loading || undefined}
onClick={inactive ? undefined : onAction}
onMouseEnter={onHoverChange ? () => onHoverChange(true) : undefined}
onMouseLeave={onHoverChange ? () => onHoverChange(false) : undefined}
>
{loading ? (
// 아이콘과 같은 크기라 전환 시 폭이 흔들리지 않는다. 이름은 버튼이 `aria-busy`로
// 이미 알리므로 스피너는 잠재운다.
<span className="fox-button__spinner" aria-hidden="true">
<FoxSpinner size="100%" label="" />
</span>
) : (
leadingIcon && (
<span className="fox-button__icon" aria-hidden="true">
{leadingIcon}
</span>
)
)}
<span className="fox-button__label">{label}</span>
{trailingIcon && (
<span className="fox-button__icon" aria-hidden="true">
{trailingIcon}
</span>
)}
</button>
);
}