Skip to content

Commit caaba79

Browse files
authored
feat: left/right tooltips
Create `left-center` + `right-center` aligned `ToolTips` + `Popovers`
1 parent f0caa61 commit caaba79

28 files changed

Lines changed: 1104 additions & 659 deletions

packages/gamut/src/Coachmark/index.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@ import { useRef } from 'react';
22
import * as React from 'react';
33

44
import { DelayedRenderWrapper } from '../DelayedRenderWrapper';
5-
import { Popover, PopoverBaseProps, PopoverProps } from '../Popover';
5+
import {
6+
Popover,
7+
PopoverFocusProps,
8+
PopoverProps,
9+
PopoverYPositionType,
10+
} from '../Popover';
611

7-
export type CoachmarkProps = PopoverBaseProps & {
12+
export type CoachmarkProps = PopoverFocusProps & {
813
/**
914
* Applied to the element to which the coachmark points.
1015
*/
@@ -29,7 +34,9 @@ export type CoachmarkProps = PopoverBaseProps & {
2934
/**
3035
* Props to be passed into the popover component.
3136
*/
32-
popoverProps?: Partial<PopoverProps>;
37+
popoverProps?: Partial<
38+
Omit<PopoverProps, 'beak' | 'position'> & PopoverYPositionType
39+
>;
3340
};
3441

3542
export const Coachmark: React.FC<CoachmarkProps> = ({

packages/gamut/src/Popover/Popover.tsx

100644100755
Lines changed: 55 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { useCallback, useEffect, useState } from 'react';
2-
import * as React from 'react';
32
import { useWindowScroll, useWindowSize } from 'react-use';
43

54
import { FocusTrap } from '../FocusTrap';
@@ -11,45 +10,20 @@ import {
1110
PopoverPortal,
1211
RaisedDiv,
1312
} from './elements';
13+
import { getBeakVariant } from './styles/beak';
1414
import { PopoverProps } from './types';
15-
16-
const findScrollingParent = ({
17-
parentElement,
18-
}: HTMLElement): HTMLElement | null => {
19-
if (parentElement) {
20-
const { overflow, overflowY, overflowX } = getComputedStyle(parentElement);
21-
if (
22-
[overflow, overflowY, overflowX].some((val) =>
23-
['scroll', 'auto'].includes(val)
24-
)
25-
) {
26-
return parentElement;
27-
}
28-
return findScrollingParent(parentElement); // parent of this parent is used via prop destructure
29-
}
30-
return null;
31-
};
32-
33-
const findResizingParent = ({
34-
parentElement,
35-
}: HTMLElement): HTMLElement | null => {
36-
if (parentElement) {
37-
const { overflow, overflowY, overflowX } = getComputedStyle(parentElement);
38-
if ([overflow, overflowY, overflowX].some((val) => val === 'clip')) {
39-
return parentElement;
40-
}
41-
return findResizingParent(parentElement); // parent of this parent is used via prop destructure
42-
}
43-
return null;
44-
};
15+
import {
16+
findResizingParent,
17+
findScrollingParent,
18+
getDefaultOffset,
19+
} from './utils';
4520

4621
export const Popover: React.FC<PopoverProps> = ({
4722
animation,
4823
align = 'left',
4924
beak,
5025
children,
5126
className,
52-
horizontalOffset = 0,
5327
isOpen,
5428
onRequestClose,
5529
outline = false,
@@ -60,30 +34,72 @@ export const Popover: React.FC<PopoverProps> = ({
6034
role,
6135
variant,
6236
targetRef,
63-
verticalOffset = variant === 'secondary' ? 15 : 20,
37+
horizontalOffset = getDefaultOffset({
38+
axis: 'horizontal',
39+
position,
40+
variant,
41+
}),
42+
verticalOffset = getDefaultOffset({ axis: 'vertical', position, variant }),
43+
6444
widthRestricted,
6545
}) => {
46+
const [popoverHeight, setPopoverHeight] = useState<number>(0);
47+
const [popoverWidth, setPopoverWidth] = useState<number>(0);
6648
const [targetRect, setTargetRect] = useState<DOMRect>();
6749
const [isInViewport, setIsInViewport] = useState(true);
6850
const { width, height } = useWindowSize();
6951
const { x, y } = useWindowScroll();
7052

53+
const getRaisedDivDimsRef = (popover: HTMLDivElement) => {
54+
if (popover && popoverHeight === 0 && popoverWidth === 0) {
55+
const { height, width } = popover.getBoundingClientRect();
56+
setPopoverHeight(height);
57+
setPopoverWidth(width);
58+
}
59+
};
60+
7161
const getPopoverPosition = useCallback(() => {
7262
if (!targetRect) return {};
7363

64+
const isLRCentered = position === 'center';
65+
7466
const positions = {
7567
above: Math.round(targetRect.top - verticalOffset),
7668
below: Math.round(targetRect.top + targetRect.height + verticalOffset),
69+
center: Math.round(
70+
targetRect.top +
71+
targetRect.height / 2 -
72+
popoverHeight / 2 +
73+
verticalOffset
74+
),
7775
};
7876
const alignments = {
79-
right: Math.round(window.scrollX + targetRect.right + horizontalOffset),
80-
left: Math.round(window.scrollX + targetRect.left - horizontalOffset),
77+
right: isLRCentered
78+
? Math.round(targetRect.right + popoverWidth + horizontalOffset)
79+
: Math.round(window.scrollX + targetRect.right + horizontalOffset),
80+
left: isLRCentered
81+
? Math.round(targetRect.left - popoverWidth - horizontalOffset)
82+
: Math.round(window.scrollX + targetRect.left - horizontalOffset),
83+
center: Math.round(
84+
targetRect.left +
85+
targetRect.width / 2 -
86+
popoverWidth / 2 +
87+
horizontalOffset
88+
),
8189
};
8290
return {
8391
top: positions[position],
8492
left: alignments[align],
8593
};
86-
}, [targetRect, verticalOffset, horizontalOffset, align, position]);
94+
}, [
95+
align,
96+
horizontalOffset,
97+
popoverHeight,
98+
popoverWidth,
99+
position,
100+
targetRect,
101+
verticalOffset,
102+
]);
87103

88104
useEffect(() => {
89105
setTargetRect(targetRef?.current?.getBoundingClientRect());
@@ -149,12 +165,12 @@ export const Popover: React.FC<PopoverProps> = ({
149165
},
150166
[onRequestClose, targetRef]
151167
);
152-
153168
if ((!isOpen || !targetRef) && !animation) return null;
154169
const alignment =
155170
(variant === 'primary' || beak) && beak !== 'center'
156171
? 'aligned'
157172
: 'centered';
173+
158174
const contents = (
159175
<PopoverContainer
160176
align={align}
@@ -169,15 +185,14 @@ export const Popover: React.FC<PopoverProps> = ({
169185
<RaisedDiv
170186
alignment={alignment}
171187
outline={outline ? 'outline' : 'boxShadow'}
188+
ref={getRaisedDivDimsRef}
172189
variant={variant}
173190
widthRestricted={widthRestricted}
174191
>
175192
{beak && (
176193
<BeakBox variant={position}>
177194
<Beak
178-
beak={`${position}-${beak}${
179-
variant === 'secondary' ? '-sml' : ''
180-
}`}
195+
beak={getBeakVariant({ align, position, beak, variant })}
181196
data-testid="popover-beak"
182197
hasBorder={outline || variant === 'secondary'}
183198
size={variant === 'secondary' ? 'sml' : 'lrg'}

packages/gamut/src/Popover/elements.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,26 @@ import { timingValues, variant } from '@codecademy/gamut-styles';
22
import { StyleProps } from '@codecademy/variance';
33
import styled from '@emotion/styled';
44
import { AnimatePresence, motion } from 'framer-motion';
5-
import * as React from 'react';
65

76
import { BodyPortal } from '../BodyPortal';
87
import { Box, FlexBox } from '../Box';
9-
import { popoverToolTipBodyAlignments } from '../Tip/shared/styles';
8+
import { popoverToolTipBodyAlignments } from '../Tip/shared/styles/styles';
109
import { WithChildrenProp } from '../utils';
10+
import {
11+
popoverStates,
12+
raisedDivVariants,
13+
transformValues,
14+
} from './styles/base';
15+
import { patternContainerBaseStyles } from './styles/pattern';
1116
import {
1217
beakBorderStates,
1318
beakBoxVariants,
1419
beakSize,
1520
beakVariants,
1621
outlineVariants,
17-
patternContainerBaseStyles,
1822
patternVariantStyles,
19-
popoverStates,
20-
raisedDivVariants,
21-
transformValues,
2223
widthStates,
23-
} from './styles';
24+
} from './styles/variants';
2425
import { PopoverProps } from './types';
2526

2627
export type PopoverVariants = StyleProps<typeof raisedDivVariants> & {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { states, variant } from '@codecademy/gamut-styles';
2+
3+
import { toolTipBodyCss } from '../../Tip/shared/styles/styles';
4+
5+
export const borderStyles = { border: 1 } as const;
6+
export const popoverPrimaryBgColor = `background`;
7+
8+
/**
9+
* For the Popover + Tooltip style files:
10+
*
11+
* 'above' + 'below' map to position, 'top' + 'bottom' map to beak alignment
12+
* variants for both follow this formula: `position`-`beakPosition`
13+
* Popovers additionally will have `-sml` added to the end of this string if they are the `secondary` variant
14+
*
15+
*/
16+
17+
export const transformValues = {
18+
right: 'translateX(-100%)',
19+
left: 'translateX(0%)',
20+
above: 'translateY(-100%)',
21+
below: 'translateY(0%)',
22+
center: '',
23+
};
24+
25+
export const popoverStates = states({
26+
widthRestricted: {
27+
minWidth: '4rem',
28+
maxWidth: '16rem',
29+
},
30+
});
31+
32+
export const raisedDivVariants = variant({
33+
base: {
34+
zIndex: 1,
35+
},
36+
defaultVariant: 'primary',
37+
variants: {
38+
primary: {
39+
bg: popoverPrimaryBgColor,
40+
borderRadius: 'sm',
41+
},
42+
secondary: { ...toolTipBodyCss },
43+
},
44+
});

0 commit comments

Comments
 (0)