Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[@mantine/core] add a prop to choose how to position floating arrow #3100

Merged
merged 1 commit into from Dec 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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' },
];