Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
## [0.19.2] - 2026-07-14

**Branch**: `ui-v0.19.2-Field접근성개선`
### 수정
- fix: Field라벨htmlFor분리및aria속성자동주입으로접근성보강

### 변경
- chore: v0.19.2버전범프

---
## [0.19.1] - 2026-07-14

**Branch**: `ui-v0.19.1-레이아웃및토큰체계정비`
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@youngduck/yd-ui",
"version": "0.19.1",
"version": "0.19.2",
"type": "module",
"main": "./dist/index.cjs.js",
"module": "./dist/index.esm.js",
Expand Down
2 changes: 1 addition & 1 deletion src/components/Layouts/Field/Field.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ YD-UI 디자인 시스템의 폼 필드 컴포넌트입니다.
- \`label\` + 임의의 입력(children) + \`description\` / \`error\` 메시지 구조
- \`error\` 지정 시 설명 대신 에러가 표시되고 \`role="alert"\` 로 스크린 리더에 즉시 전달
- \`required\` 지정 시 라벨 옆에 필수 표시(*)
- label 요소로 감싸므로 네이티브 input 은 라벨 클릭 시 자동 포커스
- 단일 입력 요소에 \`id\` / \`aria-describedby\` / \`aria-invalid\` 자동 주입 — 라벨은 \`htmlFor\` 로 연결되어 클릭 시 포커스, 스크린 리더가 설명·에러를 함께 읽음
- 라벨·메시지의 색상/타이포그래피/간격은 모두 디자인 토큰에서 일괄 적용

## 사용 가이드
Expand Down
46 changes: 36 additions & 10 deletions src/components/Layouts/Field/Field.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
/**
* 작성자: KYD
* 기능: 폼 입력 하나를 라벨 + 입력 + 설명/에러 메시지의 표준 구조로 묶는 컴포넌트
* 프로세스 설명: label 요소로 감싸므로 네이티브 input 은 라벨 클릭 시 자동으로 포커스됩니다
* 프로세스 설명: label 은 htmlFor 로 입력과 연결하고(블록 요소 children 을 label 로 감싸는 HTML 표준 위반 방지),
* 단일 입력 요소에는 id / aria-describedby / aria-invalid 를 자동 주입해 스크린 리더가 설명·에러를 읽을 수 있게 합니다
*/

import { useId } from 'react'
import { useId, Children, isValidElement, cloneElement } from 'react'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

React.Fragment가 children으로 전달될 때의 런타임 경고를 방지하기 위해 Fragment 타입을 체크할 수 있도록 Fragment를 import 목록에 추가합니다.

Suggested change
import { useId, Children, isValidElement, cloneElement } from 'react'
import { useId, Children, isValidElement, cloneElement, Fragment } from 'react'


type InjectedInputProps = {
id?: string
'aria-describedby'?: string
'aria-invalid'?: boolean
}

type FieldProps = {
/** 입력 위에 표시되는 라벨 */
Expand All @@ -15,24 +22,43 @@ type FieldProps = {
error?: string
/** 필수 입력 표시(*) 여부 */
required?: boolean
/** 라벨이 감쌀 입력 요소 */
/** 라벨과 연결되는 입력 요소 */
children: React.ReactNode
} & Omit<React.ComponentPropsWithoutRef<'label'>, 'children'>
} & React.ComponentPropsWithoutRef<'div'>

export function Field({ label, description, error, required, children, className = '', ...props }: FieldProps) {
const messageId = useId()
const uniqueId = useId()
const messageId = `${uniqueId}-message`
const hasMessage = Boolean(error || description)

// 단일 입력 요소면 id / aria 속성을 주입해 라벨·메시지와 연결 (복수/비요소 children 은 그대로 렌더링)
let inputId = `${uniqueId}-input`
let content = children
try {
const child = Children.only(children)
if (isValidElement<InjectedInputProps>(child)) {
inputId = child.props.id ?? inputId
content = cloneElement(child, {
id: inputId,
'aria-describedby': hasMessage ? messageId : undefined,
'aria-invalid': error ? true : undefined,
})
}
Comment on lines +38 to +46

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

개선 제안 및 고려사항

  1. Fragment 방어 코드 추가: children이 단일 요소이지만 <React.Fragment>인 경우, cloneElement로 HTML 속성(id, aria-*)을 주입하면 런타임 경고가 발생합니다. child.type !== Fragment 조건을 추가하여 이를 방지하는 것이 안전합니다.
  2. 기존 aria 속성 보존: 자식 요소가 이미 자체적으로 aria-describedbyaria-invalid 속성을 가지고 있을 수 있습니다. 현재 방식은 이를 완전히 덮어쓰므로, 기존 속성이 있다면 이를 유지하거나 병합(aria-describedby인 경우 공백으로 구분하여 결합)하도록 개선하는 것이 좋습니다.
  3. 커스텀 컴포넌트 전달 한계 (구조적 고려사항): DatePicker처럼 내부 대화형 요소(예: <button>)를 <div> 컨테이너로 감싸고 ...props를 컨테이너에 스프레드하는 컴포넌트의 경우, idaria-* 속성이 실제 버튼이 아닌 최상위 <div>에 부여됩니다. 이로 인해 라벨 클릭 시 포커스가 가지 않거나 스크린 리더 접근성이 깨질 수 있습니다. 향후 DatePicker 등 커스텀 입력 컴포넌트들이 idaria-* 속성을 내부 대화형 요소로 전달하도록 수정하는 작업을 함께 검토해 주세요.
    const child = Children.only(children)
    if (isValidElement<InjectedInputProps>(child) && child.type !== Fragment) {
      inputId = child.props.id ?? inputId
      const existingDescribedBy = child.props['aria-describedby']
      content = cloneElement(child, {
        id: inputId,
        'aria-describedby': hasMessage
          ? (existingDescribedBy ? `${existingDescribedBy} ${messageId}` : messageId)
          : existingDescribedBy,
        'aria-invalid': error ? true : child.props['aria-invalid'],
      })
    }

} catch {
// children 이 단일 요소가 아닌 경우 주입 없이 그대로 반환
}

return (
<label className={`yds-field ${className}`} {...props}>
<span className="yds-field-label">
<div className={`yds-field ${className}`} {...props}>
<label htmlFor={inputId} className="yds-field-label">
{label}
{required && (
<span className="yds-field-required" aria-hidden="true">
*
</span>
)}
</span>
{children}
</label>
{content}
{error ? (
<span id={messageId} className="yds-field-error" role="alert">
{error}
Expand All @@ -44,7 +70,7 @@ export function Field({ label, description, error, required, children, className
</span>
)
)}
</label>
</div>
)
}

Expand Down
6 changes: 3 additions & 3 deletions ui-docs-site/components/field.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ function App() {
- **표준 구조**: 라벨(yds-c1m, primary-100) + 임의의 입력(children) + 설명/에러 메시지(yds-c1r)
- **에러 표시**: `error` 지정 시 설명 대신 에러가 표시되고 `role="alert"` 로 스크린 리더에 즉시 전달됩니다.
- **필수 표시**: `required` 지정 시 라벨 옆에 빨간 별표(*)가 붙습니다.
- **라벨 클릭 포커스**: label 요소로 감싸므로 네이티브 input 은 라벨 클릭 시 자동으로 포커스됩니다.
- **입력 무관**: Input / NumberInput / DatePicker / SelectBox 등 어떤 입력이든 children 으로 감쌀 수 있습니다.
- **접근성 자동 연결**: 단일 입력 요소에는 `id` 가 자동 부여되어 라벨과 `htmlFor` 로 연결되고, 설명/에러가 있으면 `aria-describedby`, 에러 상태면 `aria-invalid` 가 함께 주입됩니다. 네이티브 input 은 라벨 클릭 시 자동으로 포커스됩니다.
- **입력 무관**: Input / NumberInput / DatePicker / SelectBox 등 어떤 입력이든 children 으로 넣을 수 있습니다.

## 에러 상태

Expand Down Expand Up @@ -61,7 +61,7 @@ function App() {
| `required` | `boolean` | `false` | 필수 입력 표시(*) 여부 |
| `children` | `React.ReactNode` | - | 라벨이 감쌀 입력 요소 (필수) |

Field는 표준 HTML label 요소의 속성을 함께 지원합니다.
Field는 표준 HTML div 요소의 속성을 함께 지원합니다. children 이 단일 요소일 때만 `id` / `aria-describedby` / `aria-invalid` 가 자동 주입되며, 이미 `id` 를 가진 요소는 기존 id 로 라벨이 연결됩니다.

## 디자인 토큰

Expand Down
Loading