diff --git a/docs/src/docs/core/MultiSelect.mdx b/docs/src/docs/core/MultiSelect.mdx index b9828d7dcbf..4a2297e9eed 100644 --- a/docs/src/docs/core/MultiSelect.mdx +++ b/docs/src/docs/core/MultiSelect.mdx @@ -106,6 +106,12 @@ Set `searchable` prop to enable search in select and `nothingFound` prop to prov +## Controlled search + +Set `searchValue` and `onSearchChange` prop to enable controlled search in select: + + + ## Clearable Set `clearable` prop to enable clearing all values at once. diff --git a/docs/src/docs/core/Select.mdx b/docs/src/docs/core/Select.mdx index 1b72a9a3525..552553d5b82 100644 --- a/docs/src/docs/core/Select.mdx +++ b/docs/src/docs/core/Select.mdx @@ -106,6 +106,12 @@ Set `searchable` prop to enable search in select and `nothingFound` prop to prov +## Controlled search + +Set `searchValue` and `onSearchChange` prop to enable controlled search in select: + + + ## Clearable Set `clearable` prop to enable clearing select value. diff --git a/src/mantine-core/src/MultiSelect/MultiSelect.tsx b/src/mantine-core/src/MultiSelect/MultiSelect.tsx index 00976578452..82e36307d35 100644 --- a/src/mantine-core/src/MultiSelect/MultiSelect.tsx +++ b/src/mantine-core/src/MultiSelect/MultiSelect.tsx @@ -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; @@ -177,6 +180,7 @@ export const MultiSelect = forwardRef((props clearButtonLabel, variant, onSearchChange, + searchValue, disabled, initiallyOpened, radius, @@ -225,7 +229,12 @@ export const MultiSelect = forwardRef((props const [dropdownOpened, _setDropdownOpened] = useState(initiallyOpened); const [hovered, setHovered] = useState(-1); const [direction, setDirection] = useState('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({ @@ -244,11 +253,6 @@ export const MultiSelect = forwardRef((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 ); @@ -288,7 +292,7 @@ export const MultiSelect = forwardRef((props const filteredData = filterData({ data: sortedData, searchable, - searchValue, + searchValue: _searchValue, limit, filter, value: _value, @@ -309,7 +313,7 @@ export const MultiSelect = forwardRef((props useDidUpdate(() => { setHovered(-1); - }, [searchValue]); + }, [_searchValue]); useDidUpdate(() => { if (!disabled && _value.length > data.length) { @@ -472,7 +476,7 @@ export const MultiSelect = forwardRef((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) { @@ -563,9 +567,9 @@ export const MultiSelect = forwardRef((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 = @@ -682,7 +686,7 @@ export const MultiSelect = forwardRef((props [classes.searchInputEmpty]: _value.length === 0, })} onKeyDown={handleInputKeydown} - value={searchValue} + value={_searchValue} onChange={handleInputChange} onFocus={handleInputFocus} onBlur={handleInputBlur} diff --git a/src/mantine-core/src/Select/Select.tsx b/src/mantine-core/src/Select/Select.tsx index 3919b905b04..11ff498e8f5 100644 --- a/src/mantine-core/src/Select/Select.tsx +++ b/src/mantine-core/src/Select/Select.tsx @@ -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; @@ -187,6 +190,7 @@ export const Select = forwardRef((props, ref) => limit, disabled, onSearchChange, + searchValue, rightSection, rightSectionWidth, creatable, @@ -253,7 +257,13 @@ export const Select = forwardRef((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); diff --git a/src/mantine-demos/src/demos/core/MultiSelect/MultiSelect.demo.searchableControlled.tsx b/src/mantine-demos/src/demos/core/MultiSelect/MultiSelect.demo.searchableControlled.tsx new file mode 100644 index 00000000000..8f367b64338 --- /dev/null +++ b/src/mantine-demos/src/demos/core/MultiSelect/MultiSelect.demo.searchableControlled.tsx @@ -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 ( + + ); +} +`; + +function Demo() { + const [searchValue, onSearchChange] = useState(''); + + return ( +
+ +
+ ); +} + +export const searchableControlled: MantineDemo = { + type: 'demo', + code, + component: Demo, +}; diff --git a/src/mantine-demos/src/demos/core/MultiSelect/index.ts b/src/mantine-demos/src/demos/core/MultiSelect/index.ts index 838beda8a1c..4dc7b80e08c 100644 --- a/src/mantine-demos/src/demos/core/MultiSelect/index.ts +++ b/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'; diff --git a/src/mantine-demos/src/demos/core/Select/Select.demo.searchableControlled.tsx b/src/mantine-demos/src/demos/core/Select/Select.demo.searchableControlled.tsx new file mode 100644 index 00000000000..545c61d078b --- /dev/null +++ b/src/mantine-demos/src/demos/core/Select/Select.demo.searchableControlled.tsx @@ -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 ( + + + ); +} + +export const searchableControlled: MantineDemo = { + type: 'demo', + code, + component: Demo, +}; diff --git a/src/mantine-demos/src/demos/core/Select/index.ts b/src/mantine-demos/src/demos/core/Select/index.ts index 1bdc8c7eed5..3f2af24aba6 100644 --- a/src/mantine-demos/src/demos/core/Select/index.ts +++ b/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';