Created
January 25, 2024 16:09
-
-
Save christopherbauer/267109f7fe99e2a57b9e62a25fbf3ff6 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
import React, { createContext, useContext, useMemo } from 'react'; | |
import type { CSSProperties, PropsWithChildren } from 'react'; | |
import type { DraggableSyntheticListeners, UniqueIdentifier } from '@dnd-kit/core'; | |
import { useSortable } from '@dnd-kit/sortable'; | |
import { CSS } from '@dnd-kit/utilities'; | |
import { Icon, IconProps } from '../Icon'; | |
import { Button, ListItem, ListItemProps } from '@chakra-ui/react'; | |
import styled from '@emotion/styled'; | |
type Props = ListItemProps & { | |
id: UniqueIdentifier; | |
}; | |
interface Context { | |
attributes: Record<string, any>; | |
listeners: DraggableSyntheticListeners; | |
ref(node: HTMLElement | null): void; | |
} | |
const SortableItemContext = createContext<Context>({ | |
attributes: {}, | |
listeners: undefined, | |
ref() {}, | |
}); | |
export function SortableItem({ children, id, ...rest }: PropsWithChildren<Props>) { | |
const { attributes, isDragging, listeners, setNodeRef, setActivatorNodeRef, transform, transition } = useSortable({ id }); | |
const context = useMemo( | |
() => ({ | |
attributes, | |
listeners, | |
ref: setActivatorNodeRef, | |
}), | |
[attributes, listeners, setActivatorNodeRef] | |
); | |
const style: CSSProperties = { | |
opacity: isDragging ? 0.4 : undefined, | |
transform: CSS.Translate.toString(transform), | |
transition, | |
}; | |
return ( | |
<SortableItemContext.Provider value={context}> | |
<DraggableItem ref={setNodeRef} style={style} {...rest}> | |
{children} | |
</DraggableItem> | |
</SortableItemContext.Provider> | |
); | |
} | |
const DraggableItem = styled(ListItem)` | |
display: flex; | |
justify-content: space-between; | |
flex-grow: 1; | |
align-items: center; | |
box-shadow: 0 0 0 calc(1px / var(--scale-x, 1)) rgba(63, 63, 68, 0.05), 0 1px calc(3px / var(--scale-x, 1)) 0 rgba(34, 33, 81, 0.15); | |
border-radius: calc(4px / var(--scale-x, 1)); | |
box-sizing: border-box; | |
list-style: none; | |
color: #333; | |
font-weight: 400; | |
font-size: 1rem; | |
font-family: sans-serif; | |
&:hover { | |
background-color: rgba(0, 0, 0, 0.05); | |
} | |
&:focus-visible { | |
box-shadow: 0 0px 0px 2px #4c9ffe; | |
} | |
`; | |
type DragHandleProps = Omit<IconProps, 'icon'>; | |
export const DragHandle = (props: DragHandleProps) => { | |
const { attributes, listeners, ref } = useContext(SortableItemContext); | |
return ( | |
<Dragger {...attributes} {...listeners} ref={ref}> | |
<Icon {...props} icon="grip-lines-vertical" /> | |
</Dragger> | |
); | |
}; | |
const Dragger = styled(Button)` | |
display: flex; | |
width: 12px; | |
padding: 15px; | |
align-items: center; | |
justify-content: center; | |
flex: 0 0 auto; | |
touch-action: none; | |
cursor: var(--cursor, pointer); | |
border-radius: 5px; | |
border: none; | |
outline: none; | |
appearance: none; | |
background-color: transparent; | |
-webkit-tap-highlight-color: transparent; | |
`; |
This file contains hidden or 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
import React, { useMemo, useState } from 'react'; | |
import type { ReactNode } from 'react'; | |
import { DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'; | |
import type { Active, UniqueIdentifier } from '@dnd-kit/core'; | |
import { SortableContext, arrayMove, sortableKeyboardCoordinates } from '@dnd-kit/sortable'; | |
import { DragHandle, SortableItem } from './SortableItem'; | |
import styled from '@emotion/styled'; | |
import { Container, List, ListProps } from '@chakra-ui/react'; | |
import { SortableOverlay } from './SortableOverlay'; | |
interface BaseItem { | |
id: UniqueIdentifier; | |
} | |
type Props<T extends BaseItem> = Omit<ListProps, 'onChange'> & { | |
items: T[]; | |
onChange(items: T[]): void; | |
renderItem(item: T, i?: number): ReactNode; | |
}; | |
export function SortableList<T extends BaseItem>({ items, onChange, renderItem, ...rest }: Props<T>) { | |
const [active, setActive] = useState<Active | null>(null); | |
const activeItem = useMemo(() => items.find((item) => item.id === active?.id), [active, items]); | |
const sensors = useSensors( | |
useSensor(PointerSensor), | |
useSensor(KeyboardSensor, { | |
coordinateGetter: sortableKeyboardCoordinates, | |
}) | |
); | |
return ( | |
<DndContext | |
sensors={sensors} | |
onDragStart={({ active }) => { | |
setActive(active); | |
}} | |
onDragEnd={({ active, over }) => { | |
if (over && active.id !== over?.id) { | |
const activeIndex = items.findIndex(({ id }) => id === active.id); | |
const overIndex = items.findIndex(({ id }) => id === over.id); | |
onChange(arrayMove(items, activeIndex, overIndex)); | |
} | |
setActive(null); | |
}} | |
onDragCancel={() => { | |
setActive(null); | |
}} | |
> | |
<List {...rest}> | |
<SortableContext items={items}> | |
<SortableListComponent role="application"> | |
{items.map((item, i) => ( | |
<React.Fragment key={item.id}>{renderItem(item, i)}</React.Fragment> | |
))} | |
</SortableListComponent> | |
</SortableContext> | |
<SortableOverlay>{activeItem ? renderItem(activeItem) : null}</SortableOverlay> | |
</List> | |
</DndContext> | |
); | |
} | |
const SortableListComponent = styled(Container)` | |
display: flex; | |
flex-direction: column; | |
gap: 10px; | |
padding: 0; | |
list-style: none; | |
`; | |
SortableList.Item = SortableItem; | |
SortableList.DragHandle = DragHandle; |
This file contains hidden or 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
import type { PropsWithChildren } from 'react'; | |
import { DragOverlay, defaultDropAnimationSideEffects } from '@dnd-kit/core'; | |
import type { DropAnimation } from '@dnd-kit/core'; | |
const dropAnimationConfig: DropAnimation = { | |
sideEffects: defaultDropAnimationSideEffects({ | |
styles: { | |
active: { | |
opacity: '0.4', | |
}, | |
}, | |
}), | |
}; | |
interface Props {} | |
export function SortableOverlay({ children }: PropsWithChildren<Props>) { | |
return <DragOverlay dropAnimation={dropAnimationConfig}>{children}</DragOverlay>; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment