ホバーカード
stimeo--hover-card
ホバー/フォーカスで開くリッチな補助カード。開閉の遅延と hoverable な橋渡しを担う。
stimeo--hover-card コントローラは、ホバー/フォーカスで非モーダルの補助カードを開く。 Disclosure の慣行(aria-expanded)に従う。ツールチップと違いリンク等の操作要素を含められるため、カードには role="dialog" を付けず、中身はトリガー自身からも到達できることを前提とする。 openDelay/closeDelay でちらつきを防ぎ、カードにも open/close を結んでポインタの橋渡しを行う。遅延クローズはフォーカスを再確認するため、カード内リンクへ Tab しても開いたままになる。開いている間は document レベルで Escape を監視し、フォーカス位置に依らず閉じる。開いてもフォーカスは奪わない。配置はこの Playground の CSS が持つ。
キーボード操作
| キー | 動作 |
|---|---|
| Esc | 開いているカードを閉じる(document 全体で監視。フォーカス位置に依らない)。 |
<%# Markup for the hover_card demo.
Hover/focus on the link opens a rich supplementary card after a delay. The card
has no role="dialog" (it's purely supplementary), and its contents are assumed to
be reachable from the trigger's target too. The open/close delay, the hoverable
bridge, and Escape-to-dismiss are the library's. %>
<span class="hover-card" data-controller="stimeo--hover-card">
<a
class="hover-card__trigger"
href="#jane"
data-stimeo--hover-card-target="trigger"
aria-expanded="false"
aria-controls="hover-card-panel"
data-action="
mouseenter->stimeo--hover-card#open
mouseleave->stimeo--hover-card#close
focusin->stimeo--hover-card#open
focusout->stimeo--hover-card#close
keydown->stimeo--hover-card#onKeydown">
<%= t("components.hover_card.demo.trigger") %>
</a>
<%# The card has no role="dialog" (supplementary display, not an interrupting dialog). %>
<div
class="hover-card__panel"
id="hover-card-panel"
data-stimeo--hover-card-target="card"
data-action="
mouseenter->stimeo--hover-card#open
mouseleave->stimeo--hover-card#close"
hidden>
<p class="hover-card__name"><%= t("components.hover_card.demo.name") %></p>
<p class="hover-card__role"><%= t("components.hover_card.demo.role") %></p>
<a class="hover-card__link" href="#follow"><%= t("components.hover_card.demo.follow") %></a>
</div>
</span>
/*
* Presentation-only styles for the hover_card demo.
* The library opens/closes the card by toggling its hidden attribute. Placement
* is static — the consumer's CSS responsibility.
*/
.hover-card {
position: relative;
display: inline-block;
}
.hover-card__trigger {
color: var(--accent, var(--color-primary));
text-decoration: underline;
}
.hover-card__panel {
position: absolute;
top: calc(100% + 0.5rem);
left: 0;
z-index: 10;
width: 18rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
padding: 1rem;
background: var(--surface, var(--surface-card));
border: 1px solid var(--border-strong);
border-radius: 0.5rem;
box-shadow: 0 8px 24px rgb(15 23 42 / 0.12);
}
/* Setting display: flex overrides the hidden attribute's default display:none, so
re-declare display:none while closed to honor the library's hidden toggle. */
.hover-card__panel[hidden] {
display: none;
}
.hover-card__name {
font-weight: 600;
}
.hover-card__role {
font-size: 0.875rem;
color: var(--color-text-muted);
}
.hover-card__link {
margin-top: 0.5rem;
color: var(--accent, var(--color-primary));
}
// hover_card opt-in positioning demo (consumer-side JS).
//
// The core controller (stimeo--hover-card) only handles open/close, delay, and
// dismiss. Viewport-edge flip/shift is optionally connected via the opt-in
// stimeo-ui/positioning module. We watch the card's hidden and, while open, track
// the trigger's coordinates.
import { attachPositioning } from 'stimeo-ui/positioning';
document.querySelectorAll('[data-controller~="stimeo--hover-card"]').forEach((root) => {
const trigger = root.querySelector('[data-stimeo--hover-card-target="trigger"]');
const card = root.querySelector('[data-stimeo--hover-card-target="card"]');
if (!trigger || !card) return;
let detach = null;
const sync = () => {
if (!card.hidden && !detach) {
detach = attachPositioning(trigger, card, {
placement: 'bottom-start',
offset: 8,
padding: 8,
});
} else if (card.hidden && detach) {
detach();
detach = null;
card.style.position = card.style.left = card.style.top = '';
}
};
new MutationObserver(sync).observe(card, { attributes: true, attributeFilter: ['hidden'] });
sync();
});
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。 共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--hover-card"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
trigger
必須
|
ホバー/フォーカスでカードを開く要素(例: リンク)。aria-expanded で状態を反映し、フォーカスは奪わない。 |
data-stimeo--hover-card-target="trigger" |
card
必須
|
hidden と data-state で表示制御する非モーダルのポップオーバー内容。open/close を結ぶことでホバー可能な橋渡しになる。 |
data-stimeo--hover-card-target="card" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
openDelay
|
ホバー/フォーカスで開くまでの待機ミリ秒。ちらつき防止(既定 300)。 | data-stimeo--hover-card-open-delay-value |
closeDelay
|
離脱/ブラーで閉じるまでの待機ミリ秒(フォーカスを再確認)。ちらつき防止(既定 200)。 | data-stimeo--hover-card-close-delay-value |
アクション
| 名前 | 説明 | アクション |
|---|---|---|
close
|
closeDelay 後に閉じる予約をする。保留中の open を取り消し、フォーカスがカード内なら中止する。 |
stimeo--hover-card#close |
onKeydown
|
開いている間、Escape で即座にカードを閉じる。 | stimeo--hover-card#onKeydown |
open
|
openDelay 後(0 なら即時)にカードを開く。保留中の close を取り消す。 |
stimeo--hover-card#open |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
aria-expanded |
トリガー | 開いている間は "true"、閉じている間は "false"。 |
hidden |
カード | 開で属性なし、閉で付与。 |
data-state |
カード | "open" / "closed"。フェード遷移用の任意フック。 |