-
Notifications
You must be signed in to change notification settings - Fork 98
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
44d7160
commit 868b383
Showing
33 changed files
with
1,386 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
@use '../variables'; | ||
|
||
$block: '.#{variables.$ns}list-composable'; | ||
|
||
#{$block} { | ||
& ul { | ||
// TODO: more clever reset default styles? | ||
padding-inline-start: 0; | ||
} | ||
|
||
outline: none; | ||
display: flex; | ||
flex-direction: column; | ||
flex: 1 1 auto; | ||
width: 100%; | ||
|
||
&__container { | ||
flex: 1 1 auto; | ||
|
||
&_virtualized { | ||
height: var(--yc-list-height); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import React from 'react'; | ||
|
||
import {ListContainer} from './components/ListContainer/ListContainer'; | ||
import {ListProvider} from './components/ListProvider/ListProvider'; | ||
import type {ListProviderProps} from './types'; | ||
|
||
export interface ListComposableProps<T> extends Omit<ListProviderProps<T>, 'children'> { | ||
children?: React.ReactNode; | ||
} | ||
|
||
export function ListComposable<T>({children, ...props}: ListComposableProps<T>) { | ||
return <ListProvider {...props}>{children || <ListContainer />}</ListProvider>; | ||
} |
42 changes: 42 additions & 0 deletions
42
src/components/ListComposable/__stories__/ListComposable.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import React from 'react'; | ||
|
||
import type {Meta, StoryFn} from '@storybook/react'; | ||
|
||
import {Flex} from '../../layout'; | ||
import {ListComposable, ListComposableProps} from '../ListComposable'; | ||
import {ListContainer} from '../components/ListContainer/ListContainer'; | ||
import {ListActionButton} from '../components/ListControls/ListControls'; | ||
import {ListFilter} from '../components/ListFilter/ListFilter'; | ||
|
||
import {ListResetButton} from './ListResetButton'; | ||
import {createRandomizedData} from './makeData'; | ||
|
||
export default { | ||
title: 'ListComposable/BaseExample', | ||
component: ListComposable, | ||
} as Meta; | ||
|
||
const data = createRandomizedData(1000); | ||
|
||
const DefaultTemplate: StoryFn<ListComposableProps<unknown>> = () => ( | ||
<React.StrictMode> | ||
<Flex gap="5" style={{width: '100%', height: '300px'}}> | ||
<ListComposable items={data} size="s" selectable="multiple"> | ||
<Flex direction="column" gap="3" width={400}> | ||
<ListFilter /> | ||
<ListContainer virtualized /> | ||
<Flex gap="2"> | ||
<ListResetButton /> | ||
<ListActionButton | ||
actionText="Accept" | ||
onActionClick={(...args) => | ||
alert(args.map((arg) => JSON.stringify(arg, null, 2)).join('\n')) | ||
} | ||
/> | ||
</Flex> | ||
</Flex> | ||
</ListComposable> | ||
</Flex> | ||
</React.StrictMode> | ||
); | ||
export const Examples = DefaultTemplate.bind({}); |
22 changes: 22 additions & 0 deletions
22
src/components/ListComposable/__stories__/ListResetButton.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import React from 'react'; | ||
|
||
import {Button} from '../../Button/Button'; | ||
import {useListContext} from '../components/ListContext/ListContext'; | ||
|
||
interface ListResetButtonProps {} | ||
|
||
export const ListResetButton = (_props: ListResetButtonProps) => { | ||
const {setSelected, size, onFilterChange, formatInternalItems} = useListContext(); | ||
|
||
const handleClick = () => { | ||
setSelected(() => ({})); | ||
onFilterChange(''); | ||
formatInternalItems((initialData) => initialData); | ||
}; | ||
|
||
return ( | ||
<Button width="max" onClick={handleClick} size={size}> | ||
Reset | ||
</Button> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import {faker} from '@faker-js/faker/locale/en'; | ||
|
||
import type {ListItemType} from '../types'; | ||
|
||
const RANDOM_WORDS = Array(50) | ||
.fill(null) | ||
.map(() => faker.person.fullName()); | ||
|
||
export function createRandomizedData<T = string>( | ||
num = 1000, | ||
hasDepth = true, | ||
getData?: (title: string) => T, | ||
): ListItemType<T>[] { | ||
const data = []; | ||
|
||
for (let i = 0; i < num; i++) { | ||
data.push(createRandomizedItem<T>(hasDepth ? 0 : 3, getData)); | ||
} | ||
|
||
return data; | ||
} | ||
|
||
function base<T>(title: string): T { | ||
return {title} as T; | ||
} | ||
|
||
function createRandomizedItem<T>( | ||
depth: number, | ||
getData: (title: string) => T = base, | ||
): ListItemType<T> { | ||
const item: ListItemType<T> = { | ||
data: getData(RANDOM_WORDS[Math.floor(Math.random() * RANDOM_WORDS.length)]), | ||
}; | ||
|
||
const numChildren = depth < 3 ? Math.floor(Math.random() * 5) : 0; | ||
|
||
if (numChildren > 0) { | ||
item.children = []; | ||
} | ||
|
||
for (let i = 0; i < numChildren; i++) { | ||
if (item.children) { | ||
item.children.push(createRandomizedItem(depth + 1, getData)); | ||
} | ||
} | ||
|
||
return item; | ||
} |
81 changes: 81 additions & 0 deletions
81
src/components/ListComposable/components/ListContainer/ListContainer.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import React from 'react'; | ||
|
||
import {bListComposable} from '../../constants'; | ||
import type { | ||
ListContainerRenderProps, | ||
ListItemBaseData, | ||
ListItemRendererProps, | ||
RenderListItemViewProps, | ||
} from '../../types'; | ||
import {computeItemSize} from '../../utils/computeItemSize'; | ||
import {useListContext} from '../ListContext/ListContext'; | ||
import {ListItemRenderer} from '../ListItemRenderer/ListItemRenderer'; | ||
import {ListItemView} from '../ListItemView/ListItemView'; | ||
import {SimpleListContainer} from '../SimpleListContainer/SimpleListContainer'; | ||
import {VirtualizedListContainer} from '../VirtualizedListContainer/VirtualizedListContainer'; | ||
interface ListContainerProps<T> { | ||
virtualized?: boolean; | ||
prepareCustomData?(props: T): ListItemBaseData; | ||
getItemSize?(index: number): number; | ||
renderItemView?: (props: RenderListItemViewProps) => React.JSX.Element; | ||
renderItem?(props: ListItemRendererProps<T>): React.ReactNode; | ||
renderContainer?(props: ListContainerRenderProps<T>): React.ReactNode; | ||
} | ||
|
||
export function ListContainer<T>({ | ||
virtualized, | ||
prepareCustomData, | ||
getItemSize: _getItemSize, | ||
renderItem, | ||
renderContainer, | ||
renderItemView = ListItemView, | ||
}: ListContainerProps<T>) { | ||
const {listRef, size, containerRef, handleKeyDown, byId, order} = useListContext<T>(); | ||
|
||
const items = React.useMemo(() => { | ||
return order.map((id) => (prepareCustomData ? prepareCustomData(byId[id]) : byId[id])); | ||
}, [byId, order, prepareCustomData]); | ||
|
||
const Container = | ||
renderContainer || virtualized ? VirtualizedListContainer : SimpleListContainer; | ||
|
||
const RenderItem = React.useCallback( | ||
(props: ListItemRendererProps<T>) => | ||
renderItem ? ( | ||
renderItem(props) | ||
) : ( | ||
<ListItemRenderer | ||
{...(props as ListItemRendererProps<ListItemBaseData>)} | ||
View={renderItemView} | ||
/> | ||
), | ||
[renderItem, renderItemView], | ||
); | ||
|
||
const getItemSize = React.useMemo(() => { | ||
if (_getItemSize) { | ||
return _getItemSize; | ||
} | ||
|
||
if (prepareCustomData) { | ||
return (index: number): number => | ||
computeItemSize(size, prepareCustomData(byId[order[index]])); | ||
} | ||
|
||
return () => computeItemSize(size); | ||
}, [_getItemSize, byId, order, prepareCustomData, size]); | ||
|
||
return ( | ||
<div | ||
className={bListComposable({virtualized}, 'container')} | ||
ref={containerRef} | ||
tabIndex={-1} | ||
role="listbox" | ||
onKeyDown={handleKeyDown} | ||
> | ||
<Container ref={listRef} items={items} getItemSize={getItemSize}> | ||
{RenderItem} | ||
</Container> | ||
</div> | ||
); | ||
} |
17 changes: 17 additions & 0 deletions
17
src/components/ListComposable/components/ListContext/ListContext.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import React from 'react'; | ||
|
||
import type {ListContextType} from '../../types'; | ||
|
||
export const ListContext = React.createContext<ListContextType<unknown> | null>(null); | ||
|
||
export const useListContext = <T,>() => { | ||
const context = React.useContext(ListContext); | ||
|
||
if (!context) { | ||
throw new Error( | ||
'Trying to use "ListContext" out of scope. Ensure that you use "useListContext" hook inside "ListProvider"', | ||
); | ||
} | ||
|
||
return context as ListContextType<T>; | ||
}; |
33 changes: 33 additions & 0 deletions
33
src/components/ListComposable/components/ListControls/ListControls.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import React from 'react'; | ||
|
||
import {Button} from '../../../Button/Button'; | ||
import type {ListItemId} from '../../types'; | ||
import {useListContext} from '../ListContext/ListContext'; | ||
|
||
interface ListControlsProps<T> { | ||
actionText: string; | ||
onActionClick(selectedItems: T[], selectedIds: Record<ListItemId, boolean>): void; | ||
} | ||
|
||
export function ListActionButton<T>({actionText, onActionClick}: ListControlsProps<T>) { | ||
const {size, selected, byId} = useListContext<T>(); | ||
|
||
const handleActionClick = React.useCallback(() => { | ||
onActionClick( | ||
Object.entries(selected).reduce<T[]>((acc, [itemId, isSelected]) => { | ||
if (isSelected && byId[itemId]) { | ||
acc.push(byId[itemId]); | ||
} | ||
|
||
return acc; | ||
}, []), | ||
selected, | ||
); | ||
}, [byId, onActionClick, selected]); | ||
|
||
return ( | ||
<Button view="action" size={size} onClick={handleActionClick} width="max"> | ||
{actionText} | ||
</Button> | ||
); | ||
} |
71 changes: 71 additions & 0 deletions
71
src/components/ListComposable/components/ListFilter/ListFilter.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import React from 'react'; | ||
|
||
import debounce from 'lodash/debounce'; | ||
|
||
import {TextInput, TextInputProps} from '../../../controls'; | ||
import type {ListItemType} from '../../types'; | ||
import {defaultFilterItems} from '../../utils/defaultFilterItems'; | ||
import {useListContext} from '../ListContext/ListContext'; | ||
|
||
interface ListFilterProps<T> | ||
extends Omit<TextInputProps, 'ref' | 'onUpdate' | 'onChange' | 'value'> { | ||
/** | ||
* Override default filtration logic | ||
*/ | ||
filterItems?(value: string, items: ListItemType<T>[]): ListItemType<T>[]; | ||
/** | ||
* Override only logic with item affiliation | ||
*/ | ||
filterItem?(value: string, item: T): boolean; | ||
debounceTimeout?: number; | ||
} | ||
|
||
const DEFAULT_DEBOUNCE_TIMEOUT = 300; | ||
|
||
function defaultFilterFn<T>(value: string, item: T): boolean { | ||
return item && typeof item === 'object' && 'title' in item && typeof item.title === 'string' | ||
? item.title.includes(value) | ||
: true; | ||
} | ||
export function ListFilter<T>({ | ||
filterItem, | ||
filterItems, | ||
debounceTimeout = DEFAULT_DEBOUNCE_TIMEOUT, | ||
...props | ||
}: ListFilterProps<T>) { | ||
const {refFilter, filter, onFilterChange, formatInternalItems, size} = useListContext<T>(); | ||
|
||
const handleUpdate = React.useMemo(() => { | ||
const runFiltration = (nextFilterValue: string) => { | ||
if (filterItems) { | ||
return (items: ListItemType<T>[]) => filterItems(nextFilterValue, items); | ||
} | ||
|
||
if (nextFilterValue) { | ||
return (items: ListItemType<T>[]) => | ||
defaultFilterItems(items, (item) => | ||
(filterItem || defaultFilterFn)(nextFilterValue, item), | ||
); | ||
} | ||
|
||
// reset initial data | ||
return (items: ListItemType<T>[]) => items; | ||
}; | ||
|
||
return debounce((value) => formatInternalItems(runFiltration(value)), debounceTimeout); | ||
}, [filterItem, filterItems, formatInternalItems, debounceTimeout]); | ||
|
||
React.useEffect(() => { | ||
handleUpdate(filter); | ||
}, [filter, handleUpdate]); | ||
|
||
return ( | ||
<TextInput | ||
{...props} | ||
size={size} | ||
value={filter} | ||
ref={refFilter} | ||
onUpdate={onFilterChange} | ||
/> | ||
); | ||
} |
92 changes: 92 additions & 0 deletions
92
src/components/ListComposable/components/ListItemRenderer/ListItemRenderer.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
/* eslint-disable react/display-name */ | ||
import React from 'react'; | ||
|
||
import {ChevronDown, ChevronUp} from '@gravity-ui/icons'; | ||
|
||
import {Icon} from '../../../Icon'; | ||
import {Label} from '../../../Label'; | ||
import {Text} from '../../../Text'; | ||
import type { | ||
ListItemBaseData, | ||
ListItemRendererProps as ListItemRendererPropsBase, | ||
RenderListItemViewProps, | ||
} from '../../types'; | ||
import {createListItemQa} from '../../utils/createListItemQa'; | ||
import {useListContext} from '../ListContext/ListContext'; | ||
|
||
interface ListItemRendererProps extends ListItemRendererPropsBase<ListItemBaseData> { | ||
View: (props: RenderListItemViewProps) => React.JSX.Element; | ||
} | ||
|
||
export const ListItemRenderer = ({item, id: index, View}: ListItemRendererProps) => { | ||
const { | ||
activeItem, | ||
order, | ||
groupsState, | ||
size, | ||
expandedState, | ||
onItemClick, | ||
onGroupItemClick, | ||
itemHandlers, | ||
selected, | ||
itemsState, | ||
disabled, | ||
} = useListContext(); | ||
const id = order[index]; | ||
const indentation = itemsState[id]?.indentation || 0; | ||
const expanded = id in expandedState ? expandedState[id] : true; | ||
const isGroup = id in groupsState; | ||
|
||
const {handlers, onClick, qa} = React.useMemo(() => { | ||
let onClick; | ||
|
||
if (isGroup) { | ||
onClick = () => onGroupItemClick(id); | ||
} else if (onItemClick) { | ||
onClick = () => onItemClick(id); | ||
} | ||
|
||
return { | ||
handlers: itemHandlers(id), | ||
onClick, | ||
qa: createListItemQa(id), | ||
}; | ||
}, [itemHandlers, id, isGroup, onItemClick, onGroupItemClick]); | ||
|
||
return ( | ||
<View | ||
{...handlers} | ||
{...item} | ||
title={ | ||
isGroup ? ( | ||
<Text | ||
as="div" | ||
ellipsis | ||
variant="subheader-1" | ||
color={disabled[id] ? 'secondary' : undefined} | ||
> | ||
{item.title} | ||
</Text> | ||
) : ( | ||
item.title | ||
) | ||
} | ||
isGroup={isGroup} | ||
active={id === activeItem} | ||
selected={Boolean(selected[id])} | ||
onClick={onClick} | ||
key={index} | ||
rightSlot={isGroup ? <Label>{groupsState[id].childrenCount}</Label> : null} | ||
qa={qa} | ||
leftSlot={ | ||
isGroup && groupsState[id].childrenCount > 0 ? ( | ||
<Icon data={expanded ? ChevronDown : ChevronUp} size={16} /> | ||
) : null | ||
} | ||
indentation={indentation} | ||
size={size} | ||
activeOnHover={!isGroup} | ||
disabled={disabled[id]} | ||
/> | ||
); | ||
}; |
32 changes: 32 additions & 0 deletions
32
src/components/ListComposable/components/ListItemView/ListItemView.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
@use '../../../variables'; | ||
|
||
$block: '.#{variables.$ns}list-item-view'; | ||
|
||
#{$block} { | ||
&__grip { | ||
cursor: grab; | ||
} | ||
|
||
&:hover#{$block}_activeOnHover, | ||
&_active#{$block}_activeOnHover, | ||
&_active { | ||
background: var(--yc-color-base-simple-hover); | ||
} | ||
|
||
&_clickable { | ||
cursor: pointer; | ||
} | ||
|
||
&_selected, | ||
&_selected:hover#{$block}_activeOnHover { | ||
background: var(--yc-color-base-selection); | ||
} | ||
|
||
&_hidden { | ||
display: none; | ||
} | ||
|
||
&__slot { | ||
width: 16px; | ||
} | ||
} |
126 changes: 126 additions & 0 deletions
126
src/components/ListComposable/components/ListItemView/ListItemView.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import React from 'react'; | ||
|
||
import {Check, Grip} from '@gravity-ui/icons'; | ||
import type {QAProps} from 'src/components/types'; | ||
|
||
import {Icon} from '../../../Icon'; | ||
import {Text, colorText} from '../../../Text'; | ||
import {Flex, spacing} from '../../../layout'; | ||
import {block} from '../../../utils/cn'; | ||
import {modToHeight} from '../../constants'; | ||
import type {ListSizeTypes} from '../../types'; | ||
|
||
import './ListItemView.scss'; | ||
|
||
const b = block('list-item-view'); | ||
|
||
export interface ListItemViewProps extends QAProps { | ||
/** | ||
* Ability to override default html tag | ||
*/ | ||
as?: keyof JSX.IntrinsicElements; | ||
draggable?: boolean; | ||
/** | ||
* @default `m` | ||
*/ | ||
size?: ListSizeTypes; | ||
selected?: boolean; | ||
active?: boolean; | ||
/** | ||
* display: hidden; | ||
*/ | ||
hidden?: boolean; | ||
disabled?: boolean; | ||
/** | ||
* By default hovered elements has active styles. You can disable this behavior | ||
*/ | ||
activeOnHover?: boolean; | ||
/** | ||
* Build in indentation component to render nested views structure | ||
*/ | ||
indentation?: number; | ||
/** | ||
* Show selected icon if selected and reserve space for this icon | ||
*/ | ||
selectable?: boolean; | ||
/** | ||
* Note: if passed and `disabled` option is `true` click will not be appear | ||
*/ | ||
onClick?(): void; | ||
style?: React.CSSProperties; | ||
title: string | React.ReactNode; | ||
subtitle?: string; | ||
leftSlot?: React.ReactNode; | ||
rightSlot?: React.ReactNode; | ||
} | ||
|
||
export const Slot = ({children}: {children?: React.ReactNode}) => { | ||
return <div className={b('slot')}>{children}</div>; | ||
}; | ||
|
||
export const computeIndentation = (payload = 0) => { | ||
return React.Children.toArray(Array(payload < 0 ? 0 : payload).fill(<Slot />)); | ||
}; | ||
|
||
export const ListItemView = ({ | ||
as = 'li', | ||
leftSlot, | ||
rightSlot, | ||
title, | ||
subtitle, | ||
size = 'm', | ||
active, | ||
hidden, | ||
selected, | ||
draggable, | ||
disabled, | ||
activeOnHover = true, | ||
indentation, | ||
selectable = true, | ||
onClick: _onClick, | ||
...rest | ||
}: ListItemViewProps) => { | ||
const onClick = disabled ? undefined : _onClick; | ||
|
||
return ( | ||
<Flex | ||
onClick={onClick} | ||
alignItems="center" | ||
className={b( | ||
{hidden, active, selected, activeOnHover, clickable: Boolean(onClick)}, | ||
spacing({px: 2}), | ||
)} | ||
as={as} | ||
gap="4" | ||
justifyContent="space-between" | ||
style={{height: modToHeight[size][Number(Boolean(subtitle))]}} | ||
{...rest} | ||
> | ||
<Flex gap="2" alignItems="center"> | ||
{selectable && ( | ||
<Slot> | ||
{selected ? ( | ||
<Icon data={Check} size={16} className={colorText({color: 'info'})} /> | ||
) : null} | ||
</Slot> | ||
)} | ||
|
||
{computeIndentation(indentation)} | ||
|
||
{leftSlot} | ||
<Flex direction="column" gap="0.5"> | ||
{typeof title === 'string' ? ( | ||
<Text color={disabled ? 'hint' : undefined}>{title}</Text> | ||
) : ( | ||
title | ||
)} | ||
{subtitle && <Text color={disabled ? 'hint' : 'secondary'}>{subtitle}</Text>} | ||
</Flex> | ||
</Flex> | ||
<Flex gap="2"> | ||
{draggable && <Icon data={Grip} size={16} className={b('grip')} />} | ||
{rightSlot} | ||
</Flex> | ||
</Flex> | ||
); | ||
}; |
80 changes: 80 additions & 0 deletions
80
src/components/ListComposable/components/ListItemView/__stories__/ListItemView.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import React from 'react'; | ||
|
||
import type {Meta, StoryFn} from '@storybook/react'; | ||
|
||
import {UserAvatar} from '../../../../UserAvatar'; | ||
import {Flex} from '../../../../layout'; | ||
import {ListItemView, ListItemViewProps} from '../ListItemView'; | ||
|
||
export default { | ||
title: 'ListComposable/ListItemView', | ||
component: ListItemView, | ||
} as Meta; | ||
|
||
const title = 'title'; | ||
const subtitle = 'subtitle'; | ||
|
||
const stories: ListItemViewProps[] = [ | ||
{ | ||
title, | ||
draggable: true, | ||
activeOnHover: false, | ||
subtitle, | ||
disabled: true, | ||
leftSlot: ( | ||
<UserAvatar imgUrl="https://avatars.mds.yandex.net/get-yapic/69015/enc-137b8b64288fa6fc5ec58c6b83aea00e7723c8fa5638c078312a1134d8ee32ac/islands-retina-50" /> | ||
), | ||
}, | ||
{ | ||
title, | ||
draggable: true, | ||
subtitle, | ||
activeOnHover: false, | ||
}, | ||
{ | ||
title, | ||
|
||
draggable: true, | ||
subtitle, | ||
selected: true, | ||
leftSlot: ( | ||
<UserAvatar imgUrl="https://avatars.mds.yandex.net/get-yapic/69015/enc-137b8b64288fa6fc5ec58c6b83aea00e7723c8fa5638c078312a1134d8ee32ac/islands-retina-50" /> | ||
), | ||
}, | ||
{ | ||
title, | ||
selected: true, | ||
disabled: true, | ||
// draggable: true, | ||
leftSlot: ( | ||
<UserAvatar imgUrl="https://avatars.mds.yandex.net/get-yapic/69015/enc-137b8b64288fa6fc5ec58c6b83aea00e7723c8fa5638c078312a1134d8ee32ac/islands-retina-50" /> | ||
), | ||
}, | ||
{ | ||
title, | ||
draggable: true, | ||
}, | ||
{ | ||
title, | ||
|
||
draggable: true, | ||
subtitle, | ||
|
||
leftSlot: ( | ||
<UserAvatar imgUrl="https://avatars.mds.yandex.net/get-yapic/69015/enc-137b8b64288fa6fc5ec58c6b83aea00e7723c8fa5638c078312a1134d8ee32ac/islands-retina-50" /> | ||
), | ||
indentation: 1, | ||
}, | ||
{ | ||
title: 'Group 1', | ||
}, | ||
]; | ||
|
||
const DefaultTemplate: StoryFn<ListItemViewProps> = () => ( | ||
<Flex direction="column" width={300}> | ||
{stories.map((props, i) => ( | ||
<ListItemView key={i} {...props} /> | ||
))} | ||
</Flex> | ||
); | ||
export const Examples = DefaultTemplate.bind({}); |
207 changes: 207 additions & 0 deletions
207
src/components/ListComposable/components/ListProvider/ListProvider.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,207 @@ | ||
import React from 'react'; | ||
|
||
import type { | ||
ListItemId, | ||
ListItemType, | ||
ListProviderProps, | ||
SetActiveItem, | ||
SetGroupState, | ||
} from '../../types'; | ||
import {findNextIndex} from '../../utils/findNextIndex'; | ||
import {scrollToItem} from '../../utils/scrollToItem'; | ||
import {ListContext} from '../ListContext/ListContext'; | ||
|
||
import {useDisabledState} from './useDisabledState'; | ||
import {useFilterControlledState} from './useFilterControlledState'; | ||
import {useGroupsExpandedState} from './useGroupsExpandedState'; | ||
import {useItemsControlledState} from './useItemsControlledState'; | ||
import {usePreparedItemsState} from './usePreparedItemsState'; | ||
import {useSelectedState} from './useSelectedState'; | ||
|
||
export function ListProvider<T>({ | ||
children, | ||
items: originalItems, | ||
size = 'm', | ||
disabled: outerDisabledState, | ||
expandedState: outerExpandedState, | ||
initialActiveItemId, | ||
onItemClick: _onItemClick, | ||
selected: userSelectedState, | ||
onGroupItemClick: _onGroupItemClick, | ||
selectable, | ||
filter: userControlledFilterValue, | ||
}: ListProviderProps<T>) { | ||
const refFilter = React.useRef<HTMLInputElement>(null); | ||
const containerRef = React.useRef<HTMLDivElement>(null); | ||
const listRef = React.useRef(null); | ||
|
||
const [disabled, setDisabled] = useDisabledState(outerDisabledState); | ||
const [selected, setSelected] = useSelectedState(userSelectedState); | ||
const [filter, onFilterChange] = useFilterControlledState(userControlledFilterValue); | ||
const [expandedState, setGroupState] = useGroupsExpandedState(outerExpandedState); | ||
|
||
const [activeItem, setActiveItem] = React.useState<ListItemId | null>(() => { | ||
if (initialActiveItemId && initialActiveItemId in byId) { | ||
return initialActiveItemId; | ||
} | ||
|
||
return null; | ||
}); | ||
const [_items, setItems] = useItemsControlledState(originalItems); | ||
|
||
const formatInternalItems = React.useCallback( | ||
(formatFn?: (items: ListItemType<T>[]) => ListItemType<T>[]) => { | ||
if (formatFn) { | ||
setItems(formatFn(originalItems)); | ||
} | ||
}, | ||
[originalItems, setItems], | ||
); | ||
|
||
const [{order, byId, groupsState, itemsState}] = usePreparedItemsState<T>( | ||
_items, | ||
expandedState, | ||
); | ||
|
||
const itemHandlers = React.useMemo(() => { | ||
return (id: ListItemId) => ({ | ||
onMouseEnter: () => setActiveItem(id), | ||
onMouseLeave: () => setActiveItem(null), | ||
}); | ||
}, [setActiveItem]); | ||
|
||
const _activateItem = React.useCallback( | ||
(index?: number, scrollTo = true) => { | ||
if (typeof index === 'number' && order[index]) { | ||
if (scrollTo) { | ||
scrollToItem(order[index], containerRef.current ?? undefined); | ||
} | ||
|
||
setActiveItem(order[index]); | ||
} | ||
}, | ||
[order], | ||
); | ||
|
||
const handleKeyMove = React.useCallback( | ||
(event: React.KeyboardEvent, step: number, defaultItemIndex = 0) => { | ||
event.preventDefault(); | ||
const notSureIndex = order.findIndex((i) => i === activeItem); | ||
const index = (notSureIndex > -1 ? notSureIndex : defaultItemIndex) + step; | ||
_activateItem( | ||
findNextIndex({ | ||
list: order, | ||
index, | ||
step: Math.sign(step), | ||
disabledItems: disabled, | ||
}), | ||
); | ||
}, | ||
[_activateItem, activeItem, order, disabled], | ||
); | ||
|
||
const onGroupItemClick = React.useCallback( | ||
(id: ListItemId, fromKeyboard = false) => { | ||
if (_onGroupItemClick) { | ||
_onGroupItemClick(byId[id], id, fromKeyboard); | ||
} else { | ||
setGroupState((x) => { | ||
return { | ||
...x, | ||
[id]: typeof x[id] === 'undefined' ? false : !x[id], | ||
}; | ||
}); | ||
} | ||
}, | ||
[_onGroupItemClick, byId, setGroupState], | ||
); | ||
|
||
const onItemClick = React.useMemo(() => { | ||
if (_onItemClick) { | ||
return (id: ListItemId, fromKeyboard = false) => { | ||
_onItemClick(byId[id], id, fromKeyboard); | ||
}; | ||
} | ||
|
||
if (selectable) { | ||
return (id: ListItemId, _fromKeyboard = false) => { | ||
setSelected((x) => | ||
selectable === 'multiple' | ||
? { | ||
...x, | ||
[id]: !x[id], | ||
} | ||
: {[id]: !x[id]}, | ||
); | ||
}; | ||
} | ||
|
||
return undefined; | ||
}, [selectable, _onItemClick, byId, setSelected]); | ||
|
||
const handleKeyDown = React.useCallback( | ||
(event: React.KeyboardEvent<HTMLUListElement | HTMLDivElement>) => { | ||
switch (event.key) { | ||
case 'ArrowDown': { | ||
handleKeyMove(event, 1, -1); | ||
break; | ||
} | ||
case 'ArrowUp': { | ||
handleKeyMove(event, -1); | ||
break; | ||
} | ||
case ' ': | ||
case 'Enter': { | ||
if (activeItem && !disabled[activeItem]) { | ||
event.preventDefault(); | ||
// user try to control groups state outside | ||
if (activeItem in groupsState) { | ||
onGroupItemClick(activeItem, true); | ||
} else { | ||
onItemClick?.(activeItem, true); | ||
} | ||
} | ||
break; | ||
} | ||
default: { | ||
if (refFilter.current) { | ||
refFilter.current.focus(); | ||
} | ||
} | ||
} | ||
}, | ||
[onGroupItemClick, activeItem, groupsState, handleKeyMove, onItemClick, disabled], | ||
); | ||
|
||
return ( | ||
<ListContext.Provider | ||
value={{ | ||
itemHandlers, | ||
activeItem, | ||
size, | ||
handleKeyDown, | ||
containerRef, | ||
listRef, | ||
refFilter, | ||
setActiveItem: setActiveItem as SetActiveItem, | ||
byId, | ||
order, | ||
groupsState, | ||
formatInternalItems, | ||
filter, | ||
onFilterChange, | ||
expandedState, | ||
setSelected, | ||
setGroupState: setGroupState as SetGroupState, | ||
onItemClick, | ||
selected, | ||
onGroupItemClick, | ||
itemsState, | ||
disabled, | ||
setDisabled, | ||
}} | ||
> | ||
{children} | ||
</ListContext.Provider> | ||
); | ||
} |
18 changes: 18 additions & 0 deletions
18
src/components/ListComposable/components/ListProvider/useDisabledState.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import React from 'react'; | ||
|
||
import type {ListItemId} from '../../types'; | ||
|
||
export const useDisabledState = (outerDisabledState?: Record<ListItemId, boolean>) => { | ||
const firstRenderRef = React.useRef(true); | ||
const [state, setState] = React.useState(() => outerDisabledState || {}); | ||
|
||
React.useEffect(() => { | ||
if (firstRenderRef.current) { | ||
firstRenderRef.current = false; | ||
} else { | ||
setState(outerDisabledState || {}); | ||
} | ||
}, [outerDisabledState]); | ||
|
||
return [state, setState] as const; | ||
}; |
11 changes: 11 additions & 0 deletions
11
src/components/ListComposable/components/ListProvider/useFilterControlledState.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import React from 'react'; | ||
|
||
export const useFilterControlledState = (externalFilterValue?: string) => { | ||
const [filter, setFilter] = React.useState(() => externalFilterValue ?? ''); | ||
|
||
React.useEffect(() => { | ||
setFilter(externalFilterValue ?? ''); | ||
}, [externalFilterValue]); | ||
|
||
return [filter, setFilter] as const; | ||
}; |
16 changes: 16 additions & 0 deletions
16
src/components/ListComposable/components/ListProvider/useGroupsExpandedState.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import React from 'react'; | ||
|
||
export const useGroupsExpandedState = (groupsExpandedState?: Record<string, boolean>) => { | ||
const isFirstRenderRef = React.useRef(true); | ||
const [state, setState] = React.useState(() => groupsExpandedState || {}); | ||
|
||
React.useEffect(() => { | ||
if (isFirstRenderRef.current) { | ||
isFirstRenderRef.current = false; | ||
} else { | ||
setState(groupsExpandedState || {}); | ||
} | ||
}, [groupsExpandedState]); | ||
|
||
return [state, setState] as const; | ||
}; |
18 changes: 18 additions & 0 deletions
18
src/components/ListComposable/components/ListProvider/useItemsControlledState.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import React from 'react'; | ||
|
||
import type {ListItemType} from '../../types'; | ||
|
||
export function useItemsControlledState<T>(_items: ListItemType<T>[]) { | ||
const isFirstRenderRef = React.useRef(true); | ||
const [items, setItems] = React.useState(() => _items); | ||
|
||
React.useEffect(() => { | ||
if (isFirstRenderRef.current) { | ||
isFirstRenderRef.current = false; | ||
} else { | ||
setItems(_items); | ||
} | ||
}, [_items]); | ||
|
||
return [items, setItems] as const; | ||
} |
25 changes: 25 additions & 0 deletions
25
src/components/ListComposable/components/ListProvider/usePreparedItemsState.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import React from 'react'; | ||
|
||
import type {ListItemType, ParsedState} from '../../types'; | ||
import {flattenItems} from '../../utils/flattenItems'; | ||
import {parseDataWithChildren} from '../../utils/parseDataWithChildren'; | ||
|
||
export function usePreparedItemsState<T>( | ||
items: ListItemType<T>[], | ||
expandedState?: Record<string, boolean>, | ||
) { | ||
const isFirstRenderRef = React.useRef(true); | ||
const [state, setState] = React.useState<ParsedState<T>>(() => parseDataWithChildren<T>(items)); | ||
const [order, setOrder] = React.useState(() => flattenItems(items, expandedState)); | ||
|
||
React.useEffect(() => { | ||
if (isFirstRenderRef.current) { | ||
isFirstRenderRef.current = false; | ||
} else { | ||
setState(parseDataWithChildren<T>(items)); | ||
setOrder(flattenItems(items, expandedState)); | ||
} | ||
}, [items, expandedState]); | ||
|
||
return [{order, ...state}]; | ||
} |
18 changes: 18 additions & 0 deletions
18
src/components/ListComposable/components/ListProvider/useSelectedState.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import React from 'react'; | ||
|
||
import type {ListItemId} from '../../types'; | ||
|
||
export const useSelectedState = (_selected?: Record<ListItemId, boolean>) => { | ||
const isFirstRenderRef = React.useRef(true); | ||
const [selected, setSelected] = React.useState(() => _selected || {}); | ||
|
||
React.useEffect(() => { | ||
if (isFirstRenderRef.current) { | ||
isFirstRenderRef.current = false; | ||
} else { | ||
setSelected(_selected || {}); | ||
} | ||
}, [_selected]); | ||
|
||
return [selected, setSelected] as const; | ||
}; |
16 changes: 16 additions & 0 deletions
16
src/components/ListComposable/components/SimpleListContainer/SimpleListContainer.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import React from 'react'; | ||
|
||
import type {ListContainerRenderProps} from '../../types'; | ||
|
||
export const SimpleListContainer = React.forwardRef(function <T>( | ||
{items, children, className}: ListContainerRenderProps<T>, | ||
ref: any, | ||
) { | ||
return ( | ||
<ul ref={ref} className={className}> | ||
{items.map((item, index) => children({id: index, item}))} | ||
</ul> | ||
); | ||
}); | ||
|
||
SimpleListContainer.displayName = 'SimpleListContainer'; |
54 changes: 54 additions & 0 deletions
54
...omponents/ListComposable/components/VirtualizedListContainer/VirtualizedListContainer.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import React from 'react'; | ||
|
||
import AutoSizer, {Size} from 'react-virtualized-auto-sizer'; | ||
import {VariableSizeList as List} from 'react-window'; | ||
|
||
import type {ListContainerRenderProps} from '../../types'; | ||
|
||
interface RenderRowProps<T> { | ||
index: number; | ||
style: React.CSSProperties; | ||
data: T[]; | ||
} | ||
|
||
export const VirtualizedListContainer = React.forwardRef(function <T>( | ||
{items, className, getItemSize, children}: ListContainerRenderProps<T>, | ||
ref: any, | ||
) { | ||
const RenderRow = React.useCallback( | ||
({index, style, data: items}: RenderRowProps<T>) => { | ||
return ( | ||
<div style={style} key={index}> | ||
{children({ | ||
id: index, | ||
item: items[index], | ||
})} | ||
</div> | ||
); | ||
}, | ||
[children], | ||
); | ||
|
||
return ( | ||
<AutoSizer> | ||
{({width, height}: Size) => ( | ||
<List | ||
overscanCount={10} | ||
className={className} | ||
ref={ref} | ||
width={width} | ||
height={height} | ||
itemCount={items.length} | ||
itemData={items} | ||
itemSize={getItemSize} | ||
// TODO: implement page size | ||
// onItemsRendered={console.log} | ||
> | ||
{RenderRow} | ||
</List> | ||
)} | ||
</AutoSizer> | ||
); | ||
}); | ||
|
||
VirtualizedListContainer.displayName = 'VirtualizedListContainer'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import {block} from '../utils/cn'; | ||
|
||
import './ListComposable.scss'; | ||
|
||
export const bListComposable = block('list-composable'); | ||
|
||
export const GROUPED_ID_SEPARATOR = '-'; | ||
|
||
export const modToHeight = { | ||
s: [24, 44], | ||
m: [28, 48], | ||
l: [36, 52], | ||
xl: [44, 58], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
import type {QAProps} from '../types'; | ||
|
||
import type {modToHeight} from './constants'; | ||
|
||
export type ListItemId = string; | ||
/** | ||
* Default fallback id type. | ||
* The core idea is to store original index and and nested children index | ||
*/ | ||
export type GroupedId = string; | ||
|
||
export interface ListItemType<T = unknown> { | ||
/** | ||
* Required if you want to control list state from internal place. | ||
* For example to control expanded groups state | ||
*/ | ||
id?: string; | ||
data: T; | ||
children?: ListItemType<T>[]; | ||
} | ||
export type GroupParsedState = { | ||
childrenCount: number; | ||
}; | ||
|
||
export type ItemParsedState = { | ||
indentation: number; | ||
}; | ||
export type ParsedState<T = unknown> = { | ||
/** | ||
* stored internal meta info about item | ||
* Note: Groups are also items | ||
*/ | ||
itemsState: Record<ListItemId, ItemParsedState>; | ||
/** | ||
* Normalized original data | ||
*/ | ||
byId: Record<ListItemId, T>; | ||
/** | ||
* Re | ||
*/ | ||
groupsState: Record<ListItemId, GroupParsedState>; | ||
}; | ||
|
||
export type ListSizeTypes = keyof typeof modToHeight; | ||
|
||
export interface ListItemBaseData { | ||
title: string; | ||
subtitle?: string; | ||
leftSlot?: React.ReactNode; | ||
rightSlot?: React.ReactNode; | ||
} | ||
|
||
export interface ListItemRendererProps<T> { | ||
id: number; | ||
item: T; | ||
} | ||
|
||
export interface ListContainerRenderProps<T> { | ||
items: T[]; | ||
getItemSize(index: number): number; | ||
className?: string; | ||
children(props: ListItemRendererProps<T>): React.ReactNode; | ||
} | ||
|
||
export type OnListItemClick<T> = ( | ||
item: ListItemType<T>['data'], | ||
itemId: ListItemId, | ||
fromKeyboard?: boolean, | ||
) => void; | ||
|
||
export type SetActiveItem = (id: ListItemId | null) => void; | ||
|
||
export type ExpandedState = Record<ListItemId, boolean>; | ||
|
||
export type SetGroupState = (value: (currentState: ExpandedState) => ExpandedState) => void; | ||
|
||
export interface RenderListItemViewProps extends QAProps { | ||
/** | ||
* @default `m` | ||
*/ | ||
size: ListSizeTypes; | ||
isGroup: boolean; | ||
selected: boolean; | ||
active: boolean; | ||
disabled: boolean; | ||
/** | ||
* By default hovered elements has active styles. You can switch off this behavior | ||
*/ | ||
activeOnHover: boolean; | ||
indentation: number; | ||
onClick?(): void; | ||
title: string | React.ReactNode; | ||
subtitle?: string; | ||
leftSlot: React.ReactNode; | ||
rightSlot: React.ReactNode; | ||
} | ||
|
||
export type ListContextType<T> = ParsedState<T> & { | ||
activeItem: ListItemId | null; | ||
size: ListSizeTypes; | ||
order: ListItemId[]; | ||
handleKeyDown(e: React.KeyboardEvent<HTMLUListElement | HTMLDivElement>): void; | ||
containerRef: React.RefObject<HTMLDivElement>; | ||
listRef: React.RefObject<HTMLUListElement>; | ||
refFilter: React.RefObject<HTMLInputElement>; | ||
expandedState: ExpandedState; | ||
setActiveItem: SetActiveItem; | ||
setSelected(fn: (selected: Record<ListItemId, boolean>) => Record<ListItemId, boolean>): void; | ||
onItemClick?(id: ListItemId, fromKeyboard?: boolean): void; | ||
onGroupItemClick(id: ListItemId, fromKeyboard?: boolean): void; | ||
setGroupState: SetGroupState; | ||
itemHandlers(id: ListItemId): { | ||
onMouseEnter: () => void; | ||
onMouseLeave: () => void; | ||
}; | ||
selected: Record<ListItemId, boolean>; | ||
formatInternalItems(formatFn?: (originalItems: ListItemType<T>[]) => ListItemType<T>[]): void; | ||
filter: string; | ||
onFilterChange(value: string): void; | ||
disabled: Record<ListItemId, boolean>; | ||
setDisabled(fn: (state: Record<ListItemId, boolean>) => Record<ListItemId, boolean>): void; | ||
}; | ||
|
||
export interface ListProviderProps<T> { | ||
items: ListItemType<T>[]; | ||
size?: ListSizeTypes; | ||
children: React.ReactNode; | ||
onItemClick?: OnListItemClick<T>; | ||
onGroupItemClick?: OnListItemClick<T>; | ||
expandedState?: Record<string, boolean>; | ||
initialActiveItemId?: string; | ||
/** | ||
* Ability to control items selections from outside | ||
*/ | ||
selected?: Record<ListItemId, boolean>; | ||
/** | ||
* Needs than you try to use `selected` list state | ||
*/ | ||
selectable?: 'single' | 'multiple'; | ||
/** | ||
* Ability to control filter input value from outside | ||
*/ | ||
filter?: string; | ||
/** | ||
* Ability to control disabled items state from outside | ||
*/ | ||
disabled?: Record<ListItemId, boolean>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import {modToHeight} from '../constants'; | ||
import type {ListItemBaseData, ListSizeTypes} from '../types'; | ||
|
||
export const computeItemSize = (size: ListSizeTypes, item?: ListItemBaseData) => { | ||
return modToHeight[size][item ? Number(Boolean(item.subtitle)) : 0]; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export const createListItemQa = (itemId: string, listId?: string) => | ||
listId ? `${listId}-${itemId}` : `${itemId}`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import type {ListItemType} from '../types'; | ||
|
||
export function defaultFilterItems<T>( | ||
items: ListItemType<T>[], | ||
filterFn: (data: T) => boolean, | ||
): ListItemType<T>[] { | ||
console.time('defaultFilterItems'); | ||
const getChildren = (result: ListItemType<T>[], item: ListItemType<T>) => { | ||
if (item.children) { | ||
const children = item.children.reduce(getChildren, []); | ||
|
||
if (children.length) { | ||
result.push({data: item.data, children}); | ||
} else if (filterFn(item.data)) { | ||
result.push({data: item.data, children: []}); | ||
} | ||
} else if (filterFn(item.data)) { | ||
result.push(item); | ||
} | ||
|
||
return result; | ||
}; | ||
|
||
const res = items.reduce<ListItemType<T>[]>(getChildren, []); | ||
console.timeEnd('defaultFilterItems'); | ||
return res; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
interface FindNextItemsProps { | ||
list: string[]; | ||
index: number; | ||
step: number; | ||
disabledItems?: Record<string, boolean>; | ||
} | ||
|
||
export const findNextIndex = ({list, index, step, disabledItems = {}}: FindNextItemsProps) => { | ||
const dataLength = list.length; | ||
let currentIndex = (index + dataLength) % dataLength; | ||
|
||
for (let i = 0; i < dataLength; i += 1) { | ||
if (list[currentIndex] && !disabledItems[currentIndex]) { | ||
return currentIndex; | ||
} | ||
currentIndex = (currentIndex + dataLength + step) % dataLength; | ||
} | ||
|
||
return undefined; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import type {ListItemId, ListItemType} from '../types'; | ||
|
||
import {getGroupedId} from './groupedIdUtils'; | ||
|
||
export function flattenItems<T>( | ||
items: ListItemType<T>[], | ||
groupsExpandedState: Record<ListItemId, boolean> = {}, | ||
): ListItemId[] { | ||
console.time('flattenItems'); | ||
|
||
const getNestedIds = ( | ||
order: string[], | ||
item: ListItemType<T>, | ||
index: number, | ||
parentId?: string, | ||
) => { | ||
const groupedId = getGroupedId(index, parentId); | ||
const id = item.id || groupedId; | ||
|
||
order.push(id); | ||
|
||
if (item.children) { | ||
// remove from order collapsed groups | ||
if (!(id in groupsExpandedState && !groupsExpandedState[id])) { | ||
order.push( | ||
...item.children.reduce<string[]>( | ||
(acc, item, idx) => getNestedIds(acc, item, idx, groupedId), | ||
[], | ||
), | ||
); | ||
} | ||
} | ||
|
||
return order; | ||
}; | ||
|
||
const result = items.reduce<string[]>((acc, item, index) => getNestedIds(acc, item, index), []); | ||
console.timeEnd('flattenItems'); | ||
return result; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import {GROUPED_ID_SEPARATOR} from '../constants'; | ||
import type {GroupedId} from '../types'; | ||
|
||
export const getGroupedId = (index: string | number, parentId?: string): GroupedId => | ||
parentId ? `${parentId}${GROUPED_ID_SEPARATOR}${index}` : `${index}`; | ||
|
||
export const parseGroupedId = (parentId: GroupedId): string[] => | ||
parentId.split(GROUPED_ID_SEPARATOR); |
39 changes: 39 additions & 0 deletions
39
src/components/ListComposable/utils/parseDataWithChildren.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import type {ListItemType, ParsedState} from '../types'; | ||
|
||
import {getGroupedId, parseGroupedId} from './groupedIdUtils'; | ||
|
||
export function parseDataWithChildren<T>(items: ListItemType<T>[]): ParsedState<T> { | ||
console.time('parseDataWithChildren'); | ||
const result = { | ||
byId: {}, | ||
groupsState: {}, | ||
itemsState: {}, | ||
} as ParsedState<T>; | ||
|
||
const traverseItems = (item: ListItemType<T>, index: number, parentId?: string) => { | ||
const groupedId = getGroupedId(index, parentId); | ||
const id = item.id || groupedId; | ||
|
||
result.byId[id] = item.data; | ||
|
||
if (!result.itemsState[id]) { | ||
result.itemsState[id] = {indentation: 0}; | ||
} | ||
|
||
if (parentId) { | ||
result.itemsState[id].indentation = parseGroupedId(parentId).length; | ||
} | ||
|
||
if (item.children) { | ||
result.groupsState[id] = { | ||
childrenCount: item.children.length, | ||
}; | ||
|
||
item.children.forEach((item, index) => traverseItems(item, index, groupedId)); | ||
} | ||
}; | ||
|
||
items.forEach((item, index) => traverseItems(item, index)); | ||
console.timeEnd('parseDataWithChildren'); | ||
return result; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import type {ListItemId} from '../types'; | ||
|
||
import {createListItemQa} from './createListItemQa'; | ||
|
||
export const scrollToItem = (itemId: ListItemId, containerRef?: HTMLDivElement) => { | ||
if (document) { | ||
const element = (containerRef || document).querySelector( | ||
`[data-qa="${createListItemQa(itemId)}"]`, | ||
); | ||
|
||
if (element) { | ||
element.scrollIntoView({ | ||
block: 'nearest', | ||
}); | ||
} | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters