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

refactor(floating): mv all floating components to same API #6212

Merged
merged 3 commits into from
Dec 6, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export const ActionSheetDropdownMenu = ({
)}
style={style}
getRootRef={elementRef}
forcePortal={false}
usePortal={false}
BlackySoul marked this conversation as resolved.
Show resolved Hide resolved
>
<FocusTrap onClose={onClose} {...restProps} onClick={onClick}>
{children}
Expand Down
2 changes: 1 addition & 1 deletion packages/vkui/src/components/ActionSheet/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import type { PlacementWithAuto } from '../../lib/floating/types';
import type { PlacementWithAuto } from '../../lib/floating/types/common';
import { FocusTrapProps } from '../FocusTrap/FocusTrap';

export type ToggleRef = Element | null | undefined | React.RefObject<Element>;
Expand Down
77 changes: 34 additions & 43 deletions packages/vkui/src/components/AppRoot/AppRootPortal.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,21 @@
import * as React from 'react';
import { createPortal } from 'react-dom';
import { useAppearance } from '../../hooks/useAppearance';
import { useIsClient } from '../../hooks/useIsClient';
import { createPortal } from '../../lib/createPortal';
import { isRefObject } from '../../lib/isRefObject';
import { HasChildren } from '../../types';
import { AppearanceProvider } from '../AppearanceProvider/AppearanceProvider';
import { AppRootContext } from './AppRootContext';
import { AppRootContext, type AppRootContextInterface } from './AppRootContext';

export interface AppRootPortalProps extends HasChildren {
className?: string;
forcePortal?: boolean;
/**
* Кастомный root-элемент портала.
* При передаче вместе с `forcePorta=true` игнорируется `portalRoot` и `disablePortal`
* из контекста `AppRoot`.
* - При передаче `true` будет использовать `portalRoot` из контекста `AppRoot`.
* - При передаче элемента будут игнорироваться `portalRoot` и `disablePortal` из контекста `AppRoot`.
*/
portalRoot?: HTMLElement | React.RefObject<HTMLElement> | null;
usePortal?: boolean | HTMLElement | React.RefObject<HTMLElement> | null;
}

export const AppRootPortal = ({
children,
className,
forcePortal: forcePortalProp,
portalRoot: portalRootProp = null,
}: AppRootPortalProps) => {
export const AppRootPortal = ({ children, usePortal }: AppRootPortalProps) => {
const { portalRoot, mode, disablePortal } = React.useContext(AppRootContext);
const appearance = useAppearance();

Expand All @@ -32,40 +24,39 @@
return null;
}

const forcePortal = forcePortalProp ?? mode !== 'full';

const portalContainer = getPortalContainer(portalRootProp, portalRoot);

const ignoreDisablePortalFlagFromContext = portalRootProp && forcePortal;
const shouldUsePortal = ignoreDisablePortalFlagFromContext
? true
: !disablePortal && portalContainer && forcePortal;
const portalContainer = resolvePortalContainer(usePortal, portalRoot);
if (!portalContainer || shouldDisablePortal(usePortal, mode, Boolean(disablePortal))) {
return children;
}

return shouldUsePortal && portalContainer ? (
createPortal(
<AppearanceProvider value={appearance}>
<div className={className}>{children}</div>
</AppearanceProvider>,
portalContainer,
)
) : (
<React.Fragment>{children}</React.Fragment>
return createPortal(
<AppearanceProvider value={appearance}>{children}</AppearanceProvider>,
portalContainer,
);
};

/**
* Получает из кастомного пропа `partialRootProp` и `partialRoot` контекста
* контейнер-элемент для портала.
* `partialRootProp` может быть ref элементом.
*
*/
function getPortalContainer(
portalRootProp?: HTMLElement | React.RefObject<HTMLElement> | null,
portalRoot?: HTMLElement | null,
function shouldDisablePortal(
usePortal: AppRootPortalProps['usePortal'],
mode: AppRootContextInterface['mode'],
disablePortal: boolean,
) {
if (usePortal !== undefined) {
if (typeof usePortal !== 'boolean') {
return false;

Check warning on line 45 in packages/vkui/src/components/AppRoot/AppRootPortal.tsx

View check run for this annotation

Codecov / codecov/patch

packages/vkui/src/components/AppRoot/AppRootPortal.tsx#L44-L45

Added lines #L44 - L45 were not covered by tests
}
return disablePortal || usePortal !== true;

Check warning on line 47 in packages/vkui/src/components/AppRoot/AppRootPortal.tsx

View check run for this annotation

Codecov / codecov/patch

packages/vkui/src/components/AppRoot/AppRootPortal.tsx#L47

Added line #L47 was not covered by tests
}
// fallback
return disablePortal || mode !== 'full';
}

function resolvePortalContainer<PortalRootFromContext extends HTMLElement | null | undefined>(
usePortal: AppRootPortalProps['usePortal'],
portalRootFromContext: PortalRootFromContext,
) {
if (!portalRootProp) {
return portalRoot;
if (usePortal === true || !usePortal) {
return portalRootFromContext ? portalRootFromContext : null;
}

return isRefObject(portalRootProp) ? portalRootProp.current : portalRootProp;
return isRefObject(usePortal) ? usePortal.current : usePortal;

Check warning on line 61 in packages/vkui/src/components/AppRoot/AppRootPortal.tsx

View check run for this annotation

Codecov / codecov/patch

packages/vkui/src/components/AppRoot/AppRootPortal.tsx#L61

Added line #L61 was not covered by tests
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export const CustomSelectDropdown = ({
),
className,
)}
forcePortal={forcePortal}
usePortal={forcePortal}
autoUpdateOnTargetResize
{...restProps}
>
Expand Down
6 changes: 3 additions & 3 deletions packages/vkui/src/components/FixedLayout/FixedLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { useGlobalEventListener } from '../../hooks/useGlobalEventListener';
import { usePlatform } from '../../hooks/usePlatform';
import { useDOM } from '../../lib/dom';
import { HTMLAttributesWithRootRef } from '../../types';
import { OnboardingTooltipContainer } from '../OnboardingTooltip/OnboardingTooltipContainer';
import { SplitColContext } from '../SplitCol/SplitColContext';
import { TooltipContainer } from '../Tooltip/TooltipContainer';
import styles from './FixedLayout.module.css';

const stylesVertical = {
Expand Down Expand Up @@ -79,7 +79,7 @@ export const FixedLayout = ({
useGlobalEventListener(window, 'resize', doResize);

return (
<TooltipContainer
<OnboardingTooltipContainer
{...restProps}
fixed
ref={ref}
Expand All @@ -93,6 +93,6 @@ export const FixedLayout = ({
style={{ ...style, width }}
>
{children}
</TooltipContainer>
</OnboardingTooltipContainer>
);
};
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
.PopperArrow {
.FloatingArrow {
position: absolute;
}

.PopperArrow__in {
.FloatingArrow__in {
content: '';
display: block;

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

.PopperArrow--placement-right {
.FloatingArrow--placement-right {
transform: rotate(90deg) translate(50%, -50%);
transform-origin: right;
}

.PopperArrow--placement-bottom {
.FloatingArrow--placement-bottom {
transform: rotate(180deg);
}

.PopperArrow--placement-left {
.FloatingArrow--placement-left {
transform: rotate(-90deg) translate(-50%, -50%);
transform-origin: left;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,72 +3,102 @@
import type { Placement } from '../../lib/floating';
import type { HasDataAttribute, HTMLAttributesWithRootRef } from '../../types';
import { DefaultIcon } from './DefaultIcon';
import styles from './PopperArrow.module.css';
import styles from './FloatingArrow.module.css';

export type Coords = {
x?: number;
y?: number;
};

const placementClassNames = {
right: styles['PopperArrow--placement-right'],
bottom: styles['PopperArrow--placement-bottom'],
left: styles['PopperArrow--placement-left'],
right: styles['FloatingArrow--placement-right'],
bottom: styles['FloatingArrow--placement-bottom'],
left: styles['FloatingArrow--placement-left'],
};

export interface PopperArrowProps
export interface FloatingArrowProps
extends HTMLAttributesWithRootRef<HTMLDivElement>,
HasDataAttribute {
/**
* Сдвиг стрелки относительно текущих координат.
*/
offset?: number;
/**
* Включает абсолютное смещение по `offset`.
*/
isStaticOffset?: boolean;
coords?: Coords;
placement: Placement;
placement?: Placement;
iconStyle?: React.CSSProperties;
iconClassName?: string;
Icon?: React.ComponentType<React.SVGAttributes<SVGSVGElement>>;
}

export const PopperArrow = ({
/**
* @private
*/
export const FloatingArrow = ({
offset,
isStaticOffset,
coords,
iconStyle,
iconClassName,
placement,
placement = 'bottom',

Check warning on line 46 in packages/vkui/src/components/FloatingArrow/FloatingArrow.tsx

View check run for this annotation

Codecov / codecov/patch

packages/vkui/src/components/FloatingArrow/FloatingArrow.tsx#L46

Added line #L46 was not covered by tests
getRootRef,
Icon = DefaultIcon,
...restProps
}: PopperArrowProps) => {
const [arrowPlacement, arrowStyles] = getArrowPositionData(placement, coords);
}: FloatingArrowProps) => {
const [arrowPlacement, arrowStyles] = getArrowPositionData(
placement,
coords,
offset,
isStaticOffset,
);

return (
<div
ref={getRootRef}
style={arrowStyles}
className={classNames(
styles['PopperArrow'],
styles['FloatingArrow'],
arrowPlacement && placementClassNames[arrowPlacement],
)}
{...restProps}
>
<Icon className={classNames(styles['PopperArrow__in'], iconClassName)} style={iconStyle} />
<Icon className={classNames(styles['FloatingArrow__in'], iconClassName)} style={iconStyle} />
</div>
);
};

function getArrowPositionData(
placement: Placement,
coords: Coords = { x: 0, y: 0 },
offset = 0,
isStaticOffset = false,
): [undefined | 'right' | 'bottom' | 'left', React.CSSProperties] {
const withOffset = (isVerticalPlacement: boolean) => {
const parsedCoords = { x: coords.x || 0, y: coords.y || 0 };

if (isVerticalPlacement) {
return isStaticOffset ? offset : parsedCoords.y + offset;

Check warning on line 83 in packages/vkui/src/components/FloatingArrow/FloatingArrow.tsx

View check run for this annotation

Codecov / codecov/patch

packages/vkui/src/components/FloatingArrow/FloatingArrow.tsx#L83

Added line #L83 was not covered by tests
} else {
return isStaticOffset ? offset : parsedCoords.x + offset;
}
};

if (placement.startsWith('top')) {
return [
'bottom',
{
top: '100%',
left: coords.x,
left: withOffset(false),
},
];
} else if (placement.startsWith('right')) {
return [
'left',
{
top: coords.y,
top: withOffset(true),
left: 0,
},
];
Expand All @@ -77,14 +107,14 @@
undefined,
{
bottom: '100%',
left: coords.x,
left: withOffset(false),
},
];
} else {
return [
'right',
{
top: coords.y,
top: withOffset(true),
right: 0,
},
];
Expand Down
Loading
Loading