Skip to content

Commit

Permalink
renderPopup for Select (#1312)
Browse files Browse the repository at this point in the history
  • Loading branch information
GermanVor authored Feb 7, 2024
1 parent 8c45f7c commit 70f6ba8
Show file tree
Hide file tree
Showing 9 changed files with 270 additions and 55 deletions.
87 changes: 87 additions & 0 deletions src/components/Select/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,92 @@ const MyComponent = () => {

<!--/GITHUB_BLOCK-->

### Render custom popup

To render custom popup use the `renderPopup` property.

<!--LANDING_BLOCK
<ExampleBlock
code={`
<Select
filterable
renderSelectedOption={({renderList, renderFilter}) => {
return (
<React.Fragment>
{renderFilter()}
<div className="CustomElement" />
{renderList()}
</React.Fragment>
);
}}
>
<Select.Option value="val_1" data={{color: '#8FE1A1'}}>Value 1</Select.Option>
<Select.Option value="val_2" data={{color: '#38C0A8'}}>Value 2</Select.Option>
<Select.Option value="val_3" data={{color: '#3A7AC3'}}>Value 3</Select.Option>
<Select.Option value="val_4" data={{color: '#534581'}}>Value 4</Select.Option>
</Select>
`}
>
<UIKit.Select
filterable
placeholder="Custom selected options"
renderSelectedOption={({renderList, renderFilter}) => {
return (
<React.Fragment>
{renderFilter()}
<div style={{width: "100%", height: "20px", backgroundColor: "tomato"}} />
{renderList()}
</React.Fragment>
);
}}
>
<UIKit.Select.Option value="val_1" data={{color: '#8FE1A1'}}>Value 1</UIKit.Select.Option>
<UIKit.Select.Option value="val_2" data={{color: '#38C0A8'}}>Value 2</UIKit.Select.Option>
<UIKit.Select.Option value="val_3" data={{color: '#3A7AC3'}}>Value 3</UIKit.Select.Option>
<UIKit.Select.Option value="val_4" data={{color: '#534581'}}>Value 4</UIKit.Select.Option>
</UIKit.Select>
</ExampleBlock>
LANDING_BLOCK-->

<!--GITHUB_BLOCK-->

```tsx
import type {SelectProps} from '@gravity-ui/uikit';

const renderPopup: SelectProps['renderPopup'] = ({renderList, renderFilter}) => {
return (
<React.Fragment>
{renderFilter()}
<div className="CustomElement" />
{renderList()}
</React.Fragment>
);
};

const MyComponent = () => {
return (
<Select filterable renderPopup={renderPopup}>
<Select.Option value="val_1" data={{color: '#8FE1A1'}}>
Value 1
</Select.Option>
<Select.Option value="val_2" data={{color: '#38C0A8'}}>
Value 2
</Select.Option>
<Select.Option value="val_3" data={{color: '#3A7AC3'}}>
Value 3
</Select.Option>
<Select.Option value="val_4" data={{color: '#534581'}}>
Value 4
</Select.Option>
</Select>
);
};
```

<!--/GITHUB_BLOCK-->

### Error

The state of the `Select` in which you want to indicate incorrect user input. To change `Select` appearance, use the `validationState` property with the `"invalid"` value. An optional message text can be added via the `errorMessage` property. By default, message text is rendered outside the component.
Expand Down Expand Up @@ -1028,6 +1114,7 @@ LANDING_BLOCK-->
| [renderOption](#render-custom-options) | Used to render user options | `function` | |
| renderOptionGroup | Used to render user option groups | `function` | |
| [renderSelectedOption](#render-custom-selected-options) | Used to render user selected options | `function` | |
| [renderPopup](#render-custom-popup) | Used to render user popup content | `function` | |
| [size](#size) | Control / options size | `string` | `'m'` |
| value | Values that represent selected options | `string[]` | |
| view | Control view | `string` | `'normal'` |
Expand Down
91 changes: 57 additions & 34 deletions src/components/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {DEFAULT_VIRTUALIZATION_THRESHOLD, selectBlock} from './constants';
import {useQuickSearch} from './hooks';
import {initialState, reducer} from './store';
import {Option, OptionGroup} from './tech-components';
import type {SelectProps} from './types';
import type {SelectProps, SelectRenderPopup} from './types';
import type {SelectFilterRef} from './types-misc';
import {
activateFirstClickableItem,
Expand All @@ -34,6 +34,15 @@ type SelectComponent = (<T = any>(
p: SelectProps<T> & {ref?: React.Ref<HTMLButtonElement>},
) => React.ReactElement) & {Option: typeof Option} & {OptionGroup: typeof OptionGroup};

export const DEFAULT_RENDER_POPUP: SelectRenderPopup = ({renderFilter, renderList}) => {
return (
<React.Fragment>
{renderFilter()}
{renderList()}
</React.Fragment>
);
};

export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(function Select<T = any>(
props: SelectProps<T>,
ref: React.Ref<HTMLButtonElement>,
Expand All @@ -48,6 +57,7 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(function
renderOptionGroup,
renderSelectedOption,
renderEmptyOptions,
renderPopup = DEFAULT_RENDER_POPUP,
getOptionHeight,
getOptionGroupHeight,
filterOption,
Expand Down Expand Up @@ -245,6 +255,51 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(function
),
});

const _renderFilter = () => {
if (filterable) {
return (
<SelectFilter
ref={filterRef}
size={size}
value={filter}
placeholder={filterPlaceholder}
onChange={handleFilterChange}
onKeyDown={handleFilterKeyDown}
renderFilter={renderFilter}
/>
);
}

return null;
};

const _renderList = () => {
if (filteredFlattenOptions.length || props.loading) {
return (
<SelectList
ref={listRef}
size={size}
value={value}
mobile={mobile}
flattenOptions={filteredFlattenOptions}
multiple={multiple}
virtualized={virtualized}
onOptionClick={handleOptionClick}
renderOption={renderOption}
renderOptionGroup={renderOptionGroup}
getOptionHeight={getOptionHeight}
getOptionGroupHeight={getOptionGroupHeight}
loading={props.loading}
onLoadMore={props.onLoadMore}
selectId={`select-${selectId}`}
onChangeActive={setActiveIndex}
/>
);
}

return <EmptyOptions filter={filter} renderEmptyOptions={renderEmptyOptions} />;
};

return (
<div
ref={controlWrapRef}
Expand Down Expand Up @@ -291,39 +346,7 @@ export const Select = React.forwardRef<HTMLButtonElement, SelectProps>(function
id={`select-popup-${selectId}`}
placement={popupPlacement}
>
{filterable && (
<SelectFilter
ref={filterRef}
size={size}
value={filter}
placeholder={filterPlaceholder}
onChange={handleFilterChange}
onKeyDown={handleFilterKeyDown}
renderFilter={renderFilter}
/>
)}
{filteredFlattenOptions.length || props.loading ? (
<SelectList
ref={listRef}
size={size}
value={value}
mobile={mobile}
flattenOptions={filteredFlattenOptions}
multiple={multiple}
virtualized={virtualized}
onOptionClick={handleOptionClick}
renderOption={renderOption}
renderOptionGroup={renderOptionGroup}
getOptionHeight={getOptionHeight}
getOptionGroupHeight={getOptionGroupHeight}
loading={props.loading}
onLoadMore={props.onLoadMore}
selectId={`select-${selectId}`}
onChangeActive={setActiveIndex}
/>
) : (
<EmptyOptions filter={filter} renderEmptyOptions={renderEmptyOptions} />
)}
{renderPopup({renderFilter: _renderFilter, renderList: _renderList})}
</SelectPopup>

<OuterAdditionalContent
Expand Down
30 changes: 25 additions & 5 deletions src/components/Select/__stories__/SelectShowcase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,7 @@ const ExampleItem = (props: {
)}
</h3>
{mode === Mode.VIEW ? (
<Select
{...selectProps}
value={value}
onUpdate={(nextValue) => setValue(nextValue)}
>
<Select {...selectProps} value={value} onUpdate={setValue}>
{children}
</Select>
) : (
Expand Down Expand Up @@ -414,6 +410,30 @@ export const SelectShowcase = (props: SelectProps) => {
<Select.Option value="val4" content="Value4" />
</ExampleItem>
</div>

<ExampleItem
title="Select with custom popup"
selectProps={{
...props,
filterable: true,
renderPopup: ({renderFilter, renderList}) => {
return (
<React.Fragment>
<div>{'---- Before Filter ----'}</div>
{renderFilter()}
<div>{'---- After Filter, Before List ----'}</div>
{renderList()}
<div>{'---- After List ----'}</div>
</React.Fragment>
);
},
}}
>
<Select.Option value="val1" content="Value1" />
<Select.Option value="val2" content="Value2" />
<Select.Option value="val3" content="Value3" />
<Select.Option value="val4" content="Value4" />
</ExampleItem>
</div>
);
};
23 changes: 17 additions & 6 deletions src/components/Select/__tests__/Select.filter.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import userEvent from '@testing-library/user-event';

import {cleanup} from '../../../../test-utils/utils';
import {TextInput} from '../../controls';
import type {SelectOption, SelectProps} from '../types';
import type {SelectOption, SelectProps, SelectRenderPopup} from '../types';

import {TEST_QA, generateOptions, generateOptionsGroups, setup} from './utils';

Expand All @@ -17,7 +17,7 @@ const onFilterChange = jest.fn();
const FILTER_PLACEHOLDER = 'Filter placeholder';
const EMPTY_OPTIONS_QA = 'empty-options';

const renderCustomFilter: SelectProps['renderFilter'] = (props) => {
const RENDER_CUSTOM_FILTER: SelectProps['renderFilter'] = (props) => {
const {value, ref, onChange, onKeyDown} = props;

return (
Expand All @@ -31,17 +31,28 @@ const renderCustomFilter: SelectProps['renderFilter'] = (props) => {
);
};

const RENDER_POPUP: SelectRenderPopup = ({renderList, renderFilter}) => {
return (
<React.Fragment>
{renderFilter()}
{renderList()}
</React.Fragment>
);
};

describe('Select filter', () => {
test.each<[string, Partial<SelectProps>]>([
['default', {renderFilter: undefined}],
['custom', {renderFilter: renderCustomFilter}],
])('base functional with %s filter section', async (_, {renderFilter}) => {
test.each([
['default', undefined, undefined],
['custom', RENDER_CUSTOM_FILTER, RENDER_POPUP],
['custom', RENDER_CUSTOM_FILTER, undefined],
])('base functional with %s filter section', async (_, renderFilter, renderPopup) => {
const {getByTestId, getByPlaceholderText, getAllByRole, queryAllByRole} = setup({
options: generateOptions(40),
filterPlaceholder: FILTER_PLACEHOLDER,
filterable: true,
onFilterChange,
renderFilter,
renderPopup,
});
const user = userEvent.setup();
const selectControl = getByTestId(TEST_QA);
Expand Down
57 changes: 57 additions & 0 deletions src/components/Select/__tests__/Select.renderPopup.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react';

import userEvent from '@testing-library/user-event';

import {SelectQa} from '../constants';

import {DEFAULT_OPTIONS, TEST_QA, setup} from './utils';

const QA = 'SELECT_RENDER_POPUP_TEST_QA';

describe('Select renderPopup', () => {
test('default case', async () => {
const {getByTestId} = setup({
options: DEFAULT_OPTIONS,
filterable: true,
renderPopup: ({renderFilter, renderList}) => {
return (
<React.Fragment>
{renderFilter()}
<div data-qa={QA} />
{renderList()}
</React.Fragment>
);
},
});

const user = userEvent.setup();
const selectControl = getByTestId(TEST_QA);
// open select popup
await user.click(selectControl);

const filterInput = getByTestId(SelectQa.FILTER_INPUT);
expect(filterInput).toBeVisible();

const list = getByTestId(SelectQa.LIST);
expect(list).toBeVisible();

const customPopupDiv = getByTestId(QA);
expect(customPopupDiv).toBeVisible();
});

test('empty options', async () => {
const {getByTestId} = setup({
options: [],
renderEmptyOptions: () => <div data-qa={QA} />,
renderPopup: ({renderList}) => renderList(),
});

const user = userEvent.setup();
const selectControl = getByTestId(TEST_QA);
// open select popup
await user.click(selectControl);

const emptyContent = getByTestId(QA);
expect(emptyContent).toBeVisible();
});
});
Loading

0 comments on commit 70f6ba8

Please sign in to comment.