From 0a84ded33ef324156c2ab5d2add91c3fe6aaf9e1 Mon Sep 17 00:00:00 2001 From: Sukkyu Chung Date: Fri, 15 Mar 2024 15:01:15 +0900 Subject: [PATCH] Expose `arrowBoundaryOffset ` and internalize `arrowSize` calculation on RAC `` and `` (#5936) Expose `arrowBoundaryOffset ` and internalize `arrowSize` calculation on RAC `` and `` (#5936) --- .../react-aria-components/src/Popover.tsx | 21 +- .../react-aria-components/src/Tooltip.tsx | 23 +- .../stories/Popover.stories.tsx | 285 +++++++++++++++++- .../stories/Tooltip.stories.tsx | 267 ++++++++++++++++ 4 files changed, 585 insertions(+), 11 deletions(-) diff --git a/packages/react-aria-components/src/Popover.tsx b/packages/react-aria-components/src/Popover.tsx index 058ae05b4a9..cceb6bd1117 100644 --- a/packages/react-aria-components/src/Popover.tsx +++ b/packages/react-aria-components/src/Popover.tsx @@ -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, Omit, OverlayTriggerProps, RenderProps, SlotProps { +export interface PopoverProps extends Omit, Omit, OverlayTriggerProps, RenderProps, 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 @@ -130,9 +130,20 @@ interface PopoverInnerProps extends AriaPopoverProps, RenderProps(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; @@ -164,7 +175,7 @@ function PopoverInner({state, isExiting, UNSTABLE_portalContainer, ...props}: Po data-entering={isEntering || undefined} data-exiting={isExiting || undefined}> {!props.isNonModal && } - + {renderProps.children} diff --git a/packages/react-aria-components/src/Tooltip.tsx b/packages/react-aria-components/src/Tooltip.tsx index 31a9895df9d..b3e77c29f85 100644 --- a/packages/react-aria-components/src/Tooltip.tsx +++ b/packages/react-aria-components/src/Tooltip.tsx @@ -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 { +export interface TooltipProps extends PositionProps, Pick, OverlayTriggerProps, AriaLabelingProps, RenderProps { /** * The ref for the element which the tooltip positions itself with respect to. * @@ -118,13 +119,25 @@ export {_Tooltip as Tooltip}; function TooltipInner(props: TooltipProps & {isExiting: boolean, tooltipRef: RefObject}) { let state = useContext(TooltipTriggerStateContext)!; + // Calculate the arrow size internally + // Referenced from: packages/@react-spectrum/tooltip/src/TooltipTrigger.tsx + let arrowRef = useRef(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; @@ -151,7 +164,7 @@ function TooltipInner(props: TooltipProps & {isExiting: boolean, tooltipRef: Ref data-placement={placement} data-entering={isEntering || undefined} data-exiting={props.isExiting || undefined}> - + {renderProps.children} diff --git a/packages/react-aria-components/stories/Popover.stories.tsx b/packages/react-aria-components/stories/Popover.stories.tsx index 343de0f34a7..327a334b358 100644 --- a/packages/react-aria-components/stories/Popover.stories.tsx +++ b/packages/react-aria-components/stories/Popover.stories.tsx @@ -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 { @@ -48,3 +48,286 @@ export const PopoverExample = () => ( ); + +export const PopoverArrowBoundaryOffsetExample = { + argTypes: { + topLeft: { + defaultValue: 25, + control: { + type: 'range', + min: -100, + max: 100 + } + }, + topRight: { + defaultValue: 25, + control: { + type: 'range', + min: -100, + max: 100 + } + }, + leftTop: { + defaultValue: 15, + control: { + type: 'range', + min: -100, + max: 100 + } + }, + leftBotton: { + defaultValue: 15, + control: { + type: 'range', + min: -100, + max: 100 + } + }, + rightTop: { + defaultValue: 15, + control: { + type: 'range', + min: -100, + max: 100 + } + }, + rightBottom: { + defaultValue: 15, + control: { + type: 'range', + min: -100, + max: 100 + } + }, + bottomLeft: { + defaultValue: 25, + control: { + type: 'range', + min: -100, + max: 100 + } + }, + bottomRight: { + defaultValue: 25, + control: { + type: 'range', + min: -100, + max: 100 + } + } + }, + render: ({topLeft, topRight, leftTop, leftBotton, rightTop, rightBottom, bottomLeft, bottomRight}: any) => { + return ( +
+
+
+ + + + + + + + + +
Top left
+
+
+
+
+
+ + + + + + + + + +
Top right
+
+
+
+
+
+
+
+ + + + + + + + + +
Left top
+
+
+
+
+
+ + + + + + + + + +
Left bottom
+
+
+
+
+
+
+
+ + + + + + + + + +
Right top
+
+
+
+
+
+ + + + + + + + + +
Right bottom
+
+
+
+
+
+
+
+ + + + + + + + + +
Bottom left
+
+
+
+
+
+ + + + + + + + + +
Bottom right
+
+
+
+
+
+
+ ); + } +}; diff --git a/packages/react-aria-components/stories/Tooltip.stories.tsx b/packages/react-aria-components/stories/Tooltip.stories.tsx index 0392c09dd5e..f0c5415e4a2 100644 --- a/packages/react-aria-components/stories/Tooltip.stories.tsx +++ b/packages/react-aria-components/stories/Tooltip.stories.tsx @@ -38,3 +38,270 @@ export const TooltipExample = () => (
); + +export const TooltipArrowBoundaryOffsetExample = { + argTypes: { + topLeft: { + defaultValue: 25, + control: { + type: 'range', + min: -100, + max: 100 + } + }, + topRight: { + defaultValue: 25, + control: { + type: 'range', + min: -100, + max: 100 + } + }, + leftTop: { + defaultValue: 15, + control: { + type: 'range', + min: -100, + max: 100 + } + }, + leftBotton: { + defaultValue: 15, + control: { + type: 'range', + min: -100, + max: 100 + } + }, + rightTop: { + defaultValue: 15, + control: { + type: 'range', + min: -100, + max: 100 + } + }, + rightBottom: { + defaultValue: 15, + control: { + type: 'range', + min: -100, + max: 100 + } + }, + bottomLeft: { + defaultValue: 25, + control: { + type: 'range', + min: -100, + max: 100 + } + }, + bottomRight: { + defaultValue: 25, + control: { + type: 'range', + min: -100, + max: 100 + } + } + }, + render: ({topLeft, topRight, leftTop, leftBotton, rightTop, rightBottom, bottomLeft, bottomRight}: any) => { + return ( +
+
+
+ + + + + + + + + Top left + + +
+
+ + + + + + + + + Top right + + +
+
+
+
+ + + + + + + + + Left top + + +
+
+ + + + + + + + + Left bottom + + +
+
+
+
+ + + + + + + + + Right top + + +
+
+ + + + + + + + + Right bottom + + +
+
+
+
+ + + + + + + + + Bottom left + + +
+
+ + + + + + + + + Bottom right + + +
+
+
+ ); + } +};