💬

Radix UIのTooltipをタップでも表示する方法、あるいはホバーとフォーカスしか受け付けない理由

2024/06/19に公開

Radix UITooltipで実装していたときに、モバイルでタップしてもポップアップが表示されず困ってしまったことがありました。なぜなら、デスクトップではホバー、モバイルではタップで表示されることが要件だったからです。

この記事ではタップ操作でも表示できるようにした方法と、なぜタップ操作で表示できなくなっているかを紹介します。

Tooltipはアクションボタンに対するチップ(補足情報)を提示するUI

Issueを探っているとRadix UIのUI Software Engineerであるbenoitgrelardさんがコメントしていました(Raycastの中の人でもある!)。

This is by design. Tooltips are problematic on touch devices because there is no hover interaction, so if it was on tap it would fight with the general button action, or require 2 taps. That's also why tooltips should just be used generally for extra information that isn't mandatory do use your interface.
これはデザインによるものです。ツールチップはタッチデバイスでは問題があります。なぜなら、ホバーインタラクションがないため、もしそれがタップされた場合、一般的なボタンアクションと喧嘩してしまうか、2回タップする必要があるからです。ツールチップは、インターフェイスを使用するために必須ではない余分な情報のために一般的に使用されるべきです。

In that case, a Popover is probably more appropriate then.
この場合、Popoverの方が適切でしょう。

There are no such convention in the existing accessibility guidelines as touch is problematic.
既存のアクセシビリティ・ガイドラインには、タッチが問題であるとして、そのような規約はありません。

This is because a tooltip is a "tip" for a "tool", that tool is an interactive element (99% of the time should be a button), hence on touch devices if you'd show the tooltip on touch, it would already be too late anyway because the action the tool performs would have been triggered so it simply doesn't make much sense. Other alternatives result in poor UX such as asking the user to press twice (once for the tooltip, and a second for the action).
なぜなら、ツールチップは "ツール "のための "チップ "であり、そのツールはインタラクティブな要素(99%はボタンであるべき)であるため、タッチデバイスでツールチップを表示する場合、ツールが実行するアクションがトリガーされているため、すでに手遅れであり、あまり意味をなさないからです。他の選択肢は、ユーザーに2回(ツールチップのために1回、アクションのために2回)押すように要求するような悪いUXをもたらします。

Hopefully this makes sense why they are ignored on touch.
タッチで無視される理由がこれで理解できるといいのですが。

See here where it specifically only mentions hover and focus: https://> www.w3.org/WAI/ARIA/apg/patterns/tooltip/
特にホバーとフォーカスのみに言及しているこちらをご覧ください: https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/

[Tooltip] Tooltip component doesn't work on iOS Safari (14.4) · Issue #955 · radix-ui/primitives

要約すると

  • Tooltipはそもそもタッチデバイスで操作できないUIである
  • なぜなら、ツールを実行するためのアクションボタンに指定するものなので、クリックやタップをTooltipに指定することができないから
  • Authoring Practicesでもフォーカスとホバー時に表示されるものとしていて、クリックやタップに関する記述もない
  • 補足情報やヒントのための使用にとどめて、必ず提供しなければいけない重要な情報の場合はPopoverのほうが適切である

UIデザインとしてのTooltipの役割は確かにそうだよなというところ。
Radix UIのドキュメントにあるTooltipっぽいものもPopoverだったりします。Popoverってドロップダウンメニュー的なイメージだったので、Tooltip的な見せ方をしていたのが目から鱗でした。

HeadlessUIにはTooltipがないのですが、React Ariashadcn/uiもRadix UIと同じ挙動になっていました。
ただReact Tooltipはタップ操作ができるようになっていたり、Material UIは長押しで操作可能になったいたりと、ライブラリごとのバラつきはありそうです。

Tooltipをタップでも表示する実装方法

とはいえ、今回の実装はアクションボタンに対するものではなかったので、モバイルでも表示する方法を探していると、同じIssue内で提示されていた2つの実装方法がよさそうでした。

setTimeoutでイベントの実行順序を調整する方法

To add to solution above, there was still an issue where on mobile I needed to click twice to get the tooltip, because onOpenChange was running before click handler on initial click somehow. The "solution" was to add timeout state update to onFocus on Trigger element.
上記の解決策を補足すると、モバイルでは、ツールチップを表示するために2回クリックする必要があるという問題が残っていました。これは、onOpenChangeが、何らかの理由で最初のクリック時にクリックハンドラよりも先に実行されていたためです。解決策」は、Trigger要素のonFocusにタイムアウト状態の更新を追加することでした。

const [open, setOpen] = useState(false);

  return (
    <RadixTooltip.Provider>
      <RadixTooltip.Root open={open} onOpenChange={setOpen}>
        <RadixTooltip.Trigger
          onClick={() => setOpen((prevOpen) => !prevOpen)}
          onFocus={() => setTimeout(() => setOpen(true), 0)} // timeout needed to run this after onOpenChange to prevent bug on mobile
          onBlur={() => setOpen(false)}
        >
          {children}
        </RadixTooltip.Trigger>
        <RadixTooltip.Portal>
          <RadixTooltip.Content>
            // tooltip content
            <RadixTooltip.Arrow />
          </RadixTooltip.Content>
        </RadixTooltip.Portal>
      </RadixTooltip.Root>
    </RadixTooltip.Provider>
  );

onMouseEnterとonMouseLeave、onFocusとonBlurをそれぞれ指定する方法

I found a naive way to make Tooltip working on both pointing device and touch device. In my use case, it's just an info icon like your website, it's not a general button, so there will be no event conflict. Not sure any downside of this solution.
https://codesandbox.io/s/smoosh-forest-553n1q?file=/App.js
ポインティングデバイスとタッチデバイスの両方でツールチップを動作させる素朴な方法を見つけました。私の使用ケースでは、それはあなたのウェブサイトのような単なる情報アイコンであり、一般的なボタンではないので、イベントの競合はありません。この解決策の欠点はわかりません。

const TooltipDemo = () => {
  const [open, setOpen] = useState(false);

  return (
    <Tooltip open={open}>
      <TooltipTrigger
        asChild
        onMouseEnter={() => {
          setOpen(true);
        }}
        onMouseLeave={() => {
          setOpen(false);
        }}
        onFocus={() => {
          setOpen(true);
        }}
        onBlur={() => {
          setOpen(false);
        }}
      >
        <IconButton>
          <PlusIcon />
        </IconButton>
      </TooltipTrigger>
      <StyledContent sideOffset={5}>
        Add to library
        <StyledArrow />
      </StyledContent>
    </Tooltip>
  );
};

こちらのほうが単純で、アップデートの影響を受けにくいかもしれません。

まとめ

  • Tooltipはアクションボタンのヒントを出すUIであり、ホバーとフォーカスで表示されるもの
  • TooltipではなくPopoverが適切な場合がある
  • 公式ではないがタップで開くための実装方法がある
  • タップに対応しているかはライブラリによるが、アクセシビリティの観点では問題とされていないと思われる

TooltipというよくあるUIですが、用途や使い方をよく理解していなかったことに気づきました。
実装や挙動に関してはエンジニアのほうが気づきやすく提案しやすいと思うので、デザイナーにフィードバックをして認識合わせをするのも有益かもしれません。

chot Inc. tech blog

Discussion