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-19
08-18
08-18
08-18
File name
Commit message
Commit date
08-19
"use client";
import type { ComponentPropsWithRef, ReactNode } from "react";
import { FoxInfoIcon, FoxQuestionIcon } from "../../icons";
import { cx } from "../../utils";
/** 무엇을 여는 단추인지. 아이콘과 기본 이름이 여기서 갈린다. */
export type FoxContextualTriggerType = "help" | "information";
/**
* 네이티브 `<button>`이 받는 것을 그대로 넘겨받는다 — onFocus·onBlur·onKeyDown·form 등.
* 툴팁을 띄우는 자리에서 `onFocus`·`onBlur`가 실제로 필요하다(마우스만 보면 키보드
* 사용자가 툴팁을 볼 수 없다). 의미가 겹치는 것만 걷어내고 아래에서 다시 정의한다.
*/
type NativeButtonProps = Omit<
ComponentPropsWithRef<"button">,
"type" | "className" | "children" | "hidden" | "onClick"
>;
export interface FoxContextualTriggerProps extends NativeButtonProps {
/** 무엇을 여는 단추인지. 네이티브 `type`은 언제나 `"button"`으로 고정된다. */
type?: FoxContextualTriggerType;
/**
* 읽어 줄 이름. 넘기지 않으면 계열이 정한 기본값을 쓴다(`help`는 "도움말",
* `information`은 "설명").
*
* 아이콘만 있는 단추라 이 글자가 **유일한 이름이다.** 한 화면에 여러 개가 있으면
* "배송비 도움말"처럼 무엇에 대한 것인지 적어 준다 — 같은 이름이 여럿이면 소리로는
* 구분되지 않는다.
*/
ariaLabel?: string;
/**
* 이 단추가 설명하는 내용의 id. **툴팁을 여는 자리에 쓴다** — 트리거가
* `aria-describedby`로 툴팁을 가리켜야 보조기술이 그 글을 읽어 준다(`FoxTooltip`·
* `FoxTooltipRich`는 스스로 아무 역할도 갖지 않는다).
*/
describedBy?: string;
/**
* 눌러서 여는 판이 있을 때 그 판의 id. **팝오버를 여는 자리에 쓴다.**
* `expanded`와 함께 넘긴다.
*/
controls?: string;
/** 눌러서 여는 판이 지금 열려 있는지(`aria-expanded`). 팝오버를 여는 자리에서만 쓴다. */
expanded?: boolean;
disabled?: boolean;
/** 참이면 렌더하지 않는다(DOM에 남지 않는다). */
hidden?: boolean;
/** 눌렀을 때. 팝오버처럼 눌러서 여는 판에 쓴다. */
onAction?: () => void;
/**
* 마우스가 올라오고 벗어날 때. **툴팁을 띄우고 지우는 자리에 쓴다** — 이 단추는 상태를
* 갖지 않으므로 무엇을 보일지는 호출부가 정한다.
*
* ⚠️ 마우스만 보면 키보드 사용자가 툴팁을 볼 수 없다. 호출부가 `onFocus`·`onBlur`도 함께
* 다뤄야 한다 — 네이티브 속성이라 그대로 넘기면 된다.
*/
onHoverChange?: (hovered: boolean) => void;
/** 배치 조정용. 모양이 달라야 하면 여기 말고 `type`을 쓴다. */
className?: string;
}
/** `Record`로 고정해 계열을 추가하면 항목 누락이 타입 에러가 되게 한다. */
const TYPE_CLASS: Record<FoxContextualTriggerType, string> = {
help: "fox-contextual-trigger--help",
information: "fox-contextual-trigger--information",
};
/** 계열이 정하는 아이콘. 시안 `ico/Question`·`ico/Info`이고 굵기는 둘 다 regular다. */
const TYPE_ICON: Record<FoxContextualTriggerType, ReactNode> = {
help: <FoxQuestionIcon />,
information: <FoxInfoIcon />,
};
/** 이름을 안 주면 쓰는 기본값. 무엇을 여는 단추인지까지만 말한다. */
const TYPE_LABEL: Record<FoxContextualTriggerType, string> = {
help: "도움말",
information: "설명",
};
/**
* @fox 도움말·설명 단추. 툴팁이나 팝오버를 여는 24px짜리 아이콘 단추다.
*
* 상태를 갖지 않는다 — 무엇을 언제 보여 줄지는 호출부가 정하고, 이 단추는 그 배선을 걸 자리
* (`describedBy`·`controls`·`expanded`)와 손이 닿는 자리를 준다.
*
* `FoxIconButton`과 다른 조각이다. 저쪽은 폼 컨트롤과 높이를 맞추는 단추라 크기가
* `form(height-*)`을 따르는데(가장 작은 xsm도 32px), 이쪽은 글줄 옆에 붙는 24px 고정이고
* 모서리도 `radius-2`(4px)다.
*
* 배경은 세 상태 모두 `action-secondary` 계열이다 — 기본이 투명이라 아래 배경이 그대로
* 비치고, hover·pressed에서만 연한 판이 깔린다(`FoxTag`와 같은 규칙).
*
* ⚠️ 스타일은 이 파일이 import하지 않는다 — 호스트 앱이 `@use "@fox/styles/components"`
* (또는 개별 파티셜)로 한 번 불러와야 한다.
*/
export function FoxContextualTrigger({
type = "help",
ariaLabel,
describedBy,
controls,
expanded,
disabled = false,
hidden = false,
onAction,
onHoverChange,
className,
...rest
}: FoxContextualTriggerProps) {
if (hidden) {
return null;
}
return (
<button
{...rest}
type="button"
disabled={disabled}
aria-label={ariaLabel ?? TYPE_LABEL[type]}
aria-describedby={describedBy}
aria-controls={controls}
aria-expanded={expanded}
className={cx("fox-contextual-trigger", TYPE_CLASS[type], className)}
onClick={onAction}
onMouseEnter={onHoverChange ? () => onHoverChange(true) : undefined}
onMouseLeave={onHoverChange ? () => onHoverChange(false) : undefined}
>
<span className="fox-contextual-trigger__icon" aria-hidden="true">
{TYPE_ICON[type]}
</span>
</button>
);
}