-
Notifications
You must be signed in to change notification settings - Fork 2.9k
fix(react-positioning): don't treat static overflow:hidden ancestors as a hide-middleware clipping boundary #36605
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
Paul Mardling (PaulGMardling)
wants to merge
12
commits into
microsoft:master
Choose a base branch
from
PaulGMardling:fix/36604-tooltip-escaped-hide-boundary
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 3 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
9e4ee6d
fix(react-positioning): don't treat static overflow:hidden ancestors …
PaulGMardling 05f0206
test(react-positioning): pin hide() wrapper's boundary mapping and do…
PaulGMardling db252ac
fix(react-tooltip): split TooltipOverflowHidden.stories.tsx into sing…
PaulGMardling 649edda
Merge branch 'master' into fix/36604-tooltip-escaped-hide-boundary
PaulGMardling c4671f6
Merge branch 'master' into fix/36604-tooltip-escaped-hide-boundary
PaulGMardling 8485cfb
test(react-tooltip): clarify overflow regression suite name
PaulGMardling 006c67e
refactor(react-positioning): inline hide boundary mapping
PaulGMardling 1b22a42
fix(react-tooltip): scope hide boundary behavior to tooltips
PaulGMardling a8f6855
test(react-tooltip): cover nested static overflow:hidden inside a scr…
PaulGMardling c8cefc9
test(react-positioning): add Jest coverage for scrollParent boundary …
PaulGMardling b74bd10
test(react-tooltip): add Jest/JSDOM regression test for #36604 static…
PaulGMardling 98fa096
test(react-tooltip): drop fragile heavy-mock hide/escaped integration…
PaulGMardling File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
7 changes: 7 additions & 0 deletions
7
change/@fluentui-react-positioning-95a68719-9f3f-4da2-8ac8-e2c22ecd0b4d.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| "type": "patch", | ||
| "comment": "fix: do not treat non-scrolling overflow:hidden ancestors as a clipping boundary for the hide middleware's escaped/referenceHidden detection", | ||
| "packageName": "@fluentui/react-positioning", | ||
| "email": "paulmardling@microsoft.com", | ||
| "dependentChangeType": "patch" | ||
| } | ||
53 changes: 53 additions & 0 deletions
53
packages/react-components/react-positioning/library/src/middleware/hide.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| import { hide as baseHide } from '@floating-ui/dom'; | ||
| import { hide } from './hide'; | ||
|
|
||
| jest.mock('@floating-ui/dom', () => { | ||
| const actual = jest.requireActual('@floating-ui/dom'); | ||
| return { | ||
| ...actual, | ||
| hide: jest.fn(actual.hide), | ||
| }; | ||
| }); | ||
|
|
||
| /** | ||
| * `./hide.ts` relies on an implicit, lightly-documented behavior of `@floating-ui/dom`'s `hide` | ||
| * middleware: passing `boundary: []` (as opposed to the default `'clippingAncestors'`) causes its | ||
| * internal `getClippingRect` to skip all intermediate DOM clipping ancestors and only consider the | ||
| * `rootBoundary` (the viewport, by default) — see #36604 and the comment in `./hide.ts`. | ||
| * | ||
| * That deeper, real-browser geometry contract (that `boundary: []` genuinely behaves as | ||
| * "viewport-only") is covered by the Cypress regression tests in react-tooltip's `Tooltip.cy.tsx` | ||
| * (both the pre-existing scroll-based test for #32882, and the new static `overflow: hidden` test | ||
| * for #36604) — a real browser is required to reliably exercise floating-ui's | ||
| * offset-parent/scale/layout math; jsdom's emulation of that math doesn't match real browser | ||
| * behavior closely enough to pin it in a unit test here. | ||
| * | ||
| * These tests instead pin the narrower, fully deterministic thing this module is responsible for: | ||
| * that it maps `hasScrollableElement` to the correct `boundary` option passed to the underlying | ||
| * `@floating-ui/dom` `hide` middleware. | ||
| */ | ||
| describe('hide', () => { | ||
|
PaulGMardling marked this conversation as resolved.
Outdated
|
||
| afterEach(() => { | ||
| jest.clearAllMocks(); | ||
| }); | ||
|
|
||
| it.each([ | ||
| ['referenceHidden', true, 'clippingAncestors'], | ||
| ['referenceHidden', false, []], | ||
| ['escaped', true, 'clippingAncestors'], | ||
| ['escaped', false, []], | ||
| ] as const)( | ||
| 'strategy=%s, hasScrollableElement=%s -> boundary=%s', | ||
| (strategy, hasScrollableElement, expectedBoundary) => { | ||
| hide({ strategy, hasScrollableElement }); | ||
|
|
||
| expect(baseHide).toHaveBeenCalledWith({ strategy, boundary: expectedBoundary }); | ||
| }, | ||
| ); | ||
|
|
||
| it('defaults to a viewport-only boundary when hasScrollableElement is not provided', () => { | ||
| hide({ strategy: 'escaped' }); | ||
|
|
||
| expect(baseHide).toHaveBeenCalledWith({ strategy: 'escaped', boundary: [] }); | ||
| }); | ||
| }); | ||
43 changes: 43 additions & 0 deletions
43
packages/react-components/react-positioning/library/src/middleware/hide.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import type { Middleware } from '@floating-ui/dom'; | ||
| import { hide as baseHide } from '@floating-ui/dom'; | ||
|
|
||
| export interface HideMiddlewareOptions { | ||
|
PaulGMardling marked this conversation as resolved.
Outdated
|
||
| strategy: 'referenceHidden' | 'escaped'; | ||
| /** | ||
| * Whether the positioned element has a scrollable ancestor (an ancestor whose `overflow` is | ||
| * `auto`, `scroll`, or `overlay`), as opposed to merely a clipping one (`overflow: hidden` | ||
| * with no scrollable content). | ||
| * | ||
| * When there's no scrollable ancestor, non-scrolling `overflow: hidden` ancestors are excluded | ||
| * from the clipping boundary used to compute `referenceHidden`/`escaped`. Otherwise, a trigger | ||
| * placed in a tightly-fitted `overflow: hidden` container (a common layout pattern, e.g. a flex | ||
| * toolbar) would have its tooltip permanently hidden, even though nothing is actually being | ||
| * scrolled out of view. | ||
| */ | ||
| hasScrollableElement?: boolean; | ||
| } | ||
|
|
||
| /** | ||
| * Wraps the floating UI hide middleware for easier usage of our options | ||
| */ | ||
| export function hide(options: HideMiddlewareOptions): Middleware { | ||
| const { strategy, hasScrollableElement } = options; | ||
|
|
||
| return baseHide({ | ||
| strategy, | ||
| // Only consider intermediate clipping ancestors (including non-scrolling `overflow: hidden` | ||
| // containers) when there's an ancestor that can actually be scrolled. Otherwise, fall back to | ||
| // the viewport as the sole boundary by passing an empty array, so static, non-scrolling | ||
| // clipping ancestors aren't treated as a boundary an element can be "hidden" or "escaped" by. | ||
| // | ||
| // This relies on an implicit (undocumented in floating-ui's public docs, but verified against | ||
| // its source) behavior of `@floating-ui/dom@^1.6.12`'s `getClippingRect`: passing a `boundary` | ||
| // that isn't `'clippingAncestors'` (e.g. `[]`) skips all intermediate DOM clipping ancestors | ||
| // and only considers `rootBoundary` (the viewport, by default). This wrapper's own mapping of | ||
| // `hasScrollableElement` to `boundary` is pinned by unit tests in `./hide.test.ts`. The deeper | ||
| // real-browser geometry contract (that `boundary: []` genuinely behaves as viewport-only) is | ||
| // covered by react-tooltip's Cypress tests (`Tooltip.cy.tsx`, regressions #32882 and #36604) — | ||
| // if a future `@floating-ui/dom` upgrade changes this behavior, those should fail. | ||
| boundary: hasScrollableElement ? 'clippingAncestors' : [], | ||
| }); | ||
| } | ||
2 changes: 2 additions & 0 deletions
2
packages/react-components/react-positioning/library/src/middleware/index.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
50 changes: 50 additions & 0 deletions
50
...eact-components/react-tooltip/stories/src/Tooltip/TooltipStaticOverflowHidden.stories.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import * as React from 'react'; | ||
| import type { JSXElement } from '@fluentui/react-components'; | ||
| import { Button, makeStyles, tokens, Tooltip } from '@fluentui/react-components'; | ||
|
|
||
| const useStyles = makeStyles({ | ||
| root: { | ||
| display: 'flex', | ||
| flexDirection: 'column', | ||
| gap: tokens.spacingVerticalS, | ||
| alignItems: 'flex-start', | ||
| }, | ||
| description: { | ||
| margin: 0, | ||
| fontSize: tokens.fontSizeBase300, | ||
| }, | ||
| staticContainer: { | ||
| display: 'flex', | ||
| overflow: 'hidden', | ||
| border: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke1}`, | ||
| borderRadius: tokens.borderRadiusMedium, | ||
| padding: tokens.spacingVerticalXS, | ||
| }, | ||
| }); | ||
|
|
||
| export const StaticOverflowHidden = (): JSXElement => { | ||
| const styles = useStyles(); | ||
|
|
||
| return ( | ||
| <div className={styles.root}> | ||
| <p className={styles.description}> | ||
| The button below sits in a tightly-fitted, non-scrolling <code>overflow: hidden</code> container. The tooltip | ||
| should still appear on hover, since nothing is being scrolled out of view. | ||
| </p> | ||
| <div className={styles.staticContainer}> | ||
| <Tooltip content="I should still appear" relationship="label"> | ||
| <Button>Hover me</Button> | ||
| </Tooltip> | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| StaticOverflowHidden.parameters = { | ||
| docs: { | ||
| description: { | ||
| story: | ||
| 'A tooltip trigger placed inside a static, non-scrolling `overflow: hidden` container (e.g. a flex toolbar) should still show its tooltip, since it is not a scroll boundary being escaped.', | ||
| }, | ||
| }, | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.