Web:Components:MenuItem: create menu item component

This commit is contained in:
Timofey Boyko 2021-10-26 21:31:42 +08:00
parent 94a466ed48
commit 20dcdde09b
9 changed files with 462 additions and 1 deletions

View File

@ -0,0 +1,37 @@
# DropDownItem
Is a item of DropDown or ContextMenu component
### Usage
```js
import MenuItem from "@appserver/components/menu-item";
```
```jsx
<MenuItem
isSeparator={false}
isHeader={false}
label="Button 1"
icon="static/images/nav.logo.react.svg"
onClick={() => console.log("Button 1 clicked")}
/>
```
An item can act as separator, header, line, line with arrow or container.
When used as container, it will retain all styling features and positioning. To disable hover effects in container mode, you can use _noHover_ property.`
### Properties
| Props | Type | Required | Values | Default | Description |
| ------------- | :------------: | :------: | :----: | :-------------: | ---------------------------------------------------------- |
| `className` | `string` | - | - | - | Accepts class |
| `id` | `string` | - | - | - | Accepts id |
| `icon` | `string` | - | - | - | Dropdown item icon |
| `label` | `string` | - | - | `Dropdown item` | Dropdown item text |
| `isHeader` | `bool` | - | - | `false` | Tells when the dropdown item should display like header |
| `isSeparator` | `bool` | - | - | `false` | Tells when the dropdown item should display like separator |
| `noHover` | `bool` | - | - | `false` | Disable default style hover effect |
| `onClick` | `func` | - | - | - | What the dropdown item will trigger when clicked |
| `style` | `obj`, `array` | - | - | - | Accepts css style |

View File

@ -0,0 +1,80 @@
import React from "react";
import PropTypes from "prop-types";
import { ReactSVG } from "react-svg";
import { StyledMenuItem, StyledText, IconWrapper } from "./styled-menu-item";
//TODO: Add arrow type
const MenuItem = (props) => {
//console.log("MenuItem render");
const {
isHeader,
isSeparator,
label,
icon,
children,
onClick,
className,
} = props;
const onClickAction = (e) => {
onClick && onClick(e);
};
return (
<StyledMenuItem {...props} className={className} onClick={onClickAction}>
{icon && (
<IconWrapper isHeader={isHeader}>
<ReactSVG src={icon} className="drop-down-item_icon" />
</IconWrapper>
)}
{isSeparator ? (
<></>
) : label ? (
<StyledText isHeader={isHeader} truncate={true}>
{label}
</StyledText>
) : (
children && children
)}
</StyledMenuItem>
);
};
MenuItem.propTypes = {
/** Tells when the menu item should display like separator */
isSeparator: PropTypes.bool,
/** Tells when the menu item should display like header */
isHeader: PropTypes.bool,
/** Accepts tab-index */
tabIndex: PropTypes.number,
/** menu item text */
label: PropTypes.string,
/** menu item icon */
icon: PropTypes.string,
/** Disable default style hover effect */
noHover: PropTypes.bool,
/** What the menu item will trigger when clicked */
onClick: PropTypes.func,
/** Children elements */
children: PropTypes.any,
/** Accepts class */
className: PropTypes.string,
/** Accepts id */
id: PropTypes.string,
/** Accepts css style */
style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
/** Accepts css text-overflow */
textOverflow: PropTypes.bool,
};
MenuItem.defaultProps = {
isSeparator: false,
isHeader: false,
noHover: false,
textOverflow: false,
tabIndex: -1,
label: "",
};
export default MenuItem;

View File

@ -0,0 +1,51 @@
import React from "react";
import MenuItem from ".";
export default {
title: "Components/MenuItem",
component: MenuItem,
argTypes: {
onClick: { action: "onClick" },
},
parameters: {
docs: {
description: {
component: `Is a item of DropDown or ContextMenu component
An item can act as separator, header, line, line with arrow or container.
When used as container, it will retain all styling features and positioning. To disable hover effects in container mode, you can use _noHover_ property.`,
},
},
},
};
const Template = () => {
return (
<div style={{ width: "250px", position: "relative" }}>
<MenuItem
icon={"static/images/nav.logo.react.svg"}
label="Header(tablet or mobile)"
isHeader={true}
onClick={() => console.log("Header clicked")}
noHover={true}
/>
<MenuItem
icon={"static/images/nav.logo.react.svg"}
label="First item"
onClick={() => console.log("Button 1 clicked")}
/>
<MenuItem isSeparator={true} />
<MenuItem
icon={"static/images/nav.logo.react.svg"}
label="Item after separator"
onClick={() => console.log("Button 2 clicked")}
/>
<MenuItem onClick={() => console.log("Button 3 clicked")}>
<div>some child without styles</div>
</MenuItem>
</div>
);
};
export const Default = Template.bind({});

View File

@ -0,0 +1,76 @@
import React from "react";
import { mount, shallow } from "enzyme";
import MenuItem from ".";
const baseProps = {
isSeparator: false,
isHeader: false,
tabIndex: -1,
label: "test",
disabled: false,
icon: "static/images/nav.logo.react.svg",
noHover: false,
onClick: jest.fn(),
};
describe("<MenuItem />", () => {
it("renders without error", () => {
const wrapper = mount(<MenuItem {...baseProps} />);
expect(wrapper).toExist();
});
it("check isSeparator props", () => {
const wrapper = mount(<MenuItem {...baseProps} isSeparator={true} />);
expect(wrapper.prop("isSeparator")).toEqual(true);
});
it("check isHeader props", () => {
const wrapper = mount(<MenuItem {...baseProps} isHeader={true} />);
expect(wrapper.prop("isHeader")).toEqual(true);
});
it("check noHover props", () => {
const wrapper = mount(<MenuItem {...baseProps} noHover={true} />);
expect(wrapper.prop("noHover")).toEqual(true);
});
it("causes function onClick()", () => {
const onClick = jest.fn();
const wrapper = shallow(
<MenuItem id="test" {...baseProps} onClick={onClick} />
);
wrapper.find("#test").simulate("click");
expect(onClick).toHaveBeenCalled();
});
it("render without child", () => {
const wrapper = shallow(<MenuItem>test</MenuItem>);
expect(wrapper.props.children).toEqual(undefined);
});
it("accepts id", () => {
const wrapper = mount(<MenuItem {...baseProps} id="testId" />);
expect(wrapper.prop("id")).toEqual("testId");
});
it("accepts className", () => {
const wrapper = mount(<MenuItem {...baseProps} className="test" />);
expect(wrapper.prop("className")).toEqual("test");
});
it("accepts style", () => {
const wrapper = mount(<MenuItem {...baseProps} style={{ color: "red" }} />);
expect(wrapper.getDOMNode().style).toHaveProperty("color", "red");
});
});

View File

@ -0,0 +1,163 @@
import styled, { css } from "styled-components";
import Base from "../themes/base";
import Text from "../text/";
import { tablet } from "../utils/device";
import { isMobile } from "react-device-detect";
const styledHeaderText = css`
font-size: ${(props) => props.theme.menuItem.text.header.fontSize};
line-height: ${(props) => props.theme.menuItem.text.header.lineHeight};
`;
const styledMobileText = css`
font-size: ${(props) => props.theme.menuItem.text.mobile.fontSize};
line-height: ${(props) => props.theme.menuItem.text.mobile.lineHeight};
`;
const StyledText = styled(Text)`
font-weight: ${(props) => props.theme.menuItem.text.fontWeight};
font-size: ${(props) => props.theme.menuItem.text.fontSize};
line-height: ${(props) => props.theme.menuItem.text.lineHeight};
margin: ${(props) => props.theme.menuItem.text.margin};
color: ${(props) => props.theme.menuItem.text.color};
text-align: left;
text-transform: none;
text-decoration: none;
user-select: none;
${isMobile
? (props) => (props.isHeader ? styledHeaderText : styledMobileText)
: null}
@media ${tablet} {
${(props) => (props.isHeader ? styledHeaderText : styledMobileText)}
}
`;
StyledText.defaultProps = { theme: Base };
const StyledMenuItem = styled.div`
display: ${(props) =>
props.isHeader && !isMobile
? "none"
: props.textOverflow
? "block"
: "flex"};
align-items: center;
width: 100%;
height: ${(props) =>
isMobile
? props.isHeader
? props.theme.menuItem.header.height
: props.theme.menuItem.mobile.height
: props.theme.menuItem.height};
max-height: ${(props) =>
isMobile
? props.isHeader
? props.theme.menuItem.header.height
: props.theme.menuItem.mobile.height
: props.theme.menuItem.height};
border: none;
border-bottom: ${(props) =>
isMobile && props.isHeader
? props.theme.menuItem.header.borderBottom
: props.theme.menuItem.borderBottom};
cursor: ${(props) => (isMobile && props.isHeader ? "default" : "pointer")};
margin: 0;
margin-bottom: ${(props) =>
isMobile && props.isHeader
? props.theme.menuItem.header.marginBottom
: props.theme.menuItem.marginBottom};
padding: ${(props) =>
isMobile
? props.theme.menuItem.mobile.padding
: props.theme.menuItem.padding};
box-sizing: border-box;
background: none;
outline: 0 !important;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
@media ${tablet} {
display: ${(props) => (props.textOverflow ? "block" : "flex")};
height: ${(props) =>
props.isHeader
? props.theme.menuItem.header.height
: props.theme.menuItem.mobile.height};
max-height: ${(props) =>
props.isHeader
? props.theme.menuItem.header.height
: props.theme.menuItem.mobile.height};
padding: ${(props) => props.theme.menuItem.mobile.padding};
border: none;
border-bottom: ${(props) =>
props.isHeader
? props.theme.menuItem.header.borderBottom
: props.theme.menuItem.borderBottom};
margin-bottom: ${(props) =>
props.isHeader
? props.theme.menuItem.header.marginBottom
: props.theme.menuItem.marginBottom};
cursor: ${(props) => (props.isHeader ? "default" : "pointer")};
}
.drop-down-item_icon {
path {
fill: ${(props) => props.theme.menuItem.svgFill};
}
}
&:hover {
background-color: ${(props) =>
props.noHover
? props.theme.menuItem.background
: props.theme.menuItem.hover};
}
${(props) =>
props.isSeparator &&
css`
border-bottom: ${props.theme.menuItem.separator.borderBottom};
cursor: default !important;
margin: ${props.theme.menuItem.separator.margin};
height: ${props.theme.menuItem.separator.height};
width: ${props.theme.menuItem.separator.width};
&:hover {
cursor: default !important;
}
`}
`;
StyledMenuItem.defaultProps = { theme: Base };
const IconWrapper = styled.div`
display: flex;
align-items: center;
width: ${(props) =>
props.isHeader && isMobile
? props.theme.menuItem.iconWrapper.header.width
: props.theme.menuItem.iconWrapper.width};
height: ${(props) =>
props.isHeader && isMobile
? props.theme.menuItem.iconWrapper.header.height
: props.theme.menuItem.iconWrapper.height};
@media ${tablet} {
width: ${(props) =>
props.isHeader
? props.theme.menuItem.iconWrapper.header.width
: props.theme.menuItem.iconWrapper.width};
height: ${(props) =>
props.isHeader
? props.theme.menuItem.iconWrapper.header.height
: props.theme.menuItem.iconWrapper.height};
}
svg {
&:not(:root) {
width: 100%;
height: 100%;
}
}
`;
IconWrapper.defaultProps = { theme: Base };
export { StyledMenuItem, StyledText, IconWrapper };

View File

@ -0,0 +1,3 @@
<svg width="4" height="8" viewBox="0 0 4 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.5255 4L0.153211 1.10054C-0.0755111 0.820987 -0.0446084 0.400121 0.222235 0.160507C0.489078 -0.0791068 0.890813 -0.0467326 1.11954 0.232817L3.8468 3.56614C4.05107 3.8158 4.05107 4.1842 3.8468 4.43386L1.11954 7.76718C0.890813 8.04673 0.489078 8.07911 0.222235 7.83949C-0.0446084 7.59988 -0.0755111 7.17901 0.153211 6.89946L2.5255 4Z" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 460 B

View File

@ -0,0 +1,3 @@
<svg width="4" height="8" viewBox="0 0 4 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.146447 7.35355C-0.0488156 7.15829 -0.0488156 6.84171 0.146447 6.64645L2.79289 4L0.146446 1.35355C-0.0488158 1.15829 -0.0488158 0.841709 0.146446 0.646447C0.341708 0.451184 0.658291 0.451184 0.853553 0.646447L3.85355 3.64645C4.04882 3.84171 4.04882 4.15829 3.85355 4.35355L0.853553 7.35355C0.658291 7.54882 0.341709 7.54882 0.146447 7.35355Z" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 510 B

View File

@ -1613,6 +1613,54 @@ const Base = {
// }, // },
// }, // },
// }, // },
menuItem: {
iconWrapper: {
width: "16px",
height: "16px",
header: {
width: "24px",
height: "24px",
},
},
separator: {
borderBottom: `1px solid ${grayLightMid} !important`,
margin: "6px 16px 6px 16px !important",
height: "1px !important",
width: "calc(100% - 32px) !important",
},
text: {
header: {
fontSize: "15px",
lineHeight: "16px",
},
mobile: {
fontSize: "13px",
lineHeight: "36px",
},
fontSize: "12px",
lineHeight: "30px",
fontWeight: "600",
margin: "0 0 0 8px",
color: black,
},
hover: grayLight,
background: "none",
svgFill: black,
header: {
height: "55px",
borderBottom: `1px solid ${grayLightMid}`,
marginBottom: "6px",
},
height: "30px",
borderBottom: "none",
marginBottom: "0",
padding: "0 12px",
mobile: {
height: "36px",
padding: "0 16px",
},
},
}; };
export default Base; export default Base;

@ -1 +1 @@
Subproject commit b1063eae56d183b5c0b6eb887115c378f3941ebe Subproject commit 8177bad15d567d997a79478a65d32662a6f773b1