Skip to content

Commit

Permalink
[@mantine/core] TransferList: Add transferIcon, transferAllIcon p…
Browse files Browse the repository at this point in the history
…rops, controlled search and tuple syntax for `seachPlaceholder` and `nothingFound` props (#2769)

* [@mantine/core] add custom icons capability to transfer-list

* [@mantine/core] add controlled search and per-list wording to transfer-list

Co-authored-by: Jérémie van der Sande <jeremie.van-der-sande@ubisoft.com>
  • Loading branch information
jvdsande and Jérémie van der Sande committed Oct 22, 2022
1 parent 81836fa commit 450840a
Show file tree
Hide file tree
Showing 8 changed files with 269 additions and 11 deletions.
25 changes: 25 additions & 0 deletions docs/src/docs/core/TransferList.mdx
Expand Up @@ -44,10 +44,35 @@ 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} />

## Custom control icons

<Demo data={TransferListDemos.customIcons} />

## Responsive styles

Set `breakpoint` prop to specify at which breakpoint TransferList will collapse to 1 column:
Expand Down
24 changes: 18 additions & 6 deletions src/mantine-core/src/TransferList/RenderList/RenderList.tsx
Expand Up @@ -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';
Expand All @@ -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 All @@ -31,6 +34,8 @@ export interface RenderListProps extends DefaultProps<RenderListStylesNames> {
radius: MantineNumberSize;
listComponent?: React.FC<any>;
limit?: number;
transferIcon?: React.FunctionComponent<{ reversed }>;
transferAllIcon?: React.FunctionComponent<{ reversed }>;
}

const icons = {
Expand All @@ -54,9 +59,14 @@ export function RenderList({
selection,
itemComponent: ItemComponent,
listComponent,
transferIcon: TransferIcon,
transferAllIcon: TransferAllIcon,
searchPlaceholder,
query,
onSearch,
filter,
nothingFound,
placeholder,
title,
showTransferAll,
reversed,
Expand All @@ -75,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 @@ -181,6 +190,9 @@ export function RenderList({
}
};

const transferIcon = reversed ? <Icons.Prev /> : <Icons.Next />;
const transferAllIcon = reversed ? <Icons.First /> : <Icons.Last />;

return (
<div className={cx(classes.transferList, className)}>
{title && (
Expand All @@ -195,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 All @@ -216,7 +228,7 @@ export function RenderList({
onClick={onMove}
unstyled={unstyled}
>
{reversed ? <Icons.Prev /> : <Icons.Next />}
{TransferIcon ? <TransferIcon reversed={reversed} /> : transferIcon}
</ActionIcon>

{showTransferAll && (
Expand All @@ -229,7 +241,7 @@ export function RenderList({
onClick={onMoveAll}
unstyled={unstyled}
>
{reversed ? <Icons.First /> : <Icons.Last />}
{TransferAllIcon ? <TransferAllIcon reversed={reversed} /> : transferAllIcon}
</ActionIcon>
)}
</div>
Expand All @@ -247,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
52 changes: 47 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 All @@ -53,6 +63,12 @@ export interface TransferListProps

/** Limit amount of items showed at a time */
limit?: number;

/** Change icon used for the transfer selected control */
transferIcon?: React.FunctionComponent<{ reversed: boolean }>;

/** Change icon used for the transfer all control */
transferAllIcon?: React.FunctionComponent<{ reversed: boolean }>;
}

export function defaultFilter(query: string, item: TransferListItem) {
Expand All @@ -63,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 @@ -75,8 +92,11 @@ export const TransferList = forwardRef<HTMLDivElement, TransferListProps>((props
onChange,
itemComponent,
searchPlaceholder,
searchValues,
onSearch,
filter,
nothingFound,
placeholder,
titles,
initialSelection,
listHeight,
Expand All @@ -88,10 +108,18 @@ export const TransferList = forwardRef<HTMLDivElement, TransferListProps>((props
styles,
limit,
unstyled,
transferIcon,
transferAllIcon,
...others
} = 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 @@ -126,9 +154,9 @@ export const TransferList = forwardRef<HTMLDivElement, TransferListProps>((props
const sharedListProps = {
itemComponent,
listComponent,
searchPlaceholder,
transferIcon,
transferAllIcon,
filter,
nothingFound,
height: listHeight,
showTransferAll,
classNames,
Expand All @@ -154,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 @@ -165,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,43 @@
import React from 'react';
import { IconFilePlus, IconFolderPlus, IconFileMinus, IconFolderMinus } from '@tabler/icons';
import { MantineDemo } from '@mantine/ds';
import { Wrapper } from './_wrapper';

const code = `
import {
IconFilePlus,
IconFolderPlus,
IconFileMinus,
IconFolderMinus,
} from '@tabler/icons';
import { TransferList } from '@mantine/core';
function Demo() {
return (
<TransferList
transferIcon={({ reversed }) => (reversed ? <IconFileMinus /> : <IconFilePlus />)}
transferAllIcon={({ reversed }) => (reversed ? <IconFolderMinus /> : <IconFolderPlus />)}
{/* ...other props */}
/>
)
}
`;

function Demo() {
return (
<Wrapper
searchPlaceholder="Search..."
nothingFound="Nothing here"
titles={['Frameworks', 'Libraries']}
breakpoint="sm"
transferIcon={({ reversed }) => (reversed ? <IconFileMinus /> : <IconFilePlus />)}
transferAllIcon={({ reversed }) => (reversed ? <IconFolderMinus /> : <IconFolderPlus />)}
/>
);
}

export const customIcons: MantineDemo = {
type: 'demo',
component: Demo,
code,
};

0 comments on commit 450840a

Please sign in to comment.