DnD-kit activating dragging when adding element to list - javascript

I have list with draggable elements.
When I'm adding elements to list Dragging is turned on for 1 sec. Looks like on the first render o element it turns on. It is important because when isDragging is true hover of element is turn on. So it is like when you add element to list there is hover for 1 sec and it disappears.
I've tried to disable attributes, listeners, setNodeRef, transform, transition, isDragging, sensors, modify sensors, but none of that worked.
Did someone had same problem?
List:
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
modifiers={[restrictToVerticalAxis, restrictToParentElement]}
>
<SortableContext items={sortableUniqueIdentifier as string[]} strategy={verticalListSortingStrategy}>
{items?.map({id}) => (
<SortableItem
key={id}
id={id}
/>
),
)}
</SortableContext>
</DndContext>
);
elements:
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
const style = {
transform: CSS.Translate.toString(transform),
transition,
touchAction: 'none',
};
return (
<div key={id} ref={setNodeRef as React.ForwardedRef<HTMLDivElement>} style={style}>
<p className="text">SOME TEXT </p>
<Button className="draggingButton" {...attributes} {...listeners}>
<Image src="/dnd-button.svg" alt="dnd image" width={24} height={24} />
</Button>
</div>
);

Related

Framer Motion StaggerChildren Animation on Child Removal do not Trigger Animation

I have a list (ParentBox.tsx) that contains many items (Box.tsx). When clicking the Add button, the ParentBox has one additional unique Box. The animation works fine. However, there are two scenarios where it does not:
When I click on the Box, it removes the item from the list. Framer Motion removes the Box from the user interface without exit animation.
When clicking "Remove All", the whole list of items is removed. There is no exit stagger effect.
I want to have an individual element of the list animated out, and when the whole list is cleared, have them one by one animated out.
Full Repro in CodeSanbox
Parent Box
const variantsBoxContainer: Variants = {
hidden: {
transition: {
staggerChildren: 0.1,
delayChildren: 0.3,
staggerDirection: -1
}
},
show: {
transition: {
staggerChildren: 0.1,
delayChildren: 0.3,
staggerDirection: 1
}
}
};
let id = 3;
export const ParentBox = (props: ParentBoxProps) => {
const [items, setItems] = useState<Item[]>([
{ id: 1, text: "Test #1" },
{ id: 2, text: "Test #2" }
]);
return (
<motion.div
className="parentbox"
>
<button
onClick={() => {
id++;
setItems([...items, { id: id, text: `Click to delete id ${id}` }]);
}}
>
Add
</button>
<button
onClick={() => {
id++;
setItems([]);
}}
>
Remove All
</button>
<motion.ol
variants={variantsBoxContainer}
initial="hidden"
animate="show"
exit="hidden"
>
<AnimatePresence mode="popLayout">
{items
.sort((a, b) => a.id - b.id)
.map((d) => (
<Box
key={d.id}
data={d}
onRemove={(item) => {
const newList = items.filter((i) => i.id !== item.id);
console.log(newList);
setItems(newList);
}}
/>
))}
</AnimatePresence>
</motion.ol>
</motion.div>
);
};
Box
const variantBox: Variants = {
hidden: { opacity: 0, top: -100, transition: { duration: 2 } },
show: { opacity: 1, top: 0, transition: { duration: 2 } }
};
export const Box = (props: BoxProps) => {
return (
<motion.li
className="box"
variants={variantBox}
onClick={() => {
props.onRemove(props.data);
}}
>
{props.data.text}
</motion.li>
);
};
What I have tried so far:
Adding/Removing the explicit mention of initial, animate, exit on the Box component.
Adding/Removing the when option.
Tried all mode in the AnimatedPresence
Try to add a function for the hidden (exit) variant to have a custom delay per index
Ensure all Box all have unique key
Let me know if you have any idea what I am missing to have the animation on Box removal (children).
CodeSanbox
Exit animations will work if you explicitly indicate which variant to use for the animation states:
export const Box = (props: BoxProps) => {
return (
<motion.li
custom={props.index}
className="box"
variants={variantBox}
exit="hidden"
initial="hidden"
animate="show"
onClick={() => {
props.onRemove(props.data);
}}
>
{props.data.text}
</motion.li>
);
};
I believe AnimatePresence is conflicting with the staggerChildren prop since it appears between the parent and children. See this issue on GitHub.
Quickest workaround is probably to use dynamic variants and manually set a delay in the variants for the Box component (based on the index in the items array.

How to set HTML attributes with variables?

In my HTML I have a lot of repeated elements with quite a few attributes each and most of them are the same. It's taking up a lot of space and I could clean up the code if I could just set the attributes as a variable and use that variable in all my elements. So is there a way to do this?
For example one of my elements look like this,
<button className='btn btn-secondary dropdown-toggle' style={{ margin: "5px" }} type='button' data-bs-toggle='dropdown' aria-expanded='false'>
Delete
</button>
And there are at least 10 other buttons with the exact same set of attributes and takes up almost 100 lines worth of space. Is there a way I can set the attributes in one place then apply it to every element to save space?
If you are reusing a collection of html attributes, here are two ways you could handle this:
Save the attributes into an object and spread the object onto each <button> that uses them.
Create a <CustomButton> component and encapsulate the shared attributes in the component. Because each attribute is exposed as a prop in the component interface, each prop/attribute has a default value but can be overwritten if necessary.
// #1
const defaultButtonProps = {
className: "btn btn-secondary dropdown-toggle",
style: { margin: "5px" },
type: "button",
"data-bs-toggle": "dropdown",
"aria-expanded": "false"
};
export default function App() {
return (
<div className="App">
<div>
{/* #1 */}
<button {...defaultButtonProps}>Delete</button>
<button {...defaultButtonProps}>Save</button>
</div>
<div>
{/* #2 */}
<CustomButton />
<CustomButton>Save</CustomButton>
</div>
</div>
);
}
// #2
const CustomButton = ({
className = "btn btn-secondary dropdown-toggle",
style = { margin: "5px" },
type = "button",
dataBsToggle = "dropdown",
ariaExpanded = "false",
children = "Delete"
}) => {
return (
<button
className={className}
style={style}
type={type}
data-bs-toggle={dataBsToggle}
aria-expanded={ariaExpanded}
>
{children}
</button>
);
};

Expand/Collapse all data

I am making a Accordion and when we click each individual item then its opening or closing well.
Now I have implemented expand all or collapse all option to that to make all the accordions expand/collapse.
Accordion.js
const accordionArray = [
{ heading: "Heading 1", text: "Text for Heading 1" },
{ heading: "Heading 2", text: "Text for Heading 2" },
{ heading: "Heading 3", text: "Text for Heading 3" }
];
.
.
.
{accordionArray.map((item, index) => (
<div key={index}>
<Accordion>
<Heading>
<div className="heading-box">
<h1 className="heading">{item.heading}</h1>
</div>
</Heading>
<Text expandAll={expandAll}>
<p className="text">{item.text}</p>
</Text>
</Accordion>
</div>
))}
And text.js is a file where I am making the action to open any particular content of the accordion and the code as follows,
import React from "react";
class Text extends React.Component {
render() {
return (
<div style={{ ...this.props.style }}>
{this.props.expandAll ? (
<div className={`content open`}>
{this.props.render && this.props.render(this.props.text)}
</div>
) : (
<div className={`content ${this.props.text ? "open" : ""}`}>
{this.props.text ? this.props.children : ""}
{this.props.text
? this.props.render && this.props.render(this.props.text)
: ""}
</div>
)}
</div>
);
}
}
export default Text;
Here via this.props.expandAll I am getting the value whether the expandAll is true or false. If it is true then all accordion will get the class className={`content open`} so all will gets opened.
Problem:
The open class is applied but the inside text content is not rendered.
So this line doesn't work,
{this.props.render && this.props.render(this.props.text)}
Requirement:
If expand all/collapse all button is clicked then all the accordions should gets opened/closed respectively.
This should work irrespective of previously opened/closed accordion.. So if Expand all then it should open all the accordion or else needs to close all accordion even though it was opened/closed previously.
Links:
This is the link of the file https://codesandbox.io/s/react-accordion-forked-sm5fw?file=/src/GetAccordion.js where the props are actually gets passed down.
Edit:
If I use {this.props.children} then every accordion gets opened.. No issues.
But if I open any accordion manually on click over particular item then If i click expand all then its expanded(expected) but If I click back Collapse all option then not all the accordions are closed.. The ones which we opened previously are still in open state.. But expected behavior here is that everything should gets closed.
In your file text.js
at line number 9. please replace the previous code by:
{this.props.children}
Tried in the sandbox and worked for me.
///
cant add a comment so editing the answer itself.
Accordian.js contains your hook expandAll and the heading boolean is already happening GetAccordian.js.
I suggest moving the expand all to GetAccordian.js so that you can control both values.
in this case this.props.render is not a function and this.props.text is undefined, try replacing this line
<div className={`content open`}>
{this.props.render && this.props.render(this.props.text)}
</div>
by this:
<div className={`content open`}>
{this.props.children}
</div>
EDIT: //
Other solution is to pass the expandAll property to the Accordion component
<Accordion expandAll={expandAll}>
<Heading>
<div className="heading-box">
<h1 className="heading">{item.heading}</h1>
</div>
</Heading>
<Text>
<p className="text">{item.text}</p>
</Text>
</Accordion>
then in getAccordion.js
onShow = (i) => {
this.setState({
active: this.props.expandAll ? -1: i,
reserve: this.props.expandAll ? -1: i
});
if (this.state.reserve === i) {
this.setState({
active: -1,
reserve: -1
});
}
};
render() {
const children = React.Children.map(this.props.children, (child, i) => {
return React.cloneElement(child, {
heading: this.props.expandAll || this.state.active === i,
text: this.props.expandAll || this.state.active + stage === i,
onShow: () => this.onShow(i)
});
});
return <div className="accordion">{children}</div>;
}
};
Building off of #lissettdm answer, it's not clear to me why getAccordion and accordion are two separate entities. You might have a very valid reason for the separation, but the fact that the two components' states are interdependent hints that they might be better implemented as one component.
Accordion now controls the state of it's children directly, as before, but without using getAccordion. Toggling expandAll now resets the states of the individual items as well.
const NormalAccordion = () => {
const accordionArray = [ //... your data ];
const [state, setState] = useState({
expandAll: false,
...accordionArray.map(item => false),
});
const handleExpandAll = () => {
setState((prevState) => ({
expandAll: !prevState.expandAll,
...accordionArray.map(item => !prevState.expandAll),
}));
};
const handleTextExpand = (id) => {
setState((prevState) => ({
...prevState,
[id]: !prevState[id]
}));
};
return (
<>
<div className="w-full text-right">
<button onClick={handleExpandAll}>
{state.expandAll ? `Collapse All` : `Expand All`}
</button>
</div>
<br />
{accordionArray.map((item, index) => (
<div key={index}>
<div className="accordion">
<Heading handleTextExpand={handleTextExpand} id={index}>
<div className="heading-box">
<h1 className="heading">{item.heading}</h1>
</div>
</Heading>
<Text shouldExpand={state[index]}>
<p className="text">{item.text}</p>
</Text>
</div>
</div>
))}
</>
);
};
Heading passes back the index so the parent component knows which item to turn off.
class Heading extends React.Component {
handleExpand = () => {
this.props.handleTextExpand(this.props.id);
};
render() {
return (
<div
style={ //... your styles}
onClick={this.handleExpand}
>
{this.props.children}
</div>
);
}
}
Text only cares about one prop to determine if it should display the expand content.
class Text extends React.Component {
render() {
return (
<div style={{ ...this.props.style }}>
<div
className={`content ${this.props.shouldExpand ? "open" : ""}`}
>
{this.props.shouldExpand ? this.props.children : ""}
</div>
</div>
);
}
}

How do I make a map that when changing the state does not modify all the elements - React

I am using a json with several values, one of them is "iframe" which can be "si" (yes) or "no" depending on whether it is an iframe or not.
With that value (yes / no) I need (this.props.tabsiframe === 'yes') to show an <iframe> or a <div>.
My code works, because if (this.props.tabsiframe === 'yes') is an iframe it paints an iframe, but if the next element is (this.props.tabsiframe === 'no' ) change all the elements of <iframe> to <div>.
The re-rendering changes the already created divs and converts them to iframe if iframe = yes and if it is = it does not change the iframe to div. This is an example of what happens
https://drive.google.com/file/d/1cY9eHq_aBuQb8b_HtYUXjC6lqrYCJe3p/view
Maybe it happens because every time I click on the element the menu is updated: divIframe: {tabsDivIframe: [... new Set (this.state.divIframe.tabsDivIframe), url] .filter (function (el) { return the;})},. A new url is added to the array, so I guess that's what it renders and it changes the divs to iframes or iframes to divs.
class App extends Component {
constructor(props, context){
super(props, context);
["openTabs", "removeTab"].forEach((method) => {
this[method] = this[method].bind(this);
});
this.state = {
tabs:{
tabsLi: [],
},
divIframe:{
tabsDivIframe: [],
},
tabsiframe : '',
showtabs: true,
}
}
openTabs(e, url, iframe, trdtitle){
e.preventDefault();
this.setState({
showtabs: false,
})
if (this.state.tabs.tabsLi.includes(trdtitle) === false){
this.setState({
tabs: { tabsLi:[...new Set(this.state.tabs.tabsLi),trdtitle].filter(function(el) { return el; })},
divIframe: { tabsDivIframe:[...new Set(this.state.divIframe.tabsDivIframe),url].filter(function(el) { return el; })},
tabsiframe: iframe,
}, () => {
console.log(this.state.tabs.tabsLi);console.log(this.state.divIframe.tabsDivIframe);console.log(this.state.tabsiframe)
})
}
}
render(){
return (
<><Tabs
showtabs={this.state.showtabs}
tabs={this.state.tabs}
tabsLi={this.state.tabs.tabsLi}
divIframe={this.state.divIframe}
tabsDivIframe={this.state.divIframe.tabsDivIframe}
tabsiframe={this.state.tabsiframe}
openTabs={this.openTabs}
removeTab={this.removeTab}
/>
</>
)
}
}
class Tabs extends Component {
render(){
return(
<div id="content-tabs" className="tabs">
{( this.props.showtabs)
? (
<>
<div className="waiting-leads">
<p>Parece que todavía no hay ningún lead...</p>
<h3>¡Ánimo, ya llega!</h3>
<img src={imgDinosaurio} alt="Dinosaurio"></img>
</div>
</>
) : (
<>
<DivAndIframe
tabsDivIframe={this.props.divIframe.tabsDivIframe}
tabsiframe={this.props.tabsiframe}
/>
</>
)}
</div>
);
}
}
class DivAndIframe extends Component{
render(){
return(
<>
{this.props.tabsDivIframe.map((url, index) =>
<div key={url.toString() id={"myTab" + index}>
{( this.props.tabsiframe === 'si')
? (
<iframe title={"iframe"+index} className="iframeTab" src={url}></iframe>
) : (
<div>{url}</div>
)}
</div>
)}
</>
);
}
}
The problem you are having here is within your map.
Here is a state map to try to explain:
After First tab Click (div):
this.props.tabsiframe = 'no'
this.props.tabsDivIframe = ['www.sampleurl1.com']
After Second tab Click (iframe):
this.props.tabsiframe = 'yes'
this.props.tabsDivIframe = ['www.iframe2.com', 'www.sampleurl1.com']
so on the virtual DOM after the second click, the map will again iterate through this.props.tabDivIframe and evaluate this.props.tabsiframe as 'si' or 'no' for everything.
{this.props.tabsDivIframe.map((url, index) =>
<div key={url.toString() id={"myTab" + index}>
{( this.props.tabsiframe === 'si') //this line here; we share this same value for all tabs
? (
<iframe title={"iframe"+index} className="iframeTab" src={url}></iframe>
) : (
<div>{url}</div>
)}
</div>
)}
I wrote a small interactive sample of your problem here: https://jsfiddle.net/eojrx6my/6/
You will need to store individual 'tabsiframe' for each url, or somehow identify them individually

Display different images onMouseOver in React

I'm looking to display a different image for every link I hover over. When I hover over each link, both images display on top of each other. I feel as if my issue stems from the conditional, which will show any image I place within it and not just one specific image.
I'm wondering if there's a better approach. Perhaps holding the images within the state?
My code:
class PhotoIndex extends Component {
state = {
hover: false
}
mouseOver = () => {
this.setState({ hover: true })
}
mouseOut = () => {
this.setState({ hover: false })
}
render() {
return (
<Wrapper>
<IndexWrapper>
<li>
<StyledLink
onMouseOver={this.mouseOver}
onMouseOut={this.mouseOut}
to="/checkered-flag/">Checkered Flag
{this.state.hover
?
<Fade >
<div style={{ position: 'relative' }}>
<img
style={{ position: 'absolute', top: '-200px', left: '100%' }}
src={car14}
alt="red car parked in a parkin lot"
/>
</div>
</Fade>
: null}
</StyledLink>
</li>
<li>
<StyledLink
onMouseOver={this.mouseOver}
onMouseOut={this.mouseOut}>
Birds Nest
{this.state.hover
?
<Fade >
<div style={{ position: 'relative' }}>
<img
style={{ position: 'absolute', top: '-200px', left: '100%' }}
src={car15}
alt="blue car parked in a grassy field"
/>
</div>
</Fade>
: null}
</StyledLink>
</li>
<li>
<StyledLink>The Grand Turret</StyledLink>
</li>
<li>
<StyledLink>Simulation Theory</StyledLink>
</li>
</IndexWrapper>
</Wrapper>
)
}
}
I will try to simplify the code to explain the solution.
If you wish to go with this solution the images should be numbered in order with a set structure. For example car0.jpg ,car1.jpg, car2.jpg .....
ImageGetter.js
import car1 from './cars/car1.jpg';
import car2 from './cars/car2.jpg';
export default {
car1,car2
}
In the above code I am importing the images and exporting them so they can be consumed by any component which uses the ImageGetter object.
PhotoIndex.js
import ImageGetter from './ImageGetter';
class PhotoIndex extends Component {
constructor() {
super();
this.state = {
carImgNum: '0',
hover: false
}
}
mouseOver = () => {
const max = 5; //Max number of images
const newcarImgNum = Math.floor(Math.random() * Math.floor(max));
this.setState({ hover: true, carImgNum: newcarImgNum });
}
render() {
const { carImgNum } = this.state;
return (
<div onMouseOver={this.mouseOver}>
<img src={ImageGetter[`car${carImgNum}`]} alt="" />
</div>
)
}
}
export default PhotoIndex;
You will have to create a default state for the number of the image which will be displayed. Over here the default image displayed will be car0.jpg.
In mouseOver function you will have to define how many images are available. (You can make the number of images dynamic with some other function too.).
It then creates a random number from 0 to the max number you specified and sets the value to the carImgNum state.
Over in the render method I am de structuring the state to get the carImgNum value.
I then pass the ImageGetter into src of the image tag and dynamically target the image i need to pass using templatestrings.

Categories

Resources