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..54f28ac658 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..996f864e78 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,7 @@ 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..cdc0d85360 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,9 +92,9 @@ export const WithGroupSelectionControlledStateAndCustomIconExample = ({ })); }} > - ) : undefined, diff --git a/src/components/TreeSelect/__stories__/components/WithItemLinksAndActionsExample.tsx b/src/components/TreeSelect/__stories__/components/WithItemLinksAndActionsExample.tsx index c649526331..4f6f056828 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,7 @@ 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..c8b765106b --- /dev/null +++ b/src/components/useList/components/ListItemExpandIcon/ListItemExpandIcon.scss @@ -0,0 +1,26 @@ +@use '../../../variables'; + +$block: '.#{variables.$ns}list-item-expand-icon'; + +#{$block} { + flex-shrink: 0; + + transition: transform 0.1s ease; + + &_disableTransition { + transition: none; + } + + &_position_start { + transform: rotate(-90deg); + } + + &_position_end { + transform: rotate(180deg); + transition: transform 0.2s ease; + } + + &_expanded { + transform: rotate(0deg); + } +} diff --git a/src/components/useList/components/ListItemExpandIcon/ListItemExpandIcon.tsx b/src/components/useList/components/ListItemExpandIcon/ListItemExpandIcon.tsx new file mode 100644 index 0000000000..36b7db216e --- /dev/null +++ b/src/components/useList/components/ListItemExpandIcon/ListItemExpandIcon.tsx @@ -0,0 +1,35 @@ +import React from 'react'; + +import {ChevronDown} from '@gravity-ui/icons'; + +import {Icon} from '../../../Icon'; +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 Partial {} + +export const ListItemExpandIcon = ({ + expanded, + disableTransition, + position = 'start', + disabled, +}: ListItemExpandIconProps) => { + return ( + + ); +}; + +// For correct rendering inside `Button` component +ListItemExpandIcon.displayName = 'Icon'; 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..0eab82650c --- /dev/null +++ b/src/components/useList/components/ListItemExpandIcon/__stories__/ListItemExpandIcon.stories.tsx @@ -0,0 +1,72 @@ +import React from 'react'; + +import type {Meta, StoryObj} from '@storybook/react'; + +import {Button} from '../../../../Button'; +import {RadioButton} from '../../../../RadioButton'; +import {Text} from '../../../../Text'; +import {Flex} from '../../../../layout'; +import type {ListItemExpandIconProps} from '../ListItemExpandIcon'; +import {ListItemExpandIcon} from '../ListItemExpandIcon'; + +const meta: Meta = { + title: 'Lab/useList/ListItemExpandIcon', + component: ListItemExpandIcon, +}; + +export default meta; + +type Story = StoryObj; + +export const Default = { + render: (args) => ( + + + Position: start + + + + + + + Position: start + + + + + + + ), +} satisfies Story; + +const InsideButtonExample = (props: ListItemExpandIconProps) => { + const [expanded, setExpanded] = React.useState(false); + const [position, setPosition] = React.useState<'start' | 'end'>('start'); + + return ( + + + Icon position: + + + + Click on button to change state: + + + + ); +}; + +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..dc62d2fc6e --- /dev/null +++ b/src/components/useList/components/ListItemExpandIcon/__stories__/list-item-expand-icon.md @@ -0,0 +1,70 @@ +### 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 + + + Position: start + + + + + + + Position: start + + + + + + +``` + + + +#### Render icon inside Button component: + +```jsx + +const InsideButtonExample = (props: ListItemExpandIconProps) => { + const [expanded, setExpanded] = React.useState(false); + const [position, setPosition] = React.useState<'start' | 'end'>('start'); + + return ( + + + Icon position: + + + + Click on button to change state: + + + + ); +}; +``` + + 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..aebe10086a 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,18 @@ export const ListItemViewContent = ({ expanded, selected, title, + expandIconPosition = 'start', + renderExpandIcon: RenderExpandIcon = ListItemExpandIcon, }: ListItemViewContentProps) => { + const expandIconNode = isGroup ? ( + + ) : null; + return ( @@ -72,13 +84,7 @@ export const ListItemViewContent = ({ {renderSafeIndentation(indentation)} - {isGroup ? ( - - ) : null} + {expandIconPosition === 'start' && expandIconNode} {startSlot} @@ -104,7 +110,10 @@ export const ListItemViewContent = ({ - {endSlot} + + {expandIconPosition === '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..5163b2e12b 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, @@ -216,6 +218,7 @@ const stories: ListItemViewProps[] = [ ), expanded: true, + renderExpandIcon: (props) => , isGroup: true, startSlot: , indentation: 1, @@ -232,6 +235,33 @@ const stories: ListItemViewProps[] = [ ), }, + { + id: '13', + size: 'l', + content: { + title, + startSlot: , + indentation: 1, + isGroup: true, + expanded: false, + expandIconPosition: 'end', + }, + }, + { + id: '14', + size: 'l', + content: { + title: 'Custom icon end', + isGroup: true, + expanded: false, + expandIconPosition: '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 6af991e373..d952425601 100644 --- a/src/components/useList/types.ts +++ b/src/components/useList/types.ts @@ -39,6 +39,15 @@ export type ItemState = { indentation: number; }; +type ExpandIconPositionType = 'start' | 'end'; + +export interface ListItemExpandIconRenderProps { + position: ExpandIconPositionType; + expanded: boolean | undefined; + disableTransition: boolean | undefined; + disabled: boolean | undefined; +} + export type ListItemViewContentType = { title: React.ReactNode; subtitle?: React.ReactNode; @@ -49,7 +58,18 @@ export type ListItemViewContentType = { */ indentation?: number; isGroup?: boolean; + /** + * Required prop if `isGroup` - `true` + */ expanded?: boolean; + /** + * @default - 'start' + */ + expandIconPosition?: ExpandIconPositionType; + /** + * 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,