I am struggling on how to properly set up a long list of Menu Items on a Select control that uses sticky SubHeaders. The problem is that when the items scroll they obscure the subheaders.
I looked at the Material UI examples of grouped Select items as a start. I wanted behavior that looked like the Material UI example with pinned subHeader Lists.
Here is a codeSandbox of what I'm trying.
Below is a snippet of my code:
<Select
className={classes.root}
MenuProps={{ className: classes.menu }}
value="Pick one"
onChange={e => {}}
>
{subHeaders.map(header => (
<li key={header}>
<ul>
<ListSubheader>{header}</ListSubheader>
{items.map(item => (
<MenuItem key={item} value={item}>
{item}
</MenuItem>
))}
</ul>
</li>
))}
</Select>
Here is a snapshot of the problem:
Using the Select component we can even reproduce the behavior with some corrections. But it won't work for you. The Select component does not expect items nested within your child's elements. That way, we will never be able to identify the element that is selected.
Alternatively, we have the Autocomplete component.
It can better supply what you need.
Regarding the example you provided, we can do something, but again, we will not be able to maintain the state of the selected item.
To achieve the same behavior as the list, we need to apply the same behavior to the list that the Menu will render.
Select will render a Menu that inherits List, so we can apply the same behavior as the list example through the prop MenuListProps property.
I applied the fixes to your example
I hope it helps.
I managed to make a working solution of Material-ui select with and sticky MenuItems.
use MaterialUI MenuItem instead of all the <li> <ul> <ListSubheader>
const [isOpen, setIsOpen] = useState(false);
const [value, setValue] = useState();
const onToggle = () => {
setIsOpen((prev) => !prev);
};
const onClose = () => {
setIsOpen(false);
};
const _onChange = (event: React.ChangeEvent<{ value: unknown }>) => {
const valueToSelect = event.target.value as Value;
if (
isResetSeletced(valueToSelect) ||
(multiple
? !valueToSelect.length ||
valueToSelect.length < minSelections ||
(valueToSelect as string[]).some((option) => !option)
: !valueToSelect?.length && minSelections > 0)
) {
return;
}
event.persist();
onChange(valueToSelect);
};
const renderValue = (selected: any) => {
if (!selected.length) {
return '';
}
if (multiple) {
const isReachedLimit = selected.length > MAX_SELECTIONS;
const hiddenTags = isReachedLimit ? (
<span>+{value.length - MAX_SELECTIONS}</span>
) : null;
const selectionsToShow = isReachedLimit
? selected.slice(0, MAX_SELECTIONS)
: selected;
return (
<StyledTagsContainer>
<Tags values={selectionsToShow} onRemoveTag={onRemoveTag} />
{hiddenTags}
</StyledTagsContainer>
);
}
return selected;
};
const resetMenuItem = secondaryOptions?.map((resetItem, index) => {
return (
<MenuItem
key={resetItem.value + index}
onClick={() => {
resetItem.onClick();
}}
isLast={!index}
isSelected={
resetItem.value === resetSelected?.value ||
resetItem.value === value ||
(multiple && resetItem.value === value[0])
}
value={resetItem.value}
icon={<RadioIcon />}
>
{resetItem.text}
</MenuItem>
);
});
<Select
displayEmpty
onClose={onClose}
value={value}
onChange={_onChange}
renderValue={renderValue}
open={isOpen}
>
{menuItems}
<div style={{ position: 'sticky', bottom: 0 }}>
{resetMenuItem}
</div>
</Select>
Related
I have a mapped component which iterates through API data. It passes props to each one and therefore each card looks different. See example below.
https://gyazo.com/39b8bdc4842e5b45a8ccc3f7ef3490b0
With the following, I would like to achieve two goals:
When the component is selected, it uses state to STAY SELECTED, and changes the colour as such to lets say blue for that selected component.
I hope this makes sense. How do I index a list as such and ensure the colour and state remains active based on this selection?
See below.
The level above, I map the following cards using these props.
{
jobs.length > 0 &&
jobs.map(
(job) =>
<JobCard key={job.id} job={job}
/>)
}
I am then using the following code for my components:
const JobCard = ({ job }) => {
const responseAdjusted = job.category.label
const responseArray = responseAdjusted.split(" ")[0]
return (
<CardContainer>
<CardPrimary>
<CardHeader>
<CardHeaderTopRow>
<Typography variant = "cardheader1">
{job.title}
</Typography>
<HeartDiv>
<IconButton color={open ? "error" : "buttoncol"} sx={{ boxShadow: 3}} fontSize ="2px" size="small" fontSize="inherit">
<FavoriteIcon fontSize="inherit"
onClick={()=> setOpen(prevOpen => !prevOpen)}/>
</IconButton>
</HeartDiv>
</CardHeaderTopRow>
<Typography variant = "subtitle4" color="text.secondary">
{job.company.display_name}
</Typography>
</CardHeader>
<CardSecondary>
</CardSecondary>
</CardPrimary>
</CardContainer>
)
}
You can attach a handler on the <CardPrimary> component by passing a function to the onClick event. That way whenever you click anywhere on the card div, the function will be triggered.
const [isSelected, setIsSelected] = useState(false);
<CardPrimary onClick={() => setIsSelected(true)} className={isSelected ? "css-class-to-highlight-div" : undefined>
....
</CardPrimary>
If I'm understanding what you're asking for, which I believe is to have your component be highlighted when it is clicked, then you need to modify the 'CardContainer' component to render with an 'onClick' parameter.
Example:
function CardContainer(props) {
const cssClass = 'highlighted';
const my_id = props.id || 'need_an_id';
var clearExistingHighlight = () => [...document.getElementByClassName(cssClass)].forEach((elem)=>elem.classList.remove(cssClass));
var isHighlighted = () => document.getElementById(my_id).classList.contains(cssClass);
var setHighlighted = (e) => {
clearExistingHighlight();
e.target.classList.add(cssClass);
}
return (
<div id={my_id} onClick={setHighlighted}>Cheeseburger fry</div>
)
}
If you don't want the highlight to disappear, you can get rid of the clearExistingHighlight function. Or if you want it to toggle, I recommend a modification of #sid's answer:
const {useState} = React;
function CardContainer(props) {
const [isSelected, setIsSelected] = useState(false);
<div onClick={() => setIsSelected(!isSelected)} className={isSelected ? "highlighted" : undefined>
}
style.css:
.highlighted {
background-color: 'orange';
}
You can do all of this without any react hook and rely instead on CSS classes. You can use the 'isHighlighted' method to determine if a given component is highlighted or not.
I have a list container component, which is the parent. It maps out the list rows. On the list row component, which is the child, every item has a button to toggle a pop-up menu, which has a button for edit, and a button for delete. The menu itself is a sibling to the list rows because if I include it in the list rows component, each row will render a menu and when toggled, they would all stack up on top of each other. The edit and delete buttons toggle either a form for the edit, or directly remove the item.
What I currently have is:
// Parent / Container
const [itemID, setItemID] = useState(null);
const handleMenuOpen = (id) => (e) => {
setAnchorEl(e.currentTarget); // for menu placement
setItemID(id);
};
const handleItemDelete = () => {
dispatch(deleteItem(itemID));
};
<List>
<ListRow handleMenuOpen={handleMenuOpen} />
<Menu handleItemDelete={handleItemDelete} itemID={itemID} />
</List>;
// List Row
<Button onClick={handleMenuOpen(item.id)} />;
// Menu
<MenuItem onClick={() => handleModalOpen(itemID)} />;
<MenuItem onClick={() => handleItemDelete()} />;
The edit button works fine but no matter how I try, I cannot get setItemID to work from the onClick on the list item. It always come out as the initial value of null. I console logged that the ID in the function parameter came out properly but the setState hook did not work.
I tried putting the useState on the list item and pass the ID through useContext but came out undefined when handledItemDelete was called.
I tried using ref on the child to get the ID from the parent, which also came out as undefined.
I cannot think of how to use useEffect to check for a change in the handleMenuOpen parameter.
I am out of ideas. Anyone know what the issue is and how to fix it?
You should probably just pass the handleMenuOpen function and rely on the selected element and then store it's id in itemID variable.
const handleMenuOpen = (e) => {
setAnchorEl(e.currentTarget); // for menu placement
setItemID(e.currentTarget.id);
};
<MenuItem onClick={handleMenuOpen} />;
i had the same problem before. I think you should handle the popup toggling in the child component, so something like this.
function Parent() {
function handleDelete(item) {
deleteFunction(item.id)
}
return (
<div>
{[].map((item, index) => {
return (
<ListRowItem key={index} handleDelete={handleDelete} item={item} />
)
})}
</div>
)
}
function ListRowItem({handleDelete, item}) {
const [isMenuOpen, setIsMenuOpen] = useState(false)
const [isModelVisible, setIsModalVisible] = useState(false)
return (
<div>
<Button onClick={isMenuOpen === true ? () => setIsMenuOpen(true) : () => setIsMenuOpen(false)} />
{isModelVisible === true ? <ModalItem /> :null}
{isMenuOpen === true ?
<div>
<MenuItem onClick={() => setIsModalVisible(true)} />
<MenuItem onClick={() => handleDelete(item.id)} />
</div>
: null}
</div>
)
}
I assume you are doing a certain loop to render each List Row inside the List component
let's say all items are in an items array which you loop:
{items.map(item => (
<ListRow handleMenuOpen={handleMenuOpen}/>
<Menu handleItemDelete={handleItemDelete} item={item} />
)}
now in the Menu container or component, you would have the item and pass it to the Menu item
recently I have been using react-virtualized library to render my tree item view. I have followed example from the docs however I end up having very strange problem with items disappearing when I scroll down.
I have created codesandbox to show this behaviour and code.
https://codesandbox.io/s/bitter-snow-23vci?file=/src/App.js
Main idea of virtualized list to render it as a list.
If you pass down tree like structure and render it like in your code sample
<List
....
rowCount={data.length}
/>
You don't change rowCount value and keep expanded state in your Node component.
const Node = ({ data, listRef, depth }) => {
const [isExpanded, setIsExpanded] = React.useState(false);
But then you scroll out of screen your Node element will be destroyed and recreated then you return.
You need to keep your selections outside of Node element.
like
// [key]: value structure there key is id of element and value [true, false].
const rootObject = {[elementId]: true};
const App = () => {
const [visibleNodes, setVisibleNodes] = useState(rootObject)
....
<List
...
rowRenderer={({ index, style, key }) => {
return (
<Node
setVisibleNodes={setVisibleNodes}
visibleNodes={visibleNodes}
style={style}
key={key}
data={data[index]}
listRef={ref}
depth={1}
/>
);
}}
rowCount={data.length}
width={width}
/>
And in Node
const Node = ({ data, listRef, depth, setVisibleNodes, visibleNodes }) => {
const isExpanded = visibleNodes[data.id];
const handleClick = (e) => {
if (data.children.length === 0) return;
e.stopPropagation();
setVisibleNodes({...visibleNodes, [data.id]: !!isExpanded});
listRef.current.recomputeRowHeights();
listRef.current.forceUpdate();
};
return (
<div onClick={handleClick}>
{data.children.length ? (isExpanded ? "[-]" : "[+]") : ""} {data.name}
{isExpanded && (
<div style={{ marginLeft: depth * 15 }}>
{data.children.map((child, index) => (
<Node
key={index}
data={child}
listRef={listRef}
depth={depth + 1}
/>
))}
</div>
)}
</div>
);
};
I think it works)
But it's better to do such things like real list and make tree hierarchy just visually. By that way you'll use Virtualisation List as it was purposed by creators)
Libraries like react-virtualized, react-window and react-virtuoso have item count property like in code below from materal-ui. However it is located within return. Is there any way to make item counterupdatable?
export default function VirtualizedList() {
const classes = useStyles();
return (
<div className={classes.root}>
<FixedSizeList height={400} width={300} itemSize={46} itemCount={200}>
{renderRow}
</FixedSizeList>
</div>
);
}
Yes you can pass on a dynamic value to the itemCount property in FixedSizeList. It take care of it and also ensure that the scroll remain where it is currently
A sample code would look like
const Example = () => {
const [rowCount, setRowCount] = useState(10);
useEffect(() => {
setTimeout(() => {
console.log("changed");
setRowCount(1000);
}, 10000);
}, []);
console.log(rowCount);
return (
<List
className="List"
height={150}
itemCount={rowCount}
itemSize={35}
width={300}
>
{Row}
</List>
);
};
Working demo
I have the following piece of code for my component. The desired behaviour for the button is to change the className for each li, but this is not working.
const Booking = (props) => {
let { hidden } = useContext(ContextBooking)
let completed = props.completed
return (
<li
className={ //should change according to the button click below
completed && hidden ?
'booking-complete hide'
: completed ?
'booking-complete'
:
'bookings'
}}
key={props.id}
id={props.id}
>
<h3>{props.date}</h3>
<h4>{props.time}</h4>
<h5>{props.name}</h5>
</li>
)
}
{!completed && (
<button
onClick={() => {
if (!completed && !hidden) {
completed = !completed //does make it false
hidden = !hidden //does make it false
} //above works, but won't change classname for each 'li'
else if (completed && hidden) {
completed = !completed
hidden = !hidden
}
}}>
Complete
</button>
)}
In another component, I am creating multiple of these 'Booking' components, by filling in the details with info that come from a json file
const DisplayBookings = () => {
const display = (day) => allBookings.map(item => //allBookings is a json file
item.day === day &&
<Booking
completed={item.completed}
key={item.id}
id={item.id}
time={item.time}
name={item.name}
date={item.date}
/>
)
I emphasised json file as I believe it could be the source of the problem?
A component can in most cases not update its own props, and doing so even if possible is an antipattern.
You can instead use state for updating the components state.
You can create hooks for setting state like this:
const [isCompleted, setIsCompleted] = useState(props.completed);
const [isHidden, setIsHidden] = useState(hidden);
Then in your onClick you use this to update the values:
setIsCompleted(!isCompleted);
setIsHidden(!isHidden);