diff --git a/src/components/Table/__stories__/Table.stories.tsx b/src/components/Table/__stories__/Table.stories.tsx index 144bc1b0b4..ca6e588c39 100644 --- a/src/components/Table/__stories__/Table.stories.tsx +++ b/src/components/Table/__stories__/Table.stories.tsx @@ -164,7 +164,7 @@ const WithTableActionsTemplate: StoryFn> = (args) => { ({title})} + mapItemDataToContentProps={(title) => ({title})} title="Actions select example" /> ); diff --git a/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.tsx b/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.tsx index 6cca789c33..840921b0a7 100644 --- a/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.tsx +++ b/src/components/Table/hoc/withTableSettings/TableColumnSetup/TableColumnSetup.tsx @@ -25,7 +25,7 @@ import type { } from '../../../../TreeSelect/types'; import {TextInput} from '../../../../controls/TextInput'; import {Flex} from '../../../../layout/Flex/Flex'; -import type {ListItemCommonProps, ListItemViewProps} from '../../../../useList'; +import type {ListItemViewContentType, ListItemViewProps} from '../../../../useList'; import {ListContainerView, ListItemView, useListFilter} from '../../../../useList'; import {block} from '../../../../utils/cn'; import type {TableColumnConfig} from '../../../Table'; @@ -190,16 +190,18 @@ const useDndRenderItem = (sortable: boolean | undefined) => { }) => { const isDragDisabled = sortable === false || renderContainerProps?.isDragDisabled === true; const endSlot = isDragDisabled ? undefined : ; - const hasSelectionIcon = !item.isRequired; const startSlot = item.isRequired ? : undefined; const selected = item.isRequired ? false : props.selected; const commonProps: ListItemViewProps = { ...props, selected, - startSlot, - hasSelectionIcon, - endSlot, + selectionViewType: item.isRequired ? 'single' : 'multiple', + content: { + ...props.content, + startSlot, + endSlot, + }, }; if (isDragDisabled) { @@ -241,7 +243,7 @@ export type TableColumnSetupItem = TableSetting & { sticky?: TableColumnConfig['sticky']; }; -const mapItemDataToProps = (item: TableColumnSetupItem): ListItemCommonProps => { +const mapItemDataToContentProps = (item: TableColumnSetupItem): ListItemViewContentType => { return { title: item.title, }; @@ -435,7 +437,7 @@ export const TableColumnSetup = (props: TableColumnSetupProps) => { return ( ({ renderItem: propsRenderItem, renderContainer = ListContainer, onItemClick: propsOnItemClick, - mapItemDataToProps, + mapItemDataToContentProps, }: TreeListProps) => { const uniqId = useUniqId(); const treeListId = id ?? uniqId; @@ -71,7 +71,7 @@ export const TreeList = ({ id: itemId, size, multiple, - mapItemDataToProps, + mapItemDataToContentProps, onItemClick, list, }); diff --git a/src/components/TreeList/__stories__/TreeListDocs.md b/src/components/TreeList/__stories__/TreeListDocs.md index 1655bfbe67..c1009f7018 100644 --- a/src/components/TreeList/__stories__/TreeListDocs.md +++ b/src/components/TreeList/__stories__/TreeListDocs.md @@ -25,7 +25,7 @@ const items: ListItemType[] = ['one', 'two', 'free', 'four', 'five']; const list = useList({items}); - ({title: item})} />; + ({title: item})} />; ``` ### Example with state: @@ -56,7 +56,7 @@ const Component = () => { ({title})} + mapItemDataToContentProps={({title}) => ({title})} /> ); }; @@ -64,18 +64,18 @@ const Component = () => { ## Props: -| Name | Description | Type | Default | -| :----------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------: | :-----: | -| list | result of [list](/docs/lab-uselist--docs#uselist) hook. | `UseListResult` | | -| containerRef | a reference to the DOM element of the List container inside which to search for its elements; | `React.RefObject` | | -| qa | Selector for tests | `string` | | -| size | The size of the element. This also affects the rounding radius of the list element | `s \| m \| l \| xl` | `m` | -| mapItemDataToProps | Map list item data (`T`) to `ListItemView` props | `(data: T) => ListItemCommonProps` | | -| multiple | One or multiple elements selected list | `boolean` | `false` | -| id | id attribute | `string` | | -| renderItem | Redefine the rendering of a list item. For example, add dividers between list items or wrap an item in a link component. As a view component to display a list item, use [ListItemView](/docs/lab-uselist--docs#listitemview); | `(props: TreeListRenderItem) => React.JSX.Element` | | -| renderContainer | Render custom list container. | `(props: TreeListRenderContainer) => React.JSX.Element` | | -| onItemClick | Override default on click behavior. Pass `null` to disable on click handler | `(props: {id: ListItemId; list: UseListResult}, e: React.SyntheticEvent) => void \| null` | | +| Name | Description | Type | Default | +| :------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------: | :-----: | +| list | result of [list](/docs/lab-uselist--docs#uselist) hook. | `UseListResult` | | +| containerRef | a reference to the DOM element of the List container inside which to search for its elements; | `React.RefObject` | | +| qa | Selector for tests | `string` | | +| size | The size of the element. This also affects the rounding radius of the list element | `s \| m \| l \| xl` | `m` | +| mapItemDataToContentProps | Map list item data (`T`) to `ListItemView` `content` prop | `(data: T) => ListItemViewContentProps` | | +| multiple | One or multiple elements selected list | `boolean` | `false` | +| id | id attribute | `string` | | +| renderItem | Redefine the rendering of a list item. For example, add dividers between list items or wrap an item in a link component. As a view component to display a list item, use [ListItemView](/docs/lab-uselist--docs#listitemview); | `(props: TreeListRenderItem) => React.JSX.Element` | | +| renderContainer | Render custom list container. | `(props: TreeListRenderContainer) => React.JSX.Element` | | +| onItemClick | Override default on click behavior. Pass `null` to disable on click handler | `(props: {id: ListItemId; list: UseListResult}, e: React.SyntheticEvent) => void \| null` | | ### TreeListRenderItem props: diff --git a/src/components/TreeList/__stories__/stories/DefaultStory.tsx b/src/components/TreeList/__stories__/stories/DefaultStory.tsx index e3a63f60e9..df90be1e4e 100644 --- a/src/components/TreeList/__stories__/stories/DefaultStory.tsx +++ b/src/components/TreeList/__stories__/stories/DefaultStory.tsx @@ -12,7 +12,7 @@ function identity(value: T): T { } export interface DefaultStoryProps - extends Omit, 'items' | 'mapItemDataToProps'> { + extends Omit, 'items' | 'mapItemDataToContentProps'> { itemsCount?: number; } @@ -34,7 +34,7 @@ export const DefaultStory = ({itemsCount = 5, ...props}: DefaultStoryProps) => { {...props} list={listWithGroups} onItemClick={null} - mapItemDataToProps={identity} + mapItemDataToContentProps={identity} /> @@ -46,7 +46,7 @@ export const DefaultStory = ({itemsCount = 5, ...props}: DefaultStoryProps) => { {...props} list={listWithNoGroups} onItemClick={null} - mapItemDataToProps={identity} + mapItemDataToContentProps={identity} /> diff --git a/src/components/TreeList/__stories__/stories/InfinityScrollStory.tsx b/src/components/TreeList/__stories__/stories/InfinityScrollStory.tsx index b0dda4d8a7..b3a11c6bc0 100644 --- a/src/components/TreeList/__stories__/stories/InfinityScrollStory.tsx +++ b/src/components/TreeList/__stories__/stories/InfinityScrollStory.tsx @@ -21,7 +21,7 @@ function identity(value: T): T { export interface InfinityScrollStoryProps extends Omit< TreeListProps, - 'value' | 'onUpdate' | 'items' | 'multiple' | 'size' | 'mapItemDataToProps' + 'value' | 'onUpdate' | 'items' | 'multiple' | 'size' | 'mapItemDataToContentProps' > { itemsCount?: number; } @@ -44,13 +44,18 @@ export const InfinityScrollStory = ({itemsCount = 3, ...storyProps}: InfinityScr {...storyProps} size="l" list={list} - mapItemDataToProps={identity} + mapItemDataToContentProps={identity} multiple={multiple} renderItem={({props, context: {isLastItem, childrenIds}}) => { const node = ( {childrenIds.length} : undefined} + content={{ + ...props.content, + endSlot: childrenIds ? ( + + ) : undefined, + }} /> ); diff --git a/src/components/TreeList/__stories__/stories/WithDisabledElementsStory.tsx b/src/components/TreeList/__stories__/stories/WithDisabledElementsStory.tsx index d79539aaa9..50c8f06b97 100644 --- a/src/components/TreeList/__stories__/stories/WithDisabledElementsStory.tsx +++ b/src/components/TreeList/__stories__/stories/WithDisabledElementsStory.tsx @@ -8,7 +8,7 @@ import {TreeList} from '../../TreeList'; import type {TreeListProps} from '../../types'; export interface WithDisabledElementsStoryProps - extends Omit, 'items' | 'mapItemDataToProps'> {} + extends Omit, 'items' | 'mapItemDataToContentProps'> {} const items: ListItemType<{text: string}>[] = [ { @@ -50,7 +50,7 @@ export const WithDisabledElementsStory = ({...storyProps}: WithDisabledElementsS {...storyProps} list={list} containerRef={containerRef} - mapItemDataToProps={({text}) => ({title: text})} + mapItemDataToContentProps={({text}) => ({title: text})} onItemClick={({id}) => { getListItemClickHandler({list})({id}); alert( diff --git a/src/components/TreeList/__stories__/stories/WithDndListStory.tsx b/src/components/TreeList/__stories__/stories/WithDndListStory.tsx index 22bcb3827d..2aaf2d0ec7 100644 --- a/src/components/TreeList/__stories__/stories/WithDndListStory.tsx +++ b/src/components/TreeList/__stories__/stories/WithDndListStory.tsx @@ -42,7 +42,7 @@ const randomItems: CustomDataType[] = createRandomizedData({ }).map(({data}, idx) => ({someRandomKey: data, id: String(idx)})); export interface WithDndListStoryProps - extends Omit, 'items' | 'mapItemDataToProps'> {} + extends Omit, 'items' | 'mapItemDataToContentProps'> {} export const WithDndListStory = (storyProps: WithDndListStoryProps) => { const [items, setItems] = React.useState(randomItems); @@ -117,10 +117,13 @@ export const WithDndListStory = (storyProps: WithDndListStoryProps) => { index, renderContainerProps, }) => { - const commonProps = { + const commonProps: ListItemViewProps = { ...props, - title: data.someRandomKey, - endSlot: , + content: { + ...props.content, + title: data.someRandomKey, + endSlot: , + }, }; // here passed props from `renderContainer` method. @@ -151,7 +154,7 @@ export const WithDndListStory = (storyProps: WithDndListStoryProps) => { {...storyProps} list={list} containerRef={containerRef} - mapItemDataToProps={({someRandomKey}) => ({title: someRandomKey})} + mapItemDataToContentProps={({someRandomKey}) => ({title: someRandomKey})} renderContainer={renderContainer} renderItem={renderItem} /> diff --git a/src/components/TreeList/__stories__/stories/WithFiltrationAndControlsStory.tsx b/src/components/TreeList/__stories__/stories/WithFiltrationAndControlsStory.tsx index a2463b8684..6c77a72dc6 100644 --- a/src/components/TreeList/__stories__/stories/WithFiltrationAndControlsStory.tsx +++ b/src/components/TreeList/__stories__/stories/WithFiltrationAndControlsStory.tsx @@ -15,7 +15,10 @@ interface Entity { } export interface WithFiltrationAndControlsStoryProps - extends Omit, 'value' | 'onUpdate' | 'items' | 'mapItemDataToProps'> { + extends Omit< + TreeListProps, + 'value' | 'onUpdate' | 'items' | 'mapItemDataToContentProps' + > { itemsCount?: number; } @@ -59,7 +62,7 @@ export const WithFiltrationAndControlsStory = ({ x} + mapItemDataToContentProps={(x) => x} renderContainer={renderContainer} /> diff --git a/src/components/TreeList/__stories__/stories/WithGroupSelectionAndCustomIconStory.tsx b/src/components/TreeList/__stories__/stories/WithGroupSelectionAndCustomIconStory.tsx index 0483db50b0..3b1e011016 100644 --- a/src/components/TreeList/__stories__/stories/WithGroupSelectionAndCustomIconStory.tsx +++ b/src/components/TreeList/__stories__/stories/WithGroupSelectionAndCustomIconStory.tsx @@ -6,7 +6,7 @@ import {Button} from '../../../Button'; import {Icon} from '../../../Icon'; import {Flex, spacing} from '../../../layout'; import {ListItemView, useList} from '../../../useList'; -import type {ListItemCommonProps, ListItemId} from '../../../useList'; +import type {ListItemId, ListItemViewContentType} from '../../../useList'; import {createRandomizedData} from '../../../useList/__stories__/utils/makeData'; import {TreeList} from '../../TreeList'; import type {TreeListProps} from '../../types'; @@ -24,12 +24,14 @@ interface CustomDataStructure { export interface WithGroupSelectionAndCustomIconStoryProps extends Omit< TreeListProps, - 'value' | 'onUpdate' | 'items' | 'cantainerRef' | 'size' | 'mapItemDataToProps' + 'value' | 'onUpdate' | 'items' | 'cantainerRef' | 'size' | 'mapItemDataToContentProps' > { itemsCount?: number; } -const mapCustomDataStructureToKnownProps = (props: CustomDataStructure): ListItemCommonProps => ({ +const mapCustomDataStructureToKnownProps = ( + props: CustomDataStructure, +): ListItemViewContentType => ({ title: props.a, }); @@ -61,28 +63,26 @@ export const WithGroupSelectionAndCustomIconStory = ({ {...props} list={list} size="l" - mapItemDataToProps={mapCustomDataStructureToKnownProps} + mapItemDataToContentProps={mapCustomDataStructureToKnownProps} onItemClick={onItemClick} - renderItem={({ - data, - props: { - expanded, // don't use default ListItemView expand icon - ...preparedProps - }, - context: {childrenIds}, - }) => { + renderItem={({id, props: itemProps, context: {childrenIds}}) => { // has no group - preparedProps.hasSelectionIcon = Boolean(props.multiple); + itemProps.selectionViewType = props.multiple ? 'multiple' : 'single'; return ( - } - endSlot={ - childrenIds ? ( + {...itemProps} + content={{ + ...itemProps.content, + isGroup: false, + startSlot: ( + + ), + + endSlot: childrenIds ? ( - ) : undefined - } + ) : undefined, + }} /> ); }} diff --git a/src/components/TreeList/__stories__/stories/WithItemLinksAndActionsStory.tsx b/src/components/TreeList/__stories__/stories/WithItemLinksAndActionsStory.tsx index 261371afa9..58fb2fad2a 100644 --- a/src/components/TreeList/__stories__/stories/WithItemLinksAndActionsStory.tsx +++ b/src/components/TreeList/__stories__/stories/WithItemLinksAndActionsStory.tsx @@ -21,7 +21,7 @@ function identity(value: T): T { } export interface WithItemLinksAndActionsStoryProps - extends Omit, 'items' | 'size' | 'mapItemDataToProps'> {} + extends Omit, 'items' | 'size' | 'mapItemDataToContentProps'> {} export const WithItemLinksAndActionsStory = (props: WithItemLinksAndActionsStoryProps) => { const items = React.useMemo(() => createRandomizedData({num: 10, depth: 1}), []); @@ -43,49 +43,41 @@ export const WithItemLinksAndActionsStory = (props: WithItemLinksAndActionsStory { + renderItem={({id, props: itemProps, context: {childrenIds}}) => { return ( // eslint-disable-next-line jsx-a11y/anchor-is-valid { - e.stopPropagation(); - e.preventDefault(); - }} - items={[ - { - action: (e) => { - e.stopPropagation(); - console.log( - `Clicked by action with id: ${state.id}`, - ); + {...itemProps} + content={{ + ...itemProps.content, + isGroup: false, + endSlot: ( + { + e.stopPropagation(); + e.preventDefault(); + }} + items={[ + { + action: (e) => { + e.stopPropagation(); + console.log(`Clicked by action with id: ${id}`); + }, + text: 'action 1', }, - text: 'action 1', - }, - ]} - defaultSwitcherProps={{ - extraProps: { - 'aria-label': moreOptionsButton, - }, - }} - /> - } - startSlot={ - childrenIds ? ( + ]} + defaultSwitcherProps={{ + extraProps: { + 'aria-label': moreOptionsButton, + }, + }} + /> + ), + startSlot: childrenIds ? ( ) : ( 0 ? {ml: 1} : undefined} + spacing={ + (itemProps.content.indentation || 0) > 0 + ? {ml: 1} + : undefined + } > - ) - } + ), + }} /> ); diff --git a/src/components/TreeList/types.ts b/src/components/TreeList/types.ts index 9dbb33aa22..d74241d5c3 100644 --- a/src/components/TreeList/types.ts +++ b/src/components/TreeList/types.ts @@ -3,11 +3,11 @@ import type React from 'react'; import type {QAProps} from '../types'; import type { ListContainerProps, - ListItemCommonProps, ListItemId, ListItemListContextProps, ListItemSize, - RenderItemProps, + ListItemViewCommonProps, + ListItemViewContentType, UseListResult, } from '../useList'; @@ -15,7 +15,7 @@ export type TreeListRenderItem = (props: { id: ListItemId; data: T; // required item props to render - props: RenderItemProps; + props: ListItemViewCommonProps; // internal list context props context: ListItemListContextProps; list: UseListResult; @@ -29,7 +29,7 @@ export type TreeListContainerProps = ListContainerProps & { export type TreeListRenderContainer = (props: TreeListContainerProps) => React.JSX.Element; -export type TreeListMapItemDataToProps = (item: T) => ListItemCommonProps; +export type TreeListMapItemDataToContentProps = (item: T) => ListItemViewContentType; export type TreeListOnItemClickPayload = {id: ListItemId; list: UseListResult}; @@ -57,5 +57,8 @@ export interface TreeListProps extends QAProps { * `null` - disable default click handler */ onItemClick?: null | TreeListOnItemClick; - mapItemDataToProps: TreeListMapItemDataToProps; + /** + * List item `data` to ListItemView `content` props + */ + mapItemDataToContentProps: TreeListMapItemDataToContentProps; } diff --git a/src/components/TreeSelect/TreeSelect.tsx b/src/components/TreeSelect/TreeSelect.tsx index 73084f2ebd..8330f09b93 100644 --- a/src/components/TreeSelect/TreeSelect.tsx +++ b/src/components/TreeSelect/TreeSelect.tsx @@ -57,7 +57,7 @@ export const TreeSelect = React.forwardRef(function TreeSelect( renderControl, renderItem = defaultItemRenderer as TreeListRenderItem, renderContainer, - mapItemDataToProps, + mapItemDataToContentProps, onFocus, onBlur, getItemId, @@ -173,7 +173,9 @@ export const TreeSelect = React.forwardRef(function TreeSelect( mapItemDataToProps(list.structure.itemsById[itemId]).title), + value.map( + (itemId) => mapItemDataToContentProps(list.structure.itemsById[itemId]).title, + ), ).join(', ')} view="normal" pin="round-round" @@ -224,7 +226,7 @@ export const TreeSelect = React.forwardRef(function TreeSelect( containerRef={containerRef} onItemClick={handleItemClick} renderContainer={renderContainer} - mapItemDataToProps={mapItemDataToProps} + mapItemDataToContentProps={mapItemDataToContentProps} renderItem={renderItem ?? defaultItemRenderer} /> diff --git a/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx b/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx index 5c5c25035f..a2f20a9d13 100644 --- a/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx +++ b/src/components/TreeSelect/__stories__/TreeSelect.stories.tsx @@ -42,7 +42,7 @@ export default { const DefaultTemplate: StoryFn< Omit< TreeSelectProps<{title: string}>, - 'value' | 'onUpdate' | 'items' | 'mapItemDataToProps' + 'value' | 'onUpdate' | 'items' | 'mapItemDataToContentProps' > & { itemsCount?: number; } @@ -55,7 +55,7 @@ const DefaultTemplate: StoryFn< {...props} placeholder="-" items={items} - mapItemDataToProps={(x) => x} + mapItemDataToContentProps={(x) => x} onItemClick={({id, list}) => { getListItemClickHandler({list})({id}); console.log('clicked on item with id: ', id); diff --git a/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx b/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx index 0e868891ff..4695224a0b 100644 --- a/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx +++ b/src/components/TreeSelect/__stories__/components/InfinityScrollExample.tsx @@ -23,7 +23,7 @@ function identity(value: T): T { export interface InfinityScrollExampleProps extends Omit< TreeSelectProps, - 'value' | 'onUpdate' | 'items' | 'mapItemDataToProps' | 'multiple' | 'defaultValue' + 'value' | 'onUpdate' | 'items' | 'mapItemDataToContentProps' | 'multiple' | 'defaultValue' > { itemsCount?: number; } @@ -75,16 +75,20 @@ export const InfinityScrollExample = ({ { + renderItem={({props, context: {isLastItem, childrenIds}}) => { const node = ( {childrenIds.length} + ) : undefined, + }} className={sp({mx: 1})} - endSlot={childrenIds ? : undefined} /> ); diff --git a/src/components/TreeSelect/__stories__/components/WithDisabledElementsExample.tsx b/src/components/TreeSelect/__stories__/components/WithDisabledElementsExample.tsx index 38c991e6ae..fdb31b7530 100644 --- a/src/components/TreeSelect/__stories__/components/WithDisabledElementsExample.tsx +++ b/src/components/TreeSelect/__stories__/components/WithDisabledElementsExample.tsx @@ -10,7 +10,7 @@ interface Entity { } export interface WithDisabledElementsExampleProps - extends Omit, 'items' | 'mapItemDataToProps'> {} + extends Omit, 'items' | 'mapItemDataToContentProps'> {} const items: ListItemType[] = [ { @@ -42,7 +42,7 @@ export const WithDisabledElementsExample = ({...props}: WithDisabledElementsExam items={items} getItemId={({id}) => id} containerRef={containerRef} - mapItemDataToProps={({text}) => ({title: text})} + mapItemDataToContentProps={({text}) => ({title: text})} /> ); }; diff --git a/src/components/TreeSelect/__stories__/components/WithDndListExample.tsx b/src/components/TreeSelect/__stories__/components/WithDndListExample.tsx index 09ef3ffb8b..39b1c9d821 100644 --- a/src/components/TreeSelect/__stories__/components/WithDndListExample.tsx +++ b/src/components/TreeSelect/__stories__/components/WithDndListExample.tsx @@ -38,7 +38,7 @@ type CustomDataType = {someRandomKey: string; id: string}; export interface WithDndListExampleProps extends Omit< TreeSelectProps, - 'value' | 'onUpdate' | 'items' | 'mapItemDataToProps' + 'value' | 'onUpdate' | 'items' | 'mapItemDataToContentProps' > {} const randomItems: CustomDataType[] = createRandomizedData({ @@ -108,10 +108,13 @@ export const WithDndListExample = (storyProps: WithDndListExampleProps) => { index, renderContainerProps, }) => { - const commonProps = { + const commonProps: ListItemViewProps = { ...props, - title: data.someRandomKey, - endSlot: , + content: { + ...props.content, + title: data.someRandomKey, + endSlot: , + }, }; // here passed props from `renderContainer` method. @@ -144,7 +147,7 @@ export const WithDndListExample = (storyProps: WithDndListExampleProps) => { items={items} // you can omit this prop here. If prop `id` passed, TreeSelect would take it by default getItemId={({id}) => id} - mapItemDataToProps={({someRandomKey}) => ({ + mapItemDataToContentProps={({someRandomKey}) => ({ title: someRandomKey, })} renderContainer={renderContainer} diff --git a/src/components/TreeSelect/__stories__/components/WithFiltrationAndControlsExample.tsx b/src/components/TreeSelect/__stories__/components/WithFiltrationAndControlsExample.tsx index 9e7348f234..add573d653 100644 --- a/src/components/TreeSelect/__stories__/components/WithFiltrationAndControlsExample.tsx +++ b/src/components/TreeSelect/__stories__/components/WithFiltrationAndControlsExample.tsx @@ -17,7 +17,7 @@ function identity(value: T): T { export interface WithFiltrationAndControlsExampleProps extends Omit< TreeSelectProps<{title: string}>, - 'value' | 'onUpdate' | 'items' | 'mapItemDataToProps' | 'defaultValue' | 'multiple' + 'value' | 'onUpdate' | 'items' | 'mapItemDataToContentProps' | 'defaultValue' | 'multiple' > { itemsCount?: number; } @@ -51,7 +51,7 @@ export const WithFiltrationAndControlsExample = ({ } - renderItem={({props, data}) => ( + renderItem={({props}) => (
- +
)} renderContainer={renderContainer} diff --git a/src/components/TreeSelect/__stories__/components/WithGroupSelectionControlledStateAndCustomIcon.tsx b/src/components/TreeSelect/__stories__/components/WithGroupSelectionControlledStateAndCustomIcon.tsx index b7229d7b98..0518f155dc 100644 --- a/src/components/TreeSelect/__stories__/components/WithGroupSelectionControlledStateAndCustomIcon.tsx +++ b/src/components/TreeSelect/__stories__/components/WithGroupSelectionControlledStateAndCustomIcon.tsx @@ -6,7 +6,7 @@ import {Button} from '../../../Button'; import {Icon} from '../../../Icon'; import {Flex, spacing} from '../../../layout'; import {ListItemView} from '../../../useList'; -import type {ListItemCommonProps, ListItemId, UseListResult} from '../../../useList'; +import type {ListItemId, ListItemViewContentType, UseListResult} from '../../../useList'; import {createRandomizedData} from '../../../useList/__stories__/utils/makeData'; import {TreeSelect} from '../../TreeSelect'; import type {TreeSelectProps} from '../../types'; @@ -21,18 +21,20 @@ interface CustomDataStructure { export interface WithGroupSelectionControlledStateAndCustomIconExampleProps extends Omit< TreeSelectProps, - 'value' | 'onUpdate' | 'items' | 'mapItemDataToProps' | 'size' + 'value' | 'onUpdate' | 'items' | 'mapItemDataToContentProps' | 'size' > { itemsCount?: number; } -const mapCustomDataStructureToKnownProps = (props: CustomDataStructure): ListItemCommonProps => ({ +const mapCustomDataStructureToKnownProps = ( + props: CustomDataStructure, +): ListItemViewContentType => ({ title: props.a, }); export const WithGroupSelectionControlledStateAndCustomIconExample = ({ itemsCount = 5, - ...props + ...storyProps }: WithGroupSelectionControlledStateAndCustomIconExampleProps) => { // const [value, setValue] = React.useState([]); const [open, setOpen] = React.useState(true); @@ -55,35 +57,30 @@ export const WithGroupSelectionControlledStateAndCustomIconExample = ({ return ( { + renderItem={({id, props, context: {childrenIds}, list}) => { // groups items are selectable too - renderProps.hasSelectionIcon = Boolean(props.multiple); + props.selectionViewType = storyProps.multiple ? 'multiple' : 'single'; return ( - } - endSlot={ - childrenIds ? ( + {...props} + content={{ + ...props.content, + isGroup: false, + startSlot: ( + + ), + endSlot: childrenIds ? ( - ) : undefined - } + ) : undefined, + }} /> ); }} diff --git a/src/components/TreeSelect/__stories__/components/WithItemLinksAndActionsExample.tsx b/src/components/TreeSelect/__stories__/components/WithItemLinksAndActionsExample.tsx index 5fa4174397..c649526331 100644 --- a/src/components/TreeSelect/__stories__/components/WithItemLinksAndActionsExample.tsx +++ b/src/components/TreeSelect/__stories__/components/WithItemLinksAndActionsExample.tsx @@ -18,10 +18,16 @@ function identity(value: T): T { export interface WithItemLinksAndActionsExampleProps extends Omit< TreeSelectProps<{title: string}>, - 'value' | 'onUpdate' | 'items' | 'mapItemDataToProps' | 'size' | 'open' | 'onOpenChange' + | 'value' + | 'onUpdate' + | 'items' + | 'mapItemDataToContentProps' + | 'size' + | 'open' + | 'onOpenChange' > {} -export const WithItemLinksAndActionsExample = (props: WithItemLinksAndActionsExampleProps) => { +export const WithItemLinksAndActionsExample = (storyProps: WithItemLinksAndActionsExampleProps) => { const [value, setValue] = React.useState([]); const [open, setOpen] = React.useState(true); const items = React.useMemo(() => createRandomizedData({num: 10, depth: 1}), []); @@ -39,22 +45,14 @@ export const WithItemLinksAndActionsExample = (props: WithItemLinksAndActionsExa return ( { + renderItem={({id, props, context: {childrenIds}, list}) => { return ( // eslint-disable-next-line jsx-a11y/anchor-is-valid onItemClick(state.id, list)} - endSlot={ - { - e.stopPropagation(); - e.preventDefault(); - }} - items={[ - { - action: (e) => { - e.stopPropagation(); - console.log( - `Clicked by action with id: ${state.id}`, - ); + {...props} + content={{ + ...props.content, + isGroup: false, + endSlot: ( + { + e.stopPropagation(); + e.preventDefault(); + }} + items={[ + { + action: (e) => { + e.stopPropagation(); + console.log( + `Clicked by action with id: ${id}`, + ); + }, + text: 'action 1', }, - text: 'action 1', - }, - ]} - /> - } - startSlot={ - childrenIds ? ( + ]} + /> + ), + startSlot: childrenIds ? ( @@ -108,12 +108,17 @@ export const WithItemLinksAndActionsExample = (props: WithItemLinksAndActionsExa 0 ? {ml: 1} : undefined} + spacing={ + (props.content.indentation ?? 0) > 0 + ? {ml: 1} + : undefined + } > - ) - } + ), + }} + onClick={() => onItemClick(id, list)} /> ); diff --git a/src/components/useList/__stories__/Docs.mdx b/src/components/useList/__stories__/Docs.mdx index a7a6ee20ff..2416c5919c 100644 --- a/src/components/useList/__stories__/Docs.mdx +++ b/src/components/useList/__stories__/Docs.mdx @@ -74,7 +74,7 @@ function List() { {list.structure.items.map((_, i) => { const {props} = getItemRenderState({ id: String(i), - mapItemDataToProps: (title) => ({title}), + mapItemDataToContentProps: (title) => ({title}), onItemClick, list, }); @@ -114,7 +114,7 @@ function List() { {(id) => { const {props} = getItemRenderState({ id: String(i), - mapItemDataToProps: (title) => ({title}), + mapItemDataToContentProps: (title) => ({title}), onItemClick, list, }); diff --git a/src/components/useList/__stories__/components/FlattenList.tsx b/src/components/useList/__stories__/components/FlattenList.tsx index 932e6b4201..ddb2e9d501 100644 --- a/src/components/useList/__stories__/components/FlattenList.tsx +++ b/src/components/useList/__stories__/components/FlattenList.tsx @@ -72,10 +72,10 @@ export const FlattenList = ({itemsCount, size}: FlattenListProps) => { id, size, onItemClick, - mapItemDataToProps: (x) => x, + mapItemDataToContentProps: (x) => x, list, }); - return ; + return ; }} diff --git a/src/components/useList/__stories__/components/InfinityScrollList.tsx b/src/components/useList/__stories__/components/InfinityScrollList.tsx index dfbc1adc55..0fb25c3a54 100644 --- a/src/components/useList/__stories__/components/InfinityScrollList.tsx +++ b/src/components/useList/__stories__/components/InfinityScrollList.tsx @@ -72,7 +72,7 @@ export const InfinityScrollList = ({size}: InfinityScrollListProps) => { size, onItemClick, multiple: true, - mapItemDataToProps: (x) => x, + mapItemDataToContentProps: (x) => x, list, }); const node = ; diff --git a/src/components/useList/__stories__/components/ListWithDnd.tsx b/src/components/useList/__stories__/components/ListWithDnd.tsx index 1ffde3968d..d2fad034a9 100644 --- a/src/components/useList/__stories__/components/ListWithDnd.tsx +++ b/src/components/useList/__stories__/components/ListWithDnd.tsx @@ -77,10 +77,15 @@ export const ListWithDnd = ({size, itemsCount, 'aria-label': ariaLabel}: ListWit id, size, onItemClick, - mapItemDataToProps: (x) => x, + mapItemDataToContentProps: (x) => x, list, }); + console.log( + '🚀 ~ {list.structure.visibleFlattenIds.map ~ props:', + props, + ); + return ( ( , + }} {...provided.draggableProps} {...provided.dragHandleProps} dragging={snapshot.isDragging} ref={provided.innerRef} - endSlot={} role="option" /> )} diff --git a/src/components/useList/__stories__/components/PopupWithTogglerList.tsx b/src/components/useList/__stories__/components/PopupWithTogglerList.tsx index 2a835a7969..a96422f85c 100644 --- a/src/components/useList/__stories__/components/PopupWithTogglerList.tsx +++ b/src/components/useList/__stories__/components/PopupWithTogglerList.tsx @@ -83,11 +83,16 @@ export const PopupWithTogglerList = ({size, itemsCount}: PopupWithTogglerListPro id, size, onItemClick, - mapItemDataToProps: (x) => x, + mapItemDataToContentProps: (x) => x, list, }); - return ; + return ( + + ); }} /> diff --git a/src/components/useList/__stories__/components/RecursiveList.tsx b/src/components/useList/__stories__/components/RecursiveList.tsx index 7a1e1ef61b..fd33c4000b 100644 --- a/src/components/useList/__stories__/components/RecursiveList.tsx +++ b/src/components/useList/__stories__/components/RecursiveList.tsx @@ -58,11 +58,16 @@ export const RecursiveList = ({size, itemsCount, 'aria-label': ariaLabel}: Recur size, onItemClick, multiple: true, - mapItemDataToProps: (x) => x, + mapItemDataToContentProps: (x) => x, list, }); - return ; + return ( + + ); }} /> diff --git a/src/components/useList/__stories__/docs/get-item-render-state.md b/src/components/useList/__stories__/docs/get-item-render-state.md index da4a87325c..99f5e954b1 100644 --- a/src/components/useList/__stories__/docs/get-item-render-state.md +++ b/src/components/useList/__stories__/docs/get-item-render-state.md @@ -19,7 +19,7 @@ const {data, props, context} = getItemRenderState({ multiple: true, size, // list size onItemClick, - mapItemDataToProps: (item) => ({title: item.title}), + mapItemDataToContentProps: (item) => ({title: item.title}), list, }); @@ -28,16 +28,16 @@ return ; #### Props: -| Name | Description | Type | Default | -| :----------------- | :--------------------------------------------------------------------------------- | :------------------------------------------------------------: | :-----: | -| id | `id` of list item | `ListItemId` | | -| list | result of `useList` hook | `UseListResult` | | -| multiple | One or multiple elements selected list | `boolean` | | -| onItemClick | Optional on click handler | `(payload :{id: ListItemId}, e: React.SyntheticEvent) => void` | | -| size | The size of the element. This also affects the rounding radius of the list element | `s \| m \| l \| xl` | `m` | -| mapItemDataToProps | Map list item data (`T`) to `ListItemView` props | `(data: T) => ListItemCommonProps` | | +| Name | Description | Type | Default | +| :------------------------ | :--------------------------------------------------------------------------------- | :------------------------------------------------------------: | :-----: | +| id | `id` of list item | `ListItemId` | | +| list | result of `useList` hook | `UseListResult` | | +| multiple | One or multiple elements selected list | `boolean` | | +| onItemClick | Optional on click handler | `(payload :{id: ListItemId}, e: React.SyntheticEvent) => void` | | +| size | The size of the element. This also affects the rounding radius of the list element | `s \| m \| l \| xl` | `m` | +| mapItemDataToContentProps | Map list item data (`T`) to `ListItemView` `content` prop | `(data: T) => ListItemViewContentProps` | | -##### ListItemCommonProps +##### ListItemViewContentProps | Name | Type | Note | | :-------- | :---------------: | :------: | @@ -83,7 +83,7 @@ const onItemClick = () => {}; multiple: false, size, // list size onItemClick, - mapItemDataToProps, + mapItemDataToContentProps, list, }); diff --git a/src/components/useList/__stories__/docs/list-container-view.md b/src/components/useList/__stories__/docs/list-container-view.md index 46285a926b..49844445d9 100644 --- a/src/components/useList/__stories__/docs/list-container-view.md +++ b/src/components/useList/__stories__/docs/list-container-view.md @@ -17,7 +17,7 @@ The default container for all custom lists. Contains all html attributes and sty const containerRef = React.useRef(null); - - + + ; ``` diff --git a/src/components/useList/__stories__/docs/list-item-view.md b/src/components/useList/__stories__/docs/list-item-view.md index d0b6bbbe70..7db477f06d 100644 --- a/src/components/useList/__stories__/docs/list-item-view.md +++ b/src/components/useList/__stories__/docs/list-item-view.md @@ -30,9 +30,12 @@ const List = () => { } /> ) }} @@ -43,24 +46,30 @@ const List = () => { #### Props: +| Name | Description | Type | Default | +| :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :-------------------------------: | :-----: | +| id | Required prop. Set `[data-list-item="${id}"]` data attribute. By this it core list engine finds elements to scroll to. | `string` | | +| as | If needed, override `html` tag. By default - `li` | `HTMLElement` | `li` | +| size | The size of the element. This also affects the rounding radius of the list element | `s \| m \| l \| xl` | `m` | +| height | The height of the element in pixels. By default, it is calculated depending on the `size` parameter and the presence of the `subtitle` parameter.
Also you can define item height by two variants:
- component props `height`;
- css custom property `--g-list-item-height`; | `number ` | | +| selected | The selected state of the component | `boolean ` | | +| active | The state when the element is in the user's focus, but not selected. It can also be used when you drag an element | `boolean ` | | +| disabled | The disabled state. It also prevents clicking on an element | `boolean ` | | +| activeOnHover | directly control hover behavior | `boolean ` | | +| onClick | On item click callback. If `disabled` option is `true` click don't appears | `() => void` | | +| content | Typed props or ReactNode in difficult cases | `ContentProps \| React.ReactNode` | | + +#### ContentProps + | Name | Description | Type | Default | | :--------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------: | :-----: | -| id | Required prop. Set `[data-list-item="${id}"]` data attribute. By this it core list engine finds elements to scroll to. | `string` | | | title | Base required prop to use. If passed string, applies default component styles according design system. Pass you own component if you wont custom behavior; | `React.ReactNode` | | | subtitle | Slot under `title`. If passed string apply predefined styles. Or you can pass custom `React.ReactNode` to use you own behavior | `React.ReactNode` | | -| as | If needed, override `html` tag. By default - `li` | `HTMLElement` | `li` | -| size | The size of the element. This also affects the rounding radius of the list element | `s \| m \| l \| xl` | `m` | -| height | The height of the element in pixels. By default, it is calculated depending on the `size` parameter and the presence of the `subtitle` parameter | `number ` | | -| selected | The selected state of the component | `boolean ` | | -| active | The state when the element is in the user's focus, but not selected. It can also be used when you drag an element | `boolean ` | | -| disabled | The disabled state. It also prevents clicking on an element | `boolean ` | | -| activeOnHover | directly control hover behavior | `boolean ` | | +| style | Inline styles if needed | `React.CSSProperties` | | +| className | Custom class name to mix with | `string` | | +| dragging | manage view of dragging element. Required for draggable list implementation | `boolean` | | | indentation | Affects the visual indentation of the element content | `number ` | | | hasSelectionIcon | Show selected icon if selected and reserve space for this icon | `boolean ` | | -| onClick | On item click callback. If `disabled` option is `true` click don't appears | `() => void` | | -| startSlot | Custom slot before `title` | `React.ReactNode` | | | endSlot | Custom slot before `title` | `React.ReactNode` | | -| style | Inline styles if needed | `React.CSSProperties` | | -| className | Custom class name to mix with | `string` | | | expanded | Adds a visual representation of a group element if the value is different from `undefined` | `string \| undefined` | | -| dragging | manage view of dragging element. Required for draggable list implementation | `boolean` | | +| startSlot | Custom slot before `title` | `React.ReactNode` | | diff --git a/src/components/useList/__stories__/docs/list-recursive-renderer.md b/src/components/useList/__stories__/docs/list-recursive-renderer.md index 71558379e0..fc5d29b61f 100644 --- a/src/components/useList/__stories__/docs/list-recursive-renderer.md +++ b/src/components/useList/__stories__/docs/list-recursive-renderer.md @@ -49,7 +49,7 @@ function List() { {(id) => { const {props} = getItemRenderState({ id: String(i), - mapItemDataToProps: (title) => ({title}), + mapItemDataToContentProps: (title) => ({title}), onItemClick, list, }); diff --git a/src/components/useList/components/ListItemView/ListItemView.scss b/src/components/useList/components/ListItemView/ListItemView.scss index b589561020..9eb47b09d9 100644 --- a/src/components/useList/components/ListItemView/ListItemView.scss +++ b/src/components/useList/components/ListItemView/ListItemView.scss @@ -4,6 +4,14 @@ $block: '.#{variables.$ns}list-item-view'; #{$block} { flex-shrink: 0; + display: flex; + flex-grow: 1; + align-items: center; + + &__content { + width: 100%; + height: 100%; + } &__main-content { width: 100%; diff --git a/src/components/useList/components/ListItemView/ListItemView.tsx b/src/components/useList/components/ListItemView/ListItemView.tsx index 83caf482ff..8250e1212d 100644 --- a/src/components/useList/components/ListItemView/ListItemView.tsx +++ b/src/components/useList/components/ListItemView/ListItemView.tsx @@ -1,64 +1,53 @@ import React from 'react'; -import {Check, ChevronDown, ChevronUp} from '@gravity-ui/icons'; - -import {Icon} from '../../../Icon'; -import {Text, colorText} from '../../../Text'; -import {Flex, spacing} from '../../../layout'; -import type {FlexProps} from '../../../layout'; +import {spacing} from '../../../layout'; import type {QAProps} from '../../../types'; -import {block} from '../../../utils/cn'; import {LIST_ITEM_DATA_ATR, modToHeight} from '../../constants'; -import type {ListItemCommonProps, ListItemId, ListItemSize} from '../../types'; - -import './ListItemView.scss'; +import type {ListItemId, ListItemSize, ListItemViewContentType} from '../../types'; -const b = block('list-item-view'); +import {ListItemViewContent, isListItemContentPropsGuard} from './ListItemViewContent'; +import {b} from './styles'; -export interface ListItemViewProps - extends QAProps, - ListItemCommonProps { - /** - * Ability to override default html tag - */ - as?: T; +export interface ListItemViewCommonProps extends QAProps { /** * @default `m` */ size?: ListItemSize; - height?: number; - selected?: boolean; - active?: boolean; - disabled?: boolean; /** - * By default hovered elements has active styles. You can disable this behavior + * `[${LIST_ITEM_DATA_ATR}="${id}"]` data attribute to find element. + * For example for scroll to */ - activeOnHover?: boolean; + id: ListItemId; /** - * Build in indentation component to render nested views structure + * Note: if passed and `disabled` option is `true` click will not be appear */ - indentation?: number; + onClick?: React.ComponentPropsWithoutRef['onClick']; + selected?: boolean; + disabled?: boolean; + active?: boolean; + selectionViewType?: 'single' | 'multiple'; + content: ListItemViewContentType; +} + +export interface ListItemViewProps + extends Omit { /** - * Show selected icon if selected and reserve space for this icon + * Ability to override default html tag */ - hasSelectionIcon?: boolean; + as?: T; + height?: number; /** - * Note: if passed and `disabled` option is `true` click will not be appear + * By default hovered elements has active styles. You can disable this behavior */ - onClick?: React.ComponentPropsWithoutRef['onClick']; + activeOnHover?: boolean; style?: React.CSSProperties; className?: string; role?: React.AriaRole; - expanded?: boolean; /** * Add active styles and change selection behavior during dnd is performing */ dragging?: boolean; - /** - * `[${LIST_ITEM_DATA_ATR}="${id}"]` data attribute to find element. - * For example for scroll to - */ - id: ListItemId; + content: ListItemViewContentType | React.ReactNode; } type ListItemViewRef = React.ComponentPropsWithRef['ref']; @@ -66,32 +55,6 @@ type ListItemViewRef = React.ComponentPropsWithRef< type ListItemViewPropsWithTypedAttrs = ListItemViewProps & Omit, keyof ListItemViewProps>; -interface SlotProps extends FlexProps { - indentation?: number; -} - -export const ListItemViewSlot = ({ - children, - indentation: indent = 1, - className, - ...props -}: SlotProps) => { - return ( - - {children} - - ); -}; - -const renderSafeIndentation = (indentation?: number) => { - if (indentation && indentation >= 1) { - return ( - - ); - } - return null; -}; - export const ListItemView = React.forwardRef(function ListItemView< T extends React.ElementType = 'li', >( @@ -102,32 +65,35 @@ export const ListItemView = React.forwardRef(function ListItemView< active, selected, disabled, + selectionViewType = 'multiple', activeOnHover: propsActiveOnHover, className, - hasSelectionIcon = true, - indentation, - startSlot, - subtitle, - endSlot, - title, height, - expanded, dragging, - style, + style: propsStyle, + content, role = 'option', onClick: _onClick, ...rest - }: ListItemViewPropsWithTypedAttrs, + }: ListItemViewProps, ref?: ListItemViewRef, ) { - const as: React.ElementType = asProps || 'li'; - const isGroup = typeof expanded === 'boolean'; + const Tag: React.ElementType = asProps || 'li'; const onClick = disabled ? undefined : _onClick; const activeOnHover = typeof propsActiveOnHover === 'boolean' ? propsActiveOnHover : Boolean(onClick); + const style = { + minHeight: `var(--g-list-item-height, ${ + height ?? + modToHeight[size][ + Number(Boolean(isListItemContentPropsGuard(content) ? content?.subtitle : false)) + ] + }px)`, + ...propsStyle, + }; return ( - - - {hasSelectionIcon && ( - - {selected ? ( - - ) : null} - - )} - - {renderSafeIndentation(indentation)} - - {isGroup ? ( - - ) : null} - - {startSlot} - -
- {typeof title === 'string' ? ( - - {title} - - ) : ( - title - )} - {typeof subtitle === 'string' ? ( - - {subtitle} - - ) : ( - subtitle - )} -
-
- - {endSlot} -
+ {isListItemContentPropsGuard(content) ? ( + + ) : ( + content + )} + ); }) as ({ ref, diff --git a/src/components/useList/components/ListItemView/ListItemViewContent.tsx b/src/components/useList/components/ListItemView/ListItemViewContent.tsx new file mode 100644 index 0000000000..571ac29f61 --- /dev/null +++ b/src/components/useList/components/ListItemView/ListItemViewContent.tsx @@ -0,0 +1,110 @@ +import React from 'react'; + +import {Check, ChevronDown, ChevronUp} from '@gravity-ui/icons'; + +import {Icon} from '../../../Icon'; +import {Text, colorText} from '../../../Text'; +import {Flex} from '../../../layout'; +import type {FlexProps} from '../../../layout'; +import type {ListItemViewContentType} from '../../types'; + +import {b} from './styles'; + +export const isListItemContentPropsGuard = ( + props: ListItemViewContentType | React.ReactNode, +): props is ListItemViewContentType => { + return typeof props === 'object' && props !== null && 'title' in props; +}; + +interface SlotProps extends FlexProps { + indentation?: number; +} + +const ListItemViewSlot = ({children, indentation = 1, className, ...props}: SlotProps) => { + return ( + + {children} + + ); +}; + +const renderSafeIndentation = (indentation?: number) => { + if (indentation && indentation >= 1) { + return ( + + ); + } + return null; +}; + +interface ListItemViewContentProps extends ListItemViewContentType { + selected?: boolean; + disabled?: boolean; + /** + * Show selected icon if selected and reserve space for this icon + */ + hasSelectionIcon: boolean; +} + +export const ListItemViewContent = ({ + startSlot, + subtitle, + endSlot, + disabled, + hasSelectionIcon, + isGroup, + indentation, + expanded, + selected, + title, +}: ListItemViewContentProps) => { + return ( + + + {hasSelectionIcon && ( + + {selected ? ( + + ) : null} + + )} + + {renderSafeIndentation(indentation)} + + {isGroup ? ( + + ) : null} + + {startSlot} + +
+ {typeof title === 'string' ? ( + + {title} + + ) : ( + title + )} + {typeof subtitle === 'string' ? ( + + {subtitle} + + ) : ( + subtitle + )} +
+
+ + {endSlot} +
+ ); +}; diff --git a/src/components/useList/components/ListItemView/__stories__/ListItemView.stories.tsx b/src/components/useList/components/ListItemView/__stories__/ListItemView.stories.tsx index 2a9d5be1ec..2010745208 100644 --- a/src/components/useList/components/ListItemView/__stories__/ListItemView.stories.tsx +++ b/src/components/useList/components/ListItemView/__stories__/ListItemView.stories.tsx @@ -9,6 +9,7 @@ import {Flex, sp} from '../../../../layout'; import type {ListItemId} from '../../../../useList/types'; import {ListItemView as ListItemViewComponent} from '../ListItemView'; import type {ListItemViewProps} from '../ListItemView'; +import {isListItemContentPropsGuard} from '../ListItemViewContent'; export default { title: 'Lab/useList/ListItemView', @@ -72,128 +73,164 @@ const EndSlot = ({selfStart}: {selfStart?: boolean}) => ( const stories: ListItemViewProps[] = [ { id: '1', - title, - activeOnHover: false, - subtitle, + content: { + title, + subtitle, + startSlot: , + }, disabled: true, - startSlot: , + activeOnHover: false, }, { id: '2', - title, - subtitle: 'activeOnHover - false', + content: { + title, + subtitle: 'activeOnHover - false', + endSlot: , + }, + selected: true, activeOnHover: false, - endSlot: , }, { id: '3', - title, + selectionViewType: 'single', + content: { + title, + subtitle, + startSlot: , + }, + selected: true, size: 'l', - subtitle, - hasSelectionIcon: false, - startSlot: , }, { id: '4', - title, + content: { + title, + startSlot: , + }, disabled: true, size: 'xl', height: 60, - startSlot: , }, { id: '5', size: 'l', - startSlot: , - title, + content: { + startSlot: , + title, + }, }, { id: '6', - title: 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex quos officiis, voluptates nobis doloribus veritatis quo odit sequi eligendi aliquam quasi officia qui deserunt autem quas necessitatibus nam possimus aperiam.', - size: 'l', - subtitle: 'indentation 1', - startSlot: , - indentation: 1, + content: { + title: 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex quos officiis, voluptates nobis doloribus veritatis quo odit sequi eligendi aliquam quasi officia qui deserunt autem quas necessitatibus nam possimus aperiam.', + subtitle: 'indentation 1', + startSlot: , + indentation: 1, + endSlot: , + }, selected: true, - endSlot: , + size: 'l', }, { id: '7', - expanded: true, size: 'xl', - title: 'Group 1', - endSlot: , + content: { + isGroup: true, + expanded: true, + title: 'Group 1', + endSlot: , + }, }, { id: '8', - hasSelectionIcon: false, - expanded: true, + selectionViewType: 'single', + content: { + title: 'Group 1', + expanded: true, + isGroup: true, + }, disabled: true, size: 'xl', - title: 'Group 1', }, { id: '9', - hasSelectionIcon: false, - title: ( - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex quos officiis, - voluptates nobis doloribus veritatis quo odit sequi eligendi aliquam quasi officia - qui deserunt autem quas necessitatibus nam possimus aperiam. - - ), + selectionViewType: 'single', + content: { + subtitle: ( + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex quos officiis, + voluptates nobis doloribus veritatis quo odit sequi eligendi aliquam quasi + officia qui deserunt autem quas necessitatibus nam possimus aperiam. + + ), + startSlot: , + title: ( + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex quos officiis, + voluptates nobis doloribus veritatis quo odit sequi eligendi aliquam quasi + officia qui deserunt autem quas necessitatibus nam possimus aperiam. + + ), + endSlot: , + }, size: 'l', - subtitle: ( - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex quos officiis, - voluptates nobis doloribus veritatis quo odit sequi eligendi aliquam quasi officia - qui deserunt autem quas necessitatibus nam possimus aperiam. - - ), - startSlot: , selected: true, className: sp({p: 2}), - endSlot: , }, { id: '10', - title: ( - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex quos officiis, - voluptates nobis doloribus veritatis quo odit sequi eligendi aliquam quasi officia - qui deserunt autem quas necessitatibus nam possimus aperiam. - - ), - size: 'l', - subtitle: ( - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex quos officiis, - voluptates nobis doloribus veritatis quo odit sequi eligendi aliquam quasi officia - qui deserunt autem quas necessitatibus nam possimus aperiam. - - ), - startSlot: , + content: { + title: ( + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex quos officiis, + voluptates nobis doloribus veritatis quo odit sequi eligendi aliquam quasi + officia qui deserunt autem quas necessitatibus nam possimus aperiam. + + ), + subtitle: ( + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex quos officiis, + voluptates nobis doloribus veritatis quo odit sequi eligendi aliquam quasi + officia qui deserunt autem quas necessitatibus nam possimus aperiam. + + ), + startSlot: , + indentation: 1, + endSlot: , + }, selected: true, - indentation: 1, + size: 'l', className: sp({p: 2}), - endSlot: , }, { id: '11', - title: 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex quos officiis, voluptates nobis doloribus veritatis quo odit sequi eligendi aliquam quasi officia qui deserunt autem quas necessitatibus nam possimus aperiam.', size: 'l', - subtitle: ( - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex quos officiis, - voluptates nobis doloribus veritatis quo odit sequi eligendi aliquam quasi officia - qui deserunt autem quas necessitatibus nam possimus aperiam. - + content: { + title: 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex quos officiis, voluptates nobis doloribus veritatis quo odit sequi eligendi aliquam quasi officia qui deserunt autem quas necessitatibus nam possimus aperiam.', + subtitle: ( + + Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex quos officiis, + voluptates nobis doloribus veritatis quo odit sequi eligendi aliquam quasi + officia qui deserunt autem quas necessitatibus nam possimus aperiam. + + ), + expanded: true, + isGroup: true, + startSlot: , + indentation: 1, + endSlot: , + }, + }, + { + id: '12', + size: 'l', + content: ( + + + Override list item context with react node + ), - expanded: true, - startSlot: , - indentation: 1, - selected: true, - endSlot: , }, ]; @@ -202,35 +239,55 @@ const ListItemViewTemplate: StoryFn = () => { const [selectedById, setSelectedById] = React.useState>({}); return ( - - {stories.map((props, i) => ( - - ))} + + {stories.map((props, i) => { + let expanded: boolean | undefined; + + if (isListItemContentPropsGuard(props.content) && props.content.isGroup) { + expanded = + props.id in expandedById ? expandedById[props.id] : props.content.expanded; + } + + return ( + + ); + })} ); - function handleClick({id, expanded}: ListItemViewProps) { - const isGroup = typeof expanded === 'boolean'; + function handleClick({id, content}: ListItemViewProps) { + if (isListItemContentPropsGuard(content)) { + const isGroup = content.isGroup; + + return () => { + if (isGroup) { + setExpandedById((prevState) => ({ + ...prevState, + [id]: id in prevState ? !prevState[id] : !content.expanded, + })); + } else { + setSelectedById((prevState) => ({ + ...prevState, + [id]: !prevState[id], + })); + } + }; + } - return () => { - if (isGroup) { - setExpandedById((prevState) => ({ - ...prevState, - [id]: typeof prevState[id] === 'undefined' ? !expanded : !prevState[id], - })); - } else { - setSelectedById((prevState) => ({ - ...prevState, - [id]: !prevState[id], - })); - } - }; + return undefined; } }; export const ListItemView = ListItemViewTemplate.bind({}); diff --git a/src/components/useList/components/ListItemView/index.ts b/src/components/useList/components/ListItemView/index.ts index 8b2f736a59..0df3d97ab0 100644 --- a/src/components/useList/components/ListItemView/index.ts +++ b/src/components/useList/components/ListItemView/index.ts @@ -1,2 +1,3 @@ export {ListItemView} from './ListItemView'; -export type {ListItemViewProps} from './ListItemView'; +export {isListItemContentPropsGuard} from './ListItemViewContent'; +export type {ListItemViewProps, ListItemViewCommonProps} from './ListItemView'; diff --git a/src/components/useList/components/ListItemView/styles.ts b/src/components/useList/components/ListItemView/styles.ts new file mode 100644 index 0000000000..23774472a7 --- /dev/null +++ b/src/components/useList/components/ListItemView/styles.ts @@ -0,0 +1,5 @@ +import {block} from '../../../utils/cn'; + +import './ListItemView.scss'; + +export const b = block('list-item-view'); diff --git a/src/components/useList/types.ts b/src/components/useList/types.ts index 5afa809406..6af991e373 100644 --- a/src/components/useList/types.ts +++ b/src/components/useList/types.ts @@ -1,5 +1,3 @@ -import type {QAProps} from '../types'; - export type ListItemId = string; export type ListItemSize = 's' | 'm' | 'l' | 'xl'; @@ -41,11 +39,17 @@ export type ItemState = { indentation: number; }; -export type ListItemCommonProps = { +export type ListItemViewContentType = { title: React.ReactNode; subtitle?: React.ReactNode; startSlot?: React.ReactNode; endSlot?: React.ReactNode; + /** + * Build in indentation component to render nested views structure + */ + indentation?: number; + isGroup?: boolean; + expanded?: boolean; }; export type ListItemListContextProps = ItemState & @@ -53,19 +57,6 @@ export type ListItemListContextProps = ItemState & isLastItem: boolean; }; -export type RenderItemProps = { - size: ListItemSize; - id: ListItemId; - onClick: ((e: React.SyntheticEvent) => void) | undefined; - selected: boolean | undefined; - disabled: boolean; - expanded: boolean | undefined; - active: boolean; - indentation: number; - hasSelectionIcon?: boolean; -} & ListItemCommonProps & - QAProps; - export type ParsedState = { /** * Stored internal meta info about item diff --git a/src/components/useList/utils/getItemRenderState.tsx b/src/components/useList/utils/getItemRenderState.tsx index c66cc05fb6..31aa525412 100644 --- a/src/components/useList/utils/getItemRenderState.tsx +++ b/src/components/useList/utils/getItemRenderState.tsx @@ -1,12 +1,12 @@ /* eslint-disable valid-jsdoc */ import type {QAProps} from '../../types'; +import type {ListItemViewCommonProps} from '../components/ListItemView'; import type { - ListItemCommonProps, ListItemId, ListItemListContextProps, ListItemSize, + ListItemViewContentType, ListOnItemClick, - RenderItemProps, UseListResult, } from '../types'; @@ -19,7 +19,7 @@ type ItemRendererProps = QAProps & { */ multiple?: boolean; id: ListItemId; - mapItemDataToProps(data: T): ListItemCommonProps; + mapItemDataToContentProps(data: T): ListItemViewContentType; onItemClick?: ListOnItemClick; list: UseListResult; }; @@ -31,7 +31,7 @@ export const getItemRenderState = ({ qa, list, onItemClick, - mapItemDataToProps, + mapItemDataToContentProps, size = 'm', multiple = false, id, @@ -43,24 +43,20 @@ export const getItemRenderState = ({ id === list.structure.visibleFlattenIds[list.structure.visibleFlattenIds.length - 1], }; - let expanded; // `undefined` value means than tree list will look as nested list without groups - - // isGroup - if (list.state.expandedById && id in list.state.expandedById) { - expanded = list.state.expandedById[id]; - } - - const props: RenderItemProps = { + const props: ListItemViewCommonProps = { id, size, - expanded, - active: id === list.state.activeItemId, - indentation: context.indentation, - disabled: Boolean(list.state.disabledById?.[id]), selected: Boolean(list.state.selectedById[id]), - hasSelectionIcon: Boolean(multiple) && !context.childrenIds, // hide multiple selection view at group nodes + disabled: Boolean(list.state.disabledById?.[id]), + active: id === list.state.activeItemId, onClick: onItemClick ? (e: React.SyntheticEvent) => onItemClick({id}, e) : undefined, - ...mapItemDataToProps(list.structure.itemsById[id]), + selectionViewType: Boolean(multiple) && !context.childrenIds ? 'multiple' : 'single', // no multiple selection at group nodes + content: { + expanded: list.state.expandedById?.[id], + indentation: context.indentation, + isGroup: list.state.expandedById && id in list.state.expandedById, + ...mapItemDataToContentProps(list.structure.itemsById[id]), + }, }; if (qa) {