Skip to content

Commit 3a48d44

Browse files
authored
Update calculatePosition to handle overlays that have margins on them (#6831)
1 parent ec75091 commit 3a48d44

File tree

3 files changed

+66
-6
lines changed

3 files changed

+66
-6
lines changed

packages/@react-aria/overlays/src/calculatePosition.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -417,15 +417,21 @@ export function calculatePositionInternal(
417417

418418
// All values are transformed so that 0 is at the top/left of the overlay depending on the orientation
419419
// Prefer the arrow being in the center of the trigger/overlay anchor element
420-
let preferredArrowPosition = childOffset[crossAxis] + .5 * childOffset[crossSize] - position[crossAxis];
420+
// 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
421+
// 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"
422+
// is positioned after the margin of its parent (aka overlay) so we need to subtract it to get the proper coordinate transform
423+
let preferredArrowPosition = childOffset[crossAxis] + .5 * childOffset[crossSize] - position[crossAxis] - margins[AXIS[crossAxis]];
421424

422425
// Min/Max position limits for the arrow with respect to the overlay
423426
const arrowMinPosition = arrowSize / 2 + arrowBoundaryOffset;
424-
const arrowMaxPosition = overlaySize[crossSize] - (arrowSize / 2) - arrowBoundaryOffset;
427+
// overlaySize[crossSize] - margins = true size of the overlay
428+
const overlayMargin = AXIS[crossAxis] === 'left' ? margins.left + margins.right : margins.top + margins.bottom;
429+
const arrowMaxPosition = overlaySize[crossSize] - overlayMargin - (arrowSize / 2) - arrowBoundaryOffset;
425430

426431
// Min/Max position limits for the arrow with respect to the trigger/overlay anchor element
427-
const arrowOverlappingChildMinEdge = childOffset[crossAxis] - position[crossAxis] + (arrowSize / 2);
428-
const arrowOverlappingChildMaxEdge = childOffset[crossAxis] + childOffset[crossSize] - position[crossAxis] - (arrowSize / 2);
432+
// Same margin accomodation done here as well as for the preferredArrowPosition
433+
const arrowOverlappingChildMinEdge = childOffset[crossAxis] + (arrowSize / 2) - (position[crossAxis] + margins[AXIS[crossAxis]]);
434+
const arrowOverlappingChildMaxEdge = childOffset[crossAxis] + childOffset[crossSize] - (arrowSize / 2) - (position[crossAxis] + margins[AXIS[crossAxis]]);
429435

430436
// Clamp the arrow positioning so that it always is within the bounds of the anchor and the overlay
431437
const arrowPositionOverlappingChild = clamp(preferredArrowPosition, arrowOverlappingChildMinEdge, arrowOverlappingChildMaxEdge);

packages/@react-spectrum/s2/src/Tooltip.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
import {centerPadding, colorScheme, UnsafeStyles} from './style-utils' with {type: 'macro'};
2323
import {ColorScheme} from '@react-types/provider';
2424
import {ColorSchemeContext} from './Provider';
25-
import {createContext, forwardRef, MutableRefObject, ReactNode, useCallback, useContext} from 'react';
25+
import {createContext, forwardRef, MutableRefObject, ReactNode, useCallback, useContext, useState} from 'react';
2626
import {DOMRef} from '@react-types/shared';
2727
import {keyframes} from '../style/style-macro' with {type: 'macro'};
2828
import {style} from '../style/spectrum-theme' with {type: 'macro'};
@@ -154,19 +154,25 @@ function Tooltip(props: TooltipProps, ref: DOMRef<HTMLDivElement>) {
154154
} = useContext(InternalTooltipTriggerContext);
155155
let colorScheme = useContext(ColorSchemeContext);
156156
let {locale, direction} = useLocale();
157+
let [borderRadius, setBorderRadius] = useState(0);
157158

158159
// TODO: should we pass through lang and dir props in RAC?
159160
let tooltipRef = useCallback((el: HTMLDivElement) => {
160161
(domRef as MutableRefObject<HTMLDivElement>).current = el;
161162
if (el) {
162163
el.lang = locale;
163164
el.dir = direction;
165+
let spectrumBorderRadius = window.getComputedStyle(el).borderRadius;
166+
if (spectrumBorderRadius !== '') {
167+
setBorderRadius(parseInt(spectrumBorderRadius, 10));
168+
}
164169
}
165170
}, [locale, direction, domRef]);
166171

167172
return (
168173
<AriaTooltip
169174
{...props}
175+
arrowBoundaryOffset={borderRadius}
170176
containerPadding={containerPadding}
171177
crossOffset={crossOffset}
172178
offset={offset}

packages/@react-spectrum/s2/stories/Tooltip.stories.tsx

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import Crop from '../s2wf-icons/S2_Icon_Crop_20_N.svg';
1616
import LassoSelect from '../s2wf-icons/S2_Icon_LassoSelect_20_N.svg';
1717
import type {Meta} from '@storybook/react';
1818
import {style} from '../style/spectrum-theme' with {type: 'macro'};
19+
import {userEvent, within} from '@storybook/testing-library';
1920

2021
const meta: Meta<typeof CombinedTooltip> = {
2122
component: CombinedTooltip,
@@ -25,7 +26,8 @@ const meta: Meta<typeof CombinedTooltip> = {
2526
tags: ['autodocs'],
2627
argTypes: {
2728
onOpenChange: {table: {category: 'Events'}}
28-
}
29+
},
30+
decorators: [(Story) => <div style={{height: '100px', width: '200px', display: 'flex', alignItems: 'end', justifyContent: 'center', paddingBottom: 10}}><Story /></div>]
2931
};
3032

3133
export default meta;
@@ -78,6 +80,22 @@ export const Example = (args: any) => {
7880
);
7981
};
8082

83+
Example.play = async ({canvasElement}) => {
84+
await userEvent.tab();
85+
let body = canvasElement.ownerDocument.body;
86+
await within(body).findByRole('tooltip');
87+
};
88+
89+
90+
Example.story = {
91+
argTypes: {
92+
isOpen: {
93+
control: 'select',
94+
options: [true, false, undefined]
95+
}
96+
}
97+
};
98+
8199
export const LongLabel = (args: any) => {
82100
let {
83101
trigger,
@@ -114,8 +132,38 @@ export const LongLabel = (args: any) => {
114132
);
115133
};
116134

135+
LongLabel.story = {
136+
argTypes: {
137+
isOpen: {
138+
control: 'select',
139+
options: [true, false, undefined]
140+
}
141+
}
142+
};
143+
144+
LongLabel.play = async ({canvasElement}) => {
145+
await userEvent.tab();
146+
let body = canvasElement.ownerDocument.body;
147+
await within(body).findByRole('tooltip');
148+
};
149+
117150
export const ColorScheme = (args: any) => (
118151
<Provider colorScheme="dark" background="base" styles={style({padding: 48})}>
119152
<Example {...args} />
120153
</Provider>
121154
);
155+
156+
ColorScheme.story = {
157+
argTypes: {
158+
isOpen: {
159+
control: 'select',
160+
options: [true, false, undefined]
161+
}
162+
}
163+
};
164+
165+
ColorScheme.play = async ({canvasElement}) => {
166+
await userEvent.tab();
167+
let body = canvasElement.ownerDocument.body;
168+
await within(body).findByRole('tooltip');
169+
};

0 commit comments

Comments
 (0)