useInlineEdit

useInlineEdit({ value, onCommit, onCancel })는 파일 이름처럼 값을 그 자리에서 편집합니다. 편집이 열려 있는지, 초안, 그리고 편집을 시작하는 버튼, 입력과 그 버튼들을 담는 그룹, 입력에 줄 props를 반환합니다.

이 페이지의 내용
const nameId = useId();
const edit = useInlineEdit({ value: name, onCommit: setName });

{edit.editing ? (
  <Stack.Root {...edit.getGroupProps()} direction="row" gap="condensed" align="center">
    <Field.Root aria-label="Name">
      <Field.TextInput {...edit.getInputProps()} />
    </Field.Root>
    <Button.Root onClick={edit.commit}>Save</Button.Root>
    <Button.Root variant="secondary" onClick={edit.cancel}>
      Cancel
    </Button.Root>
  </Stack.Root>
) : (
  <Stack.Root direction="row" gap="condensed" align="center">
    <Text.Root id={nameId}>{name}</Text.Root>
    <Button.Root {...edit.getTriggerProps()} variant="secondary" aria-describedby={nameId}>
      Rename
    </Button.Root>
  </Stack.Root>
)}

Rename을 누르고 새 이름을 입력한 다음, Enter로 저장하거나 Escape로 이름을 되돌려 보세요. 이름을 지우면 거절된 편집을 볼 수 있습니다.

Q3 report

트리거가 부르는 start()는 value로 편집을 열고, 입력에 포커스를 주고 텍스트를 선택하므로, 입력하면 이름이 바뀝니다. start(draft)는 저장에 실패한 이름을 다시 내밀 때처럼 다른 텍스트로 편집을 엽니다. 편집이 열려 있는 동안에는 둘 다 아무것도 하지 않습니다.

Enter와 그룹을 떠나는 포커스는 onCommit으로 초안을 저장합니다. 빈 이름처럼 편집을 열어 두려면 false를 반환하세요. 포커스가 편집을 떠난 뒤에는 Escape가 더 이상 편집에 닿지 않으므로, false는 대신 값을 되돌리고 onCancel을 부릅니다. value와 같은 초안은 onCommit을 부르지 않고 편집을 닫습니다. Escape는 취소하고 onCancel을 부릅니다. 한국어나 일본어 조합 중에 누른 Enter와 Escape는 입력에 맡깁니다. Safari는 조합을 끝내는 Escape를 조합이 끝난 뒤에 보낼 수 있고, 그때는 Todae의 다른 필드에서처럼 Escape가 편집을 취소합니다. Dialog나 Drawer 안의 편집에서 Escape는 편집을 취소하고 대화 상자는 열어 둡니다. 다음 Escape가 대화 상자를 닫습니다.

저장이나 취소가 안에 포커스가 있는 편집을 닫으면, 포커스는 트리거가 페이지에 돌아온 뒤 트리거로 돌아갑니다. 포커스가 다른 곳으로 옮겨 가서 닫힌 편집은 포커스를 그대로 둡니다. 탭이나 창을 바꾸면 편집은 열린 채로 남습니다.

접근성을 지키는 조합

Todae가 맡는 것: 입력에 포커스를 주고 텍스트를 선택하는 것. IME 조합은 건드리지 않는 Enter와 Escape. 모달 안에서 Escape와 뒤로 가기 제스처가 편집에 먼저 닿게 하는 것. 포커스가 그룹을 떠날 때 저장하되, 창이 포커스를 잃을 때는 저장하지 않는 것. 그룹 안에서 텍스트 필드, select, 슬라이더, 편집 가능한 요소가 아닌 곳을 누르는 동안 포커스를 입력에 두어, Safari에서도 마우스로 Cancel을 누르면 취소되게 하는 것. 포커스를 트리거로 되돌리는 것.

직접 제공할 것: 입력과 아이콘만 있는 각 버튼의 이름. 마크업 안 제자리에 놓은 트리거, 그룹, 입력. 초안을 거절할 때 aria-describedby로 입력에 이은 오류 메시지.

주의할 점: 무엇을 편집하는지 이름으로 말하지 않는 트리거. 예제처럼 값으로 설명해서, 포커스가 돌아올 때 새 값이 읽히게 하세요. 이유를 보여 주지 않고 초안을 거절하는 것. 그룹 밖의 Save나 Cancel 버튼. 입력에서 그 버튼으로 포커스가 옮겨 가면 클릭보다 먼저 저장됩니다. 값을 key로 쓰는 목록. 이름을 바꾸면 행의 key가 바뀌어 React가 새 행을 마운트하고, 포커스는 돌아갈 곳이 없습니다. 행의 key는 id로 정하세요. 팝업이 포털처럼 페이지의 다른 곳에 렌더링되는 그룹 안의 컨트롤. 포커스가 팝업으로 옮겨 가면 그룹을 떠난 것이 되어 저장됩니다.

언제 쓰나요

쓸 때: 파일, 목록, 행의 이름처럼 보이는 자리에서 바꾸는 짧은 값.

쓰지 않을 때: 여러 줄의 텍스트, 또는 함께 바뀌는 여러 값 → Form. 페이지에 맞지 않으면 Dialog 안에 둡니다. Todae의 부품으로 그린 패턴 전체 → Inline edit 레시피.

Props

useInlineEdit props
이름타입설명
value 필수string

저장된 값. 편집은 매번 이 값에서 시작합니다.

onCommit 필수(draft: string) => boolean | void

사람이 저장할 때 초안과 함께 불립니다. Enter, commit을 부르는 Save 버튼, 편집을 떠나는 포커스가 그렇습니다. 빈 이름처럼 편집을 이어 가려면 false를 반환하세요. 포커스가 편집을 떠난 뒤라면 false는 값을 되돌리고 onCancel을 부릅니다. 초안이 value와 같으면 불리지 않고 편집만 닫힙니다.

onCancel() => void

사람이 취소할 때 불립니다. Escape나 cancel을 부르는 Cancel 버튼이 그렇습니다. onCommit이 거절한 초안을 둔 채 포커스가 편집을 떠날 때도 불립니다.