[react-headless-components-preview] usePositioning overwrites anchor-name → Tooltip+Menu on one trigger breaks positioning
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 3/5
- Thời gian dự kiến
- 1-2 ngày
- Mức phù hợp với người mới
- 68/100
- Loại issue
- Lỗi
- Độ rõ ràng
- Khá rõ ràng
- Mức độ hoạt động
- Ít trao đổi
- Công nghệ
- react, typescript
- Lĩnh vực
- frontend
Hướng nghiên cứu
Bắt đầu trong packages/react-components/react-headless-components-preview/library/src/hooks/usePositioning/usePositioning.ts và tái hiện composition Tooltip-plus-Menu từ issue. Theo dõi effect và cleanup của anchor-name, sau đó thêm hoặc tìm một regression test tập trung bao quát hai instance usePositioning trên cùng một trigger. Hoàn tất khi cả hai popover vẫn giữ các positioning anchor hợp lệ mà không xóa các tên không liên quan.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
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
- Ngôn ngữ chính
- TypeScript
- Star
- 20.3k
- Fork
- 2.9k
- Merge trung bình
- 2 ngày 7 giờ
- Pull request đã merge (30 ngày)
- 69
Chuẩn bị môi trường
Khởi chạy dev container của dự án ngay trên trình duyệt, bằng tài khoản GitHub của bạn.
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Không có hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của microsoft/fluentui
-
[Bug]: Vertical Slider still logs the deprecated 'slider-vertical' warning in Chromium 132+Có thể đã có người làm @jakesalvesen đã nhận hôm nay. Đang mởFluent UI react-components (v9) Needs: Triage :mag: Type: Bug :bug:
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 74/100
microsoft/fluentui#36945 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
Maintainer thường phản hồi trong vòng 1 ngày
-
@fluentui/web-components: fluent-menu-list loses its arrow keys after it is moved in the DOM (Firefox, Safari)Có thể đã có người làm @asxxb đã nhận 7 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 84/100
Maintainer thường phản hồi trong vòng 1 ngày
-
[Feature]: export getHashCode for Avatar from avatar.ts for Fluentv8 PersonaInitialsColor / PersonaCoin supportCó thể đã có người làm @ValentinaKozlova đã nhận 20 ngày trước. Đang mởComponent: Avatar Fluent UI react-components (v9) From Shield Needs: Backlog review Type: Feature
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
microsoft/fluentui#36802 · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của microsoft/fluentui
Issue tương tự
-
RED ladder stopped @ 4db4ed91Đang mởarea:workflow bug ready-for-human
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 78/100
fil-donadoni/tolaria#6108 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
apache/rocketmq-dashboard#7234 ·
Maintainer thường phản hồi trong vòng 3 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
vitest-dev/vitest#11656 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
e2e-failure ready-to-code
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
redhat-developer/rhdh-plugin-export-overlays#4460 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 62/100
ehmpathy/rhachet-roles-bhrain#655 ·
Maintainer thường phản hồi trong vòng 1 ngày