Skip to content
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
16 changes: 16 additions & 0 deletions src/components/GridItem/GridItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,24 @@ class GridItem extends React.PureComponent<GridItemProps, GridItemState> {
context!: React.ContextType<typeof DashKitContext>;

_isAsyncItem = false;
_isMounted = false;
controller: AbortController | null = null;

state: GridItemState = {
isFocused: false,
};

componentDidMount() {
this._isMounted = true;
}

componentWillUnmount() {
this._isMounted = false;
if (this.controller) {
this.controller.abort();
}
}

renderOverlay() {
const {isPlaceholder} = this.props;
const {editMode} = this.context;
Expand Down Expand Up @@ -132,6 +144,10 @@ class GridItem extends React.PureComponent<GridItemProps, GridItemState> {

// requestAnimationFrame to make call after alert() or confirm()
requestAnimationFrame(() => {
if (!this._isMounted) {
return;
}

// Adding elment an changing focus
document.body.appendChild(focusDummy);
focusDummy.focus();
Expand Down
192 changes: 97 additions & 95 deletions src/components/GridLayout/GridLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,14 @@ import type {DragOverEvent} from 'react-grid-layout';

import type {PluginRef, PluginWidgetProps, ReactGridLayoutProps} from 'src/typings';

import {
COMPACT_TYPE_HORIZONTAL_NOWRAP,
DEFAULT_GROUP,
DRAGGABLE_CANCEL_CLASS_NAME,
TEMPORARY_ITEM_ID,
} from '../../constants';
import {DashKitContext} from '../../context';
import {COMPACT_TYPE_HORIZONTAL_NOWRAP, DEFAULT_GROUP, TEMPORARY_ITEM_ID} from '../../constants';
import type {DashKitCtxShape} from '../../context';
import {DashKitContext} from '../../context';
import type {ConfigItem, ConfigLayout, DraggedOverItem} from '../../shared';
import {resolveLayoutGroup} from '../../utils';
import GridItem from '../GridItem/GridItem';

import {Layout} from './ReactGridLayout';
import {GroupLayout} from './GroupLayout';
import type {
CurrentDraggingElement,
GridLayoutProps,
Expand Down Expand Up @@ -49,6 +44,16 @@ export default class GridLayout extends React.PureComponent<GridLayoutProps, Gri
private _memoGroupsProps: Record<string, Partial<ReactGridLayoutProps>> = {};
private _memoGroupsLayouts: Record<string, MemoGroupLayout> = {};
private _memoCallbacksForGroups: Record<string, GroupCallbacks> = {};
private _memoGroupsItems: Record<string, ConfigItem[]> = {};

private _sharedDragRef: React.MutableRefObject<{
isDragging: boolean;
sourceGroup: string | null;
}> = {current: {isDragging: false, sourceGroup: null}};
private _sharedDragPositionRef: React.MutableRefObject<{
offsetX: number;
offsetY: number;
} | null> = {current: null};

private _timeout?: NodeJS.Timeout;
private _lastReloadAt?: number;
Expand Down Expand Up @@ -83,6 +88,7 @@ export default class GridLayout extends React.PureComponent<GridLayoutProps, Gri
this._memoCallbacksForGroups = {};
this._memoGroupsProps = {};
this._memoGroupsLayouts = {};
this._memoGroupsItems = {};
this._memoForwardedPluginRef = [];
}

Expand Down Expand Up @@ -181,17 +187,41 @@ export default class GridLayout extends React.PureComponent<GridLayoutProps, Gri
return this._memoCallbacksForGroups[group];
}

getMemoGroupItems(group: string, nextItems: ConfigItem[]): ConfigItem[] {
const prev = this._memoGroupsItems[group];
if (
prev &&
prev.length === nextItems.length &&
prev.every((item, i) => item === nextItems[i])
) {
return prev;
}
this._memoGroupsItems[group] = nextItems;
return nextItems;
}

getMemoGroupProps = (
group: string,
renderLayout: ConfigLayout[],
properties: Partial<ReactGridLayoutProps>,
nextProperties: Partial<ReactGridLayoutProps>,
) => {
// Return stable ref to prevent useMemo invalidation when properties are shallowly equal.
const prevProperties = this._memoGroupsProps[group];
const keysNext = Object.keys(nextProperties) as Array<keyof ReactGridLayoutProps>;
const stableProperties =
prevProperties &&
Object.keys(prevProperties).length === keysNext.length &&
keysNext.every((k) => prevProperties[k] === nextProperties[k])
? prevProperties
: nextProperties;

// Needed for _onDropDragOver
this._memoGroupsProps[group] = properties;
this._memoGroupsProps[group] = stableProperties;

return {
layout: this.getMemoGroupLayout(group, renderLayout).layout,
callbacks: this.getMemoGroupCallbacks(group),
stableProperties,
};
};

Expand Down Expand Up @@ -336,6 +366,9 @@ export default class GridLayout extends React.PureComponent<GridLayoutProps, Gri
item,
cursorPosition: {offsetX, offsetY},
};

// Update imperatively so DragOverLayout reads fresh cursor offset without re-render.
this._sharedDragPositionRef.current = {offsetX, offsetY};
}

this.setState({currentDraggingElement, draggedOverGroup: group});
Expand Down Expand Up @@ -445,6 +478,7 @@ export default class GridLayout extends React.PureComponent<GridLayoutProps, Gri
if (this.context.dragOverPlugin) {
this.setState({isDragging: true});
} else {
this._sharedDragRef.current = {isDragging: true, sourceGroup: group};
this._initDragCoordinatesWatcher(element);
this.updateDraggingElementState(group, layoutItem, e);
this.setState({isDragging: true});
Expand Down Expand Up @@ -570,6 +604,8 @@ export default class GridLayout extends React.PureComponent<GridLayoutProps, Gri

const groupedLayout = this.mergeGroupsLayout(group, newLayout);

this._sharedDragRef.current = {isDragging: false, sourceGroup: null};
this._sharedDragPositionRef.current = null;
this.setState({
isDragging: false,
currentDraggingElement: null,
Expand Down Expand Up @@ -614,6 +650,8 @@ export default class GridLayout extends React.PureComponent<GridLayoutProps, Gri
},
);

this._sharedDragRef.current = {isDragging: false, sourceGroup: null};
this._sharedDragPositionRef.current = null;
this.setState({
isDragging: false,
currentDraggingElement: null,
Expand All @@ -637,6 +675,7 @@ export default class GridLayout extends React.PureComponent<GridLayoutProps, Gri
}

const groupedLayout = this.mergeGroupsLayout(group, newLayout, item);
this._sharedDragRef.current = {isDragging: false, sourceGroup: null};
this.setState({isDragging: false});

onDrop?.(groupedLayout, item, e);
Expand Down Expand Up @@ -732,107 +771,62 @@ export default class GridLayout extends React.PureComponent<GridLayoutProps, Gri
offset = 0,
groupGridProperties?: (props: ReactGridLayoutProps) => ReactGridLayoutProps,
) {
const {
registerManager,
editMode,
noOverlay,
focusable,
draggableHandleClassName,
outerDnDEnable,
onItemMountChange,
onItemRender,
onItemFocus,
onItemBlur,
} = this.context;
const {registerManager} = this.context;
const {currentDraggingElement, draggedOverGroup, isDragging, isDraggedOut} = this.state;

const {currentDraggingElement, draggedOverGroup} = this.state;
const hasOwnGroupProperties = typeof groupGridProperties === 'function';
const properties = hasOwnGroupProperties
? groupGridProperties({...registerManager.gridLayout})
: registerManager.gridLayout;
let {compactType} = properties;

if (compactType === COMPACT_TYPE_HORIZONTAL_NOWRAP) {
compactType = 'horizontal';
}

const {callbacks, layout} = this.getMemoGroupProps(group, renderLayout, properties);
const hasSharedDragItem = Boolean(
currentDraggingElement && currentDraggingElement.group !== group,
const {callbacks, layout, stableProperties} = this.getMemoGroupProps(
group,
renderLayout,
properties,
);

const compactType: 'vertical' | 'horizontal' | null | undefined =
stableProperties.compactType === COMPACT_TYPE_HORIZONTAL_NOWRAP
? 'horizontal'
: stableProperties.compactType;

const isDragCaptured = Boolean(
currentDraggingElement &&
group === currentDraggingElement.group &&
draggedOverGroup !== null &&
draggedOverGroup !== group,
);
const currentDraggingItemId =
currentDraggingElement && 'id' in currentDraggingElement.item
? currentDraggingElement.item.id
: null;

// Non-source groups get stable false/null — memo skips re-render on drag move.
const isSourceGroup = Boolean(currentDraggingElement?.group === group);
const groupIsAnyDragging = isDragging && isSourceGroup;
const groupCurrentDraggingItemId = isSourceGroup ? currentDraggingItemId : null;
const groupIsAnyDraggedOut = isSourceGroup ? isDraggedOut : false;

return (
<Layout
<GroupLayout
key={`group_${group}`}
isDraggable={editMode}
isResizable={editMode}
// Group properties
{...properties}
// Layout props
group={group}
renderItems={renderItems}
offset={offset}
properties={stableProperties}
compactType={compactType}
callbacks={callbacks}
layout={layout}
draggableCancel={`.${DRAGGABLE_CANCEL_CLASS_NAME}`}
draggableHandle={
draggableHandleClassName ? `.${draggableHandleClassName}` : undefined
}
// Default callbacks
onDragStart={callbacks.onDragStart}
onDrag={callbacks.onDrag}
onDragStop={callbacks.onDragStop}
onResizeStart={callbacks.onResizeStart}
onResize={callbacks.onResize}
onResizeStop={callbacks.onResizeStop}
// External Drag N Drop options
onDragTargetRestore={callbacks.onDragTargetRestore}
onDropDragOver={callbacks.onDropDragOver}
onDrop={callbacks.onDrop}
hasSharedDragItem={hasSharedDragItem}
sharedDragPosition={currentDraggingElement?.cursorPosition}
temporaryPlaceholder={this.renderTemporaryPlaceholder(stableProperties)}
dragStateRef={this._sharedDragRef}
sharedDragPositionRef={this._sharedDragPositionRef}
isDragCaptured={isDragCaptured}
isDroppable={Boolean(outerDnDEnable) && editMode}
>
{renderItems.map((item, i) => {
const keyId = item.id;
const isDragging = this.state.isDragging;
const isCurrentDraggedItem =
currentDraggingElement &&
'id' in currentDraggingElement.item &&
currentDraggingElement.item.id === keyId;
const isDraggedOut = isCurrentDraggedItem && this.state.isDraggedOut;
const itemNoOverlay =
hasOwnGroupProperties && 'noOverlay' in properties
? properties.noOverlay
: noOverlay;

return (
<GridItem
key={keyId}
forwardedPluginRef={this.getMemoForwardRefCallback(offset + i)} // forwarded ref to plugin
id={keyId}
item={item}
layout={layout}
adjustWidgetLayout={this.adjustWidgetLayout}
isDragging={isDragging}
isDraggedOut={isDraggedOut || undefined}
noOverlay={itemNoOverlay}
focusable={focusable}
withCustomHandle={Boolean(draggableHandleClassName)}
onItemMountChange={onItemMountChange}
onItemRender={onItemRender}
gridLayout={properties}
onItemFocus={onItemFocus}
onItemBlur={onItemBlur}
/>
);
})}
{this.renderTemporaryPlaceholder(properties)}
</Layout>
isAnyDragging={groupIsAnyDragging}
currentDraggingItemId={groupCurrentDraggingItemId}
isAnyDraggedOut={groupIsAnyDraggedOut}
adjustWidgetLayout={this.adjustWidgetLayout}
getMemoForwardRefCallback={this.getMemoForwardRefCallback}
/>
);
}

Expand Down Expand Up @@ -896,8 +890,15 @@ export default class GridLayout extends React.PureComponent<GridLayoutProps, Gri
items = itemsByGroup[id] || [];
}

const element = this.renderGroup(id, layout, items, offset, group.gridProperties);
offset += items.length;
const stableItems = this.getMemoGroupItems(id, items);
const element = this.renderGroup(
id,
layout,
stableItems,
offset,
group.gridProperties,
);
offset += stableItems.length;

if (group.render) {
const groupContext = {
Expand All @@ -914,7 +915,8 @@ export default class GridLayout extends React.PureComponent<GridLayoutProps, Gri
return element;
});
} else {
return this.renderGroup(DEFAULT_GROUP, defaultRenderLayout, defaultRenderItems, offset);
const stableDefaultItems = this.getMemoGroupItems(DEFAULT_GROUP, defaultRenderItems);
return this.renderGroup(DEFAULT_GROUP, defaultRenderLayout, stableDefaultItems, offset);
}
}
}
Loading
Loading