-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
203 additions
and
0 deletions.
There are no files selected for viewing
132 changes: 132 additions & 0 deletions
132
packages/design-system/src/lib/components/transfer-list/TransferList.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
import { useState } from 'react' | ||
import { ListGroup } from 'react-bootstrap' | ||
import { Button } from '../button/Button' | ||
import { Row } from '../grid/Row' | ||
import { Col } from '../grid/Col' | ||
import { Form } from '../form/Form' | ||
|
||
function not(a: readonly TransferListItem[], b: readonly TransferListItem[]) { | ||
return a.filter((item) => !b.some((bItem) => bItem.value === item.value)) | ||
} | ||
|
||
function intersection(a: readonly TransferListItem[], b: readonly TransferListItem[]) { | ||
return a.filter((item) => b.some((bItem) => bItem.value === item.value)) | ||
} | ||
|
||
export interface TransferListItem { | ||
value: string | number | ||
label: string | ||
} | ||
|
||
export interface TransferListProps { | ||
availableItems: TransferListItem[] | ||
defaultSelected?: TransferListItem[] | ||
onChange?: (selected: TransferListItem[]) => void | ||
} | ||
|
||
export const TransferList = ({ | ||
availableItems, | ||
defaultSelected = [], | ||
onChange | ||
}: TransferListProps) => { | ||
const [checked, setChecked] = useState<readonly TransferListItem[]>([]) | ||
const [left, setLeft] = useState<readonly TransferListItem[]>( | ||
not(availableItems, defaultSelected) | ||
) | ||
const [right, setRight] = useState<readonly TransferListItem[]>( | ||
intersection(availableItems, defaultSelected) | ||
) | ||
|
||
const leftChecked = intersection(checked, left) | ||
const rightChecked = intersection(checked, right) | ||
|
||
const handleToggle = (item: TransferListItem) => () => { | ||
const currentIndex = checked.findIndex((checkedItem) => checkedItem.value === item.value) | ||
const newChecked = [...checked] | ||
|
||
if (currentIndex === -1) { | ||
newChecked.push(item) | ||
} else { | ||
newChecked.splice(currentIndex, 1) | ||
} | ||
|
||
setChecked(newChecked) | ||
} | ||
|
||
const handleAllRight = () => { | ||
setRight(right.concat(left)) | ||
onChange && onChange(right.concat(left)) | ||
setLeft([]) | ||
} | ||
|
||
const handleCheckedRight = () => { | ||
setRight(right.concat(leftChecked)) | ||
onChange && onChange(right.concat(leftChecked)) | ||
setLeft(not(left, leftChecked)) | ||
setChecked(not(checked, leftChecked)) | ||
} | ||
|
||
const handleCheckedLeft = () => { | ||
setLeft(left.concat(rightChecked)) | ||
setRight(not(right, rightChecked)) | ||
onChange && onChange(not(right, rightChecked)) | ||
setChecked(not(checked, rightChecked)) | ||
} | ||
|
||
const handleAllLeft = () => { | ||
setLeft(left.concat(right)) | ||
setRight([]) | ||
onChange && onChange([]) | ||
} | ||
|
||
const customList = (items: readonly TransferListItem[]) => ( | ||
<div style={{ width: 200, height: 230, overflow: 'auto' }}> | ||
<ListGroup as="ul"> | ||
{items.map((item: TransferListItem) => { | ||
const labelId = `transfer-list-item-${item.value}-label` | ||
|
||
return ( | ||
<ListGroup.Item as="li" key={item.value}> | ||
<Form.Group.Checkbox | ||
label={item.label} | ||
onChange={handleToggle(item)} | ||
id={labelId} | ||
checked={checked.indexOf(item) !== -1} | ||
tabIndex={-1} | ||
/> | ||
</ListGroup.Item> | ||
) | ||
})} | ||
</ListGroup> | ||
</div> | ||
) | ||
|
||
return ( | ||
<Row> | ||
<Col>{customList(left)}</Col> | ||
<Col> | ||
<Col> | ||
<Button onClick={handleAllRight} disabled={left.length === 0} aria-label="move all right"> | ||
≫ | ||
</Button> | ||
<Button | ||
onClick={handleCheckedRight} | ||
disabled={leftChecked.length === 0} | ||
aria-label="move selected right"> | ||
> | ||
</Button> | ||
<Button | ||
onClick={handleCheckedLeft} | ||
disabled={rightChecked.length === 0} | ||
aria-label="move selected left"> | ||
< | ||
</Button> | ||
<Button onClick={handleAllLeft} disabled={right.length === 0} aria-label="move all left"> | ||
≪ | ||
</Button> | ||
</Col> | ||
</Col> | ||
<Col>{customList(right)}</Col> | ||
</Row> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
70 changes: 70 additions & 0 deletions
70
packages/design-system/src/lib/stories/transfer-list/TransferList.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import type { Meta, StoryObj } from '@storybook/react' | ||
import { TransferList, TransferListItem } from '../../components/transfer-list/TransferList' | ||
|
||
/** | ||
* ## Description | ||
* The transfer list component is an element that allows users to transfer on or more items between two lists. | ||
* The list on the left is the source list and the list on the right is the target list. | ||
* The right list can be empty or have some items already. | ||
*/ | ||
|
||
const meta: Meta<typeof TransferList> = { | ||
title: 'Transfer List', | ||
component: TransferList, | ||
tags: ['autodocs'] | ||
} | ||
|
||
const availableItems: TransferListItem[] = [ | ||
{ | ||
label: 'Item 1', | ||
value: 1 | ||
}, | ||
{ | ||
label: 'Item 2', | ||
value: 2 | ||
}, | ||
{ | ||
label: 'Item 3', | ||
value: 3 | ||
}, | ||
{ | ||
label: 'Item 4', | ||
value: 4 | ||
}, | ||
{ | ||
label: 'Item 5', | ||
value: 5 | ||
}, | ||
{ | ||
label: 'Item 6', | ||
value: 6 | ||
} | ||
] | ||
|
||
const defaultSelected: TransferListItem[] = [ | ||
{ | ||
label: 'Item 4', | ||
value: 4 | ||
}, | ||
{ | ||
label: 'Item 5', | ||
value: 5 | ||
} | ||
] | ||
|
||
export default meta | ||
type Story = StoryObj<typeof TransferList> | ||
|
||
const onChangeFn = (items: TransferListItem[]) => { | ||
console.log(items) | ||
} | ||
|
||
export const Single: Story = { | ||
render: () => ( | ||
<TransferList | ||
availableItems={availableItems} | ||
defaultSelected={defaultSelected} | ||
onChange={onChangeFn} | ||
/> | ||
) | ||
} |