Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from Oct 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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,
};