diff --git a/packages/gamut/src/Coachmark/index.tsx b/packages/gamut/src/Coachmark/index.tsx index 756471f3c64..a4316f6fde3 100644 --- a/packages/gamut/src/Coachmark/index.tsx +++ b/packages/gamut/src/Coachmark/index.tsx @@ -2,9 +2,14 @@ import { useRef } from 'react'; import * as React from 'react'; import { DelayedRenderWrapper } from '../DelayedRenderWrapper'; -import { Popover, PopoverBaseProps, PopoverProps } from '../Popover'; +import { + Popover, + PopoverFocusProps, + PopoverProps, + PopoverYPositionType, +} from '../Popover'; -export type CoachmarkProps = PopoverBaseProps & { +export type CoachmarkProps = PopoverFocusProps & { /** * Applied to the element to which the coachmark points. */ @@ -29,7 +34,9 @@ export type CoachmarkProps = PopoverBaseProps & { /** * Props to be passed into the popover component. */ - popoverProps?: Partial; + popoverProps?: Partial< + Omit & PopoverYPositionType + >; }; export const Coachmark: React.FC = ({ diff --git a/packages/gamut/src/Popover/Popover.tsx b/packages/gamut/src/Popover/Popover.tsx old mode 100644 new mode 100755 index 16c7735ee34..45716f09120 --- a/packages/gamut/src/Popover/Popover.tsx +++ b/packages/gamut/src/Popover/Popover.tsx @@ -1,5 +1,4 @@ import { useCallback, useEffect, useState } from 'react'; -import * as React from 'react'; import { useWindowScroll, useWindowSize } from 'react-use'; import { FocusTrap } from '../FocusTrap'; @@ -11,37 +10,13 @@ import { PopoverPortal, RaisedDiv, } from './elements'; +import { getBeakVariant } from './styles/beak'; import { PopoverProps } from './types'; - -const findScrollingParent = ({ - parentElement, -}: HTMLElement): HTMLElement | null => { - if (parentElement) { - const { overflow, overflowY, overflowX } = getComputedStyle(parentElement); - if ( - [overflow, overflowY, overflowX].some((val) => - ['scroll', 'auto'].includes(val) - ) - ) { - return parentElement; - } - return findScrollingParent(parentElement); // parent of this parent is used via prop destructure - } - return null; -}; - -const findResizingParent = ({ - parentElement, -}: HTMLElement): HTMLElement | null => { - if (parentElement) { - const { overflow, overflowY, overflowX } = getComputedStyle(parentElement); - if ([overflow, overflowY, overflowX].some((val) => val === 'clip')) { - return parentElement; - } - return findResizingParent(parentElement); // parent of this parent is used via prop destructure - } - return null; -}; +import { + findResizingParent, + findScrollingParent, + getDefaultOffset, +} from './utils'; export const Popover: React.FC = ({ animation, @@ -49,7 +24,6 @@ export const Popover: React.FC = ({ beak, children, className, - horizontalOffset = 0, isOpen, onRequestClose, outline = false, @@ -60,30 +34,72 @@ export const Popover: React.FC = ({ role, variant, targetRef, - verticalOffset = variant === 'secondary' ? 15 : 20, + horizontalOffset = getDefaultOffset({ + axis: 'horizontal', + position, + variant, + }), + verticalOffset = getDefaultOffset({ axis: 'vertical', position, variant }), + widthRestricted, }) => { + const [popoverHeight, setPopoverHeight] = useState(0); + const [popoverWidth, setPopoverWidth] = useState(0); const [targetRect, setTargetRect] = useState(); const [isInViewport, setIsInViewport] = useState(true); const { width, height } = useWindowSize(); const { x, y } = useWindowScroll(); + const getRaisedDivDimsRef = (popover: HTMLDivElement) => { + if (popover && popoverHeight === 0 && popoverWidth === 0) { + const { height, width } = popover.getBoundingClientRect(); + setPopoverHeight(height); + setPopoverWidth(width); + } + }; + const getPopoverPosition = useCallback(() => { if (!targetRect) return {}; + const isLRCentered = position === 'center'; + const positions = { above: Math.round(targetRect.top - verticalOffset), below: Math.round(targetRect.top + targetRect.height + verticalOffset), + center: Math.round( + targetRect.top + + targetRect.height / 2 - + popoverHeight / 2 + + verticalOffset + ), }; const alignments = { - right: Math.round(window.scrollX + targetRect.right + horizontalOffset), - left: Math.round(window.scrollX + targetRect.left - horizontalOffset), + right: isLRCentered + ? Math.round(targetRect.right + popoverWidth + horizontalOffset) + : Math.round(window.scrollX + targetRect.right + horizontalOffset), + left: isLRCentered + ? Math.round(targetRect.left - popoverWidth - horizontalOffset) + : Math.round(window.scrollX + targetRect.left - horizontalOffset), + center: Math.round( + targetRect.left + + targetRect.width / 2 - + popoverWidth / 2 + + horizontalOffset + ), }; return { top: positions[position], left: alignments[align], }; - }, [targetRect, verticalOffset, horizontalOffset, align, position]); + }, [ + align, + horizontalOffset, + popoverHeight, + popoverWidth, + position, + targetRect, + verticalOffset, + ]); useEffect(() => { setTargetRect(targetRef?.current?.getBoundingClientRect()); @@ -149,12 +165,12 @@ export const Popover: React.FC = ({ }, [onRequestClose, targetRef] ); - if ((!isOpen || !targetRef) && !animation) return null; const alignment = (variant === 'primary' || beak) && beak !== 'center' ? 'aligned' : 'centered'; + const contents = ( = ({ {beak && ( & { diff --git a/packages/gamut/src/Popover/styles/base.ts b/packages/gamut/src/Popover/styles/base.ts new file mode 100644 index 00000000000..97cc96bd106 --- /dev/null +++ b/packages/gamut/src/Popover/styles/base.ts @@ -0,0 +1,44 @@ +import { states, variant } from '@codecademy/gamut-styles'; + +import { toolTipBodyCss } from '../../Tip/shared/styles/styles'; + +export const borderStyles = { border: 1 } as const; +export const popoverPrimaryBgColor = `background`; + +/** + * For the Popover + Tooltip style files: + * + * 'above' + 'below' map to position, 'top' + 'bottom' map to beak alignment + * variants for both follow this formula: `position`-`beakPosition` + * Popovers additionally will have `-sml` added to the end of this string if they are the `secondary` variant + * + */ + +export const transformValues = { + right: 'translateX(-100%)', + left: 'translateX(0%)', + above: 'translateY(-100%)', + below: 'translateY(0%)', + center: '', +}; + +export const popoverStates = states({ + widthRestricted: { + minWidth: '4rem', + maxWidth: '16rem', + }, +}); + +export const raisedDivVariants = variant({ + base: { + zIndex: 1, + }, + defaultVariant: 'primary', + variants: { + primary: { + bg: popoverPrimaryBgColor, + borderRadius: 'sm', + }, + secondary: { ...toolTipBodyCss }, + }, +}); diff --git a/packages/gamut/src/Popover/styles/beak.ts b/packages/gamut/src/Popover/styles/beak.ts new file mode 100644 index 00000000000..78597b22fbb --- /dev/null +++ b/packages/gamut/src/Popover/styles/beak.ts @@ -0,0 +1,214 @@ +import { + beakBottomStyles, + beakLeftCenterStyles, + beakRightCenterStyles, + beakStylesBase, + beakTopStyles, + getBeakBgAndRotation, + tooltipBgColor, +} from '../../Tip/shared/styles/styles'; +import { PopoverProps } from '../types'; +import { popoverPrimaryBgColor } from './base'; + +const positionAbove = { + top: 'calc(100% - 10px)', + ...beakStylesBase, + ...getBeakBgAndRotation({ alignment: 'above', color: popoverPrimaryBgColor }), +} as const; + +const positionBelow = { + top: '-10px', + ...beakStylesBase, + ...getBeakBgAndRotation({ alignment: 'below', color: popoverPrimaryBgColor }), +} as const; + +const beakRight = { + right: '25px', +}; + +const beakLeft = { + left: '25px', +}; + +const beakXCenter = { + left: 'calc(50% - 10px)', +}; + +const beakYCenter = { + top: 'calc(50% - 10px)', +}; + +const positionAboveSml = { + top: 'calc(100% - 8px)', + ...beakTopStyles, +} as const; + +const positionBelowSml = { + top: '-8px', + ...beakBottomStyles, +} as const; + +const beakRightSml = { + right: '1.5rem', + bg: tooltipBgColor, +}; + +const beakLeftSml = { + left: '1.5rem', + bg: tooltipBgColor, +}; + +const beakXCenterSml = { + left: 'calc(50% - 8px)', +}; + +const beakYCenterSml = { + top: 'calc(50% - 8px)', +}; + +const beakRightCenterStylesSml = { + ...beakRightCenterStyles, + left: -8, +}; +export const beakRightCenterStylesLrg = { + ...beakStylesBase, + ...getBeakBgAndRotation({ alignment: 'right', color: popoverPrimaryBgColor }), + left: -10, +}; + +const beakLeftCenterStylesSml = { + ...beakLeftCenterStyles, + right: -8, +}; +const beakLeftCenterStylesLrg = { + ...beakStylesBase, + ...getBeakBgAndRotation({ alignment: 'left', color: popoverPrimaryBgColor }), + right: -10, +}; + +export const beakBoxX = { + alignItems: 'flex-end', + height: '15px', + width: '100%', + justifyContent: 'center', + left: 0, +}; + +export const beakBoxY = { + height: '100%', + width: '15px', +}; + +export const beakVariantsArray = [ + 'below-left', + 'below-right', + 'below-center', + 'above-left', + 'above-right', + 'above-center', + 'below-left-sml', + 'below-right-sml', + 'below-center-sml', + 'above-left-sml', + 'above-right-sml', + 'above-center-sml', + 'center-right', + 'center-left', + 'center-left-sml', + 'center-right-sml', +]; + +export const getBeakVariant = ({ + align, + position, + beak, + variant, +}: Pick) => { + const beakAlignment = position === 'center' ? align : beak; + return `${position}-${beakAlignment}${variant === 'secondary' ? '-sml' : ''}`; +}; + +export const createBeakVariantFromAlignment = (alignment: string) => { + let styleObject = {}; + const isSml = alignment.includes('sml'); + const isAbove = alignment.includes('above'); + const isBelow = alignment.includes('below'); + const isRight = alignment.includes('right'); + const isXCentered = alignment.includes('-center'); + const isYCentered = alignment.startsWith('center-'); + + if (isSml) { + if (isYCentered) { + // center-x-sml + styleObject = { ...beakYCenterSml }; + if (isRight) { + // center-right-sml + styleObject = { + ...styleObject, + ...beakRightCenterStylesSml, + }; + } else { + // center-left-sml + styleObject = { + ...styleObject, + ...beakLeftCenterStylesSml, + }; + } + } else { + if (isAbove) { + // above-x-sml + styleObject = { ...positionAboveSml }; + } else { + // below-x-sml + styleObject = { ...positionBelowSml }; + } + if (isRight) { + // above-right-sml + below-right-sml + styleObject = { ...styleObject, ...beakRightSml }; + } else if (isXCentered) { + // above-center-sml + below-center-sml + styleObject = { ...styleObject, ...beakXCenterSml }; + if (isAbove) { + // above-center-sml + styleObject = { ...styleObject, ...beakTopStyles }; + } else if (isBelow) { + // below-center-sml + styleObject = { ...styleObject, ...beakBottomStyles }; + } + } else { + // above-left-sml + below-left-sml + styleObject = { ...styleObject, ...beakLeftSml }; + } + } + } else if (isYCentered) { + // center-x + styleObject = { ...beakYCenter }; + if (isRight) { + // center-right + styleObject = { ...styleObject, ...beakRightCenterStylesLrg }; + } else { + // center-left + styleObject = { ...styleObject, ...beakLeftCenterStylesLrg }; + } + } else { + if (isAbove) { + // above-x + styleObject = { ...positionAbove }; + } else { + // below-x + styleObject = { ...positionBelow }; + } + if (isRight) { + // above-right + below-right + styleObject = { ...styleObject, ...beakRight }; + } else if (isXCentered) { + // above-center + below-center + styleObject = { ...styleObject, ...beakXCenter }; + } else { + // above-left + below-left + styleObject = { ...styleObject, ...beakLeft }; + } + } + + return { ...styleObject }; +}; diff --git a/packages/gamut/src/Popover/styles/pattern.ts b/packages/gamut/src/Popover/styles/pattern.ts new file mode 100644 index 00000000000..8d05d2dfb33 --- /dev/null +++ b/packages/gamut/src/Popover/styles/pattern.ts @@ -0,0 +1,47 @@ +export const patternContainerBaseStyles = { + bg: 'transparent', + borderRadius: 'sm', + overflow: 'hidden', + position: 'absolute', + width: '100%', +} as const; + +export const patternVariantArray = [ + 'above-left', + 'above-right', + 'below-left', + 'below-right', +]; + +export const patternAbove = { + top: '-8px', +}; + +export const patternBelow = { + top: '8px', +}; + +export const patternRight = { + left: '-8px', +}; + +export const patternLeft = { + left: '8px', +}; + +export const createPatternVariantFromAlignment = (alignment: string) => { + let styleObject = {}; + + if (alignment.includes('above')) { + styleObject = { ...patternAbove }; + } else { + styleObject = { ...patternBelow }; + } + if (alignment.includes('right')) { + styleObject = { ...patternRight, ...styleObject }; + } else { + styleObject = { ...patternLeft, ...styleObject }; + } + + return { ...styleObject }; +}; diff --git a/packages/gamut/src/Popover/styles.tsx b/packages/gamut/src/Popover/styles/variants.ts similarity index 52% rename from packages/gamut/src/Popover/styles.tsx rename to packages/gamut/src/Popover/styles/variants.ts index 2788c921591..1ebf78d59e7 100644 --- a/packages/gamut/src/Popover/styles.tsx +++ b/packages/gamut/src/Popover/styles/variants.ts @@ -1,34 +1,19 @@ import { states, variant } from '@codecademy/gamut-styles'; -import { tooltipArrowHeight, toolTipBodyCss } from '../Tip/shared/styles'; -import { createVariantsFromAlignments } from '../Tip/shared/utils'; +import { createVariantsFromAlignments } from '../../Tip/shared/styles/createVariantsUtils'; +import { tooltipArrowHeight } from '../../Tip/shared/styles/styles'; +import { borderStyles } from './base'; import { + beakBoxX, + beakBoxY, + beakVariantsArray, createBeakVariantFromAlignment, +} from './beak'; +import { createPatternVariantFromAlignment, -} from './utils'; - -export const popoverStates = states({ - widthRestricted: { - minWidth: '4rem', - maxWidth: '16rem', - }, -}); - -export const raisedDivVariants = variant({ - base: { - zIndex: 1, - }, - defaultVariant: 'primary', - variants: { - primary: { - bg: 'background', - borderRadius: 'sm', - }, - secondary: { ...toolTipBodyCss }, - }, -}); + patternVariantArray, +} from './pattern'; -const borderStyles = { border: 1 } as const; export const outlineVariants = variant({ defaultVariant: 'boxShadow', prop: 'outline', @@ -54,39 +39,6 @@ export const beakBorderStates = states({ hasBorder: borderStyles, }); -export const beakBoxVariants = variant({ - base: { - alignItems: 'flex-end', - height: '15px', - justifyContent: 'center', - left: 0, - position: 'absolute', - width: '100%', - }, - variants: { - above: { - bottom: -15, - }, - below: { - top: -15, - }, - }, -}); -const beakVariantsArray = [ - 'below-left', - 'below-right', - 'below-center', - 'above-left', - 'above-right', - 'above-center', - 'below-left-sml', - 'below-right-sml', - 'below-center-sml', - 'above-left-sml', - 'above-right-sml', - 'above-center-sml', -]; - const beakVariantStyles = createVariantsFromAlignments( beakVariantsArray, createBeakVariantFromAlignment @@ -94,7 +46,6 @@ const beakVariantStyles = createVariantsFromAlignments( export const beakVariants = variant({ base: { - transform: 'rotate(45deg)', background: 'transparent', zIndex: 1, position: 'fixed', @@ -119,29 +70,26 @@ export const beakSize = variant({ }, }); -export const patternContainerBaseStyles = { - bg: 'transparent', - borderRadius: 'sm', - overflow: 'hidden', - position: 'absolute', - width: '100%', -} as const; - -const patternVariantArray = [ - 'above-left', - 'above-right', - 'below-left', - 'below-right', -]; - export const patternVariantStyles = createVariantsFromAlignments( patternVariantArray, createPatternVariantFromAlignment ); -export const transformValues = { - right: 'translateX(-100%)', - left: 'translateX(0%)', - above: 'translateY(-100%)', - below: 'translateY(0%)', -}; +export const beakBoxVariants = variant({ + base: { + position: 'absolute', + }, + variants: { + above: { + bottom: -15, + ...beakBoxX, + }, + below: { + top: -15, + ...beakBoxX, + }, + center: { + ...beakBoxY, + }, + }, +}); diff --git a/packages/gamut/src/Popover/types.tsx b/packages/gamut/src/Popover/types.tsx old mode 100644 new mode 100755 index 7f8d43a3403..b85d4d4595c --- a/packages/gamut/src/Popover/types.tsx +++ b/packages/gamut/src/Popover/types.tsx @@ -27,10 +27,44 @@ export type SkippedFocusTrapPopoverProps = { skipFocusTrap: true; }; -export type PopoverBaseProps = +export type PopoverFocusProps = | FocusTrapPopoverProps | SkippedFocusTrapPopoverProps; +export type PopoverYPositionType = { + /** + * Which horizontal edge of the source component to align against. Center aligns it centered to the component. + */ + position?: 'above' | 'below'; + /** + * Which side to position the beak. If not provided, beak will not be rendered. Position `center` Popovers can only be used with `center` beaks. + */ + beak?: 'left' | 'right' | 'center'; + /** + * Pattern component to use as a background. + */ + pattern?: React.ComponentType; +}; + +export type PopoverXPositionType = { + /** + * Which horizontal edge of the source component to align against. Center aligns it centered to the component. + */ + position: 'center'; + /** + * Which side to position the beak. If not provided, beak will not be rendered.Position `center` Popovers can only be used with `center` beaks. + */ + beak?: 'center'; + /** + * Pattern component to use as a background. + */ + pattern?: never; +}; + +type PopoverPositionType = PopoverYPositionType | PopoverXPositionType; + +export type PopoverBaseProps = PopoverPositionType & PopoverFocusProps; + export type PopoverProps = PopoverBaseProps & PopoverVariants & Pick, 'role'> & { @@ -43,7 +77,7 @@ export type PopoverProps = PopoverBaseProps & /** * Which vertical edge of the source component to align against. */ - align?: 'left' | 'right'; + align?: 'left' | 'right' | 'center'; /** * Number of pixels to offset the popover vertically from the source component. */ @@ -56,22 +90,10 @@ export type PopoverProps = PopoverBaseProps & * Whether to add outline style (i.e. used for dropdowns and coachmarks). */ outline?: boolean; - /** - * Which horizontal edge of the source componet to align against. - */ - position?: 'above' | 'below'; - /** - * Which side to position the beak. If not provided, beak will not be rendered. - */ - beak?: 'left' | 'right' | 'center'; /** * Whether the popover is rendered. */ isOpen: boolean; - /** - * Pattern component to use as a background. - */ - pattern?: React.ComponentType; /** * The target element around which the popover will be positioned. diff --git a/packages/gamut/src/Popover/utils.ts b/packages/gamut/src/Popover/utils.ts new file mode 100644 index 00000000000..3e11065d8ef --- /dev/null +++ b/packages/gamut/src/Popover/utils.ts @@ -0,0 +1,52 @@ +import { PopoverProps } from './types'; + +export const findScrollingParent = ({ + parentElement, +}: HTMLElement): HTMLElement | null => { + if (parentElement) { + const { overflow, overflowY, overflowX } = getComputedStyle(parentElement); + if ( + [overflow, overflowY, overflowX].some((val) => + ['scroll', 'auto'].includes(val) + ) + ) { + return parentElement; + } + return findScrollingParent(parentElement); // parent of this parent is used via prop destructure + } + return null; +}; + +export const findResizingParent = ({ + parentElement, +}: HTMLElement): HTMLElement | null => { + if (parentElement) { + const { overflow, overflowY, overflowX } = getComputedStyle(parentElement); + if ([overflow, overflowY, overflowX].some((val) => val === 'clip')) { + return parentElement; + } + return findResizingParent(parentElement); // parent of this parent is used via prop destructure + } + return null; +}; + +const offsets = { + primary: 20, + secondary: 15, +}; + +export const getDefaultOffset = ({ + axis, + position, + variant = 'primary', +}: Pick & { + axis: 'horizontal' | 'vertical'; +}) => { + let newPosition = 0; + if (position === 'center' && axis === 'horizontal' && variant) { + newPosition = offsets[variant]; + } else if (position !== 'center' && axis === 'vertical' && variant) { + newPosition = offsets[variant]; + } + return newPosition; +}; diff --git a/packages/gamut/src/Popover/utils.tsx b/packages/gamut/src/Popover/utils.tsx deleted file mode 100644 index f07b3274f3a..00000000000 --- a/packages/gamut/src/Popover/utils.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { theme } from '@codecademy/gamut-styles'; - -import { tooltipBackgroundColor } from '../Tip/shared/styles'; - -const popoverAbove = { - borderLeft: 'none', - borderTop: 'none', - top: 'calc(100% - 10px)', -} as const; - -const popoverBelow = { - borderRight: 'none', - borderBottom: 'none', - top: '-10px', -} as const; - -const beakRight = { - right: '25px', -}; - -const beakLeft = { - left: '25px', -}; - -const beakCenter = { - left: 'calc(50% - 10px)', -}; - -const popoverAboveSml = { - borderLeft: 'none', - borderTop: 'none', - top: 'calc(100% - 8px)', -} as const; - -const popoverBelowSml = { - borderRight: 'none', - borderBottom: 'none', - top: '-8px', -} as const; - -const beakRightSml = { - right: '1.5rem', - bg: tooltipBackgroundColor, -}; - -const beakLeftSml = { - left: '1.5rem', - bg: tooltipBackgroundColor, -}; - -const beakCenterSml = { - left: 'calc(50% - 8px)', -}; - -const beakCenterSmlAbove = { - backgroundImage: `linear-gradient(to top left, ${theme.colors[tooltipBackgroundColor]} 55%, rgba(0,0,0,0) 20%)`, -}; - -const beakCenterSmlBelow = { - backgroundImage: `linear-gradient(to bottom right, ${theme.colors[tooltipBackgroundColor]} 55%, rgba(0,0,0,0) 20%)`, -}; - -export const createBeakVariantFromAlignment = (alignment: string) => { - let styleObject = {}; - - if (alignment.includes('sml')) { - if (alignment.includes('above')) { - styleObject = { ...popoverAboveSml }; - } else { - styleObject = { ...popoverBelowSml }; - } - if (alignment.includes('right')) { - styleObject = { ...beakRightSml, ...styleObject }; - } else if (alignment.includes('center')) { - styleObject = { ...beakCenterSml, ...styleObject }; - if (alignment.includes('above')) { - styleObject = { ...beakCenterSmlAbove, ...styleObject }; - } else if (alignment.includes('below')) { - styleObject = { ...beakCenterSmlBelow, ...styleObject }; - } - } else { - styleObject = { ...beakLeftSml, ...styleObject }; - } - } else { - if (alignment.includes('above')) { - styleObject = { ...popoverAbove }; - } else { - styleObject = { ...popoverBelow }; - } - if (alignment.includes('right')) { - styleObject = { ...beakRight, ...styleObject }; - } else if (alignment.includes('center')) { - styleObject = { ...beakCenter, ...styleObject }; - } else { - styleObject = { ...beakLeft, ...styleObject }; - } - } - - return { ...styleObject }; -}; - -const patternAbove = { - top: '-8px', -}; - -const patternBelow = { - top: '8px', -}; - -const patternRight = { - left: '-8px', -}; - -const patternLeft = { - left: '8px', -}; - -export const createPatternVariantFromAlignment = (alignment: string) => { - let styleObject = {}; - - if (alignment.includes('above')) { - styleObject = { ...patternAbove }; - } else { - styleObject = { ...patternBelow }; - } - if (alignment.includes('right')) { - styleObject = { ...patternRight, ...styleObject }; - } else { - styleObject = { ...patternLeft, ...styleObject }; - } - - return { ...styleObject }; -}; diff --git a/packages/gamut/src/Tip/InfoTip/styles.tsx b/packages/gamut/src/Tip/InfoTip/styles.tsx index d66d1e58a75..9d5a8953bcb 100644 --- a/packages/gamut/src/Tip/InfoTip/styles.tsx +++ b/packages/gamut/src/Tip/InfoTip/styles.tsx @@ -12,9 +12,9 @@ import { Box } from '../../Box'; import { ButtonSelectors } from '../../ButtonBase/ButtonBase'; import { tooltipArrowHeight, - tooltipBackgroundColor, + tooltipBgColor, tooltipVariantStyles, -} from '../shared/styles'; +} from '../shared/styles/styles'; import { TipPlacementComponentProps } from '../shared/types'; const textColor = 'secondary'; @@ -45,7 +45,7 @@ export const infoButtonStyles = css({ export type InfoButtonStatesProps = StyleProps; -export const newInfoTipAlignmentVariants = variant({ +export const infoTipAlignmentVariants = variant({ prop: 'alignment', base: { bg: 'transparent', @@ -55,12 +55,11 @@ export const newInfoTipAlignmentVariants = variant({ transitionDelay: `${timing.fast}`, position: 'absolute', '&::after': { - bg: tooltipBackgroundColor, + bg: tooltipBgColor, content: '""', display: 'block', height: `${tooltipArrowHeight}`, position: 'absolute', - transform: 'rotate(45deg)', width: `${tooltipArrowHeight}`, borderStyle: 'solid', }, @@ -81,5 +80,5 @@ export const InfoTipContainer = styled(Box)< StyleProps >` ${infoVisibilityStates} - ${newInfoTipAlignmentVariants} + ${infoTipAlignmentVariants} `; diff --git a/packages/gamut/src/Tip/PreviewTip/elements.tsx b/packages/gamut/src/Tip/PreviewTip/elements.tsx index 4d743fed464..6f42308c3c4 100644 --- a/packages/gamut/src/Tip/PreviewTip/elements.tsx +++ b/packages/gamut/src/Tip/PreviewTip/elements.tsx @@ -7,7 +7,7 @@ import { Anchor } from '../../Anchor'; import { Box, FlexBox, GridBox } from '../../Box'; import { ButtonSelectors } from '../../ButtonBase/ButtonBase'; import { Shimmer } from '../../Loading/Shimmer'; -import { patternContainerBaseStyles } from '../../Popover/styles'; +import { patternContainerBaseStyles } from '../../Popover/styles/pattern'; import { Text } from '../../Typography'; import { PreviewTipContent, TipPlacementComponentProps } from '../shared/types'; import { diff --git a/packages/gamut/src/Tip/ToolTip/elements.tsx b/packages/gamut/src/Tip/ToolTip/elements.tsx index 42ba92509fe..10807597aea 100644 --- a/packages/gamut/src/Tip/ToolTip/elements.tsx +++ b/packages/gamut/src/Tip/ToolTip/elements.tsx @@ -2,13 +2,11 @@ import { StyleProps } from '@codecademy/variance'; import styled from '@emotion/styled'; import { Box } from '../../Box'; -import { inlineToolTipState, toolTipAlignmentVariants } from '../shared/styles'; +import { toolTipAlignmentVariants } from '../shared/styles/styles'; export interface ToolTipContainerProps - extends StyleProps, - StyleProps {} + extends StyleProps {} export const ToolTipContainer = styled(Box)( - toolTipAlignmentVariants, - inlineToolTipState + toolTipAlignmentVariants ); diff --git a/packages/gamut/src/Tip/shared/FloatingTip.tsx b/packages/gamut/src/Tip/shared/FloatingTip.tsx index 8f78549f657..6a09b3dde0d 100644 --- a/packages/gamut/src/Tip/shared/FloatingTip.tsx +++ b/packages/gamut/src/Tip/shared/FloatingTip.tsx @@ -1,4 +1,3 @@ -import { CheckerDense } from '@codecademy/gamut-patterns'; import { useLayoutEffect, useRef, useState } from 'react'; import * as React from 'react'; import { useMeasure } from 'react-use'; @@ -10,8 +9,12 @@ import { FloatingTipTextWrapper, TargetContainer, } from './elements'; +import { + getAlignmentStyles, + getPopoverAlignmentAndPattern, +} from './styles/composeVariantsUtils'; import { TipWrapperProps } from './types'; -import { getAlignmentWidths, getPopoverAlignment, runWithDelay } from './utils'; +import { runWithDelay } from './utils'; type FocusOrMouseEvent = | React.FocusEvent @@ -34,29 +37,22 @@ export const FloatingTip: React.FC = ({ wrapperRef, }) => { const ref = useRef(null); - const [childRef, { width: tipWidth }] = useMeasure(); - const [offset, setOffset] = useState(0); const [isOpen, setIsOpen] = useState(false); const [isFocused, setIsFocused] = useState(false); - useLayoutEffect(() => { - const isCentered = alignment.includes('center'); + const commonPopoverProps = getPopoverAlignmentAndPattern({ alignment, type }); + const dims = getAlignmentStyles({ avatar, alignment, type }); + const [childRef, { width: tipWidth }] = useMeasure(); - if (ref?.current) { - if (!isCentered) { + const [offset, setOffset] = useState(undefined); + + useLayoutEffect(() => { + if (ref?.current?.clientWidth) { + if (type === 'info' || type === 'preview') setOffset(-ref.current.clientWidth / 2 + 32); - } else { - const trueTw = tipWidth + 16; - const targetWidth = ref?.current.clientWidth; - const diffOs = (trueTw - targetWidth) / 2; - setOffset(diffOs); - } } - }, [alignment, tipWidth]); - - const popoverAlignments = getPopoverAlignment({ alignment, type }); - const dims = getAlignmentWidths({ avatar, alignment, type }); + }, [alignment, tipWidth, type]); let hoverDelay: NodeJS.Timeout | undefined; let focusDelay: NodeJS.Timeout | undefined; @@ -130,20 +126,18 @@ export const FloatingTip: React.FC = ({ {children} = ({ alignment, @@ -37,7 +37,7 @@ export const InlineTip: React.FC = ({ : InfoTipContainer; const inlineWrapperProps = isHoverType ? {} : { hideTip: isTipHidden }; const tipWrapperProps = isHoverType ? ({ inheritDims } as const) : {}; - const tipBodyAlignment = getAlignmentWidths({ alignment, avatar, type }); + const tipBodyAlignment = getAlignmentStyles({ alignment, avatar, type }); const target = ( = ({ const tipBody = ( @@ -65,7 +64,7 @@ export const InlineTip: React.FC = ({ color="currentColor" id={id} role={type === 'tool' ? 'tooltip' : undefined} - width={narrow ? narrowWidth : undefined} + width={narrow ? narrowWidth : 'max-content'} zIndex="auto" > {type === 'preview' ? ( diff --git a/packages/gamut/src/Tip/shared/elements.tsx b/packages/gamut/src/Tip/shared/elements.tsx index 055b4cef033..0649622924d 100644 --- a/packages/gamut/src/Tip/shared/elements.tsx +++ b/packages/gamut/src/Tip/shared/elements.tsx @@ -6,12 +6,11 @@ import { Box, FlexBox } from '../../Box'; import { Selectors } from '../../ButtonBase/ButtonBase'; import { Popover } from '../../Popover'; import { - centerWidths, inlineToolTipBodyAlignments, narrowWidth, toolTipBodyCss, toolTipWidthRestrictions, -} from './styles'; +} from './styles/styles'; const tipWrapperStyles = { position: 'relative', @@ -22,9 +21,6 @@ const tipWrapperStyles = { const floatingTipTextStates = states({ isHoverType: { alignItems: 'flexStart' }, narrow: { width: narrowWidth }, - centered: { - ...centerWidths, - }, }); const inlineTipStates = states({ @@ -34,7 +30,10 @@ const inlineTipStates = states({ export const FloatingTipTextWrapper = styled(FlexBox)< StyleProps >( - css({ flexDirection: 'column', overflowWrap: 'break-word' }), + css({ + flexDirection: 'column', + overflowWrap: 'break-word', + }), floatingTipTextStates ); diff --git a/packages/gamut/src/Tip/shared/styles.tsx b/packages/gamut/src/Tip/shared/styles.tsx deleted file mode 100644 index 3ea4806e545..00000000000 --- a/packages/gamut/src/Tip/shared/styles.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import { - fontSmoothPixel, - states, - theme, - variant, -} from '@codecademy/gamut-styles'; - -import { tipAlignmentArray } from './types'; -import { - createToolTipVariantFromAlignment, - createVariantsFromAlignments, -} from './utils'; - -export const tooltipBackgroundColor = `background-contrast`; -export const tooltipArrowHeight = `1rem`; -const containerOffsetVertical = 12; - -export const narrowWidth = 64; -export const centerWidths = { minWidth: 64, maxWidth: 128 } as const; -const alignedAvatarWidth = { - maxWidth: { _: '95vw', xs: '600px' }, - width: 'max-content', -} as const; -const alignedMaxWidth = { width: 256 } as const; -const alignedPreviewWidth = { width: 418 } as const; - -const previewTipPadding = { p: 16 } as const; - -export const topStyles = { - bottom: 'calc(100% + 4px)', - pb: containerOffsetVertical, -} as const; - -const beforeStyles = { - content: '""', - position: 'absolute', - width: '100%', -}; - -export const topStylesBefore = { ...beforeStyles, height: 16, bottom: '-4px' }; - -export const topStylesAfter = { - borderColor: 'border-primary', - borderWidth: '0 1px 1px 0', - bottom: '0.25rem', -} as const; - -export const bottomStyles = { - top: 'calc(100% + 4px)', - pt: containerOffsetVertical, -} as const; - -export const bottomStylesBefore = { - ...beforeStyles, - height: 24, - top: '-8px', -}; - -export const bottomStylesAfter = { - borderColor: 'border-primary', - borderWidth: '1px 0 0 1px', - top: '0.25rem', -} as const; - -export const centerStyles = { - ...centerWidths, - left: 'calc(50% - 4rem)', -} as const; - -export const centerStylesAfter = { left: 'calc(50% - 0.5rem)' } as const; - -// This halfway fills the square we use to create the 'beak' of the tip so it does not overlap the tip text on the 'center' alignments -export const topCenterStylesAfter = { - backgroundImage: `linear-gradient(to top left, ${theme.colors[tooltipBackgroundColor]} 55%, rgba(0,0,0,0) 20%)`, -}; - -export const bottomCenterStylesAfter = { - backgroundImage: `linear-gradient(to bottom right, ${theme.colors[tooltipBackgroundColor]} 55%, rgba(0,0,0,0) 20%)`, -}; - -export const alignedStylesAfter = { bg: tooltipBackgroundColor }; - -export const leftStyles = { - justifyContent: 'flex-end', - right: 'calc(50% - 2rem)', -} as const; - -export const leftStylesAfter = { - right: '1.5rem', - ...alignedStylesAfter, -} as const; - -export const rightStyles = { - left: 'calc(50% - 2rem)', -} as const; - -export const rightStylesAfter = { - left: '1.5rem', - ...alignedStylesAfter, -} as const; - -export const tooltipVariantStyles = createVariantsFromAlignments( - tipAlignmentArray, - createToolTipVariantFromAlignment -); - -const centeredBodyStyles = { m: 'auto', p: 4, textAlign: 'center' } as const; - -const alignedBodyStyles = { p: 16 } as const; - -export const toolTipAlignmentVariants = variant({ - prop: 'alignment', - base: { - bg: 'transparent', - display: 'flex', - fontSmoothPixel, - maxWidth: '70vw', - opacity: 0, - position: 'absolute', - visibility: 'hidden', - - '&::after': { - content: '""', - display: 'block', - height: `${tooltipArrowHeight}`, - position: 'absolute', - transform: 'rotate(45deg)', - width: `${tooltipArrowHeight}`, - borderStyle: 'solid', - }, - }, - variants: tooltipVariantStyles, -}); - -export const inlineToolTipState = states({ - isToolTip: { width: '70vw' }, -}); - -export const inlineToolTipBodyAlignments = variant({ - prop: 'alignment', - variants: { - centered: { - ...centeredBodyStyles, - ...centerWidths, - }, - aligned: { - ...alignedBodyStyles, - ...alignedMaxWidth, - }, - previewAligned: { - ...alignedPreviewWidth, - ...previewTipPadding, - }, - avatarAligned: { - ...alignedAvatarWidth, - ...previewTipPadding, - }, - }, -}); - -export const popoverToolTipBodyAlignments = variant({ - prop: 'alignment', - variants: { - centered: { - ...centeredBodyStyles, - }, - aligned: { - ...alignedBodyStyles, - }, - }, -}); - -export const toolTipWidthRestrictions = variant({ - prop: 'dims', - variants: { - centered: { - ...centerWidths, - }, - aligned: { - ...alignedMaxWidth, - }, - avatarAligned: { - ...alignedAvatarWidth, - }, - previewAligned: { - ...alignedPreviewWidth, - }, - }, -}); - -export const toolTipBodyCss = { - bg: tooltipBackgroundColor, - color: 'text', - border: 1, - boxShadow: 'none', - borderRadius: 'sm', - display: 'inline-block', - fontSize: 14, - lineHeight: 'base', -} as const; diff --git a/packages/gamut/src/Tip/shared/styles/composeVariantsUtils.ts b/packages/gamut/src/Tip/shared/styles/composeVariantsUtils.ts new file mode 100644 index 00000000000..f2ff5386ca3 --- /dev/null +++ b/packages/gamut/src/Tip/shared/styles/composeVariantsUtils.ts @@ -0,0 +1,150 @@ +import { CheckerDense } from '@codecademy/gamut-patterns'; + +import { + PopoverProps, + PopoverXPositionType, + PopoverYPositionType, +} from '../../../Popover'; +import { TipPlacementComponentProps, TipWrapperProps } from '../types'; +import { + beforeStylesHorizontal, + bottomStyles, + bottomStylesAfter, + bottomStylesBefore, + centerHorizontal, + horizontalCenterStyles, + leftAlignStyles, + leftAlignStylesAfter, + leftVertStyles, + leftVertStylesAfter, + rightAlignStyles, + rightAlignStylesAfter, + rightVertStyles, + rightVertStylesAfter, + topStyles, + topStylesAfter, + topStylesBefore, + verticalCenterStyles, + verticalCenterStylesAfter, +} from './styles'; + +export const getAlignmentStyles = ({ + alignment, + avatar, + type, +}: Pick) => { + if (avatar) return 'avatarAligned'; + if (type === 'preview') return 'previewAligned'; + if (alignment.startsWith('left') || alignment.startsWith('right')) + return 'horizontalCenter'; + return alignment.includes('center') ? 'vertCenter' : 'aligned'; +}; + +export const getPopoverAlignmentAndPattern = ({ + alignment = 'top-left', + type, +}: Partial>): + | (PopoverXPositionType & Pick) + | (PopoverYPositionType & Pick) => { + const popoverAlignment: PopoverYPositionType & Pick = { + align: 'right', + beak: 'right', + position: 'above', + pattern: undefined, + }; + + if (type === 'tool') { + popoverAlignment.align = 'center'; + } + + if (type === 'preview') { + popoverAlignment.pattern = CheckerDense; + } + + if (alignment.includes('center')) { + popoverAlignment.beak = 'center'; + } + + if (alignment.includes('right-') || alignment.includes('left-')) { + if (alignment.includes('left-')) { + popoverAlignment.align = 'left'; + } else { + popoverAlignment.align = 'right'; + } + return { + beak: 'center', + position: 'center', + align: popoverAlignment.align, + }; + } + + if (alignment.includes('bottom')) popoverAlignment.position = 'below'; + + if (alignment.includes('-right')) { + popoverAlignment.align = 'left'; + popoverAlignment.beak = 'left'; + } + + return popoverAlignment; +}; + +export const createToolTipVariantFromAlignment = (alignment: string) => { + let styleObject = {}; + let styleObjectAfter = {}; + let styleObjectBefore = {}; + + const isRight = alignment.includes('right'); + const isTop = alignment.includes('top'); + const isCenter = alignment.includes('center'); + const isBottom = alignment.includes('bottom'); + const isLRAligned = + alignment.startsWith('right') || alignment.startsWith('left'); + + // top-center, top-right, + top-left styles + if (isTop) { + styleObject = { ...topStyles }; + styleObjectAfter = { ...topStylesAfter }; + styleObjectBefore = { ...topStylesBefore }; + // bottom-center, bottom-right, + bottom-left styles + } else if (isBottom) { + styleObject = { ...bottomStyles }; + styleObjectAfter = { ...bottomStylesAfter }; + styleObjectBefore = { ...bottomStylesBefore }; + } else if (isLRAligned) { + // right-center & left-center styles + styleObject = { ...horizontalCenterStyles }; + styleObjectAfter = { ...centerHorizontal }; + styleObjectBefore = { ...beforeStylesHorizontal }; + // right-center styles + if (isRight) { + styleObject = { ...styleObject, ...rightAlignStyles }; + styleObjectAfter = { ...styleObjectAfter, ...rightAlignStylesAfter }; + // left-center styles + } else { + styleObject = { ...styleObject, ...leftAlignStyles }; + styleObjectAfter = { ...styleObjectAfter, ...leftAlignStylesAfter }; + } + } + + // top-center + bottom-center styles + if (isCenter && !isLRAligned) { + styleObject = { ...styleObject, ...verticalCenterStyles }; + styleObjectAfter = { ...styleObjectAfter, ...verticalCenterStylesAfter }; + } else if (!isCenter && !isLRAligned) { + // top-right, bottom-right + if (isRight) { + styleObject = { ...styleObject, ...rightVertStyles }; + styleObjectAfter = { ...styleObjectAfter, ...rightVertStylesAfter }; + } else { + // top-left, bottom-left + styleObject = { ...styleObject, ...leftVertStyles }; + styleObjectAfter = { ...styleObjectAfter, ...leftVertStylesAfter }; + } + } + + return { + ...styleObject, + '&::after': styleObjectAfter, + '&::before': styleObjectBefore, + }; +}; diff --git a/packages/gamut/src/Tip/shared/styles/createVariantsUtils.ts b/packages/gamut/src/Tip/shared/styles/createVariantsUtils.ts new file mode 100644 index 00000000000..1d666240d88 --- /dev/null +++ b/packages/gamut/src/Tip/shared/styles/createVariantsUtils.ts @@ -0,0 +1,12 @@ +export const createVariantsFromAlignments = ( + array: readonly string[], + composeStyleObjectFunc: (alignment: string) => {} +) => { + const variantsObject = Object.fromEntries( + array.map((alignment) => { + const alignmentStyles = composeStyleObjectFunc(alignment); + return [alignment, alignmentStyles]; + }) + ); + return variantsObject; +}; diff --git a/packages/gamut/src/Tip/shared/styles/styles.tsx b/packages/gamut/src/Tip/shared/styles/styles.tsx new file mode 100644 index 00000000000..c79049d5b44 --- /dev/null +++ b/packages/gamut/src/Tip/shared/styles/styles.tsx @@ -0,0 +1,285 @@ +import { fontSmoothPixel, theme, variant } from '@codecademy/gamut-styles'; + +import { popoverPrimaryBgColor } from '../../../Popover/styles/base'; +import { tipAlignmentArray } from '../types'; +import { createToolTipVariantFromAlignment } from './composeVariantsUtils'; +import { createVariantsFromAlignments } from './createVariantsUtils'; + +/** + * For the Popover + Tooltip style files: + * + * 'above' + 'below' map to position, 'top' + 'bottom' map to beak alignment + * variants for both follow this formula: `position`-`beakPosition` + * Popovers additionally will have `-sml` added to the end of this string if they are the `secondary` variant + * + */ + +export const tooltipBgColor = `background-contrast`; +export const tooltipArrowHeight = `1rem`; +const containerOffsetVertical = 12; +const borderColor = 'border-primary'; + +export const narrowWidth = 64; +export const verticalCenterWidths = { minWidth: 64, maxWidth: 128 } as const; +export const horizontalCenterWidths = { + height: 'fit-content', + minWidth: 4, + maxWidth: 128, +} as const; + +export const centerHorizontal = { top: '0', bottom: '0', my: 'auto' } as const; + +const alignedAvatarWidth = { + maxWidth: { _: '95vw', xs: '600px' }, + width: 'max-content', +} as const; +const alignedMaxWidth = { width: 256 } as const; +const alignedPreviewWidth = { width: 418 } as const; + +const previewTipPadding = { p: 16 } as const; + +/* This halfway fills the square we use to create the 'beak' of the tip so it does not overlap the tip text on the 'center' alignments +We split the backgroundImage out because we share these styles with Popover * */ + +const beakBackgroundRotation = { + above: 'rotate(45deg)', + below: 'rotate(-135deg)', + right: 'rotate(135deg)', + left: 'rotate(-45deg)', +}; + +type GetBeakBackgroundType = { + alignment: keyof typeof beakBackgroundRotation; + color: typeof tooltipBgColor | typeof popoverPrimaryBgColor; +}; + +export const getBeakBgAndRotation = ({ + alignment, + color, +}: GetBeakBackgroundType) => { + return { + transform: beakBackgroundRotation[alignment], + backgroundImage: `linear-gradient(to top left, ${theme.colors[color]} 55%, rgba(0,0,0,0) 20%)`, + }; +}; + +export const beakStylesBase = { + borderColor, + borderWidth: '0 1px 1px 0', +}; + +export const beakTopStyles = { + ...beakStylesBase, + ...getBeakBgAndRotation({ alignment: 'above', color: tooltipBgColor }), +}; + +export const beakBottomStyles = { + ...beakStylesBase, + ...getBeakBgAndRotation({ alignment: 'below', color: tooltipBgColor }), +}; + +export const beakRightCenterStyles = { + ...beakStylesBase, + ...getBeakBgAndRotation({ alignment: 'right', color: tooltipBgColor }), +}; + +export const beakLeftCenterStyles = { + ...beakStylesBase, + ...getBeakBgAndRotation({ alignment: 'left', color: tooltipBgColor }), +}; + +const beforeStylesVert = { + content: '""', + position: 'absolute', + width: '100%', +}; + +export const topStyles = { + bottom: 'calc(100% + 4px)', + pb: containerOffsetVertical, +} as const; + +export const topStylesAfter = { + ...beakTopStyles, + bottom: '0.25rem', +} as const; + +export const topStylesBefore = { + ...beforeStylesVert, + height: 16, + bottom: '-4px', +}; + +export const bottomStyles = { + top: 'calc(100% + 4px)', + pt: containerOffsetVertical, +} as const; + +export const beforeStylesHorizontal = { + content: '""', + position: 'absolute', + height: '100%', +}; + +export const bottomStylesBefore = { + ...beforeStylesVert, + height: 24, + top: '-8px', +}; + +export const bottomStylesAfter = { + ...beakBottomStyles, + top: '0.25rem', +} as const; + +export const rightAlignStyles = { + pl: containerOffsetVertical, + left: '100%', +} as const; + +export const horizontalCenterStyles = { + ...horizontalCenterWidths, + ...centerHorizontal, +} as const; + +export const leftAlignStyles = { + pr: containerOffsetVertical, + right: '100%', +} as const; + +export const verticalCenterStyles = { + ...verticalCenterWidths, + left: 'calc(50% - 4rem)', + width: '70vw', +} as const; + +export const verticalCenterStylesAfter = { left: 'calc(50% - 0.5rem)' }; + +export const leftVertStyles = { + justifyContent: 'flex-end', + right: 'calc(50% - 2rem)', +} as const; + +export const leftVertStylesAfter = { + right: '1.5rem', +} as const; + +export const rightVertStyles = { left: 'calc(50% - 2rem)' } as const; +export const rightVertStylesAfter = { + left: '1.5rem', +} as const; + +export const rightAlignStylesAfter = { + left: '4px', + ...beakRightCenterStyles, +} as const; + +export const leftAlignStylesAfter = { + right: '4px', + ...beakLeftCenterStyles, +} as const; + +export const tooltipVariantStyles = createVariantsFromAlignments( + tipAlignmentArray, + createToolTipVariantFromAlignment +); + +const centeredBodyStyles = { + p: 4, + textAlign: 'center', + minWidth: 'inherit', + maxWidth: 'inherit', +} as const; + +const alignedBodyStyles = { p: 16 } as const; + +export const toolTipAlignmentVariants = variant({ + prop: 'alignment', + base: { + bg: 'transparent', + display: 'flex', + fontSmoothPixel, + maxWidth: '70vw', + opacity: 0, + position: 'absolute', + visibility: 'hidden', + '&::after': { + content: '""', + display: 'block', + height: `${tooltipArrowHeight}`, + position: 'absolute', + width: `${tooltipArrowHeight}`, + borderStyle: 'solid', + }, + }, + variants: tooltipVariantStyles, +}); + +export const inlineToolTipBodyAlignments = variant({ + prop: 'alignment', + variants: { + horizontalCenter: { + ...centeredBodyStyles, + }, + vertCenter: { + ...centeredBodyStyles, + mx: 'auto', + }, + aligned: { + ...alignedBodyStyles, + ...alignedMaxWidth, + }, + previewAligned: { + ...alignedPreviewWidth, + ...previewTipPadding, + }, + avatarAligned: { + ...alignedAvatarWidth, + ...previewTipPadding, + }, + }, +}); + +export const popoverToolTipBodyAlignments = variant({ + prop: 'alignment', + variants: { + centered: { + ...centeredBodyStyles, + }, + aligned: { + ...alignedBodyStyles, + }, + }, +}); + +export const toolTipWidthRestrictions = variant({ + prop: 'dims', + variants: { + horizontalCenter: { + ...horizontalCenterWidths, + }, + vertCenter: { + ...verticalCenterWidths, + }, + aligned: { + ...alignedMaxWidth, + }, + avatarAligned: { + ...alignedAvatarWidth, + }, + previewAligned: { + ...alignedPreviewWidth, + }, + }, +}); + +export const toolTipBodyCss = { + bg: tooltipBgColor, + color: 'text', + border: 1, + boxShadow: 'none', + borderRadius: 'sm', + display: 'inline-block', + fontSize: 14, + lineHeight: 'base', +} as const; diff --git a/packages/gamut/src/Tip/shared/types.tsx b/packages/gamut/src/Tip/shared/types.tsx index bec611f0876..3fb51f99f35 100644 --- a/packages/gamut/src/Tip/shared/types.tsx +++ b/packages/gamut/src/Tip/shared/types.tsx @@ -9,7 +9,12 @@ export const tipBaseAlignmentArray = [ 'top-right', ] as const; -const tipCenterAlignmentArray = ['bottom-center', 'top-center'] as const; +const tipCenterAlignmentArray = [ + 'bottom-center', + 'left-center', + 'right-center', + 'top-center', +] as const; export const tipAlignmentArray = [ ...tipBaseAlignmentArray, diff --git a/packages/gamut/src/Tip/shared/utils.tsx b/packages/gamut/src/Tip/shared/utils.tsx old mode 100644 new mode 100755 index 34039a56bbf..f6252938418 --- a/packages/gamut/src/Tip/shared/utils.tsx +++ b/packages/gamut/src/Tip/shared/utils.tsx @@ -1,117 +1,9 @@ import { timingValues } from '@codecademy/gamut-styles'; -import { PopoverProps } from '../../Popover'; -import { - bottomCenterStylesAfter, - bottomStyles, - bottomStylesAfter, - bottomStylesBefore, - centerStyles, - centerStylesAfter, - leftStyles, - leftStylesAfter, - rightStyles, - rightStylesAfter, - topCenterStylesAfter, - topStyles, - topStylesAfter, - topStylesBefore, -} from './styles'; -import { TipPlacementComponentProps, TipWrapperProps } from './types'; - export const runWithDelay = (func: () => void) => { return setTimeout(func, timingValues?.base); }; -export const getAlignmentWidths = ({ - alignment, - avatar, - type, -}: Pick) => { - if (avatar) return 'avatarAligned'; - if (type === 'preview') return 'previewAligned'; - return alignment.includes('center') ? 'centered' : 'aligned'; -}; - -export const getPopoverAlignment = ({ - alignment = 'top-left', - type, -}: Partial>) => { - const popoverAlignment: Pick = { - align: 'right', - beak: 'right', - position: 'above', - }; - - if (type === 'tool') { - popoverAlignment.align = undefined; - } - - if (alignment.includes('bottom')) popoverAlignment.position = 'below'; - - if (alignment.includes('right')) { - popoverAlignment.align = 'left'; - popoverAlignment.beak = 'left'; - } - - if (alignment.includes('center')) { - popoverAlignment.beak = 'center'; - } - - return popoverAlignment; -}; - -export const createToolTipVariantFromAlignment = (alignment: string) => { - let styleObject = {}; - let styleObjectAfter = {}; - let styleObjectBefore = {}; - - if (alignment.includes('top')) { - styleObject = { ...topStyles }; - styleObjectAfter = { ...topStylesAfter }; - styleObjectBefore = { ...topStylesBefore }; - } else { - styleObject = { ...bottomStyles }; - styleObjectAfter = { ...bottomStylesAfter }; - styleObjectBefore = { ...bottomStylesBefore }; - } - - if (alignment.includes('center')) { - styleObject = { ...styleObject, ...centerStyles }; - styleObjectAfter = { ...styleObjectAfter, ...centerStylesAfter }; - if (alignment.includes('top')) { - styleObjectAfter = { ...styleObjectAfter, ...topCenterStylesAfter }; - } else { - styleObjectAfter = { ...styleObjectAfter, ...bottomCenterStylesAfter }; - } - } else if (alignment.includes('right')) { - styleObject = { ...styleObject, ...rightStyles }; - styleObjectAfter = { ...styleObjectAfter, ...rightStylesAfter }; - } else { - styleObject = { ...styleObject, ...leftStyles }; - styleObjectAfter = { ...styleObjectAfter, ...leftStylesAfter }; - } - - return { - ...styleObject, - '&::after': styleObjectAfter, - '&::before': styleObjectBefore, - }; -}; - -export const createVariantsFromAlignments = ( - array: readonly string[], - composeStyleObjectFunc: (alignment: string) => {} -) => { - const variantsObject = Object.fromEntries( - array.map((alignment) => { - const alignmentStyles = composeStyleObjectFunc(alignment); - return [alignment, alignmentStyles]; - }) - ); - return variantsObject; -}; - export const escapeKeyPressHandler = ( event: React.KeyboardEvent ) => { diff --git a/packages/styleguide/src/lib/Molecules/Coachmark/Coachmark.stories.tsx b/packages/styleguide/src/lib/Molecules/Coachmark/Coachmark.stories.tsx index db97d53363a..d0259d84cb8 100644 --- a/packages/styleguide/src/lib/Molecules/Coachmark/Coachmark.stories.tsx +++ b/packages/styleguide/src/lib/Molecules/Coachmark/Coachmark.stories.tsx @@ -60,6 +60,7 @@ export const Customized: Story = { beak: 'left', outline: true, pattern: CheckerDense, + position: 'above', }} /> ), diff --git a/packages/styleguide/src/lib/Molecules/Popover/Popover.mdx b/packages/styleguide/src/lib/Molecules/Popover/Popover.mdx index 503efe1e75a..e45657f4c9a 100644 --- a/packages/styleguide/src/lib/Molecules/Popover/Popover.mdx +++ b/packages/styleguide/src/lib/Molecules/Popover/Popover.mdx @@ -35,11 +35,11 @@ Popovers are generally used for interactive contents, such as new and exciting f ### Beak -Using the beak prop, you can add a beak to the popover. +Using the `beak` prop, you can add a beak that is right, left, or center aligned to a `Popover`. -Tooltips with a _centered_ beak -`center`- are smaller sized and meant for a single small number of words, like keyboard shortcuts. Similar to the center aligned ToolTip. +Popovers with a _centered_ beak -`center`- are intended to be smaller sized and meant for a single small number of words. They are similar to our ToolTips. Please note that center aligned `Popover`s `align="center"` can only render a centered beak. @@ -52,7 +52,11 @@ The outline color is `theme.colors.secondary` so in Dark Mode, the outline will ### Position -Using the position prop, you can add a position (`above` or `below`) to the popover. It will be `below` by default. +Using the `position` prop, you can add a position to the `Popover`. + +`above` and `below` will position the `Popover` centered to the vertical line of the target element. + +`center` will position the `Popover` centered to the horizontal center of the target element and shifted to the aligned side. @@ -65,6 +69,14 @@ Using the position prop, you can add a position (`above` or `below`) to the popo #### Below + + #### Center Left + + + + #### Center Right + + diff --git a/packages/styleguide/src/lib/Molecules/Popover/Popover.stories.tsx b/packages/styleguide/src/lib/Molecules/Popover/Popover.stories.tsx index 5a98677ee73..341c958d137 100644 --- a/packages/styleguide/src/lib/Molecules/Popover/Popover.stories.tsx +++ b/packages/styleguide/src/lib/Molecules/Popover/Popover.stories.tsx @@ -33,7 +33,7 @@ const PopoverExample = ({ p = 16, ...rest }: PopoverExampleProps) => { const toggleOpen = () => setOpen(!open); return ( <> - + Open Popover @@ -60,7 +60,7 @@ export const Default: Story = { }; export const Beak: Story = { - render: (args) => , + render: (args) => , }; export const BeakCentered: Story = { @@ -68,34 +68,71 @@ export const BeakCentered: Story = { }; export const Outline: Story = { - render: (args) => , + render: (args) => ( + + ), }; export const Above: Story = { - render: (args) => , + render: (args) => , }; export const Below: Story = { - render: (args) => , + render: (args) => , +}; +export const CenterLeft: Story = { + render: (args) => ( + + ), +}; + +export const CenterRight: Story = { + render: (args) => ( + + ), }; export const PopoverCheckerDense: Story = { - render: (args) => , + render: (args) => ( + + ), }; export const PopoverCheckerLoose: Story = { - render: (args) => , + render: (args) => ( + + ), }; export const PopoverCheckerRegular: Story = { - render: (args) => , + render: (args) => ( + + ), }; export const PopoverDiagonalADense: Story = { - render: (args) => , + render: (args) => ( + + ), }; export const PopoverDiagonalALoose: Story = { - render: (args) => , + render: (args) => ( + + ), }; export const PopoverDiagonalARegular: Story = { - render: (args) => , + render: (args) => ( + + ), }; const PopoverWithoutFocus = (args: PopoverProps) => { @@ -135,6 +172,12 @@ export const Animation: Story = { export const Variant: Story = { render: (args) => ( - + ), }; diff --git a/packages/styleguide/src/lib/Molecules/Tips/InfoTip/InfoTip.stories.tsx b/packages/styleguide/src/lib/Molecules/Tips/InfoTip/InfoTip.stories.tsx index 3c69cc27c3d..3b35e7184ed 100644 --- a/packages/styleguide/src/lib/Molecules/Tips/InfoTip/InfoTip.stories.tsx +++ b/packages/styleguide/src/lib/Molecules/Tips/InfoTip/InfoTip.stories.tsx @@ -74,7 +74,6 @@ const PlacementExample: React.FC = (args) => { This text is in a small space and needs floating placement {' '} - {/* @ts-expect-error Storybook is not correctly typing some components */} ); @@ -84,7 +83,7 @@ export const Placement: Story = { render: (args) => , }; -const WithLinksOrButtonsExample: React.FC = () => { +const WithLinksOrButtonsExample: React.FC = ({ args }) => { const ref = useRef(null); const onClick = ({ isTipHidden }: { isTipHidden: boolean }) => { @@ -106,6 +105,7 @@ const WithLinksOrButtonsExample: React.FC = () => { } placement="floating" onClick={onClick} + {...args} /> ); diff --git a/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.mdx b/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.mdx index 7dda587b40b..102670720f6 100644 --- a/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.mdx +++ b/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.mdx @@ -28,7 +28,13 @@ export const parameters = { Tooltips are not meant to be used for long-form content or to replace the need for a label. They should be used sparingly and only when necessary. -## Use Cases +## Variants + +### Alignments + +Our `ToolTip` all align to the center of the interactive component they are describing. They can be aligned to the top, bottom, left, or right of the component. + + ### IconButton + ToolTip @@ -60,4 +66,4 @@ When a Button is disabled with a tooltip, you must use the `aria-disabled` prop - + diff --git a/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx b/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx index 6bfce1234b9..5aa2996009e 100644 --- a/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx +++ b/packages/styleguide/src/lib/Molecules/Tips/ToolTip/ToolTip.stories.tsx @@ -16,20 +16,31 @@ import type { Meta, StoryObj } from '@storybook/react'; const meta: Meta = { component: ToolTip, - args: {}, + args: { id: 'fill-id', info: 'Tooltip' }, }; export default meta; type Story = StoryObj; -export const Default: Story = { +const alignments = [ + 'top-center', + 'bottom-center', + 'left-center', + 'right-center', +] as const; + +export const Alignments: Story = { render: () => ( - - - - Click me - - + + {alignments.map((alignment) => { + return ( + + ); + })} ), }; @@ -39,8 +50,8 @@ export const WithIconButton: Story = { + {' '} + ), }; @@ -100,3 +121,15 @@ export const Disabled: Story = { ), }; + +export const Default: Story = { + render: (args) => ( + + + + Click me + + + + ), +};