Citations

패키지 네임스페이스가 아니라 레시피입니다. 답변 속 인용은 답변의 출처 목록을 가리키는 번호 붙은 Link이고, 그 링크에 단 인터랙티브 Tooltip이 출처를 미리 보여 줍니다. 마크다운 렌더러가 답변의 각 인용을 이 표시로 바꿉니다. Citation.* export는 없습니다.

이 페이지의 내용

번호 위에 포인터를 올리거나 포커스를 옮기면 출처를 미리 볼 수 있습니다. 번호를 따라가면 목록으로 이동합니다.

A tooltip is drawn in the top layer through the Popover APISource 1Popover API
developer.mozilla.org
, so no ancestor can clip it, and CSS anchor positioning places it against its triggerSource 2CSS anchor positioning
developer.mozilla.org
.

A manual popover stays open until code hides it, which leaves hover and focus to the pageSource 3The popover attribute
html.spec.whatwg.org
.

Sources

  1. Popover API developer.mozilla.org
  2. CSS anchor positioning developer.mozilla.org
  3. The popover attribute html.spec.whatwg.org

인용 표시

표시마다 interactive: true를 준 useTooltip을 하나씩 쓰므로, 미리 보기에 출처 링크를 넣을 수 있고 포인터가 미리 보기로 건너갈 수 있습니다. 목록에서 해당 출처 항목을 가리키는 표시의 Link.Root에 getTriggerProps를 펼칩니다. 보이는 글자는 본문과 같은 크기의 괄호 속 번호로, Todae 기본 테마에서 약 18~22×20 CSS 픽셀입니다. 이는 WCAG 2.2 Target Size (Minimum)의 24×24보다 작지만, 이 기준은 문장 안의 대상을 예외로 두므로 표시는 그 예외로 기준을 충족합니다. 괄호는 aria-hidden이므로, 링크 이름은 앞의 시각적으로 숨긴 단어 덕분에 "Source 1"로 유지됩니다. 표시의 스타일을 바꿔도 괄호는 숨긴 채로 둡니다. Tooltip.Root는 <span>으로 렌더링되므로 표시와 미리 보기가 렌더러가 쓴 문단 안에 들어갑니다. 미리 보기의 내용은 링크, <br />, Text.Root as="span" 같은 구문 요소(phrasing content)로만 채웁니다. 개발 환경에서는 팁 안의 <p>나 <div>에 Todae가 오류를 기록합니다.

import { Link, Text, Tooltip, useTooltip, VisuallyHidden } from '@tounsoo/todae';
import { useId } from 'react';

type Source = { title: string; site: string; url: string };

export function Citation({ number, source, href }: { number: number; source: Source; href: string }) {
  const tooltip = useTooltip({ interactive: true });
  const titleId = useId();
  return (
    <>
      <Link.Root href={href} {...tooltip.getTriggerProps()}>
        <VisuallyHidden.Root>Source </VisuallyHidden.Root>
        <span aria-hidden>[</span>
        {number}
        <span aria-hidden>]</span>
      </Link.Root>
      <Tooltip.Root {...tooltip.getTooltipProps()} aria-labelledby={titleId}>
        <Link.Root id={titleId} href={source.url}>
          {source.title}
        </Link.Root>
        <br />
        <Text.Root as="span" tone="muted">
          {source.site}
        </Text.Root>
      </Tooltip.Root>
    </>
  );
}

출처 목록

답변 글 뒤에 Prose 안의 번호 목록 <ol>로 출처를 나열해, 목록의 번호가 표시의 번호와 맞게 합니다. 대화에는 "source 1"이 있는 답변이 여럿이므로, 메시지마다 useId()를 하나 써서 표시가 가리킬 항목 id를 만듭니다. 목록의 이름은 제목에서 aria-labelledby로 가져옵니다.

const id = useId();

<Message.Root aria-label="Assistant">
  <Message.Content>
    <Prose.Root>
      <p>
        A tooltip is drawn in the top layer through the Popover API
        <Citation number={1} source={sources[0]} href={`#${id}-source-1`} />.
      </p>
      <Text.Root id={`${id}-sources`} weight="bold">
        Sources
      </Text.Root>
      <ol aria-labelledby={`${id}-sources`}>
        {sources.map((source, index) => (
          <li key={source.url} id={`${id}-source-${index + 1}`}>
            <Link.Root href={source.url}>{source.title}</Link.Root>{' '}
            <Text.Root as="span" tone="muted">
              {source.site}
            </Text.Root>
          </li>
        ))}
      </ol>
    </Prose.Root>
  </Message.Content>
</Message.Root>

접근성을 지키는 조합

Todae가 맡는 것: useTooltip은 포인터를 올리면 tooltip.delay.show 토큰만큼 기다렸다가, 포커스를 받으면 바로 미리 보기를 열고, 열려 있는 동안 미리 보기를 표시의 aria-describedby에 더합니다. Tooltip.Root는 놓은 자리에 렌더링되므로, 표시 바로 뒤에 두면 Tab이 표시에서 미리 보기의 링크로 이동합니다. Escape는 미리 보기를 닫고, 안에서 누르면 포커스를 표시로 돌려보냅니다. interactive인 Tooltip.Root는 포커스를 가두지 않는 비모달 role="dialog"입니다.

직접 제공할 것: 맥락 없는 번호만으로는 뜻이 없으므로, 표시마다 "Source 1" 같은 이름. 미리 보기마다 이름(여기서는 aria-labelledby로 연결한 제목). 페이지에서 겹치지 않는 목록 항목 id. 출처 목록의 이름이 되는 제목.

주의할 점: 터치 화면에서는 탭하면 표시를 따라 목록으로 이동하고, 미리 보기는 브라우저가 탭한 링크에 포커스를 줄 때만 열리므로, 미리 보기에 보이는 것은 모두 목록에도 둡니다. 미리 보기가 열린 동안에는 표시마다 미리 보기의 링크라는 Tab 정지가 하나 더 생깁니다. 목록의 번호는 브라우저의 목록 표시이므로, Tailwind의 preflight처럼 이를 없애는 CSS reset을 쓰면 목록에 번호가 사라집니다. 미리 보기에는 링크만 둡니다. 복사 같은 버튼은 목록이나 Message actions에 둡니다. 답변 아래의 출처 칩 줄처럼 문장 밖에 따로 보이는 표시는 문장 안에 있지 않으므로 예외에 해당하지 않습니다. 이런 표시에는 24×24 CSS 픽셀 이상의 대상을 주거나, 각 표시를 중심으로 한 지름 24픽셀 원이 다른 대상에도, 서로에게도 닿지 않도록 간격을 둡니다. 아직 스트리밍 중인 답변의 표시는 출처가 정해진 뒤에 렌더링해, 번호와 링크가 포인터 아래에서 바뀌지 않게 합니다.

언제 쓰나요

쓸 때: 검색 결과, 문서, 도구 출력 같은 출처를 글 안에서 인용하는 답변.

쓰지 않을 때: 글에 표시 없이 출처 목록만 있을 때 → 목록만. 미리 보기가 필요 없는 문서 페이지의 각주 → <ol>을 가리키는 일반 링크. 메시지에 첨부한 파일 → Attachment.