How to show Dropdown Menu when Right-Clicking on Table Row? - reactjs

How do I open a dropdown menu when a row is right-clicked in a table?
I've tried using Table's onRow onContextMenu: event => { setVisible(true); } function with dropdown outside the table <Dropdown overlay={menu} visible={visible}><Table>...</Table></Dropdown>, but the dropdown menu opens at the bottom of the table, not where the mouse is.
I've found a working demo sandbox but its with ant design 3.0.0 and doesn't work with the latest version.

First, add visible state in your component:
const [visible, setVisible] = useState(false);
Then pass the visible state into Dropdown component that use contextMenu as trigger and your Menu component as the overlay:
<Dropdown overlay={menu} visible={visible} trigger={["contextMenu"]}>
<Table ... />
</Dropdown>
Add states that will keep position and value of our clicked cell:
const [posX, setPosX] = useState(0);
const [posY, setPosY] = useState(0);
const [value, setValue] = useState("");
Next, use onRow attribute where we pass a Function that return onContextMenu and onClick Function at your Table component.
We will receive an event at onContextMenu by which we get the clicked position and use onClick to hide the menu
<Table
onRow={(_record, _rowIndex) => {
return {
onContextMenu: (event) => {
event.preventDefault();
// grab and keep the clicked position
const { clientX, clientY, target } = event;
setPosX(clientX);
setPosY(clientY);
// grab the clicked cell value
setValue(target.innerHTML);
// show the overlay Menu
setVisible(true);
},
// hide the overlay Menu on click
onClick: () => setVisible(false)
};
}}
...
/>
And finally, the overlay Menu positioning. Here we use inline style to set the position. Since Dropdown component default positioning is bottomLeft so the position will be at:
left: 0 // left side
top: n // where n is the bottom of the table which is equal to the height of your table
This is why your dropdown menu opens at the bottom of the table.
To fix this, we need to set the top value as:
// the formula
topPosition = clientY - tableHeight - menuHeight;
// in our example we get:
topPosition = clientY - 220 - 72
= clientY - 292;
So the clicked position becomes:
left: clientX,
top: clientY - 292
and at the menu we set by using inline style as follow:
const menu = (
<Menu
style={{ top: posY - 292, left: posX }}
onClick={onClick}
items={[
{
key: "1",
label: "Action 1"
},
{
key: "2",
label: "Action 2"
}
]}
/>
);
This is a minimal example:
Additional Info
If you need to access the row value from the menu, you can get the information from record props at onRow event handler and grab the value at onContextMenu event handler. So, when you do a right click at each row, the record value will be set as rowRecord state that could be used inside Menu component that is rendered inside a useMemo hook later. Since the menu now have been memoized, now the onClick method have to use useCallback hook too.
const [rowRecord, setRowRecord] = useState();
...
const onClick = useCallback(
(_item) => {
console.log(value);
setVisible(false);
},
[value]
);
...
const menu = useMemo(() => {
console.log(rowRecord);
return (
<Menu
style={{ top: posY - 292, left: posX }}
onClick={onClick}
items={[
{
key: "1",
label: rowRecord?.name
},
{
key: "2",
label: rowRecord?.email
}
]}
/>
);
}, [onClick, posX, posY, rowRecord]);
...
<Table
onRow={(record, _rowIndex) => {
return {
onContextMenu: (event) => {
event.preventDefault();
...
setRowRecord(record);
},
...
};
}}
...
/>
Actually, the rowRecord state could be used inside our component immediately after we set the record value by using setRowRecord setter. But again, it depends on your own preference.

Related

React - Resetting children state when parent changes its state in functional components

I'm working with a list of notes in React Native, and I was using a bad-performant method to select/deselect the notes when I'm "edit mode". Everytime I selected a note, the application had to re-render the entire list everytime. If I do a test with 100 notes, I get input lags when I select/deselect a note, obviously.
So I decided to move the "select state" to the single Child component. By doing this, I'm having the re-render only on that component, so it's a huge improvement of performance. Until here, everything's normal.
The problem is when I'm disabling edit mode. If I select, for example, 3 notes, and I disable the "edit mode", those notes will remain selected (indeed also the style will persist). I'd like to reset the state of all the selected note, or finding a valid alternative.
I recreated the scene using React (not React Native) on CodeSandbox with a Parent and a Child: https://codesandbox.io/s/loving-field-bh0k9k
The behavior is exactly the same. I hope you can help me out. Thanks.
tl;dr:
Use-case:
Go in Edit Mode by selecting a note for .5s
Select 2/3 elements by clicking on them
Disable Edit Mode by selecting a note for .5s
Expectation: all elements get deselected (state of children resetted)
Reality: elements don't get deselected (state of children remains the same)
this is easy enough to do with a useEffect hook.
It allows you to "watch" variable changes over time.
When editMode changes the contents of the Effect hook runs, so when editMode goes from true to false, it will set the item's selected state.
Add this to your <Child /> component:
useEffect(() => {
if (!editMode) {
setSelected(false);
}
}, [editMode]);
If you use React.memo you can cache the Child components and prevent their re-renders.
const Parent = () => {
const [editMode, setEditMode] = useState(false);
const [childrenList, setChildrenList] = useState(INITIAL_LIST);
const [selected, setSelected] = useState([]);
const toggleEditMode = useCallback(() => {
if (editMode) {
setSelected([]);
}
setEditMode(!editMode);
}, [editMode]);
const deleteSelectedChildren = () => {
setChildrenList(childrenList.filter((x) => !selected.includes(x.id)));
setEditMode(false);
};
const onSelect = useCallback((id) => {
setSelected((prev) => {
if (prev.includes(id)) {
return prev.filter((x) => x !== id);
}
return [...prev, id];
});
}, []);
// Check when <Parent /> is re-rendered
console.log("Parent");
return (
<>
<h1>Long press an element to enable "Edit Mode"</h1>
<ul className="childrenWrapper">
{childrenList.map((content, index) => (
<Child
key={content.id}
index={index}
content={content}
editMode={editMode}
toggleEditMode={toggleEditMode}
onSelect={onSelect}
selected={selected.includes(content.id)}
/>
))}
</ul>
{editMode && (
<button onClick={deleteSelectedChildren}>DELETE SELECTED</button>
)}
</>
);
};
You have to wrap the functions you pass as props inside useCallback, otherwise they will be different on every Parent render, invalidating the memoization.
import { useRef, memo } from "react";
const Child = memo(
({ content, editMode, toggleEditMode, onSelect, selected }) => {
// Prevent re-rendering when setting timer thread
const timerRef = useRef();
// Toggle selection of the <Child /> and update selectedChildrenIndexes
const toggleCheckbox = () => {
if (!editMode) return;
onSelect(content.id);
};
// Toggle Edit mode after .5s of holding press on a Child component
const longPressStartHandler = () => {
timerRef.current = setTimeout(toggleEditMode, 500);
};
// Release setTimeout thread in case it's pressed less than .5s
const longPressReleaseHandler = () => {
clearTimeout(timerRef.current);
};
// Check when <Child /> is re-rendered
console.log("Child - " + content.id);
return (
<li
className={`childContent ${editMode && "editMode"} ${
selected && "selected"
}`}
onMouseDown={longPressStartHandler}
onMouseUp={longPressReleaseHandler}
onClick={toggleCheckbox}
>
<pre>
<code>{JSON.stringify(content)}</code>
</pre>
{editMode && (
<input type="checkbox" onChange={toggleCheckbox} checked={selected} />
)}
</li>
);
}
);
You can see a working example here.

How to uncheck all state selected rows key using onclick function Table - Ant Design

I have function button where this button need to clear all selected rows key, what happen right now, the state is clear already but still checked on the table. I don't know if my function is right or is there need to change inside my function or use the useEffect. I am using Ant Design for the table and React Js for the front end side.
Here is my Button:
<Button type="primary" onClick={unCheckHandler} className="btn-pink">Clear</Button>
Here is my handler function:
const [selectedRowsKeys, setSelectedRowsKeys] = useState([]);
const [selectedRows, setSelectedRows] = useState([]);
const onSelectedRowKeysChange = (selectedRows, selectedRowsKeys) =>{
setSelectedRows(selectedRows)
setSelectedRowsKeys(selectedRowsKeys);
}
const rowSelection = {
selectedRows,
selectedRowsKeys,
onChange: onSelectedRowKeysChange
};
const unCheckHandler = () => {
setSelectedRowsKeys([])
setSelectedRows([])
}
useEffect(() => {
},[unCheckHandler])
console.log(selectedRowsKeys,"---", selectedRows)
Table:
<Table
dataSource={props.dataSource}
columns={props.columns}
size="small"
rowSelection={{
type: "checkbox",
...rowSelection,
}}
pagination={false}
/>
Sample Log:
You need to set the rowSelection.selectedRowKeys property as per the Ant Design documentation. You're only setting selectedRows at the moment which doesn't seem to work.
The example here demonstrates how it works.
const rowSelection = {
selectedRowKeys,
onChange: onSelectedRowKeysChange
};

Scroll down to a specific View in React Native ScrollView

I want to be able to scroll a after pressing button so that it visible on screen. How can I tell a react-native ScrollView move to a certain?
Hello you can use the property scrollTo like below
import {useRef} from "react"
import {
ScrollView,
Button,
} from 'react-native';
const YouComp = () => {
const refScrollView = useRef(null);
const moveTo = () => {
refScrollView.current.scrollTo({x: value1, y: value2});
// or just refScrollView.current.scrollTo({x: value1}); if you want to scroll horizontally
// or just refScrollView.current.scrollTo({y: value2}); if you want to scroll vertically
}
return (<>
<Button onPress={moveTo} title="title" />
<ScrollView ref={refScrollView}>
...
</ScrollView>
</>);
}
You can set whether x or y value or both
Check the full doc here
First you need to create reference to element
this.scrollViewRefName = React.createRef()
Then pass it to ref attribute
<ScrollView ref={this.scrollViewRefName}>
Then you trigger the function from your button with scrollToTheEnd or wherever you want to scroll within the element
<View style={styles.ButtonContainer}>
<Button onPress={() => { this.scrollViewRef.current.scrollToTheEnd }} />
</View>
Note that you may need extra callback function in onPress depending on from which context you have the components
using ref and scrollTo is just bullshit and dose not always work.
Here is how i did it.
const [scrollYPosition, setScrollYPosition] = useState(0);
const [data, setData] = useState([]);
const goToItem = () => {
// lets go to item 200
// the 200 is the item position and the 150 is the item height.
setScrollYPosition(200 * 150);
}
<ScrollView contentOffset = {
{
y: scrollYPosition,
x: 0
}
}>
// papulate your data and lets say that each item has 150 in height
</ScrollView>

How to re-animate react-spring animation using hooks on button click?

Following simple component from the official examples:
import {useSpring, animated} from 'react-spring'
function App() {
const props = useSpring({opacity: 1, from: {opacity: 0}})
return <animated.div style={props}>I will fade in</animated.div>
}
Question
How do I animate the fadeIn-effect (or any other animation) again for example when I click on a button or when a promise is resolved?
You can basically make two effect with useSpring and an event.
You can change the style for example the opacity depending on the state of an event.
You can restart an animation on state change. The easiest way to restart is to rerender it.
I created an example. I think you want the second case. In my example I rerender the second component with changing its key property.
const Text1 = ({ on }) => {
const props = useSpring({ opacity: on ? 1 : 0, from: { opacity: 0 } });
return <animated.div style={props}>I will fade on and off</animated.div>;
};
const Text2 = () => {
const props = useSpring({ opacity: 1, from: { opacity: 0 } });
return <animated.div style={props}>I will restart animation</animated.div>;
};
function App() {
const [on, set] = React.useState(true);
return (
<div className="App">
<Text1 on={on} />
<Text2 key={on} />
<button onClick={() => set(!on)}>{on ? "On" : "Off"}</button>
</div>
);
}
Here is the working example: https://codesandbox.io/s/upbeat-kilby-ez7jy
I hope this is what you meant.

React.useEffect stack execution prevents parent from setting defaults

I have attached a simplified example that demonstrates my issue:
https://codesandbox.io/s/reactusehook-stack-issue-piq15
I have a parent component that receives a configuration, of which screen should be rendered. the rendered screen should have control over the parent appearance. In the example above I demonstrated it with colors. But the actual use case is flow screen that has next and back buttons which can be controlled by the child.
in the example I define common props for the screens:
type GenericScreenProps = {
setColor: (color: string) => void;
};
I create the first screen, that does not care about the color (parent should default)
const ScreenA = (props: GenericScreenProps) => {
return <div>screen A</div>;
};
I create a second screen that explicitly defines a color when mounted
const ScreenB = ({ setColor }: GenericScreenProps) => {
React.useEffect(() => {
console.log("child");
setColor("green");
}, [setColor]);
return <div>screen B</div>;
};
I create a map to be able to reference the components by an index
const map: Record<string, React.JSXElementConstructor<GenericScreenProps>> = {
0: ScreenA,
1: ScreenB
};
and finally I create the parent, that has a button that swaps the component and sets the default whenever the component changes
const App = () => {
const [screenId, setScreenId] = useState(0);
const ComponentToRender = map[screenId];
const [color, setColor] = useState("red");
React.useEffect(() => {
console.log("parent");
setColor("red"); // default when not set should be red
}, [screenId]);
const onButtonClick = () => setScreenId((screenId + 1) % Object.keys(map).length)
return (
<div>
<button style={{ color }} onClick={onButtonClick}>
Button
</button>
<ComponentToRender setColor={setColor} />
</div>
);
};
In this example, the default color should be red, for screen A. and green for the second screen.
However, the color stays red because useEffect is using a stack to execute the code. if you run the code you will see that once the button clicked there will be child followed by parent in log.
I have considered the following solution, but they are not ideal:
each child has to explicitly define the color, no way to enforce it without custom lint rules
convert the parent into a react class component, there has to be a hooks solution
This might be an anti-pattern where child component controls how its parent behave, by I could not identify a way of doing that without replicating the parent container for each screen. the reason I want to keep a single parent is to enable transition between the screens.
If I understood the problem correctly, there is no need to pass down setColor to the children. Making expressions more explicit might make a bit longer code, but I think it helps in readability. As what you shared is a simplified example, please let me know if it fits your real case:
const ScreenA = () => {
return <div>screen A</div>;
};
const ScreenB = () => {
return <div>screen B</div>;
};
const App = () => {
const [screen, setScreen] = useState<"a" | "b">("a");
const [color, setColor] = useState<"red" | "green">("red");
const onButtonClick = () => {
if (screen === "a") {
setScreen("b");
setColor("green");
} else {
setScreen("a");
setColor("red");
}
};
return (
<div>
<button style={{ color }} onClick={onButtonClick}>
Button
</button>
{screen === "a" ? <ScreenA /> : <ScreenB />}
</div>
);
};
render(<App />, document.getElementById("root"));

Resources