Skip to content

Commit

Permalink
Update calculatePosition to handle overlays that have margins on them (
Browse files Browse the repository at this point in the history
  • Loading branch information
LFDanLu committed Aug 16, 2024
1 parent ec75091 commit 3a48d44
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 6 deletions.
14 changes: 10 additions & 4 deletions packages/@react-aria/overlays/src/calculatePosition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
8 changes: 7 additions & 1 deletion packages/@react-spectrum/s2/src/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'};
Expand Down Expand Up @@ -154,19 +154,25 @@ function Tooltip(props: TooltipProps, ref: DOMRef<HTMLDivElement>) {
} = 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) => {
(domRef as MutableRefObject<HTMLDivElement>).current = el;
if (el) {
el.lang = locale;
el.dir = direction;
let spectrumBorderRadius = window.getComputedStyle(el).borderRadius;
if (spectrumBorderRadius !== '') {
setBorderRadius(parseInt(spectrumBorderRadius, 10));
}
}
}, [locale, direction, domRef]);

return (
<AriaTooltip
{...props}
arrowBoundaryOffset={borderRadius}
containerPadding={containerPadding}
crossOffset={crossOffset}
offset={offset}
Expand Down
50 changes: 49 additions & 1 deletion packages/@react-spectrum/s2/stories/Tooltip.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import Crop from '../s2wf-icons/S2_Icon_Crop_20_N.svg';
import LassoSelect from '../s2wf-icons/S2_Icon_LassoSelect_20_N.svg';
import type {Meta} from '@storybook/react';
import {style} from '../style/spectrum-theme' with {type: 'macro'};
import {userEvent, within} from '@storybook/testing-library';

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

export default meta;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) => (
<Provider colorScheme="dark" background="base" styles={style({padding: 48})}>
<Example {...args} />
</Provider>
);

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');
};

0 comments on commit 3a48d44

Please sign in to comment.