Skip to content
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

Expose arrowBoundaryOffset and internalize arrowSize calculation on RAC <Popover /> and <Tooltip /> #5936

Merged
merged 8 commits into from
Mar 15, 2024
21 changes: 16 additions & 5 deletions packages/react-aria-components/src/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@

import {AriaPopoverProps, DismissButton, Overlay, PlacementAxis, PositionProps, usePopover} from 'react-aria';
import {ContextValue, forwardRefType, HiddenContext, RenderProps, SlotProps, useContextProps, useEnterAnimation, useExitAnimation, useRenderProps} from './utils';
import {filterDOMProps, mergeProps} from '@react-aria/utils';
import {filterDOMProps, mergeProps, useLayoutEffect} from '@react-aria/utils';
import {OverlayArrowContext} from './OverlayArrow';
import {OverlayTriggerProps, OverlayTriggerState, useOverlayTriggerState} from 'react-stately';
import {OverlayTriggerStateContext} from './Dialog';
import React, {createContext, ForwardedRef, forwardRef, RefObject, useContext} from 'react';
import React, {createContext, ForwardedRef, forwardRef, RefObject, useContext, useRef, useState} from 'react';

export interface PopoverProps extends Omit<PositionProps, 'isOpen'>, Omit<AriaPopoverProps, 'popoverRef' | 'triggerRef' | 'offset'>, OverlayTriggerProps, RenderProps<PopoverRenderProps>, SlotProps {
export interface PopoverProps extends Omit<PositionProps, 'isOpen'>, Omit<AriaPopoverProps, 'popoverRef' | 'triggerRef' | 'offset' | 'arrowSize'>, OverlayTriggerProps, RenderProps<PopoverRenderProps>, SlotProps {
/**
* The name of the component that triggered the popover. This is reflected on the element
* as the `data-trigger` attribute, and can be used to provide specific
Expand Down Expand Up @@ -130,9 +130,20 @@ interface PopoverInnerProps extends AriaPopoverProps, RenderProps<PopoverRenderP
}

function PopoverInner({state, isExiting, UNSTABLE_portalContainer, ...props}: PopoverInnerProps) {
// Calculate the arrow size internally (and remove props.arrowSize from PopoverProps)
// Referenced from: packages/@react-spectrum/tooltip/src/TooltipTrigger.tsx
let arrowRef = useRef<HTMLDivElement>(null);
let [arrowWidth, setArrowWidth] = useState(0);
useLayoutEffect(() => {
if (arrowRef.current && state.isOpen) {
setArrowWidth(arrowRef.current.getBoundingClientRect().width);
}
}, [state.isOpen, arrowRef]);

let {popoverProps, underlayProps, arrowProps, placement} = usePopover({
...props,
offset: props.offset ?? 8
offset: props.offset ?? 8,
arrowSize: arrowWidth
}, state);

let ref = props.popoverRef as RefObject<HTMLDivElement>;
Expand Down Expand Up @@ -164,7 +175,7 @@ function PopoverInner({state, isExiting, UNSTABLE_portalContainer, ...props}: Po
data-entering={isEntering || undefined}
data-exiting={isExiting || undefined}>
{!props.isNonModal && <DismissButton onDismiss={state.close} />}
<OverlayArrowContext.Provider value={{...arrowProps, placement}}>
<OverlayArrowContext.Provider value={{...arrowProps, placement, ref: arrowRef}}>
{renderProps.children}
</OverlayArrowContext.Provider>
<DismissButton onDismiss={state.close} />
Expand Down
23 changes: 18 additions & 5 deletions packages/react-aria-components/src/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,19 @@
*/

import {AriaLabelingProps, FocusableElement} from '@react-types/shared';
import {AriaPositionProps, mergeProps, OverlayContainer, PlacementAxis, PositionProps, useOverlayPosition, useTooltip, useTooltipTrigger} from 'react-aria';
import {ContextValue, forwardRefType, Provider, RenderProps, useContextProps, useEnterAnimation, useExitAnimation, useRenderProps} from './utils';
import {FocusableProvider} from '@react-aria/focus';
import {mergeProps, OverlayContainer, PlacementAxis, PositionProps, useOverlayPosition, useTooltip, useTooltipTrigger} from 'react-aria';
import {OverlayArrowContext} from './OverlayArrow';
import {OverlayTriggerProps, TooltipTriggerProps, TooltipTriggerState, useTooltipTriggerState} from 'react-stately';
import React, {createContext, ForwardedRef, forwardRef, ReactNode, RefObject, useContext, useRef} from 'react';
import React, {createContext, ForwardedRef, forwardRef, ReactNode, RefObject, useContext, useRef, useState} from 'react';
import {useLayoutEffect} from '@react-aria/utils';

export interface TooltipTriggerComponentProps extends TooltipTriggerProps {
children: ReactNode
}

export interface TooltipProps extends PositionProps, OverlayTriggerProps, AriaLabelingProps, RenderProps<TooltipRenderProps> {
export interface TooltipProps extends PositionProps, Pick<AriaPositionProps, 'arrowBoundaryOffset'>, OverlayTriggerProps, AriaLabelingProps, RenderProps<TooltipRenderProps> {
/**
* The ref for the element which the tooltip positions itself with respect to.
*
Expand Down Expand Up @@ -118,13 +119,25 @@ export {_Tooltip as Tooltip};
function TooltipInner(props: TooltipProps & {isExiting: boolean, tooltipRef: RefObject<HTMLDivElement>}) {
let state = useContext(TooltipTriggerStateContext)!;

// Calculate the arrow size internally
// Referenced from: packages/@react-spectrum/tooltip/src/TooltipTrigger.tsx
let arrowRef = useRef<HTMLDivElement>(null);
let [arrowWidth, setArrowWidth] = useState(0);
useLayoutEffect(() => {
if (arrowRef.current && state.isOpen) {
setArrowWidth(arrowRef.current.getBoundingClientRect().width);
}
}, [state.isOpen, arrowRef]);

let {overlayProps, arrowProps, placement} = useOverlayPosition({
placement: props.placement || 'top',
targetRef: props.triggerRef!,
overlayRef: props.tooltipRef,
offset: props.offset,
crossOffset: props.crossOffset,
isOpen: state.isOpen
isOpen: state.isOpen,
arrowSize: arrowWidth,
arrowBoundaryOffset: props.arrowBoundaryOffset
});

let isEntering = useEnterAnimation(props.tooltipRef, !!placement) || props.isEntering || false;
Expand All @@ -151,7 +164,7 @@ function TooltipInner(props: TooltipProps & {isExiting: boolean, tooltipRef: Ref
data-placement={placement}
data-entering={isEntering || undefined}
data-exiting={props.isExiting || undefined}>
<OverlayArrowContext.Provider value={{...arrowProps, placement}}>
<OverlayArrowContext.Provider value={{...arrowProps, placement, ref: arrowRef}}>
{renderProps.children}
</OverlayArrowContext.Provider>
</div>
Expand Down
217 changes: 216 additions & 1 deletion packages/react-aria-components/stories/Popover.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* governing permissions and limitations under the License.
*/

import {Button, Dialog, DialogTrigger, Heading, Popover} from 'react-aria-components';
import {Button, Dialog, DialogTrigger, Heading, OverlayArrow, Popover} from 'react-aria-components';
import React from 'react';

export default {
Expand Down Expand Up @@ -48,3 +48,218 @@ export const PopoverExample = () => (
</Popover>
</DialogTrigger>
);

export const PopoverArrowBoundaryOffsetExample = () => {
return (
<div style={{display: 'flex', flexDirection: 'column'}}>
<div style={{display: 'flex'}}>
<div style={{padding: 12}}>
<DialogTrigger>
<Button style={{width: 200, height: 100}}>Top left</Button>
<Popover
placement="top left"
arrowBoundaryOffset={25}
sookmax marked this conversation as resolved.
Show resolved Hide resolved
style={{
background: 'Canvas',
color: 'CanvasText',
border: '1px solid gray',
padding: 8,
zIndex: 5,
borderRadius: '30px'
}}>
<OverlayArrow style={{display: 'flex'}}>
<svg width="12" height="12" viewBox="0 0 12 12" style={{display: 'block'}}>
<path d="M0 0,L6 6,L12 0" fill="white" strokeWidth={1} stroke="gray" />
</svg>
</OverlayArrow>
<Dialog style={{outline: 'none'}}>
<div>Top left</div>
</Dialog>
</Popover>
</DialogTrigger>
</div>
<div style={{padding: 12}}>
<DialogTrigger>
<Button style={{width: 200, height: 100}}>Top right</Button>
<Popover
placement="top right"
arrowBoundaryOffset={25}
style={{
background: 'Canvas',
color: 'CanvasText',
border: '1px solid gray',
padding: 8,
zIndex: 5,
borderRadius: '30px'
}}>
<OverlayArrow style={{display: 'flex'}}>
<svg width="12" height="12" viewBox="0 0 12 12" style={{display: 'block'}}>
<path d="M0 0,L6 6,L12 0" fill="white" strokeWidth={1} stroke="gray" />
</svg>
</OverlayArrow>
<Dialog style={{outline: 'none'}}>
<div>Top right</div>
</Dialog>
</Popover>
</DialogTrigger>
</div>
</div>
<div style={{display: 'flex'}}>
<div style={{padding: 12}}>
<DialogTrigger>
<Button style={{width: 200, height: 100}}>Left top</Button>
<Popover
placement="left top"
arrowBoundaryOffset={15}
style={{
background: 'Canvas',
color: 'CanvasText',
border: '1px solid gray',
padding: 8,
zIndex: 5,
borderRadius: '30px'
}}>
<OverlayArrow style={{display: 'flex'}}>
<svg width="12" height="12" viewBox="0 0 12 12" style={{display: 'block', transform: 'rotate(-90deg)'}}>
<path d="M0 0,L6 6,L12 0" fill="white" strokeWidth={1} stroke="gray" />
</svg>
</OverlayArrow>
<Dialog style={{outline: 'none'}}>
<div>Left top</div>
</Dialog>
</Popover>
</DialogTrigger>
</div>
<div style={{padding: 12}}>
<DialogTrigger>
<Button style={{width: 200, height: 100}}>Left bottom</Button>
<Popover
placement="left bottom"
arrowBoundaryOffset={15}
style={{
background: 'Canvas',
color: 'CanvasText',
border: '1px solid gray',
padding: 8,
zIndex: 5,
borderRadius: '30px'
}}>
<OverlayArrow style={{display: 'flex'}}>
<svg width="12" height="12" viewBox="0 0 12 12" style={{display: 'block', transform: 'rotate(-90deg)'}}>
<path d="M0 0,L6 6,L12 0" fill="white" strokeWidth={1} stroke="gray" />
</svg>
</OverlayArrow>
<Dialog style={{outline: 'none'}}>
<div>Left bottom</div>
</Dialog>
</Popover>
</DialogTrigger>
</div>
</div>
<div style={{display: 'flex'}}>
<div style={{padding: 12}}>
<DialogTrigger>
<Button style={{width: 200, height: 100}}>Right top</Button>
<Popover
placement="right top"
arrowBoundaryOffset={15}
style={{
background: 'Canvas',
color: 'CanvasText',
border: '1px solid gray',
padding: 8,
zIndex: 5,
borderRadius: '30px'
}}>
<OverlayArrow style={{display: 'flex'}}>
<svg width="12" height="12" viewBox="0 0 12 12" style={{display: 'block', transform: 'rotate(90deg)'}}>
<path d="M0 0,L6 6,L12 0" fill="white" strokeWidth={1} stroke="gray" />
</svg>
</OverlayArrow>
<Dialog style={{outline: 'none'}}>
<div>Right top</div>
</Dialog>
</Popover>
</DialogTrigger>
</div>
<div style={{padding: 12}}>
<DialogTrigger>
<Button style={{width: 200, height: 100}}>Right bottom</Button>
<Popover
placement="right bottom"
arrowBoundaryOffset={15}
style={{
background: 'Canvas',
color: 'CanvasText',
border: '1px solid gray',
padding: 8,
zIndex: 5,
borderRadius: '30px'
}}>
<OverlayArrow style={{display: 'flex'}}>
<svg width="12" height="12" viewBox="0 0 12 12" style={{display: 'block', transform: 'rotate(90deg)'}}>
<path d="M0 0,L6 6,L12 0" fill="white" strokeWidth={1} stroke="gray" />
</svg>
</OverlayArrow>
<Dialog style={{outline: 'none'}}>
<div>Right bottom</div>
</Dialog>
</Popover>
</DialogTrigger>
</div>
</div>
<div style={{display: 'flex'}}>
<div style={{padding: 12}}>
<DialogTrigger>
<Button style={{width: 200, height: 100}}>Bottom left</Button>
<Popover
placement="bottom left"
arrowBoundaryOffset={25}
style={{
background: 'Canvas',
color: 'CanvasText',
border: '1px solid gray',
padding: 8,
zIndex: 5,
borderRadius: '30px'
}}>
<OverlayArrow style={{display: 'flex'}}>
<svg width="12" height="12" viewBox="0 0 12 12" style={{display: 'block', transform: 'rotate(180deg)'}}>
<path d="M0 0,L6 6,L12 0" fill="white" strokeWidth={1} stroke="gray" />
</svg>
</OverlayArrow>
<Dialog style={{outline: 'none'}}>
<div>Bottom left</div>
</Dialog>
</Popover>
</DialogTrigger>
</div>
<div style={{padding: 12}}>
<DialogTrigger>
<Button style={{width: 200, height: 100}}>Bottom right</Button>
<Popover
placement="bottom right"
arrowBoundaryOffset={25}
style={{
background: 'Canvas',
color: 'CanvasText',
border: '1px solid gray',
padding: 8,
zIndex: 5,
borderRadius: '30px'
}}>
<OverlayArrow style={{display: 'flex'}}>
<svg width="12" height="12" viewBox="0 0 12 12" style={{display: 'block', transform: 'rotate(180deg)'}}>
<path d="M0 0,L6 6,L12 0" fill="white" strokeWidth={1} stroke="gray" />
</svg>
</OverlayArrow>
<Dialog style={{outline: 'none'}}>
<div>Bottom right</div>
</Dialog>
</Popover>
</DialogTrigger>
</div>
</div>
</div>
);
};
Loading