I have a list, build with material-ui. There are a lot of items in it, so scrollbar is visible.
What I would like to do is scroll to the selected item. Have any ideas of how to implement this?
Here is a demo sendbox link
After click on the item list should looks like this (selected item is in the center):
I know there is an accepted answer here, but I think using
<ListItem autoFocus={true}/>
would scroll that list item into view. The same logic to set the list item to be marked as selected could be used for setting the autoFocus attribute as well.
Hold a ref to the List, and upon click on ListItem, calculate how much you need to scroll based on:
list item height
the index of the selected item
number of visible list items.
const scrollableListRef = React.createRef();
function Row(props) {
const { index, style } = props;
const placeSelectedItemInTheMiddle = (index) => {
const LIST_ITEM_HEIGHT = 46;
const NUM_OF_VISIBLE_LIST_ITEMS = 9;
const amountToScroll = LIST_ITEM_HEIGHT * (index - (NUM_OF_VISIBLE_LIST_ITEMS / 2) + 1) ;
scrollableListRef.current.scrollTo(amountToScroll, 0);
}
return (
<ListItem button style={style} key={index}
onClick={() => {placeSelectedItemInTheMiddle(index)}}>
<ListItemText primary={`Item ${index + 1}`} />
</ListItem>
);
}
Chandra Hasa's answer should include a bit of extra explanation but the edit queue was full.
Material-ui has a built-in way to do this with the autoFocus prop. See the table below taken from the MUI Docs.
Prop
Type
Default
Description
autoFocus
boolean
false
If true, the list item is focused during the first mount. Focus will also be triggered if the value changes from false to true.
❗️ The autoFocus prop should only be true for one item in the list. If you add autoFocus={true} to every ListItem, it will continuously scroll to each element, like OP mentioned in his comment.
The browser will scroll to the one ListItem that has autoFocus={true}.
If you want to always scroll to the same ListItem every time the component renders, you can add autoFocus={true} to that one ListItem component, like this:
<List>
<ListItem>item1</ListItem>
<ListItem autoFocus={true}>The list will automatically scroll to this item</ListItem>
<ListItem>item3</ListItem>
</List>
In most cases, people want to dynamically determine which ListItem to scroll to. In this case, the value of the autoFocus prop needs to evaluate to a boolean. It should still only be true for one item in the list.
One use case is if you're rendering a long list with one option already selected. It's good to automatically scroll so the selected option is visible.
/* The list will auto-scroll to the **one** item that has an
`autoFocus` prop that evaluates to `true`.
If `selectedItem === item2`,
the list will automatically scroll to `item2`.
*/
<List>
<ListItem autoFocus={ selectedItem === item1 }> item1 </ListItem>
<ListItem autoFocus={ selectedItem === item2 }> item2 </ListItem>
<ListItem autoFocus={ selectedItem === item3 }> item3 </ListItem>
</List>
❗️ This question and answers use MUI v4. MUI v5 deprecated the autoFocus prop for ListItem components. Use ListItemButton instead. Don't worry, it doesn't look like an MUI Button component.
Deprecated - checkout ListItemButton instead (Source: MUI Docs)
Related
I have a map with 5 markers on it and accordion with 5 elements on the side. Each marker has a corresponding accordion.
I want to click on a marker and expand the corresponding accordion. Both accordion and markers on the map have the same key values (screenshot below).
I use Map() function to generate accordion as well as markers. Simplified code looks something like this:
function Markers() {
const map = useMap();
return (
loc.map(loc => {
return (
<Marker
icon={locationMarker}
key={loc.properties.id}
position={[loc.properties.y, loc.properties.x]}}}>
</Marker>
)
})
)
}
export default Markers
function LocationCard() {
return (
<Container>
{loc.map(loc => (
<Accordion key={loc.properties.id}>
<AccordionSummary>
<Typography> {loc.properties.title} </Typography>
</AccordionSummary>
<AccordionDetails>
<Typography> { loc.properties.description } </Typography>
</AccordionDetails>
</Accordion>
))
}
</Container>
);
}
export default LocationCard
I am basically looking for a functionality "On marker click, expand accordion".
Any idea how I can achieve this with my current setup?
Thanks
An accordion will internally store its own state (an uncontrolled component) to track if it's expanded but you can override it, making it a controlled component.
Go up the component tree of your markers + accordions to find the lowest common intersection where we can store some state and pass it down to both components.
Store the selected marker ID (or an array if you want to have many accordions open at once) and an event handler function to update the state when a marker is pressed.
const [selectedMarkerID, setSelectedMarkerID] = useState<number | undefined>();
const handleMarkerPressed = (id: number) => () => {
// If a marker is pressed a second time, close the accordion
setSelectedMarkerID(selectedMarkerID !== id ? id : undefined);
}
return (
<>
<MyMarkers markers={markers} handleMarkerPressed={handleMarkerPressed />
<MyAccordions markers={markers} selectedMarkerID={selectedMarkerID} />
</>)
Then in your markers.map function, set the expanded property like the following:
<Accordion
key={loc.properties.id}
expanded={selectedMarkerID}>
// If you want to disable opening of accordions, override the onClick
onClick={undefined}
>
And your marker should look something like:
<Marker
...
onClick={handleOpenMarker(marker.id)}
/>
An alternative to prop drilling the state + markers + event handler down both components would be some kind of store or context API
More info available in the MUI accordion docs
Edit
Here's a Codebox example of passing the state + event handler around to manage which accordions are expanded: https://codesandbox.io/s/intelligent-feather-x9grwv?file=/src/App.tsx
The goal is to keep the markers in sync with the accordions, to summarise:
The accordion state is hoisted up a level to the App.tsx component
The marker event handler + accordion event handler is passed down from App.tsx
I've implemented a drawer similar to the example shown here. For working reproductions, please follow the link above and edit the Responsive Drawer on Stackblitz or Codesandbox. All that needs to be done to see the issue is to add onClick={(e) => console.log(e.target.tagName)} to the <ListItem button>.
Everything works as expected, except if you click on the top/bottom edge of a ListItem - in that case, I'm not able to get to the value assigned to the ListItem, and it's treated like an escape/cancellation and closes the drawer. In the <ListItem> the method onClick={(e) => console.log(e.target.tagName) will correctly log SPAN if you click in the middle, but will log DIV and be unresponsive if you click on the edge.
Example of one of the list items:
<Collapse in = {isOpen} timeout = 'auto' unmountOnExit>
<List component = 'div' disablePadding>
<ListItem button key = {'Something'} value = {'Something'} sx = {{pl: 4}} onClick = {(e) => handleSelect(e)}>
<ListItemIcon><StarBorder /></ListItemIcon>
<ListItemText primary = {'Something'} />
</ListItem>
</List>
</Collapse>
Overall structure of the drawer:
<List>
<Box>
<ListItem />
<Collapse>
<ListItem />
<ListItem />
</Collapse>
</Box>
</List>
onClick:
const handleSelect = (e) =>
{
const parentTag = e.target.tagName
if (parentTag === 'DIV')
{
console.log(e.target.innerHTML)
for (let child of e.target.children)
{
if (child.tagName === 'SPAN')
{
console.log(child.innerHTML)
}
}
}
else if (parentTag === 'SPAN')
{
console.log(e.target.innerHTML)
}
}
If you were to click in the middle of a ListItem, then parentTag === 'SPAN', and the console will log Something as expected.
But if you click on the top or bottom edge, then parentTag === 'DIV', and console.log(e.target.innerHTML) will show the following:
<div class="MuiListItemIcon-root..."><svg class="MuiSvgIcon-root..."><path d="..."></path>
</svg></div><div class="MuiListItemText-root..."><span class="MuiTypography-root...">
Something
</span></div><span class="MuiTouchRipple-root..."><span class="css..."><span
class="MuiTouchRipple..."></span></span></span>
There are three <span> elements, and I need the value of the first. However, console.log(child.innerHTML) always logs the later ones:
<span class="css..."><span
class="MuiTouchRipple..."></span></span>
Is there a way to get to the actual value I need? Or a better way to handle this, maybe by making the <div> unclickable/expanding the click area of the ListItem?
We can traverse till topmost parent div and search the content span from there:
const handleSelect = (e) => {
let target = e.target;
// get to the parent
while (!target.classList.contains('MuiButtonBase-root')) {
target = target.parentElement;
}
// get the content span
target = target.querySelector('.MuiTypography-root');
// utilize the content
setContent(`Clicked on: ${selected.tagName}, content: ${target.innerHTML}`);
console.log(selected);
handleDrawerToggle();
};
Even if you click on svg path element, above code will get you to the desired span element.
demo on stackblitz.
Also, we can prevent clicks on parent div using pointer-events:none CSS rule. But this will create huge unclickable area. And the SVG icon is also clickable :/ We'll have to make a lot of changes in CSS to bring the desired span in front of/covering everything.
Old answer
If you are trying to figure out which item got clicked then you can define onclick handler like this:
<ListItem button key={text}
onClick={(e) => console.log(text) } >
OR
<ListItem button key={text}
onClick={(e) => handleSelect(text) } >
This will give you the list item name right away. Then you can open corresponding content.
That's actually a CSS problem. You need to make the child elements width and height equal to the parent elements width and height. This is true for every element which is inline by default and you want to work with it.
Here are some docs about the CSS box model:
box model
understanding the inline box model
In this case, you want to change the display element in ListItem to div
AKA
<ListItem component="div">
// some stuff
</ListItem>
When I collapse a TreeItem, I want all it's descendants TreeItems (it's children, their children, etc.) that are open to collapse too. At the existing state they do disappear, but the next time I expand the parent TreeItem they are expanded as well.
The TreeView is "uncontrolled" by default meaning that the component will handle the state for you, and you as a consumer get whatever default behavior MUI set. In order to achieve what you want you'll need to make the TreeView a "controlled" component. There is an example of using the controlled component here: https://mui.com/components/tree-view/#controlled-tree-view
The example on the MUI page is doing more than just handling the expanding/collapsing of nodes. In the simplest form you need to explicitly handle the "expanded" prop that gets passed into the TreeView yourself.
const Tree = () => {
// nodes "1", "1.1", "2" will start expanded
const [expanded,setExpanded] = useState(["1","1.2","2"]);
const createHandler = (id) => () => {
// if node was collapsed, add to expanded list
if(!expanded.includes(id)) {
setExpanded([...expanded,id]);
} else {
// remove clicked node, and children of clicked node from expanded list
setExpanded(expanded.filter(i => i !== id && !i.startsWith(`${id}.`));
}
};
return (
<TreeView expanded={expanded}>
<TreeItem nodeId="1" label="A" onClick={createHandler("1")}>
<TreeItem nodeId="1.1" label="B" />
<TreeItem nodeId="1.2" label="C" onClick={createHandler("1.2")}>
<TreeItem nodeId="1.2.1" label="D" />
</TreeItem>
</TreeItem>
<TreeItem nodeId="2" label="E" onClick={createHandler("2")}>
<TreeItem nodeId="2.1" label="F" />
</TreeItem>
</TreeView>
);
}
Note: there be some mistakes in the above example, I'm unable to run it at the moment to verify correctness.
currently I have a signup form with 5 options but I'm trying to find a way to limit so the user can only select 2 options and in case the user selects a third option the first one would be unchecked, I had found a way of doing this in plain js but I haven't found a react way of doing it. This is what I have so far, would it be better to handle with plain js instead of react?
{iconsPool.map((src, index) => (
<Box className="test">
<input type="checkbox" className="iconsCheckbox" id={iconsPool.id} />
<label for={iconsPool.id}>
<img className="signupIcons" src={iconsPool[index].src} key={index} />
</label>
{console.log(iconsPool)}
</Box>
))}
This can be implemented with a state as an array with 2 elements.
Two items of the state Array will represent the index of selected items.
If an checkbox is clicked, that checkbox and the one clicked right before will be checked. (Therefore unchecking the one that was clicked even before that)
This can be done by pushing the index of newly clicked checkbox into the head of array, and removing the last item of the array.
When an checked checkbox is clicked again, (therefore it should be unchecked,) the index of the checkbox is searched from the state array, and removed by replacing that value with undefined
Below is code, as an example
...
const [checkedItems, setCheckedItems] = useState([undefined,undefined])
// When an Item is clicked
const onClickItem = (itemIndex:number, isSelected: boolean) =>{
if(isSelected){
setCheckedItems(c=>([itemIndex,c[0]]))
} else {
if(itemIndex === checkedItems[0]){
setCheckedItems(c=>([undefined,c[1]]))
} else if(itemIndex === checkedItems[1]){
setCheckedItems(c=>([c[0],undefined]))
}
}
}
I have a column list of elements, and each of them has hidden div, I want to show hidden element on click, but when I'm clicking another div, I want to hide current and show the one I clicked last, how can I do it correctly?
You could have two specific classes, one that hides element and one that shows element. when clicking on the element you can use jQuery or JavaScript to toggle the class that shows element for the hidden div of that specific element and hide everything for any other div.
The component you're rendering could take in an active prop and only render the second div if this prop is true.
With this, you could then keep track of which element is selected in the parent.
I threw together a working example with very simple content
import React, { useState } from 'react';
const ListItem = ({id, isActive, handleClick}) => {
return (
<div id={id} onClick={handleClick}>
Here is the element
{!!isActive && <div>I am the selected element</div>}
</div>
);
};
export const List = () => {
const [selectedItem, setSelectedItem] = useState();
const items = ['one', 'two', 'three'];
const handleClick = (event) => {
setSelectedItem(event.target.id);
}
return (
<div>
{items.map(item => (
<ListItem id={item} isActive={item===selectedItem} handleClick={handleClick}/>
))}
</div>
)
}