Display context menu while typing into textfield - reactjs

I am trying to implement a context menu that should be displayed while the user is typing into a textfield using the MaterialUI context menu. The idea is to display suggestions in the context menu based on the currently typed in word in the textfield. So the context menu is being shown next to the caret input and should not disturb the user from continue typing.
But the problem is when the menu is displayed the focus is taken from the textfield and the user is unable to continue typing until the menu has been closed.
Is there a way to allow the menu to be shown and still keep the focus on the textfield allowing the user to continue typing?
<Menu
keepMounted
open={contextMenuOpen}
onClose={handleClose}
anchorReference="anchorPosition"
anchorPosition={
{ top: caretPositionY, left: caretPositionX}
}
>
<MenuItem onClick={handleClose}>Suggestion1</MenuItem>
<MenuItem onClick={handleClose}>Suggestion2</MenuItem>
<MenuItem onClick={handleClose}>Suggestion3</MenuItem>
<MenuItem onClick={handleClose}>Suggestion4</MenuItem>
</Menu>

If you can't make it work with Material UI Menu, the here is the alternative:-
Demo.js (using simple ul element with package classnames):-
import React, { useState, useEffect } from "react";
import classNames from "classnames";
import { makeStyles } from "#material-ui/core/styles";
import { Button, TextField } from "#material-ui/core";
import "./style.css";
const Demo = () => {
const classes = useStyles();
const [search, setSearch] = useState("");
const [openMenu, setOpenMenu] = useState(false);
const [menuItems, setMenuItems] = useState([
{ id: 1, name: "Profile" },
{ id: 2, name: "My Account" },
{ id: 3, name: "Logout" }
]);
const [menuItemsFiltered, setMenuItemsFiltered] = useState(menuItems);
const handleClose = () => {
setOpenMenu(false);
};
const handleSearch = value => {
if (value === "") {
setOpenMenu(false);
setMenuItemsFiltered(menuItems);
} else {
setOpenMenu(true);
setMenuItemsFiltered(() =>
menuItems.filter(
item => item.name.toLowerCase().indexOf(value.toLowerCase()) > -1
)
);
}
};
useEffect(() => {
handleSearch(search);
}, [search]);
return (
<div className={classes.root}>
<TextField
type="search"
value={search}
onChange={e => setSearch(e.target.value)}
/>
<div>
<Button
aria-controls="simple-menu"
aria-haspopup="true"
onClick={() => setOpenMenu(!openMenu)}
>
Open Menu
</Button>
<ul
className={
openMenu ? classes.menu : classNames(classes.menu, classes.menuHide)
}
>
{menuItemsFiltered.map(item => (
<li
key={item.id}
className={classes.menuItem}
onClick={() => handleClose()}
>
{item.name}
</li>
))}
</ul>
</div>
</div>
);
};
export default Demo;
const useStyles = makeStyles(theme => ({
root: {
display: "flex"
},
menu: {
listStyle: "none",
padding: "0.5rem",
backgroundColor: theme.palette.grey[100],
"& li:not(:last-child)": {
marginBottom: "1rem"
}
},
menuHide: {
display: "none"
},
menuItem: {
"&:hover": {
cursor: "pointer",
color: theme.palette.primary.main
}
}
}));
You can see the working demo here in sandbox.

Update 2022
https://codesandbox.io/s/react-insert-context-menu-value-into-textarea-rhq5wq?file=/src/App.tsx
Addtional feature is setting cursor after the pasted value
import { Box, Menu, MenuItem, TextField, Typography } from "#mui/material";
import React, { useLayoutEffect } from "react";
import { useRef, useState } from "react";
export default function App() {
const [value, setValue] = useState(
"Lorem ipsum, dolor sit amet consectetur adipisicing elit. Accusamus, nemo. Doloremque eligendi aliquam repellendus reiciendis doloribus excepturi asperiores quaerat corporis."
);
const [selectionEnd, setSelectionEnd] = useState(0);
const textareaRef = useRef();
const [contextMenu, setContextMenu] = React.useState<{
mouseX: number;
mouseY: number;
} | null>(null);
const handleContextMenu = (event: React.MouseEvent) => {
event.preventDefault();
setContextMenu(
contextMenu === null
? {
mouseX: event.clientX + 2,
mouseY: event.clientY - 6
}
: // repeated contextmenu when it is already open closes it with Chrome 84 on Ubuntu
// Other native context menus might behave different.
// With this behavior we prevent contextmenu from the backdrop to re-locale existing context menus.
null
);
};
const handleClose = () => {
setContextMenu(null);
};
const insertText = (text) => () => {
const selectionStart = textareaRef.current.selectionStart;
const selectionEnd = textareaRef.current.selectionEnd;
const insertText = ` {{${text}}} `;
setSelectionEnd(selectionEnd + insertText.length);
const newValue = `${value.substring(
0,
selectionStart
)}${insertText}${value.substring(selectionEnd, value.length)}`;
setValue(newValue);
handleClose();
};
useLayoutEffect(() => {
//Sets the cursor at the end of inserted text
textareaRef.current.selectionEnd = selectionEnd;
}, [selectionEnd]);
return (
<Box onContextMenu={handleContextMenu}>
<Typography mb={2}>Right click on textarea to add some text</Typography>
<TextField
label="Textarea"
multiline
minRows={10}
inputRef={textareaRef}
value={value}
onChange={({ target }) => setValue(target.value)}
fullWidth
/>
<Menu
open={contextMenu !== null}
onClose={handleClose}
anchorReference="anchorPosition"
anchorPosition={
contextMenu !== null
? { top: contextMenu.mouseY, left: contextMenu.mouseX }
: undefined
}
>
<MenuItem onClick={insertText("VarA")}>VarA</MenuItem>
<MenuItem onClick={insertText("VarB")}>VarB</MenuItem>
</Menu>
</Box>
);
}

Related

How can I reset a dragged event to its original position on some condition in fullcallendar

I am working on fullCalendar. I want to implement something like this:
After dragging and dropping an event, a popup shows
If we cancel the event it needs to go back to its previous position
But the issue I am facing is that I am not able to find a way to return event back to its original position after dragging.
I am thinking to store the start dragging position and store also store the drop position. But I don't know if it is the right why to do that. Is there is any built in feature to do that or set the event back?
import React, { useState, useRef, useEffect } from "react";
import FullCalendar from "#fullcalendar/react"; // must go before plugins
import dayGridPlugin from "#fullcalendar/daygrid"; // a plugin!
import interactionPlugin, { Draggable } from "#fullcalendar/interaction"; // needed for dayClick
import timeGridPlugin from "#fullcalendar/timegrid";
import { resources } from "./records/resources";
import { events } from "./records/events";
import { Col, Row } from "react-bootstrap";
import "#fullcalendar/daygrid/main.css";
import "#fullcalendar/timegrid/main.css";
import "bootstrap/dist/css/bootstrap.css";
import { AlertBox } from "./atertBox";
import resourceTimelinePlugin from "#fullcalendar/resource-timeline";
function App() {
const [tableState, setTableState] = useState(events);
const [show, setShow] = useState(false);
const [showModal, setShowModal] = useState(false);
const dateRef = useRef();
const addEvent = (e) => {
const newEvent = {
id: tableState.length + 1,
title: e.target.title.value,
start: e.target.startdate.value + ":00+00:00",
end: e.target.enddate.value + ":00+00:00",
resourceId: "d",
// resourceEditable: false, to Preventing shifting between resources
};
setTableState((prev) => {
console.log(...prev);
console.log();
console.log(newEvent);
return [...prev, newEvent];
});
e.preventDefault();
};
const handleDateClick = (arg) => {
console.log("Arg", arg.dateStr);
};
const saveRecord = (record) => {
var today = new Date(record);
// var dd = String(today.getDate()).padStart(2, "0");
// var mm = String(today.getMonth() + 1).padStart(2, "0"); //January is 0!
// var yyyy = today.getFullYear();
// today = yyyy + "-" + mm + "-" + dd;
console.log(today, "today");
dateRef.current = { today };
setShow(true);
};
const injectwithButton = (args) => {
return (
<div>
<button onClick={() => saveRecord(args.date)}>
{args.dayNumberText}
</button>
</div>
);
};
useEffect(() => {
let draggableEl = document.getElementById("external-events");
new Draggable(draggableEl, {
itemSelector: ".fc-event",
minDistance: 5,
eventData: function (eventEl) {
console.log("drag element ", eventEl);
let title = eventEl.getAttribute("title");
let id = eventEl.getAttribute("data");
return {
title: title,
id: id,
};
},
});
}, []);
const eventClicked = (event) => {
console.log("event",event)
};
return (
<div className="animated fadeIn p-4 demo-app">
<Row>
<Col lg={3} sm={3} md={3}>
<div
id="external-events"
style={{
padding: "10px",
width: "20%",
height: "auto",
}}
>
<p align="center">
<strong> Events</strong>
</p>
{events.map((event, index) => (
<div
className="fc-event"
title={event.title}
data={event.id}
key={index}
>
{event.title}
</div>
))}
</div>
</Col>
<Col lg={9} sm={9} md={9}>
<div className="demo-app-calendar" id="mycalendartest">
<FullCalendar
plugins={[
dayGridPlugin,
timeGridPlugin,
interactionPlugin,
resourceTimelinePlugin,
]}
initialView="resourceTimeline"
headerToolbar={{
left: "today,prev,next",
center: "title",
right:
"new dayGridMonth,timeGridWeek,timeGridDay,resourceTimeline",
}}
customButtons={{
new: {
text: "add new event",
click: (e) => setShow(true),
},
}}
eventColor="grey"
nowIndicator={true}
events={tableState}
resources={resources}
dateClick={handleDateClick}
eventClick={eventClicked}
editable={true}
eventDragStart={(e) => console.log("ee", e)}
eventDrop={(drop) => {
console.log("drop", drop.oldEvent._instance.range);
console.log("drop", drop.oldEvent._def.publicId);
}}
eventResizableFromStart={true}
eventResize={(resize) => console.log("resize", resize)}
dayCellContent={injectwithButton}
schedulerLicenseKey="CC-Attribution-NonCommercial-NoDerivatives"
droppable={true}
// eventReceive={(e) => {
// console.log("receive", e);
// console.log("receive", e.event._instance.range);
// }}
drop={(drop) => {
console.log("external drop", drop);
}}
/>
</div>
</Col>
</Row>
</div>
);
}
export default App;
If you're talking about dragging an existing calendar event to a new position, then eventDrop provides a revert callback which, if called, will send the event back to its previous position.
If you're talking about dragging an external event onto the calendar, eventReceive provides a revert callback which, if called, will reverse the action and the event will not be dropped onto the calendar.

multi step form with React , Formik and Material UI, issue with checkboxes and autocomplete

I hope you could help me with this part, so I have no problem with the stepper and no problem with the forms too when it comes to getting data input ​​and validating fields with yup .
but I have a problem with the checkboxes and autocomplete. when I go back to a previous step, I lose the values ​​I have already entered and validation no longer works with checkbox and autocomplete field.
There is an example of a hook that I made for the checkboxes that I will use later in a form :
const CheckBoxField = (props) => {
const { label, ...rest } = props;
const [field, meta, helper] = useField(props);
const { setValue } = helper;
const [touched, error] = at(meta, "touched", "error");
const isError = error && touched && true;
function __renderHelperText() {
if (isError) {
return <FormHelperText>{error}</FormHelperText>;
}
}
function _onChange(event) {
setValue(event.target.checked);
}
const configFormControl = {
...field,
...rest,
onChange: _onChange,
};
return (
<FormControl
component="fieldset"
{...configFormControl}
error={Boolean(isError)}
>
<FormControlLabel
// value={field.checked}
checked={field.checked}
label={label}
onChange={_onChange}
control={
<BpCheckbox
{...configFormControl}
// checked={field.checked}
color="primary"
/>
}
color="primary"
/>
{__renderHelperText()}
</FormControl>
);
};
And there is the code for validation :
import * as yup from "yup";
import signUpFormModel from "../signUpFormModel";
const {
formField: {
terms //Boolean , used in checkboxes, should be true
},
} = signUpFormModel;
const signUpValidationSchema = [
yup.object().shape({
[terms.name]: yup.boolean().oneOf([true], `${terms.requiredErrMsg}`),
//other fields ...
}),
//other forms ...
];
export default signUpValidationSchema;
My Initial value :
import signUpFormModel from "../signUpFormModel";
const {
formField: {
terms,
},
} = signUpFormModel;
const formInitialValue = {
[terms.name]: false,
};
export default formInitialValue;
and some props helper (used as model for my forms)
import React, { Fragment } from "react";
import { Link } from "#mui/material";
const signUpFormModel = {
formId: "registration",
formField: {
terms: {
type: "checkbox",
name: "terms",
id: "terms",
label: () => (
<Fragment>
J'accepte les{" "}
<Link href="#" variant="body2">
termes et conditions générales d'utilisation
</Link>{" "}
et la{" "}
<Link href="#" variant="body2">
politique de confidentialité
</Link>
.
</Fragment>
),
requiredErrMsg: "Vous devez accepter les termes et conditions",
},
},
};
export default signUpFormModel;
Finaly the form itself :
import React from "react";
import { Grid } from "#mui/material";
import CheckBoxField from "../CheckBoxField";
const PrimarySignUpForm = (props) => {
const {
formField: {
terms,
},
} = props;
return (
<Grid container spacing={2}>
<Grid item xs={12}>
<CheckBoxField name={terms.name} label={terms.label()} />
</Grid>
</Grid>
);
};
export default PrimarySignUpForm;
And this is how I made the stepper :
import React, { Fragment, useState } from "react";
import SignUpFormLogs from "../forms/SignUpFormLogs";
import { Formik, Form } from "formik";
import formInitialValuefrom from "../formModel/formInitialValue";
import signUpFormModel from "../formModel/signUpFormModel";
import signUpValidationSchema from "../formModel/signUpValidationSchema";
import {
Button,
Link,
Step,
StepLabel,
Stepper,
Typography,
} from "#mui/material";
import { Box } from "#mui/system";
const steps = ["step1"];
const { formId, formField } = signUpFormModel;
function _renderSteps(step) {
switch (step) {
case "step1":
return <SignUpFormLogs formField={formField} />;
default:
return <div>Not Found</div>;
}
}
function _sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
const SignUp = () => {
const [activeStep, setActiveStep] = useState(0);
const currentValidationSchema = signUpValidationSchema[activeStep]; //activeStep
const isLastStep = activeStep === steps.length - 1;
async function _submitForm(values, actions) {
await _sleep(1000);
console.log(JSON.stringify(values, null, 2));
actions.setSubmitting(false);
setActiveStep(activeStep + 1);
}
function _handleSubmit(values, actions) {
if (isLastStep) {
_submitForm(values, actions);
} else {
setActiveStep(activeStep + 1);
actions.setTouched({});
actions.setSubmitting(false);
}
}
function _handleBack() {
setActiveStep(activeStep - 1);
}
return (
<Fragment>
<Stepper activeStep={activeStep} sx={{ pt: 3, pb: 5 }}>
{steps.map((label) => (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
{activeStep === steps.length ? (
<Typography variant="h6">Last step</Typography>
) : (
<Formik
initialValues={formInitialValuefrom}
validationSchema={currentValidationSchema}
onSubmit={_handleSubmit}
>
{({ isSubmitting }) => (
<Form id={formId}>
{_renderSteps(steps[activeStep])}
{activeStep !== 0 && (
<Button
onClick={_handleBack}
disabled={isSubmitting}
variant="outlined"
color="primary"
sx={{ mt: 4, mb: 2 }}
>
Back
</Button>
)}
<Button
type="submit"
disabled={isSubmitting}
variant="contained"
color="primary"
sx={{ mt: 4, mb: 2 }}
>
{isLastStep ? "Enregistrer" : "Suivant"}
</Button>
{isSubmitting && (
<Typography variant="h6">submitting...</Typography>
)}
</Form>
)}
</Formik>
)}
</Fragment>
);
};
export default SignUp;

Opening links in new tab in React Native application

I am quite new to React and I need to make a dashboard, where I will have a table with some data.
One of the column in the table is clickable and when I click on a particular cell, it will take that cell value and display more details about that item "in a new tab".
Now this is easy with a browser, where you open a new tab on link click. But this is an app i am making on chromium. more of a desktop app.
But I do not want a new window whole together. I need a new tab panel to open with the previous table still there in one tab, and the details in the new tab.
so when I go back to the previous tab and click on another item, it opens a third tab with the details of this item.
Example below (Sorry I am not allowed to insert pictures yet. Please click the link to see them.)
1st picture with initial table.
First Picture With the initial table
Now, if I click on Accountant, a new tab should appear as in second image:
Second image with a new tab opened
I think I found a solution to this. I am pasting it here for someone looking for a solution.
import React, { useState, useCallback, useEffect } from "react";
import PropTypes from "prop-types";
import { withStyles } from "#material-ui/core/styles";
import AppBar from "#material-ui/core/AppBar";
import { Tabs, Tab, IconButton } from "#material-ui/core";
import CloseIcon from "#material-ui/icons/Close";
import TabContainer from "../TabContainer/index.jsx";
const styles = theme => ({
root: {
flexGrow: 1,
backgroundColor: theme.palette.background.paper
},
colorPrimary: {
color: "red"
}
});
export const TabsDemo = ({
tabs,
selectedTab = 1,
onClose,
tabsProps = { indicatorColor: "primary" },
...rest
}) => {
const [activeTab, setActiveTab] = useState(selectedTab);
const [activetabs, setActiveTabs] = useState([]);
// useEffect(() => {
// if (activeTab !== selectedTab) setActiveTab(selectedTab);
// }, [setActiveTab, selectedTab, activeTab]);
// const handleChange = useCallback(event => setActiveTab(event.target.value), [
// setActiveTab,
// ]);
useEffect(() => {
setActiveTabs(tabs);
}, [tabs]);
const handleChange = useCallback((event, activeTab) => {
setActiveTab(activeTab);
}, []);
const handleClose = useCallback(
(event, tabToDelete) => {
event.stopPropagation();
const tabToDeleteIndex = activetabs.findIndex(
tab => tab.id === tabToDelete.id
);
const updatedTabs = activetabs.filter((tab, index) => {
return index !== tabToDeleteIndex;
});
const previousTab =
activetabs[tabToDeleteIndex - 1] ||
activetabs[tabToDeleteIndex + 1] ||
{};
setActiveTabs(updatedTabs);
setActiveTab(previousTab.id);
},
[activetabs]
);
return (
<>
<div>
<Tabs value={activeTab} onChange={handleChange}>
{activetabs.map(tab => (
<Tab
key={tab.id}
value={tab.id}
label={
typeof tab.label === "string" ? (
<span>
{" "}
tab.label
{tab.closeable && (
<IconButton
component="div"
onClick={event => handleClose(event, tab)}
>
<CloseIcon />
</IconButton>
)}
</span>
) : (
tab.label
)
}
/>
))}
</Tabs>
{activetabs.map(tab =>
activeTab === tab.id ? (
<TabContainer key={tab.id}>{tab.component}</TabContainer>
) : null
)}
</div>
</>
);
};
TabsDemo.propTypes = {
classes: PropTypes.object.isRequired,
tabs: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
id: PropTypes.number.isRequired,
component: PropTypes.object.isRequired,
closeable: PropTypes.bool.isRequired
}).isRequired
).isRequired
};
export default withStyles(styles)(TabsDemo);
Below is the link of a code sandbox that you can use to understand this fully :
https://codesandbox.io/s/mui-closeable-tab-gw2hw?file=/src/components/tabsdemo/index.jsx:0-3123

Is there a Material UI based tree select component?

I have been looking for a reliable component with a clear API documentation that would allow me to display a "tree view" structure for a select input as part of a form. The closest I have came across is vue-treeselect with many supported features such as: disabling branch nodes, disable item selection and more; the issue is that it's only available on Vue JS. My project is using Material UI as its design system, any component that supports it would be very great. Thanks
I needed a to deal with tree data in a project as well. I ended up creating MUI Tree Select.
You can demo it in this sandbox.
I searched a lot for that in the end I made this by myself sandbox.
you can choose to parent or child with this and you can custom it easily.
import { ThemeProvider, createTheme } from "#mui/material/styles";
import React, { useState } from "react";
import ReactDOM from "react-dom";
import TreeItem from "#mui/lab/TreeItem";
import { Popover, TextField, Typography } from "#mui/material";
import clsx from "clsx";
import { TreeView, useTreeItem } from "#mui/lab";
import ExpandMoreIcon from "#mui/icons-material/ExpandMore";
import ChevronRightIcon from "#mui/icons-material/ChevronRight";
import { useMediaQuery } from "#mui/material";
const data = [
{
id: "root",
name: "Parent",
children: [
{
id: "1",
name: "Child - 1"
},
{
id: "3",
name: "Child - 3",
children: [
{
id: "4",
name: "Child - 4"
}
]
}
]
},
{
id: "1root",
name: "Parent1",
children: [
{
id: "5",
name: "Child - 1-1"
},
{
id: "7",
name: "Child - 3-1",
children: [
{
id: "8",
name: "Child - 4-1"
}
]
}
]
}
];
const CustomContent = React.forwardRef(function CustomContent(props, ref) {
const {
classes,
className,
label,
nodeId,
icon: iconProp,
expansionIcon,
displayIcon
} = props;
const {
disabled,
expanded,
selected,
focused,
handleExpansion,
handleSelection,
preventSelection
} = useTreeItem(nodeId);
const icon = iconProp || expansionIcon || displayIcon;
const handleMouseDown = (event) => {
preventSelection(event);
};
const handleExpansionClick = (event) => {
handleExpansion(event);
};
const handleSelectionClick = (event) => {
handleSelection(event);
};
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div
className={clsx(className, classes.root, {
[classes.expanded]: expanded,
[classes.selected]: selected,
[classes.focused]: focused,
[classes.disabled]: disabled
})}
onMouseDown={handleMouseDown}
ref={ref}
style={{ padding: "3px 0" }}
>
<div onClick={handleExpansionClick} className={classes.iconContainer}>
{icon}
</div>
<Typography
onClick={handleSelectionClick}
component="div"
className={classes.label}
>
{label}
</Typography>
</div>
);
});
const CustomTreeItem = (props) => (
<TreeItem ContentComponent={CustomContent} {...props} />
);
export default function RichObjectTreeView({ formik, edit }) {
const [anchorEl, setAnchorEl] = React.useState(null);
const [equipmentItem, setEquipmentItem] = useState("");
const [equipmentId, setEquipmentId] = useState("");
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const open = Boolean(anchorEl);
const id = open ? "simple-popover" : undefined;
const renderTree = (nodes) => (
<CustomTreeItem key={nodes.id} nodeId={nodes.id} label={nodes.name}>
{Array.isArray(nodes.children)
? nodes.children.map((node) => renderTree(node))
: null}
</CustomTreeItem>
);
return (
<>
<TextField
variant="standard"
required={false}
label="Equipment Item"
name="equipmentItem"
id="equipmentItem"
defaultValue={equipmentItem}
value={equipmentItem}
className="w-100"
inputProps={{ readOnly: !edit }}
onClick={handleClick}
/>
<Popover
id={id}
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: "bottom",
horizontal: "left"
}}
>
<TreeView
aria-label="icon expansion"
defaultSelected={equipmentId}
selected={equipmentId}
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
onNodeSelect={(e, id) => {
setEquipmentId(id);
setEquipmentItem(e.target.innerText);
}}
sx={{
height: 200,
flexGrow: 1,
minWidth: "200px",
overflowY: "auto"
}}
>
{data.map((item, i) => renderTree(item))}
</TreeView>
</Popover>
</>
);
}
You can install this library here.
it cover both reactive forms and ngforms too
Check this out https://www.npmjs.com/package/mat-tree-select-input
Probably this is what you are looking for:
https://github.com/dowjones/react-dropdown-tree-select (it also has a theme for mui like style)

How to manage react state for a list of JSX.Elements correctly

I am using react-hooks to manage a list of JSX.Elements. However, once the element changed, trying to delete it will cause unexpected behavior.
I had tried using useReducer, remove by index etc, still unexpected updated result occurred.
FolderPage.tsx
import React, { useState, useEffect } from 'react';
import { Button, Box, Grid } from 'grommet';
import { Add, Close } from 'grommet-icons';
import { Files } from '../Component/Files/Files';
import { ePub } from '../extension/ePub/ePub';
interface Props {}
export const FolderPage: React.FC<Props> = () => {
const [state, setState] = useState([<Files openFileHandlers={[ePub]} />]);
const newFolderPanel = () => setState(prev => prev.concat(<Files openFileHandlers={[ePub]} />));
const removePanel = (panel: JSX.Element) => setState(prevState => prevState.filter(s => s !== panel));
return (
<div>
<Box align="start" pad="xsmall">
<Button icon={<Add />} label="New Folder Panel" onClick={newFolderPanel} primary />
</Box>
{state.map((s, index) => (
<Grid key={index}>
<Box align="end">
<Button
icon={<Close color="white" style={{ backgroundColor: 'red', borderRadius: '50%', padding: '.25rem' }} />}
type="button"
onClick={() => removePanel(s)}
/>
</Box>
{s}
</Grid>
))}
</div>
);
};
For example, in usage:
What should I change my code so my delete click will delete the matched element?
There is a way to work around it. For each item inside the array. Instead of storing item directly, stored it as {id: yourAssignedNumber, content: item}.
In this way, you could have control of the id, and remove by comparing the id only. This way, it will work correctly.
import React, { useState, useRef } from 'react';
import { Button, Row, Col } from 'antd';
import { Files } from '../Components/Files/Files';
import { fileHandler } from '../model/fileHandler';
interface Props {
fileHandlers?: fileHandler[];
}
export const FolderPage: React.FC<Props> = ({ fileHandlers }) => {
const [state, setState] = useState([{ key: -1, content: <Files fileHandlers={fileHandlers} /> }]);
const key = useRef(0);
const newFolderPanel = () =>
setState(prev =>
prev.concat({
key: key.current++,
content: <Files fileHandlers={fileHandlers} />
})
);
const removePanel = (key: number) => setState(prevState => prevState.filter(s => s.key !== key));
return (
<Row>
<Button type="primary" icon="plus" onClick={newFolderPanel} style={{ margin: '.75rem' }}>
New Foldr Panel
</Button>
{state.map(({ key, content }) => (
<Col key={key}>
<div
style={{
background: '#75ff8133',
display: 'grid',
justifyItems: 'end',
padding: '.5rem 1rem'
}}>
<Button onClick={() => removePanel(key)} icon="close" type="danger" shape="circle" />
</div>
{content}
</Col>
))}
</Row>
);
};

Resources