Skip to content

Commit 097c670

Browse files
refactor: improve reorder filters (#3606)
* refactor: improve reorder in sections component * refactor: filters sections * chore: fix eslint
1 parent 2774b3b commit 097c670

6 files changed

Lines changed: 217 additions & 46 deletions

File tree

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
},
5757
"dependencies": {
5858
"@atlaskit/pragmatic-drag-and-drop": "^1.7.4",
59+
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
5960
"@blueprintjs/core": "^6.0.0",
6061
"@blueprintjs/icons": "^6.0.0",
6162
"@blueprintjs/select": "^6.0.0",

src/component/elements/Sections.tsx

Lines changed: 198 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { Icon, Tag } from '@blueprintjs/core';
1+
import type { ElementDropTargetEventBasePayload } from '@atlaskit/pragmatic-drag-and-drop/dist/types/adapter/element-adapter.js';
2+
import type { ElementDragPayload } from '@atlaskit/pragmatic-drag-and-drop/dist/types/internal-types.js';
3+
import type { Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
4+
import { Colors, Icon, Tag } from '@blueprintjs/core';
25
import styled from '@emotion/styled';
36
import type {
47
CSSProperties,
@@ -15,8 +18,55 @@ import {
1518
useRef,
1619
useState,
1720
} from 'react';
21+
import { createPortal } from 'react-dom';
1822

19-
import { draggable, dropTargetForElements } from './pdnd.cjs';
23+
import { assert } from '../utility/assert.js';
24+
25+
import {
26+
attachClosestEdge,
27+
combine,
28+
draggable,
29+
dropTargetForElements,
30+
extractClosestEdge,
31+
getReorderDestinationIndex,
32+
pointerOutsideOfPreview,
33+
setCustomNativeDragPreview,
34+
} from './pdnd.cjs';
35+
36+
interface DropIndicatorProps {
37+
edge: Edge;
38+
}
39+
40+
const DropIndicator = styled.div<DropIndicatorProps>`
41+
position: absolute;
42+
z-index: 3;
43+
background-color: ${Colors.BLUE3};
44+
height: 2px;
45+
left: 0;
46+
right: 0;
47+
pointer-events: none;
48+
${(props) => props.edge === 'top' && 'top: -1px;'}
49+
${(props) => props.edge === 'bottom' && 'bottom: -1px;'}
50+
`;
51+
52+
const Preview = styled.div`
53+
padding-block: 4px;
54+
padding-inline: 8px;
55+
border-radius: 4px;
56+
background-color: #f4f5f7;
57+
max-width: 360px;
58+
white-space: nowrap;
59+
overflow: hidden;
60+
text-overflow: ellipsis;
61+
`;
62+
63+
type DraggableState =
64+
| { type: 'idle' }
65+
| { type: 'preview'; container: HTMLElement }
66+
| { type: 'dragging' };
67+
68+
const idleState: DraggableState = { type: 'idle' };
69+
const draggingState: DraggableState = { type: 'dragging' };
2070

2171
interface SelectionsContextState {
2272
isOverflow: boolean;
@@ -167,14 +217,13 @@ interface BaseSectionProps {
167217

168218
interface SectionItemProps extends BaseSectionProps {
169219
id?: string;
220+
index?: number;
170221
onClick?: (id, event?: MouseEvent<HTMLDivElement>) => void;
171222
children?: ReactNode | ((options: { isOpen?: boolean }) => ReactNode);
172223
isOpen: boolean;
173224
sticky?: boolean;
174-
onReorder?: (
175-
sourceId: string | undefined,
176-
targetId: string | undefined,
177-
) => void;
225+
onReorder?: (sourceId: number, targetId: number) => void;
226+
dragLabel?: string;
178227
}
179228

180229
interface SectionProps {
@@ -220,6 +269,7 @@ function SectionBody(props: HTMLAttributes<HTMLDivElement>) {
220269

221270
function SectionItem(props: SectionItemProps) {
222271
const {
272+
dragLabel = props.title,
223273
id = props.title,
224274
title,
225275
onClick,
@@ -232,30 +282,116 @@ function SectionItem(props: SectionItemProps) {
232282
sticky = false,
233283
arrowProps = { hide: false, style: {} },
234284
onReorder,
285+
index,
235286
} = props;
236287

237288
const { isOverflow, matchContentHeight } = useSections();
238289

239290
const wrapperRef = useRef<HTMLDivElement>(null);
240291
const handleRef = useRef<HTMLDivElement>(null);
292+
const [state, setState] = useState<DraggableState>(idleState);
293+
const [closestEdge, setClosestEdge] = useState<Edge | null>(null);
294+
const isReorderActive = typeof onReorder === 'function';
241295

242296
useEffect(() => {
243-
if (!wrapperRef.current || !handleRef.current) return;
244-
245-
const cleanDrag = draggable({
246-
element: wrapperRef.current,
247-
dragHandle: handleRef.current,
248-
getInitialData: () => ({ id }),
249-
});
297+
const dragHandle = handleRef.current;
298+
const element = wrapperRef.current;
299+
if (!element || !dragHandle || !isReorderActive) return;
300+
301+
const data = { id, index };
302+
303+
function canDrop({ source }: { source: ElementDragPayload }): boolean {
304+
return source.data.id !== id;
305+
}
306+
307+
function dragHandler({ source, self }: ElementDropTargetEventBasePayload) {
308+
const isSource = source.element === dragHandle;
309+
if (isSource) {
310+
// eslint-disable-next-line react-you-might-not-need-an-effect/no-chain-state-updates
311+
setClosestEdge(null);
312+
return;
313+
}
314+
315+
const closestEdge = extractClosestEdge(self.data);
316+
317+
const sourceIndex = source.data.index as number;
318+
assert(typeof sourceIndex === 'number', 'index is not defined');
319+
const isItemBeforeSource = index === sourceIndex - 1;
320+
const isItemAfterSource = index === sourceIndex + 1;
321+
322+
const isDropIndicatorHidden =
323+
(isItemBeforeSource && closestEdge === 'bottom') ||
324+
(isItemAfterSource && closestEdge === 'top');
325+
326+
setClosestEdge(isDropIndicatorHidden ? null : closestEdge);
327+
}
328+
329+
const cleanDrag = combine(
330+
draggable({
331+
element,
332+
dragHandle,
333+
getInitialData: () => data,
334+
onGenerateDragPreview({ nativeSetDragImage }) {
335+
setCustomNativeDragPreview({
336+
nativeSetDragImage,
337+
getOffset: pointerOutsideOfPreview({
338+
x: '8px',
339+
y: '8px',
340+
}),
341+
render({ container }) {
342+
setState({ type: 'preview', container });
343+
344+
return () => setState(draggingState);
345+
},
346+
});
347+
},
348+
onDragStart() {
349+
setState(draggingState);
350+
},
351+
onDrop() {
352+
setState(idleState);
353+
},
354+
}),
355+
dropTargetForElements({
356+
element,
357+
canDrop,
358+
getIsSticky: () => false,
359+
getData({ input }) {
360+
return attachClosestEdge(data, {
361+
element,
362+
input,
363+
allowedEdges: ['top', 'bottom'],
364+
});
365+
},
366+
onDrag: dragHandler,
367+
368+
onDragLeave() {
369+
setClosestEdge(null);
370+
},
371+
onDrop() {
372+
setClosestEdge(null);
373+
},
374+
}),
375+
);
250376

251377
return () => {
252378
cleanDrag();
253379
};
254-
}, [id]);
255-
380+
}, [id, index, isReorderActive]);
256381
return (
257-
<DroppableSectionWrapper key={id} id={id} onReorder={onReorder}>
258-
<div ref={wrapperRef}>
382+
<DroppableSectionWrapper
383+
key={id}
384+
id={id}
385+
onReorder={onReorder}
386+
index={index}
387+
>
388+
<div
389+
style={{
390+
position: 'relative',
391+
opacity: state && state.type === 'dragging' ? 0.3 : 1,
392+
}}
393+
ref={wrapperRef}
394+
>
259395
<SectionWrapper
260396
isOpen={isOpen}
261397
isOverflow={isOverflow}
@@ -284,7 +420,10 @@ function SectionItem(props: SectionItemProps) {
284420
arrowProps={arrowProps}
285421
/>
286422
<Wrapper isOpen={isOpen}>{children}</Wrapper>
423+
{closestEdge && <DropIndicator edge={closestEdge} />}
287424
</SectionWrapper>
425+
{state.type === 'preview' &&
426+
createPortal(<Preview>{dragLabel}</Preview>, state.container)}
288427
</div>
289428
</DroppableSectionWrapper>
290429
);
@@ -376,36 +515,60 @@ function MainSectionHeader(props: MainSectionHeaderProps) {
376515
);
377516
}
378517

379-
interface DroppableProps extends Pick<SectionItemProps, 'id' | 'onReorder'> {
518+
interface DroppableProps
519+
extends Pick<SectionItemProps, 'id' | 'onReorder' | 'index'> {
380520
children: ReactNode;
381521
}
382522

383-
function DroppableSectionWrapper({ id, onReorder, children }: DroppableProps) {
523+
function DroppableSectionWrapper(props: DroppableProps) {
524+
const { id, onReorder, index: indexOfTarget, children } = props;
384525
const ref = useRef<HTMLDivElement>(null);
385-
const [isOver, setIsOver] = useState(false);
386526

387527
useEffect(() => {
388-
if (!ref.current) return;
528+
if (typeof onReorder !== 'function') return;
529+
530+
const element = ref.current;
531+
532+
if (!element) return;
389533

390534
return dropTargetForElements({
391-
element: ref.current,
392-
canDrop: ({ source }) => source.data?.id !== id,
393-
onDrop: ({ source }) => {
394-
const sourceId = source.data?.id as string;
395-
setIsOver(false);
396-
onReorder?.(sourceId, id);
535+
element,
536+
onDrop: ({ location, source }) => {
537+
const { index: startIndex } = source.data;
538+
const target = location.current.dropTargets[0];
539+
if (!target) {
540+
return;
541+
}
542+
543+
if (
544+
typeof startIndex !== 'number' ||
545+
typeof indexOfTarget !== 'number'
546+
) {
547+
return;
548+
}
549+
550+
const targetData = target.data;
551+
const closestEdgeOfTarget = extractClosestEdge(targetData);
552+
553+
const finishIndex = getReorderDestinationIndex({
554+
startIndex,
555+
closestEdgeOfTarget,
556+
indexOfTarget,
557+
axis: 'vertical',
558+
});
559+
560+
if (finishIndex === startIndex) {
561+
// If there would be no change, we skip the update
562+
return;
563+
}
564+
565+
onReorder?.(startIndex, finishIndex);
397566
},
398-
onDragEnter: () => setIsOver(true),
399-
onDragLeave: () => setIsOver(false),
400567
});
401-
}, [id, onReorder]);
568+
}, [id, indexOfTarget, onReorder]);
402569

403570
return (
404-
<div
405-
ref={ref}
406-
data-drop-id={id}
407-
style={{ ...(isOver && { opacity: 0.1 }) }}
408-
>
571+
<div ref={ref} data-drop-id={id}>
409572
{children}
410573
</div>
411574
);

src/component/elements/pdnd.cts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,11 @@ export {
77
draggable,
88
dropTargetForElements,
99
} from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
10+
export {
11+
attachClosestEdge,
12+
extractClosestEdge,
13+
} from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
14+
export { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
15+
export { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';
16+
export { pointerOutsideOfPreview } from '@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview';
17+
export { getReorderDestinationIndex } from '@atlaskit/pragmatic-drag-and-drop-hitbox/util/get-reorder-destination-index';

src/component/panels/filtersPanel/Filters/FiltersSectionsPanel.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -262,12 +262,11 @@ function FiltersInner(props: FiltersInnerProps) {
262262
openSection(null);
263263
}
264264

265-
function handleReorderFilters(sourceId, targetId) {
266-
if (!sourceId || !targetId) {
267-
return;
268-
}
269-
270-
dispatch({ type: 'REORDER_FILTERS', payload: { sourceId, targetId } });
265+
function handleReorderFilters(sourceIndex, targetIndex) {
266+
dispatch({
267+
type: 'REORDER_FILTERS',
268+
payload: { sourceIndex, targetIndex },
269+
});
271270
}
272271

273272
return (
@@ -278,8 +277,10 @@ function FiltersInner(props: FiltersInnerProps) {
278277
const enableEdit = activeFilterID === id || filter.value === null;
279278
return (
280279
<Sections.Item
280+
index={index}
281281
key={id}
282282
id={name}
283+
dragLabel={getFilterLabel(name)}
283284
onReorder={handleReorderFilters}
284285
title={error || getFilterLabel(name)}
285286
serial={index + 1}

src/component/reducer/actions/FiltersActions.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ type DeletePhaseCorrectionTrace = ActionType<
218218
>;
219219
type ReorderFiltersAction = ActionType<
220220
'REORDER_FILTERS',
221-
{ sourceId: string; targetId: string }
221+
{ sourceIndex: number; targetIndex: number }
222222
>;
223223

224224
type SetOneDimensionPhaseCorrectionPivotPoint = ActionType<
@@ -2022,7 +2022,7 @@ function handleReorderFilters(
20222022
draft: Draft<State>,
20232023
action: ReorderFiltersAction,
20242024
) {
2025-
const { sourceId, targetId } = action.payload;
2025+
const { sourceIndex, targetIndex } = action.payload;
20262026
const spectrum = getSpectrum(draft);
20272027

20282028
if (!spectrum) {
@@ -2031,9 +2031,6 @@ function handleReorderFilters(
20312031

20322032
const filters = spectrum.filters;
20332033

2034-
const sourceIndex = filters.findIndex((filter) => filter.name === sourceId);
2035-
const targetIndex = filters.findIndex((filter) => filter.name === targetId);
2036-
20372034
if (sourceIndex === -1 || targetIndex === -1) return;
20382035

20392036
const sourceFilter = filters[sourceIndex];

0 commit comments

Comments
 (0)