Skip to content

Commit

Permalink
[@mantine/core] add controlled search and per-list wording to transfe…
Browse files Browse the repository at this point in the history
…r-list
  • Loading branch information
Jérémie van der Sande committed Oct 21, 2022
1 parent eaeef08 commit 14ed785
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 8 deletions.
21 changes: 21 additions & 0 deletions docs/src/docs/core/TransferList.mdx
Expand Up @@ -44,6 +44,27 @@ Value should be a tuple of two arrays which contain values from data:

<Demo data={TransferListDemos.initialSelection} />

## 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:

<Demo data={TransferListDemos.controlledSearch} />

## 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.

<Demo data={TransferListDemos.placeholder} />

## Custom wording for each list

`placeholder`, `nothingFound` and `searchPlaceholder` props can take a tuple of values instead of a single value to
customize each list independently.

<Demo data={TransferListDemos.differentPlaceholders} />

## Grouping items

<Demo data={TransferListDemos.group} />
Expand Down
11 changes: 8 additions & 3 deletions src/mantine-core/src/TransferList/RenderList/RenderList.tsx
Expand Up @@ -20,8 +20,11 @@ export interface RenderListProps extends DefaultProps<RenderListStylesNames> {
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;
Expand Down Expand Up @@ -59,8 +62,11 @@ export function RenderList({
transferIcon: TransferIcon,
transferAllIcon: TransferAllIcon,
searchPlaceholder,
query,
onSearch,
filter,
nothingFound,
placeholder,
title,
showTransferAll,
reversed,
Expand All @@ -79,7 +85,6 @@ export function RenderList({
);
const unGroupedItems: React.ReactElement<any>[] = [];
const groupedItems: React.ReactElement<any>[] = [];
const [query, setQuery] = useState('');
const [hovered, setHovered] = useState(-1);
const filteredData = data.filter((item) => filter(query, item)).slice(0, limit);
const ListComponent = listComponent || 'div';
Expand Down Expand Up @@ -202,7 +207,7 @@ export function RenderList({
unstyled={unstyled}
value={query}
onChange={(event) => {
setQuery(event.currentTarget.value);
onSearch(event.currentTarget.value);
setHovered(0);
}}
onFocus={() => setHovered(0)}
Expand Down Expand Up @@ -254,7 +259,7 @@ export function RenderList({
</>
) : (
<Text color="dimmed" unstyled={unstyled} size="sm" align="center" mt="sm">
{nothingFound}
{!query && placeholder ? placeholder : nothingFound}
</Text>
)}
</ListComponent>
Expand Down
42 changes: 37 additions & 5 deletions 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';
Expand All @@ -11,7 +12,7 @@ export type TransferListStylesNames = RenderListStylesNames;

export interface TransferListProps
extends DefaultProps<TransferListStylesNames>,
Omit<React.ComponentPropsWithoutRef<'div'>, 'value' | 'onChange'> {
Omit<React.ComponentPropsWithoutRef<'div'>, 'value' | 'onChange' | 'placeholder'> {
/** Current value */
value: TransferListData;

Expand All @@ -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;
Expand Down Expand Up @@ -69,6 +79,7 @@ const defaultProps: Partial<TransferListProps> = {
itemComponent: DefaultItem,
filter: defaultFilter,
titles: [null, null],
placeholder: [null, null],
listHeight: 150,
listComponent: SelectScrollArea,
showTransferAll: true,
Expand All @@ -81,8 +92,11 @@ export const TransferList = forwardRef<HTMLDivElement, TransferListProps>((props
onChange,
itemComponent,
searchPlaceholder,
searchValues,
onSearch,
filter,
nothingFound,
placeholder,
titles,
initialSelection,
listHeight,
Expand All @@ -100,6 +114,12 @@ export const TransferList = forwardRef<HTMLDivElement, TransferListProps>((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;
Expand Down Expand Up @@ -136,9 +156,7 @@ export const TransferList = forwardRef<HTMLDivElement, TransferListProps>((props
listComponent,
transferIcon,
transferAllIcon,
searchPlaceholder,
filter,
nothingFound,
height: listHeight,
showTransferAll,
classNames,
Expand All @@ -164,6 +182,13 @@ export const TransferList = forwardRef<HTMLDivElement, TransferListProps>((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}
/>

Expand All @@ -175,6 +200,13 @@ export const TransferList = forwardRef<HTMLDivElement, TransferListProps>((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}
/>
Expand Down
@@ -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 (
<Stack>
<Text>
<Text component="span" weight="bold">Left search: </Text>
{search[0] || '---'}
{' / '}
<Text component="span" weight="bold">Right search: </Text>
{search[1] || '---'}
</Text>
<TransferList
searchValues={search}
onSearch={setSearch}
{/* ...other props */}
/>
</Stack>
);
}
`;

function Demo() {
const [search, setSearch] = useState<[string, string]>(['', '']);

return (
<Stack>
<Text>
<Text component="span" weight="bold">
Left search:{' '}
</Text>
{search[0] || '---'}
{' / '}
<Text component="span" weight="bold">
Right search:{' '}
</Text>
{search[1] || '---'}
</Text>

<Wrapper
searchPlaceholder="Search..."
nothingFound="Nothing here"
titles={['Frameworks', 'Libraries']}
breakpoint="sm"
searchValues={search}
onSearch={setSearch}
/>
</Stack>
);
}

export const controlledSearch: MantineDemo = {
type: 'demo',
component: Demo,
code,
};
@@ -0,0 +1,34 @@
import React from 'react';
import { MantineDemo } from '@mantine/ds';
import { Wrapper } from './_wrapper';

const code = `
function Demo() {
return (
<TransferList
searchPlaceholder={['Search item to add...', 'Search item to remove...']}
nothingFound={['Cannot find item to add', 'Cannot find item to remove']}
placeholder={['No item left to add', 'No item left ro remove']}
{/* ...other props */}
/>
);
}
`;

function Demo() {
return (
<Wrapper
searchPlaceholder={['Search item to add...', 'Search item to remove...']}
nothingFound={['Cannot find item to add', 'Cannot find item to remove']}
placeholder={['No item left to add', 'No item left ro remove']}
titles={['Frameworks', 'Libraries']}
breakpoint="sm"
/>
);
}

export const differentPlaceholders: MantineDemo = {
type: 'demo',
component: Demo,
code,
};
@@ -0,0 +1,32 @@
import React from 'react';
import { MantineDemo } from '@mantine/ds';
import { Wrapper } from './_wrapper';

const code = `
function Demo() {
return (
<TransferList
nothingFound="Nothing found"
placeholder="No item left"
{/* ...other props */}
/>
);
}
`;

function Demo() {
return (
<Wrapper
nothingFound="Nothing found"
placeholder="No item left"
titles={['Frameworks', 'Libraries']}
breakpoint="sm"
/>
);
}

export const placeholder: MantineDemo = {
type: 'demo',
component: Demo,
code,
};
3 changes: 3 additions & 0 deletions src/mantine-demos/src/demos/core/TransferList/index.ts
Expand Up @@ -4,3 +4,6 @@ export { itemComponent } from './TransferList.demo.itemComponent';
export { initialSelection } from './TransferList.demo.initialSelection';
export { group } from './TransferList.demo.group';
export { customIcons } from './TransferList.demo.customIcons';
export { placeholder } from './TransferList.demo.placeholder';
export { controlledSearch } from './TransferList.demo.controlledSearch';
export { differentPlaceholders } from './TransferList.demo.differentPlaceholders';

0 comments on commit 14ed785

Please sign in to comment.