diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bb6473..c2332a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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-레이아웃및토큰체계정비` diff --git a/package.json b/package.json index fd6804f..0706289 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/Layouts/Field/Field.stories.tsx b/src/components/Layouts/Field/Field.stories.tsx index cad81e5..3c1b97b 100644 --- a/src/components/Layouts/Field/Field.stories.tsx +++ b/src/components/Layouts/Field/Field.stories.tsx @@ -24,7 +24,7 @@ YD-UI 디자인 시스템의 폼 필드 컴포넌트입니다. - \`label\` + 임의의 입력(children) + \`description\` / \`error\` 메시지 구조 - \`error\` 지정 시 설명 대신 에러가 표시되고 \`role="alert"\` 로 스크린 리더에 즉시 전달 - \`required\` 지정 시 라벨 옆에 필수 표시(*) -- label 요소로 감싸므로 네이티브 input 은 라벨 클릭 시 자동 포커스 +- 단일 입력 요소에 \`id\` / \`aria-describedby\` / \`aria-invalid\` 자동 주입 — 라벨은 \`htmlFor\` 로 연결되어 클릭 시 포커스, 스크린 리더가 설명·에러를 함께 읽음 - 라벨·메시지의 색상/타이포그래피/간격은 모두 디자인 토큰에서 일괄 적용 ## 사용 가이드 diff --git a/src/components/Layouts/Field/Field.tsx b/src/components/Layouts/Field/Field.tsx index 89b145c..1991fd5 100644 --- a/src/components/Layouts/Field/Field.tsx +++ b/src/components/Layouts/Field/Field.tsx @@ -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' + +type InjectedInputProps = { + id?: string + 'aria-describedby'?: string + 'aria-invalid'?: boolean +} type FieldProps = { /** 입력 위에 표시되는 라벨 */ @@ -15,24 +22,43 @@ type FieldProps = { error?: string /** 필수 입력 표시(*) 여부 */ required?: boolean - /** 라벨이 감쌀 입력 요소 */ + /** 라벨과 연결되는 입력 요소 */ children: React.ReactNode -} & Omit, '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(child)) { + inputId = child.props.id ?? inputId + content = cloneElement(child, { + id: inputId, + 'aria-describedby': hasMessage ? messageId : undefined, + 'aria-invalid': error ? true : undefined, + }) + } + } catch { + // children 이 단일 요소가 아닌 경우 주입 없이 그대로 반환 + } return ( -