refactor: 폼 라벨을 FoxFormLabel로 일원화
develop에서 온 FoxFormLabel과 내가 두었던 form-field의 label 믹스인이 같은 일을 두 갈래로 하고 있었다. 라벨 모양을 아는 곳을 하나로 줄인다. - FoxFormLabel에 `as` 프롭 추가 — 칸 하나를 가리킬 때는 label, 칸 여러 개를 묶은 필드에서는 span. 가리킬 대상이 하나가 아니라 htmlFor를 쓸 수 없고, 아무것도 가리키지 않는
@7b3a7e5c93cbfdae977abaee1f314d2a55236448
--- @fox/core/components/fox-address/fox-address.tsx
+++ @fox/core/components/fox-address/fox-address.tsx
... | ... | @@ -1,6 +1,7 @@ |
| 1 | 1 |
"use client"; |
| 2 | 2 |
|
| 3 | 3 |
import { useId, useState, type ReactNode } from "react";
|
| 4 |
+import { FoxFormLabel } from "../fox-form-label";
|
|
| 4 | 5 |
import { cx } from "../../utils";
|
| 5 | 6 |
import { FoxButton } from "../fox-button";
|
| 6 | 7 |
import { FoxInput } from "../fox-input";
|
... | ... | @@ -131,11 +132,15 @@ |
| 131 | 132 |
}; |
| 132 | 133 |
|
| 133 | 134 |
return ( |
| 134 |
- <div className={cx("fox-address", className)}>
|
|
| 135 |
+ <div |
|
| 136 |
+ className={cx("fox-address", className)}
|
|
| 137 |
+ role="group" |
|
| 138 |
+ aria-labelledby={label ? `${autoId}-label` : undefined}
|
|
| 139 |
+ > |
|
| 135 | 140 |
{label && (
|
| 136 |
- <span className="fox-address__label" id={`${autoId}-label`}>
|
|
| 141 |
+ <FoxFormLabel as="span" id={`${autoId}-label`}>
|
|
| 137 | 142 |
{label}
|
| 138 |
- </span> |
|
| 143 |
+ </FoxFormLabel> |
|
| 139 | 144 |
)} |
| 140 | 145 |
|
| 141 | 146 |
<div className="fox-address__row"> |
--- @fox/core/components/fox-certified/fox-certified.tsx
+++ @fox/core/components/fox-certified/fox-certified.tsx
... | ... | @@ -1,6 +1,7 @@ |
| 1 | 1 |
"use client"; |
| 2 | 2 |
|
| 3 | 3 |
import { useEffect, useId, useRef, useState, type ChangeEvent } from "react";
|
| 4 |
+import { FoxFormLabel } from "../fox-form-label";
|
|
| 4 | 5 |
import { cx } from "../../utils";
|
| 5 | 6 |
import { FoxButton } from "../fox-button";
|
| 6 | 7 |
import { FoxPhoneNumber } from "../fox-phone-number";
|
... | ... | @@ -190,11 +191,15 @@ |
| 190 | 191 |
}; |
| 191 | 192 |
|
| 192 | 193 |
return ( |
| 193 |
- <div className={cx("fox-certified", className)}>
|
|
| 194 |
+ <div |
|
| 195 |
+ className={cx("fox-certified", className)}
|
|
| 196 |
+ role="group" |
|
| 197 |
+ aria-labelledby={label ? `${autoId}-label` : undefined}
|
|
| 198 |
+ > |
|
| 194 | 199 |
{label && (
|
| 195 |
- <span className="fox-certified__label" id={`${autoId}-label`}>
|
|
| 200 |
+ <FoxFormLabel as="span" id={`${autoId}-label`}>
|
|
| 196 | 201 |
{label}
|
| 197 |
- </span> |
|
| 202 |
+ </FoxFormLabel> |
|
| 198 | 203 |
)} |
| 199 | 204 |
|
| 200 | 205 |
<div className="fox-certified__row"> |
--- @fox/core/components/fox-email/fox-email.tsx
+++ @fox/core/components/fox-email/fox-email.tsx
... | ... | @@ -1,6 +1,7 @@ |
| 1 | 1 |
"use client"; |
| 2 | 2 |
|
| 3 | 3 |
import { useId, useState, type ReactNode } from "react";
|
| 4 |
+import { FoxFormLabel } from "../fox-form-label";
|
|
| 4 | 5 |
import { cx } from "../../utils";
|
| 5 | 6 |
import { FoxButton } from "../fox-button";
|
| 6 | 7 |
import { FoxInput } from "../fox-input";
|
... | ... | @@ -156,11 +157,15 @@ |
| 156 | 157 |
}; |
| 157 | 158 |
|
| 158 | 159 |
return ( |
| 159 |
- <div className={cx("fox-email", className)}>
|
|
| 160 |
+ <div |
|
| 161 |
+ className={cx("fox-email", className)}
|
|
| 162 |
+ role="group" |
|
| 163 |
+ aria-labelledby={label ? `${autoId}-label` : undefined}
|
|
| 164 |
+ > |
|
| 160 | 165 |
{label && (
|
| 161 |
- <span className="fox-email__label" id={`${autoId}-label`}>
|
|
| 166 |
+ <FoxFormLabel as="span" id={`${autoId}-label`}>
|
|
| 162 | 167 |
{label}
|
| 163 |
- </span> |
|
| 168 |
+ </FoxFormLabel> |
|
| 164 | 169 |
)} |
| 165 | 170 |
|
| 166 | 171 |
<div className="fox-email__row"> |
--- @fox/core/components/fox-file-upload/fox-file-upload.tsx
+++ @fox/core/components/fox-file-upload/fox-file-upload.tsx
... | ... | @@ -9,6 +9,7 @@ |
| 9 | 9 |
type CSSProperties, |
| 10 | 10 |
type DragEvent, |
| 11 | 11 |
} from "react"; |
| 12 |
+import { FoxFormLabel } from "../fox-form-label";
|
|
| 12 | 13 |
import { cx } from "../../utils";
|
| 13 | 14 |
import { FoxButton } from "../fox-button";
|
| 14 | 15 |
import { FoxIconButton } from "../fox-icon-button";
|
... | ... | @@ -310,14 +311,16 @@ |
| 310 | 311 |
return ( |
| 311 | 312 |
<div |
| 312 | 313 |
className={cx("fox-file-upload", mode === "area" && "fox-file-upload--area", className)}
|
| 314 |
+ role="group" |
|
| 315 |
+ aria-labelledby={label ? `${autoId}-label` : undefined}
|
|
| 313 | 316 |
onDragOver={handleDragOver}
|
| 314 | 317 |
onDragLeave={handleDragLeave}
|
| 315 | 318 |
onDrop={handleDrop}
|
| 316 | 319 |
> |
| 317 | 320 |
{label && (
|
| 318 |
- <span className="fox-file-upload__label" id={`${autoId}-label`}>
|
|
| 321 |
+ <FoxFormLabel as="span" id={`${autoId}-label`}>
|
|
| 319 | 322 |
{label}
|
| 320 |
- </span> |
|
| 323 |
+ </FoxFormLabel> |
|
| 321 | 324 |
)} |
| 322 | 325 |
|
| 323 | 326 |
{/* 파일 고르기는 네이티브 입력이 맡고 화면에는 버튼만 보인다. */}
|
--- @fox/core/components/fox-form-label/fox-form-label.tsx
+++ @fox/core/components/fox-form-label/fox-form-label.tsx
... | ... | @@ -16,6 +16,13 @@ |
| 16 | 16 |
type NativeLabelProps = Omit<ComponentPropsWithRef<"label">, "className" | "hidden">; |
| 17 | 17 |
|
| 18 | 18 |
export interface FoxFormLabelProps extends NativeLabelProps {
|
| 19 |
+ /** |
|
| 20 |
+ * 그릴 태그. 칸 하나를 가리킬 때는 `label`이고, 칸 여러 개를 묶은 필드 |
|
| 21 |
+ * (전화번호·주소처럼)에서는 `span`이다 — 가리킬 대상이 하나가 아니라 `htmlFor`를 쓸 수 |
|
| 22 |
+ * 없고, 아무것도 가리키지 않는 `<label>`을 두면 의미가 어긋난다. 그때 이름 연결은 |
|
| 23 |
+ * 감싼 쪽이 `aria-labelledby`로 한다. |
|
| 24 |
+ */ |
|
| 25 |
+ as?: "label" | "span"; |
|
| 19 | 26 |
requirement?: FoxFormLabelRequirement; |
| 20 | 27 |
/** "(선택)" 대신 쓸 문구. */ |
| 21 | 28 |
optionalText?: string; |
... | ... | @@ -36,6 +43,7 @@ |
| 36 | 43 |
* (또는 개별 파티셜)로 한 번 불러와야 한다. |
| 37 | 44 |
*/ |
| 38 | 45 |
export function FoxFormLabel({
|
| 46 |
+ as: Tag = "label", |
|
| 39 | 47 |
requirement, |
| 40 | 48 |
optionalText = "(선택)", |
| 41 | 49 |
hidden = false, |
... | ... | @@ -48,7 +56,7 @@ |
| 48 | 56 |
} |
| 49 | 57 |
|
| 50 | 58 |
return ( |
| 51 |
- <label {...rest} className={cx("fox-form-label", className)}>
|
|
| 59 |
+ <Tag {...rest} className={cx("fox-form-label", className)}>
|
|
| 52 | 60 |
{children}
|
| 53 | 61 |
{requirement === "required" ? (
|
| 54 | 62 |
<span className="fox-form-label__required" aria-hidden="true"> |
... | ... | @@ -58,6 +66,6 @@ |
| 58 | 66 |
{requirement === "optional" ? (
|
| 59 | 67 |
<span className="fox-form-label__optional">{optionalText}</span>
|
| 60 | 68 |
) : null} |
| 61 |
- </label> |
|
| 69 |
+ </Tag> |
|
| 62 | 70 |
); |
| 63 | 71 |
} |
--- @fox/core/components/fox-phone-number/fox-phone-number.tsx
+++ @fox/core/components/fox-phone-number/fox-phone-number.tsx
... | ... | @@ -9,6 +9,7 @@ |
| 9 | 9 |
type KeyboardEvent, |
| 10 | 10 |
type ReactNode, |
| 11 | 11 |
} from "react"; |
| 12 |
+import { FoxFormLabel } from "../fox-form-label";
|
|
| 12 | 13 |
import { cx } from "../../utils";
|
| 13 | 14 |
import { FoxInput, type FoxInputState } from "../fox-input";
|
| 14 | 15 |
import { FoxSelect, type FoxSelectItem } from "../fox-select";
|
... | ... | @@ -269,9 +270,9 @@ |
| 269 | 270 |
aria-labelledby={label ? labelId : undefined}
|
| 270 | 271 |
> |
| 271 | 272 |
{label && (
|
| 272 |
- <span className="fox-phone-number__label" id={labelId}>
|
|
| 273 |
+ <FoxFormLabel as="span" id={labelId}>
|
|
| 273 | 274 |
{label}
|
| 274 |
- </span> |
|
| 275 |
+ </FoxFormLabel> |
|
| 275 | 276 |
)} |
| 276 | 277 |
|
| 277 | 278 |
{body}
|
--- @fox/styles/_form-field.scss
+++ @fox/styles/_form-field.scss
... | ... | @@ -1,8 +1,10 @@ |
| 1 | 1 |
// 입력 계열 공용 조각. |
| 2 | 2 |
// |
| 3 |
-// 시안에서 form-label · helper-message · 입력 상자가 모두 같은 공용 컴포넌트라 |
|
| 3 |
+// 시안에서 helper-message · 입력 상자가 모두 같은 공용 컴포넌트라 |
|
| 4 | 4 |
// FoxInput · FoxTextArea · FoxPhoneNumber · FoxCertified · FoxEmail · FoxAddress · |
| 5 | 5 |
// FoxFileUpload가 같은 선언을 되풀이했다. 그 되풀이를 여기로 모은다. |
| 6 |
+// |
|
| 7 |
+// 라벨은 여기 없다 — FoxFormLabel이 컴포넌트로 갖는다. |
|
| 6 | 8 |
// |
| 7 | 9 |
// 믹스인만 있고 규칙이 없어 @use 해도 CSS가 늘지 않는다 — 부르는 자리에서만 나온다. |
| 8 | 10 |
// 그래서 파티셜을 따로 쓰든 묶음(components.scss)으로 쓰든 출력은 그대로다. |
... | ... | @@ -22,17 +24,6 @@ |
| 22 | 24 |
flex-direction: column; |
| 23 | 25 |
gap: fox.gap($gap); |
| 24 | 26 |
inline-size: 100%; |
| 25 |
-} |
|
| 26 |
- |
|
| 27 |
-/// 시안 form-label. 입력 크기와 무관하게 항상 label/md다(공용 컴포넌트라 크기를 따라가지 |
|
| 28 |
-/// 않는다). |
|
| 29 |
-@mixin label {
|
|
| 30 |
- color: fox.color(font-neutral-default); |
|
| 31 |
- font-family: fox.font-family(body); |
|
| 32 |
- font-size: fox.font-size(label-md); |
|
| 33 |
- font-weight: fox.font-weight(medium); |
|
| 34 |
- line-height: 1; |
|
| 35 |
- letter-spacing: -0.025em; |
|
| 36 | 27 |
} |
| 37 | 28 |
|
| 38 | 29 |
/// 시안 helper-message. 오류·포커스 색은 상태 규칙이 덮는다. |
--- @fox/styles/_fox-address.scss
+++ @fox/styles/_fox-address.scss
... | ... | @@ -27,9 +27,6 @@ |
| 27 | 27 |
.fox-address {
|
| 28 | 28 |
@include field.root; |
| 29 | 29 |
|
| 30 |
- &__label {
|
|
| 31 |
- @include field.label; |
|
| 32 |
- } |
|
| 33 | 30 |
|
| 34 | 31 |
// 우편번호는 남는 폭을 먹고 버튼은 글자만큼만 차지한다. |
| 35 | 32 |
&__row {
|
--- @fox/styles/_fox-certified.scss
+++ @fox/styles/_fox-certified.scss
... | ... | @@ -41,9 +41,6 @@ |
| 41 | 41 |
.fox-certified {
|
| 42 | 42 |
@include field.root; |
| 43 | 43 |
|
| 44 |
- &__label {
|
|
| 45 |
- @include field.label; |
|
| 46 |
- } |
|
| 47 | 44 |
|
| 48 | 45 |
// 전화번호는 남는 폭을 먹고 버튼은 글자만큼만 차지한다. |
| 49 | 46 |
&__row {
|
--- @fox/styles/_fox-email.scss
+++ @fox/styles/_fox-email.scss
... | ... | @@ -29,9 +29,6 @@ |
| 29 | 29 |
.fox-email {
|
| 30 | 30 |
@include field.root; |
| 31 | 31 |
|
| 32 |
- &__label {
|
|
| 33 |
- @include field.label; |
|
| 34 |
- } |
|
| 35 | 32 |
|
| 36 | 33 |
// 좁아지면 칸이 찌그러지는 대신 줄이 넘어간다. |
| 37 | 34 |
&__row {
|
--- @fox/styles/_fox-file-upload.scss
+++ @fox/styles/_fox-file-upload.scss
... | ... | @@ -37,9 +37,6 @@ |
| 37 | 37 |
gap: fox.gap(7); |
| 38 | 38 |
} |
| 39 | 39 |
|
| 40 |
- &__label {
|
|
| 41 |
- @include field.label; |
|
| 42 |
- } |
|
| 43 | 40 |
|
| 44 | 41 |
&__description {
|
| 45 | 42 |
margin: 0; |
--- @fox/styles/_fox-form-label.scss
+++ @fox/styles/_fox-form-label.scss
... | ... | @@ -10,8 +10,8 @@ |
| 10 | 10 |
// <span class="fox-form-label__optional">(선택)</span> <!-- 선택일 때 --> |
| 11 | 11 |
// </label> |
| 12 | 12 |
// |
| 13 |
-// 글자 자체는 `form-field`의 `label` 믹스인이 갖는다 — 시안에서 이 컴포넌트와 입력 계열의 |
|
| 14 |
-// 라벨이 같은 공용 조각이라 값이 갈라질 이유가 없다. 이 파일에는 표시 두 개만 남는다. |
|
| 13 |
+// 시안에서 이 컴포넌트와 입력 계열의 라벨이 같은 공용 조각이라, 라벨 모양은 여기 한 곳에만 |
|
| 14 |
+// 둔다. 칸을 여러 개 묶은 필드(전화번호·주소 등)도 자기 라벨을 그리지 않고 이 클래스를 쓴다. |
|
| 15 | 15 |
// |
| 16 | 16 |
// 필수의 `*`는 `aria-hidden`이다. 필수 여부는 입력 요소의 `required`(또는 `aria-required`)가 |
| 17 | 17 |
// 알리는 것이고, 별표는 그 눈에 보이는 짝일 뿐이라 두 번 읽히면 안 된다. 반대로 "(선택)"은 |
... | ... | @@ -25,13 +25,16 @@ |
| 25 | 25 |
// - display / align-items: 구조 규칙. |
| 26 | 26 |
|
| 27 | 27 |
@use "@fox/styles/abstracts" as fox; |
| 28 |
-@use "@fox/styles/form-field" as field; |
|
| 29 | 28 |
|
| 30 | 29 |
.fox-form-label {
|
| 31 |
- @include field.label; |
|
| 32 |
- |
|
| 33 | 30 |
display: inline-flex; |
| 34 | 31 |
align-items: center; |
| 32 |
+ color: fox.color(font-neutral-default); |
|
| 33 |
+ font-family: fox.font-family(body); |
|
| 34 |
+ font-size: fox.font-size(label-md); |
|
| 35 |
+ font-weight: fox.font-weight(medium); |
|
| 36 |
+ line-height: 1; |
|
| 37 |
+ letter-spacing: -0.025em; |
|
| 35 | 38 |
|
| 36 | 39 |
// 시안 label/md-em — 기본 라벨보다 굵고 강조 색이다. |
| 37 | 40 |
&__required {
|
--- @fox/styles/_fox-phone-number.scss
+++ @fox/styles/_fox-phone-number.scss
... | ... | @@ -44,9 +44,6 @@ |
| 44 | 44 |
.fox-phone-number {
|
| 45 | 45 |
@include field.root; |
| 46 | 46 |
|
| 47 |
- &__label {
|
|
| 48 |
- @include field.label; |
|
| 49 |
- } |
|
| 50 | 47 |
|
| 51 | 48 |
// ── unit ────────────────────────────────────────────────────────────────── |
| 52 | 49 |
&__row {
|
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?