Skip to content

Commit

Permalink
feat(Select,List): graceful replacement list item view of the List
Browse files Browse the repository at this point in the history
  • Loading branch information
IsaevAlexandr committed Apr 23, 2024
1 parent c34590e commit ee7dcfb
Show file tree
Hide file tree
Showing 26 changed files with 391 additions and 66 deletions.
13 changes: 12 additions & 1 deletion src/components/List/List.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,27 @@ $block: '.#{variables.$ns}list';
flex: 1 1 auto;
}

&__item-new,
&__item,
&__empty-placeholder {
box-sizing: border-box;
display: flex;
align-items: center;
padding: var(--_--item-padding);
user-select: none;
overflow: hidden;
}

&__item,
&__empty-placeholder {
padding: var(--_--item-padding);
}

&__item-new {
&_sortable[data-rbd-drag-handle-context-id]:active {
cursor: grabbing;
}
}

&__item {
&_active {
background: var(--g-color-base-simple-hover);
Expand Down
38 changes: 34 additions & 4 deletions src/components/List/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ import {VariableSizeList} from 'react-window';

import {TextInput} from '../controls';
import {MobileContext} from '../mobile';
import {useDirection} from '../theme';
import {ThemeContext, useDirection} from '../theme';
import {block} from '../utils/cn';
import {getUniqId} from '../utils/common';

import {ListLoadingIndicator} from './ListLoadingIndicator';
import {ListLoadingIndicatorNew} from './ListLoadingIndicatorNew';
import {ListItem, SimpleContainer, defaultRenderItem} from './components';
import {listNavigationIgnoredKeys} from './constants';
import type {ListItemData, ListItemProps, ListProps} from './types';
Expand Down Expand Up @@ -62,6 +63,8 @@ const ListContainer = React.forwardRef<VariableSizeList, VariableSizeListProps>(
ListContainer.displayName = 'ListContainer';

export class List<T = unknown> extends React.Component<ListProps<T>, ListState<T>> {
static contextType = ThemeContext;

static defaultProps: Partial<ListProps<ListItemData<unknown>>> = listDefaultProps;

static moveListElement<T = unknown>(
Expand Down Expand Up @@ -91,6 +94,8 @@ export class List<T = unknown> extends React.Component<ListProps<T>, ListState<T
return undefined;
}

context: React.ContextType<typeof ThemeContext>;

state: ListState<T> = {
items: this.props.items,
filter: '',
Expand Down Expand Up @@ -256,11 +261,21 @@ export class List<T = unknown> extends React.Component<ListProps<T>, ListState<T
}
};

private get newListViewMigration() {
return typeof this.props.newListView === 'undefined'
? Boolean(this.context?._newListView)
: this.props.newListView;
}

private renderItemContent: ListItemProps<T>['renderItem'] = (item, isItemActive, itemIndex) => {
const {onLoadMore} = this.props;
if (!this.newListViewMigration) {
const {onLoadMore} = this.props;

if (isObject(item) && 'value' in item && item.value === this.loadingItem.value) {
return <ListLoadingIndicator onIntersect={itemIndex === 0 ? undefined : onLoadMore} />;
if (isObject(item) && 'value' in item && item.value === this.loadingItem.value) {
return (
<ListLoadingIndicator onIntersect={itemIndex === 0 ? undefined : onLoadMore} />
);
}
}

return this.props.renderItem
Expand Down Expand Up @@ -288,11 +303,26 @@ export class List<T = unknown> extends React.Component<ListProps<T>, ListState<T
? this.props.selectedItemIndex.includes(index)
: index === this.props.selectedItemIndex;

if (this.newListViewMigration) {
const {onLoadMore} = this.props;

if (isObject(item) && 'value' in item && item.value === this.loadingItem.value) {
return (
<ListLoadingIndicatorNew
size={this.props.size || 'm'}
onIntersect={index === 0 ? undefined : onLoadMore}
/>
);
}
}

return (
<ListItem
key={index}
newListView={this.newListViewMigration}
style={style}
itemIndex={index}
hasSelectionIcon={Boolean(this.props.multiple)}
item={item}
sortable={sortable}
sortHandleAlign={sortHandleAlign}
Expand Down
24 changes: 24 additions & 0 deletions src/components/List/ListLoadingIndicatorNew.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';

import {useIntersection} from '../../hooks';
import {Loader} from '../Loader';
import {Flex} from '../layout';
import {computeItemSize} from '../useList';
import type {ListItemSize} from '../useList';

export interface ListLoadingIndicatorNewProps {
size: ListItemSize;
onIntersect: (() => void) | undefined;
}

export const ListLoadingIndicatorNew = ({size, onIntersect}: ListLoadingIndicatorNewProps) => {
const ref = React.useRef<HTMLDivElement | null>(null);

useIntersection({element: ref.current, onIntersect});

return (
<Flex ref={ref} justifyContent="center" height={computeItemSize(size)}>
<Loader />
</Flex>
);
};
7 changes: 1 addition & 6 deletions src/components/List/__stories__/ListShowcase.scss
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
}

&__select {
overflow: hidden;
$button: '.#{variables.$ns}button';
box-sizing: border-box;
display: flex;
Expand All @@ -78,10 +79,4 @@
}
}
}

&__select-text {
overflow: hidden;
text-overflow: ellipsis;
flex: 1 1 auto;
}
}
8 changes: 5 additions & 3 deletions src/components/List/__stories__/ListShowcase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import _random from 'lodash/random';
import _range from 'lodash/range';

import {Button} from '../../Button';
import {Text} from '../../Text';
import {TextInput} from '../../controls';
import {Flex} from '../../layout';
import {cn} from '../../utils/cn';
import {List} from '../List';

Expand Down Expand Up @@ -164,8 +166,8 @@ export function ListShowcase() {
onItemClick={(value) => console.log(value)}
renderItem={(item, _isActive, index) => {
return (
<div className={b('select')}>
<div className={b('select-text')}>{item.title}</div>
<Flex className={b('select')} gap="1">
<Text ellipsis>{item.title}</Text>
<Button
view="flat"
size="s"
Expand All @@ -176,7 +178,7 @@ export function ListShowcase() {
>
Select
</Button>
</div>
</Flex>
);
}}
filterItem={(filter) => (item) => item.title.includes(filter)}
Expand Down
46 changes: 45 additions & 1 deletion src/components/List/components/ListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {Grip} from '@gravity-ui/icons';
import type {DraggableProvided} from 'react-beautiful-dnd';

import {Icon} from '../../Icon';
import {ListItemView} from '../../useList';
import {block} from '../../utils/cn';
import {eventBroker} from '../../utils/event-broker';
import {ListQa} from '../constants';
Expand Down Expand Up @@ -37,7 +38,10 @@ export class ListItem<T = unknown> extends React.Component<ListItemProps<T>> {
sortHandleAlign,
itemClassName,
selected,
hasSelectionIcon,
active,
size,
newListView,
role = 'listitem',
isDragging = false,
} = this.props;
Expand All @@ -51,6 +55,41 @@ export class ListItem<T = unknown> extends React.Component<ListItemProps<T>> {
right: undefined,
};

if (newListView) {
return (
<ListItemView
size={size}
role={role}
selected={selected}
aria-selected={selected}
qa={active ? ListQa.ACTIVE_ITEM : undefined}
dragging={isDragging}
active={active}
hasSelectionIcon={hasSelectionIcon}
disabled={item.disabled}
className={b(
'item-new',
{
sortable,
'sort-handle-align': sortHandleAlign,
},
itemClassName,
)}
{...this.props.provided?.draggableProps}
{...this.props.provided?.dragHandleProps}
style={getStyle(this.props.provided, fixedStyle)}
onClick={item.disabled ? undefined : this.onClick}
onClickCapture={this.onClickCapture}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
ref={this.setRef}
id={`${this.props.listId}-item-${this.props.itemIndex}`}
startSlot={this.renderSortIcon()}
title={this.renderContent()}
/>
);
}

return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
<div
Expand Down Expand Up @@ -102,7 +141,12 @@ export class ListItem<T = unknown> extends React.Component<ListItemProps<T>> {
}

private renderContent() {
const {renderItem = defaultRenderItem, item, active, itemIndex} = this.props;
const {renderItem = defaultRenderItem, item, active, itemIndex, newListView} = this.props;

if (newListView) {
return renderItem(item, active, itemIndex);
}

return <div className={b('item-content')}>{renderItem(item, active, itemIndex)}</div>;
}

Expand Down
17 changes: 17 additions & 0 deletions src/components/List/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {DraggableProvided} from 'react-beautiful-dnd';

import type {TextInputSize} from '../controls';
import type {QAProps} from '../types';
import type {ListItemSize} from '../useList';

export type ListSortHandleAlign = 'left' | 'right';

Expand All @@ -14,6 +15,15 @@ export type ListItemData<T> = T & {disabled?: boolean};
export type ListProps<T = unknown> = QAProps & {
items: ListItemData<T>[];
className?: string;
/**
* Affects only items selected view
*/
multiple?: boolean;
/**
* ListItem view behavior for soft migration.
* In later versions of uikit will be removed.
*/
newListView?: boolean;
itemClassName?: string;
itemsClassName?: string;
filterClassName?: string;
Expand Down Expand Up @@ -53,7 +63,13 @@ export type ListItemProps<T> = {
itemIndex: number;
active: boolean;
selected: boolean;
// TODO: написать soft миграцию
newListView?: boolean;
itemClassName?: string;
/**
* switch selection view from background to selected icon
*/
hasSelectionIcon?: boolean;
sortable?: boolean;
sortHandleAlign?: ListSortHandleAlign;
style?: React.CSSProperties;
Expand All @@ -64,4 +80,5 @@ export type ListItemProps<T> = {
listId?: string;
provided?: DraggableProvided;
isDragging?: boolean;
size?: ListItemSize;
};
9 changes: 9 additions & 0 deletions src/components/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {List} from '../List';
import {OuterAdditionalContent} from '../controls/common/OuterAdditionalContent/OuterAdditionalContent';
import {errorPropsMapper} from '../controls/utils';
import {useMobile} from '../mobile';
import {useThemeContext} from '../theme/useThemeContext';
import type {CnMods} from '../utils/cn';

import {EmptyOptions, SelectControl, SelectFilter, SelectList, SelectPopup} from './components';
Expand Down Expand Up @@ -90,6 +91,7 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(function
hasCounter,
renderCounter,
title,
newListView: propsNewListView,
} = props;
const mobile = useMobile();
const [{filter}, dispatch] = React.useReducer(reducer, initialState);
Expand All @@ -100,6 +102,10 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(function
const filterRef = React.useRef<SelectFilterRef>(null);
const listRef = React.useRef<List<FlattenOption>>(null);
const handleControlRef = useForkRef(ref, controlRef);
const theme = useThemeContext();

const newListView =
typeof propsNewListView === 'undefined' ? Boolean(theme?._newListView) : propsNewListView;

const handleFilterChange = React.useCallback(
(nextFilter: string) => {
Expand Down Expand Up @@ -288,6 +294,7 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(function
return (
<SelectList
ref={listRef}
newListView={newListView}
size={size}
value={value}
mobile={mobile}
Expand Down Expand Up @@ -348,6 +355,8 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(function

<SelectPopup
ref={controlWrapRef}
size={size}
newListView={newListView}
className={popupClassName}
controlRef={controlRef}
width={popupWidth}
Expand Down
8 changes: 8 additions & 0 deletions src/components/Select/__stories__/Select.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {SelectProps} from '..';

import {SelectPopupWidthShowcase} from './SelectPopupWidthShowcase';
import {SelectShowcase} from './SelectShowcase';
import {SelectWithLoader} from './SelectWithLoader';
import {UseSelectOptionsShowcase} from './UseSelectOptionsShowcase';

export default {
Expand Down Expand Up @@ -36,14 +37,19 @@ const DefaultTemplate: StoryFn<SelectProps> = (args) => (
<Select.Option value="val4" content="Value4" />
</Select>
);

const ShowcaseTemplate: StoryFn<SelectProps> = (args: SelectProps) => <SelectShowcase {...args} />;
const SelectPopupWidthShowcaseTemplate: StoryFn<SelectProps> = (args) => (
<SelectPopupWidthShowcase {...args} />
);
const UseSelectOptionsShowcaseTemplate = () => {
return <UseSelectOptionsShowcase />;
};

const TemplateWithState: StoryFn<SelectProps> = (args) => <SelectWithLoader {...args} />;

export const Default = DefaultTemplate.bind({});
export const WithLoadingMoreItems = TemplateWithState.bind({});
export const Showcase = ShowcaseTemplate.bind({});
export const PopupWidth = SelectPopupWidthShowcaseTemplate.bind({});
export const UseSelectOptions = UseSelectOptionsShowcaseTemplate.bind({});
Expand All @@ -58,3 +64,5 @@ Showcase.args = {
label: '',
hasClear: false,
};

WithLoadingMoreItems.args = {};
6 changes: 6 additions & 0 deletions src/components/Select/__stories__/SelectWithLoader.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@use '../../variables';
@use '../../../../styles/mixins';

.select-with-loader {
overflow: auto;
}
Loading

0 comments on commit ee7dcfb

Please sign in to comment.