Skip to content

Commit

Permalink
Migrate playlist drag-and-drop to dnd-kit
Browse files Browse the repository at this point in the history
  • Loading branch information
martpie committed Jan 7, 2025
1 parent 3516c7c commit 51517fe
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 209 deletions.
25 changes: 0 additions & 25 deletions src/components/TrackRow.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -32,29 +32,4 @@
border-top-color: rgba(255 255 255 / 0.2);
}
}

&.reordered {
opacity: 0.5;
}

&.isReorderedOver {
&::after {
pointer-events: none;
position: absolute;
z-index: 1;
display: block;
width: 100%;
content: "";
height: 2px;
background-color: var(--main-color);
}

&.isAbove::after {
top: -2px;
}

&.isBelow::after {
bottom: -2px;
}
}
}
116 changes: 41 additions & 75 deletions src/components/TrackRow.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,41 @@
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import cx from 'classnames';
import type React from 'react';
import { useCallback, useState } from 'react';

import type { Track } from '../generated/typings';
import useFormattedDuration from '../hooks/useFormattedDuration';
import PlayingIndicator from './PlayingIndicator';

import styles from './TrackRow.module.css';
import cellStyles from './TracksListHeader.module.css';

type Props = {
selected: boolean;
track: Track;
index: number;
isPlaying: boolean;
onDoubleClick: (trackID: string) => void;
onMouseDown: (
isPlaying?: boolean;
onDoubleClick?: (trackID: string) => void;
onMouseDown?: (
event: React.MouseEvent,
trackID: string,
index: number,
) => void;
onContextMenu: (event: React.MouseEvent, index: number) => void;
onClick: (
onContextMenu?: (event: React.MouseEvent, index: number) => void;
onClick?: (
event: React.MouseEvent | React.KeyboardEvent,
trackID: string,
) => void;

draggable?: boolean;
reordered?: boolean;
onDragStart?: () => void;
onDragOver?: (trackID: string, position: 'above' | 'below') => void;
onDragEnd?: () => void;
onDrop?: (targetTrackID: string, position: 'above' | 'below') => void;
style?: React.CSSProperties;
};

export default function TrackRow(props: Props) {
const [reorderOver, setReorderOver] = useState(false);
const [reorderPosition, setReorderPosition] = useState<
'above' | 'below' | null
>(null);

const {
track,
index,
selected,
draggable,
reordered,
onMouseDown,
onClick,
onContextMenu,
Expand All @@ -54,80 +44,56 @@ export default function TrackRow(props: Props) {

const duration = useFormattedDuration(track.duration);

// TODO: migrate to react-dnd
const onDragStart = useCallback(
(event: React.DragEvent<HTMLDivElement>) => {
if (props.onDragStart) {
event.dataTransfer.setData('text/plain', props.track.id);
event.dataTransfer.dropEffect = 'move';
event.dataTransfer.effectAllowed = 'move';

props.onDragStart();
}
// Drag-and-Drop for playlists
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({
id: props.track.id,
disabled: !draggable,
data: {
type: 'playlist-track',
index,
},
[props],
);

const onDragOver = useCallback((event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();

const relativePosition =
event.nativeEvent.offsetY / event.currentTarget.offsetHeight;
const dragPosition = relativePosition < 0.5 ? 'above' : 'below';

setReorderOver(true);
setReorderPosition(dragPosition);
}, []);

const onDragLeave = useCallback(() => {
setReorderOver(false);
setReorderPosition(null);
}, []);

const onDrop = useCallback(() => {
const { onDrop } = props;

if (reorderPosition && onDrop) {
onDrop(props.track.id, reorderPosition);
}
});

setReorderOver(false);
setReorderPosition(null);
}, [props, reorderPosition]);
const style: React.CSSProperties | undefined = transform
? {
transform: CSS.Translate.toString(transform),
transition,
zIndex: 2,
visibility: isDragging ? 'hidden' : 'visible',
}
: undefined;

const trackClasses = cx(styles.track, {
[styles.selected]: selected,
[styles.reordered]: reordered,
[styles.isReorderedOver]: reorderOver,
[styles.isAbove]: reorderPosition === 'above',
[styles.isBelow]: reorderPosition === 'below',
[styles.even]: index % 2 === 0,
});

return (
<div
className={trackClasses}
onDoubleClick={() => onDoubleClick(props.track.id)}
onMouseDown={(e) => onMouseDown(e, track.id, index)}
onClick={(e) => onClick(e, props.track.id)}
onDoubleClick={() => onDoubleClick?.(props.track.id)}
onMouseDown={(e) => onMouseDown?.(e, track.id, index)}
onClick={(e) => onClick?.(e, props.track.id)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
onClick(e, track.id);
onClick?.(e, track.id);
}
}}
onContextMenu={(e) => onContextMenu(e, index)}
// biome-ignore lint/a11y/useSemanticElements: Accessibility over semantics
role="option"
onContextMenu={(e) => onContextMenu?.(e, index)}
aria-selected={selected}
tabIndex={-1} // we do not want trackrows to be focusable by the keyboard
draggable={draggable}
onDragStart={(draggable && onDragStart) || undefined}
onDragOver={(draggable && onDragOver) || undefined}
onDragLeave={(draggable && onDragLeave) || undefined}
onDrop={(draggable && onDrop) || undefined}
onDragEnd={(draggable && props.onDragEnd) || undefined}
style={props.style}
{...(props.isPlaying ? { 'data-is-playing': true } : {})}
// dnd-related props:
ref={setNodeRef}
{...listeners}
{...attributes}
style={{ ...style, ...props.style }} // memo that
>
<div className={`${styles.cell} ${cellStyles.cellTrackPlaying}`}>
{props.isPlaying ? <PlayingIndicator /> : null}
Expand Down
Loading

0 comments on commit 51517fe

Please sign in to comment.