[react-headless-components-preview] usePositioning overwrites anchor-name → Tooltip+Menu on one trigger breaks positioning
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 3/5
- 見積もり時間
- 1〜2日
- 初心者へのやさしさ
- 68/100
- issue の種類
- バグ
- 明瞭さ
- おおむね明確
- 活発さ
- 静か
- 技術スタック
- react, typescript
- 領域
- frontend
調査の方向性
packages/react-components/react-headless-components-preview/library/src/hooks/usePositioning/usePositioning.ts から開始し、issue にある Tooltip-plus-Menu の構成を再現します。anchor-name の effect と cleanup を追跡し、その後、1 つの trigger 上の 2 つの usePositioning インスタンスを対象とする、焦点を絞った回帰テストを追加するか見つけます。両方の popover が有効な positioning anchor を保持し、無関係な名前を削除しなければ完了です。
索引モデルが issue の本文から書いたものです。
説明
Summary
In @fluentui/react-headless-components-preview, usePositioning writes a single, unique anchor-name onto the trigger element and overwrites any value already there. When two positioned popovers share one trigger — e.g. a hover Tooltip + a click Menu on the same button — the two usePositioning instances clobber each other's anchor-name. Only the last writer's popover stays anchored; the other points its position-anchor at a name that no longer exists and falls back to default (static / top‑left) placement.
This blocks a common pattern (icon-only "more actions" buttons that need both a label tooltip and a menu).
Repro
<Menu>
<MenuTrigger>
<Tooltip content="More actions" relationship="label">
<Button icon={<MoreHorizontalRegular />} />
</Tooltip>
</MenuTrigger>
<MenuPopover><MenuList>{/* … */}</MenuList></MenuPopover>
</Menu>
Hover the button → the tooltip appears detached from the trigger (top‑left of the viewport) instead of beside it. The menu (click) positions correctly.
Root cause
library/src/hooks/usePositioning/usePositioning.ts:
// L50
const anchorName = `--${useId('popover-anchor-')}`; // unique per hook instance
// L71–80
useIsomorphicLayoutEffect(() => {
if (!effectiveTarget) return;
effectiveTarget.style.setProperty('anchor-name', anchorName); // ← OVERWRITES
return () => {
effectiveTarget.style.removeProperty('anchor-name'); // ← REMOVES ALL
};
}, [effectiveTarget, anchorName]);
// L105
node.style.setProperty('position-anchor', anchorName); // popover side
anchor-name is a single-valued property here, but CSS allows a list. Two hooks targeting the same element each call setProperty('anchor-name', …), so the second effect to run wins and the first popover is orphaned.
Proof (rendered DOM, menu closed + tooltip visible)
button anchor-name = "--popover-anchor-r1" ← only the MENU's name survives
div[role=tooltip popover=hint] position-anchor = "--popover-anchor-r2" ← orphaned (r2 unmapped)
div[role=presentation popover=auto] position-anchor = "--popover-anchor-r1" ← menu, OK
Control (Tooltip alone) works — button anchor-name = r0, tooltip position-anchor = r0 (match).
Proposed fix 1 — minimal (append to the anchor-name list)
Make each usePositioning instance add its name to the trigger's anchor-name list and remove only its own on cleanup, instead of overwriting:
useIsomorphicLayoutEffect(() => {
if (!effectiveTarget) return;
const read = () =>
effectiveTarget.style
.getPropertyValue('anchor-name')
.split(',').map(s => s.trim()).filter(Boolean);
const next = read();
if (!next.includes(anchorName)) {
next.push(anchorName);
effectiveTarget.style.setProperty('anchor-name', next.join(', '));
}
return () => {
const remaining = read().filter(n => n !== anchorName);
if (remaining.length) {
effectiveTarget.style.setProperty('anchor-name', remaining.join(', '));
} else {
effectiveTarget.style.removeProperty('anchor-name');
}
};
}, [effectiveTarget, anchorName]);
This preserves any app-authored anchor-name, supports N popovers per trigger, and is fully backward compatible (single-popover triggers are unchanged). Each popover keeps its own unique position-anchor, which now resolves because the trigger carries every name.
A focused regression test would assert that, with two usePositioning instances on one element, the element's anchor-name contains both names.
Proposed fix 2 — stretch (declarative intent via Interest Invokers)
Today useTooltip drives hover/focus entirely in JS (onPointerEnter/onFocus + showDelay timer + showPopover()). The tooltip content already renders as popover="hint". The web platform now offers a declarative primitive for exactly this — Interest Invokers (interestfor + interest-delay) — which the browser uses to open a popover=hint on hover/focus with no JS.
Adopt it progressively in library/src/components/Tooltip/useTooltip.ts:
const supportsInterest =
typeof CSS !== 'undefined' && CSS.supports?.('interest-delay: 0s');
state.children = applyTriggerPropsToChildren(children, {
...triggerAriaProps,
...child?.props,
ref,
// Declarative intent where supported; points at the popover=hint content
interestfor: state.content.id,
// Keep the JS hover/focus engine ONLY as a fallback (avoids double-fire)
...(supportsInterest ? {} : {
onPointerEnter: /* existing */,
onPointerLeave: /* existing */,
onFocus: /* existing */,
onBlur: /* existing */,
}),
});
Feature gate: @supports (interest-delay: 0s) / CSS.supports('interest-delay: 0s'). Set interest-delay: 0s on the content to match the existing JS timing.
Benefits: zero JS hover engine in modern browsers, native top‑layer + accessibility semantics, and the existing JS path remains as graceful degradation. Combined with fix 1, a single trigger can carry interestfor (tooltip) and popovertarget/menu click on one shared anchor — fully declarative.
Current workaround for consumers (until fix 1 ships)
For teams hitting this now (icon button needing tooltip and menu):
- Preferred — use the stable
@fluentui/react-componentsTooltip + Menu for this one composition. Those use floating‑ui, which computes each popover's position independently and does not rely on a shared CSSanchor-name, so there is no collision. Swap back to the headless/modern components once fix 1 lands. - If you must stay on the headless components, give the tooltip and the menu distinct trigger elements so their
anchor-names don't collide (e.g. don't nestTooltipinsideMenuTriggeron the same button — anchor the tooltip to a wrapping element and the menu to the button). Visually identical, but each popover anchors to a different node.
Both are temporary; fix 1 removes the need for either.
Environment
@fluentui/react-headless-components-preview(observed0.2.0; logic unchanged onmaster)- Source:
packages/react-components/react-headless-components-preview/library/src/hooks/usePositioning/usePositioning.ts
- 主要言語
- TypeScript
- スター
- 20.3k
- フォーク
- 2.9k
- 平均マージ
- 2日 6時間
- マージ済み PR(30日)
- 72
環境構築
このプロジェクトの開発コンテナを、あなたの GitHub アカウントでブラウザ上に起動します。
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドなし
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
microsoft/fluentui のほかの issue
-
[Bug]: Vertical Slider still logs the deprecated 'slider-vertical' warning in Chromium 132+対応中かも @jakesalvesen が 2 日前に担当しました。 オープンFluent UI react-components (v9) Needs: Triage :mag: Type: Bug :bug:
難易度 2/5 1〜3時間 初心者へのやさしさ 74/100
microsoft/fluentui#36945 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
メンテナーはふだん 1 日以内に返信
-
@fluentui/web-components: fluent-menu-list loses its arrow keys after it is moved in the DOM (Firefox, Safari)対応中かも @asxxb が 9 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
メンテナーはふだん 1 日以内に返信
-
[Feature]: export getHashCode for Avatar from avatar.ts for Fluentv8 PersonaInitialsColor / PersonaCoin support対応中かも @ValentinaKozlova が 22 日前に担当しました。 オープンComponent: Avatar Fluent UI react-components (v9) From Shield Needs: Backlog review Type: Feature
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
microsoft/fluentui#36802 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
microsoft/fluentui の issue をすべて見る
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
chaitin/MonkeyCode#1437 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 69/100
carbon-design-system/carbon-ai-chat#2615 ·
メンテナーはふだん 1 日以内に返信
-
e2e-failure ready-to-code
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
redhat-developer/rhdh-plugin-export-overlays#4486 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
[Bug]: fileUploadMatrix.test.ts URL case does a real DNS lookup and fails offline対応中かも @r2jaff が今日担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
ComposioHQ/composio#5054 ·
メンテナーはふだん 1 日以内に返信