Issue in removing Grandchild in a recursive component - reactjs

What I have been trying to achieve?
Create a nested context menu that is driven by a config.
Where am I stuck:
Sub menus are rendering correctly, but if there is more than 2 level, the change in root level only affects its sub-menu and not its entire tree
Here is the sandbox link for you to check.
Steps to reproduce:
On load, a menu is displayed (say menu)
Click on File, it will open its sub menu (say sub-menu1).
Click on Open in the sub-menu1, again another sub menu (say sub-menu2) is open.
Now when you click on Edit in menu, sub-menu1 disappears but not sub-menu2
I think, I know the problem. sub-menu2 is not refreshing because props or state is not changed. To hide it, we will need to trickle down some prop but can't think of elegant way to do it without state management system.

You'll have a better time if the ContextMenu component is responsible for state management and recursion is flattened into iteration.
function ContextItem({ item, onClick }) {
return (
<div className="menu-item" onClick={() => onClick(item)}>
<p className="menu-title">{item.title}</p>
{item.children && item.children.length > 0 ? <i className="right-icon">{">"}</i> : null}
</div>
);
}
function MenuList({ list, onClick }) {
return (
<div className="menu-container">
{list.map((listItem) => (
<ContextItem item={listItem} key={listItem.title} onClick={onClick} />
))}
</div>
);
}
const ContextMenu = ({ list }) => {
const [openSubmenus, setOpenSubmenus] = React.useState([]);
const clickHandler = React.useCallback((item, level) => {
if (item.children && item.children.length) {
setOpenSubmenus((oldItems) => {
return [...oldItems.slice(0, level), item.children];
});
} else {
setOpenSubmenus([]); // item selected, clear submenus
alert(item.title);
}
}, []);
const menus = [list, ...openSubmenus];
return (
<div className="menu">
{menus.map((menu, level) => (
<MenuList
key={level}
list={menu}
level={level}
onClick={(item) => clickHandler(item, level)}
/>
))}
</div>
);
};
const menuList = [{
title: "File",
children: [{
title: "Close",
children: [],
action: "fileClose",
}, {
title: "Open",
children: [{
title: "A:\\",
children: [],
action: "",
}, {
title: "C:\\",
children: [],
action: "",
}, {
title: "\\",
children: [],
action: "",
}],
action: "",
}, {
title: "Find",
children: [{
title: "here",
children: [],
}, {
title: "elsewhere",
children: [],
}],
action: "",
}, {
title: "Backup",
children: [],
action: "backup",
}],
action: "",
}, {
title: "Edit",
children: [],
action: "edit",
}];
function App() {
return <ContextMenu list={menuList} />;
}
ReactDOM.render(<App />, document.getElementById("root"));
.menu {
display: flex;
flex-direction: row;
}
.menu-container {
display: flex;
flex-direction: column;
background-color: #eee;
border: 1px solid gray;
border-radius: 4px;
}
.menu-item {
display: flex;
flex-direction: row;
margin: 2px;
max-width: 200px;
line-height: 30px;
padding: 5px 10px;
}
.menu-title {
min-width: 80px;
height: 30px;
flex-grow: 1;
margin: 0;
vertical-align: middle;
}
.menu-title.active {
background-color: blue;
color: white;
}
.right-icon {
width: 25px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.0.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.0.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>

You could use the label as the key to reset the ContextMenu state when selectedItem change (assuming the label is unique at a given depth, but it seems reasonable, otherwise you could add unique ids).
export const ContextMenu = ({list}) => {
const [selectedItem, setSelectedItem] = useState();
return (
<div className="menu">
<div className="menu-container">
{list.map((listItem) => {
return (
<ContextItem
item={listItem}
key={listItem.title}
onClick={setSelectedItem}
/>
);
})}
</div>
{selectedItem?.children.length > 0 && <ContextMenu
key={selectedItem.title}
list={selectedItem.children}/>}
</div>
);
};

Related

How can I style active Link ,if i have Grid material-ui

I have Grids from material-ui, when you click on it, the link changes,when i want to change text color to black.
Please tell me how to get a different text color if the link is active
I just don't want to use NavLink.Who had such a problem I know if you use NavLink then you can get a different color through activeStyle
export const gridLayoutChildCSS = css`
&& {
height: 64px;
width: 100%;
&:hover {
background-color: ${(props) => props.theme.colors.grey};
transition: 0.1s ease-in-out;
}
cursor: pointer;
}
`;
export const gridLayoutIconCSS = css`
&& {
height: 26px;
}
`;
export const gridLayoutChildTextCSS = css`
&& {
font-family: Fira Sans;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 24px;
letter-spacing: 0.02em;
text-align: left;
color: ${(props) => props.theme.colors.wetAsphalt};
}
`;
export const Sidebar: React.FC = () => {
const [isLoading, setIsLoading] = useState(false);
const handleRedirect = useHandleRedirect();
const topItems = [
{ icon: <SvgCalendarIcon />, title: 'Пункт', url: NEW_PATHS.welcome.main },
{ icon: <SvgDocumentsIcon />, title: 'Список документов', url: '' },
{ icon: <SvgCalendarIcon />, title: 'График работы', url: '' },
{ icon: <SvgPeopleIcon />, title: 'Сотрудники', url: '' },
];
const bottomItems = [
{ icon: <SvgQuestionIcon />, title: 'Помощь', url: '' },
{ icon: <SvgProfileIcon />, title: 'Профайл', url: '' },
{ icon: <SvgSettingsIcon />, title: 'Настройки', url: '' },
];
return (
<Styled.Root>
{isLoading ? (
<OldSidebar />
) : (
{topItems.map((topItem) => {
return (
<Grid
css={gridLayoutChildCSS}
onClick={() => handleRedirect(topItem.url)}
container
direction="row"
justifyContent="center"
alignItems="center"
>
<Grid css={gridLayoutIconCSS} item xs={2}>
{topItem.icon}
</Grid>
<Grid item xs={7}>
<Typography css={gridLayoutChildTextCSS}>{topItem.title}</Typography>
</Grid>
</Grid>
);
})}
)}
</Styled.Root>
);
};
set [isactive,setIsactive] state and when clicked set isactive to true and change the classname when isactive is true

How do I make sidebar automatically disappear once a menu item clicked

I implemented a some Sidebar code and would like the sidebar to automatically close once an item is clicked. Currently have to click the 'X' to hide sidebar. Below is the existing code for the Sidebar and Sidebar sub-menu.
Probably a one line fix if you know what you are doing, but I am new to React, so any help would be appreciated.
...
import React, { useState } from 'react';
import styled from 'styled-components';
import { Link } from 'react-router-dom';
import * as FaIcons from 'react-icons/fa';
import * as AiIcons from 'react-icons/ai';
import { SidebarData } from './SidebarData';
import SubMenu from './SubMenu';
import { IconContext } from 'react-icons/lib';
const Nav = styled.div`
// background: #15171c;
background: black;
height: 60px;
display: flex;
justify-content: flex-start;
align-items: center;
`;
const NavIcon = styled(Link)`
margin-left: 2rem;
font-size: 2rem;
height: 60px;
display: flex;
justify-content: flex-start;
align-items: center;
`;
// background: #15171c;
const SidebarNav = styled.nav`
background: #15171c;
width: 250px;
height: 150vh;
display: flex;
justify-content: center;
position: fixed;
top: 0;
left: ${({ sidebar }) => (sidebar ? '0' : '-100%')};
transition: 350ms;
z-index: 10;
`;
const SidebarWrap = styled.div`
width: 100%;
`;
const Sidebar = () => {
const [sidebar, setSidebar] = useState(false);
const showSidebar = () => setSidebar(!sidebar);
return (
<>
<IconContext.Provider value={{ color: '#fff' }}>
<Nav>
<NavIcon to='#'>
<FaIcons.FaBars onClick={showSidebar} />
</NavIcon>
</Nav>
<SidebarNav sidebar={sidebar}>
<SidebarWrap>
<NavIcon to='#'>
<AiIcons.AiOutlineClose onClick={showSidebar} />
</NavIcon>
{SidebarData.map((item, index) => {
return <SubMenu item={item} key={index} />;
})}
</SidebarWrap>
</SidebarNav>
</IconContext.Provider>
</>
);
};
export default Sidebar;
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
const SidebarLink = styled(Link)`
display: flex;
color: #e1e9fc;
justify-content: space-between;
align-items: center;
padding: 20px;
list-style: none;
height: 40px;
text-decoration: none;
font-size: 18px;
//#c6c8cf
//#252831
&:hover {
background: #c6c8cf;
border-left: 4px solid #632ce4;
cursor: pointer;
}
`;
const SidebarLabel = styled.span`
margin-left: 16px;
`;
const DropdownLink = styled(Link)`
background: #414757;
height: 40px;
padding-left: 3rem;
display: flex;
align-items: center;
text-decoration: none;
color: #f5f5f5;
font-size: 18px;
// background: #632ce4;
&:hover {
background: #d9dbd5;
cursor: pointer;
}
`;
const SubMenu = ({ item }) => {
const [subnav, setSubnav] = useState(false);
const showSubnav = () => setSubnav(!subnav);
return (
<>
<SidebarLink to={item.path} onClick={item.subNav && showSubnav}>
<div>
{item.icon}
<SidebarLabel>{item.title}</SidebarLabel>
</div>
<div>
{item.subNav && subnav
? item.iconOpened
: item.subNav
? item.iconClosed
: null}
</div>
</SidebarLink>
{subnav &&
item.subNav.map((item, index) => {
return (
<DropdownLink to={item.path} key={index}>
{item.icon}
<SidebarLabel>{item.title}</SidebarLabel>
</DropdownLink>
);
})}
</>
);
};
export default SubMenu;
import React from 'react';
// import * as FaIcons from 'react-icons/fa';
import * as AiIcons from 'react-icons/ai';
import * as IoIcons from 'react-icons/io';
import * as RiIcons from 'react-icons/ri';
import * as FaIcons from 'react-icons/fa';
export const SidebarData = [
{
title: 'Login',
path: '/login',
icon: <AiIcons.AiOutlineLogin />,
},
{
title: 'Site Master',
path: '/list-sitemaster',
icon: <FaIcons.FaBroadcastTower />,
},
{
title: 'Site Report Cards',
path: '/sitereportcardmaster',
icon: <IoIcons.IoIosPaper />,
},
{
title: 'Report Card Master',
path: '/reportcardmaster',
icon: <IoIcons.IoIosPaper />
},
{
title: 'Categories',
path: '/categories',
icon: <IoIcons.IoIosClipboard />,
iconClosed: <RiIcons.RiArrowDownSFill />,
iconOpened: <RiIcons.RiArrowUpSFill />,
subNav: [
{
title: 'Asset Type',
path: '/assettype',
icon: <IoIcons.IoIosPaper />,
cName: 'sub-nav'
},
{
title: 'Asset Sub Type',
path: '/assetsubtype',
icon: <IoIcons.IoIosPaper />,
cName: 'sub-nav'
},
{
title: 'Document Type',
path: '/documenttype',
icon: <IoIcons.IoIosPaper />,
cName: 'sub-nav'
},
{
title: 'Document Sub Type',
path: '/documentsubtype',
icon: <IoIcons.IoIosPaper />,
cName: 'sub-nav'
},
{
title: 'Report Card Category',
path: '/reportcarddetailcategory',
icon: <IoIcons.IoIosPaper />,
cName: 'sub-nav'
},
{
title: 'Contact Detail Type',
path: '/contactdetailtype',
icon: <IoIcons.IoIosPaper />,
cName: 'sub-nav'
},
{
title: 'Lighting Type',
path: '/lightingtype',
icon: <IoIcons.IoIosPaper />,
cName: 'sub-nav'
}
]
},
{
title: 'Company Master',
path: '/cpymaster-toplevel',
icon: <AiIcons.AiFillHome />,
iconClosed: <RiIcons.RiArrowDownSFill />,
iconOpened: <RiIcons.RiArrowUpSFill />,
subNav: [
{
title: 'CompanyMaster',
path: '/cpymaster',
icon: <AiIcons.AiFillHome />,
cName: 'sub-nav'
},
{
title: 'Users',
path: '/users',
icon: <IoIcons.IoIosPerson />,
cName: 'sub-nav'
},
{
title: 'Contacts',
path: '/contactmaster',
icon: <IoIcons.IoIosPeople />,
cName: 'sub-nav'
}
]
},
{
title: 'My Profile',
path: '/add-usersmall',
icon: <AiIcons.AiOutlineUser/>,
},
{
title: 'PDF',
path: '/pdfcomponent',
icon: <AiIcons.AiOutlineUser/>,
},
{
title: 'Users PageSort Test',
path: '/userstable',
icon: <AiIcons.AiOutlineUser/>,
},
{
title: 'Logout',
path: '/logout',
icon: <AiIcons.AiOutlineLogin />,
}
];
...
You have a state for the sidebar in
const Sidebar = () => {
const [sidebar, setSidebar] = useState(false);
const showSidebar = () => setSidebar(!sidebar);
...
If I'm not wrong, you close it in
<AiIcons.AiOutlineClose onClick={showSidebar} />
You can pass down that showSidebar function to the link components, somthing like
<SubMenu item={item} key={index} toggleSidebar={showSidebar}/>
(Also, avoid using index as key)
Receiving the funtion in the SubMenu component
const SubMenu = ({ item,toggleSidebar }) => {
thus, you are able to use it in the link
The following code in the subMenu.js along with #Daniel toggleSidebar code in sideBar.js rendered the desired results.
...
return (
<>
<SidebarLink to={item.path} onClick={item.subNav && showSubnav}>
<div>
{item.icon}
{/*following onClick={toggleSidebar} allows mainm menu items without subnav to close sidebar */}
<SidebarLabel onClick={toggleSidebar}>{item.title}</SidebarLabel>
</div>
<div>
{item.subNav && subnav
? item.iconOpened
: item.subNav
? item.iconClosed
: null}
</div>
</SidebarLink>
{subnav &&
item.subNav.map((item, index) => {
return (
//following onClick={toggleSidebar} allows submenu selections to close sidebar
<DropdownLink to={item.path} key={index} onClick={toggleSidebar}>
{item.icon}
<SidebarLabel>{item.title}</SidebarLabel>
</DropdownLink>
);
})}
</>
);
...

React-select, open sub-menu when hover over an option

I'm trying to build a submenu inside a main menu with React-select, it should be something like this:
When hovering over an option from the main menu, it triggers the submenu to open at the side.
Is there a way to do this using react-select? I couldn't find any example or documentation on this, is there a function like ```optionOnMouseover`` for this? Thank you in advance!
const options = [
{ value: 'chocolate', label: 'Chocolate' },
{ value: 'strawberry', label: 'Strawberry' },
{ value: 'vanilla', label: 'Vanilla' },
];
...
<Select
value={...}
onChange={...}
options={options}
/>```
This is on click, but if you need on hover,
just modify it
import React, { useState } from "react";
import ReactDOM from "react-dom";
import Select, { components } from "react-select"
const CustomOption = (props) => {
const [submenu, setSubmenu] = useState(false)
const [height, setHeight] = useState(0)
const handleOption = (e) => {
if(submenu) {
setSubmenu(false)
} else {
setHeight(e.clientY)
setSubmenu(true)
}
}
const handleSubOption = (e) => {
console.log('clicked')
}
const { data } = props;
return data.custom ? (
<>
<div onClick={handleOption} className="customs">
{data.label} <span className="caret"/>
{
submenu && (
<div className="dropdown-submenu">
<div className="drops" onClick={handleSubOption}>
Test dropdown 1
</div>
<div className="drops" onClick={handleSubOption}>
Test dropdown 2
</div>
<div className="drops" onClick={handleSubOption}>
Test dropdown 3
</div>
</div>
)
}
</div>
<style jsx>{`
.customs {
height: 36px;
padding: 8px;
position: relative;
}
.drops {
height: 36px;
padding: 8px;
}
.customs:hover, .drops:hover {
background-color: #17cf76;
}
.dropdown-submenu {
position: fixed;
top: ${height - 10}px;
left: 410px;
min-height: 36px;
overflow: auto;
border: 1px solid hsl(0,0%,80%);
border-radius: 4px;
color: #212529;
}
`}</style>
</>
) : (
<components.Option {...props} />
);
};
const options = [
{ custom: true, label: "I'm a custom link", value: "cust" }
];
function App() {
return (
<>
<Select classNamePrefix="category-select" className="w-30" components={{ Option: CustomOption }} options={options} />
<style jsx global>{`
* {
font-family: sans-serif;
text-align: center;
}
.w-30 {
width: 30% !important;
}
`}</style>
</>
)
}
export default App

React Component wont render in Codepen

I have a simple radio button group component on codepen here that is not rendering in codepen. I want to post this to the code review stackexchange, since it is one of the first components i've built and will be necessary in many places on a web app I am building. However for that post, I want my codepen example to be working.
I think I am probably breaking some rule about how to use es6 in react to get the app to render, but I am struggling to debug. My console.logs() are not helping, and the error messages in codepen arent helping a ton either.
Since I linked to my codepen, I have to accompany it with code, so here's what I have in my codepen at the moment:
import React, { Component } from 'react';
import { ToggleButton, ToggleButtonGroup } from 'react-bootstrap';
class ToolButtonGroup extends Component {
constructor(props) {
super(props);
};
render() {
// Get Variables from the params prop
const { header, buttons, initialVal } = this.props.params;
const { borderRadius, margin, padding, fontsize, border } = this.props.params;
const { gridID, gridColumns, minRowHeight } = this.props.params;
// Create the individual buttons
const pageButtons = buttons.map((buttoninfo, idx) => {
return (
<ToggleButton
key={idx}
style={{
"borderRadius": borderRadius,
"margin": margin,
"padding": padding,
"fontSize": fontsize,
"border": border
}}
bsSize="large"
value={buttoninfo.value}>
{buttoninfo.label}
</ToggleButton>
)
})
// Return the button group
return(
<div
style={{"border": "1px solid red" }}
id={gridID}>
<h2 style={{
"width": "100%",
"margin": "0 auto",
"fontSize": "1.75em",
"marginTop": "5px",
"border": "none"
}}
>{header}</h2>
<ToggleButtonGroup
type="radio"
name="charttype-options"
defaultValue={initialVal}
onChange={this.props.handler}
style={{
"display": "grid",
"gridTemplateColumns": "repeat(" + gridColumns + ", 1fr)",
"gridAutoRows": "auto",
"gridGap": "8px"
}}
>
{pageButtons}
</ToggleButtonGroup>
</div>
)
}
}
class StarterApp extends Component {
constructor(props){
super(props);
this.state = {
pitchersOrHitters: "",
position: ""
}
}
// Button and Select Handlers!
handlePitchHitChange = (pitchersOrHitters) => {
this.setState({pitchersOrHitters})
}
handlePositionChange = (position) => {
this.setState({ position: position });
}
render() {
console.log("A")
// 0. Load State and Props
const { pitchersOrHitters, position } = this.state;
// Pitcher or Hitter Radio Button Group params
const pitchOrHitButtonGroup = {
borderRadius: "25px",
margin: "1% 10%",
padding: "5%",
fontsize: "2em",
border: "2px solid #BBB",
gridColumns: 1, minRowHeight: "10px", "gridID": "buttons1",
header: "Choose One:",
buttons: [
{ value: "Pitchers", label: "Pitchers" },
{ value: "Hitters", label: "Hitters" },
],
initialVal: "Pitchers"}
// Pitcher or Hitter Radio Button Group params
const positionButtonGroup = {
borderRadius: "10px",
margin: "1% 10%",
padding: "5%",
fontsize: "1.25em",
border: "2px solid #BBB",
gridColumns: 4, minRowHeight: "20px", "gridID": "buttons2",
header: "Choose One:",
buttons: [
{ value: "SP", label: "SP" },
{ value: "RP", label: "RP" },
{ value: "1B", label: "1B" },
{ value: "2B", label: "2B" },
{ value: "SS", label: "SS" },
{ value: "3B", label: "3B" },
{ value: "LF", label: "LF" },
{ value: "RF", label: "RF" },
{ value: "CF", label: "CF" }
],
initialVal: "SP"}
return(
<div className="chart-grid-container">
<ToolButtonGroup
params={pitchOrHitButtonGroup}
value={pitchersOrHitters}
handler={this.handlePitchHitChange} />
<ToolButtonGroup
params={positionButtonGroup}
value={position}
handler={this.handlePositionChange} />
</div>
)
}
}
ReactDOM.render(
<StarterApp />,
document.getElementById('root')
);
.chart-grid-container {
display: grid;
grid-template-columns: repeat(12, 1fr);
grid-auto-rows: minmax(200px, auto);
grid-gap: 5px;
grid-template-areas:
"btns1 btns1 btns2 btns2 btns2 btns2 . . . . . .";
}
#buttons1 { grid-area: btns1; }
#buttons2 { grid-area: btns2; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.1/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.1/react-dom.min.js"></script>
<div id='root'>
COME ON WORK!
</div>
Im unsurprisingly struggling to get this code snippet working as well. Although in this case, it is because I don't know how to include react-bootstrap, which is something I've already done in my codepen.
Thanks!
I noticed I got the errors when using import statements on that specific project.
This is probably a limitation of transpiling engine on the codepen. Better if you use some platform (ie: enter link description here) that already has all of these solved out for you.
Here is your code on codesandbox.

ReactJs - Horizontal Mobile Menu using ReactCSSTransitionGroup

Mobile Menu UI: (PanelGroup shared with ancestor & descendant components)
Inital MenuPanel passes a PanelGroup (with an navItems array in props) to the DOM.
Some of the navItems passed have subNavItems.
Those with subNavItems also pass a PanelGroup with its children to the DOM.
What is expected to happen:
When the MenuPanel is triggered navItems are listed and those with subNavItems have a marker for the user to click to reveal the subNavItems.
When subNavItems are selected, all previous navItems are to disappear and only the subNavItems display.
Desired Result: Items up the chain disappear & selected child nodes appear after Family Law navItem is clicked
Actual Result: Items up the chain remain and selected child nodes write ontop of them after Family Law navItem is clicked
MobileNavButton: (the blue menu button above)
export default class MobileNavButton extends Component {
constructor(props) {
super(props);
this.state = {
showMobileMenuGroup: false
}
}
static propTypes = {
btn: PropTypes.object.isRequired,
viewDevice: PropTypes.object.isRequired
};
handleShowHide(e) {
e.preventDefault();
const doesShow = !this.state.showMobileMenuGroup;
this.setState({showMobileMenuGroup: doesShow});
}
render() {
const { btn, viewDevice } = this.props;
return (
<section>
.....
{ btn.clickEvent ?
<li ref="menuButton">
<a onClick={ this.handleShowHide.bind(this) }>
<icon className={ btn.icon }/>
<h2>{ btn.innerText }</h2>
</a>
<MobileMenuPanel childPages={ btn.childPages } showMobileMenuGroup={ this.state.showMobileMenuGroup } />
</li> : ''
}
</section>
);
}
}
MenuPanel:
export default class MobileMenuPanel extends Component {
static propTypes = {
childPages: PropTypes.array.isRequired,
showMobileMenuGroup: PropTypes.bool.isRequired
};
render() {
const { showMobileMenuGroup } = this.props;
const { childPages } = this.props;
return (
<div className='imenupanel' style={showMobileMenuGroup ? showSlide : hideSlide } >
<MobileMenuGroup childPages={ childPages } showMobileMenuGroup={showMobileMenuGroup ? true : false } />
</div>
);
}
}
MobileMenuGroup:
export default class MobileMenuGroup extends Component {
static propTypes = {
childPages: PropTypes.array.isRequired,
showMobileMenuGroup: PropTypes.bool.isRequired
};
render() {
const { childPages } = this.props;
const { showMobileMenuGroup } = this.props;
return (
<div className='imenu' style={ showMobileMenuGroup ? { display: 'block' } : { display: 'none' } }>
{
childPages.map((childPage, idx) => {
return (
<MobileNavMenuItem key={ idx } menuItem={ childPage }/>
)
})
}
</div>
);
}
}
MobileNavMenuItem:
export default class MobileNavMenuItem extends Component {
constructor(props) {
super(props);
this.state = {
showMobileMenuGroup: false
}
}
static propTypes = {
menuItem: PropTypes.object.isRequired
};
showChildren(e) {
e.preventDefault();
let showMobileMenuGroup = !this.state.showMobileMenuGroup;
this.setState({ showMobileMenuGroup: showMobileMenuGroup });
}
render() {
const { menuItem } = this.props;
const childPages = menuItem.childPages !== undefined ? menuItem.childPages.length : 0;
let menuItemsStyle = childPages > 0 ? 'imenuitem iright' : 'imenuitem'
return (
<div className={ menuItemsStyle } pageId={ menuItem.pageId } childPages={ childPages }
>
{ childPages > 0 ?
<span onClick={ this.showChildren.bind(this) } >
<div style={ { color: '#FFFFFF', padding: '12px 15px' } }>{ menuItem.title }</div>
<MenuPanelGroup childPages={ menuItem.childPages } showMobileMenuGroup={ this.state.showMobileMenuGroup } />
</span>
:
<a href={ menuItem.linkTo }>{ menuItem.title }</a>
}
</div>
);
}
}
I went with ReactCSSTransitionGroup
Here is sample code that achieved my goals:
Index.js
// "react": "^0.14.7",
// "react-addons-css-transition-group": "^0.14.7",
// "react-dom": "^0.14.7",
import React from 'react';
import { render } from 'react-dom';
const ReactCSSTransitionGroup = require('react-addons-css-transition-group')
const SlideTransition = React.createClass({
propTypes: {
depth: React.PropTypes.number.isRequired,
name: React.PropTypes.string
},
getDefaultProps() {
return {
name: 'slider'
};
},
getInitialState() {
return {direction: 'right'};
},
componentWillReceiveProps(newProps) {
const direction = newProps.depth > this.props.depth ? 'right' : 'left';
this.setState({direction});
},
render() {
const {name, depth} = this.props;
const outerProps = {
className: `${name}-outer-wrapper ${this.props.className}`
};
console.log(`name: ${name} depth: ${depth}`);
const transProps = {
component: 'div',
transitionName: `${name}-${this.state.direction}`,
className: `${name}-transition-group`
};
const innerProps = {
ref: 'inner',
key: depth,
className: `${name}-inner-wrapper` // imenupanel
};
return <div {...this.props} {...outerProps}>
<ReactCSSTransitionGroup transitionName="example" transitionEnterTimeout={500} transitionLeaveTimeout={300} {...transProps}>
<div {...innerProps}>
{this.props.children}
</div>
</ReactCSSTransitionGroup>
</div>;
}
});
const Browser = React.createClass({
getInitialState() {
return {
path: []
}
},
navUp() {
this.setState({path: this.state.path.slice(0, -1)})
},
navDown(index) {
this.setState({path: this.state.path.concat(index)})
},
render() {
const {path} = this.state;
const items = path.reduce(function(items, key) {
return items[key].children;
}, this.props.items);
console.log(path);
console.log(this.props.items);
return <div className="browser">
<SlideTransition depth={path.length} className="items-container">
<div className="menuItem">{path.length > 0 ? <a style={ { color: 'white' } } onClick={this.navUp}>Back</a> : 'Home'}</div>
{items.map(function(item, index) {
if (item.children) {
return <div key={item.name} style={ { clear: 'both' } } className="menuItem" >
<a style={ { display: 'inline!Important', color: 'white' } } href="#">{item.name}</a>
<a style={ { display: 'inline!Important', float: 'right', background: 'transparent' } } className="arrow" onClick={e => this.navDown(index)} key={item.name}> </a></div>;
} else {
return <div className="menuItem" key={item.name} key={item.name}><a href="#"style={ { color: 'white' } }>{item.name}</a></div>;
}
}.bind(this))}
</SlideTransition>
</div>;
}
});
const data = [
{name: 'Animal', children: [
{name: 'Land', children: [
{name: 'Cheetah'},
{name: 'Ant'},
]},
{name: 'Air', children: [
{name: 'Eagle'}
]},
{name: 'Water', children: [
{name: 'Water 1', children: [{name: 'Level'}]},
{name: 'Water 2', children: [
{name: 'Water 2', children: [
{name: 'Water 2 -1'},
{name: 'Water 2 -2', children: [
{name: 'Water 2-2-1'},
{name: 'Water 2-2 -2'},
{name: 'Water 2-2-3'}
]},
{name: 'Water 2 -3'}
]}
]}
]}
]},
{name: 'Vegetable', children: [
{name: 'Broccoli'},
{name: 'IE6'},
]},
{name: 'Mineral', children: [
{name: 'Granite'},
{name: 'Uraninite'},
]},
];
render(<Browser items={data} />, document.getElementById('MobileNav'));
index.html styles
<style>
a {
cursor: pointer;
display: block;
color:#fff;
}
.items-container {
max-width: 20rem;
border: 2px solid #000;
border-radius: .25rem;
}
.arrow {
padding: .25rem .5rem;
}
.menuItem {
color: #FFFFFF;
font-size: 15px;
border-bottom: solid 1px #5a5a5a;
padding: 10px 0 10px 10px;
}
a.arrow:after {
content: '\e0ea';
color: #fff;
font-family: 'icomoon-ult';
font-style: normal;
font-weight: normal;
text-transform: none;
}
a.item:hover {
background: #eee;
}
/* Generic slider styles */
.slider-outer-wrapper {
position: relative;
overflow: hidden;
transition: max-height .2s ease-in;
}
.slider-transition-group {
width: 200%;
overflow: hidden;
}
.slider-inner-wrapper {
background-color: #000;
width: 50%;
float: right;
transition: all .2s ease-in-out;
height: 400px;
}
.slider-inner-wrapper:first-child {
position: relative;
left: -50%;
}
.slider-right-enter {
transform: translate3d(100%, 0, 0);
}
.slider-left-enter {
transform: translate3d(-100%, 0, 0);
}
.slider-right-enter-active,
.slider-left-enter-active,
.slider-right-leave,
.slider-left-leave {
transform: translate3d(0, 0, 0);
}
.slider-right-leave-active {
transform: translate3d(-100%, 0, 0);
}
.slider-left-leave-active {
transform: translate3d(100%, 0, 0);
}
</style>

Resources