Make multitple controlled material-ui tooltips open one at the time - javascript

I have a component where i use a few other componentes. I used to have the problem when a select menu was open, the tooltip overlaps the menu and don't stop showing. Now that i have make the Tooltip 'controlled' when i hover on one component it shows all the tooltips, and when click in one stop appearing (this is what i want but on hover one at the same time). This is how it looks:
I have my tooltip like this outside the function of the component im working on:
function Tooltip(props) {
const classes = tooltipStyles();
return <TooltipBase placement="bottom" classes={classes} {...props} />;
}
And this is how it looks in every component i mannage inside the main component:
<Tooltip title="Estatus" open={openTooltip}>
<div
onMouseEnter={() => {
setOpenTooltip(true);
}}
onMouseLeave={() => {
setOpenTooltip(false);
}}
>
<Chip
small
onIconClicked={null}
label="Por hacer"
avatarProps={{
size: 'extraSmall',
backgroundColor: '#F57C00',
icon: null,
}}
isDropdown
onChipClicked={handleOpen}
withPopover
popoverKey="status"
popoverOpen={chipStatusOpen}
popOverContent={PopupStatus}
onPopoverClose={handleClose}
/>
<PopupFilters
anchorEl={anchorElPopupFilters}
onClose={() => setAnchorElPopupFilters(null)}
/>
</div>
</Tooltip>
These are the functions im using to open/close the tooltips an select/popups
const handleClose = () => {
setChipStatus(false);
setAnchorEl(null);
};
const handleOpen = () => {
setChipStatus(true);
setOpenTooltip(false);
};
And the useStates that i use to make it work
const [chipStatusOpen, setChipStatus] = useState(false);
const [openTooltip, setOpenTooltip] = useState(false);
I want to open one at a time. How can i achieve that? What am i doing wrong?

Related

Why does the state switch while the function is running?

The site has a button for deleting records (DeleteForeverIcon in the code). When you click on this button, a window opens (made according to the documentation using Dialog Mui).
When you click on the "Confirm" button, the handleDeleteItem() function is launched, which deletes the entry. But I can’t understand why the window closes while this function is running, because I don’t switch the state anywhere
Looking for a solution on my own, I added console.log() to my code (below is the same code, only with console.log()) and came up with the following conclusion: when I run the handleDeleteItem() function, the open state switches to false and so the window closes. But why does it switch to false?
export function DeleteButton({ item }) {
const { urlParams } = useContext(PageContext)
const { firestore } = useContext(AppContext)
const [open, setOpen] = useState(false);
console.log(open, "window close") // console.log here
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const handleDeleteItem = async () => {
console.log("start") // console.log here
await deleteItem()
console.log("finish") // console.log here
}
return (
<ButtonGroup>
<DeleteForeverIcon onClick={handleClickOpen}/>
<Dialog
open={open}
onClose={handleClose}>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button onClick={handleDeleteItem}>Confirm</Button>
</DialogActions>
</Dialog>
</ButtonGroup >
)
}
That is, summing up the question, I would like the window to close only after the deleteItem () function has completed, and not during its execution
Based on some further clarification in the comments, it seems as though your issue is to do with the fact that calling deleteItem(...) causes your state to update in your parent components (due to an onSnapshot firebase listener). Your parent components are responsible for rendering the children components. When your state updates, the item/row that you deleted won't be in the new state value, and so the component that was rendering your Dialog previously won't be rendered (because it has been deleted).
Here is a minified example of your issue:
const { useState } = React;
const List = () => {
const [items, setItems] = useState(["a", "b", "c", "d", "e"]);
const handleDelete = (charToDel) => {
setItems(items => items.filter(char => char !== charToDel));
}
return <ul>
{items.map(char =>
<li key={char}>{char} - <DeleteButton value={char} onDelete={handleDelete}/></li>
)}
</ul>
}
const DeleteButton = ({value, onDelete}) => {
const [open, setOpen] = useState(false);
return <React.Fragment>
<button onClick={() => setOpen(true)}>×</button>
<dialog open={open}>
<p>Delete {value}?</p>
<button onClick={() => onDelete(value)}>Confirm</button>
</dialog>
</React.Fragment>
}
ReactDOM.createRoot(document.body).render(<List />);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
So since you're not rendering the component that renders the Dialog once you remove the item, you won't be rendering the <Dialog> anymore and so it disappears.
One way to fix this is to lift the <Dialog> component up to a component that doesn't get removed when you remove an item from your state. By the looks of things, the closest parent component that has this property is DevicesTable. In there you can render your dialog and keep track of a selectedItem to determine which item that should be deleted, which you can set based on the item you press (see code comments below):
// DevicesTable component
const [selectedItem, setSelectedItem] = useState();
const handleClose = () => {
setSelectedItem(null);
}
const handleDeleteItem = () => { // this doesn't need to be `async` if not using `await`
deleteItem(selectedItem, firestore, urlParams); // this doesn't need to be `await`ed if you don't have any code following it
}
return (
<>
{/* vvvvvv -- move dialog here -- vvvvvv */}
<Dialog open={!!selectedItem} onClose={handleClose}>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button onClick={handleDeleteItem}>Confirm</Button>
</DialogActions>
</Dialog>
{/* ^^^^^^ -- move dialog here -- ^^^^^^ */}
<TableContainer className="TableContainerGridStyle">
<Table className="TableStyle">
<DevicesTableHeader />
<TableBody className="TableBodyStyle">
{devices.map((device) =>
<DevicesTableCell
device={device}
onDeleteButtonPress={() => setSelectedItem(device)} /* <--- set the selected item */
key={device.description.id}
/>
)}
</TableBody>
</Table>
</TableContainer>
</>
);
For brevity, I've removed the open state and instead used the presence of the selectedItem to determine if the modal should be open or not, but you can of course add that back in if you wish and set both the selectedItem and the open state when opening and closing the modal.
Within DevicesTableCell, you would then grab the onDeleteButtonPress prop, and then pass it to DeleteButton like so:
// v-- grab the function
function DevicesTableCell({ device, onDeleteButtonPress }) {
...
<DeleteButton item={device} onDeleteButtonPress={onDeleteButtonPress}/> {/* pass it to the componennt */}
...
}
Within DeleteButton you should then invoke the onDeleteButtonPress function:
<DeleteForeverIcon onClick={onDeleteButtonPress} />
If you don't like the idea of passing callbacks down through multiple components like this, you can avoid that by using useReducer with a context, as described here.

Deduct if the menu is open in react-select DropdownIndicator handler

I'd need to change the style of the arrow in react-select. I learned it can be done by using the components prop like in the code sample below.
However, the props coming to DropdownIndicator do not seem to provide any information if the menu is opened. I would need to use that information to change the arrow style depending on whether the menu is open or closed.
How could I get that information?
import ReactSelect, { components } from 'react-select';
...
const DropdownIndicator = (props) => {
const { isFocused } = props;
// Which prop tells if the menu is open? Certainly isFocused is not the correct one.
const caretClass = isFocused ? 'caret-up' : 'caret-down';
return (
<components.DropdownIndicator {...props}>
<div className={`${caretClass}`} />
</components.DropdownIndicator>
);
};
return (<ReactSelect
components={{ DropdownIndicator }}
placeholder={placeholder}
value={value}
onBlur={onBlur}
name={name}
...
/>)
I think react-select is passing all selectProps in custom components. And there is field called menuIsOpen in selectProps which is used to determine whether dropdown is open or not.
So you can access menuIsOpen by following:-
const DropdownIndicator = (props) => {
const { menuIsOpen } = props.selectProps;
// menuIsOpen will tell if dropdown is open or not
const caretClass = menuIsOpen ? 'caret-up' : 'caret-down';
return (
<components.DropdownIndicator {...props}>
<div className={`${caretClass}`} />
</components.DropdownIndicator>
);
};

Material-table: Is it possible to open detailsPanel by default?

I'm trying to create a subtable of the main React Material-Table.
Everything is working properly as it should work, details panel (subtable) is showing on toggle icon press.
Are there any ways to show it opened by default? I mean to remove the toggle icon and show the detailPanel right from the component render?
Here is how my mat-table looks like (I didn't want to insert the whole component code, cause it will be too much code, full code is in the sandbox):
<MaterialTable
icons={tableIcons}
tableRef={tableRef}
columns={tableColumns}
data={tableData}
onRowClick={(evt, selectedRow) =>
setSelectedRow(selectedRow.tableData.id)
}
title="Remote Data Example"
detailPanel={detailSubtable}
options={{
rowStyle: rowData => ({
backgroundColor:
selectedRow === rowData.tableData.id ? "#EEE" : "#FFF"
})
}}
/>
And a link to the Codesandbox
As per knowledge, there is not any proper way or props to achieve this but you can do native DoM manipulation.
Provide custom Icon in DetailPanel icon with some unique ID like this:
DetailPanel: forwardRef((props, ref) => (
<div id="my-id" style={{ display: "none" }}>
<ChevronRight {...props} ref={ref} />
</div>
)),
Now, On componentDidMount find this element and trigger a click event on it and hide parent node like this
useEffect(() => {
const myToggler = document.getElementById("my-id");
if (!!myToggler) {
myToggler.click();
myToggler.parentNode.style.display = "none";
}
}, []);
here is the working sandbox link forked from yours, let me know if I am missing something.
If you see the source code There is a props called defaultExpanded which should work but there is an open issue which is causing the issue of not opening the panel by default.
To make it work (until the issue is fixed), you can imperatively modify the material-table's component the state in the useEffect
Like this
useEffect(() => {
tableRef.current.state.data = tableRef.current.state.data.map(data => {
console.log(data);
data.tableData.showDetailPanel = tableRef.current.props.detailPanel;
return data;
});
}, []);
Working demo of your code
This solution works for any number of rows/detailsPanel.

Using a Modal inside a list seems to pick the last value of the list

I have a list of cities and I'm trying to include a modal with a trash can icon to delete the city next to each item. The problem I have is that the modal seems to pick the last item of the list for EVERY item on the list.
When you click on the icon on any element on the list the confirmation modal always points to the last element on the list and I'm not sure what am I doing wrong. :(
I tried using a Confirm element instead only to find out it's using the modal underneath and I get the same results.
Any gurus around who can help me troubleshoot this will be greatly appreciated!
import React, { useState, useCallback } from "react";
import { List, Icon, Modal, Button } from "semantic-ui-react";
import "semantic-ui-css/semantic.min.css";
const CitiesList = () => {
const [deleteButtonOpen, setDeleteButtonOpen] = useState(false);
const cities = [{ name: "London" }, { name: "Paris" }, { name: "Porto" }];
const handleConfirmDeleteCityModal = useCallback(city => {
console.log("[handleConfirmDeleteCityModal] city", city);
// dispatch(deleteCity(city))
setDeleteButtonOpen(false);
}, []);
const showDeleteCityModal = useCallback(() => {
setDeleteButtonOpen(true);
}, []);
const handleCancelDeleteCityModal = useCallback(() => {
setDeleteButtonOpen(false);
}, []);
return (
<List>
{cities.map(c => (
<List.Item>
<List.Content className="list-item-content">
<List.Header as="h4">{c.name}</List.Header>
</List.Content>
<List.Content floated="left">
<Modal
size="tiny"
open={deleteButtonOpen}
onClose={() => handleCancelDeleteCityModal()}
trigger={
<Icon
name="trash alternate outline"
size="small"
onClick={() => showDeleteCityModal()}
/>
}
>
<Modal.Header>{`Delete City ${c.name}`}</Modal.Header>
<Modal.Content>
<p>Are you sure you want to delete this city?</p>
</Modal.Content>
<Modal.Actions>
<Button negative>No</Button>
<Button
positive
icon="checkmark"
labelPosition="right"
content="Yes"
onClick={() => handleConfirmDeleteCityModal(c)}
/>
</Modal.Actions>
</Modal>
</List.Content>
</List.Item>
))}
</List>
);
};
export default CitiesList;
Here is the example: https://codesandbox.io/s/optimistic-borg-56bwg?from-embed
The problem is this:
<Modal
size="tiny"
open={deleteButtonOpen}
onClose={() => handleCancelDeleteCityModal()}
trigger={
<Icon
name="trash alternate outline"
size="small"
onClick={() => showDeleteCityModal()}
/>
}
>
you use single flag deleteButtonOpen for controlling visibility of all modals. When you set it to true I suppose all modals are opened and you see only the latest one.
Normally I would render single modal and pass as props content of which item I want to show inside.
But if not using separate open flag for each modal should fix it, e.g. https://codesandbox.io/s/vigilant-banzai-byc4t

Hypertexting by pasting a link using a toolbar

I have to say I started Javascript and React this week so I am not really familiar with it yet or with anything in the front end.
I have a link button in side a toolbar. I want to be able to click it, opening a text box where I can write a link, and then the text is hypertexted with it. Just want to say that any tip is appreciated.
Something like the following pictures.
I have coded the toolbar already and am using the slate-react module for the Editor (the text editor used). I am trying to follow what was done in a GitHub example, which is not exactly the same.
So, in essence, it is a link component inside a toolbar, which is inside a "Tooltip" component (that contains the horizontal toolbar plus another vertical bar), which is inside the editor.
My question is: How do I use react and slate editor to tie the Links together in the toolbar? Does the Link component need a state and onChange function? How can I include the Link component in the toolbar (button group), alongside the other buttons within "const Marks"?
I get that these questions might be basic but I am a beginner and would appreciate explanation.
My created Link component can wrap and unwrap link. When clicked,
onClickLink = event => {
event.preventDefault()
const { value } = this.state
const hasLinks = this.hasLinks()
const change = value.change()
if (hasLinks) {
change.call(this.unwrapLink)
}
else
{
const href = window.prompt('Enter the URL of the link:')
change.call(this.wrapLink, href)
}
this.onChange(change)
}
The wrap, unwrap and hasLinks boolean
class Links extends React.Component {
onChange = ({ value }) => {
this.setState({ value })
}
wrapLink(change, href) {
change.wrapInline({
type: 'link',
data: { href },
})
change.moveToEnd() }
unwrapLink(change) {
change.unwrapInline('link') }
hasLinks = () => {
const { value } = this.state
return value.inlines.some(inline => inline.type == 'link')
}
To render it in the editor.
const renderNode = ({ children, node, attributes }) => {
switch (node.type) {
case 'link': {
const { data } = node
const href = data.get('href')
return (
<a {...attributes} href={href}>
{children}
</a>
)
}
The "Tooltip" component, holding MarkSelect (the horizontal toolbar like the one in the picures) and another vertical bar called NodeSelector.
function Tooltip({ onChange, value }: Props) {
return (
<Fragment>
<SelectionPlacement
value={value}
render={({ placement: { left, top, isActive } }) => (
<div
id=...
{
isActive,
},
)}
style={{ left, top }}
>
<NodeSelector onChange={onChange} value={value} />
<MarkSelector onChange={onChange} value={value} />
</div>
)}
/>
The MarkSelector and other Marks (buttons) in the button group.
const MarkSelector = function MarkSelector({ onChange, value }: Props) {
return (
<ButtonGroup className=...>
{Marks.map(({ tooltip, text, type }) => {
const isActive = value.activeMarks.some(mark => mark.type === type);
return (
<Tooltip key={type} title={tooltip}>
<Button
className={classNames({ 'secondary-color': isActive })}
onMouseDown={event => {
event.preventDefault();
const change = value.change().toggleMark(type);
onChange(change);
}}
size=...
style=...
}}
>
{text}
</Button>
</Tooltip>
);
})}
</ButtonGroup>
);
};
const Marks = [
{
type: BOLD,
text: <strong>B</strong>,
tooltip: (
<strong>
Bold
<div className=...</div>
</strong>
),
},
{
type: ITALIC,
text:...
The editor with the tooltip.
render() {
const { onChangeHandler, onKeyDown, value, readOnly } = this.props;
return (
<div
className=...
id=...
style=..
>
{!readOnly && (
<EditorTooltip value={value} onChange={onChangeHandler} />
)}
<SlateEditor
ref=...
className=...
placeholder=...
value={value}
plugins={plugins}
onChange={onChangeHandler}
onKeyDown={onKeyDown}
renderNode={renderNode}
renderMark={renderMark}
readOnly={readOnly}
/>
{!readOnly && <ClickablePadding onClick={this.focusAtEnd} grow />}
</div>
);
}
Although it is not a recommended way to manipulate DOM directly in data-driven frontend frameworks, but you could always get the HTML element of the link, and set its innerHTML (which is the hypertext) according to internal states. This is hacky but it might works.

Categories