Skip to content

Instantly share code, notes, and snippets.

@thisisgurkaran
Last active February 9, 2026 16:51
Show Gist options
  • Select an option

  • Save thisisgurkaran/4e07fd9cffc62db41abe5a1c5a8ca61f to your computer and use it in GitHub Desktop.

Select an option

Save thisisgurkaran/4e07fd9cffc62db41abe5a1c5a8ca61f to your computer and use it in GitHub Desktop.
import { useState } from "react";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Checkbox from "@mui/material/Checkbox";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
import TextField from "@mui/material/TextField";
import Typography from "@mui/material/Typography";
import SearchIcon from "@mui/icons-material/Search";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
import InputAdornment from "@mui/material/InputAdornment";
import { filterByQuery } from "./transferListUtils";
export type TransferListProps<T> = {
availableItems: T[];
selectedItems: T[];
checkedInAvailable: T[];
checkedInSelected: T[];
onToggle: (item: T) => () => void;
onMoveRight: () => void;
onMoveLeft: () => void;
getKey: (item: T) => string;
getLabel: (item: T) => string;
getSecondary: (item: T) => string;
availableTitle?: string;
selectedTitle?: string;
searchAvailablePlaceholder?: string;
searchSelectedPlaceholder?: string;
};
export function TransferList<T>({
availableItems,
selectedItems,
checkedInAvailable,
checkedInSelected,
onToggle,
onMoveRight,
onMoveLeft,
getKey,
getLabel,
getSecondary,
availableTitle = "Available",
selectedTitle = "Selected",
searchAvailablePlaceholder = "Search available...",
searchSelectedPlaceholder = "Search selected...",
}: TransferListProps<T>) {
console.log(availableItems, "availabelItms");
const [searchAvailable, setSearchAvailable] = useState("");
const [searchSelected, setSearchSelected] = useState("");
const availableFiltered = filterByQuery(
availableItems,
searchAvailable,
getLabel,
getSecondary
);
const selectedFiltered = filterByQuery(
selectedItems,
searchSelected,
getLabel,
getSecondary
);
const renderList = (
title: string,
totalCount: number,
items: T[],
checked: T[]
) => (
<Card variant="outlined" sx={{ minWidth: 220, maxHeight: 320 }}>
<CardContent sx={{ py: 1, "&:last-child": { pb: 1 } }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
{title} ({totalCount})
</Typography>
<List
dense
component="div"
role="list"
sx={{ overflow: "auto", maxHeight: 240 }}
>
{items.map((item) => {
const key = getKey(item);
const label = getLabel(item);
const isChecked = checked.some((c) => getKey(c) === key);
return (
<ListItem
key={key}
role="listitem"
component="button"
onClick={onToggle(item)}
sx={{ textAlign: "left", width: "100%", cursor: "pointer" }}
>
<ListItemIcon>
<Checkbox checked={isChecked} disableRipple />
</ListItemIcon>
<ListItemText primary={label} secondary={getSecondary(item)} />
</ListItem>
);
})}
</List>
</CardContent>
</Card>
);
return (
<Box
sx={{
display: "flex",
alignItems: "flex-start",
gap: 2,
flexWrap: "wrap",
}}
>
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
<TextField
placeholder={searchAvailablePlaceholder}
value={searchAvailable}
onChange={(e) => setSearchAvailable(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && e.preventDefault()}
size="small"
sx={{ minWidth: 220 }}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon fontSize="small" />
</InputAdornment>
),
}}
/>
{renderList(
availableTitle,
availableItems.length,
availableFiltered,
checkedInAvailable
)}
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
gap: 1,
}}
>
<Button
variant="outlined"
size="small"
onClick={onMoveRight}
disabled={checkedInAvailable.length === 0}
aria-label="move selected right"
>
<ChevronRightIcon />
</Button>
<Button
variant="outlined"
size="small"
onClick={onMoveLeft}
disabled={checkedInSelected.length === 0}
aria-label="move selected left"
>
<ChevronLeftIcon />
</Button>
</Box>
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
<TextField
placeholder={searchSelectedPlaceholder}
value={searchSelected}
onChange={(e) => setSearchSelected(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && e.preventDefault()}
size="small"
sx={{ minWidth: 220 }}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon fontSize="small" />
</InputAdornment>
),
}}
/>
{renderList(
selectedTitle,
selectedItems.length,
selectedFiltered,
checkedInSelected
)}
</Box>
</Box>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment