user10960236
user10960236

Reputation:

React Dnd out of position after scroll

I'm using react-beautiful-dnd to make table rows draggable.
The dragging is working fine if I am going from top to bottom, and when I scroll the page up it gets out of position.
I have no idea why.
Also, I didn't find anything weird with css

I have no idea why this is happening and don't know how to fix this. Here is an example of my problem.

enter image description here

This is my code:

import update from "immutability-helper";
import * as React from "react";
import { DragDropContext, Draggable, Droppable } from "react-beautiful-dnd";
import { WithNamespaces, withNamespaces } from "react-i18next";
import { toastr } from "react-redux-toastr";
import * as HttpHelper from "../../httpHelper";
import { FormState } from "../common/ValidatedForm";
type Props = WithNamespaces & {
  id: number;
  displayName: string;
  type: string;
  language: any;
};

interface Fields {
  columns: any;
}

type State = FormState<Fields> & {
  isLoading: boolean,
  canSave: boolean,
  isSaving: boolean,
  possibleTags: any,
  configTagModalActive: boolean,
  previewModalActive: boolean,
  activeTag: any
};
const getItemStyle = (isDragging: any, draggableStyle: any) => ({
  ...draggableStyle,
  opacity: isDragging ? 1 : 1,
  boxShadow: "0px 0px 0px 1px #8b8b8b",
});
const shadowColor = "#a0a0a057";
const Column = (props: any) => {
  function findindex(val: any, pt: any) {
    const list = pt ? props.possibleTags : props.tags;
    return list.findIndex((item: any) => val == item.name);
  }
  function findindexofhelptext(val: any, pt: any) {
    const list = pt;
    return list.findIndex((item: any) => val == item.language);
  }
  return (
    <tr ref={props.provided.innerRef} {...props.provided.draggableProps} style={getItemStyle(props.snapshot.isDragging, props.provided.draggableProps.style)} className={"draggablerow " + (props.snapshot.isDragging ? "draggedrow" : "") } key={props.indexnr} data-id={props.index} >
      <td {...props.provided.dragHandleProps} style={{width: "50px", textAlign: "center", cursor: "move"}}><i className="fa fa-bars" style={{lineHeight: "40px", fontSize: "24px"}}></i></td>
      <td style={{ textAlign: "center", width: "100px" }}>
        <input
          type="checkbox"
          className="flipswitch"
          id={props.index}
          checked={props.export}
          onChange={props.toggleVisible}
        />
      </td>
      <td style={{width: "350px" }}>
        <input
          type="text"
          name="caption"
          id={props.index}
          className="form-control"
          value={props.caption}
          onChange={props.onTextUpdate}
          style={{boxShadow: "2px 2px 3px 1px" + shadowColor}}
        />
      </td>
      <td style={{width: "350px" }}>
        <input
          type="text"
          name="fieldname"
          id={props.index}
          className="form-control"
          value={props.fieldname}
          onChange={props.onTextUpdate}
          style={{boxShadow: "2px 2px 3px 1px" + shadowColor}}
        />
      </td>
      <td style={{width: "400px"}}>
        <div className="tags-input" style={tagInputStyle}>
          {Object.keys(props.tags).map((key, i) =>
            <div key={i} className="tag" onClick={props.onConfigButtonClicked} data-id={i} data-parent={props.index}>
              {props.tags[i].name} <i className="fa fa-trash" id={props.index} data-key={i} data-name={props.tags[i].name} onClick={props.onDeleteTag} style={{float: "right"}} ></i>
            </div>
          )}
        </div>
      </td>
      <td style={{ textAlign: "center", width: "100px" }}>
        <button onClick={() => props.onDeleteColumn(props.index)} type="button" style={{padding : "8px 16px", boxShadow: "2px 2px 2px 1px" + shadowColor }} className="btn btn-danger btn-rounded"><i className="fa fa-trash"></i></button>
      </td>
      </tr>
  );
};
const reorder = (list: any, startIndex: any, endIndex: any) => {
  const result = Array.from(list);
  const [removed] = result.splice(startIndex, 1);
  result.splice(endIndex, 0, removed);
  return result;
};
interface SetColumnsResponse extends HttpHelper.ResponseData { columns: any; }

class CrmConnectorColumns extends React.Component<Props, State> {

  constructor(props: Props) {
    super(props);
    this.moveColumn = this.moveColumn.bind(this);
    this.state = {
      isLoading: true,
      isSaving: false,
      canSave: false,
      errorColor: "danger",
      fields: { columns: [] },
      deleteModalActive: false,
      configTagModalActive: false,
      previewModalActive: false,
      activeTag: {name: "", attributes: [{name: "", value: ""}]},
      possibleTags: [
        {name: "PRIMARY", status: "new", helptexts: [
          {language: "nl", helptext: "Dit is de primary key"},
          {language: "en", helptext: "This is the primary key"}
        ], attributes: [], uses: 1},
        {name: "SUBTITLE", status: "new", helptexts: [
          {language: "nl", helptext: "Dit is de subtitel van een record"},
          {language: "en", helptext: "This is The subtitle of a record"}
        ], attributes: [], uses: 1},
        {name: "URL", status: "new", helptexts: [
          {language: "nl", helptext: "De waarde wordt gezien als link."},
          {language: "en", helptext: "The value becomes a link."}
        ], attributes: [
          {name: "link", status: "new", helptexts: [
            {language: "nl", helptext: "De link krijgt deze waarde. Voorbeeld waarde is \"http://www.google.nl?search=[naam]\". de waarde van \"[naam]\" wordt ingevuld."},
            {language: "en", helptext: "The link gets this value. Example value is \"http://www.google.nl?search=[name]\". the value of \"[name]\" gets filled in."}
          ]}
        ], uses: undefined},
        {name: "TITLE", status: "new", helptexts: [
          {language: "nl", helptext: "Dit is de hoofdtitel van een record"},
          {language: "en", helptext: "This is the maintitle of a record"}
        ], attributes: [], uses: 1},
        {name: "PHONE", status: "new", helptexts: [
          {language: "nl", helptext: "De waarde wordt gezien als telefoonnummer"},
          {language: "en", helptext: "The value becomes a phonenumber"}
        ], attributes: [], uses: undefined},
        {name: "BUTTON", status: "new", helptexts: [
          {language: "nl", helptext: "Uiterlijk van een knop"},
          {language: "en", helptext: "The value becomes a button"}
        ], attributes: [], uses: undefined},
        {name: "EMAIL", status: "new", helptexts: [
          {language: "nl", helptext: "De waarde wordt gezien als e-mail adres"},
          {language: "en", helptext: "The value becomes a emailaddress"}
        ], attributes: [], uses: undefined},
        {name: "IMAGE", status: "new", helptexts: [
          {language: "nl", helptext: "De waarde wordt als afbeelding weergegeven"},
          {language: "en", helptext: "The value gets displayed as image"}
        ], attributes: [], uses: undefined},
        {name: "HTML", status: "new", helptexts: [
          {language: "nl", helptext: "De waarde wordt gezien als HTML"},
          {language: "en", helptext: "The value gets seen as custom HTML"}
        ], attributes: [
          {name: "HTML code", status: "new", helptexts: [
            {language: "nl", helptext: "Vul hier je custom HTML code in. De waarde tussen de [] word vervangen door de data."},
            {language: "en", helptext: "Enter your custom HTML here. The value between the [] will be replaced for the value."}
          ]}
        ], uses: undefined}
      ]
    };
    this.onDragEnd = this.onDragEnd.bind(this);
  }
  onDragEnd(result: any) {
    // dropped outside the columns table
    if (!result.destination) {
      return;
    }
    let newlist = [...this.state.fields.columns];
    newlist = reorder(
      newlist,
      result.source.index,
      result.destination.index
    );
    Object.keys(newlist).forEach((nr) => {
      newlist[parseInt(nr, 10)].index = parseInt(nr, 10);
    });
    this.setState({ fields: { columns: newlist } });
    this.setState({ canSave: true });
  }
  async componentDidMount() {
    console.log("Start select columns");
    const fields = await HttpHelper.getJson<Fields>(`/${this.props.type}/${this.props.id}/columns`);
    this.setState(prevState => {
      return update(prevState, {
        fields: { $set: fields },
        isLoading: { $set: false },
      });
    });
    if (this.state.fields.columns == undefined) {
      this.setState({ fields: { columns: [] } });
    }
    for (let i = 0; i < fields.columns.length; i++) {
      fields.columns[i].index = i;
    }
    this.setState({ fields: { columns: fields.columns } });
    const newlist = [...this.state.possibleTags];

    for (const column of fields.columns) {
      for (const tags of column.tags) {
        const index = newlist.map((item) => item.name).indexOf(tags.name);
        if (newlist[index].uses > 0) {
          newlist[index].uses = 0;
        }
      }
    }
    this.setState({ possibleTags: newlist });
  }
  moveColumn(index: any, indexnr: any) {
    const cards = this.state.fields.columns;
    const sourceCard = cards.find((card: any) => card.index === index);
    const sortCards = cards.filter((card: any) => card.index !== index);
    sortCards.splice(indexnr, 0, sourceCard);
     Object.keys(sortCards).forEach((nr) => {
    sortCards[nr].index = parseInt(nr, 10);
    });
    this.setState({ fields: { columns: sortCards } });
    this.setState({ canSave: true });
  }

  onDragStart = (e: any) => {
    e.dataTransfer.effectAllowed = "move";
    e.dataTransfer.setData("text/html", e.target.parentNode);
    e.dataTransfer.setDragImage(e.target.parentNode, 20, 20);
  }
  ondragOver(e: any) {
    e.preventDefault();
  }

  public render() {
    const columns = this.state.fields.columns || [] ;
    const { t } = this.props;
    let placeholder: any;

    if (columns.length < 1) {
      placeholder = <tr style={{boxShadow: "0px 0px 0px 1px #8b8b8b", textAlign: "center"}} className={"draggablerow"}><td colSpan={6} >{t("placeholder")}</td></tr>;
    }
    return (
      <form>
        <div className="App">
          <main>
            <button onClick={this.onSubmit} className="btn btn-primary" type="submit" style={{float: "right", boxShadow: "2px 2px 3px 1px" + shadowColor}} disabled={!this.state.canSave || this.state.isSaving}>{this.state.isSaving ? <i className="fa fa-spinner fa-spin"></i> : ""} {this.props.t("update")}</button>
            <button onClick={this.onPreviewButtonClicked} type="button" className="btn btn-primary"  style={{float: "right", boxShadow: "2px 2px 3px 1px" + shadowColor, marginRight: "5px"}} >Preview</button><br/><br/>
            <DragDropContext onDragEnd={this.onDragEnd}>
              <table className="col-8 table columns" style={{tableLayout: "auto"}} >
                <thead className="" style={{border: "2px solid #1b2847", background: "#1b2847", color: "white"}}>
                  <tr>
                    <th colSpan={2} style={{textAlign: "center"}}>
                      <button onClick={this.onAddColumn} disabled={columns.length > 14 ? true : false} type="button" style={{padding : "8px 16px", boxShadow: "2px 2px 3px 1px" + shadowColor }} className="btn btn-primary btn-rounded"><i className="fa fa-plus"></i> </button>
                    </th>
                    <th>{t("displayname")}</th>
                    <th>Element</th>
                    <th>Tags</th>
                    <th></th>
                  </tr>
                </thead>
                <Droppable droppableId="droppable" direction="vertical">
                  {(provided: any) => (
                    <tbody  ref={provided.innerRef}>
                      {Object.keys(columns).map((element, key) => (
                        <Draggable key={"draggable" + key} draggableId={element} index={key}>
                        {(provided, snapshot) => (
                          <Column
                          key={"column" + key}
                          indexnr={key}
                          toggleVisible={this.toggleVisible}
                          onTextUpdate={this.onTextUpdate}
                          onDeleteColumn={this.onDeleteColumn}
                          onDeleteTag={this.onDeleteTag}
                          onAddTag={this.onAddTag}
                          possibleTags={this.state.possibleTags}
                          onConfigButtonClicked={this.onConfigButtonClicked}
                          onPreviewButtonClicked={this.onPreviewButtonClicked}
                          onClosePreview={this.onClosePreview}
                          provided={provided}
                          snapshot={snapshot}
                          language={this.props.language}
                          {...columns[key]}
                          />
                        )}
                        </Draggable>
                      ))}
                      {provided.placeholder}
                     </tbody>
                  )}
                </Droppable>
              </table>
            </DragDropContext>
          </main>
        </div>

      </form>
    );
  }
}
export default withNamespaces(["crmConnectorColumns", "Common"])(CrmConnectorColumns);

I hope someone can find out why my draggable get out of position when I scroll down on the page.

Upvotes: 20

Views: 11101

Answers (4)

Gabriel Petersson
Gabriel Petersson

Reputation: 10442

I had this exact issue when my Draggables was inside a parent using transform. They addressed it here in the docs: https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/guides/reparenting.md

Basically what you need to do is render the draggable component in a portal, which they have already built in. Here is an example from my code, notice "renderClone", which renders the dragged item:

    <DragDropContext onDragEnd={handleDragEnd}>
      <Droppable
        droppableId={"droppable-1"}
        renderClone={renderDraggedCondition}
      >
        {(provider) => (
          <DroppableContainer
            ref={provider.innerRef}
            {...provider.droppableProps}
          >
            {renderedConditions}
            {provider.placeholder}
          </DroppableContainer>
        )}
      </Droppable>
    </DragDropContext>

Upvotes: 0

I have also faced the same issue.

The only work around I found is

*Fixing Height of the item in case of vertical drag. *Fixing Width of the item in case of horizontal drag.

Also one More thing to mention set the display property of the item dragged to block.

Upvotes: 0

E Min
E Min

Reputation: 45

I have run in to the same problem. The issue for me was that droppable(list) was inside, for example, main container which was scrollable (i.e. overflow: scroll).

I resolved the issue by converting the droppable into scrollable instead of main container

Example that had an issue

.main {
  background: #eee;
  padding: 3rem;
  height: 200px;
  overflow-y: scroll;
}
.droppable {
  padding: 1rem;
  background: #aaa;
}
.draggable {
  margin: 0.5rem 0;
  padding: 1rem;
  background: #ccc;
}
<div class="main">
  <div class="droppable">
    <div class="draggable">
        <span class="text"> item</span>
    </div>
      <div class="draggable">
          <span class="text"> item</span>
    </div>
      <div class="draggable">
        <span class="text"> item</span>
    </div>
  </div>
</div>

Example with issue resolved

.main {
  background: #eee;
  padding: 3rem;
  height: 200px;
}
.droppable {
  padding: 1rem;
  background: #aaa;
  height: 180px;
  overflow-y: scroll;
}
.draggable {
  margin: 0.5rem 0;
  padding: 1rem;
  background: #ccc;
}
<div class="main">
  <div class="droppable">
    <div class="draggable">
        <span class="text"> item</span>
    </div>
      <div class="draggable">
          <span class="text"> item</span>
    </div>
      <div class="draggable">
        <span class="text"> item</span>
    </div>
  </div>
</div>

Changes were made in just CSS to make droppable container shorter than the main container and added overvlow-y:scroll to the droppable

Upvotes: 2

Robert Key
Robert Key

Reputation: 274

Perhaps it's too late for the answer, but for someone, it can be helpful. If you look closer, you'll see the offset when you scroll and this is the reason for broken styles. For a solution, you should think about the scroll container, if you append scroll to HTMLElement, not Window, you need to check this example. This issue related to react-beautiful-dnd itself and the updated version will fix that.

Upvotes: 2

Related Questions