Skip to content

Commit

Permalink
[PickerInput]: rework DataPickerBody and DataPickerRow from class com…
Browse files Browse the repository at this point in the history
…ponents to FC
  • Loading branch information
AlekseyManetov committed Feb 27, 2025
1 parent 13c1f81 commit d481f0c
Show file tree
Hide file tree
Showing 10 changed files with 111 additions and 130 deletions.
39 changes: 0 additions & 39 deletions uui-components/src/pickers/PickerBodyBase.tsx

This file was deleted.

9 changes: 4 additions & 5 deletions uui-components/src/pickers/hooks/usePickerInput.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import React, { useMemo, useEffect, useCallback, useContext } from 'react';
import { Modifier } from 'react-popper';
import {
DataRowProps, isMobile, mobilePopperModifier, Lens, PickerFooterProps, DataSourceState, UuiContext,
DataRowProps, isMobile, mobilePopperModifier, Lens, PickerFooterProps, DataSourceState, UuiContext, IHasRawProps,
} from '@epam/uui-core';
import { PickerTogglerProps } from '../PickerToggler';
import { PickerBodyBaseProps } from '../PickerBodyBase';
import { applyValueToDataSourceState, dataSourceStateToValue } from '../bindingHelpers';
import { handleDataSourceKeyboard } from '../KeyboardUtils';
import { i18n } from '../../i18n';
Expand Down Expand Up @@ -169,7 +168,7 @@ export function usePickerInput<TItem, TId, TProps>(props: UsePickerInputProps<TI
);
};

const getPickerBodyProps = (rows: DataRowProps<TItem, TId>[]): Omit<PickerBodyBaseProps, 'rows'> => {
const getPickerBodyProps = (rows: DataRowProps<TItem, TId>[]) => {
return {
value: getDataSourceState(),
onValueChange: handleDataSourceValueChange,
Expand All @@ -179,10 +178,10 @@ export function usePickerInput<TItem, TId, TProps>(props: UsePickerInputProps<TI
'aria-multiselectable': props.selectionMode === 'multi' ? true : null,
'aria-orientation': 'vertical',
...props.rawProps?.body,
},
} as IHasRawProps<React.HTMLAttributes<HTMLDivElement>>['rawProps'],
renderNotFound: props.renderNotFound,
renderEmpty: props.renderEmpty,
onKeyDown: (e) => handlePickerInputKeyboard(rows, e),
onKeyDown: (e: React.KeyboardEvent<HTMLElement>) => handlePickerInputKeyboard(rows, e),
minCharsToSearch: props.minCharsToSearch,
fixedBodyPosition: props.fixedBodyPosition,
searchDebounceDelay: props.searchDebounceDelay,
Expand Down
1 change: 0 additions & 1 deletion uui-components/src/pickers/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export * from './bindingHelpers';
export * from './PickerBodyBase';
export * from './KeyboardUtils';
export * from './PickerToggler';
export * from './DataPickerRow';
Expand Down
2 changes: 1 addition & 1 deletion uui-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"csstype": "2.6.10",
"dayjs": "1.11.12",
"lodash.debounce": "4.0.8",
"react-fast-compare": "^3.2.2",
"react-fast-compare": "3.2.2",
"react-popper": "2.3.0"
},
"peerDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions uui/components/filters/FilterPickerBody.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import { DataRowProps, DataSourceListProps, DataSourceState, DropdownBodyProps, isMobile, PickerFilterConfig, usePrevious, PickerInputBaseProps } from '@epam/uui-core';
import { PickerBodyBaseProps, usePickerInput } from '@epam/uui-components';
import { usePickerInput } from '@epam/uui-components';
import { DataPickerRow, PickerItem, DataPickerBody, DataPickerFooter, PickerInputProps, PickerItemProps, DataPickerRowProps, DataPickerFooterProps, DataPickerBodyProps } from '../pickers';
import { settings } from '../../settings';

Expand Down Expand Up @@ -93,7 +93,7 @@ export function FilterPickerBody<TItem, TId>({
return props.renderFooter ? props.renderFooter(footerProps) : <DataPickerFooter { ...footerProps } size={ settings.sizes.filtersPanel.pickerInput.body.default as DataPickerFooterProps<any, any>['size'] } />;
};

const renderBody = (bodyProps: DataSourceListProps & Omit<PickerBodyBaseProps, 'rows'>, rows: DataRowProps<TItem, TId>[]) => {
const renderBody = (bodyProps: DataSourceListProps & Omit<DataPickerBodyProps, 'rows'>, rows: DataRowProps<TItem, TId>[]) => {
const renderedDataRows = rows.map((props) => renderRow(props, dataSourceState));
const maxHeight = isMobile() ? document.documentElement.clientHeight : props.maxBodyHeight || pickerHeight;
const maxWidth = isMobile() ? undefined : 360;
Expand Down
146 changes: 85 additions & 61 deletions uui/components/pickers/DataPickerBody.tsx
Original file line number Diff line number Diff line change
@@ -1,109 +1,133 @@
import React from 'react';
import React, { useEffect, useRef } from 'react';
import {
Lens, DataSourceState, isMobile, cx, Overwrite, IDropdownBodyProps, devLogger,
DataSourceState, isMobile, cx, Overwrite, IDropdownBodyProps, devLogger, DataSourceListProps, IEditable,
IHasRawProps, PickerInputBaseProps, usePrevious,
} from '@epam/uui-core';
import { FlexCell, PickerBodyBase, PickerBodyBaseProps } from '@epam/uui-components';
import { FlexCell } from '@epam/uui-components';
import { SearchInput, SearchInputProps } from '../inputs';
import { FlexRow, VirtualList } from '../layout';
import { Text } from '../typography';
import { i18n } from '../../i18n';
import { ControlSize } from '../types';
import { settings } from '../../settings';
import css from './DataPickerBody.module.scss';
import isEqual from 'react-fast-compare';

export interface DataPickerBodyModsOverride {}

interface DataPickerBodyMods {
searchSize?: ControlSize;
}

export interface DataPickerBodyProps extends Overwrite<DataPickerBodyMods, DataPickerBodyModsOverride>, PickerBodyBaseProps, IDropdownBodyProps {
export interface DataPickerBodyProps extends Overwrite<DataPickerBodyMods, DataPickerBodyModsOverride>,
DataSourceListProps, IEditable<DataSourceState>, IHasRawProps<React.HTMLAttributes<HTMLDivElement>>, IDropdownBodyProps,
Pick<PickerInputBaseProps<any, any>, 'minCharsToSearch' | 'renderEmpty' | 'renderNotFound' | 'fixedBodyPosition' | 'searchDebounceDelay'> {
maxHeight?: number;
editMode?: 'dropdown' | 'modal';
selectionMode?: 'single' | 'multi';
maxWidth?: number;
onKeyDown?(e: React.KeyboardEvent<HTMLElement>): void;
rows: React.ReactNode[];
scheduleUpdate?: () => void;
search: IEditable<string>;
showSearch?: boolean | 'auto';
}

export class DataPickerBody extends PickerBodyBase<DataPickerBodyProps> {
lens = Lens.onEditableComponent<DataSourceState>(this);
searchLens = this.lens.prop('search').default('');
getSearchSize = () => (isMobile() ? settings.sizes.pickerInput.body.mobile.searchInput : this.props.searchSize) as SearchInputProps['size'];
export function DataPickerBody(props:DataPickerBodyProps) {
const prevProps = usePrevious(props);

renderEmpty() {
const search = this.searchLens.get();
const showSearch = props.showSearch === 'auto' ? props.totalCount > 10 : Boolean(props.showSearch);

const searchRef = useRef<HTMLInputElement>(null);

if (this.props.renderEmpty) {
return this.props.renderEmpty({
minCharsToSearch: this.props.minCharsToSearch,
onClose: this.props.onClose,
useEffect(() => {
if (showSearch && !isMobile()) {
searchRef.current?.focus({ preventScroll: true });
}
}, [showSearch]);

useEffect(() => {
if (props.rows.length !== prevProps?.rows.length || (!isEqual(prevProps?.value.checked, props.value.checked) && !props.fixedBodyPosition)) {
props.scheduleUpdate?.();
}
}, [props.rows, prevProps, props.value.checked, props.fixedBodyPosition]);

const searchKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
props.onKeyDown?.(e);
if (e.shiftKey && e.key === 'Tab') e.preventDefault();
};

const renderEmpty = () => {
const search = props.search.value;

if (props.renderEmpty) {
return props.renderEmpty({
minCharsToSearch: props.minCharsToSearch,
onClose: props.onClose,
search: search,
});
}

if (this.props.minCharsToSearch && search.length < this.props.minCharsToSearch) {
if (props.minCharsToSearch && search.length < props.minCharsToSearch) {
return (
<FlexCell cx={ css.noData } grow={ 1 } textAlign="center">
<Text size={ this.props.searchSize }>{i18n.dataPickerBody.typeSearchToLoadMessage}</Text>
<Text size={ props.searchSize }>{i18n.dataPickerBody.typeSearchToLoadMessage}</Text>
</FlexCell>
);
}

if (this.props.rows.length === 0) {
if (this.props.renderNotFound) {
if (props.rows.length === 0) {
if (props.renderNotFound) {
if (__DEV__) {
devLogger.warn('[PickerInput]: renderNotFound prop is deprecated. Please use renderEmpty prop instead.');
}

return this.props.renderNotFound({
onClose: this.props.onClose,
search: this.searchLens.get(),
return props.renderNotFound({
onClose: props.onClose,
search: search,
});
}

// Default no record found message for 'NOT_FOUND' and "NO_RECORDS" reason
// TODO: make separate messages for 'NOT_FOUND' and "NO_RECORDS" reason
return (
<FlexCell cx={ css.noData } grow={ 1 } textAlign="center">
<Text size={ this.props.searchSize }>{i18n.dataPickerBody.noRecordsMessage}</Text>
<Text size={ props.searchSize }>{i18n.dataPickerBody.noRecordsMessage}</Text>
</FlexCell>
);
}
}
};

render() {
const searchSize = this.getSearchSize();
return (
<>
{this.showSearch() && (
<div key="search" className={ css.searchWrapper }>
<FlexCell grow={ 1 }>
<SearchInput
ref={ this.searchRef }
placeholder={ i18n.dataPickerBody.searchPlaceholder }
{ ...this.searchLens.toProps() }
onKeyDown={ this.searchKeyDown }
size={ searchSize }
debounceDelay={ this.props.searchDebounceDelay }
rawProps={ { dir: 'auto' } }
/>
</FlexCell>
</div>
)}
<FlexRow key="body" cx={ cx('uui-picker_input-body', css[this.props.editMode], css[this.props.selectionMode]) } rawProps={ { style: { maxHeight: this.props.maxHeight, maxWidth: this.props.maxWidth } } }>
{ this.props.rows.length === 0 && this.props.value.topIndex === 0
// We need to also ensure that topIndex === 0, because we can have state were there is no rows but topIndex > 0, in case when we scrolled lover than we have rows
// we fix this state on next render and shouldn't show empty state.
? this.renderEmpty() : (
<VirtualList
{ ...this.lens.toProps() }
rows={ this.props.rows }
rawProps={ this.props.rawProps }
rowsCount={ this.props.rowsCount }
isLoading={ this.props.isReloading }
/>
)}
</FlexRow>
</>
);
}
const searchSize = isMobile() ? settings.sizes.pickerInput.body.mobile.searchInput as SearchInputProps['size'] : props.searchSize;
return (
<>
{showSearch && (
<div key="search" className={ css.searchWrapper }>
<FlexCell grow={ 1 }>
<SearchInput
ref={ searchRef }
placeholder={ i18n.dataPickerBody.searchPlaceholder }
value={ props.search.value }
onValueChange={ props.search.onValueChange }
onKeyDown={ searchKeyDown }
size={ searchSize }
debounceDelay={ props.searchDebounceDelay }
rawProps={ { dir: 'auto' } }
/>
</FlexCell>
</div>
)}
<FlexRow key="body" cx={ cx('uui-picker_input-body', css[props.editMode], css[props.selectionMode]) } rawProps={ { style: { maxHeight: props.maxHeight, maxWidth: props.maxWidth } } }>
{ props.rows.length === 0 && props.value.topIndex === 0
? renderEmpty() : (
<VirtualList
value={ props.value }
onValueChange={ props.onValueChange }
rows={ props.rows }
rawProps={ props.rawProps }
rowsCount={ props.rowsCount }
isLoading={ props.isReloading }
/>
)}
</FlexRow>
</>
);
}
30 changes: 14 additions & 16 deletions uui/components/pickers/DataPickerRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,25 @@ export interface DataPickerRowProps<TItem, TId> extends Overwrite<DataPickerRowM
renderItem(item: TItem, rowProps: DataRowProps<TItem, TId>): React.ReactNode;
}

export class DataPickerRow<TItem, TId> extends React.Component<DataPickerRowProps<TItem, TId>> {
renderContent = () => {
export function DataPickerRow<TItem, TId>(props: DataPickerRowProps<TItem, TId>) {
const renderContent = () => {
return (
<DataPickerCell
key="name"
size={ this.props.size || (settings.sizes.pickerInput.body.dropdown.row.default as PickerCellMods['size']) }
padding={ this.props.padding || (settings.sizes.pickerInput.body.dropdown.row.cell.padding as PickerCellMods['padding']) }
rowProps={ this.props }
alignActions={ this.props.alignActions || 'top' }
renderItem={ this.props.renderItem }
size={ props.size || (settings.sizes.pickerInput.body.dropdown.row.default as PickerCellMods['size']) }
padding={ props.padding || (settings.sizes.pickerInput.body.dropdown.row.cell.padding as PickerCellMods['padding']) }
rowProps={ props }
alignActions={ props.alignActions || 'top' }
renderItem={ props.renderItem }
/>
);
};

render() {
return (
<UUIDataPickerRow
{ ...this.props }
cx={ [css.pickerRow, this.props.cx] }
renderContent={ this.renderContent }
/>
);
}
return (
<UUIDataPickerRow
{ ...props }
cx={ [css.pickerRow, props.cx] }
renderContent={ renderContent }
/>
);
}
6 changes: 3 additions & 3 deletions uui/components/pickers/PickerInput.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useImperativeHandle, useRef } from 'react';
import { PickerTogglerRenderItemParams, PickerBodyBaseProps, PickerTogglerProps, usePickerInput } from '@epam/uui-components';
import { PickerTogglerRenderItemParams, PickerTogglerProps, usePickerInput } from '@epam/uui-components';
import { Dropdown } from '../overlays/Dropdown';
import { EditMode, IHasEditMode, SizeMod } from '../types';
import {
Expand All @@ -9,7 +9,7 @@ import {
import { PickerModal } from './PickerModal';
import { PickerToggler, PickerTogglerMods } from './PickerToggler';
import { PickerBodyMobileView } from './PickerBodyMobileView';
import { DataPickerBody } from './DataPickerBody';
import { DataPickerBody, DataPickerBodyProps } from './DataPickerBody';
import { DataPickerRow, DataPickerRowProps } from './DataPickerRow';
import { DataPickerFooter } from './DataPickerFooter';
import { PickerItem, PickerItemProps } from './PickerItem';
Expand Down Expand Up @@ -164,7 +164,7 @@ function PickerInputComponent<TItem, TId>({ highlightSearchMatches = true, ...pr
);
};

const renderBody = (bodyProps: DropdownBodyProps & DataSourceListProps & Omit<PickerBodyBaseProps, 'rows'>, rows: DataRowProps<TItem, TId>[]) => {
const renderBody = (bodyProps: DropdownBodyProps & DataSourceListProps & Omit<DataPickerBodyProps, 'rows'>, rows: DataRowProps<TItem, TId>[]) => {
const renderedDataRows = rows.map((row) => renderRow(row, dataSourceState));
const bodyHeight = isMobile() ? document.documentElement.clientHeight : props.dropdownHeight || settings.sizes.pickerInput.body.dropdown.height;
const minBodyWidth = props.minBodyWidth || settings.sizes.pickerInput.body.dropdown.width;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ Array [
/>
</div>
<input
aria-invalid={false}
className="uui-input"
dir="ltr"
inputMode="search"
Expand Down
3 changes: 2 additions & 1 deletion uui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"classnames": "2.2.6",
"dayjs": "1.11.12",
"react-focus-lock": "2.13.5",
"react-popper": "2.3.0"
"react-popper": "2.3.0",
"react-fast-compare": "3.2.2"
},
"peerDependencies": {
"react": ">=16.0.0",
Expand Down

0 comments on commit d481f0c

Please sign in to comment.