diff --git a/packages/vkui/src/components/ActionSheet/types.ts b/packages/vkui/src/components/ActionSheet/types.ts index 20f434cb4f6..7c9e618163f 100644 --- a/packages/vkui/src/components/ActionSheet/types.ts +++ b/packages/vkui/src/components/ActionSheet/types.ts @@ -1,6 +1,6 @@ import type * as React from 'react'; import type { PlacementWithAuto } from '../../lib/floating/types/common'; -import type { HasComponent, HasRootRef } from '../../types'; +import type { HasComponent, HasRender, HasRootRef } from '../../types'; export type ToggleRef = Element | null | undefined | React.RefObject; @@ -34,7 +34,8 @@ export interface SharedDropdownProps extends UseFocusTrapProps, Omit, keyof UseFocusTrapProps>, HasRootRef, - HasComponent { + HasComponent, + HasRender { /** * Состояние закрытия всплывающего окна. */ diff --git a/packages/vkui/src/components/Card/Card.tsx b/packages/vkui/src/components/Card/Card.tsx index 0029d5be532..fc07b617fb4 100644 --- a/packages/vkui/src/components/Card/Card.tsx +++ b/packages/vkui/src/components/Card/Card.tsx @@ -1,9 +1,12 @@ import { classNames } from '@vkontakte/vkjs'; -import type { HasComponent, HTMLAttributesWithRootRef } from '../../types'; +import type { HasComponent, HasRender, HTMLAttributesWithRootRef } from '../../types'; import { RootComponent } from '../RootComponent/RootComponent'; import styles from './Card.module.css'; -export interface CardProps extends HTMLAttributesWithRootRef, HasComponent { +export interface CardProps + extends HTMLAttributesWithRootRef, + HasComponent, + HasRender { /** * Внешний вид карточки. */ diff --git a/packages/vkui/src/components/CardGrid/CardGrid.tsx b/packages/vkui/src/components/CardGrid/CardGrid.tsx index 7e06c8f538b..fac5f6c4d91 100644 --- a/packages/vkui/src/components/CardGrid/CardGrid.tsx +++ b/packages/vkui/src/components/CardGrid/CardGrid.tsx @@ -3,7 +3,7 @@ import { classNames } from '@vkontakte/vkjs'; import { useAdaptivity } from '../../hooks/useAdaptivity'; import { type SizeTypeValues, ViewWidth, type ViewWidthType } from '../../lib/adaptivity'; -import type { HasComponent, HTMLAttributesWithRootRef } from '../../types'; +import type { HasComponent, HasRender, HTMLAttributesWithRootRef } from '../../types'; import { RootComponent } from '../RootComponent/RootComponent'; import styles from './CardGrid.module.css'; @@ -30,7 +30,10 @@ const stylesSize = { l: 'vkuiInternalCardGrid--size-l', }; -export interface CardGridProps extends HTMLAttributesWithRootRef, HasComponent { +export interface CardGridProps + extends HTMLAttributesWithRootRef, + HasComponent, + HasRender { /** * Размер карточек. */ diff --git a/packages/vkui/src/components/CardScroll/CardScroll.tsx b/packages/vkui/src/components/CardScroll/CardScroll.tsx index 510c89088f9..228f83cf9c8 100644 --- a/packages/vkui/src/components/CardScroll/CardScroll.tsx +++ b/packages/vkui/src/components/CardScroll/CardScroll.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { classNames } from '@vkontakte/vkjs'; import { useConfigDirection } from '../../hooks/useConfigDirection'; -import type { HasComponent, HTMLAttributesWithRootRef } from '../../types'; +import type { HasComponent, HasRender, HTMLAttributesWithRootRef } from '../../types'; import { HorizontalScroll, type HorizontalScrollProps } from '../HorizontalScroll/HorizontalScroll'; import { RootComponent } from '../RootComponent/RootComponent'; import styles from './CardScroll.module.css'; @@ -17,6 +17,7 @@ const stylesSize = { export interface CardScrollProps extends HTMLAttributesWithRootRef, HasComponent, + HasRender, Pick { /** * При `size=false` ширина `Card` будет регулироваться контентом внутри. В остальных случаях — будет явно задана в процентах. diff --git a/packages/vkui/src/components/ChipsInputBase/Chip/Chip.tsx b/packages/vkui/src/components/ChipsInputBase/Chip/Chip.tsx index 8beaf9499ba..d58a4d9e781 100644 --- a/packages/vkui/src/components/ChipsInputBase/Chip/Chip.tsx +++ b/packages/vkui/src/components/ChipsInputBase/Chip/Chip.tsx @@ -6,7 +6,12 @@ import { classNames, hasReactNode } from '@vkontakte/vkjs'; import { useAdaptivity } from '../../../hooks/useAdaptivity'; import { useFocusVisible } from '../../../hooks/useFocusVisible'; import { useFocusVisibleClassName } from '../../../hooks/useFocusVisibleClassName'; -import type { HasComponent, HasDataAttribute, HTMLAttributesWithRootRef } from '../../../types'; +import type { + HasComponent, + HasDataAttribute, + HasRender, + HTMLAttributesWithRootRef, +} from '../../../types'; import { RootComponent } from '../../RootComponent/RootComponent'; import { Footnote } from '../../Typography/Footnote/Footnote'; import { VisuallyHidden } from '../../VisuallyHidden/VisuallyHidden'; @@ -26,7 +31,8 @@ const modeClassNames = { export interface ChipProps extends HasComponent, HasDataAttribute, - HTMLAttributesWithRootRef { + HTMLAttributesWithRootRef, + HasRender { /** * Режим отображения компонента. */ diff --git a/packages/vkui/src/components/ContentCard/ContentCard.tsx b/packages/vkui/src/components/ContentCard/ContentCard.tsx index 927b3facf84..c9e6da1bac0 100644 --- a/packages/vkui/src/components/ContentCard/ContentCard.tsx +++ b/packages/vkui/src/components/ContentCard/ContentCard.tsx @@ -13,7 +13,7 @@ import styles from './ContentCard.module.css'; export interface ContentCardProps extends HasRootRef, HasComponent, - Omit, + Omit, Omit, keyof React.HTMLAttributes>, HasRef { /** diff --git a/packages/vkui/src/components/Footer/Footer.tsx b/packages/vkui/src/components/Footer/Footer.tsx index a2207b760b4..9fbdba137ef 100644 --- a/packages/vkui/src/components/Footer/Footer.tsx +++ b/packages/vkui/src/components/Footer/Footer.tsx @@ -1,10 +1,12 @@ import type * as React from 'react'; import { classNames } from '@vkontakte/vkjs'; -import type { HasComponent } from '../../types'; +import type { HasComponent, HasRender } from '../../types'; import { Footnote } from '../Typography/Footnote/Footnote'; import styles from './Footer.module.css'; -export type FooterProps = React.AllHTMLAttributes & HasComponent; +export type FooterProps = React.AllHTMLAttributes & + HasComponent & + HasRender; /** * @see https://vkui.io/components/group#footer diff --git a/packages/vkui/src/components/FormItem/FormItem.tsx b/packages/vkui/src/components/FormItem/FormItem.tsx index 57032d8b5ef..dbe1f2bd501 100644 --- a/packages/vkui/src/components/FormItem/FormItem.tsx +++ b/packages/vkui/src/components/FormItem/FormItem.tsx @@ -5,7 +5,7 @@ import { classNames, hasReactNode, isPrimitiveReactNode } from '@vkontakte/vkjs' import { useAdaptivity } from '../../hooks/useAdaptivity'; import { useExternRef } from '../../hooks/useExternRef'; import { defineComponentDisplayNames } from '../../lib/react/defineComponentDisplayNames'; -import type { HasComponent, HasRootRef } from '../../types'; +import type { HasComponent, HasRender, HasRootRef } from '../../types'; import { Removable, type RemovableProps } from '../Removable/Removable'; import { RootComponent } from '../RootComponent/RootComponent'; import { Footnote } from '../Typography/Footnote/Footnote'; @@ -29,6 +29,7 @@ export interface FormItemProps extends React.AllHTMLAttributes, HasRootRef, HasComponent, + HasRender, RemovableProps { /** * Дополнительный элемент, отображаемый над содержимым. diff --git a/packages/vkui/src/components/FormItem/FormItemTop/FormItemTop.tsx b/packages/vkui/src/components/FormItem/FormItemTop/FormItemTop.tsx index af8dd2d6c29..770597884f9 100644 --- a/packages/vkui/src/components/FormItem/FormItemTop/FormItemTop.tsx +++ b/packages/vkui/src/components/FormItem/FormItemTop/FormItemTop.tsx @@ -1,8 +1,11 @@ -import type { HasComponent, HTMLAttributesWithRootRef } from '../../../types'; +import type { HasComponent, HasRender, HTMLAttributesWithRootRef } from '../../../types'; import { RootComponent } from '../../RootComponent/RootComponent'; import styles from '../FormItem.module.css'; -export interface FormItemTopProps extends HTMLAttributesWithRootRef, HasComponent {} +export interface FormItemTopProps + extends HTMLAttributesWithRootRef, + HasComponent, + HasRender {} /** * Служит оберткой для составной шапки поля, отвечая за выравнивание контента и расстановку отступов. diff --git a/packages/vkui/src/components/FormItem/FormItemTop/FormItemTopAside.tsx b/packages/vkui/src/components/FormItem/FormItemTop/FormItemTopAside.tsx index c49e2f2a70a..074354e715c 100644 --- a/packages/vkui/src/components/FormItem/FormItemTop/FormItemTopAside.tsx +++ b/packages/vkui/src/components/FormItem/FormItemTop/FormItemTopAside.tsx @@ -1,13 +1,14 @@ import type * as React from 'react'; import { classNames } from '@vkontakte/vkjs'; -import type { HasComponent, HasRootRef } from '../../../types'; +import type { HasComponent, HasRender, HasRootRef } from '../../../types'; import { Subhead } from '../../Typography/Subhead/Subhead'; import styles from '../FormItem.module.css'; export interface FormItemTopAsideProps extends React.AllHTMLAttributes, HasRootRef, - HasComponent {} + HasComponent, + HasRender {} /** * Отвечает за отрисовку дополнительного контента справа от заголовка поля. diff --git a/packages/vkui/src/components/FormItem/FormItemTop/FormItemTopLabel.tsx b/packages/vkui/src/components/FormItem/FormItemTop/FormItemTopLabel.tsx index 54bd1aa51c4..50d17fc5a6d 100644 --- a/packages/vkui/src/components/FormItem/FormItemTop/FormItemTopLabel.tsx +++ b/packages/vkui/src/components/FormItem/FormItemTop/FormItemTopLabel.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { classNames } from '@vkontakte/vkjs'; -import type { HasComponent, HasRootRef } from '../../../types'; +import type { HasComponent, HasRender, HasRootRef } from '../../../types'; import { Subhead } from '../../Typography/Subhead/Subhead'; import { FormItemContext } from '../context'; import styles from '../FormItem.module.css'; @@ -10,7 +10,8 @@ import styles from '../FormItem.module.css'; export interface FormItemTopLabelProps extends React.AllHTMLAttributes, HasRootRef, - HasComponent {} + HasComponent, + HasRender {} /** * Отвечает за отрисовку заголовка поля. По умолчанию компонент представлен тегом `label`, если передано свойство `htmlFor`. diff --git a/packages/vkui/src/components/Group/GroupContainer.tsx b/packages/vkui/src/components/Group/GroupContainer.tsx index f7467125e11..e8d6232eba1 100644 --- a/packages/vkui/src/components/Group/GroupContainer.tsx +++ b/packages/vkui/src/components/Group/GroupContainer.tsx @@ -6,7 +6,7 @@ import { useModalContext } from '../../context/ModalContext'; import { useAdaptivity } from '../../hooks/useAdaptivity'; import { type SizeTypeValues, ViewWidth, type ViewWidthType } from '../../lib/adaptivity'; import { warnOnce } from '../../lib/warnOnce'; -import type { HasComponent, HTMLAttributesWithRootRef } from '../../types'; +import type { HasComponent, HasRender, HTMLAttributesWithRootRef } from '../../types'; import { AppRootContext } from '../AppRoot/AppRootContext'; import { RootComponent } from '../RootComponent/RootComponent'; import styles from './Group.module.css'; @@ -78,7 +78,8 @@ function useGroupMode( } export type GroupContainerProps = HTMLAttributesWithRootRef & - HasComponent & { + HasComponent & + HasRender & { /** `show` (только для `mode="plain"`) - разделитель всегда показывается `hide` - разделитель всегда спрятан, diff --git a/packages/vkui/src/components/Group/GroupDescription.tsx b/packages/vkui/src/components/Group/GroupDescription.tsx index f23f8cec6f0..e87caf70359 100644 --- a/packages/vkui/src/components/Group/GroupDescription.tsx +++ b/packages/vkui/src/components/Group/GroupDescription.tsx @@ -1,9 +1,11 @@ import { classNames } from '@vkontakte/vkjs'; -import type { HasComponent, HTMLAttributesWithRootRef } from '../../types'; +import type { HasComponent, HasRender, HTMLAttributesWithRootRef } from '../../types'; import { Footnote } from '../Typography/Footnote/Footnote'; import styles from './Group.module.css'; -export type GroupDescriptionProps = HTMLAttributesWithRootRef & HasComponent; +export type GroupDescriptionProps = HTMLAttributesWithRootRef & + HasComponent & + HasRender; export const GroupDescription = ({ className, ...restProps }: GroupDescriptionProps) => ( diff --git a/packages/vkui/src/components/Group/GroupExpandedContent.tsx b/packages/vkui/src/components/Group/GroupExpandedContent.tsx index e3e3fdd30b4..e3a73b0a55b 100644 --- a/packages/vkui/src/components/Group/GroupExpandedContent.tsx +++ b/packages/vkui/src/components/Group/GroupExpandedContent.tsx @@ -1,4 +1,4 @@ -import type { HasComponent, HTMLAttributesWithRootRef } from '../../types'; +import type { HasComponent, HasRender, HTMLAttributesWithRootRef } from '../../types'; import { RootComponent } from '../RootComponent/RootComponent'; import styles from './Group.module.css'; @@ -8,7 +8,8 @@ const stylesDirection = { }; export type GroupExpandedContentProps = HTMLAttributesWithRootRef & - HasComponent & { + HasComponent & + HasRender & { /** * Направление отображения контента. */ diff --git a/packages/vkui/src/components/Header/Header.tsx b/packages/vkui/src/components/Header/Header.tsx index 81a99e2ab0b..06f38d37603 100644 --- a/packages/vkui/src/components/Header/Header.tsx +++ b/packages/vkui/src/components/Header/Header.tsx @@ -2,7 +2,7 @@ import type * as React from 'react'; import { classNames, hasReactNode, isPrimitiveReactNode } from '@vkontakte/vkjs'; -import type { HasComponent, HTMLAttributesWithRootRef } from '../../types'; +import type { HasComponent, HasRender, HTMLAttributesWithRootRef } from '../../types'; import { RootComponent } from '../RootComponent/RootComponent'; import { Caption } from '../Typography/Caption/Caption'; import { Footnote } from '../Typography/Footnote/Footnote'; @@ -12,7 +12,10 @@ import { Subhead } from '../Typography/Subhead/Subhead'; import { Title } from '../Typography/Title/Title'; import styles from './Header.module.css'; -export interface HeaderProps extends HTMLAttributesWithRootRef, HasComponent { +export interface HeaderProps + extends HTMLAttributesWithRootRef, + HasComponent, + HasRender { /** * Размер компонента. */ diff --git a/packages/vkui/src/components/MiniInfoCell/MiniInfoCell.tsx b/packages/vkui/src/components/MiniInfoCell/MiniInfoCell.tsx index 95c871d1393..e133a2b187e 100644 --- a/packages/vkui/src/components/MiniInfoCell/MiniInfoCell.tsx +++ b/packages/vkui/src/components/MiniInfoCell/MiniInfoCell.tsx @@ -23,7 +23,7 @@ export interface MiniInfoCellProps TappableOmitProps, 'hoverMode' | 'activeMode' | 'hovered' | 'activated' | 'hasHover' | 'hasActive' >, - RootComponentProps { + RootComponentProps { /** * Иконка слева.
* Рекомендуется использовать иконки размера 20. diff --git a/packages/vkui/src/components/Placeholder/Placeholder.tsx b/packages/vkui/src/components/Placeholder/Placeholder.tsx index e87a1b42ffb..b033cdcefad 100644 --- a/packages/vkui/src/components/Placeholder/Placeholder.tsx +++ b/packages/vkui/src/components/Placeholder/Placeholder.tsx @@ -1,6 +1,6 @@ import type * as React from 'react'; import { classNames, hasReactNode } from '@vkontakte/vkjs'; -import type { HasComponent, HTMLAttributesWithRootRef } from '../../types'; +import type { HasComponent, HasRender, HTMLAttributesWithRootRef } from '../../types'; import { RootComponent } from '../RootComponent/RootComponent'; import { Headline } from '../Typography/Headline/Headline'; import { Title } from '../Typography/Title/Title'; @@ -39,13 +39,17 @@ const PlaceholderIcon = (props: PlaceholderIconProps): React.ReactNode => ( ); -export type PlaceholderTitleProps = HTMLAttributesWithRootRef & HasComponent; +export type PlaceholderTitleProps = HTMLAttributesWithRootRef & + HasComponent & + HasRender; const PlaceholderTitle = ({ className, ...restProps }: PlaceholderTitleProps): React.ReactNode => ( ); -export type PlaceholderDescriptionProps = HTMLAttributesWithRootRef<HTMLElement> & HasComponent; +export type PlaceholderDescriptionProps = HTMLAttributesWithRootRef<HTMLElement> & + HasComponent & + HasRender<HTMLElement>; const PlaceholderDescription = ({ className, diff --git a/packages/vkui/src/components/RootComponent/RootComponent.test.tsx b/packages/vkui/src/components/RootComponent/RootComponent.test.tsx index 0858912aa0d..4e9b58730cd 100644 --- a/packages/vkui/src/components/RootComponent/RootComponent.test.tsx +++ b/packages/vkui/src/components/RootComponent/RootComponent.test.tsx @@ -1,6 +1,152 @@ +import { createRef, useEffect } from 'react'; +import { render, screen } from '@testing-library/react'; +import { vi } from 'vitest'; 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')); + }); + + it('keeps render subtree mounted on rerender with inline render', () => { + const mount = vi.fn(); + const unmount = vi.fn(); + + const Child = () => { + useEffect(() => { + mount(); + return unmount; + }, []); + + return null; + }; + + const { rerender, unmount: unmountRoot } = render( + <RootComponent<HTMLDivElement> + render={({ getRootRef, ...props }) => ( + <div ref={getRootRef} {...props}> + <Child /> + </div> + )} + />, + ); + + expect(mount).toHaveBeenCalledTimes(1); + expect(unmount).not.toHaveBeenCalled(); + + rerender( + <RootComponent<HTMLDivElement> + render={({ getRootRef, ...props }) => ( + <div ref={getRootRef} {...props}> + <Child /> + </div> + )} + />, + ); + + expect(mount).toHaveBeenCalledTimes(1); + expect(unmount).not.toHaveBeenCalled(); + + unmountRoot(); + expect(unmount).toHaveBeenCalledTimes(1); + }); + + it('remounts subtree on rerender with inline Component', () => { + const mount = vi.fn(); + const unmount = vi.fn(); + + const Child = () => { + useEffect(() => { + mount(); + return unmount; + }, []); + + return null; + }; + + const { rerender } = render( + <RootComponent + Component={(props) => ( + <div {...props}> + <Child /> + </div> + )} + />, + ); + + expect(mount).toHaveBeenCalledTimes(1); + expect(unmount).not.toHaveBeenCalled(); + + rerender( + <RootComponent + Component={(props) => ( + <div {...props}> + <Child /> + </div> + )} + />, + ); + + expect(mount).toHaveBeenCalledTimes(2); + expect(unmount).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/packages/vkui/src/components/RootComponent/RootComponent.tsx b/packages/vkui/src/components/RootComponent/RootComponent.tsx index 8390c99c408..362792028bd 100644 --- a/packages/vkui/src/components/RootComponent/RootComponent.tsx +++ b/packages/vkui/src/components/RootComponent/RootComponent.tsx @@ -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 & + HasRender<T>; export interface RootComponentExtendProps { /** @@ -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 }); + /** * Базовый корневой компонент. */ @@ -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} />; + } + + return <Component ref={getRootRef} {...resolvedProps} />; +}; diff --git a/packages/vkui/src/components/TabbarItem/TabbarItem.tsx b/packages/vkui/src/components/TabbarItem/TabbarItem.tsx index e6e6af15a8b..38403b9a492 100644 --- a/packages/vkui/src/components/TabbarItem/TabbarItem.tsx +++ b/packages/vkui/src/components/TabbarItem/TabbarItem.tsx @@ -7,7 +7,7 @@ import { useFocusVisibleClassName } from '../../hooks/useFocusVisibleClassName'; import { usePlatform } from '../../hooks/usePlatform'; import { callMultiple } from '../../lib/callMultiple'; import { COMMON_WARNINGS, warnOnce } from '../../lib/warnOnce'; -import type { HasComponent, HasRootRef } from '../../types'; +import type { HasComponent, HasRender, HasRootRef } from '../../types'; import { RootComponent } from '../RootComponent/RootComponent'; import { Tappable } from '../Tappable/Tappable'; import { Footnote } from '../Typography/Footnote/Footnote'; @@ -16,7 +16,8 @@ import styles from './TabbarItem.module.css'; export interface TabbarItemProps extends Omit<React.AllHTMLAttributes<HTMLElement>, 'label'>, HasRootRef<HTMLElement>, - HasComponent { + HasComponent, + HasRender<HTMLElement> { /** * Выбранное состояние компонента. */ diff --git a/packages/vkui/src/components/Typography/Typography.tsx b/packages/vkui/src/components/Typography/Typography.tsx index 7a5e43d87c1..5b44b0f2ce7 100644 --- a/packages/vkui/src/components/Typography/Typography.tsx +++ b/packages/vkui/src/components/Typography/Typography.tsx @@ -1,6 +1,6 @@ import type * as React from 'react'; import { classNames } from '@vkontakte/vkjs'; -import type { HasComponent, HasRootRef } from '../../types'; +import type { HasComponent, HasRender, HasRootRef } from '../../types'; import { RootComponent } from '../RootComponent/RootComponent'; import styles from './Typography.module.css'; @@ -34,7 +34,8 @@ export interface HasCaps { export interface TypographyProps extends React.AllHTMLAttributes<HTMLElement>, HasComponent, - HasRootRef<HTMLElement> { + HasRootRef<HTMLElement>, + HasRender<HTMLElement> { /** * Задаёт начертание шрифта, отличное от стандартного. */ diff --git a/packages/vkui/src/components/WriteBar/WriteBarTextarea/WriteBarTextarea.tsx b/packages/vkui/src/components/WriteBar/WriteBarTextarea/WriteBarTextarea.tsx index 3fe814d77dd..497d4b1b264 100644 --- a/packages/vkui/src/components/WriteBar/WriteBarTextarea/WriteBarTextarea.tsx +++ b/packages/vkui/src/components/WriteBar/WriteBarTextarea/WriteBarTextarea.tsx @@ -11,7 +11,7 @@ import { Headline } from '../../Typography/Headline/Headline'; import { Title } from '../../Typography/Title/Title'; import styles from './WriteBarTextarea.module.css'; -type WriteBarTextareaProps = RootComponentProps<HTMLTextAreaElement> & { +type WriteBarTextareaProps = Omit<RootComponentProps<HTMLTextAreaElement>, 'render'> & { /** * Вызывается при смене высоты поля ввода. */ diff --git a/packages/vkui/src/types.ts b/packages/vkui/src/types.ts index c722e57ee32..3fd49c8e176 100644 --- a/packages/vkui/src/types.ts +++ b/packages/vkui/src/types.ts @@ -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; }