Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useDraggable - render only dragging item #1088

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions cypress/integration/draggable_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,4 +416,17 @@ describe('Draggable', () => {
});
});
});

describe('Multiple Draggables', () => {
it('should render only dragging element', () => {
cy.visitStory('core-draggable-multi-draggable--basic-setup')
.findFirstDraggableItem()

.mouseMoveBy(0, 100);

cy.get('[data-testid="1"]').should('have.text', 'updated');
cy.get('[data-testid="2"]').should('have.text', 'mounted');
cy.get('[data-testid="3"]').should('have.text', 'mounted');
});
});
});
2 changes: 1 addition & 1 deletion cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function getDocumentScroll() {
}

Cypress.Commands.add('findFirstDraggableItem', () => {
return cy.get(`[data-cypress="draggable-item"`);
return cy.get(`[data-cypress="draggable-item"]`).first();
});

Cypress.Commands.add(
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@types/classnames": "^2.2.11",
"@types/react": "^16.9.43",
"@types/react-dom": "^16.9.8",
"@types/use-sync-external-store": "^0.0.3",
"babel-jest": "^27.0.2",
"babel-loader": "^8.2.1",
"chromatic": "^5.4.0",
Expand Down
5 changes: 3 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@
"react-dom": ">=16.8.0"
},
"dependencies": {
"tslib": "^2.0.0",
"@dnd-kit/accessibility": "^3.0.0",
"@dnd-kit/utilities": "^3.2.1"
"@dnd-kit/utilities": "^3.2.1",
"tslib": "^2.0.0",
"use-sync-external-store": "^1.2.0"
},
"publishConfig": {
"access": "public"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ interface Props {
}

export function RestoreFocus({disabled}: Props) {
const {active, activatorEvent, draggableNodes} = useContext(InternalContext);
const {useGloablActive, useGlobalActivatorEvent, draggableNodes} =
useContext(InternalContext);
const active = useGloablActive();
const activatorEvent = useGlobalActivatorEvent();

const previousActivatorEvent = usePrevious(activatorEvent);
const previousActiveId = usePrevious(active?.id);

Expand Down
109 changes: 59 additions & 50 deletions packages/core/src/components/DndContext/DndContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,9 @@ import {
import {DndMonitorContext, useDndMonitorProvider} from '../DndMonitor';
import {
useAutoScroller,
useCachedNode,
useCombineActivators,
useDragOverlayMeasuring,
useDroppableMeasuring,
useInitialRect,
useRect,
useRectDelta,
useRects,
Expand Down Expand Up @@ -78,12 +76,14 @@ import {
ScreenReaderInstructions,
} from '../Accessibility';

import {defaultData, defaultSensors} from './defaults';
import {defaultSensors} from './defaults';
import {
useLayoutShiftScrollCompensation,
useMeasuringConfiguration,
} from './hooks';
import type {MeasuringConfiguration} from './types';
import {createActiveAPI} from './activeAPI';
import {useActiveNodeDomValues} from './useActiveNodeDomValues';

export interface Props {
id?: string;
Expand Down Expand Up @@ -149,29 +149,23 @@ export const DndContext = memo(function DndContext({
const [status, setStatus] = useState<Status>(Status.Uninitialized);
const isInitialized = status === Status.Initialized;
const {
draggable: {active: activeId, nodes: draggableNodes, translate},
draggable: {translate},
droppable: {containers: droppableContainers},
} = state;
const node = activeId ? draggableNodes.get(activeId) : null;
const activeRects = useRef<Active['rect']['current']>({
initial: null,
translated: null,
});
const active = useMemo<Active | null>(
() =>
activeId != null
? {
id: activeId,
// It's possible for the active node to unmount while dragging
data: node?.data ?? defaultData,
rect: activeRects,
}
: null,
[activeId, node]
);

const activeAPI = useMemo(() => createActiveAPI(activeRects), []);
const draggableNodes = activeAPI.draggableNodes;
const active = activeAPI.useActive();
const activeId = active?.id || null;

const activatorEvent = activeAPI.useActivatorEvent();

const activeRef = useRef<UniqueIdentifier | null>(null);
const [activeSensor, setActiveSensor] = useState<SensorInstance | null>(null);
const [activatorEvent, setActivatorEvent] = useState<Event | null>(null);
const latestProps = useLatestValue(props, Object.values(props));
const draggableDescribedById = useUniqueId(`DndDescribedBy`, id);
const enabledDroppableContainers = useMemo(
Expand All @@ -185,16 +179,25 @@ export const DndContext = memo(function DndContext({
dependencies: [translate.x, translate.y],
config: measuringConfiguration.droppable,
});
const activeNode = useCachedNode(draggableNodes, activeId);

const activeNodeDomValues = useActiveNodeDomValues(
draggableNodes,
measuringConfiguration,
active?.id || null
);
const activeNode = activeNodeDomValues?.activeNode || null;
const initialActiveNodeRect =
activeNodeDomValues?.initialActiveNodeRect || null;
const activeNodeRect = activeNodeDomValues?.activeNodeRect || null;
const containerNodeRect = useRect(
activeNode ? activeNode.parentElement : null
);

const activationCoordinates = useMemo(
() => (activatorEvent ? getEventCoordinates(activatorEvent) : null),
[activatorEvent]
);
const autoScrollOptions = getAutoScrollerOptions();
const initialActiveNodeRect = useInitialRect(
activeNode,
measuringConfiguration.draggable.measure
);

useLayoutShiftScrollCompensation({
activeNode: activeId ? draggableNodes.get(activeId) : null,
Expand All @@ -203,14 +206,6 @@ export const DndContext = memo(function DndContext({
measure: measuringConfiguration.draggable.measure,
});

const activeNodeRect = useRect(
activeNode,
measuringConfiguration.draggable.measure,
initialActiveNodeRect
);
const containerNodeRect = useRect(
activeNode ? activeNode.parentElement : null
);
const sensorContext = useRef<SensorContext>({
activatorEvent: null,
active: null,
Expand Down Expand Up @@ -365,10 +360,10 @@ export const DndContext = memo(function DndContext({
unstable_batchedUpdates(() => {
onDragStart?.(event);
setStatus(Status.Initializing);
activeAPI.setActive(id);
dispatch({
type: Action.DragStart,
type: Action.SetInitiailCoordinates,
initialCoordinates,
active: id,
});
dispatchMonitorEvent({type: 'onDragStart', event});
});
Expand All @@ -379,16 +374,16 @@ export const DndContext = memo(function DndContext({
coordinates,
});
},
onEnd: createHandler(Action.DragEnd),
onCancel: createHandler(Action.DragCancel),
onEnd: createHandler('DragEnd'),
onCancel: createHandler('DragCancel'),
});

unstable_batchedUpdates(() => {
setActiveSensor(sensorInstance);
setActivatorEvent(event.nativeEvent);
activeAPI.setActivatorEvent(event.nativeEvent);
});

function createHandler(type: Action.DragEnd | Action.DragCancel) {
function createHandler(type: 'DragEnd' | 'DragCancel') {
return async function handler() {
const {active, collisions, over, scrollAdjustedTranslate} =
sensorContext.current;
Expand All @@ -405,26 +400,26 @@ export const DndContext = memo(function DndContext({
over,
};

if (type === Action.DragEnd && typeof cancelDrop === 'function') {
if (type === 'DragEnd' && typeof cancelDrop === 'function') {
const shouldCancel = await Promise.resolve(cancelDrop(event));

if (shouldCancel) {
type = Action.DragCancel;
type = 'DragCancel';
}
}
}

activeRef.current = null;

unstable_batchedUpdates(() => {
dispatch({type});
activeAPI.setActive(null);
dispatch({type: Action.ClearCoordinates});
setStatus(Status.Uninitialized);
setOver(null);
setActiveSensor(null);
setActivatorEvent(null);
activeAPI.setActivatorEvent(null);

const eventName =
type === Action.DragEnd ? 'onDragEnd' : 'onDragCancel';
const eventName = type === 'DragEnd' ? 'onDragEnd' : 'onDragCancel';

if (event) {
const handler = latestProps.current[eventName];
Expand Down Expand Up @@ -665,30 +660,44 @@ export const DndContext = memo(function DndContext({

const internalContext = useMemo(() => {
const context: InternalContextDescriptor = {
activatorEvent,
useMyActive: activeAPI.useMyActive,
useHasActive: activeAPI.useHasActive,
useGloablActive: activeAPI.useActive,
useMyActivatorEvent: activeAPI.useMyActivatorEvent,
useGlobalActivatorEvent: activeAPI.useActivatorEvent,
useMyActiveNodeRect: (id: UniqueIdentifier) => {
const domValues = useActiveNodeDomValues(
draggableNodes,
measuringConfiguration,
id
);
return domValues?.activeNodeRect || null;
},
activators,
active,
activeNodeRect,
ariaDescribedById: {
draggable: draggableDescribedById,
},
dispatch,
draggableNodes,
over,
measureDroppableContainers,
isDefaultContext: false,
};

return context;
}, [
activatorEvent,
activeAPI.useMyActive,
activeAPI.useHasActive,
activeAPI.useActive,
activeAPI.useMyActivatorEvent,
activeAPI.useActivatorEvent,
activators,
active,
activeNodeRect,
dispatch,
draggableDescribedById,
dispatch,
draggableNodes,
over,
measureDroppableContainers,
measuringConfiguration,
]);

return (
Expand Down
80 changes: 80 additions & 0 deletions packages/core/src/components/DndContext/activeAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type {MutableRefObject} from 'react';
import {useSyncExternalStore} from 'use-sync-external-store/shim';
import type {Active} from '../../store';

import type {UniqueIdentifier, ClientRect} from '../../types';
import {defaultData} from './defaults';

type Rects = MutableRefObject<{
initial: ClientRect | null;
translated: ClientRect | null;
}>;

export function createActiveAPI(rect: Rects) {
let activeId: UniqueIdentifier | null = null;
let active: Active | null = null;

let activatorEvent: Event | null = null;
const draggableNodes = new Map<UniqueIdentifier, any>();
const activeRects = rect;

const registry: (() => void)[] = [];

function subscribe(listener: () => void) {
registry.push(listener);
return () => {
registry.splice(registry.indexOf(listener), 1);
};
}

function getActiveInfo() {
if (!activeId) return null;
const node = draggableNodes.get(activeId);
return {
id: activeId,
rect: activeRects,
data: node ? node.data : defaultData,
};
}

return {
draggableNodes,
setActive: function (id: UniqueIdentifier | null) {
if (activeId === id) return;
activeId = id;
active = getActiveInfo();
registry.forEach((li) => li());
},

setActivatorEvent: function (event: Event | null) {
activatorEvent = event;
registry.forEach((li) => li());
},

useIsDragging: function (id: UniqueIdentifier) {
return useSyncExternalStore(subscribe, () => activeId === id);
},

useHasActive: function () {
return useSyncExternalStore(subscribe, () => activeId !== null);
},
useActive: function () {
return useSyncExternalStore(subscribe, () => active);
},

useMyActive: function (id: UniqueIdentifier) {
return useSyncExternalStore(subscribe, () =>
activeId === id ? active : null
);
},

useActivatorEvent: function () {
return useSyncExternalStore(subscribe, () => activatorEvent);
},
useMyActivatorEvent: function (id: UniqueIdentifier) {
return useSyncExternalStore(subscribe, () =>
activeId === id ? activatorEvent : null
);
},
};
}
Loading