Skip to content

Commit

Permalink
feat(FloatingArrow): reexport (#7977)
Browse files Browse the repository at this point in the history
h2. Описание

Для возможности пользователям создавать свои всплывающие окна.

h2. Release notes
h2. Новые компоненты
- Добавлен экспорт `FloatingArrow`, использующийся компонентами `Popover`, `Tooltip`, `Popper`
  • Loading branch information
inomdzhon authored Nov 25, 2024
1 parent 18d9508 commit 38328c7
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 17 deletions.
19 changes: 11 additions & 8 deletions packages/vkui/src/components/FloatingArrow/DefaultIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import * as React from 'react';

export const DEFAULT_ARROW_WIDTH = 20;
export const DEFAULT_ARROW_HEIGHT = 8;
export const DEFAULT_ARROW_PADDING = 10;

const PLATFORM_HEIGHT = 1;
const ARROW_HEIGHT_WITH_WHITE_SPACE = DEFAULT_ARROW_HEIGHT + PLATFORM_HEIGHT;

/**
* Стрелка для всплывающих окон.
*
* Примечание 1.
*
* В компоненте, SVG элемент `<path />` представляет собой стрелку с платформой в виде прямоугольника в 1px. Платформа
Expand All @@ -12,15 +21,9 @@ import * as React from 'react';
* 2. Сместить положение SVG контейнера на высоту платформы – сделано в CSS через `translateY(1px)`.
*
* https://github.com/VKCOM/VKUI/issues/2123
*
* @since 7.0.0
*/

export const DEFAULT_ARROW_WIDTH = 20;
export const DEFAULT_ARROW_HEIGHT = 8;
export const DEFAULT_ARROW_PADDING = 10;

const PLATFORM_HEIGHT = 1;
const ARROW_HEIGHT_WITH_WHITE_SPACE = DEFAULT_ARROW_HEIGHT + PLATFORM_HEIGHT;

export const DefaultIcon = (props: React.SVGAttributes<SVGSVGElement>): React.ReactNode => {
return (
<svg
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
content: '';
display: block;

/* см. Примечание 1 в FloatingArrow.tsx. */
/* см. "Примечание" в DefaultIcon.tsx. */
transform: translateY(1px);
}

Expand Down
88 changes: 88 additions & 0 deletions packages/vkui/src/components/FloatingArrow/FloatingArrow.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { render } from '@testing-library/react';
import type { Placement } from '../../lib/floating';
import { baselineComponent } from '../../testing/utils';
import { FloatingArrow, placementClassNames } from './FloatingArrow';

const PLACEMENT = ['top', 'right', 'bottom', 'left'] as const;
const PLACEMENT_WITH_SIDE = PLACEMENT.reduce<Placement[]>((acc, placement) => {
acc.push(placement, `${placement}-start`, `${placement}-end`);
return acc;
}, []);

describe(FloatingArrow, () => {
baselineComponent(FloatingArrow);

test('props to icon', () => {
const result = render(
<FloatingArrow data-testid="host" iconClassName="icon" iconStyle={{ display: 'none' }} />,
);

const icon = result.getByTestId('host').querySelector('.icon');
expect(icon).toBeInTheDocument();
expect(icon).toHaveStyle({ display: 'none' });
});

test.each(PLACEMENT_WITH_SIDE)('arrow position for %s placement', (placement) => {
const result = render(<FloatingArrow data-testid="host" placement={placement} />);

if (placement.startsWith('top')) {
expect(result.getByTestId('host')).toHaveClass(placementClassNames.bottom);
} else if (placement.startsWith('right')) {
expect(result.getByTestId('host')).toHaveClass(placementClassNames.left);
} else if (placement.startsWith('bottom')) {
expect(result.getByTestId('host')).not.toHaveClass(...Object.values(placementClassNames));
} else if (placement.startsWith('left')) {
expect(result.getByTestId('host')).toHaveClass(placementClassNames.right);
}
});

test.each(PLACEMENT_WITH_SIDE)('arrow offset by %s placement', (placement) => {
const result = render(
<FloatingArrow data-testid="host" coords={{ x: 6, y: 8 }} offset={8} placement={placement} />,
);

if (placement.startsWith('top')) {
expect(result.getByTestId('host')).toHaveStyle({ top: '100%', left: '14px' });
} else if (placement.startsWith('right')) {
expect(result.getByTestId('host')).toHaveStyle({ left: '0px', top: '16px' });
} else if (placement.startsWith('bottom')) {
expect(result.getByTestId('host')).toHaveStyle({ bottom: '100%', left: '14px' });
} else if (placement.startsWith('left')) {
expect(result.getByTestId('host')).toHaveStyle({ right: '0px', top: '16px' });
}
});

test.each(PLACEMENT_WITH_SIDE)('arrow static offset by %s placement', (placement) => {
const result = render(
<FloatingArrow
data-testid="host"
coords={{ x: 6, y: 8 }}
isStaticOffset
offset={8}
placement={placement}
/>,
);

if (placement.endsWith('end')) {
if (placement.startsWith('top')) {
expect(result.getByTestId('host')).toHaveStyle({ top: '100%', right: '8px' });
} else if (placement.startsWith('right')) {
expect(result.getByTestId('host')).toHaveStyle({ left: '0px', bottom: '8px' });
} else if (placement.startsWith('bottom')) {
expect(result.getByTestId('host')).toHaveStyle({ bottom: '100%', right: '8px' });
} else if (placement.startsWith('left')) {
expect(result.getByTestId('host')).toHaveStyle({ right: '0px', bottom: '8px' });
}
} else {
if (placement.startsWith('top')) {
expect(result.getByTestId('host')).toHaveStyle({ top: '100%', left: '8px' });
} else if (placement.startsWith('right')) {
expect(result.getByTestId('host')).toHaveStyle({ left: '0px', top: '8px' });
} else if (placement.startsWith('bottom')) {
expect(result.getByTestId('host')).toHaveStyle({ bottom: '100%', left: '8px' });
} else if (placement.startsWith('left')) {
expect(result.getByTestId('host')).toHaveStyle({ right: '0px', top: '8px' });
}
}
});
});
17 changes: 9 additions & 8 deletions packages/vkui/src/components/FloatingArrow/FloatingArrow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';
import { classNames } from '@vkontakte/vkjs';
import type { Placement } from '../../lib/floating';
import type { HasDataAttribute, HTMLAttributesWithRootRef } from '../../types';
import { RootComponent } from '../RootComponent/RootComponent';
import { DefaultIcon } from './DefaultIcon';
import styles from './FloatingArrow.module.css';

Expand All @@ -10,7 +11,7 @@ export type Coords = {
y?: number;
};

const placementClassNames = {
export const placementClassNames = {
right: styles.placementRight,
bottom: styles.placementBottom,
left: styles.placementLeft,
Expand All @@ -35,7 +36,9 @@ export interface FloatingArrowProps
}

/**
* @private
* Иконка-стрелка для всплывающих окон.
*
* @since 7.0.0
*/
export const FloatingArrow = ({
offset,
Expand All @@ -44,7 +47,6 @@ export const FloatingArrow = ({
iconStyle,
iconClassName,
placement = 'bottom',
getRootRef,
Icon = DefaultIcon,
...restProps
}: FloatingArrowProps): React.ReactNode => {
Expand All @@ -56,14 +58,13 @@ export const FloatingArrow = ({
);

return (
<div
ref={getRootRef}
style={arrowStyles}
className={classNames(styles.host, arrowPlacement && placementClassNames[arrowPlacement])}
<RootComponent
baseStyle={arrowStyles}
baseClassName={classNames(styles.host, arrowPlacement && placementClassNames[arrowPlacement])}
{...restProps}
>
<Icon className={classNames(styles.in, iconClassName)} style={iconStyle} />
</div>
</RootComponent>
);
};

Expand Down
8 changes: 8 additions & 0 deletions packages/vkui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,14 @@ export type { SkeletonProps } from './components/Skeleton/Skeleton';
*/
export { Div } from './components/Div/Div';
export type { DivProps } from './components/Div/Div';
export {
DEFAULT_ARROW_HEIGHT as DEFAULT_ICON_ARROW_HEIGHT,
DEFAULT_ARROW_WIDTH as DEFAULT_ICON_ARROW_WIDTH,
DEFAULT_ARROW_PADDING as DEFAULT_ICON_ARROW_PADDING,
DefaultIcon,
} from './components/FloatingArrow/DefaultIcon';
export { FloatingArrow } from './components/FloatingArrow/FloatingArrow';
export type { FloatingArrowProps } from './components/FloatingArrow/FloatingArrow';
export { Touch } from './components/Touch/Touch';
export type { TouchProps, CustomTouchEvent } from './components/Touch/Touch';
export { PanelSpinner } from './components/PanelSpinner/PanelSpinner';
Expand Down

0 comments on commit 38328c7

Please sign in to comment.