From 43e72a69abcafc232aca2188ee814196041f264b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20van=20der=20Sande?= Date: Sun, 4 Dec 2022 12:43:16 +0100 Subject: [PATCH] [@mantine/core] Add `arrowPosition` prop to Tooltip and Popover components to configure how the arrow is position relative to the target element (#3100) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jérémie van der Sande --- .../Floating/FloatingArrow/FloatingArrow.tsx | 25 ++++++- .../get-arrow-position-styles.ts | 71 ++++++++++++++++--- src/mantine-core/src/Floating/index.ts | 2 +- src/mantine-core/src/Floating/types.ts | 1 + .../src/Popover/Popover.context.ts | 4 +- src/mantine-core/src/Popover/Popover.tsx | 9 ++- .../PopoverDropdown/PopoverDropdown.tsx | 2 + src/mantine-core/src/Tooltip/Tooltip.tsx | 9 ++- .../Menu/Menu.demo.positionConfigurator.tsx | 8 +++ .../Tooltip/Tooltip.demo.configurator.tsx | 12 +++- .../src/shared/floating-position-data.ts | 5 ++ 11 files changed, 130 insertions(+), 18 deletions(-) diff --git a/src/mantine-core/src/Floating/FloatingArrow/FloatingArrow.tsx b/src/mantine-core/src/Floating/FloatingArrow/FloatingArrow.tsx index 2dbaf9f9681..9937c5f4096 100644 --- a/src/mantine-core/src/Floating/FloatingArrow/FloatingArrow.tsx +++ b/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( - ({ 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; } @@ -26,7 +44,10 @@ export const FloatingArrow = forwardRef( withBorder, position, arrowSize, + arrowOffset, arrowRadius, + arrowPosition, + dir: theme.dir, arrowX, arrowY, })} diff --git a/src/mantine-core/src/Floating/FloatingArrow/get-arrow-position-styles.ts b/src/mantine-core/src/Floating/FloatingArrow/get-arrow-position-styles.ts index 946440ef508..e6a8886c598 100644 --- a/src/mantine-core/src/Floating/FloatingArrow/get-arrow-position-styles.ts +++ b/src/mantine-core/src/Floating/FloatingArrow/get-arrow-position-styles.ts @@ -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, @@ -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, @@ -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, }; @@ -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, }; @@ -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, }; @@ -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, }; diff --git a/src/mantine-core/src/Floating/index.ts b/src/mantine-core/src/Floating/index.ts index 20358087dfd..9579c90c4b5 100644 --- a/src/mantine-core/src/Floating/index.ts +++ b/src/mantine-core/src/Floating/index.ts @@ -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'; diff --git a/src/mantine-core/src/Floating/types.ts b/src/mantine-core/src/Floating/types.ts index 78830891d87..5cf2ed0a32c 100644 --- a/src/mantine-core/src/Floating/types.ts +++ b/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'; diff --git a/src/mantine-core/src/Popover/Popover.context.ts b/src/mantine-core/src/Popover/Popover.context.ts index bd60e5a1ec9..3c66dec3233 100644 --- a/src/mantine-core/src/Popover/Popover.context.ts +++ b/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'; @@ -21,7 +21,9 @@ interface PopoverContext { width?: PopoverWidth; withArrow: boolean; arrowSize: number; + arrowOffset: number; arrowRadius: number; + arrowPosition: ArrowPosition; trapFocus: boolean; placement: FloatingPosition; withinPortal: boolean; diff --git a/src/mantine-core/src/Popover/Popover.tsx b/src/mantine-core/src/Popover/Popover.tsx index 3098bf1c6da..31201c3b4af 100644 --- a/src/mantine-core/src/Popover/Popover.tsx +++ b/src/mantine-core/src/Popover/Popover.tsx @@ -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 { @@ -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; @@ -137,6 +140,7 @@ const defaultProps: Partial = { arrowSize: 7, arrowOffset: 5, arrowRadius: 0, + arrowPosition: 'side', closeOnClickOutside: true, withinPortal: false, closeOnEscape: true, @@ -166,6 +170,7 @@ export function Popover(props: PopoverProps) { arrowSize, arrowOffset, arrowRadius, + arrowPosition, unstyled, classNames, styles, @@ -258,7 +263,9 @@ export function Popover(props: PopoverProps) { width, withArrow, arrowSize, + arrowOffset, arrowRadius, + arrowPosition, placement: popover.floating.placement, trapFocus, withinPortal, diff --git a/src/mantine-core/src/Popover/PopoverDropdown/PopoverDropdown.tsx b/src/mantine-core/src/Popover/PopoverDropdown/PopoverDropdown.tsx index 713dcec0af6..7a76e63d83d 100644 --- a/src/mantine-core/src/Popover/PopoverDropdown/PopoverDropdown.tsx +++ b/src/mantine-core/src/Popover/PopoverDropdown/PopoverDropdown.tsx @@ -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} /> diff --git a/src/mantine-core/src/Tooltip/Tooltip.tsx b/src/mantine-core/src/Tooltip/Tooltip.tsx index f9ffb65eeef..a85253c1096 100644 --- a/src/mantine-core/src/Tooltip/Tooltip.tsx +++ b/src/mantine-core/src/Tooltip/Tooltip.tsx @@ -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'; @@ -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; @@ -65,6 +68,7 @@ const defaultProps: Partial = { arrowSize: 4, arrowOffset: 5, arrowRadius: 0, + arrowPosition: 'side', offset: 5, transition: 'fade', transitionDuration: 100, @@ -97,6 +101,7 @@ const _Tooltip = forwardRef((props, ref) => { arrowSize, arrowOffset, arrowRadius, + arrowPosition, offset, transition, transitionDuration, @@ -171,7 +176,9 @@ const _Tooltip = forwardRef((props, ref) => { withBorder={false} position={tooltip.placement} arrowSize={arrowSize} + arrowOffset={arrowOffset} arrowRadius={arrowRadius} + arrowPosition={arrowPosition} className={classes.arrow} /> diff --git a/src/mantine-demos/src/demos/core/Menu/Menu.demo.positionConfigurator.tsx b/src/mantine-demos/src/demos/core/Menu/Menu.demo.positionConfigurator.tsx index e546ec18b15..ceca4317265 100644 --- a/src/mantine-demos/src/demos/core/Menu/Menu.demo.positionConfigurator.tsx +++ b/src/mantine-demos/src/demos/core/Menu/Menu.demo.positionConfigurator.tsx @@ -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 ( @@ -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', + }, ], }; diff --git a/src/mantine-demos/src/demos/core/Tooltip/Tooltip.demo.configurator.tsx b/src/mantine-demos/src/demos/core/Tooltip/Tooltip.demo.configurator.tsx index 48494dbe21c..ebd9307346d 100644 --- a/src/mantine-demos/src/demos/core/Tooltip/Tooltip.demo.configurator.tsx +++ b/src/mantine-demos/src/demos/core/Tooltip/Tooltip.demo.configurator.tsx @@ -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) => (
@@ -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', + }, ], }; diff --git a/src/mantine-demos/src/shared/floating-position-data.ts b/src/mantine-demos/src/shared/floating-position-data.ts index dddc7ae5bb7..7670627ad73 100644 --- a/src/mantine-demos/src/shared/floating-position-data.ts +++ b/src/mantine-demos/src/shared/floating-position-data.ts @@ -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' }, +];