diff --git a/packages/dropdowns.legacy/src/styled/menu/StyledMenu.ts b/packages/dropdowns.legacy/src/styled/menu/StyledMenu.ts index 98f47cd3b49..67af34a5484 100644 --- a/packages/dropdowns.legacy/src/styled/menu/StyledMenu.ts +++ b/packages/dropdowns.legacy/src/styled/menu/StyledMenu.ts @@ -37,8 +37,8 @@ export const StyledMenu = styled.ul.attrs(props => ({ ${props => props.hasArrow && arrowStyles(getArrowPosition(props.placement), { - size: `${props.theme.space.base * 2}px`, - inset: '1.5px', // More consistent cross-browser positioning with 1.5px + size: `${props.theme.space.base * 1.5}px`, + inset: '1px', animationModifier: props.isAnimated ? '.is-animated' : undefined })}; diff --git a/packages/dropdowns/src/views/menu/StyledMenu.ts b/packages/dropdowns/src/views/menu/StyledMenu.ts index 8283149c80f..1a3021f04a2 100644 --- a/packages/dropdowns/src/views/menu/StyledMenu.ts +++ b/packages/dropdowns/src/views/menu/StyledMenu.ts @@ -33,8 +33,8 @@ export const StyledMenu = styled(StyledListbox).attrs({ ${props => props.arrowPosition && arrowStyles(props.arrowPosition, { - size: `${props.theme.space.base * 2}px`, - inset: '1.5px', // More consistent cross-browser positioning with 1.5px + size: `${props.theme.space.base * 1.5}px`, + inset: '1px', animationModifier: '[data-garden-animate-arrow="true"]' })}; diff --git a/packages/theming/src/utils/arrowStyles.spec.tsx b/packages/theming/src/utils/arrowStyles.spec.tsx index c69aaab26cc..b52a83bb009 100644 --- a/packages/theming/src/utils/arrowStyles.spec.tsx +++ b/packages/theming/src/utils/arrowStyles.spec.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { render } from 'garden-test-utils'; import styled, { ThemeProps, DefaultTheme } from 'styled-components'; -import { math, stripUnit } from 'polished'; +import { stripUnit } from 'polished'; import arrowStyles from './arrowStyles'; import { ArrowPosition } from '../types'; @@ -29,11 +29,20 @@ const StyledDiv = styled.div` `; const getArrowSize = (size = '6px') => { - return `${Math.round(((stripUnit(size) as number) * 2) / Math.sqrt(2))}px`; + const sizeOffset = 2; + const squareSize = ((stripUnit(size) as number) * 2) / Math.sqrt(2) + sizeOffset; + const squareSizeRounded = Math.round(squareSize * 100) / 100; + + return `${squareSizeRounded}px`; }; const getArrowInset = (inset: string, size?: string) => { - return math(`${getArrowSize(size)} / -2 + ${inset}`); + const insetValue = stripUnit(inset) as number; + + const defaultInset = 0.3; + const margin = (stripUnit(getArrowSize(size)) as number) / -2; + + return `${Math.round((margin + insetValue + defaultInset) * 100) / 100}px`; }; describe('arrowStyles', () => { @@ -55,7 +64,7 @@ describe('arrowStyles', () => { POSITION.forEach(position => { const { container } = render(); - const value = math(`${getArrowSize()} / -2`); + const value = getArrowInset('0'); expect(container.firstChild).toHaveStyleRule(position, value, { modifier: '::before' }); }); diff --git a/packages/theming/src/utils/arrowStyles.ts b/packages/theming/src/utils/arrowStyles.ts index 57a2dfb5e94..64ade36f248 100644 --- a/packages/theming/src/utils/arrowStyles.ts +++ b/packages/theming/src/utils/arrowStyles.ts @@ -6,7 +6,7 @@ */ import { css, keyframes } from 'styled-components'; -import { math, stripUnit } from 'polished'; +import { stripUnit } from 'polished'; import { ArrowPosition } from '../types'; type ArrowOptions = { @@ -34,43 +34,52 @@ const animationStyles = (position: ArrowPosition, modifier: string) => { `; }; -const positionStyles = (position: ArrowPosition, size: string, inset: string) => { - const margin = math(`${size} / -2`); - const placement = math(`${margin} + ${inset}`); - let transform; +const positionStyles = (position: ArrowPosition, size: number, inset: number) => { + /** Overlap the arrow with the base element's border. + * This value + rounding have been found to work well regardless of monitor pixel density and browser. + */ + const defaultInset = 0.3; + const margin = size / -2; + const placement = Math.round((margin + inset + defaultInset) * 100) / 100; + + const marginPx = `${margin}px`; + const placementPx = `${placement}px`; + const sizePx = `${size}px`; + let positionCss; + let transform; if (position.startsWith('top')) { transform = 'rotate(-135deg)'; positionCss = css` - top: ${placement}; - right: ${position === 'top-right' && size}; - left: ${position === 'top' ? '50%' : position === 'top-left' && size}; - margin-left: ${position === 'top' && margin}; + top: ${placementPx}; + right: ${position === 'top-right' && sizePx}; + left: ${position === 'top' ? '50%' : position === 'top-left' && sizePx}; + margin-left: ${position === 'top' && marginPx}; `; } else if (position.startsWith('right')) { transform = 'rotate(-45deg)'; positionCss = css` - top: ${position === 'right' ? '50%' : position === 'right-top' && size}; - right: ${placement}; - bottom: ${position === 'right-bottom' && size}; - margin-top: ${position === 'right' && margin}; + top: ${position === 'right' ? '50%' : position === 'right-top' && sizePx}; + right: ${placementPx}; + bottom: ${position === 'right-bottom' && sizePx}; + margin-top: ${position === 'right' && marginPx}; `; } else if (position.startsWith('bottom')) { transform = 'rotate(45deg)'; positionCss = css` - right: ${position === 'bottom-right' && size}; - bottom: ${placement}; - left: ${position === 'bottom' ? '50%' : position === 'bottom-left' && size}; - margin-left: ${position === 'bottom' && margin}; + right: ${position === 'bottom-right' && sizePx}; + bottom: ${placementPx}; + left: ${position === 'bottom' ? '50%' : position === 'bottom-left' && sizePx}; + margin-left: ${position === 'bottom' && marginPx}; `; } else if (position.startsWith('left')) { transform = 'rotate(135deg)'; positionCss = css` - top: ${position === 'left' ? '50%' : position === 'left-top' && size}; + top: ${position === 'left' ? '50%' : position === 'left-top' && sizePx}; bottom: ${size}; - left: ${placement}; - margin-top: ${position === 'left' && margin}; + left: ${placementPx}; + margin-top: ${position === 'left' && marginPx}; `; } @@ -120,9 +129,19 @@ const positionStyles = (position: ArrowPosition, size: string, inset: string) => * @component */ export default function arrowStyles(position: ArrowPosition, options: ArrowOptions = {}) { - const inset = options.inset || '0'; - const size = options.size === undefined ? 6 : (stripUnit(options.size) as number); - const squareSize = `${Math.round((size * 2) / Math.sqrt(2))}px`; + const inset = stripUnit(options.inset || '0') as number; + const size = stripUnit(options.size || '6') as number; + + /** + * Adjusts the size to account for the overlap between the arrow and the base element. + * This value + rounding have been found to work well regardless of monitor pixel density and browser. + */ + const sizeOffset = 2; + + const squareSize = (size * 2) / Math.sqrt(2) + sizeOffset; + const squareSizeRounded = Math.round(squareSize * 100) / 100; + const squareSizePx = `${squareSizeRounded}px`; + const afterOffset = 0; const beforeOffset = afterOffset + 2; @@ -131,7 +150,7 @@ export default function arrowStyles(position: ArrowPosition, options: ArrowOptio * 2. Apply shared properties to ::before and ::after. * 3. Display border with inherited border-color * 4. Clip the outer square forming the arrow border into a triangle so that the - * border merge with the container's. + * border merges with the container's. * 5. Clip the inner square forming the arrow body into a triangle so that it * doesn't interfere with container content. */ @@ -144,26 +163,25 @@ export default function arrowStyles(position: ArrowPosition, options: ArrowOptio position: absolute; border-width: inherit; border-style: inherit; - width: ${squareSize}; - height: ${squareSize}; + background-color: inherit; + width: ${squareSizePx}; + height: ${squareSizePx}; content: ''; box-sizing: inherit; } &::before { border-color: inherit; /* [3] */ - background-color: transparent; clip-path: polygon(100% ${beforeOffset}px, ${beforeOffset}px 100%, 100% 100%); /* [4] */ } &::after { border-color: transparent; background-clip: content-box; - background-color: inherit; clip-path: polygon(100% ${afterOffset}px, ${afterOffset}px 100%, 100% 100%); /* [5] */ } - ${positionStyles(position, squareSize, inset)}; + ${positionStyles(position, squareSizeRounded, inset)}; ${options.animationModifier && animationStyles(position, options.animationModifier)}; `; } diff --git a/packages/tooltips/src/elements/Tooltip.spec.tsx b/packages/tooltips/src/elements/Tooltip.spec.tsx index 8e60728d60e..3529e5edc44 100644 --- a/packages/tooltips/src/elements/Tooltip.spec.tsx +++ b/packages/tooltips/src/elements/Tooltip.spec.tsx @@ -134,7 +134,7 @@ describe('Tooltip', () => { { color: PALETTE.white, bgColor: PALETTE.grey[900], - borderColor: 'transparent' + borderColor: PALETTE.grey[900] } ], [ @@ -143,7 +143,7 @@ describe('Tooltip', () => { { color: PALETTE.white, bgColor: PALETTE.grey[700], - borderColor: 'transparent' + borderColor: PALETTE.grey[700] } ] ])( diff --git a/packages/tooltips/src/styled/StyledTooltip.ts b/packages/tooltips/src/styled/StyledTooltip.ts index 608d4b9e3bb..f8e8925918b 100644 --- a/packages/tooltips/src/styled/StyledTooltip.ts +++ b/packages/tooltips/src/styled/StyledTooltip.ts @@ -29,7 +29,6 @@ interface IStyledTooltipProps extends Pick) => { @@ -74,22 +73,16 @@ const sizeStyles = ({ } let arrowSize; - let arrowInset; if (hasArrow) { if (size === 'small' || size === 'medium') { arrowSize = margin; - arrowInset = type === 'dark' ? '0px' : '1px'; - } else { - arrowInset = type === 'dark' ? '0px' : '1px'; - - if (size === 'large') { - margin = `${theme.space.base * 2}px`; - arrowSize = margin; - } else if (size === 'extra-large') { - margin = `${theme.space.base * 3}px`; - arrowSize = `${theme.space.base * 2.5}px`; - } + } else if (size === 'large') { + margin = `${theme.space.base * 2}px`; + arrowSize = margin; + } else if (size === 'extra-large') { + margin = `${theme.space.base * 3}px`; + arrowSize = `${theme.space.base * 2.5}px`; } } @@ -104,11 +97,7 @@ const sizeStyles = ({ font-size: ${fontSize}; overflow-wrap: ${overflowWrap}; - ${hasArrow && - arrowStyles(getArrowPosition(theme, placement), { - size: arrowSize, - inset: arrowInset - })}; + ${hasArrow && arrowStyles(getArrowPosition(theme, placement), { size: arrowSize })}; ${StyledParagraph} { margin-top: ${paragraphMarginTop}; @@ -128,28 +117,28 @@ const colorStyles = ({ theme, type }: IStyledTooltipProps & ThemeProps({ text-align: ${props => (props.theme.rtl ? 'right' : 'left')}; font-weight: ${props => props.theme.fontWeights.regular}; - ${props => sizeStyles(props)}; + ${sizeStyles}; &[aria-hidden='true'] { display: none;