-
Notifications
You must be signed in to change notification settings - Fork 221
chore: add optional parent boundary clamping for popover trigger positioning #4344
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
Changes from 7 commits
f99b7a0
3661f47
868ecf7
a6b5a9e
7899dc1
4a5ea43
562cb5e
88dff5c
843e241
ad2c82d
f22680b
4f229bd
8cd39b8
620c8e5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,6 +27,8 @@ interface PopoverContainerProps { | |
| */ | ||
| trackKey?: string | number; | ||
| minVisibleBlockSize?: number; | ||
| /** Optional parent element to clamp popover position within its bounds */ | ||
| parentRef?: React.RefObject<HTMLElement>; | ||
|
||
| position: PopoverProps.Position; | ||
| zIndex?: React.CSSProperties['zIndex']; | ||
| arrow: (position: InternalPosition | null) => React.ReactNode; | ||
|
|
@@ -52,6 +54,7 @@ export default function PopoverContainer({ | |
| trackRef, | ||
| getTrack: externalGetTrack, | ||
| trackKey, | ||
| parentRef, | ||
| minVisibleBlockSize, | ||
| arrow, | ||
| children, | ||
|
|
@@ -90,6 +93,7 @@ export default function PopoverContainer({ | |
| popoverRef, | ||
| bodyRef, | ||
| arrowRef, | ||
| parentRef, | ||
| getTrack: getTrack.current, | ||
| contentRef, | ||
| allowScrollToFit, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,13 +13,20 @@ import { | |
| scrollRectangleIntoView, | ||
| } from '../internal/utils/scrollable-containers'; | ||
| import { BoundingBox, InternalPosition, Offset, PopoverProps, Rect } from './interfaces'; | ||
| import { calculatePosition, getDimensions, getOffsetDimensions, isCenterOutside } from './utils/positions'; | ||
| import { | ||
| calculatePosition, | ||
| clampRectStart, | ||
| getDimensions, | ||
| getOffsetDimensions, | ||
| isCenterOutside, | ||
| } from './utils/positions'; | ||
|
|
||
| export default function usePopoverPosition({ | ||
| popoverRef, | ||
| bodyRef, | ||
| arrowRef, | ||
| getTrack, | ||
| parentRef, | ||
| contentRef, | ||
| allowScrollToFit, | ||
| allowVerticalOverflow, | ||
|
|
@@ -34,6 +41,7 @@ export default function usePopoverPosition({ | |
| arrowRef: React.RefObject<HTMLDivElement | null>; | ||
| getTrack: () => null | HTMLElement | SVGElement; | ||
| contentRef: React.RefObject<HTMLDivElement | null>; | ||
| parentRef?: React.RefObject<HTMLElement>; | ||
| allowScrollToFit?: boolean; | ||
| allowVerticalOverflow?: boolean; | ||
| preferredPosition: PopoverProps.Position; | ||
|
|
@@ -88,7 +96,7 @@ export default function usePopoverPosition({ | |
| // Get rects representing key elements | ||
| // Use getComputedStyle for arrowRect to avoid modifications made by transform | ||
| const viewportRect = getViewportRect(document.defaultView!); | ||
| const trackRect = getLogicalBoundingClientRect(track); | ||
| const trackRect = getClampedTrackRect(track, parentRef?.current); | ||
| const arrowRect = getDimensions(arrow); | ||
| const { containingBlock, boundary } = findUpUntilMultiple({ | ||
| startElement: popover, | ||
|
|
@@ -183,7 +191,7 @@ export default function usePopoverPosition({ | |
| if (!track) { | ||
| return; | ||
| } | ||
| const trackRect = getLogicalBoundingClientRect(track); | ||
| const trackRect = getClampedTrackRect(track, parentRef?.current); | ||
|
|
||
| const newTrackOffset = toRelativePosition( | ||
| trackRect, | ||
|
|
@@ -209,13 +217,14 @@ export default function usePopoverPosition({ | |
| bodyRef, | ||
| contentRef, | ||
| arrowRef, | ||
| parentRef, | ||
| keepPosition, | ||
| preferredPosition, | ||
| renderWithPortal, | ||
| allowVerticalOverflow, | ||
| minVisibleBlockSize, | ||
| allowScrollToFit, | ||
| hideOnOverscroll, | ||
| minVisibleBlockSize, | ||
| ] | ||
| ); | ||
| return { updatePositionHandler, popoverStyle, internalPosition, positionHandlerRef, isOverscrolling }; | ||
|
|
@@ -262,3 +271,11 @@ function isBoundary(element: HTMLElement) { | |
| const computedStyle = getComputedStyle(element); | ||
| return !!computedStyle.clipPath && computedStyle.clipPath !== 'none'; | ||
| } | ||
|
|
||
| function getClampedTrackRect(track: HTMLElement | SVGElement, parentRef?: HTMLElement | null) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add unit testing coverage for this new logic?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, sure! I just wanted to get your feedback on the implementation first.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks! Note, Codecov is still complaining about line 280 below not being covered. You might want to either add coverage for
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with the latter approach as well!
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for adding the test, although Codecov still complains about line 280 not being covered. Could this be because by asserting you are only ever checking the first call, which corresponds to when the ref is null? |
||
| const trackRect = getLogicalBoundingClientRect(track); | ||
| if (!parentRef) { | ||
| return trackRect; | ||
| } | ||
| return clampRectStart(trackRect, getLogicalBoundingClientRect(parentRef)); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -345,6 +345,14 @@ function isTopOrBottom(internalPosition: InternalPosition) { | |
| return ['top', 'bottom'].includes(internalPosition.split('-')[0]); | ||
| } | ||
|
|
||
| export function clampRectStart(rect: Rect, bounds: BoundingBox) { | ||
| const parentInlineEnd = bounds.insetInlineStart + bounds.inlineSize; | ||
| const parentBlockEnd = bounds.insetBlockStart + bounds.blockSize; | ||
| rect.insetInlineStart = Math.max(bounds.insetInlineStart, Math.min(rect.insetInlineStart, parentInlineEnd)); | ||
|
||
| rect.insetBlockStart = Math.max(bounds.insetBlockStart, Math.min(rect.insetBlockStart, parentBlockEnd)); | ||
| return rect; | ||
| } | ||
|
|
||
| export function isCenterOutside(child: Rect, parent: Rect) { | ||
| const childCenter = child.insetBlockStart + child.blockSize / 2; | ||
| const overflowsBlockStart = childCenter < parent.insetBlockStart; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to be updated to the new function name
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for missing this