Skip to content
Open
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
43 changes: 41 additions & 2 deletions packages/vkui/src/components/Box/Box.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
'use client';

import * as React from 'react';
import { classNames } from '@vkontakte/vkjs';
import { resolveLayoutProps } from '../../lib/layouts';
import type { LayoutProps } from '../../lib/layouts/types';
Expand All @@ -12,7 +15,34 @@ const displayClassNames = {
'contents': styles.displayContents,
};

export interface BoxProps extends RootComponentProps<HTMLElement>, LayoutProps {
type BoxComponent = RootComponentProps<HTMLElement>['Component'] | React.ElementType[];

function composeComponents(
component: BoxComponent | undefined,
): RootComponentProps<HTMLElement>['Component'] {
if (!Array.isArray(component)) {
return component;
}

if (component.length === 0) {
return undefined;
}

return component.reduceRight<React.ElementType>(
(InnerComponent, WrapperComponent) =>
// eslint-disable-next-line react/display-name -- динамическая композиция обёрток
React.forwardRef<HTMLElement, RootComponentProps<HTMLElement>>((props, ref) => (
<WrapperComponent {...props} ref={ref} Component={InnerComponent} />
)),
'div',
);
}

export interface BoxProps extends Omit<RootComponentProps<HTMLElement>, 'Component'>, LayoutProps {
/**
*
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Пустой комментарий

*/
Component?: BoxComponent | undefined;
/**
* Возможность задать css-свойство `display`.
*/
Expand All @@ -24,12 +54,21 @@ export interface BoxProps extends RootComponentProps<HTMLElement>, LayoutProps {
*
* @since 7.9.0
*/
export const Box = ({ className, style, display, ...restProps }: BoxProps) => {
export const Box = ({
className,
style,
display,
Component: ComponentProp,
...restProps
}: BoxProps) => {
const resolvedProps = resolveLayoutProps(restProps);

const Component = React.useMemo(() => composeComponents(ComponentProp), [ComponentProp]);

return (
<RootComponent
{...resolvedProps}
Component={Component}
baseClassName={resolvedProps.className}
baseStyle={resolvedProps.style}
className={classNames(className, display && displayClassNames[display])}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use client';

import * as React from 'react';
import { usePlatform } from '../../hooks/usePlatform';
import { useResizeObserver } from '../../hooks/useResizeObserver';
import { defineComponentDisplayNames } from '../../lib/react/defineComponentDisplayNames';
import type { HasComponent } from '../../types';
import { SplitColContext } from '../SplitCol/SplitColContext';

type SplitColWidthWrapperProps = React.HTMLAttributes<HTMLElement> & HasComponent;

export const SplitColWidthWrapper: React.ForwardRefExoticComponent<
React.PropsWithoutRef<SplitColWidthWrapperProps> & React.RefAttributes<HTMLElement>
// eslint-disable-next-line react/display-name -- используется defineComponentDisplayNames
> = React.forwardRef<HTMLElement, SplitColWidthWrapperProps>(
({ Component = 'div', style, ...restProps }, forwardedRef) => {
const platform = usePlatform();
const { colRef } = React.useContext(SplitColContext);
const [width, setWidth] = React.useState<string | undefined>(undefined);

const doResize = React.useCallback(() => {
if (!colRef?.current) {
setWidth(undefined);
return;
}

const computedStyle = getComputedStyle(colRef.current);

setWidth(
`${
colRef.current.clientWidth -
parseFloat(computedStyle.paddingLeft || '0') -
parseFloat(computedStyle.paddingRight || '0')
}px`,
);
}, [colRef]);

React.useEffect(doResize, [doResize, platform]);
useResizeObserver(colRef, doResize);

return <Component {...restProps} ref={forwardedRef} style={{ width, ...style }} />;
},
);

if (process.env.NODE_ENV !== 'production') {
defineComponentDisplayNames(SplitColWidthWrapper, 'SplitColWidthWrapper');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use client';

import * as React from 'react';
import { defineComponentDisplayNames } from '../../lib/react/defineComponentDisplayNames';
import type { HasComponent } from '../../types';
import { OnboardingTooltipContainer } from './OnboardingTooltipContainer';

type OnboardingTooltipFixedContainerProps = React.HTMLAttributes<HTMLDivElement> & HasComponent;

export const OnboardingTooltipFixedContainer: React.ForwardRefExoticComponent<
React.PropsWithoutRef<OnboardingTooltipFixedContainerProps> & React.RefAttributes<HTMLDivElement>
// eslint-disable-next-line react/display-name -- используется defineComponentDisplayNames
> = React.forwardRef<HTMLDivElement, OnboardingTooltipFixedContainerProps>((props, ref) => (
<OnboardingTooltipContainer {...props} fixed ref={ref} />
));

if (process.env.NODE_ENV !== 'production') {
defineComponentDisplayNames(OnboardingTooltipFixedContainer, 'OnboardingTooltipFixedContainer');
}
12 changes: 8 additions & 4 deletions packages/vkui/src/components/PanelHeader/PanelHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ import type {
HasRef,
HTMLAttributesWithRootRef,
} from '../../types';
import { Box } from '../Box/Box';
import { useConfigProvider } from '../ConfigProvider/ConfigProviderContext';
import { FixedLayout } from '../FixedLayout/FixedLayout';
import { SplitColWidthWrapper } from '../FixedLayout/SplitColWidthWrapper';
import { OnboardingTooltipContainer } from '../OnboardingTooltip/OnboardingTooltipContainer';
import { RootComponent } from '../RootComponent/RootComponent';
import { Separator } from '../Separator/Separator';
Expand Down Expand Up @@ -201,15 +202,18 @@ export const PanelHeader = ({
getRootRef={isFixed ? getRootRef : getRef}
>
{isFixed ? (
<FixedLayout
<Box
Component={SplitColWidthWrapper}
className={classNames(styles.fixed, 'vkuiInternalPanelHeader__fixed')}
vertical="top"
position="fixed"
insetBlockStart={0}
inlineSize="100%"
getRootRef={getRef}
>
<PanelHeaderIn before={before} after={after} typographyProps={typographyProps}>
{children}
</PanelHeaderIn>
</FixedLayout>
</Box>
) : (
<PanelHeaderIn before={before} after={after} typographyProps={typographyProps}>
{children}
Expand Down
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Контекст теперь не влезает по ширине

2026-04-29.11.28.19.mov

Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
--vkui_internal--PanelHeaderContext__fade_display: none;

z-index: var(--vkui_internal--z_index_panel_header_context);
inline-size: 100%;
block-size: auto;
}

.viewWidthSmallTabletMinus {
Expand All @@ -21,9 +19,6 @@
}

.in {
position: absolute;
inset-inline-start: 0;
z-index: 1;
box-sizing: border-box;
inline-size: 100%;
padding: 8px;
Expand Down Expand Up @@ -171,3 +166,13 @@
opacity: 0;
}
}

/* stylelint-disable selector-max-universal, selector-pseudo-class-disallowed-list */
:global(.vkuiInternalPanelHeader) ~ .host,
:global(.vkuiInternalPanelHeader) ~ * .host {
inset-block-start: calc(
var(--vkui_internal--panel_header_height) +
var(--vkui_internal--safe_area_inset_top)
);
}
/* stylelint-enable selector-max-universal, selector-pseudo-class-disallowed-list */
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { render, screen } from '@testing-library/react';
import { fireEvent, render, screen } from '@testing-library/react';
import { noop } from '@vkontakte/vkjs';
import { ViewWidth } from '../../lib/adaptivity';
import {
baselineComponent,
userEvent,
waitCSSKeyframesAnimation,
withFakeTimers,
} from '../../testing/utils';
import { baselineComponent, waitForFloatingPosition, withFakeTimers } from '../../testing/utils';
import { AdaptivityProvider } from '../AdaptivityProvider/AdaptivityProvider';
import { PanelHeaderContext } from './PanelHeaderContext';
import panelHeaderContextStyles from './PanelHeaderContext.module.css';
Expand All @@ -32,7 +27,7 @@ describe('PanelHeaderContext', () => {
<PanelHeaderContext opened onClose={onClose} />
</AdaptivityProvider>,
);
await userEvent.click(document.body);
fireEvent.click(document.body);
expect(onClose).toHaveBeenCalledTimes(1);
}),
);
Expand All @@ -42,9 +37,7 @@ describe('PanelHeaderContext', () => {
withFakeTimers(async () => {
const onClose = vi.fn();
render(<PanelHeaderContext opened onClose={onClose} />);
await userEvent.click(
document.querySelector(`.${panelHeaderContextStyles.fade}`) as Element,
);
fireEvent.click(document.querySelector(`.${panelHeaderContextStyles.fade}`) as Element);
expect(onClose).toHaveBeenCalledTimes(1);
}),
);
Expand All @@ -58,7 +51,7 @@ describe('PanelHeaderContext', () => {
<div data-testid="xxx" />
</PanelHeaderContext>,
);
await userEvent.click(screen.getByTestId('xxx'));
fireEvent.click(screen.getByTestId('xxx'));
expect(onClose).not.toHaveBeenCalled();
}),
);
Expand All @@ -69,12 +62,14 @@ describe('PanelHeaderContext', () => {
<div data-testid="xxx" />
</PanelHeaderContext>,
);
expect(screen.queryByTestId('xxx')).not.toBeNull();
result.rerender(
<PanelHeaderContext opened={false} onClose={noop}>
<div data-testid="xxx" />
</PanelHeaderContext>,
);
await waitCSSKeyframesAnimation(result.getByTestId('content'));

await waitForFloatingPosition();
expect(screen.queryByTestId('xxx')).toBeNull();
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import { useAdaptivity } from '../../hooks/useAdaptivity';
import { useGlobalOnClickOutside } from '../../hooks/useGlobalOnClickOutside';
import { usePlatform } from '../../hooks/usePlatform';
import { type SizeTypeValues, ViewWidth, type ViewWidthType } from '../../lib/adaptivity';
import { useCSSKeyframesAnimationController } from '../../lib/animation';
import type { HTMLAttributesWithRootRef } from '../../types';
import { useScrollLock } from '../AppRoot/ScrollContext';
import { FixedLayout } from '../FixedLayout/FixedLayout';
import { Box } from '../Box/Box';
import { SplitColWidthWrapper } from '../FixedLayout/SplitColWidthWrapper';
import { OnboardingTooltipFixedContainer } from '../OnboardingTooltip/OnboardingTooltipFixedContainer';
import { Popover } from '../Popover/Popover';
import styles from './PanelHeaderContext.module.css';

function getViewWidthClassName(
Expand Down Expand Up @@ -43,6 +45,8 @@ export interface PanelHeaderContextProps extends HTMLAttributesWithRootRef<HTMLD
onClose: VoidFunction;
}

const ComponentDecorators = [SplitColWidthWrapper, OnboardingTooltipFixedContainer];

/**
* @see https://vkui.io/components/panel-header-context
*/
Expand All @@ -55,15 +59,17 @@ export const PanelHeaderContext = ({
}: PanelHeaderContextProps): React.ReactNode => {
const platform = usePlatform();
const { sizeX: legacySizeX, viewWidth = 'none' } = useAdaptivity();
const elementRef = React.useRef<HTMLDivElement>(null);
const [animationState, animationHandlers] = useCSSKeyframesAnimationController(
opened ? 'enter' : 'exit',
undefined,
true,
);
const visible = animationState !== 'exited';
const [visible, setVisible] = React.useState<boolean>(opened);
const [prevOpened, setPrevOpened] = React.useState<boolean>(opened);
const anchorRef = React.useRef<HTMLElement | null>(null);
const popoverRef = React.useRef<HTMLDivElement | null>(null);

useScrollLock(platform !== 'vkcom' && visible);
if (prevOpened !== opened) {
if (opened) {
setVisible(true);
}
setPrevOpened(opened);
}

const handleGlobalOnClickOutside = React.useCallback(
(event: MouseEvent) => {
Expand All @@ -75,23 +81,32 @@ export const PanelHeaderContext = ({
[opened, onClose],
);

useGlobalOnClickOutside(handleGlobalOnClickOutside, visible ? elementRef : null);
useScrollLock(platform !== 'vkcom' && visible);

useGlobalOnClickOutside(
handleGlobalOnClickOutside,
visible ? anchorRef : null,
visible ? popoverRef : null,
);

if (!visible) {
return null;
}

return (
<FixedLayout
{...restProps}
<Box
Component={ComponentDecorators}
className={classNames(
styles.host,
platform === 'ios' && styles.ios,
opened ? styles.opened : styles.closing,
getViewWidthClassName(viewWidth, legacySizeX),
className,
)}
vertical="top"
position="fixed"
inlineSize="100%"
insetBlockStart={0}
{...restProps}
>
<div
onClick={(event) => {
Expand All @@ -100,14 +115,31 @@ export const PanelHeaderContext = ({
}}
className={styles.fade}
/>
<div
data-testid={process.env.NODE_ENV === 'test' ? 'content' : undefined}
<Popover
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Насколько мы можем отказаться от Popover-а?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

вообще можно оставить старую реализации всплывашки, но в issue привязанном написано переписать на Popover

shown={opened}
disableFlipMiddleware
disableShiftMiddleware
disableCloseOnClickOutside
disableCloseOnEscKey
strategy="absolute"
trigger="manual"
role="presentation"
content={<div className={styles.content}>{children}</div>}
sameWidth
usePortal={false}
zIndex={1}
noStyling
className={styles.in}
ref={elementRef}
{...animationHandlers}
offsetByMainAxis={0}
onShownChanged={(shown) => {
if (!shown) {
setVisible(false);
}
}}
getRootRef={popoverRef}
>
<div className={styles.content}>{children}</div>
</div>
</FixedLayout>
<Box getRootRef={anchorRef} inlineSize="100%" />
</Popover>
</Box>
);
};
Loading