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

Use ResizeObserver to compensate component height when it changes #691

Open
wants to merge 1 commit 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
93 changes: 80 additions & 13 deletions lib/Draggable.js
Expand Up @@ -3,13 +3,13 @@ import * as React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import clsx from 'clsx';
import {createCSSTransform, createSVGTransform} from './utils/domFns';
import {createCSSTransform, createSVGTransform } from './utils/domFns';
import {canDragX, canDragY, createDraggableData, getBoundPosition} from './utils/positionFns';
import {dontSetMe} from './utils/shims';
import DraggableCore from './DraggableCore';
import type {ControlPosition, PositionOffsetControlPosition, DraggableCoreProps, DraggableCoreDefaultProps} from './DraggableCore';
import log from './utils/log';
import type {Bounds, DraggableEventHandler} from './utils/types';
import type {Bounds, DraggableEventHandler, LastCorePositionChangeHandler, ContentResizeHandler} from './utils/types';
import type {Element as ReactElement} from 'react';

type DraggableState = {
Expand All @@ -19,6 +19,14 @@ type DraggableState = {
slackX: number, slackY: number,
isElementSVG: boolean,
prevPropsPosition: ?ControlPosition,
prevContentDimensions: ?{
width: number,
height: number
},
lastCorePosition: ?{
x: number,
y: number
}
};

export type DraggableDefaultProps = {
Expand All @@ -45,6 +53,8 @@ export type DraggableProps = {

class Draggable extends React.Component<DraggableProps, DraggableState> {

resizeObserver: null|ResizeObserver = null;

static displayName: ?string = 'Draggable';

static propTypes = {
Expand Down Expand Up @@ -180,6 +190,7 @@ class Draggable extends React.Component<DraggableProps, DraggableState> {
// React 16.3+
// Arity (props, state)
static getDerivedStateFromProps({position}: DraggableProps, {prevPropsPosition}: DraggableState): ?$Shape<DraggableState> {
const newState: $Shape<DraggableState> = {};
// Set x/y if a new position is provided in props that is different than the previous.
if (
position &&
Expand All @@ -188,13 +199,11 @@ class Draggable extends React.Component<DraggableProps, DraggableState> {
)
) {
log('Draggable: getDerivedStateFromProps %j', {position, prevPropsPosition});
return {
x: position.x,
y: position.y,
prevPropsPosition: {...position}
};
newState.x = position.x;
newState.y = position.y;
newState.prevPropsPosition = {...position};
}
return null;
return Object.keys(newState).length ? newState : null;
}

constructor(props: DraggableProps) {
Expand All @@ -217,7 +226,9 @@ class Draggable extends React.Component<DraggableProps, DraggableState> {
slackX: 0, slackY: 0,

// Can only determine if SVG after mounting
isElementSVG: false
isElementSVG: false,
prevContentDimensions: null,
lastCorePosition: null
};

if (props.position && !(props.onDrag || props.onStop)) {
Expand All @@ -228,15 +239,67 @@ class Draggable extends React.Component<DraggableProps, DraggableState> {
}
}

onContentResize: ContentResizeHandler = ({ width: newWidth, height: newHeight, target }) => {
const prevHeight = this.state.prevContentDimensions?.height;
if(this.state.lastCorePosition && prevHeight && newHeight!==prevHeight){
const { x: lastCoreX, y: lastCoreY } = this.state.lastCorePosition;
const heightDelta = newHeight-prevHeight;
const node = this.findDOMNode();
if (node) {
const handleNode = this.props.handle ? target.querySelector(this.props.handle) || node : node;
// get current component position
const { left, top } = handleNode.getBoundingClientRect();
// Emulate mouse move to the component position
const mouseEvent = new MouseEvent('mousemove', {
bubbles: true,
cancelable: true,
clientX: left,
clientY: top
});
// compensate height delta to keep component at the position it was before resize
this.onDrag(mouseEvent, {
x: lastCoreX,
y: top > 0 ? lastCoreY : (heightDelta + top),
deltaX: 0,
deltaY: top > 0 ? 0 : heightDelta,
lastX: lastCoreX,
lastY: lastCoreY,
node
}, true);
}
}
this.setState({prevContentDimensions: {
width: newWidth,
height: newHeight
}});
};


componentDidMount() {
const node = this.findDOMNode();
// Check to see if the element passed is an instanceof SVGElement
if(typeof window.SVGElement !== 'undefined' && this.findDOMNode() instanceof window.SVGElement) {
if(typeof window.SVGElement !== 'undefined' && node instanceof window.SVGElement) {
this.setState({isElementSVG: true});
}
// observe draggable content resize
if(node){
this.resizeObserver = new ResizeObserver((entries) => {
entries.forEach(entry => {
const { target } = entry;
const { width, height } = entry.contentRect;
this.onContentResize({width, height, target});
});
});
this.resizeObserver.observe(node);
}
}

componentWillUnmount() {
this.setState({dragging: false}); // prevents invariant if unmounted while dragging
// stop observing draggable content resize
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
}

// React Strict Mode compatibility: if `nodeRef` is passed, we will use it instead of trying to find
Expand All @@ -256,8 +319,8 @@ class Draggable extends React.Component<DraggableProps, DraggableState> {
this.setState({dragging: true, dragged: true});
};

onDrag: DraggableEventHandler = (e, coreData) => {
if (!this.state.dragging) return false;
onDrag: DraggableEventHandler = (e, coreData, force) => {
if (!this.state.dragging && !force) return false;
log('Draggable: onDrag: %j', coreData);

const uiData = createDraggableData(this, coreData);
Expand Down Expand Up @@ -328,6 +391,10 @@ class Draggable extends React.Component<DraggableProps, DraggableState> {
this.setState(newState);
};

onLastCorePositionChange: LastCorePositionChangeHandler = (x, y) => {
this.setState({lastCorePosition: {x, y}});
};

render(): ReactElement<any> {
const {
axis,
Expand Down Expand Up @@ -383,7 +450,7 @@ class Draggable extends React.Component<DraggableProps, DraggableState> {
// Reuse the child provided
// This makes it flexible to use whatever element is wanted (div, ul, etc)
return (
<DraggableCore {...draggableCoreProps} onStart={this.onDragStart} onDrag={this.onDrag} onStop={this.onDragStop}>
<DraggableCore {...draggableCoreProps} onStart={this.onDragStart} onDrag={this.onDrag} onStop={this.onDragStop} onLastCorePositionChange={this.onLastCorePositionChange}>
{React.cloneElement(React.Children.only(children), {
className: className,
style: {...children.props.style, ...style},
Expand Down
5 changes: 4 additions & 1 deletion lib/DraggableCore.js
Expand Up @@ -8,7 +8,7 @@ import {createCoreData, getControlPosition, snapToGrid} from './utils/positionFn
import {dontSetMe} from './utils/shims';
import log from './utils/log';

import type {EventHandler, MouseTouchEvent} from './utils/types';
import type {EventHandler, MouseTouchEvent, LastCorePositionChangeHandler} from './utils/types';
import type {Element as ReactElement} from 'react';

// Simple abstraction for dragging events names.
Expand Down Expand Up @@ -55,6 +55,7 @@ export type DraggableCoreDefaultProps = {
onDrag: DraggableEventHandler,
onStop: DraggableEventHandler,
onMouseDown: (e: MouseEvent) => void,
onLastCorePositionChange: LastCorePositionChangeHandler,
scale: number,
};

Expand Down Expand Up @@ -224,6 +225,7 @@ export default class DraggableCore extends React.Component<DraggableCoreProps, D
onDrag: function(){},
onStop: function(){},
onMouseDown: function(){},
onLastCorePositionChange: function() {},
scale: 1,
};

Expand Down Expand Up @@ -376,6 +378,7 @@ export default class DraggableCore extends React.Component<DraggableCoreProps, D
lastX: x,
lastY: y
});
this.props.onLastCorePositionChange(x, y);
};

handleDragStop: EventHandler<MouseTouchEvent> = (e) => {
Expand Down
16 changes: 11 additions & 5 deletions lib/utils/domFns.js
Expand Up @@ -106,15 +106,21 @@ interface EventWithOffset {
clientX: number, clientY: number
}

export function getOffsetParentRect(offsetParent: HTMLElement): {
left: number;
top: number;
} {
const isBody = offsetParent === offsetParent.ownerDocument.body;
const { top, left } = isBody ? {left: 0, top: 0} : offsetParent.getBoundingClientRect();
return { top, left };
}

// Get from offsetParent
export function offsetXYFromParent(evt: EventWithOffset, offsetParent: HTMLElement, scale: number): ControlPosition {
const isBody = offsetParent === offsetParent.ownerDocument.body;
const offsetParentRect = isBody ? {left: 0, top: 0} : offsetParent.getBoundingClientRect();

const offsetParentRect = getOffsetParentRect(offsetParent);
const x = (evt.clientX + offsetParent.scrollLeft - offsetParentRect.left) / scale;
const y = (evt.clientY + offsetParent.scrollTop - offsetParentRect.top) / scale;

return {x, y};
return { x, y };
}

export function createCSSTransform(controlPos: ControlPosition, positionOffset: PositionOffsetControlPosition): Object {
Expand Down
29 changes: 21 additions & 8 deletions lib/utils/positionFns.js
@@ -1,22 +1,28 @@
// @flow
import {isNum, int} from './shims';
import {getTouch, innerWidth, innerHeight, offsetXYFromParent, outerWidth, outerHeight} from './domFns';
import { isNum, int } from './shims';
import { getTouch, innerWidth, innerHeight, offsetXYFromParent, outerWidth, outerHeight } from './domFns';

import type Draggable from '../Draggable';
import type {Bounds, ControlPosition, DraggableData, MouseTouchEvent} from './types';
import type { Bounds, ControlPosition, DraggableData, MouseTouchEvent } from './types';
import type DraggableCore from '../DraggableCore';

export function getBoundPosition(draggable: Draggable, x: number, y: number): [number, number] {
// If no bounds, short-circuit and move on
if (!draggable.props.bounds) return [x, y];
export function getBounds(draggable: Draggable): null | {
bottom?: number,
left?: number,
right?: number,
top?: number
} {
if (!draggable.props.bounds) {
return null;
}

// Clone new bounds
let {bounds} = draggable.props;
let { bounds } = draggable.props;
bounds = typeof bounds === 'string' ? bounds : cloneBounds(bounds);
const node = findDOMNode(draggable);

if (typeof bounds === 'string') {
const {ownerDocument} = node;
const { ownerDocument } = node;
const ownerWindow = ownerDocument.defaultView;
let boundNode;
if (bounds === 'parent') {
Expand All @@ -40,6 +46,13 @@ export function getBoundPosition(draggable: Draggable, x: number, y: number): [n
int(boundNodeStyle.paddingBottom) - int(nodeStyle.marginBottom)
};
}
return bounds;
}

export function getBoundPosition(draggable: Draggable, x: number, y: number): [number, number] {
const bounds = getBounds(draggable);
// If no bounds, short-circuit and move on
if (!bounds) return [x, y];

// Keep x and y below right and bottom limits...
if (isNum(bounds.right)) x = Math.min(x, bounds.right);
Expand Down
6 changes: 5 additions & 1 deletion lib/utils/types.js
@@ -1,7 +1,11 @@
// @flow

// eslint-disable-next-line no-use-before-define
export type DraggableEventHandler = (e: MouseEvent, data: DraggableData) => void | false;
export type DraggableEventHandler = (e: MouseEvent, data: DraggableData, force: ?boolean) => void | false;

export type LastCorePositionChangeHandler = (x: number, y: number ) => void | false;

export type ContentResizeHandler = (params: {width: number, height: number, target: Element }) => void | false;

export type DraggableData = {
node: HTMLElement,
Expand Down
2 changes: 1 addition & 1 deletion specs/draggable.spec.jsx
Expand Up @@ -96,7 +96,7 @@ describe('react-draggable', function () {
);

// Not easy to actually test equality here. The functions are bound as static props so we can't test those easily.
const toOmit = ['onStart', 'onStop', 'onDrag', 'onMouseDown', 'children'];
const toOmit = ['onStart', 'onStop', 'onDrag', 'onMouseDown', 'children', 'onLastCorePositionChange'];
assert.deepEqual(
_.omit(output.props, toOmit),
_.omit(expected.props, toOmit)
Expand Down