← ./articles-ja

Reactのnested interactive要素はsibling buttonsへ分解する

nested interactive elementsは、多くの場合keyboard handlerの問題ではなく、HTML構造の問題です。

React componentは最初、1つのclickable chipやcardとして作られます。後からremove、menu、secondary actionが増えると、既存のbuttonの中に別のinteractive elementを入れがちです。これはinvalid HTMLで、accessibility上も混乱を生みます。

多くの場合、containerを非interactiveにし、内部に兄弟buttonを置くのが正解です。

症状: 1つのcomponentに2つのclick targetがある

例です。

  • recent project chip: selectとremove
  • file tab: activateとclose
  • card: openとfavorite
  • list row: selectとmenu

壊れた形はこうなりがちです。

export function RecentChip({ path, onSelect, onRemove }) {
  return (
    <button onClick={onSelect}>
      <span
        role="button"
        tabIndex={0}
        onClick={onRemove}
      >
        x
      </span>
      {path}
    </button>
  );
}

interactive ancestorの中にinteractive descendantがあります。

非interactive wrapperに分ける

lidiv をwrapperにし、2つのnative buttonを並べます。

export function RecentChip({ path, onSelect, onRemove }) {
  return (
    <li className="recent-chip">
      <button className="chip-main" onClick={onSelect}>
        {path}
      </button>
      <button
        className="chip-remove"
        aria-label={`Remove ${path}`}
        onClick={onRemove}
      >
        x
      </button>
    </li>
  );
}

これで各actionが独立したsemantics、focus target、keyboard behaviorを持ちます。

spanにhandlerを足して直そうとしない

これは根本解決ではありません。

<span
  role="button"
  tabIndex={0}
  onKeyDown={(event) => {
    if (event.key === "Enter" || event.key === " ") onRemove();
  }}
>
  x
</span>

一部の症状は良くなりますが、nested interactive structureは残ります。native buttonならEnter、Space、disabled、focus、roleを最初から扱えます。

見た目はCSSで維持する

sibling構造でも、見た目は1つのchipにできます。

.recent-chip {
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--border);
  border-radius: 6px;
  overflow: hidden;
}

.chip-main,
.chip-remove {
  border: 0;
  background: transparent;
}

視覚的な一体感のために、semantic nestingを壊す必要はありません。

検証チェック

  • button の中に別の button、link、input、role="button" がない
  • 両方のactionへTabで到達できる
  • 両方にfocus visibleがある
  • custom keyboard codeなしでEnter/Spaceが動く
  • remove buttonに意味のあるaccessible nameがある
  • axeやAccessibility Insightsでnested interactiveが消える

参考