I have built a custom tree view in React, and each item contains a dropdown which is positioned using Popper. Since the child elements are not visible on render, Popper is not positioning the dropdown correctly, for example:
When the tree is open on mount (i.e the children are visible), the positioning is correct:
Each level in the tree is rendered via a CategoryNavItem component, which essentially looks like this:
<div className={ className.join(' ') }>
<div className={ `collection-nav_item-link depth${depth}` } style={{ paddingLeft: `${paddingLeft}px`}}>
<Link to={ linkTo } onClick={() => { setIsOpen(!isOpen) }}>
<i className="collection-nav_item-link_icon"></i>
<span className="collection-nav_item-link_text">{ category.name }</span>
</Link>
<Dropdown
toggleClassName="btn-icon-white btn-sm"
toggleContent={ <Icon name="ellipsis-h" />}
position="bottom-end"
size="small"
items={[
{ text: 'Edit category' },
{ text: 'Add subcategory', onClick: (() => { dispatch(openAddSubcategory(category)) }) }
]} />
</div>
{ children }
</div>
The Dropdown component is where we use Popper, and it works well everywhere else. The visibility of a CategoryNavItem is handled via the component's state in React.
Is there any way to trigger Popper's update() method programmatically in React? We should force update when toggling the item's visibility.
It turns out we just need to expose the update property from the usePopper hook, and then call it when setting the dropdown's visibility, for example:
const { styles, attributes, update } = usePopper(referenceElement, popperElement, {
placement: placement,
modifiers: [
{ name: 'arrow', options: { element: arrowElement } },
{ name: 'offset', options: { offset: [ 0, 3 ] } }
]
});
And similarly:
const toggleDropdown = (e) => {
e.preventDefault();
e.stopPropagation();
setVisible(!visible);
update();
};
According to the docs, you can manually update the propper instance so that it recomputes the tooltip position:
Manual update
You can ask Popper to recompute your tooltip's position by running instance.update().
This method will return a promise, that will be resolved with the updated State, from where you will optionally be able to read the updated positions.
const state = await popperInstance.update();
When clicking on your item visibility toggle, you could add your popper manual update, like the line of code above.
Here is the reference.
Related
I have 6 buttons as my action buttons so I decided to use an array with object in them and then mapping over them like this :
export const ACTION_BUTTONS = [
{
btnColor: "error",
onclick,
Icon: <Cancel fontSize="large" />,
title:"Cancel Operation",
className: eachAction,
},
......
]
but my buttons have onClick property too. for example :
onClick={() => setCancelRedirect(true)}
to use setCancelRedirect I need to define my state inside of my ACTION_BUTTONS.js file but it is not possible because it is not a custom hook or react function as they mentioned in https://reactjs.org/docs/hooks-rules.html
how can I solve this problem?
I tried to add onclick empty as you can see and then defining it during mapping but this is not possible.
{ACTION_BUTTONS.map((Btn) => (
<span className={Btn.className}>
<IconButton color={Btn.btnColor}>{Btn.Icon}</IconButton>
<p>{Btn.title}</p>
</span>
))}
a certain onClick will apply on all elements and I don't want this.
I have a requirement to add tooltip on hover to disabled options in a dropdown in React fluent UI.
I am able to add tooltip to singular component using https://www.npmjs.com/package/#fluentui/react-tooltip
<Tooltipcontent="Example tooltip">
<Button/>
</Tooltip>
but how to add similar behaviour to dropdown options and only for disabled options
like: "Disabled cause of non avilability"
sample dropdown fluent ui code
const options: IDropdownOption[] = [
{ key: 'fruitsHeader', text: 'Fruits', itemType: DropdownMenuItemType.Header },
{ key: 'apple', text: 'Apple' },
{ key: 'banana', text: 'Banana' },
{ key: 'orange', text: 'Orange', disabled: true },
];
export const DropdownBasicExample: React.FunctionComponent = () => {
return (
<Stack tokens={stackTokens}>
<Dropdown
placeholder="Select an option"
label="Basic uncontrolled example"
options={options}
styles={dropdownStyles}
/>
</Stack>
);
};
Thanks
Fluent UI renders every disabled option as a button element with the disabled attribute, which makes it non-interactive by default.
Here's a method to solve this that I believe is also fairly accessible:
First, define your array of IDropdownOption items so that they conditionally set the disabled and title properties:
const options: IDropdownOption[] = [
{ key: 'apple', text: 'Apple' },
{ key: 'orange',
text: 'Orange',
disabled: isOrangeDisabled,
title: isOrangeDisabled ? "This is a disabled tooltip" : ""
},
];
You're also going to need to define a custom rendering function for the items in order to apply a nice tooltip:
<Dropdown onRenderOption={onRenderOption} />
and then define a function like this in your component:
const onRenderOption = (option: IDropdownOption): JSX.Element => {
return (
<>
{option?.disabled && (
<div className="interactive">
<TooltipHost content={option.title}>
<span>{option.text}</span>
</TooltipHost>
</div>
)}
{!option?.disabled && (
<span>{option.text}</span>
)}
</>
);
};
Finally, that interactive CSS class needs to be defined in your CSS file. This will override the browser's default behaviour of making disabled elements non-interactive:
.interactive {
pointer-events: auto;
}
Some things to note:
The reason the title is set to an empty string when the option is not disabled is so that it doesn't have a string value when the interactive item is rendered. Without this, the browser will render the tooltip when you hover on a selectable item, which looks ugly.
Using the title attribute should make the component pretty usable for screen readers and other assistive technology (though I am far from an expert)
The template only renders the TooltipHost and interactive class when the object is disabled, so that the tooltip and that behaviour only kick in when the option is disabled. Because the underlying option is still disabled, you still won't be able to select it.
Goal: Create a new product component through onClick of a button where it would display a modal containing textfields to be filled up with the new product's name and price. We then pass the value of those textfields to the data.jsx (data source for rendering the product components) after clicking the Add button inside the modal in order to create a new instance of data info of the new product.
Problem: I am having trouble on passing the values of the textfields from the modal going to data.jsx since I literally have no clue how to properly pass those values into a plain database file. I think props, hooks, etc. might not work on passing the values.
Modal appearance (after clicking Add New Product button):
Modal appearance (with textfield values):
Modal source code (ModalAddProduct.jsx):
import React from "react";
import { Typography } from "#mui/material";
import {
BrowserRouter as Router,
Routes,
Route,
useNavigate
} from "react-router-dom";
import { TextField } from "#mui/material";
import { useState } from "react";
import "./ModalStyleAddProduct.css";
function ModalAddProduct({ setOpenModalAddProduct }) {
const navigate = useNavigate();
const [itemName, setItemName] = useState("");
const [itemPrice, setItemPrice] = useState("");
const navigateToDraftsPage = () => {
navigate("/draftspage");
};
return (
<div className="modalBackground3">
<div className="modalContainer3">
<div className="titleCloseBtn3">
<button
onClick={() => {
setOpenModalAddProduct(false);
}}
>
<Typography sx={{ color: "white" }}>X</Typography>
</button>
</div>
<div className="title">
<h1 className="modalTitleUp">Add New Item Product</h1>
</div>
<div className="body">
<TextField
onChange={(g1) => setItemName(g1.target.value)}
className="inputRounded"
placeholder="Item Name"
variant="outlined"
size="small"
/>
<TextField
onChange={(g2) => setItemPrice(g2.target.value)}
className="inputRounded"
placeholder="Item Price"
variant="outlined"
size="small"
sx={{ width: 100 }}
/>
</div>
<div className="footer">
<button
onClick={() => {
setOpenModalAddProduct(false);
}}
id="noBtn3"
>
Cancel
</button>
<button>Add</button>
</div>
</div>
</div>
);
}
export default ModalAddProduct;
Database source code (data.jsx):
const data = {
products: [
{
id: "1",
name: "MacBook",
price: 1400,
image: "https://picsum.photos/id/180/2400/1600"
},
{
id: "2",
name: "Old Car",
price: 2400,
image: "https://picsum.photos/id/111/4400/2656"
},
{
id: "3",
name: "W Shoes",
price: 1000,
image: "https://picsum.photos/id/21/3008/2008"
}
]
};
export default data;
Full functioning app in CodeSandbox:
https://codesandbox.io/s/addnewproductitem-button-6mtb1o?file=/src/components/modals/ModalAddProduct.jsx
App inspired and based from YT: https://www.youtube.com/watch?v=AmIdY1Eb8tY
It would indeed help a lot to gather tips and guides on how to handle data passing when it comes to plain database files which only contains a const array of objects.
Thank you very much!
All right... I think the best would be to use useContext or Redux, but you can also pass the props back to the parent (Main.js) from ModalAddProduct.
Where you have your "Add" button:
<button onClick={sendDataToParent}>Add</button>
now add sendDataToParent function in this file.
const sendDataToParent = () => {
props.sendData(itemName, itemPrice);
};
We are passing all the information we want back to the parent. We could also pass it as an object.
Now back in our main.js file:
const [updatedProductsList, addProd] = useState(products);
And we will use updatedProductsList in our map function at the bottom. On the first render, it will display your 3 products you have.
And last but not least... Remember the sendData we send (props.sendData)? Let's update our updatedProductsList that we declared using the useState hook.
const sendData = (name, price) => {
addProd([...updatedProductList, { name, price }]);
};
Let's "listen" for sendData from the child. As soon as it is triggered the sendData in the parent will be triggered.
<ModalAddProduct
sendData={sendData}
setOpenModalAddProduct={setModalOpenAddProduct}
/>
Now you need to add an image field for consistency. Ideally, as mentioned before redux could be a good tool to use so you could pass your dummy data there and update them as you go along.
Please note if you try to add the new item to the cart it will show an error. It is because the price on your form is a string, not a number. BTW, there is a concept called props drilling in react, hence why redux or useContext are preferable. I hope this is all clear :)
https://codesandbox.io/s/addnewproductitem-button-forked-8qdx9f?file=/src/components/modals/ModalAddProduct.jsx
const allTodos = [{id: 1, name: 'whatever user type'}, { }, { }, .... { }] // Defined as an array using setState in other file. Imported in this file as allTodos using props.
export const Todos = (props) => {
props.allTodos.map((prev) => {
return (
<div id="item_container">
<button type='button' className = 'check_button'
onClick = {() => setActiveTodos(prev.id)}>
<img src = {check} alt="" id = "check_img"></img>
</button>
<li id="li_item">{prev.name}</li>
</div>
)}
}
Now, the question is I want to add some specific style to the element(Button) clicked upon on. The styling I want to add is just change the background of the Button clicked upon.
I tried using conditional to set className but the styling was added to every item. So, it didn't work out.
conditional class - selecting particular class based on condition (in this case - if the activeTodos state == current index)
props.allTodos.map((prev, i) => {
<button type = 'button' key ={i}
className= {`${prev.id == activeTodos ? "active" : "inactive"}
onClick={e => setActiveTodos(prev.id)}}
</button>
}
There is some combinations (eg. There can be selected only 1 item per time or 4)
If You wan't 4 items selected (eg from 6) - then there is possiblity to hold active ids in array.
I have made autocomplete features using Downshift using react js. But the problem is when I am searching for something its input field value is disappearing when I click on the outside. Here is the sample code.
import logo from './logo.svg';
import './App.css';
import React, { useState } from "react";
import Highlighter from "react-highlight-words";
import Downshift from "downshift";
import axios from 'axios';
function App() {
const [names, setnames] = useState([{
const [searchTerm, setSearchTerm] = useState('')
const [movie, setmovie] = useState([])
fetchMovies = fetchMovies.bind(this);
inputOnChange = inputOnChange.bind(this);
function inputOnChange(event) {
if (!event.target.value) {
return;
}
fetchMovies(event.target.value);
}
function downshiftOnChange(selectedMovie) {
alert(`your favourite movie is ${selectedMovie.title}`);
}
function fetchMovies(movie) {
const moviesURL = `https://api.themoviedb.org/3/search/movie?api_key=1b5adf76a72a13bad99b8fc0c68cb085&query=${movie}`;
axios.get(moviesURL).then(response => {
setmovie(response.data.results);
// this.setState({ movies: response.data.results });
});
}
return (
<Downshift
onChange={downshiftOnChange}
itemToString={item => (item ? item.title : "")}
>
{({
selectedItem,
getInputProps,
getItemProps,
highlightedIndex,
isOpen,
inputValue,
getLabelProps
}) => (
<div>
<label
style={{ marginTop: "1rem", display: "block" }}
{...getLabelProps()}
>
Choose your favourite movie
</label>{" "}
<br />
<input
{...getInputProps({
placeholder: "Search movies",
onChange: inputOnChange
})}
/>
{isOpen ? (
<div className="downshift-dropdown">
{movie
.filter(
item =>
!inputValue ||
item.title
.toLowerCase()
.includes(inputValue.toLowerCase())
)
.slice(0, 10)
.map((item, index) => (
<div
className="dropdown-item"
{...getItemProps({ key: index, index, item })}
style={{
backgroundColor:
highlightedIndex === index ? "lightgray" : "white",
fontWeight: selectedItem === item ? "bold" : "normal"
}}
>
{item.title}
</div>
))}
</div>
) : null}
</div>
)}
</Downshift>
);
}
export default App;
This is the sample code I have written. Also, when I click shift+home, it is also not working.
Problem 1: when the user clicked the outside text field value whatever I searched this is disappearing.
Problem 2: shift + home is not working also.
Anyone has any idea how to solve this problem?
when the user clicked the outside text field value whatever I searched this is disappearing.
One way you could do it is to set the stateReducer on the Downshift component:
This function will be called each time downshift sets its internal state (or calls your onStateChange handler for control props). It allows you to modify the state change that will take place which can give you fine grain control over how the component interacts with user updates without having to use Control Props. It gives you the current state and the state that will be set, and you return the state that you want to set.
state: The full current state of downshift.
changes: These are the properties that are about to change. This also has a type property which you can learn more about in the stateChangeTypes section.
function stateReducer(state, changes) {
switch (changes.type) {
case Downshift.stateChangeTypes.mouseUp:
return {
...changes,
isOpen: true,
inputValue: state.inputValue,
};
default:
return changes;
}
}
This way if you click outside the text field the dropdown will stay open and the input value won't be reset.
For a list of all state change types see the documentation here
You might also be able to get something working using the onBlur prop on the input, but I didn't get that working.