Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -23,7 +23,7 @@ export interface MiniInfoCellProps
TappableOmitProps,
'hoverMode' | 'activeMode' | 'hovered' | 'activated' | 'hasHover' | 'hasActive'
>,
RootComponentProps<HTMLDivElement> {
RootComponentProps<HTMLElement> {
/**
* Иконка слева.<br />
* Рекомендуется использовать иконки размера 20.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,68 @@
import { createRef } from 'react';
import { render, screen } from '@testing-library/react';
import { baselineComponent } from '../../testing/utils';
import { RootComponent } from './RootComponent';

describe('RootComponent', () => {
baselineComponent(RootComponent);

describe('render prop', () => {
it('uses render instead of Component', () => {
render(
<RootComponent
Component="div"
render={({ getRootRef, ...props }) => <span data-testid="rendered" {...props} />}
/>,
);

const node = screen.getByTestId('rendered');
expect(node.tagName).toBe('SPAN');
});

it('passes resolved className and style to render', () => {
render(
<RootComponent
className="customClassName"
baseClassName="baseClassName"
style={{ color: 'red' }}
baseStyle={{ backgroundColor: 'blue' }}
render={({ getRootRef, ...props }) => <div data-testid="rendered" {...props} />}
/>,
);

const node = screen.getByTestId('rendered');
expect(node.classList.contains('customClassName')).toBe(true);
expect(node.classList.contains('baseClassName')).toBe(true);
expect(node.style.color).toBe('red');
expect(node.style.backgroundColor).toBe('blue');
});

it('passes rest props to render', () => {
render(
<RootComponent
id="custom-id"
aria-label="label"
render={({ getRootRef, ...props }) => <div data-testid="rendered" {...props} />}
/>,
);

const node = screen.getByTestId('rendered');
expect(node.id).toBe('custom-id');
expect(node.getAttribute('aria-label')).toBe('label');
});

it('forwards getRootRef through render', () => {
const ref = createRef<HTMLDivElement>();
render(
<RootComponent
getRootRef={ref}
render={({ getRootRef, ...props }) => (
<div data-testid="rendered" ref={getRootRef} {...props} />
)}
/>,
);

expect(ref.current).toBe(screen.getByTestId('rendered'));
});
});
Comment thread
inomdzhon marked this conversation as resolved.
});
47 changes: 33 additions & 14 deletions packages/vkui/src/components/RootComponent/RootComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type * as React from 'react';
import { classNames } from '@vkontakte/vkjs';
import { mergeStyle } from '../../helpers/mergeStyle';
import type { HasComponent, HasRootRef } from '../../types';
import type { HasComponent, HasRender, HasRootRef } from '../../types';
import styles from './RootComponent.module.css';

export interface RootComponentProps<T>
extends React.AllHTMLAttributes<T>,
HasRootRef<T>,
HasComponent {}
export type RootComponentProps<T> = React.AllHTMLAttributes<T> &
HasRootRef<T> &
HasComponent &
Comment thread
inomdzhon marked this conversation as resolved.
HasRender<T>;

export interface RootComponentExtendProps {
/**
Expand All @@ -24,6 +24,19 @@ export interface RootComponentInternalProps<T>
extends RootComponentProps<T>,
RootComponentExtendProps {}

interface RenderRootComponentProps<T> extends Omit<RootComponentInternalProps<T>, 'render'> {
/**
* Функция кастомного рендера. См. `RootComponentProps['render']`.
*/
render: NonNullable<RootComponentProps<T>['render']>;
}

const RenderRootComponent = <T,>({
render,
getRootRef,
...restProps
}: RenderRootComponentProps<T>): React.ReactNode => render({ ...restProps, getRootRef });

/**
* Базовый корневой компонент.
*/
Expand All @@ -34,17 +47,23 @@ export const RootComponent = <T,>({
baseStyle,
style,
getRootRef,
render,
...restProps
}: RootComponentInternalProps<T>): React.ReactNode => (
<Component
ref={getRootRef}
className={classNames(
}: RootComponentInternalProps<T>): React.ReactNode => {
const resolvedProps = {
className: classNames(
className,
baseClassName,
styles.host,
restProps.hidden === true && styles.hidden,
)}
style={mergeStyle(baseStyle, style)}
{...restProps}
/>
);
),
style: mergeStyle(baseStyle, style),
...restProps,
};

if (render) {
return <RenderRootComponent render={render} getRootRef={getRootRef} {...resolvedProps} />;

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.

А точно нужно дополнительный узел в дереве создавать? Нельзя просто?

if (render) return render({ ...resolvedProps, getRootRef });

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.

Нельзя, так как ругается компилятор

playground

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.

Нашёл несколько моментов, которые стоит поправить перед мержем:

  1. ContentCard сейчас наследует render через TappableOmitProps, но Component у него применяется к внешнему Card, а render проваливается во внутренний Tappable. Получается, что render не является аналогом Component для этого компонента. Нужно либо передавать render во внешний Card, либо явно исключить его из props.

  2. Часть компонентов, которые являются обёртками над RootComponent, типизируются через HTMLAttributesWithRootRef & HasComponent, поэтому render в их публичном API не появляется, хотя технически они его уже могут прокинуть в RootComponent. Нужно либо HasComponent расширить типом render, либо по аналогии создать тип HasRender и подключить ручками

    • Card
    • CardGrid
    • CardScroll
    • Chip
    • FormItemTop
    • GroupContainer
    • GroupExpandedContent

}

return <Component ref={getRootRef} {...resolvedProps} />;
};
11 changes: 11 additions & 0 deletions packages/vkui/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ export interface HasComponent {
Component?: React.ElementType | undefined;
}

export type HasRender<T> = {
/**
* Позволяет переопределить рендер компонента, получая собранные свойства
* (включая вычисленные `className` и `style`). Используется вместо `Component`.
*
* Позволяет гибко объединять несколько компонентов без создания
* промежуточных DOM-узлов и без ремаунта поддерева на каждый рендер.
*/
render?: ((props: React.AllHTMLAttributes<T> & HasRootRef<T>) => React.ReactNode) | undefined;
};

export interface HasAlign {
align?: AlignType | undefined;
}
Expand Down
Loading