Skip to content

Commit

Permalink
[@mantine/core] Add arrowPosition prop to Tooltip and Popover compo…
Browse files Browse the repository at this point in the history
…nents to configure how the arrow is position relative to the target element (#3100)

Co-authored-by: Jérémie van der Sande <jeremie.van-der-sande@ubisoft.com>
  • Loading branch information
jvdsande and Jérémie van der Sande committed Dec 4, 2022
1 parent c781e9c commit 43e72a6
Show file tree
Hide file tree
Showing 11 changed files with 130 additions and 18 deletions.
25 changes: 23 additions & 2 deletions src/mantine-core/src/Floating/FloatingArrow/FloatingArrow.tsx
@@ -1,19 +1,37 @@
import React, { forwardRef } from 'react';
import { useMantineTheme } from '@mantine/styles';
import { getArrowPositionStyles } from './get-arrow-position-styles';
import { FloatingPosition } from '../types';
import { ArrowPosition, FloatingPosition } from '../types';

interface FloatingArrowProps extends React.ComponentPropsWithoutRef<'div'> {
withBorder: boolean;
position: FloatingPosition;
arrowSize: number;
arrowOffset: number;
arrowRadius: number;
arrowPosition: ArrowPosition;
arrowX: number;
arrowY: number;
visible: boolean;
}

export const FloatingArrow = forwardRef<HTMLDivElement, FloatingArrowProps>(
({ withBorder, position, arrowSize, arrowRadius, visible, arrowX, arrowY, ...others }, ref) => {
(
{
withBorder,
position,
arrowSize,
arrowOffset,
arrowRadius,
arrowPosition,
visible,
arrowX,
arrowY,
...others
},
ref
) => {
const theme = useMantineTheme();
if (!visible) {
return null;
}
Expand All @@ -26,7 +44,10 @@ export const FloatingArrow = forwardRef<HTMLDivElement, FloatingArrowProps>(
withBorder,
position,
arrowSize,
arrowOffset,
arrowRadius,
arrowPosition,
dir: theme.dir,
arrowX,
arrowY,
})}
Expand Down
@@ -1,5 +1,48 @@
import { CSSObject } from '@mantine/styles';
import type { FloatingPosition, FloatingSide, FloatingPlacement } from '../types';
import type { FloatingPosition, FloatingSide, FloatingPlacement, ArrowPosition } from '../types';

function horizontalSide(
placement: FloatingPlacement | 'center',
arrowY: number,
arrowOffset: number,
arrowPosition: ArrowPosition
) {
if (placement === 'center' || arrowPosition === 'center') {
return { top: arrowY };
}

if (placement === 'end') {
return { bottom: arrowOffset };
}

if (placement === 'start') {
return { top: arrowOffset };
}

return {};
}

function verticalSide(
placement: FloatingPlacement | 'center',
arrowX: number,
arrowOffset: number,
arrowPosition: ArrowPosition,
dir: 'rtl' | 'ltr'
) {
if (placement === 'center' || arrowPosition === 'center') {
return { left: arrowX };
}

if (placement === 'end') {
return { [dir === 'ltr' ? 'right' : 'left']: arrowOffset };
}

if (placement === 'start') {
return { [dir === 'ltr' ? 'left' : 'right']: arrowOffset };
}

return {};
}

const radiusByFloatingSide: Record<
FloatingSide,
Expand All @@ -21,18 +64,24 @@ export function getArrowPositionStyles({
position,
withBorder,
arrowSize,
arrowOffset,
arrowRadius,
arrowPosition,
arrowX,
arrowY,
dir,
}: {
position: FloatingPosition;
withBorder: boolean;
arrowSize: number;
arrowOffset: number;
arrowRadius: number;
arrowPosition: ArrowPosition;
arrowX: number;
arrowY: number;
dir: 'rtl' | 'ltr';
}) {
const [side] = position.split('-') as [FloatingSide, FloatingPlacement];
const [side, placement = 'center'] = position.split('-') as [FloatingSide, FloatingPlacement];
const baseStyles = {
width: arrowSize,
height: arrowSize,
Expand All @@ -41,13 +90,13 @@ export function getArrowPositionStyles({
[radiusByFloatingSide[side]]: arrowRadius,
};

const arrowPosition = withBorder ? -arrowSize / 2 - 1 : -arrowSize / 2;
const arrowPlacement = withBorder ? -arrowSize / 2 - 1 : -arrowSize / 2;

if (side === 'left') {
return {
...baseStyles,
top: arrowY,
right: arrowPosition,
...horizontalSide(placement, arrowY, arrowOffset, arrowPosition),
right: arrowPlacement,
borderLeft: 0,
borderBottom: 0,
};
Expand All @@ -56,8 +105,8 @@ export function getArrowPositionStyles({
if (side === 'right') {
return {
...baseStyles,
top: arrowY,
left: arrowPosition,
...horizontalSide(placement, arrowY, arrowOffset, arrowPosition),
left: arrowPlacement,
borderRight: 0,
borderTop: 0,
};
Expand All @@ -66,8 +115,8 @@ export function getArrowPositionStyles({
if (side === 'top') {
return {
...baseStyles,
left: arrowX,
bottom: arrowPosition,
...verticalSide(placement, arrowX, arrowOffset, arrowPosition, dir),
bottom: arrowPlacement,
borderTop: 0,
borderLeft: 0,
};
Expand All @@ -76,8 +125,8 @@ export function getArrowPositionStyles({
if (side === 'bottom') {
return {
...baseStyles,
left: arrowX,
top: arrowPosition,
...verticalSide(placement, arrowX, arrowOffset, arrowPosition, dir),
top: arrowPlacement,
borderBottom: 0,
borderRight: 0,
};
Expand Down
2 changes: 1 addition & 1 deletion src/mantine-core/src/Floating/index.ts
Expand Up @@ -3,4 +3,4 @@ export { useFloatingAutoUpdate } from './use-floating-auto-update';
export { getFloatingPosition } from './get-floating-position/get-floating-position';
export { FloatingArrow } from './FloatingArrow/FloatingArrow';

export type { FloatingPosition, FloatingPlacement, FloatingSide } from './types';
export type { FloatingPosition, FloatingPlacement, FloatingSide, ArrowPosition } from './types';
1 change: 1 addition & 0 deletions src/mantine-core/src/Floating/types.ts
@@ -1,3 +1,4 @@
export type FloatingPlacement = 'end' | 'start';
export type FloatingSide = 'top' | 'right' | 'bottom' | 'left';
export type FloatingPosition = FloatingSide | `${FloatingSide}-${FloatingPlacement}`;
export type ArrowPosition = 'center' | 'side';
4 changes: 3 additions & 1 deletion src/mantine-core/src/Popover/Popover.context.ts
@@ -1,7 +1,7 @@
import { ReferenceType } from '@floating-ui/react-dom-interactions';
import { createSafeContext } from '@mantine/utils';
import { MantineNumberSize, MantineShadow } from '@mantine/styles';
import { FloatingPosition } from '../Floating';
import { FloatingPosition, ArrowPosition } from '../Floating';
import { MantineTransition } from '../Transition';
import { POPOVER_ERRORS } from './Popover.errors';
import { PopoverWidth } from './Popover.types';
Expand All @@ -21,7 +21,9 @@ interface PopoverContext {
width?: PopoverWidth;
withArrow: boolean;
arrowSize: number;
arrowOffset: number;
arrowRadius: number;
arrowPosition: ArrowPosition;
trapFocus: boolean;
placement: FloatingPosition;
withinPortal: boolean;
Expand Down
9 changes: 8 additions & 1 deletion src/mantine-core/src/Popover/Popover.tsx
Expand Up @@ -13,7 +13,7 @@ import {
useComponentDefaultProps,
} from '@mantine/styles';
import { MantineTransition } from '../Transition';
import { getFloatingPosition, FloatingPosition } from '../Floating';
import { getFloatingPosition, FloatingPosition, ArrowPosition } from '../Floating';
import { usePopover } from './use-popover';
import { PopoverContextProvider } from './Popover.context';
import {
Expand Down Expand Up @@ -71,6 +71,9 @@ export interface PopoverBaseProps {
/** Arrow radius in px */
arrowRadius?: number;

/** Arrow position **/
arrowPosition?: ArrowPosition;

/** Determines whether dropdown should be rendered within Portal, defaults to false */
withinPortal?: boolean;

Expand Down Expand Up @@ -137,6 +140,7 @@ const defaultProps: Partial<PopoverProps> = {
arrowSize: 7,
arrowOffset: 5,
arrowRadius: 0,
arrowPosition: 'side',
closeOnClickOutside: true,
withinPortal: false,
closeOnEscape: true,
Expand Down Expand Up @@ -166,6 +170,7 @@ export function Popover(props: PopoverProps) {
arrowSize,
arrowOffset,
arrowRadius,
arrowPosition,
unstyled,
classNames,
styles,
Expand Down Expand Up @@ -258,7 +263,9 @@ export function Popover(props: PopoverProps) {
width,
withArrow,
arrowSize,
arrowOffset,
arrowRadius,
arrowPosition,
placement: popover.floating.placement,
trapFocus,
withinPortal,
Expand Down
Expand Up @@ -95,6 +95,8 @@ export function PopoverDropdown(props: PopoverDropdownProps) {
position={ctx.placement}
arrowSize={ctx.arrowSize}
arrowRadius={ctx.arrowRadius}
arrowOffset={ctx.arrowOffset}
arrowPosition={ctx.arrowPosition}
className={classes.arrow}
/>
</Box>
Expand Down
9 changes: 8 additions & 1 deletion src/mantine-core/src/Tooltip/Tooltip.tsx
Expand Up @@ -5,7 +5,7 @@ import { getDefaultZIndex, useComponentDefaultProps } from '@mantine/styles';
import { TooltipGroup } from './TooltipGroup/TooltipGroup';
import { TooltipFloating } from './TooltipFloating/TooltipFloating';
import { useTooltip } from './use-tooltip';
import { FloatingArrow, getFloatingPosition, FloatingPosition } from '../Floating';
import { FloatingArrow, getFloatingPosition, FloatingPosition, ArrowPosition } from '../Floating';
import { MantineTransition, Transition } from '../Transition';
import { OptionalPortal } from '../Portal';
import { Box } from '../Box';
Expand Down Expand Up @@ -41,6 +41,9 @@ export interface TooltipProps extends TooltipBaseProps {
/** Arrow radius in px */
arrowRadius?: number;

/** Arrow position **/
arrowPosition?: ArrowPosition;

/** One of premade transitions ot transition object */
transition?: MantineTransition;

Expand All @@ -65,6 +68,7 @@ const defaultProps: Partial<TooltipProps> = {
arrowSize: 4,
arrowOffset: 5,
arrowRadius: 0,
arrowPosition: 'side',
offset: 5,
transition: 'fade',
transitionDuration: 100,
Expand Down Expand Up @@ -97,6 +101,7 @@ const _Tooltip = forwardRef<HTMLElement, TooltipProps>((props, ref) => {
arrowSize,
arrowOffset,
arrowRadius,
arrowPosition,
offset,
transition,
transitionDuration,
Expand Down Expand Up @@ -171,7 +176,9 @@ const _Tooltip = forwardRef<HTMLElement, TooltipProps>((props, ref) => {
withBorder={false}
position={tooltip.placement}
arrowSize={arrowSize}
arrowOffset={arrowOffset}
arrowRadius={arrowRadius}
arrowPosition={arrowPosition}
className={classes.arrow}
/>
</Box>
Expand Down
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import { Menu, MenuProps, Group } from '@mantine/core';
import { MantineDemo } from '@mantine/ds';
import { DemoMenuItems } from './_menu-items';
import { FLOATING_ARROW_POSITION_DATA } from '../../../shared/floating-position-data';

function Wrapper(props: MenuProps) {
return (
Expand Down Expand Up @@ -64,5 +65,12 @@ export const positionConfigurator: MantineDemo = {
initialValue: false,
defaultValue: false,
},
{
name: 'arrowPosition',
type: 'select',
data: FLOATING_ARROW_POSITION_DATA,
initialValue: 'side',
defaultValue: 'side',
},
],
};
@@ -1,7 +1,10 @@
import React from 'react';
import { Tooltip, TooltipProps, Group, Button } from '@mantine/core';
import { MantineDemo } from '@mantine/ds';
import { FLOATING_POSITION_DATA } from '../../../shared/floating-position-data';
import {
FLOATING_ARROW_POSITION_DATA,
FLOATING_POSITION_DATA,
} from '../../../shared/floating-position-data';

const Wrapper = (props: TooltipProps) => (
<div style={{ padding: 30 }}>
Expand Down Expand Up @@ -51,5 +54,12 @@ export const configurator: MantineDemo = {
defaultValue: 'top',
},
{ name: 'withArrow', type: 'boolean', initialValue: true, defaultValue: false },
{
name: 'arrowPosition',
type: 'select',
data: FLOATING_ARROW_POSITION_DATA,
initialValue: 'side',
defaultValue: 'side',
},
],
};
5 changes: 5 additions & 0 deletions src/mantine-demos/src/shared/floating-position-data.ts
Expand Up @@ -12,3 +12,8 @@ export const FLOATING_POSITION_DATA = [
{ label: 'bottom-start', value: 'bottom-start' },
{ label: 'bottom-end', value: 'bottom-end' },
];

export const FLOATING_ARROW_POSITION_DATA = [
{ label: 'side', value: 'side' },
{ label: 'center', value: 'center' },
];

0 comments on commit 43e72a6

Please sign in to comment.