From b6516f46ce157a1297a5887e25487f621da07b43 Mon Sep 17 00:00:00 2001 From: Isaev Alexandr Date: Wed, 23 Oct 2024 14:51:51 +0300 Subject: [PATCH] feat(ListItemExandIcon): added new list component for expanded icon view (#1762) --- .../WithFiltrationAndControlsStory.tsx | 5 +- .../WithGroupSelectionAndCustomIconStory.tsx | 20 ++++---- .../stories/WithItemLinksAndActionsStory.tsx | 16 +++---- .../WithFiltrationAndControlsExample.tsx | 5 +- ...pSelectionControlledStateAndCustomIcon.tsx | 14 +++--- .../WithItemLinksAndActionsExample.tsx | 16 +++---- src/components/useList/__stories__/Docs.mdx | 31 +++++++++++- .../ListItemExpandIcon.scss | 7 +++ .../ListItemExpandIcon/ListItemExpandIcon.tsx | 48 +++++++++++++++++++ .../ListItemExpandIcon.stories.tsx | 40 ++++++++++++++++ .../__stories__/list-item-expand-icon.md | 48 +++++++++++++++++++ .../components/ListItemExpandIcon/index.ts | 2 + .../components/ListItemView/ListItemView.scss | 4 -- .../ListItemView/ListItemViewContent.tsx | 26 ++++++---- .../__stories__/ListItemView.stories.tsx | 33 ++++++++++++- src/components/useList/hooks/useListFilter.ts | 2 +- src/components/useList/index.ts | 1 + src/components/useList/types.ts | 23 +++++++++ src/unstable.ts | 2 + 19 files changed, 288 insertions(+), 55 deletions(-) create mode 100644 src/components/useList/components/ListItemExpandIcon/ListItemExpandIcon.scss create mode 100644 src/components/useList/components/ListItemExpandIcon/ListItemExpandIcon.tsx create mode 100644 src/components/useList/components/ListItemExpandIcon/__stories__/ListItemExpandIcon.stories.tsx create mode 100644 src/components/useList/components/ListItemExpandIcon/__stories__/list-item-expand-icon.md create mode 100644 src/components/useList/components/ListItemExpandIcon/index.ts diff --git a/src/components/TreeList/__stories__/stories/WithFiltrationAndControlsStory.tsx b/src/components/TreeList/__stories__/stories/WithFiltrationAndControlsStory.tsx index 6c77a72dc6..4b6ce1a6f6 100644 --- a/src/components/TreeList/__stories__/stories/WithFiltrationAndControlsStory.tsx +++ b/src/components/TreeList/__stories__/stories/WithFiltrationAndControlsStory.tsx @@ -4,11 +4,10 @@ import {Button} from '../../../Button'; import {Text} from '../../../Text'; import {TextInput} from '../../../controls'; import {Flex, spacing} from '../../../layout'; -import {useList, useListFilter} from '../../../useList'; +import {ListContainer, useList, useListFilter} from '../../../useList'; import {createRandomizedData} from '../../../useList/__stories__/utils/makeData'; import {TreeList} from '../../TreeList'; import type {TreeListContainerProps, TreeListProps} from '../../types'; -import {RenderVirtualizedContainer} from '../components/RenderVirtualizedContainer'; interface Entity { title: string; @@ -37,7 +36,7 @@ export const WithFiltrationAndControlsStory = ({ ); } - return ; + return ; }; return {items: baseItems, renderContainer: containerRenderer}; diff --git a/src/components/TreeList/__stories__/stories/WithGroupSelectionAndCustomIconStory.tsx b/src/components/TreeList/__stories__/stories/WithGroupSelectionAndCustomIconStory.tsx index 3b1e011016..09665d3617 100644 --- a/src/components/TreeList/__stories__/stories/WithGroupSelectionAndCustomIconStory.tsx +++ b/src/components/TreeList/__stories__/stories/WithGroupSelectionAndCustomIconStory.tsx @@ -1,11 +1,11 @@ import React from 'react'; -import {ChevronDown, ChevronUp, Database, PlugConnection} from '@gravity-ui/icons'; +import {Database, PlugConnection} from '@gravity-ui/icons'; import {Button} from '../../../Button'; import {Icon} from '../../../Icon'; -import {Flex, spacing} from '../../../layout'; -import {ListItemView, useList} from '../../../useList'; +import {Flex} from '../../../layout'; +import {ListItemExpandIcon, ListItemView, useList} from '../../../useList'; import type {ListItemId, ListItemViewContentType} from '../../../useList'; import {createRandomizedData} from '../../../useList/__stories__/utils/makeData'; import {TreeList} from '../../TreeList'; @@ -84,8 +84,6 @@ export const WithGroupSelectionAndCustomIconStory = ({ endSlot: childrenIds ? ( ) : undefined, }} diff --git a/src/components/TreeList/__stories__/stories/WithItemLinksAndActionsStory.tsx b/src/components/TreeList/__stories__/stories/WithItemLinksAndActionsStory.tsx index 58fb2fad2a..c0eb89795c 100644 --- a/src/components/TreeList/__stories__/stories/WithItemLinksAndActionsStory.tsx +++ b/src/components/TreeList/__stories__/stories/WithItemLinksAndActionsStory.tsx @@ -1,12 +1,12 @@ import React from 'react'; -import {ChevronDown, ChevronUp, FolderOpen} from '@gravity-ui/icons'; +import {FolderOpen} from '@gravity-ui/icons'; import {Button} from '../../../Button'; import {DropdownMenu} from '../../../DropdownMenu'; import {Icon} from '../../../Icon'; import {Flex} from '../../../layout'; -import {ListItemView, useList} from '../../../useList'; +import {ListItemExpandIcon, ListItemView, useList} from '../../../useList'; import type {ListItemId} from '../../../useList'; import {createRandomizedData} from '../../../useList/__stories__/utils/makeData'; import {TreeList} from '../../TreeList'; @@ -96,12 +96,12 @@ export const WithItemLinksAndActionsStory = (props: WithItemLinksAndActionsStory : expandButtonLabel, }} > - + + + ) : ( ; + return ; }; return {items: baseItems, renderContainer: containerRenderer}; diff --git a/src/components/TreeSelect/__stories__/components/WithGroupSelectionControlledStateAndCustomIcon.tsx b/src/components/TreeSelect/__stories__/components/WithGroupSelectionControlledStateAndCustomIcon.tsx index 0518f155dc..7ae8af00b1 100644 --- a/src/components/TreeSelect/__stories__/components/WithGroupSelectionControlledStateAndCustomIcon.tsx +++ b/src/components/TreeSelect/__stories__/components/WithGroupSelectionControlledStateAndCustomIcon.tsx @@ -1,11 +1,11 @@ import React from 'react'; -import {ChevronDown, ChevronUp, Database, PlugConnection} from '@gravity-ui/icons'; +import {Database, PlugConnection} from '@gravity-ui/icons'; import {Button} from '../../../Button'; import {Icon} from '../../../Icon'; import {Flex, spacing} from '../../../layout'; -import {ListItemView} from '../../../useList'; +import {ListItemExpandIcon, ListItemView} from '../../../useList'; import type {ListItemId, ListItemViewContentType, UseListResult} from '../../../useList'; import {createRandomizedData} from '../../../useList/__stories__/utils/makeData'; import {TreeSelect} from '../../TreeSelect'; @@ -92,10 +92,12 @@ export const WithGroupSelectionControlledStateAndCustomIconExample = ({ })); }} > - + + + ) : undefined, }} diff --git a/src/components/TreeSelect/__stories__/components/WithItemLinksAndActionsExample.tsx b/src/components/TreeSelect/__stories__/components/WithItemLinksAndActionsExample.tsx index c649526331..ef37079c37 100644 --- a/src/components/TreeSelect/__stories__/components/WithItemLinksAndActionsExample.tsx +++ b/src/components/TreeSelect/__stories__/components/WithItemLinksAndActionsExample.tsx @@ -1,12 +1,12 @@ import React from 'react'; -import {ChevronDown, ChevronUp, FolderOpen} from '@gravity-ui/icons'; +import {FolderOpen} from '@gravity-ui/icons'; import {Button} from '../../../Button'; import {DropdownMenu} from '../../../DropdownMenu'; import {Icon} from '../../../Icon'; import {Flex} from '../../../layout'; -import {ListItemView} from '../../../useList'; +import {ListItemExpandIcon, ListItemView} from '../../../useList'; import type {ListItemId, UseListResult} from '../../../useList'; import {createRandomizedData} from '../../../useList/__stories__/utils/makeData'; import {TreeSelect} from '../../TreeSelect'; @@ -97,12 +97,12 @@ export const WithItemLinksAndActionsExample = (storyProps: WithItemLinksAndActio })); }} > - + + + ) : ( # UseList hooks and components @@ -32,6 +42,7 @@ The basic idea is that hooks take all the complex logic on themselves, and all y ### Components (View only): - [ListItemView](#listitemview); +- [ListItemExpandIcon](#listitemexpandicon); - [ListContainerView](#listcontainerview); - [ListRecursiveRenderer](#listrecursiverenderer); @@ -144,6 +155,24 @@ function List() { {ListRecursiveRenderer} + ( + + ), + ListItemExpandIconInsideButton: () => ( + + ), + }, + }} +> + {ListItemExpandIcon} + + ## Utilities {GetListItemClickHandler} diff --git a/src/components/useList/components/ListItemExpandIcon/ListItemExpandIcon.scss b/src/components/useList/components/ListItemExpandIcon/ListItemExpandIcon.scss new file mode 100644 index 0000000000..0054cd1597 --- /dev/null +++ b/src/components/useList/components/ListItemExpandIcon/ListItemExpandIcon.scss @@ -0,0 +1,7 @@ +@use '../../../variables'; + +$block: '.#{variables.$ns}list-item-expand-icon'; + +#{$block} { + flex-shrink: 0; +} diff --git a/src/components/useList/components/ListItemExpandIcon/ListItemExpandIcon.tsx b/src/components/useList/components/ListItemExpandIcon/ListItemExpandIcon.tsx new file mode 100644 index 0000000000..70c2a5f097 --- /dev/null +++ b/src/components/useList/components/ListItemExpandIcon/ListItemExpandIcon.tsx @@ -0,0 +1,48 @@ +import React from 'react'; + +import type {ArrowToggleProps} from '../../../ArrowToggle'; +import {ArrowToggle} from '../../../ArrowToggle'; +import {colorText} from '../../../Text'; +import {block} from '../../../utils/cn'; +import type {ListItemExpandIconRenderProps} from '../../types'; + +import './ListItemExpandIcon.scss'; + +const b = block('list-item-expand-icon'); + +export interface ListItemExpandIconProps extends ListItemExpandIconRenderProps {} + +export const ListItemExpandIcon = ({ + expanded, + behavior = 'action', + disabled, +}: ListItemExpandIconProps) => { + return ( + + ); +}; + +function getIconDirection({ + behavior, + expanded, +}: Pick): ArrowToggleProps['direction'] { + if (expanded && behavior === 'action') { + return 'top'; + } else if (expanded && behavior === 'state') { + return 'bottom'; + } else if (expanded && behavior === 'state-inverse') { + return 'bottom'; + } else if (behavior === 'action') { + return 'bottom'; + } else if (behavior === 'state') { + return 'right'; + } else if (behavior === 'state-inverse') { + return 'left'; + } + + return 'bottom'; +} diff --git a/src/components/useList/components/ListItemExpandIcon/__stories__/ListItemExpandIcon.stories.tsx b/src/components/useList/components/ListItemExpandIcon/__stories__/ListItemExpandIcon.stories.tsx new file mode 100644 index 0000000000..c91fbfe361 --- /dev/null +++ b/src/components/useList/components/ListItemExpandIcon/__stories__/ListItemExpandIcon.stories.tsx @@ -0,0 +1,40 @@ +import React from 'react'; + +import type {Meta, StoryObj} from '@storybook/react'; + +import {Button} from '../../../../Button'; +import type {ListItemExpandIconProps} from '../ListItemExpandIcon'; +import {ListItemExpandIcon} from '../ListItemExpandIcon'; + +const meta: Meta = { + title: 'Lab/useList/ListItemExpandIcon', + component: ListItemExpandIcon, +}; + +export default meta; + +type Story = StoryObj; + +const DefaultExample = (props: ListItemExpandIconProps) => { + return ; +}; + +export const Default = { + render: DefaultExample, +} satisfies Story; + +const InsideButtonExample = (props: ListItemExpandIconProps) => { + const [expanded, setExpanded] = React.useState(false); + + return ( + + ); +}; + +export const InsideButton = { + render: InsideButtonExample, +} satisfies Story; diff --git a/src/components/useList/components/ListItemExpandIcon/__stories__/list-item-expand-icon.md b/src/components/useList/components/ListItemExpandIcon/__stories__/list-item-expand-icon.md new file mode 100644 index 0000000000..e25b6502bf --- /dev/null +++ b/src/components/useList/components/ListItemExpandIcon/__stories__/list-item-expand-icon.md @@ -0,0 +1,48 @@ +### ListItemExpandIcon + +Base group expand icon view + +#### Import + +```tsx +import { + type unstable_ListItemExpandIconProps as ListItemExpandIconProps, + unstable_ListItemExpandIcon as ListItemExpandIcon, +} from '@gravity-ui/uikit/unstable'; +``` + +#### Base example: + +```jsx +const DefaultExample = (props: ListItemExpandIconProps) => { + return ; +}; +``` + + + +#### Render icon inside Button component: + +```jsx +const InsideButtonExample = (props: ListItemExpandIconProps) => { + const [expanded, setExpanded] = React.useState(false); + + return ( + + ); +}; +``` + + + +#### Props + +| Name | Description | Type | Default | +| :------- | :---------------------------- | :-------------------------------: | :-----: | +| expanded | icon state | `boolean` | | +| disabled | disabled view type | `boolean` | | +| behavior | The behavior of the component | `state`, `state-inverse`,`action` | | diff --git a/src/components/useList/components/ListItemExpandIcon/index.ts b/src/components/useList/components/ListItemExpandIcon/index.ts new file mode 100644 index 0000000000..4f49c3df30 --- /dev/null +++ b/src/components/useList/components/ListItemExpandIcon/index.ts @@ -0,0 +1,2 @@ +export {ListItemExpandIcon} from './ListItemExpandIcon'; +export type {ListItemExpandIconProps} from './ListItemExpandIcon'; diff --git a/src/components/useList/components/ListItemView/ListItemView.scss b/src/components/useList/components/ListItemView/ListItemView.scss index 9eb47b09d9..7e2b8a3ac6 100644 --- a/src/components/useList/components/ListItemView/ListItemView.scss +++ b/src/components/useList/components/ListItemView/ListItemView.scss @@ -56,10 +56,6 @@ $block: '.#{variables.$ns}list-item-view'; border-radius: var(--g-list-item-border-radius, 8px); } - &__icon { - flex-shrink: 0; - } - &__slot { flex-shrink: 0; } diff --git a/src/components/useList/components/ListItemView/ListItemViewContent.tsx b/src/components/useList/components/ListItemView/ListItemViewContent.tsx index 571ac29f61..38332fcb45 100644 --- a/src/components/useList/components/ListItemView/ListItemViewContent.tsx +++ b/src/components/useList/components/ListItemView/ListItemViewContent.tsx @@ -1,12 +1,13 @@ import React from 'react'; -import {Check, ChevronDown, ChevronUp} from '@gravity-ui/icons'; +import {Check} 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 {ListItemExpandIcon} from '../ListItemExpandIcon/ListItemExpandIcon'; import {b} from './styles'; @@ -57,7 +58,17 @@ export const ListItemViewContent = ({ expanded, selected, title, + expandIconPlacement = 'start', + renderExpandIcon: RenderExpandIcon = ListItemExpandIcon, }: ListItemViewContentProps) => { + const expandIconNode = isGroup ? ( + + ) : null; + return ( @@ -72,13 +83,7 @@ export const ListItemViewContent = ({ {renderSafeIndentation(indentation)} - {isGroup ? ( - - ) : null} + {expandIconPlacement === 'start' && expandIconNode} {startSlot} @@ -104,7 +109,10 @@ export const ListItemViewContent = ({ - {endSlot} + + {expandIconPlacement === 'end' && expandIconNode} + {endSlot} + ); }; diff --git a/src/components/useList/components/ListItemView/__stories__/ListItemView.stories.tsx b/src/components/useList/components/ListItemView/__stories__/ListItemView.stories.tsx index 2010745208..7b95681e4d 100644 --- a/src/components/useList/components/ListItemView/__stories__/ListItemView.stories.tsx +++ b/src/components/useList/components/ListItemView/__stories__/ListItemView.stories.tsx @@ -3,10 +3,12 @@ import React from 'react'; import type {Meta, StoryFn} from '@storybook/react'; import {Avatar} from '../../../../Avatar'; +import {Button} from '../../../../Button'; import {DropdownMenu} from '../../../../DropdownMenu'; import {Text} from '../../../../Text'; import {Flex, sp} from '../../../../layout'; import type {ListItemId} from '../../../../useList/types'; +import {ListItemExpandIcon} from '../../ListItemExpandIcon'; import {ListItemView as ListItemViewComponent} from '../ListItemView'; import type {ListItemViewProps} from '../ListItemView'; import {isListItemContentPropsGuard} from '../ListItemViewContent'; @@ -207,7 +209,7 @@ const stories: ListItemViewProps[] = [ id: '11', size: 'l', 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.', + title: 'With disable expand icon transition. 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, @@ -232,6 +234,35 @@ const stories: ListItemViewProps[] = [ ), }, + { + id: '13', + size: 'l', + content: { + title, + startSlot: , + indentation: 1, + isGroup: true, + expanded: false, + expandIconPlacement: 'end', + }, + }, + { + id: '14', + size: 'l', + content: { + title: 'Custom icon end', + isGroup: true, + expanded: false, + expandIconPlacement: 'end', + renderExpandIcon: (props) => ( + + ), + }, + }, ]; const ListItemViewTemplate: StoryFn = () => { diff --git a/src/components/useList/hooks/useListFilter.ts b/src/components/useList/hooks/useListFilter.ts index e552de961d..0ba087fb1f 100644 --- a/src/components/useList/hooks/useListFilter.ts +++ b/src/components/useList/hooks/useListFilter.ts @@ -18,7 +18,7 @@ interface UseListFilterProps { */ filterItems?(value: string, items: ListItemType[]): ListItemType[]; /** - * Override only logic with item affiliation + * Override only logic with item filtration */ filterItem?(value: string, item: T): boolean; onFilterChange?(value: string): void; diff --git a/src/components/useList/index.ts b/src/components/useList/index.ts index 0f8ff3ce06..a39d735769 100644 --- a/src/components/useList/index.ts +++ b/src/components/useList/index.ts @@ -3,6 +3,7 @@ export * from './hooks/useList'; export * from './hooks/useListKeydown'; export * from './types'; export * from './components/ListItemView'; +export * from './components/ListItemExpandIcon'; export * from './components/ListRecursiveRenderer'; export * from './components/ListContainerView'; export * from './components/ListContainer'; diff --git a/src/components/useList/types.ts b/src/components/useList/types.ts index 8aa8a22f6f..9ecbe3d0f6 100644 --- a/src/components/useList/types.ts +++ b/src/components/useList/types.ts @@ -39,6 +39,18 @@ export type ItemState = { indentation: number; }; +export interface ListItemExpandIconRenderProps { + /** + * The behavior of the component: + * + * - action - to indicate user actions. For example, for an icon inside a button; + * - state - to indicate the current state of the element; + */ + behavior: 'state' | 'state-inverse' | 'action'; + expanded?: boolean; + disabled?: boolean; +} + export type ListItemViewContentType = { title: React.ReactNode; subtitle?: React.ReactNode; @@ -49,7 +61,18 @@ export type ListItemViewContentType = { */ indentation?: number; isGroup?: boolean; + /** + * Required prop if `isGroup` - `true` + */ expanded?: boolean; + /** + * @default - 'start' + */ + expandIconPlacement?: 'start' | 'end'; + /** + * Will be applied if `isGroup` props is `true` + */ + renderExpandIcon?(props: ListItemExpandIconRenderProps): React.ReactNode; }; export type ListItemListContextProps = ItemState & diff --git a/src/unstable.ts b/src/unstable.ts index 18fdbba59f..e68da813c7 100644 --- a/src/unstable.ts +++ b/src/unstable.ts @@ -5,7 +5,9 @@ export { useListKeydown as unstable_useListKeydown, getListItemClickHandler as unstable_getListItemClickHandler, ListItemView as unstable_ListItemView, + ListItemExpandIcon as unstable_ListItemExpandIcon, type ListItemViewProps as unstable_ListItemViewProps, + type ListItemExpandIconProps as unstable_ListItemExpandIconProps, ListContainerView as unstable_ListContainerView, type ListContainerProps as unstable_ListContainerProps, ListContainer as unstable_ListContainer,