menuItem.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import React from 'react';
  2. import styled from '@emotion/styled';
  3. import omit from 'lodash/omit';
  4. import Link from 'app/components/links/link';
  5. import space from 'app/styles/space';
  6. import {callIfFunction} from 'app/utils/callIfFunction';
  7. import {Theme} from 'app/utils/theme';
  8. type MenuItemProps = {
  9. /**
  10. * Should this item act as a header
  11. */
  12. header?: boolean;
  13. /**
  14. * Should this item act as a divider
  15. */
  16. divider?: boolean;
  17. /**
  18. * The title/tooltipe of the item
  19. */
  20. title?: string;
  21. /**
  22. * Is the item disabled?
  23. */
  24. disabled?: boolean;
  25. /**
  26. * Triggered when the item is clicked
  27. */
  28. onSelect?: (eventKey: any) => void;
  29. /**
  30. * Provided to the onSelect callback when this item is selected
  31. */
  32. eventKey?: any;
  33. /**
  34. * Is the item actively seleted?
  35. */
  36. isActive?: boolean;
  37. /**
  38. * Enable to provide custom button/contents via children
  39. */
  40. noAnchor?: boolean;
  41. /**
  42. * A router target destination
  43. */
  44. to?: React.ComponentProps<typeof Link>['to'];
  45. /**
  46. * A server rendered URL.
  47. */
  48. href?: string;
  49. /**
  50. * Enable to allow default event on click
  51. */
  52. allowDefaultEvent?: boolean;
  53. className?: string;
  54. };
  55. type Props = MenuItemProps & Omit<React.HTMLProps<HTMLLIElement>, keyof MenuItemProps>;
  56. class MenuItem extends React.Component<Props> {
  57. handleClick = (e: React.MouseEvent): void => {
  58. const {onSelect, disabled, eventKey, allowDefaultEvent} = this.props;
  59. if (disabled) {
  60. return;
  61. }
  62. if (onSelect) {
  63. if (allowDefaultEvent !== true) {
  64. e.preventDefault();
  65. }
  66. callIfFunction(onSelect, eventKey);
  67. }
  68. };
  69. renderAnchor = (): React.ReactNode => {
  70. const {to, href, title, disabled, isActive, children} = this.props;
  71. if (to) {
  72. return (
  73. <MenuLink
  74. to={to}
  75. title={title}
  76. onClick={this.handleClick}
  77. tabIndex={-1}
  78. isActive={isActive}
  79. disabled={disabled}
  80. >
  81. {children}
  82. </MenuLink>
  83. );
  84. }
  85. if (href) {
  86. return (
  87. <MenuAnchor
  88. href={href}
  89. onClick={this.handleClick}
  90. tabIndex={-1}
  91. isActive={isActive}
  92. disabled={disabled}
  93. >
  94. {children}
  95. </MenuAnchor>
  96. );
  97. }
  98. return (
  99. <MenuTarget
  100. role="button"
  101. title={title}
  102. onClick={this.handleClick}
  103. tabIndex={-1}
  104. isActive={isActive}
  105. disabled={disabled}
  106. >
  107. {this.props.children}
  108. </MenuTarget>
  109. );
  110. };
  111. render() {
  112. const {
  113. header,
  114. divider,
  115. isActive,
  116. noAnchor,
  117. className,
  118. children,
  119. ...props
  120. } = this.props;
  121. let renderChildren: React.ReactNode | null = null;
  122. if (noAnchor) {
  123. renderChildren = children;
  124. } else if (header) {
  125. renderChildren = children;
  126. } else if (!divider) {
  127. renderChildren = this.renderAnchor();
  128. }
  129. return (
  130. <MenuListItem
  131. className={className}
  132. role="presentation"
  133. isActive={isActive}
  134. divider={divider}
  135. noAnchor={noAnchor}
  136. header={header}
  137. {...omit(props, ['href', 'title', 'onSelect', 'eventKey', 'to', 'as'])}
  138. >
  139. {renderChildren}
  140. </MenuListItem>
  141. );
  142. }
  143. }
  144. type MenuListItemProps = {
  145. header?: boolean;
  146. noAnchor?: boolean;
  147. isActive?: boolean;
  148. disabled?: boolean;
  149. divider?: boolean;
  150. } & React.HTMLProps<HTMLLIElement>;
  151. function getListItemStyles(props: MenuListItemProps & {theme: Theme}) {
  152. const common = `
  153. display: block;
  154. padding: ${space(0.5)} ${space(2)};
  155. &:focus {
  156. outline: none;
  157. }
  158. `;
  159. if (props.disabled) {
  160. return `
  161. ${common}
  162. color: ${props.theme.disabled};
  163. background: transparent;
  164. cursor: not-allowed;
  165. `;
  166. }
  167. if (props.isActive) {
  168. return `
  169. ${common}
  170. color: ${props.theme.white};
  171. background: ${props.theme.active};
  172. &:hover {
  173. color: ${props.theme.black};
  174. }
  175. `;
  176. }
  177. return `
  178. ${common}
  179. &:hover {
  180. background: ${props.theme.focus};
  181. }
  182. `;
  183. }
  184. function getChildStyles(props: MenuListItemProps & {theme: Theme}) {
  185. if (!props.noAnchor) {
  186. return '';
  187. }
  188. return `
  189. & a {
  190. ${getListItemStyles(props)}
  191. }
  192. `;
  193. }
  194. const MenuAnchor = styled('a', {
  195. shouldForwardProp: p =>
  196. typeof p === 'string' && ['isActive', 'disabled'].includes(p) === false,
  197. })<MenuListItemProps>`
  198. ${getListItemStyles}
  199. `;
  200. const MenuListItem = styled('li')<MenuListItemProps>`
  201. display: block;
  202. ${p =>
  203. p.divider &&
  204. `
  205. height: 1px;
  206. margin: ${space(0.5)} 0;
  207. overflow: hidden;
  208. background-color: ${p.theme.innerBorder};
  209. `}
  210. ${p =>
  211. p.header &&
  212. `
  213. padding: ${space(0.25)} ${space(1)};
  214. font-size: ${p.theme.fontSizeSmall};
  215. line-height: 1.4;
  216. color: ${p.theme.gray300};
  217. `}
  218. ${getChildStyles}
  219. `;
  220. const MenuTarget = styled('span')<MenuListItemProps>`
  221. ${getListItemStyles}
  222. display: flex;
  223. `;
  224. const MenuLink = styled(Link, {
  225. shouldForwardProp: p =>
  226. typeof p === 'string' && ['isActive', 'disabled'].includes(p) === false,
  227. })<MenuListItemProps>`
  228. ${getListItemStyles}
  229. `;
  230. export default MenuItem;