Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Controlled search in Select/MultiSelect #2485

Merged
merged 4 commits into from Sep 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/src/docs/core/MultiSelect.mdx
Expand Up @@ -106,6 +106,12 @@ Set `searchable` prop to enable search in select and `nothingFound` prop to prov

<Demo data={MultiSelectDemos.searchable} />

## Controlled search

Set `searchValue` and `onSearchChange` prop to enable controlled search in select:

<Demo data={MultiSelectDemos.searchableControlled} />

## Clearable

Set `clearable` prop to enable clearing all values at once.
Expand Down
6 changes: 6 additions & 0 deletions docs/src/docs/core/Select.mdx
Expand Up @@ -106,6 +106,12 @@ Set `searchable` prop to enable search in select and `nothingFound` prop to prov

<Demo data={SelectDemos.searchable} />

## Controlled search

Set `searchValue` and `onSearchChange` prop to enable controlled search in select:

<Demo data={SelectDemos.searchableControlled} />

## Clearable

Set `clearable` prop to enable clearing select value.
Expand Down
30 changes: 17 additions & 13 deletions src/mantine-core/src/MultiSelect/MultiSelect.tsx
Expand Up @@ -65,6 +65,9 @@ export interface MultiSelectProps
/** Called each time search query changes */
onSearchChange?(query: string): void;

/** Controlled search input value */
searchValue?: string;

/** Allow creatable option */
creatable?: boolean;

Expand Down Expand Up @@ -177,6 +180,7 @@ export const MultiSelect = forwardRef<HTMLInputElement, MultiSelectProps>((props
clearButtonLabel,
variant,
onSearchChange,
searchValue,
disabled,
initiallyOpened,
radius,
Expand Down Expand Up @@ -225,7 +229,12 @@ export const MultiSelect = forwardRef<HTMLInputElement, MultiSelectProps>((props
const [dropdownOpened, _setDropdownOpened] = useState(initiallyOpened);
const [hovered, setHovered] = useState(-1);
const [direction, setDirection] = useState<React.CSSProperties['flexDirection']>('column');
const [searchValue, setSearchValue] = useState('');
const [_searchValue, handleSearchChange] = useUncontrolled({
value: searchValue,
defaultValue: '',
finalValue: undefined,
onChange: onSearchChange,
});
const [IMEOpen, setIMEOpen] = useState(false);

const { scrollIntoView, targetRef, scrollableRef } = useScrollIntoView({
Expand All @@ -244,11 +253,6 @@ export const MultiSelect = forwardRef<HTMLInputElement, MultiSelectProps>((props
typeof handler === 'function' && handler();
};

const handleSearchChange = (val: string) => {
typeof onSearchChange === 'function' && onSearchChange(val);
setSearchValue(val);
};

const formattedData = data.map((item) =>
typeof item === 'string' ? { label: item, value: item } : item
);
Expand Down Expand Up @@ -288,7 +292,7 @@ export const MultiSelect = forwardRef<HTMLInputElement, MultiSelectProps>((props
const filteredData = filterData({
data: sortedData,
searchable,
searchValue,
searchValue: _searchValue,
limit,
filter,
value: _value,
Expand All @@ -309,7 +313,7 @@ export const MultiSelect = forwardRef<HTMLInputElement, MultiSelectProps>((props

useDidUpdate(() => {
setHovered(-1);
}, [searchValue]);
}, [_searchValue]);

useDidUpdate(() => {
if (!disabled && _value.length > data.length) {
Expand Down Expand Up @@ -472,7 +476,7 @@ export const MultiSelect = forwardRef<HTMLInputElement, MultiSelectProps>((props
}

case 'Backspace': {
if (_value.length > 0 && searchValue.length === 0) {
if (_value.length > 0 && _searchValue.length === 0) {
setValue(_value.slice(0, -1));
setDropdownOpened(true);
if (maxSelectedValues) {
Expand Down Expand Up @@ -563,9 +567,9 @@ export const MultiSelect = forwardRef<HTMLInputElement, MultiSelectProps>((props
}
};

if (isCreatable && shouldCreate(searchValue, sortedData)) {
createLabel = getCreateLabel(searchValue);
filteredData.push({ label: searchValue, value: searchValue, creatable: true });
if (isCreatable && shouldCreate(_searchValue, sortedData)) {
createLabel = getCreateLabel(_searchValue);
filteredData.push({ label: _searchValue, value: _searchValue, creatable: true });
}

const shouldRenderDropdown =
Expand Down Expand Up @@ -682,7 +686,7 @@ export const MultiSelect = forwardRef<HTMLInputElement, MultiSelectProps>((props
[classes.searchInputEmpty]: _value.length === 0,
})}
onKeyDown={handleInputKeydown}
value={searchValue}
value={_searchValue}
onChange={handleInputChange}
onFocus={handleInputFocus}
onBlur={handleInputBlur}
Expand Down
12 changes: 11 additions & 1 deletion src/mantine-core/src/Select/Select.tsx
Expand Up @@ -97,6 +97,9 @@ export interface SelectProps
/** Called each time search value changes */
onSearchChange?(query: string): void;

/** Controlled search input value */
searchValue?: string;

/** Allow creatable option */
creatable?: boolean;

Expand Down Expand Up @@ -187,6 +190,7 @@ export const Select = forwardRef<HTMLInputElement, SelectProps>((props, ref) =>
limit,
disabled,
onSearchChange,
searchValue,
rightSection,
rightSectionWidth,
creatable,
Expand Down Expand Up @@ -253,7 +257,13 @@ export const Select = forwardRef<HTMLInputElement, SelectProps>((props, ref) =>
});

const selectedValue = sortedData.find((item) => item.value === _value);
const [inputValue, setInputValue] = useState(selectedValue?.label || '');

const [inputValue, setInputValue] = useUncontrolled({
value: searchValue,
defaultValue: selectedValue?.label || '',
finalValue: undefined,
onChange: onSearchChange,
});

const handleSearchChange = (val: string) => {
setInputValue(val);
Expand Down
@@ -0,0 +1,47 @@
import React, { useState } from 'react';
import { MultiSelect } from '@mantine/core';
import { data } from './_data';

const code = `
import { MultiSelect } from '@mantine/core';

function Demo() {
const [searchValue, onSearchChange] = useState('');

return (
<MultiSelect
data={['React', 'Angular', 'Svelte', 'Vue', 'Riot', 'Next.js', 'Blitz.js']}
label="Your favorite frameworks/libraries"
placeholder="Pick all that you like"
searchable
searchValue={searchValue}
onSearchChange={onSearchChange}
nothingFound="Nothing found"
/>
);
}
`;

function Demo() {
const [searchValue, onSearchChange] = useState('');

return (
<div style={{ maxWidth: 400, marginLeft: 'auto', marginRight: 'auto' }}>
<MultiSelect
data={data}
label="Your favorite frameworks/libraries"
placeholder="Pick all that you like"
searchable
searchValue={searchValue}
onSearchChange={onSearchChange}
nothingFound="Nothing found"
/>
</div>
);
}

export const searchableControlled: MantineDemo = {
type: 'demo',
code,
component: Demo,
};
1 change: 1 addition & 0 deletions src/mantine-demos/src/demos/core/MultiSelect/index.ts
@@ -1,6 +1,7 @@
export { countries } from './MultiSelect.demo.countries';
export { usage } from './MultiSelect.demo.usage';
export { searchable } from './MultiSelect.demo.searchable';
export { searchableControlled } from './MultiSelect.demo.searchableControlled';
export { clearable } from './MultiSelect.demo.clearable';
export { configurator } from './MultiSelect.demo.configurator';
export { flip } from './MultiSelect.demo.flip';
Expand Down
@@ -0,0 +1,46 @@
import React, { useState } from 'react';
import { Select } from '@mantine/core';

const code = `
import { Select } from '@mantine/core';

function Demo() {
const [searchValue, onSearchChange] = useState('');

return (
<Select
label="Your favorite framework/library"
placeholder="Pick one"
searchable
onSearchChange={onSearchChange}
searchValue={searchValue}
nothingFound="No options"
data={['React', 'Angular', 'Svelte', 'Vue']}
/>
);
}
`;

function Demo() {
const [searchValue, onSearchChange] = useState('');

return (
<div style={{ maxWidth: 320, marginLeft: 'auto', marginRight: 'auto' }}>
<Select
label="Your favorite framework/library"
placeholder="Pick one"
searchable
onSearchChange={onSearchChange}
searchValue={searchValue}
nothingFound="No options"
data={['React', 'Angular', 'Svelte', 'Vue']}
/>
</div>
);
}

export const searchableControlled: MantineDemo = {
type: 'demo',
code,
component: Demo,
};
1 change: 1 addition & 0 deletions src/mantine-demos/src/demos/core/Select/index.ts
@@ -1,5 +1,6 @@
export { usage } from './Select.demo.usage';
export { searchable } from './Select.demo.searchable';
export { searchableControlled } from './Select.demo.searchableControlled';
export { large } from './Select.demo.large';
export { configurator } from './Select.demo.configurator';
export { flip } from './Select.demo.flip';
Expand Down