참고
- https://mui.com/material-ui/react-tabs/
Mui Tabs를 이용해서 동적으로 Tab을 추가 / 삭제해 보자. (링크에서 확인)
먼저 위의 링크를 참고하여 Tab 아래에 Text Area를 추가해 보자.
코드는 다음과 같다.
import React, { useState } from "react";
import Box from "@mui/material/Box";
import Tabs from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
import Textarea from "@mui/joy/Textarea";
const TextAreaTabPanel = (props) => {
const { children, value, index, tabs, setTabs, ...other } = props;
const handleChange = (event) => {
const updatedTabs = [...tabs];
updatedTabs[index].content = event.target.value;
setTabs(updatedTabs);
};
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{ p: 3 }}>
<Textarea
id={`textarea-${index}`}
placeholder="Input Text ..."
variant="outlined"
color="primary"
minRows={15}
maxRows={15}
value={tabs[index].content}
onChange={handleChange}
/>
</Box>
)}
</div>
);
};
const DynamicTabs = () => {
const [tabs, setTabs] = useState([
{ title: "Tab 1", content: "Content 1" },
{ title: "Tab 2", content: "Content 2" },
{ title: "Tab 3", content: "Content 3" },
]);
const [activeTab, setActiveTab] = useState(0);
const handleTabChange = (event, newValue) => {
setActiveTab(newValue);
};
return (
<Box sx={{ m: 2 }}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs
value={activeTab}
onChange={handleTabChange}
aria-label="dynamic tabs example"
>
{tabs.map((tab, index) => (
<Tab key={index} label={<span>{tab.title}</span>} />
))}
</Tabs>
</Box>
{tabs.map((tab, index) => (
<TextAreaTabPanel
key={index}
value={activeTab}
index={index}
content={tab.content}
tabs={tabs}
setTabs={setTabs}
/>
))}
</Box>
);
}
export default DynamicTabs;
탭 이름 변경하기
탭을 더블 클릭하면 탭의 이름을 변경하도록 해보자.
선택한 탭이 편집 모드인지 판단하기 위한 변수가 필요하다.
const [editIndex, setEditIndex] = useState(null);
Tab은 아래와 같이 바뀐다.
<Tab
key={index}
label={
editIndex === index ? (
<input
type="text"
value={tab.title}
autoFocus
onFocus={(e) => e.target.select()}
onChange={(e) => changeTabTitle(index, e.target.value)}
onBlur={(e) => handleTitleBlur(index, e)}
style={{ border: "none", outline: "none" }}
/>
) : (
<span onDoubleClick={() => setEditIndex(index)}>
{tab.title}
</span>
)
}
/>
편집 모드가 아니라면 span에서 tab의 제목을 그대로 보여준다.
doubleClick을 하게 되면 현재 선택한 tab의 index가 editIndex로 설정되어 편집 모드가 된다.
그리고 편집 모드가 된 tab은 input에 설정한 값이 된다.
onFocus / onBlur로 input의 포커스가 설정되거나 해제되도록 조절한다.
그리고 onChange로 제목을 변경하면 된다.
각 메서드는 전체 코드를 확인하자.
import React, { useState } from "react";
import Box from "@mui/material/Box";
import Tabs from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
import Textarea from "@mui/joy/Textarea";
const TextAreaTabPanel = (props) => {
const { children, value, index, tabs, setTabs, ...other } = props;
const handleChange = (event) => {
const updatedTabs = [...tabs];
updatedTabs[index].content = event.target.value;
setTabs(updatedTabs);
};
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{ p: 3 }}>
<Textarea
id={`textarea-${index}`}
placeholder="Input Text ..."
variant="outlined"
color="primary"
minRows={15}
maxRows={15}
value={tabs[index].content}
onChange={handleChange}
/>
</Box>
)}
</div>
);
};
const DynamicTabs = () => {
const [tabs, setTabs] = useState([
{ title: "Tab 1", content: "Content 1" },
{ title: "Tab 2", content: "Content 2" },
{ title: "Tab 3", content: "Content 3" },
]);
const [activeTab, setActiveTab] = useState(0);
const [editIndex, setEditIndex] = useState(null);
const handleTabChange = (event, newValue) => {
setActiveTab(newValue);
};
const changeTabTitle = (index, newTitle) => {
const updatedTabs = [...tabs];
updatedTabs[index].title = newTitle;
setTabs(updatedTabs);
};
const handleTitleBlur = (index, event) => {
const newTitle = event.target.value.trim();
if (newTitle !== "" && newTitle !== tabs[index].title) {
changeTabTitle(index, newTitle);
}
setEditIndex(null);
};
return (
<Box sx={{ m: 2 }}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs
value={activeTab}
onChange={handleTabChange}
aria-label="dynamic tabs example"
>
{tabs.map((tab, index) => (
<Tab
key={index}
label={
editIndex === index ? (
<input
type="text"
value={tab.title}
autoFocus
onFocus={(e) => e.target.select()}
onChange={(e) => changeTabTitle(index, e.target.value)}
onBlur={(e) => handleTitleBlur(index, e)}
style={{ border: "none", outline: "none" }}
/>
) : (
<span onDoubleClick={() => setEditIndex(index)}>
{tab.title}
</span>
)
}
/>
))}
</Tabs>
</Box>
{tabs.map((tab, index) => (
<TextAreaTabPanel
key={index}
value={activeTab}
index={index}
tabs={tabs}
setTabs={setTabs}
/>
))}
</Box>
);
}
export default DynamicTabs;
탭 추가하기
탭 추가하기 기능을 위해 + 버튼을 탭 옆에 추가하자.
import AddIcon from "@mui/icons-material/Add";
...
{tabs.map((tab, index) => (
<Tab
key={index}
label={ ... }
/>
))}
<Tab onClick={addTab} label={<AddIcon />} />
addTab은 다음과 같다.
useState로 관리되던 Tab에서 신규 탭을 추가하면 된다.
const addTab = () => {
const newTab = {
title: `New Tab`,
content: `New Content`,
};
setTabs([...tabs, newTab]);
setActiveTab(tabs.length); // Make the newly added tab active
};
이제 동적으로 TAB을 추가할 수 있게 되었다.
Tabs에 scrollable과 scrollButtons를 auto로 추가하면
탭이 많아지는 경우 자동으로 버튼이 만들어진다.
<Tabs
value={activeTab}
onChange={handleTabChange}
aria-label="dynamic tabs example"
variant="scrollable" // 'standard' | 'scrollable' | 'fullWidth';
scrollButtons="auto"
>
탭 삭제하기
탭에서 마우스 오른쪽 버튼을 누르면 팝업 창(Swal)이 나오고, Yes를 누르면 탭이 삭제되도록 해보자.
마우스 오른쪽 버튼은 onContextMenu에서 이벤트를 등록하면 된다.
<span
onDoubleClick={() => setEditIndex(index)}
onContextMenu={(event) =>
handleTabContextMenu(event, index)
}
>
{tab.title}
</span>
삭제 메서드는 다음과 같다.
삭제할 index를 filter를 이용해 지우고 useState로 갱신하였다.
const deleteTab = (index) => {
const updatedTabs = tabs.filter((tab, i) => i !== index);
setTabs(updatedTabs);
if (activeTab === index) {
setActiveTab(Math.max(activeTab - 1, 0));
}
};
const handleTabContextMenu = (event, index) => {
event.preventDefault();
Swal.fire({
title: "Are you sure?",
text: "Do you want to delete this tab?",
icon: "question",
showCancelButton: true,
confirmButtonText: "Yes",
cancelButtonText: "No",
}).then((result) => {
if (result.isConfirmed) {
deleteTab(index);
}
});
};
이제 탭을 삭제해 보자.
최종 코드는 다음과 같다.
import React, { useState } from "react";
import Box from "@mui/material/Box";
import Tabs from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
import Textarea from "@mui/joy/Textarea";
import AddIcon from "@mui/icons-material/Add";
import Swal from "sweetalert2";
const TextAreaTabPanel = (props) => {
const { children, value, index, tabs, setTabs, ...other } = props;
const handleChange = (event) => {
const updatedTabs = [...tabs];
updatedTabs[index].content = event.target.value;
setTabs(updatedTabs);
};
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{ p: 3 }}>
<Textarea
id={`textarea-${index}`}
placeholder="Input Text ..."
variant="outlined"
color="primary"
minRows={15}
maxRows={15}
value={tabs[index].content}
onChange={handleChange}
/>
</Box>
)}
</div>
);
};
const DynamicTabs = () => {
const [tabs, setTabs] = useState([
{ title: "Tab 1", content: "Content 1" },
{ title: "Tab 2", content: "Content 2" },
{ title: "Tab 3", content: "Content 3" },
]);
const [activeTab, setActiveTab] = useState(0);
const [editIndex, setEditIndex] = useState(null);
const handleTabChange = (event, newValue) => {
setActiveTab(newValue);
};
const changeTabTitle = (index, newTitle) => {
const updatedTabs = [...tabs];
updatedTabs[index].title = newTitle;
setTabs(updatedTabs);
};
const handleTitleBlur = (index, event) => {
const newTitle = event.target.value.trim();
if (newTitle !== "" && newTitle !== tabs[index].title) {
changeTabTitle(index, newTitle);
}
setEditIndex(null);
};
const addTab = () => {
const newTab = {
title: `New Tab`,
content: `New Content`,
};
setTabs([...tabs, newTab]);
setActiveTab(tabs.length); // Make the newly added tab active
};
const deleteTab = (index) => {
const updatedTabs = tabs.filter((tab, i) => i !== index);
setTabs(updatedTabs);
if (activeTab === index) {
setActiveTab(Math.max(activeTab - 1, 0));
}
};
const handleTabContextMenu = (event, index) => {
event.preventDefault();
Swal.fire({
title: "Are you sure?",
text: "Do you want to delete this tab?",
icon: "question",
showCancelButton: true,
confirmButtonText: "Yes",
cancelButtonText: "No",
}).then((result) => {
if (result.isConfirmed) {
deleteTab(index);
}
});
};
return (
<Box sx={{ m: 2 }}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs
value={activeTab}
onChange={handleTabChange}
aria-label="dynamic tabs example"
variant="scrollable"
scrollButtons="auto"
>
{tabs.map((tab, index) => (
<Tab
key={index}
label={
editIndex === index ? (
<input
type="text"
value={tab.title}
autoFocus
onFocus={(e) => e.target.select()}
onChange={(e) => changeTabTitle(index, e.target.value)}
onBlur={(e) => handleTitleBlur(index, e)}
style={{ border: "none", outline: "none" }}
/>
) : (
<span
onDoubleClick={() => setEditIndex(index)}
onContextMenu={(event) =>
handleTabContextMenu(event, index)
}
>
{tab.title}
</span>
)
}
/>
))}
<Tab onClick={addTab} label={<AddIcon />} />
</Tabs>
</Box>
{tabs.map((tab, index) => (
<TextAreaTabPanel
key={index}
value={activeTab}
index={index}
tabs={tabs}
setTabs={setTabs}
/>
))}
</Box>
);
}
export default DynamicTabs;
'개발 > React' 카테고리의 다른 글
리액트 - module.css로 CSS 스타일 관리하기 (0) | 2024.02.21 |
---|---|
리액트 Material - Alert로 플로팅 메시지 만들기 (Floating / Toast Message with Mui Alert) (0) | 2024.02.03 |
리액트 - react-diff-viewer-continued로 텍스트 비교하기 (Compare Text with Diff Viewer) (1) | 2024.01.31 |
리액트 - html2pdf로 PDF 다운로드하기 (0) | 2024.01.28 |
React Material - Stepper로 워크 플로우 관리하기 (Managing Workflows with Mui Stepper) (0) | 2024.01.24 |
댓글