diff --git a/docs/src/docs/core/TransferList.mdx b/docs/src/docs/core/TransferList.mdx
index 36966d53f69..ddd0758e58a 100644
--- a/docs/src/docs/core/TransferList.mdx
+++ b/docs/src/docs/core/TransferList.mdx
@@ -44,6 +44,27 @@ Value should be a tuple of two arrays which contain values from data:
+## Controlled search
+
+You can optionally control the search inputs by providing `searchValues` and `onSearch` props. `searchValues` should
+be a tuple of two strings, one for each list:
+
+
+
+## Empty search VS empty list
+
+You can specify a `placeholder` prop, which will be used in place of the `nothingFound` when a list is completely empty,
+and no query is set.
+
+
+
+## Custom wording for each list
+
+`placeholder`, `nothingFound` and `searchPlaceholder` props can take a tuple of two strings instead of a single string to
+customize each list independently.
+
+
+
## Grouping items
diff --git a/src/mantine-core/src/TransferList/RenderList/RenderList.tsx b/src/mantine-core/src/TransferList/RenderList/RenderList.tsx
index 25a818d4ba8..99e08552bb5 100644
--- a/src/mantine-core/src/TransferList/RenderList/RenderList.tsx
+++ b/src/mantine-core/src/TransferList/RenderList/RenderList.tsx
@@ -7,7 +7,7 @@ import { UnstyledButton } from '../../UnstyledButton';
import { ActionIcon } from '../../ActionIcon';
import { TextInput } from '../../TextInput';
import { Text } from '../../Text';
-import { Divider } from '../../Divider/Divider';
+import { Divider } from '../../Divider';
import { LastIcon, NextIcon, FirstIcon, PrevIcon } from '../../Pagination/icons';
import { TransferListItem, TransferListItemComponent } from '../types';
import useStyles from './RenderList.styles';
@@ -20,8 +20,11 @@ export interface RenderListProps extends DefaultProps {
selection: string[];
itemComponent: TransferListItemComponent;
searchPlaceholder: string;
+ query?: string;
+ onSearch(value: string): void;
filter(query: string, item: TransferListItem): boolean;
nothingFound?: React.ReactNode;
+ placeholder?: React.ReactNode;
title?: React.ReactNode;
reversed?: boolean;
showTransferAll?: boolean;
@@ -55,8 +58,11 @@ export function RenderList({
itemComponent: ItemComponent,
listComponent,
searchPlaceholder,
+ query,
+ onSearch,
filter,
nothingFound,
+ placeholder,
title,
showTransferAll,
reversed,
@@ -75,7 +81,6 @@ export function RenderList({
);
const unGroupedItems: React.ReactElement[] = [];
const groupedItems: React.ReactElement[] = [];
- const [query, setQuery] = useState('');
const [hovered, setHovered] = useState(-1);
const filteredData = data.filter((item) => filter(query, item)).slice(0, limit);
const ListComponent = listComponent || 'div';
@@ -195,7 +200,7 @@ export function RenderList({
unstyled={unstyled}
value={query}
onChange={(event) => {
- setQuery(event.currentTarget.value);
+ onSearch(event.currentTarget.value);
setHovered(0);
}}
onFocus={() => setHovered(0)}
@@ -247,7 +252,7 @@ export function RenderList({
>
) : (
- {nothingFound}
+ {!query && placeholder ? placeholder : nothingFound}
)}
diff --git a/src/mantine-core/src/TransferList/TransferList.tsx b/src/mantine-core/src/TransferList/TransferList.tsx
index 091bd3dbf17..19597b3e057 100644
--- a/src/mantine-core/src/TransferList/TransferList.tsx
+++ b/src/mantine-core/src/TransferList/TransferList.tsx
@@ -1,5 +1,6 @@
import React, { forwardRef } from 'react';
import { DefaultProps, MantineNumberSize, useComponentDefaultProps } from '@mantine/styles';
+import { useUncontrolled } from '@mantine/hooks';
import { RenderList, RenderListStylesNames } from './RenderList/RenderList';
import { SelectScrollArea } from '../Select/SelectScrollArea/SelectScrollArea';
import { DefaultItem } from './DefaultItem/DefaultItem';
@@ -11,7 +12,7 @@ export type TransferListStylesNames = RenderListStylesNames;
export interface TransferListProps
extends DefaultProps,
- Omit, 'value' | 'onChange'> {
+ Omit, 'value' | 'onChange' | 'placeholder'> {
/** Current value */
value: TransferListData;
@@ -24,11 +25,20 @@ export interface TransferListProps
/** Custom item component */
itemComponent?: TransferListItemComponent;
+ /** Controlled search queries */
+ searchValues?: [string, string];
+
+ /** Called when one of the search queries changes */
+ onSearch?(value: [string, string]): void;
+
/** Search fields placeholder */
- searchPlaceholder?: string;
+ searchPlaceholder?: string | [string, string];
/** Nothing found message */
- nothingFound?: React.ReactNode;
+ nothingFound?: React.ReactNode | [React.ReactNode, React.ReactNode];
+
+ /** Displayed when a list is empty and there is no search query */
+ placeholder?: React.ReactNode | [React.ReactNode, React.ReactNode];
/** Function to filter search results */
filter?(query: string, item: TransferListItem): boolean;
@@ -63,6 +73,7 @@ const defaultProps: Partial = {
itemComponent: DefaultItem,
filter: defaultFilter,
titles: [null, null],
+ placeholder: [null, null],
listHeight: 150,
listComponent: SelectScrollArea,
showTransferAll: true,
@@ -75,8 +86,11 @@ export const TransferList = forwardRef((props
onChange,
itemComponent,
searchPlaceholder,
+ searchValues,
+ onSearch,
filter,
nothingFound,
+ placeholder,
titles,
initialSelection,
listHeight,
@@ -92,6 +106,12 @@ export const TransferList = forwardRef((props
} = useComponentDefaultProps('TransferList', defaultProps, props);
const [selection, handlers] = useSelectionState(initialSelection);
+ const [search, handleSearch] = useUncontrolled({
+ value: searchValues,
+ defaultValue: ['', ''],
+ finalValue: ['', ''],
+ onChange: onSearch,
+ });
const handleMoveAll = (listIndex: 0 | 1) => {
const items: TransferListData = Array(2) as any;
@@ -126,9 +146,7 @@ export const TransferList = forwardRef((props
const sharedListProps = {
itemComponent,
listComponent,
- searchPlaceholder,
filter,
- nothingFound,
height: listHeight,
showTransferAll,
classNames,
@@ -154,6 +172,13 @@ export const TransferList = forwardRef((props
onMoveAll={() => handleMoveAll(0)}
onMove={() => handleMove(0)}
title={titles[0]}
+ placeholder={Array.isArray(placeholder) ? placeholder[0] : placeholder}
+ searchPlaceholder={
+ Array.isArray(searchPlaceholder) ? searchPlaceholder[0] : searchPlaceholder
+ }
+ nothingFound={Array.isArray(nothingFound) ? nothingFound[0] : nothingFound}
+ query={search[0]}
+ onSearch={(query) => handleSearch([query, search[1]])}
unstyled={unstyled}
/>
@@ -165,6 +190,13 @@ export const TransferList = forwardRef((props
onMoveAll={() => handleMoveAll(1)}
onMove={() => handleMove(1)}
title={titles[1]}
+ placeholder={Array.isArray(placeholder) ? placeholder[1] : placeholder}
+ searchPlaceholder={
+ Array.isArray(searchPlaceholder) ? searchPlaceholder[1] : searchPlaceholder
+ }
+ nothingFound={Array.isArray(nothingFound) ? nothingFound[1] : nothingFound}
+ query={search[1]}
+ onSearch={(query) => handleSearch([search[0], query])}
reversed
unstyled={unstyled}
/>
diff --git a/src/mantine-demos/src/demos/core/TransferList/TransferList.demo.controlledSearch.tsx b/src/mantine-demos/src/demos/core/TransferList/TransferList.demo.controlledSearch.tsx
new file mode 100644
index 00000000000..15571bf5f04
--- /dev/null
+++ b/src/mantine-demos/src/demos/core/TransferList/TransferList.demo.controlledSearch.tsx
@@ -0,0 +1,66 @@
+import React, { useState } from 'react';
+import { MantineDemo } from '@mantine/ds';
+import { Stack, Text } from '@mantine/core';
+import { Wrapper } from './_wrapper';
+
+const code = `
+import { useState } from 'react'
+import { TransferList, Stack, Text } from '@mantine/core';
+
+function Demo() {
+ const [search, setSearch] = useState(['', '']);
+
+ return (
+
+
+ Left search:
+ {search[0] || '---'}
+ {' / '}
+ Right search:
+ {search[1] || '---'}
+
+
+
+
+ );
+}
+`;
+
+function Demo() {
+ const [search, setSearch] = useState<[string, string]>(['', '']);
+
+ return (
+
+
+
+ Left search:{' '}
+
+ {search[0] || '---'}
+ {' / '}
+
+ Right search:{' '}
+
+ {search[1] || '---'}
+
+
+
+
+ );
+}
+
+export const controlledSearch: MantineDemo = {
+ type: 'demo',
+ component: Demo,
+ code,
+};
diff --git a/src/mantine-demos/src/demos/core/TransferList/TransferList.demo.differentPlaceholders.tsx b/src/mantine-demos/src/demos/core/TransferList/TransferList.demo.differentPlaceholders.tsx
new file mode 100644
index 00000000000..d5afd7caf5e
--- /dev/null
+++ b/src/mantine-demos/src/demos/core/TransferList/TransferList.demo.differentPlaceholders.tsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import { MantineDemo } from '@mantine/ds';
+import { Wrapper } from './_wrapper';
+
+const code = `
+function Demo() {
+ return (
+
+ );
+}
+`;
+
+function Demo() {
+ return (
+
+ );
+}
+
+export const differentPlaceholders: MantineDemo = {
+ type: 'demo',
+ component: Demo,
+ code,
+};
diff --git a/src/mantine-demos/src/demos/core/TransferList/TransferList.demo.placeholder.tsx b/src/mantine-demos/src/demos/core/TransferList/TransferList.demo.placeholder.tsx
new file mode 100644
index 00000000000..9b45924fd32
--- /dev/null
+++ b/src/mantine-demos/src/demos/core/TransferList/TransferList.demo.placeholder.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { MantineDemo } from '@mantine/ds';
+import { Wrapper } from './_wrapper';
+
+const code = `
+function Demo() {
+ return (
+
+ );
+}
+`;
+
+function Demo() {
+ return (
+
+ );
+}
+
+export const placeholder: MantineDemo = {
+ type: 'demo',
+ component: Demo,
+ code,
+};
diff --git a/src/mantine-demos/src/demos/core/TransferList/index.ts b/src/mantine-demos/src/demos/core/TransferList/index.ts
index 21921971fd8..27c5530a3a8 100644
--- a/src/mantine-demos/src/demos/core/TransferList/index.ts
+++ b/src/mantine-demos/src/demos/core/TransferList/index.ts
@@ -3,3 +3,6 @@ export { scrollbars } from './TransferList.demo.scrollbars';
export { itemComponent } from './TransferList.demo.itemComponent';
export { initialSelection } from './TransferList.demo.initialSelection';
export { group } from './TransferList.demo.group';
+export { placeholder } from './TransferList.demo.placeholder';
+export { controlledSearch } from './TransferList.demo.controlledSearch';
+export { differentPlaceholders } from './TransferList.demo.differentPlaceholders';