diff --git a/packages/@react-aria/overlays/src/calculatePosition.ts b/packages/@react-aria/overlays/src/calculatePosition.ts index d40aa79f41a..e2c4a361dff 100644 --- a/packages/@react-aria/overlays/src/calculatePosition.ts +++ b/packages/@react-aria/overlays/src/calculatePosition.ts @@ -417,15 +417,21 @@ export function calculatePositionInternal( // All values are transformed so that 0 is at the top/left of the overlay depending on the orientation // Prefer the arrow being in the center of the trigger/overlay anchor element - let preferredArrowPosition = childOffset[crossAxis] + .5 * childOffset[crossSize] - position[crossAxis]; + // childOffset[crossAxis] + .5 * childOffset[crossSize] = absolute position with respect to the trigger's coordinate system that would place the arrow in the center of the trigger + // position[crossAxis] - margins[AXIS[crossAxis]] = value use to transform the position to a value with respect to the overlay's coordinate system. A child element's (aka arrow) position absolute's "0" + // is positioned after the margin of its parent (aka overlay) so we need to subtract it to get the proper coordinate transform + let preferredArrowPosition = childOffset[crossAxis] + .5 * childOffset[crossSize] - position[crossAxis] - margins[AXIS[crossAxis]]; // Min/Max position limits for the arrow with respect to the overlay const arrowMinPosition = arrowSize / 2 + arrowBoundaryOffset; - const arrowMaxPosition = overlaySize[crossSize] - (arrowSize / 2) - arrowBoundaryOffset; + // overlaySize[crossSize] - margins = true size of the overlay + const overlayMargin = AXIS[crossAxis] === 'left' ? margins.left + margins.right : margins.top + margins.bottom; + const arrowMaxPosition = overlaySize[crossSize] - overlayMargin - (arrowSize / 2) - arrowBoundaryOffset; // Min/Max position limits for the arrow with respect to the trigger/overlay anchor element - const arrowOverlappingChildMinEdge = childOffset[crossAxis] - position[crossAxis] + (arrowSize / 2); - const arrowOverlappingChildMaxEdge = childOffset[crossAxis] + childOffset[crossSize] - position[crossAxis] - (arrowSize / 2); + // Same margin accomodation done here as well as for the preferredArrowPosition + const arrowOverlappingChildMinEdge = childOffset[crossAxis] + (arrowSize / 2) - (position[crossAxis] + margins[AXIS[crossAxis]]); + const arrowOverlappingChildMaxEdge = childOffset[crossAxis] + childOffset[crossSize] - (arrowSize / 2) - (position[crossAxis] + margins[AXIS[crossAxis]]); // Clamp the arrow positioning so that it always is within the bounds of the anchor and the overlay const arrowPositionOverlappingChild = clamp(preferredArrowPosition, arrowOverlappingChildMinEdge, arrowOverlappingChildMaxEdge); diff --git a/packages/@react-spectrum/s2/src/Tooltip.tsx b/packages/@react-spectrum/s2/src/Tooltip.tsx index e5bf123ef5b..de01359933c 100644 --- a/packages/@react-spectrum/s2/src/Tooltip.tsx +++ b/packages/@react-spectrum/s2/src/Tooltip.tsx @@ -22,7 +22,7 @@ import { import {centerPadding, colorScheme, UnsafeStyles} from './style-utils' with {type: 'macro'}; import {ColorScheme} from '@react-types/provider'; import {ColorSchemeContext} from './Provider'; -import {createContext, forwardRef, MutableRefObject, ReactNode, useCallback, useContext} from 'react'; +import {createContext, forwardRef, MutableRefObject, ReactNode, useCallback, useContext, useState} from 'react'; import {DOMRef} from '@react-types/shared'; import {keyframes} from '../style/style-macro' with {type: 'macro'}; import {style} from '../style/spectrum-theme' with {type: 'macro'}; @@ -154,6 +154,7 @@ function Tooltip(props: TooltipProps, ref: DOMRef) { } = useContext(InternalTooltipTriggerContext); let colorScheme = useContext(ColorSchemeContext); let {locale, direction} = useLocale(); + let [borderRadius, setBorderRadius] = useState(0); // TODO: should we pass through lang and dir props in RAC? let tooltipRef = useCallback((el: HTMLDivElement) => { @@ -161,12 +162,17 @@ function Tooltip(props: TooltipProps, ref: DOMRef) { if (el) { el.lang = locale; el.dir = direction; + let spectrumBorderRadius = window.getComputedStyle(el).borderRadius; + if (spectrumBorderRadius !== '') { + setBorderRadius(parseInt(spectrumBorderRadius, 10)); + } } }, [locale, direction, domRef]); return ( = { component: CombinedTooltip, @@ -25,7 +26,8 @@ const meta: Meta = { tags: ['autodocs'], argTypes: { onOpenChange: {table: {category: 'Events'}} - } + }, + decorators: [(Story) =>
] }; export default meta; @@ -78,6 +80,22 @@ export const Example = (args: any) => { ); }; +Example.play = async ({canvasElement}) => { + await userEvent.tab(); + let body = canvasElement.ownerDocument.body; + await within(body).findByRole('tooltip'); +}; + + +Example.story = { + argTypes: { + isOpen: { + control: 'select', + options: [true, false, undefined] + } + } +}; + export const LongLabel = (args: any) => { let { trigger, @@ -114,8 +132,38 @@ export const LongLabel = (args: any) => { ); }; +LongLabel.story = { + argTypes: { + isOpen: { + control: 'select', + options: [true, false, undefined] + } + } +}; + +LongLabel.play = async ({canvasElement}) => { + await userEvent.tab(); + let body = canvasElement.ownerDocument.body; + await within(body).findByRole('tooltip'); +}; + export const ColorScheme = (args: any) => ( ); + +ColorScheme.story = { + argTypes: { + isOpen: { + control: 'select', + options: [true, false, undefined] + } + } +}; + +ColorScheme.play = async ({canvasElement}) => { + await userEvent.tab(); + let body = canvasElement.ownerDocument.body; + await within(body).findByRole('tooltip'); +};