UI v0.19.1 레이아웃및토큰체계정비 - #35
Hidden character warning
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
@gemini review |
There was a problem hiding this comment.
Code Review
This pull request updates the design system to version 0.19.1, introducing new layout components (Stack, Inline, and Field) and reorganizing the token system. The Card component now supports outlined and filled variants, while the Tabs component has been simplified by removing the animated indicator. New foundations for borders, colors, and spacing scales have also been added. The review feedback suggests enhancing the accessibility of the new Field component by using React.cloneElement to automatically inject aria-describedby and aria-invalid attributes into its child input element.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
Code Review
This pull request introduces a new set of layout components (Stack, Inline, and Field) along with their corresponding design tokens, styles, stories, and documentation. It also refactors the Card component to support outlined and filled variants, and simplifies the Tabs component by removing the sliding indicator animation in favor of a simpler border and text-color highlight. A review comment highlights a critical issue in the new Field component, where wrapping the entire structure in a <label> tag violates HTML standards when block-level children are passed, causes unintended focus behaviors, and lacks proper accessibility associations (such as aria-describedby). Refactoring the container to a <div> and programmatically injecting accessibility attributes into the child input is recommended.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| import { useId } from 'react' | ||
|
|
||
| type FieldProps = { | ||
| /** 입력 위에 표시되는 라벨 */ | ||
| label: string | ||
| /** 입력 아래 보조 설명 (에러가 있으면 에러가 우선 표시) */ | ||
| description?: string | ||
| /** 에러 메시지. 지정 시 설명 대신 표시 */ | ||
| error?: string | ||
| /** 필수 입력 표시(*) 여부 */ | ||
| required?: boolean | ||
| /** 라벨이 감쌀 입력 요소 */ | ||
| children: React.ReactNode | ||
| } & Omit<React.ComponentPropsWithoutRef<'label'>, 'children'> | ||
|
|
||
| export function Field({ label, description, error, required, children, className = '', ...props }: FieldProps) { | ||
| const messageId = useId() | ||
|
|
||
| return ( | ||
| <label className={`yds-field ${className}`} {...props}> | ||
| <span className="yds-field-label"> | ||
| {label} | ||
| {required && ( | ||
| <span className="yds-field-required" aria-hidden="true"> | ||
| * | ||
| </span> | ||
| )} | ||
| </span> | ||
| {children} | ||
| {error ? ( | ||
| <span id={messageId} className="yds-field-error" role="alert"> | ||
| {error} | ||
| </span> | ||
| ) : ( | ||
| description && ( | ||
| <span id={messageId} className="yds-field-description"> | ||
| {description} | ||
| </span> | ||
| ) | ||
| )} | ||
| </label> | ||
| ) | ||
| } |
There was a problem hiding this comment.
Field 컴포넌트가 전체 영역을 <label> 태그로 감싸고 있어 다음과 같은 잠재적인 UX 및 웹 표준 문제가 발생할 수 있습니다:
- HTML 표준 위반:
<label>태그 내부에는<div>와 같은 블록 레벨 요소(Flow content)를 배치할 수 없으며, 오직 구문 콘텐츠(Phrasing content)만 포함할 수 있습니다.DatePicker나SelectBox등 내부적으로<div>를 사용하는 복잡한 컴포넌트가children으로 들어올 경우 HTML 표준에 위배됩니다. - 의도치 않은 포커스/클릭 동작:
<label>내부의 어느 곳(설명 텍스트, 에러 메시지 등)을 클릭하더라도 브라우저는 내부의 첫 번째 포커스 가능한 요소(input)로 포커스를 강제 이동시킵니다. 이로 인해 커스텀 드롭다운이나 달력 팝업이 열리자마자 닫히거나, 보조 버튼 클릭이 무시되는 등의 오동작이 발생할 수 있습니다. - 접근성(Accessibility) 누락: 에러 메시지나 설명글에
id={messageId}를 부여하고 있으나, 정작children으로 전달되는 input 요소에는aria-describedby가 연결되지 않아 스크린 리더가 설명이나 에러를 읽지 못합니다.
개선 제안
컨테이너를 <div>로 변경하고, <label>을 별도로 분리하여 htmlFor로 연결하는 구조를 권장합니다. 또한, React.cloneElement를 활용하여 children으로 들어오는 단일 input 요소에 id와 aria-describedby, aria-invalid 속성을 안전하게 자동 주입해 주면 접근성과 사용성을 모두 확보할 수 있습니다.
import { useId, Children, isValidElement, cloneElement }
from 'react'
type FieldProps = {
/** 입력 위에 표시되는 라벨 */
label: string
/** 입력 아래 보조 설명 (에러가 있으면 에러가 우선 표시) */
description?: string
/** 에러 메시지. 지정 시 설명 대신 표시 */
error?: string
/** 필수 입력 표시(*) 여부 */
required?: boolean
/** 라벨이 감쌀 입력 요소 */
children: React.ReactNode
} & React.ComponentPropsWithoutRef<'div'>
export function Field({ label, description, error, required, children, className = '', ...props }: FieldProps) {
const uniqueId = useId()
const inputId = uniqueId + "-input"
const messageId = uniqueId + "-message"
const renderChildren = () => {
try {
const child = Children.only(children)
if (isValidElement(child)) {
return cloneElement(child as React.ReactElement<any>, {
id: child.props.id || inputId,
'aria-describedby': error || description ? messageId : undefined,
'aria-invalid': error ? true : undefined,
})
}
} catch {
// children이 단일 요소가 아니거나 유효하지 않은 경우 그대로 반환
}
return children
}
return (
<div className={"yds-field " + className} {...props}>
<label htmlFor={inputId} className="yds-field-label">
{label}
{required && (
<span className="yds-field-required" aria-hidden="true">
*
</span>
)}
</label>
{renderChildren()}
{error ? (
<span id={messageId} className="yds-field-error" role="alert">
{error}
</span>
) : (
description && (
<span id={messageId} className="yds-field-description">
{description}
</span>
)
)}
</div>
)
}
Branch:
ui-v0.19.1-레이아웃및토큰체계정비추가
변경