diff --git a/src/views/components/calendar/Calendar.tsx b/src/views/components/calendar/Calendar.tsx index c1ad86dd3..26bd09633 100644 --- a/src/views/components/calendar/Calendar.tsx +++ b/src/views/components/calendar/Calendar.tsx @@ -64,7 +64,7 @@ export default function Calendar(): JSX.Element {
- + diff --git a/src/views/components/calendar/CalendarSchedules.tsx b/src/views/components/calendar/CalendarSchedules.tsx index 6a5052423..cce0c2c37 100644 --- a/src/views/components/calendar/CalendarSchedules.tsx +++ b/src/views/components/calendar/CalendarSchedules.tsx @@ -5,7 +5,7 @@ import List from '@views/components/common/List'; import ScheduleListItem from '@views/components/common/ScheduleListItem'; import Text from '@views/components/common/Text/Text'; import useSchedules, { getActiveSchedule, switchSchedule } from '@views/hooks/useSchedules'; -import React from 'react'; +import React, { useRef } from 'react'; import AddSchedule from '~icons/material-symbols/add'; @@ -20,6 +20,7 @@ import { usePrompt } from '../common/DialogProvider/DialogProvider'; export function CalendarSchedules() { const [, schedules] = useSchedules(); const showDialog = usePrompt(); + const containerRef = useRef(null); const handleAddSchedule = () => { if (schedules.length >= 10) { @@ -48,7 +49,7 @@ export function CalendarSchedules() { }; return ( -
+
MY SCHEDULES @@ -62,6 +63,7 @@ export function CalendarSchedules() { gap={10} draggables={schedules} itemKey={s => s.id} + boundingRef={containerRef} onReordered={reordered => { const activeSchedule = getActiveSchedule(); const activeIndex = reordered.findIndex(s => s.id === activeSchedule.id); diff --git a/src/views/components/common/List.tsx b/src/views/components/common/List.tsx index 78c83bb3f..8372e23df 100644 --- a/src/views/components/common/List.tsx +++ b/src/views/components/common/List.tsx @@ -1,6 +1,11 @@ -import type { DraggableProvided, DraggableProvidedDragHandleProps, OnDragEndResponder } from '@hello-pangea/dnd'; +import type { + DraggableProvided, + DraggableProvidedDragHandleProps, + OnDragEndResponder, + OnDragStartResponder, +} from '@hello-pangea/dnd'; import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'; -import type { ReactElement } from 'react'; +import type { ReactElement, RefObject } from 'react'; import React, { useCallback, useEffect, useState } from 'react'; import ExtensionRoot from './ExtensionRoot/ExtensionRoot'; @@ -18,6 +23,7 @@ export interface ListProps { onReordered: (elements: T[]) => void; itemKey: (item: T) => React.Key; gap?: number; // Impacts the spacing between items in the list + boundingRef?: RefObject; // Element that clamps the y-value for draggable items } function wrap(draggableElements: T[], keyTransform: ListProps['itemKey']) { @@ -72,51 +78,74 @@ function Item(props: { * @example * */ -function List({ draggables, itemKey, children, onReordered, gap }: ListProps): JSX.Element { +function List({ draggables, itemKey, children, onReordered, gap, boundingRef }: ListProps): JSX.Element { const [items, setItems] = useState(wrap(draggables, itemKey)); - + const [activeItem, setActiveItem] = useState(null); + const boundingEl = boundingRef?.current; const transformFunction = children; useEffect(() => { setItems(wrap(draggables, itemKey)); - // This is on purpose // eslint-disable-next-line react-hooks/exhaustive-deps }, [draggables]); + useEffect(() => { + if (!activeItem || !boundingEl) return; + + const handleMouseMove = (e: MouseEvent) => { + if (!boundingEl) return; + + const bounds = boundingEl.getBoundingClientRect(); + const isOutsideBounds = e.clientY < bounds.top || e.clientY > bounds.bottom; + + if (isOutsideBounds) { + // force end of drag + window.dispatchEvent( + new KeyboardEvent('keydown', { + key: 'Escape', + keyCode: 27, + which: 27, + bubbles: true, + }) + ); + } + }; + + document.addEventListener('mousemove', handleMouseMove); + return () => document.removeEventListener('mousemove', handleMouseMove); + }, [activeItem, boundingEl]); + const onDragEnd: OnDragEndResponder = useCallback( ({ destination, source }) => { + setActiveItem(null); + if (!destination) return; if (source.index === destination.index) return; // will reorder in place const reordered = reorder(items, source.index, destination.index); - setItems(reordered); onReordered(reordered.map(item => item.content)); }, - // This is on purpose // eslint-disable-next-line react-hooks/exhaustive-deps [items] ); + const onDragStart: OnDragStartResponder = useCallback(() => { + const el = document.querySelector('.is-dragging'); + setActiveItem(el); + }, []); + return (
- + { - let { style } = provided.draggableProps; - const transform = style?.transform; - - if (snapshot.isDragging && transform) { - let [, , y] = transform.match(/translate\(([-\d]+)px, ([-\d]+)px\)/) || []; - - style.transform = `translate3d(0px, ${y}px, 0px)`; // Apply constrained y value - } - + const { style } = provided.draggableProps; return ( ({ draggables, itemKey, children, onReordered, gap }: ListProps< {...draggableProps} style={{ ...draggableProps.style, - // if last item, don't add margin marginBottom: `${gap}px`, }} >