アンカーポジショニング付き右クリックコンテキストメニュー
anchor-name を付けた非表示の位置決め要素をクリック座標に置き、コンテキストメニューをカーソル位置に配置する。
UI Patterns
詳細な説明
やり方
コンテキストメニューはカーソルの正確な x/y 座標で開きます — 静的な要素相対ではありません。アンカーポジショニングは動きますが、まず アンカー をカーソル位置に配置するための小さなJavaScriptシムが必要です。あとはCSSが処理します。
<div class="contextable" oncontextmenu="openMenu(event)">
<!-- 右クリック可能エリア -->
</div>
<div class="cursor-anchor" id="cursor-anchor"></div>
<menu class="context-menu" id="context-menu" hidden>
<li><button>切り取り</button></li>
<li><button>コピー</button></li>
<li><button>貼り付け</button></li>
</menu>
#cursor-anchor {
position: fixed;
width: 1px;
height: 1px;
pointer-events: none;
anchor-name: --cursor;
}
#context-menu {
position: fixed;
position-anchor: --cursor;
position-area: bottom-end;
margin: 4px;
position-try-fallbacks:
bottom-start, /* 左に開く */
top-end, /* 上に開く */
top-start; /* 上 AND 左 */
}
function openMenu(e) {
e.preventDefault();
const anchor = document.getElementById('cursor-anchor');
anchor.style.left = e.clientX + 'px';
anchor.style.top = e.clientY + 'px';
document.getElementById('context-menu').hidden = false;
}
なぜ 1×1 の不可視要素?
アンカーはサイズとレイアウトボックスを持つ実DOM要素である必要があります。1×1 の pointer-events: none 要素が最小の不可視ターゲットです — メニューがそれにアンカーされ、メニュー自身の position-try-fallbacks がエッジケースを処理します(画面右端付近の右クリックでメニューが左に反転)。
なぜ position: fixed?
コンテキストメニューはユーザーがスクロールしてもページと一緒にスクロールすべきではありません。position: fixed をアンカーとメニュー両方に指定して、スクロールに関わらずビューポートに固定します。
CSSのみの代償
mousemove を listen してアンカー位置をカーソル移動毎に更新すれば、JSなしで構築できなくはないですが、上の命令的アプローチより厳密に劣ります。クリックイベントにはJSを使い、レイアウトはCSSに任せましょう。
ユースケース
ファイルマネージャーのコンテキストメニュー、テーブルセルのアクションメニュー、ドキュメントエディターの右クリックメニュー、canvas/SVG 要素上のカスタム右クリック、ブラウザのデフォルト右クリックを上書きするアプリ内右クリックメニュー。