Inline edit
패키지 네임스페이스가 아니라 레시피입니다. 보이는 자리에서 이름을 바꿀 수 있는 값은 Text와 그 뒤의 아이콘만 있는 Button입니다. 편집하는 동안에는 Field의 텍스트 입력과 Save, Cancel 버튼이 그 자리를 대신합니다. 키와 포커스는 useInlineEdit이 맡습니다. InlineEdit.* export는 없습니다.
이 페이지의 내용
Rename을 누르고 새 이름을 입력한 다음, Enter로 저장하거나 Escape로 이름을 되돌려 보세요.
Q3 report
import { Button, ErrorMessage, Field, Stack, Text, useInlineEdit } from '@tounsoo/todae';
import { Check, Pencil, X } from 'lucide-react';
import { useId } from 'react';
export function InlineName({ name, onRename }: { name: string; onRename: (name: string) => void }) {
const nameId = useId();
const errorId = useId();
const edit = useInlineEdit({
value: name,
onCommit: (draft) => {
if (draft.trim() === '') return false;
onRename(draft.trim());
},
});
const invalid = edit.draft.trim() === '';
if (edit.editing) {
return (
<Stack.Root {...edit.getGroupProps()} gap="condensed">
<Stack.Root direction="row" gap="condensed" align="center">
<Field.Root aria-label="Name" invalid={invalid} aria-describedby={invalid ? errorId : undefined}>
<Field.TextInput {...edit.getInputProps()} />
</Field.Root>
<Button.Root variant="ghost" aria-label="Save" onClick={edit.commit}>
<Button.Icon>
<Check />
</Button.Icon>
</Button.Root>
<Button.Root variant="ghost" aria-label="Cancel" onClick={edit.cancel}>
<Button.Icon>
<X />
</Button.Icon>
</Button.Root>
</Stack.Root>
{invalid ? <ErrorMessage.Root id={errorId}>Enter a name.</ErrorMessage.Root> : null}
</Stack.Root>
);
}
return (
<Stack.Root direction="row" gap="condensed" align="center">
<Text.Root id={nameId}>{name}</Text.Root>
<Button.Root {...edit.getTriggerProps()} variant="ghost" aria-label="Rename" aria-describedby={nameId}>
<Button.Icon>
<Pencil />
</Button.Icon>
</Button.Root>
</Stack.Root>
);
}
저장과 취소
Enter와 Save 버튼이 저장하고, Tab 등으로 포커스를 편집 밖으로 옮겨도 저장됩니다. Escape와 Cancel 버튼은 이름을 되돌립니다. 저장이나 취소는 이름이 설명하는 Rename으로 포커스를 되돌리므로, 스크린 리더가 버튼과 함께 새 이름을 읽고 페이지는 따로 알릴 필요가 없습니다. 빈 이름은 이름을 입력하거나 취소할 때까지 편집을 열어 두고 오류를 보여 줍니다. 빈 이름으로 포커스를 옮기면 이전 이름으로 돌아갑니다.
Table에서는 셀에 같은 마크업을 넣습니다.
서버에 저장하기
새 이름을 바로 보여 주고 보내세요. 저장에 실패하면 이전 이름을 되돌리고, useAnnouncer로 알리고, 다시 시도할 수 있게 하세요. start(draft)는 실패한 이름으로 편집을 엽니다.
const announce = useAnnouncer();
// The name that failed to save, offered again by Try again.
const [failed, setFailed] = useState<string | null>(null);
const edit = useInlineEdit({
value: name,
onCommit: (draft) => {
const next = draft.trim();
if (next === '') return false;
const before = name;
setName(next);
// rename sends the name to your server.
rename(next).catch(() => {
setName(before);
setFailed(next);
announce(`Couldn't rename to ${next}.`, { politeness: 'assertive' });
});
},
});
{failed !== null && (
<Button.Root
variant="secondary"
onClick={() => {
setFailed(null);
edit.start(failed);
}}
>
Try again
</Button.Root>
)}
접근성을 지키는 조합
Todae가 맡는 것: useInlineEdit은 입력에 포커스를 주고 이름을 선택하며, Enter와 포커스가 편집을 떠날 때 저장하고, Escape로 취소하고, IME 조합은 건드리지 않고, 둘러싼 Dialog가 닫히기 전에 편집을 취소하고, Save나 Cancel을 누르는 동안 포커스를 입력에 두고, 포커스를 Rename으로 되돌립니다. 아이콘만 있는 Button.Root는 aria-label을 tooltip으로 보여 줍니다. Field.Root invalid는 입력에 aria-invalid를 설정합니다.
직접 제공할 것: 입력과 Rename, Save, Cancel 버튼의 이름. Rename의 설명이 되는 이름의 id. 오류 메시지와, 필드의 aria-describedby에 넣은 그 id. 서버 저장이 실패했을 때의 알림. 모두 제품의 언어로.
주의할 점: 이름 목록에서 "Rename"이라고만 말하는 Rename 버튼. 예제처럼 각 버튼을 그 이름으로 설명하세요. 편집 그룹 밖의 Save나 Cancel 버튼. 포커스가 그 버튼으로 옮겨 가면서 저장됩니다. 편집이 닫힌 뒤 서버가 거절한 이름을 저장된 것처럼 보여 주는 것. 이름을 key로 쓰는 목록. 이름을 바꾸면 새 행이 마운트되어 포커스가 돌아갈 곳이 없습니다. 행의 key는 id로 정하세요.
언제 쓰나요
쓸 때: 파일, 목록, 대화, 행의 이름을 그 이름이 보이는 자리에서 바꿀 때.
쓰지 않을 때: 여러 줄의 텍스트 → Form이나 Dialog 안의 Field.Textarea. 함께 바뀌는 여러 값 → Form.