Skip to content

Commit

Permalink
feat: first approach to sortable list
Browse files Browse the repository at this point in the history
  • Loading branch information
g-saracca committed Aug 12, 2024
1 parent 105ebb4 commit fb9bba2
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 43 deletions.
105 changes: 79 additions & 26 deletions packages/design-system/src/lib/components/transfer-list/ItemsList.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,88 @@
import { useId } from 'react'
import { ListGroup } from 'react-bootstrap'
import { Form } from '../form/Form'
import { TransferListItem } from './TransferList'
import { DndContext, DragEndEvent } from '@dnd-kit/core'
import { arrayMove, SortableContext } from '@dnd-kit/sortable'
import { type TransferListItem } from './TransferList'
import { ListItem } from './ListItem'
import styles from './TransferList.module.scss'

interface ListProps {
items: readonly TransferListItem[]
side: 'left' | 'right'
checked: readonly TransferListItem[]
onToggle: (item: TransferListItem) => () => void
}
type ListProps =
| {
items: TransferListItem[]
side: 'left'
checked: TransferListItem[]
onToggle: (item: TransferListItem) => () => void
rightItems?: never
setRight?: never
onChange?: never
}
| {
items: TransferListItem[]
side: 'right'
checked: TransferListItem[]
onToggle: (item: TransferListItem) => () => void
rightItems: TransferListItem[]
setRight: React.Dispatch<React.SetStateAction<TransferListItem[]>>
onChange?: (selected: TransferListItem[]) => void
}

export const ItemsList = ({
items,
side,
checked,
onToggle,
rightItems,
setRight,
onChange
}: ListProps) => {
const handleDragEnd = (event: DragEndEvent) => {
// Prevent sorting on the left side which is not sortable but also asserts that setRight is defined
if (side === 'left') return

const { active, over } = event

if (over && active.id !== over.id) {
const oldIndex = rightItems.findIndex((item) => item.id === active.id)
const newIndex = rightItems.findIndex((item) => item.id === over.id)

const newItems = arrayMove(rightItems, oldIndex, newIndex)

setRight(newItems)

onChange && onChange(newItems)
}
}

export const ItemsList = ({ items, side, checked, onToggle }: ListProps) => {
const uniqueID = useId()
if (side === 'left') {
return (
<ListGroup as="ul" className={styles['items-list']} data-testid={`${side}-list-group`}>
{items.map((item: TransferListItem) => (
<ListItem
item={item}
side={side}
checked={checked}
onToggle={onToggle}
key={item.value}
/>
))}
</ListGroup>
)
}

return (
<ListGroup as="ul" className={styles['items-list']} data-testid={`${side}-list-group`}>
{items.map((item: TransferListItem) => {
const labelId = `transfer-list-item-${item.value}-label-${uniqueID}`

return (
<ListGroup.Item as="li" className={styles['list-item']} key={item.value}>
<Form.Group.Checkbox
label={item.label}
onChange={onToggle(item)}
id={labelId}
checked={checked.indexOf(item) !== -1}
<DndContext onDragEnd={handleDragEnd}>
<SortableContext items={items}>
<ListGroup as="ul" className={styles['items-list']} data-testid={`${side}-list-group`}>
{items.map((item: TransferListItem) => (
<ListItem
item={item}
side={side}
checked={checked}
onToggle={onToggle}
key={item.value}
/>
</ListGroup.Item>
)
})}
</ListGroup>
))}
</ListGroup>
</SortableContext>
</DndContext>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { useId } from 'react'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { ListGroup } from 'react-bootstrap'
import { Form } from '../form/Form'
import { TransferListItem } from './TransferList'
import { Stack } from '../stack/Stack'
import styles from './TransferList.module.scss'

interface ListItemProps {
item: TransferListItem
side: 'left' | 'right'
checked: readonly TransferListItem[]
onToggle: (item: TransferListItem) => () => void
}

export const ListItem = ({ item, side, checked, onToggle }: ListItemProps) => {
const { attributes, listeners, transform, transition, setNodeRef, setActivatorNodeRef } =
useSortable({ id: item.id })

const uniqueID = useId()
const labelId = `transfer-list-item-${item.value}-label-${uniqueID}`

if (side === 'left') {
return (
<ListGroup.Item as="li" className={styles['list-item']}>
<Form.Group.Checkbox
label={item.label}
onChange={onToggle(item)}
id={labelId}
checked={checked.indexOf(item) !== -1}
/>
</ListGroup.Item>
)
}

const style = {
transform: CSS.Transform.toString(transform),
transition
}

// TODO:ME Limit the drag movement to the y-axis

return (
<ListGroup.Item
as="li"
ref={setNodeRef}
{...attributes}
style={style}
className={styles['list-item']}>
<Stack direction="horizontal" gap={1}>
<div className={styles['drag-grip']} ref={setActivatorNodeRef} {...listeners}>
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg">
<circle cx="9" cy="6" r="1.5" fill="#333" />
<circle cx="15" cy="6" r="1.5" fill="#333" />
<circle cx="9" cy="12" r="1.5" fill="#333" />
<circle cx="15" cy="12" r="1.5" fill="#333" />
<circle cx="9" cy="18" r="1.5" fill="#333" />
<circle cx="15" cy="18" r="1.5" fill="#333" />
</svg>
</div>
<Form.Group.Checkbox
label={item.label}
onChange={onToggle(item)}
id={labelId}
checked={checked.indexOf(item) !== -1}
/>
</Stack>
</ListGroup.Item>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
z-index: 1;
width: 230px;
height: 240px;
overflow-x: hidden;
overflow-y: auto;
border: solid 1px $dv-border-color;
border-radius: 6px;
Expand Down Expand Up @@ -46,15 +47,6 @@
.list-item {
padding-inline: 1rem;

&:hover {
color: $dv-text-color;
background-color: color.adjust($dv-secondary-color, $alpha: -0.7);
}

&:has(input[type='checkbox']:checked) {
background-color: color.adjust($dv-secondary-color, $alpha: -0.4);
}

:global .form-check {
display: flex;
align-items: center;
Expand All @@ -76,6 +68,29 @@
padding-block: 0.5rem;
cursor: pointer;
}

.drag-grip {
border-radius: 4px;
cursor: grab;
opacity: 0;

&:hover {
background-color: $dv-secondary-color;
}
}

&:hover {
color: $dv-text-color;
background-color: color.adjust($dv-secondary-color, $alpha: -0.7);

.drag-grip {
opacity: 1;
}
}

&:has(input[type='checkbox']:checked) {
background-color: color.adjust($dv-secondary-color, $alpha: -0.4);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,18 @@ import {
import { ItemsList } from './ItemsList'
import styles from './TransferList.module.scss'

function not(a: readonly TransferListItem[], b: readonly TransferListItem[]) {
function not(a: TransferListItem[], b: TransferListItem[]) {
return a.filter((item) => !b.some((bItem) => bItem.value === item.value))
}

function intersection(a: readonly TransferListItem[], b: readonly TransferListItem[]) {
function intersection(a: TransferListItem[], b: TransferListItem[]) {
return a.filter((item) => b.some((bItem) => bItem.value === item.value))
}

export interface TransferListItem {
value: string | number
label: string
id: string | number
}

export interface TransferListProps {
Expand All @@ -37,11 +38,9 @@ export const TransferList = ({
leftLabel,
rightLabel
}: TransferListProps) => {
const [checked, setChecked] = useState<readonly TransferListItem[]>([])
const [left, setLeft] = useState<readonly TransferListItem[]>(
not(availableItems, defaultSelected)
)
const [right, setRight] = useState<readonly TransferListItem[]>(
const [checked, setChecked] = useState<TransferListItem[]>([])
const [left, setLeft] = useState<TransferListItem[]>(not(availableItems, defaultSelected))
const [right, setRight] = useState<TransferListItem[]>(
intersection(availableItems, defaultSelected)
)

Expand Down Expand Up @@ -137,7 +136,15 @@ export const TransferList = ({
</div>
<div className={styles['items-column']} tabIndex={0}>
{rightLabel && <p className={styles['column-label']}>{rightLabel}</p>}
<ItemsList items={right} side="right" checked={checked} onToggle={handleToggle} />
<ItemsList
items={right}
side="right"
checked={checked}
onToggle={handleToggle}
rightItems={right}
setRight={setRight}
onChange={onChange}
/>
</div>
</div>
)
Expand Down

0 comments on commit fb9bba2

Please sign in to comment.