Skip to content

feat(positioning): implement useSafeZoneArea() hook #34445

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

layershifter
Copy link
Member

@layershifter layershifter commented May 14, 2025

New Behavior

Note: this PR does not include integration to any component, but this will be used in Menu later.

This PR implements useSafeZone() hook which calculates a V-shaped safe zone where a mouse cursor is temporary trapped and prevents a menu from being closed:

function App() {
  const safeZoneArea = useSafeZoneArea({
    debug: true,
    timeout: 100000,
    onSafeZoneLeave: () => {},
    onSafeZoneEnter: () => {},
    onSafeZoneTimeout: () => {}
  });

  return (
    <>
      <button ref={safeZoneArea.targetRef}>
        TRIGGER
      </button>

      <Portal>
        <div ref={safeZoneArea.containerRef}>
          POPOVER
        </div>
        /* 💡 SVG to render */
        {safeZoneArea.elementToRender}
      </Portal>
    </>
  );
};

This is similar to to safePolygon() in Floating UI, however is implemented using SVGs (the solution is inspired by https://www.smashingmagazine.com/2023/08/better-context-menus-safe-triangles/).

Why SVGs over JS?

SVG elements are still HTML elements ➡️ they are a part of DOM and mouse capturing that allows to work with mouse events and avoid blocking pointerEvents on document.body when a cursor is in a safe zone:

To clarify, the problem is not to block pointer events, the problem to ensure that they will be unblocked - otherwise the whole app will be unusable.

It's also easier to debug potential issues when something goes wrong as SVGs could be visible.

With SVGs we can explore more complicated shapes in future:

image

However, considering that other libraries use JS over SVGs - there might be some problems that I am not aware yet ¯_(ツ)_/¯

How it works?

  • Once a user enters a trigger (target) element we will draw a SVG with a triangle, a triangle itself is a safe zone
  • When a user moves mouse over a trigger, we update a triangle to match the cursor position
  • If a user moves mouse directly to a container (popover), we immediately hide a safe zone
  • If a user keeps mouse over a safe zone for timeout, we hide a safe zone
2025-05-14.15.46.56.mp4

@layershifter layershifter force-pushed the feat/menu-safe-zone branch from 1880dbe to 9531b6f Compare May 14, 2025 13:33
Copy link

Pull request demo site: URL

@layershifter layershifter force-pushed the feat/menu-safe-zone branch from 9531b6f to 6a5d3dd Compare May 14, 2025 13:40
Copy link

github-actions bot commented May 14, 2025

📊 Bundle size report

Package & Exports Baseline (minified/GZIP) PR Change
react-positioning
useSafeZoneArea
0 B
0 B
11.984 kB
4.637 kB
🆕 New entry
Unchanged fixtures
Package & Exports Size (minified/GZIP)
react-avatar
Avatar
49.376 kB
15.835 kB
react-avatar
AvatarGroup
20.177 kB
7.974 kB
react-avatar
AvatarGroupItem
63.52 kB
20.047 kB
react-breadcrumb
@fluentui/react-breadcrumb - package
114.855 kB
31.795 kB
react-checkbox
Checkbox
35.189 kB
12.093 kB
react-combobox
Combobox (including child components)
107.617 kB
35.006 kB
react-combobox
Dropdown (including child components)
108.241 kB
34.952 kB
react-components
react-components: Button, FluentProvider & webLightTheme
69.732 kB
20.259 kB
react-components
react-components: Accordion, Button, FluentProvider, Image, Menu, Popover
227.132 kB
65.811 kB
react-components
react-components: FluentProvider & webLightTheme
44.567 kB
14.62 kB
react-components
react-components: entire library
1.23 MB
309.987 kB
react-datepicker-compat
DatePicker Compat
226.856 kB
64.285 kB
react-dialog
Dialog (including children components)
102.102 kB
30.643 kB
react-field
Field
23.515 kB
8.917 kB
react-input
Input
28.085 kB
9.456 kB
react-list
List
89.47 kB
26.644 kB
react-list
ListItem
112.813 kB
33.432 kB
react-menu
Menu (including children components)
156.997 kB
47.203 kB
react-menu
Menu (including selectable components)
159.979 kB
47.804 kB
react-overflow
hooks only
12.832 kB
4.828 kB
react-persona
Persona
56.267 kB
17.712 kB
react-popover
Popover
132.54 kB
41.322 kB
react-portal-compat
PortalCompatProvider
8.386 kB
2.624 kB
react-positioning
usePositioning
28.865 kB
10.146 kB
react-progress
ProgressBar
17.156 kB
6.9 kB
react-radio
Radio
32.743 kB
10.357 kB
react-radio
RadioGroup
15.831 kB
6.446 kB
react-select
Select
27.804 kB
10.14 kB
react-slider
Slider
38.3 kB
12.836 kB
react-spinbutton
SpinButton
35.284 kB
11.765 kB
react-swatch-picker
@fluentui/react-swatch-picker - package
106.687 kB
30.807 kB
react-switch
Switch
35.371 kB
11.336 kB
react-table
DataGrid
161.242 kB
45.689 kB
react-table
Table (Primitives only)
42.764 kB
13.871 kB
react-table
Table as DataGrid
132.05 kB
36.604 kB
react-table
Table (Selection only)
70.632 kB
20.011 kB
react-table
Table (Sort only)
69.275 kB
19.623 kB
react-tag-picker
@fluentui/react-tag-picker - package
188.516 kB
56.55 kB
react-tags
InteractionTag
15.506 kB
6.232 kB
react-tags
Tag
30.306 kB
9.905 kB
react-tags
TagGroup
83.817 kB
24.856 kB
react-teaching-popover
TeachingPopover
102.316 kB
30.658 kB
react-textarea
Textarea
26.652 kB
9.764 kB
react-timepicker-compat
TimePicker
110.6 kB
36.56 kB
react-tooltip
Tooltip
58.64 kB
20.312 kB
react-tree
FlatTree
148.593 kB
42.623 kB
react-tree
PersonaFlatTree
149.345 kB
42.738 kB
react-tree
PersonaTree
145.604 kB
41.626 kB
react-tree
Tree
144.858 kB
41.498 kB
🤖 This report was generated against 4631e680f01cc5ff7fb6f27bc3e599bc442ed1f1

@layershifter layershifter force-pushed the feat/menu-safe-zone branch from 6a5d3dd to ef32e89 Compare May 14, 2025 13:54
@@ -0,0 +1,132 @@
import * as React from 'react';
Copy link

@github-actions github-actions bot May 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🕵🏾‍♀️ visual changes to review in the Visual Change Report

vr-tests-react-components/Drawer 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Drawer.overlay drawer full - Dark Mode.chromium.png 2647 Changed
vr-tests-react-components/Drawer.overlay drawer full - High Contrast.chromium.png 6684 Changed
vr-tests-react-components/Positioning 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Positioning.Positioning end.chromium.png 825 Changed
vr-tests-react-components/Positioning.Positioning end.updated 2 times.chromium.png 317 Changed
vr-tests-react-components/Positioning (safe area) 1 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Positioning (safe area).using safe zone area.chromium.png 0 Added
vr-tests-react-components/Skeleton converged 1 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Skeleton converged.Opaque Skeleton with square - Dark Mode.default.chromium.png 2 Changed
vr-tests-react-components/TagPicker 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/TagPicker.disabled - RTL.disabled input hover.chromium.png 635 Changed
vr-tests-react-components/TagPicker.disabled - High Contrast.chromium.png 1321 Changed

There were 2 duplicate changes discarded. Check the build logs for more information.

@layershifter layershifter marked this pull request as ready for review May 14, 2025 15:07
@layershifter layershifter requested review from a team as code owners May 14, 2025 15:07

switch (containerPlacement.slice(0, 3)) {
case 'top':
svgStyle = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we extract the svg creation into testable factories?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that we need to extract them, I added tests for the component

@layershifter layershifter added the Status: Blocked Resolution blocked by another issue label May 15, 2025
@layershifter
Copy link
Member Author

Do not merge until it's tested in a product.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Status: Blocked Resolution blocked by another issue
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants