Skip to content

Commit

Permalink
[@mantine/core] Add controlled search value support to Select and Mul…
Browse files Browse the repository at this point in the history
…tiSelect components (#2485)

* [@mantine/core] MultiSelect: Added controlled/uncontrolled search value

* [@mantine/core] Select: Added controlled/uncontrolled search value

* [docs] Added examples with controlled search input in Select/MultiSelect

* [@mantine/core] Select: fixed handleSearchChange
  • Loading branch information
dipiash committed Sep 19, 2022
1 parent 8ceac6e commit e21e2b4
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 14 deletions.
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

0 comments on commit e21e2b4

Please sign in to comment.