I'm trying to show a tooltip when a Legend component get hover. For that I have a Father component that has an useState hook in order to pass to the Leaflet Map component the index for an array Location and change the Permanent property if these index are equal.
const permanent = i === showLocationPosition;
The showLocationPosition is the index for the location that is getting hover, getting by props for its Father component.
<Marker
position={[position[0], position[1]]}
key={index}
icon={divIcon({ className: 'marker-dot dot-hover', html: ReactDOMServer.renderToString(stringHTML), iconSize: [30, 30] })}
>
<Tooltip direction="bottom" opacity={1} offset={new Point(xPoint, 10)} permanent={permanent}>
<div className={`popup-container-header ${item.count ? 'w-80' : 'w-40 text-center'}`}>
<p className="w-full">
{type_of_country_operation ?? item.name}
</p>
{item.count && <p>{item.count}</p>}
</div>
{item.summary && <p className="popup-container-main">{item.summary}</p>}
</Tooltip>
</Marker>
I could validate that the permanent variable changes but the Tooltip does not apear.
Any advice ? Thanks!
The reason why the change in permanent doesn't help is because underlying leaflet options are treated as immutable by react-leaflet. So even as your showLocationPosition might change (which changes permanent), the Tooltip was already created and will not respond to changes in that prop.
A quick and dirty way would be to use the key prop on the tooltip also, which can be a combination of the index and the permanent status:
<Tooltip {...otherProps} key={`${index}-${permanent}`}>
This would force a rerender of that Tooltip component when the value of permanent changes.
I would consider a different approach. If you don't need to also render the Tooltip when you mouseover the Marker it originates from, just conditionally render it based on permanent:
<Marker {...markerprops}>
{permanent && <Tooltip direction="..." offset={...} permanent={permanent}>
{stuff}
</Tooltip>}
</Marker>
You may want to change the name permanent to something else, like currentOpenTooltip. Now, if you also want to have the tooltip open and close properly when the user mouses over a marker, you'll need to add a condition for that. You can use a state variable to keep track of what Marker is being moused over, and use event handlers to control that state variable.
const Father = () => {
const [currentlyMousedOverMarker, setCurrentlyMousedOverMarker] = useState(-1);
return (
<MapContainer>
{markersData.map((marker, index) => {
<Marker
{...markerprops}
eventHandlers={{
mouseenter: () => { setCurrentlyMousedOverMarker(index) },
mouseleave: () => { setCurrentlyMousedOverMarker(-1) }
}}
>
{permanent || currentlyMousedOverMarker === index &&
(
<Tooltip permanent={permanent || currentlyMousedOverMarker}>
{stuff}
</Tooltip>
)
}
</Marker>
})}
</MapContainer>
)
}
Obviously this code example is simplified and doesn't contain any of the logic you already had for permanent, but its just to give you an idea that the tooltip should only be rendered if either condition is true.
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>
Im am using leaflet cluster library (v1.1.8) and i am trying to pass options.
I want the app to stop showing coverage on hover (see picture below).
But whenever i add the options showCoverageOnHover={false} it does not work.
<MarkerClusterGroup showCoverageOnHover={false}>
<MarkersLayer
stationsToDisplay={stationsToDisplay}
stationsList={stationsList}
refreshStationsList={this.refreshStationsList}
StandsToDisplay={StandsToDisplay}
CARToDisplay={CARToDisplay}
selectedOption={selectedOption}
/>
</MarkerClusterGroup >
The documentation shows that the correct code to pass options would be :
<MarkerClusterGroup showCoverageOnHover={false} />
However, i am passing already as a prop :
<MarkerClusterGroup >
<MarkersLayer
stationsToDisplay={stationsToDisplay}
stationsList={stationsList}
refreshStationsList={this.refreshStationsList}
StandsToDisplay={StandsToDisplay}
CARToDisplay={CARToDisplay}
selectedOption={selectedOption}
/>
</MarkerClusterGroup >
Then how do i pass the option ? I have tried inside the , as below but this does not work
<MarkersLayer
stationsToDisplay={stationsToDisplay}
stationsList={stationsList}
refreshStationsList={this.refreshStationsList}
StandsToDisplay={StandsToDisplay}
CARToDisplay={CARToDisplay}
selectedOption={selectedOption}
showCoverageOnHover={false}
/>
I am quite a noob in react, so any observation and suggestion would be much appreciated ! thanks a lot
According to the new API version on 1.1.8, something like this will work:
<MarkerClusterGroup showCoverageOnHover={false} >
<MarkersLayer
stationsToDisplay={stationsToDisplay}
stationsList={stationsList}
refreshStationsList={this.refreshStationsList}
StandsToDisplay={StandsToDisplay}
CARToDisplay={CARToDisplay}
selectedOption={selectedOption}
/>
</MarkerClusterGroup >
Another example of using markerClusterGroup might be useful:
import MarkerClusterGroup from 'react-leaflet-markercluster';
<MarkerClusterGroup>
<Marker position={[49.8397, 24.0297]} />
<Marker position={[52.2297, 21.0122]} />
<Marker position={[51.5074, -0.0901]} />
</MarkerClusterGroup>;
By adding the parameters that way in the class itself, fixes the issue :
class MarkerClusterGroup extends MapLayer {
createLeafletElement(props) {
const el = new L.markerClusterGroup(
{
props,
showCoverageOnHover:false,
disableClusteringAtZoom: 13,
}
);
this.contextValue = {
...props.leaflet,
layerContainer: el
};
return el;
}
}
export default withLeaflet(MarkerClusterGroup);
I am having a problem with React state changes accros two different components
The Parent Class has the state of the commands and the functions that change the state
constructor(props) {
super(props);
this.state = {
expanded: false,
addLock: false,
pendingCommand: undefined,
commands: [],
defCommands: [],
};
render() {
return (
<div>
<this.Commands/>
</div>
);
}
The two different components are Material ui Custom Accordions in tabs
Commands = () => {
const tabs = [
<CommandAccordion
cmd={this.state.defcommands}
expan={this.state.expanded} addLock={this.state.addLock}
hdlICh={this.handleInputChange}/>,
<CommandAccordion
cmd={this.state.commands} pendCmd={this.state.pendingCommand}
expan={this.state.expanded} addLock={this.state.addLock}
hdlICh={this.handleInputChange}/>]
}
The Accordion maps through the commands like this:
const commands = props.cmd;
const pendingCommand = props.pendCmd;
{[...pendingCommand === undefined ? [] : [pendingCommand], ...commands].map((obj, index) => (
<CstmAccordion expanded={expanded === obj._id} onChange={handleChange(obj._id, index)}
onClick={() => customOnChange(obj)}
key={'accordion-defcommands-' + index}>
<CstmAccordionDetails>
<Tabs value={tabId}>
<StyledTab label="Settings"/>
<StyledTab label="Advanced settings"/>
</Tabs>
<TabPanel value={tabId} Id={0} index={index} obj={obj} funcs={funcs}
commandV={commandValue}/>
<TabPanel value={tabId} Id={1} index={index} obj={obj} funcs={funcs}
commandV={commandValue}/>
Now the tab pannel has its own state which is set at the beginning and is used in a text field as value
const [commandValue, setCommandValue] = React.useState(obj.command);
<TextField
value={commandValue}
onBlur={funcs.handleInputChange(index, 'command')}
onSelect={() => {
funcs.handleOnFieldSelect(index)
}}
helperText={`What triggers the response? ${!commandValue ? 0 : commandNameValue.length}/40`}
onChange={(event) => {
if (event.target.value.length <= 40)
setCommandValue(event.target.value);
}}/>
Now the problem is if I first load everything it opens the tab for the default command in the parent component and everything looks fine. But if I change the tab to the custom commands, the first command in the Custom-Command-Tab has the same value like the first command in the Default-Command-Tab. (With Tabs I mean const tabs =[... in Commands = () =>{ and not <TabPanel .../>)
And if I change the value in the custom command, save it and change back to the default tab now the default commands have the value of the custom commands.
Also it might be important to now that value only syncs with the other commands with the same index. Which means the second command in the second tab has its own value and not the same value like the command before it - but is still synced with the second command in the first tab.
I think it might have something to do with the order in which the comopnents are rendered but I dont konw.
I've tried managing the state in the Accordion bevore the commands.map function and not in the TabPanel, and changing the state everytime the Accordion is clicked, and giving the TabPanel the commandValue and SetCommandValue and this works in terms of the sync problem, but with that the input into the TextField and has a 2 second input delay per char at worst.
I hope you can understand my problem even if it's a bit much.
I'm using react-virualized 9 with Autosizer, List, and CellMeasurer components. I need to update the row heights when the list data has changed. It appears that since the changes to support React Fiber in version 9 the only public method for CellMeasurer is now measure(). Most of the examples use the previous resetMeasurementForRow() method. The current CellMeasurer doc doesn't seem to have any info on the new public methods. Not sure if I've overlooked something but any help is appreciated.
const cache = new CellMeasurerCache({
defaultHeight: 60,
fixedWidth: true
});
<AutoSizer>
{({ width, height }) => (
<List
deferredMeasurementCache={cache}
height={height}
ref={element => { this.list = element; }}
rowCount={list.length}
rowHeight={cache.rowHeight}
rowRenderer={this.rowRenderer}
width={width}
/>
)}
</AutoSizer>
rowRenderer({ index, key, parent, style }) {
return (
<CellMeasurer
cache={cache}
columnIndex={0}
key={key}
overscanRowCount={10}
parent={parent}
ref={element => { this.cellMeasurer = element; }}
rowIndex={index}
>
{({ measure }) => {
this.measure = measure.bind(this);
return <MyList index={index} data={list[index]} style={style} />;
}}
</CellMeasurer>
);
}
componentWillReceiveProps(nextProps) {
// Some change in data occurred, I'll probably use Immutable.js here
if (this.props.list.length !== nextProps.list.length) {
this.measure();
this.list.recomputeRowHeights();
}
}
I need to update the row heights when the list data has changed.
The current CellMeasurer doc doesn't seem to have any info on the new public methods.
Admittedly the docs could be improved, with regard to the new CellMeasurer. In this case though, you need to do 2 things in respond to your row data/sizes changing:
If a specific list-item has changed size then you need to clear its cached size so it can be remeasured. You do this by calling clear(index) on CellMeasurerCache. (Pass the index of the row that's changed.)
Next you'll need to let List know that its size information needs to be recalculated. You do this by calling recomputeRowHeights(index). (Pass the index of the row that's changed.)
For an example of something similar to what you're describing, check out the example Twitter-like app I built with react-virtualized. You can see the source here.
if (this.props.list.length !== nextProps.list.length) {
cache.clearAll();
}
This helped me! :)