menuItem.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  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. className?: string;
  50. };
  51. type Props = MenuItemProps & Omit<React.HTMLProps<HTMLLIElement>, keyof MenuItemProps>;
  52. class MenuItem extends React.Component<Props> {
  53. handleClick = (e: React.MouseEvent): void => {
  54. const {onSelect, disabled, eventKey} = this.props;
  55. if (disabled) {
  56. return;
  57. }
  58. if (onSelect) {
  59. e.preventDefault();
  60. callIfFunction(onSelect, eventKey);
  61. }
  62. };
  63. renderAnchor = (): React.ReactNode => {
  64. const {to, href, title, disabled, isActive, children} = this.props;
  65. if (to) {
  66. return (
  67. <MenuLink
  68. to={to}
  69. title={title}
  70. onClick={this.handleClick}
  71. tabIndex={-1}
  72. isActive={isActive}
  73. disabled={disabled}
  74. >
  75. {children}
  76. </MenuLink>
  77. );
  78. }
  79. if (href) {
  80. return (
  81. <MenuAnchor
  82. href={href}
  83. onClick={this.handleClick}
  84. tabIndex={-1}
  85. isActive={isActive}
  86. disabled={disabled}
  87. >
  88. {children}
  89. </MenuAnchor>
  90. );
  91. }
  92. return (
  93. <MenuTarget
  94. role="button"
  95. title={title}
  96. onClick={this.handleClick}
  97. tabIndex={-1}
  98. isActive={isActive}
  99. disabled={disabled}
  100. >
  101. {this.props.children}
  102. </MenuTarget>
  103. );
  104. };
  105. render() {
  106. const {
  107. header,
  108. divider,
  109. isActive,
  110. noAnchor,
  111. className,
  112. children,
  113. ...props
  114. } = this.props;
  115. let renderChildren: React.ReactNode | null = null;
  116. if (noAnchor) {
  117. renderChildren = children;
  118. } else if (header) {
  119. renderChildren = children;
  120. } else if (!divider) {
  121. renderChildren = this.renderAnchor();
  122. }
  123. return (
  124. <MenuListItem
  125. className={className}
  126. role="presentation"
  127. isActive={isActive}
  128. divider={divider}
  129. noAnchor={noAnchor}
  130. header={header}
  131. {...omit(props, ['href', 'title', 'onSelect', 'eventKey', 'to'])}
  132. >
  133. {renderChildren}
  134. </MenuListItem>
  135. );
  136. }
  137. }
  138. type MenuListItemProps = {
  139. header?: boolean;
  140. noAnchor?: boolean;
  141. isActive?: boolean;
  142. disabled?: boolean;
  143. divider?: boolean;
  144. } & React.HTMLProps<HTMLLIElement>;
  145. function getListItemStyles(props: MenuListItemProps & {theme: Theme}) {
  146. const common = `
  147. display: block;
  148. padding: ${space(0.5)} ${space(2)};
  149. &:focus {
  150. outline: none;
  151. }
  152. `;
  153. if (props.disabled) {
  154. return `
  155. ${common}
  156. color: ${props.theme.disabled};
  157. background: transparent;
  158. cursor: not-allowed;
  159. `;
  160. }
  161. if (props.isActive) {
  162. return `
  163. ${common}
  164. color: ${props.theme.white};
  165. background: ${props.theme.active};
  166. &:hover {
  167. color: ${props.theme.black};
  168. }
  169. `;
  170. }
  171. return `
  172. ${common}
  173. &:hover {
  174. background: ${props.theme.focus};
  175. }
  176. `;
  177. }
  178. function getChildStyles(props: MenuListItemProps & {theme: Theme}) {
  179. if (!props.noAnchor) {
  180. return '';
  181. }
  182. return `
  183. & a {
  184. ${getListItemStyles(props)}
  185. }
  186. `;
  187. }
  188. const MenuAnchor = styled('a', {
  189. shouldForwardProp: p => ['isActive', 'disabled'].includes(p) === false,
  190. })<MenuListItemProps>`
  191. ${getListItemStyles}
  192. `;
  193. const MenuListItem = styled('li')<MenuListItemProps>`
  194. display: block;
  195. ${p =>
  196. p.divider &&
  197. `
  198. height: 1px;
  199. margin: ${space(0.5)} 0;
  200. overflow: hidden;
  201. background-color: ${p.theme.innerBorder};
  202. `}
  203. ${p =>
  204. p.header &&
  205. `
  206. padding: ${space(0.25)} ${space(1)};
  207. font-size: ${p.theme.fontSizeSmall};
  208. line-height: 1.4;
  209. color: ${p.theme.gray300};
  210. `}
  211. ${getChildStyles}
  212. `;
  213. const MenuTarget = styled('span')<MenuListItemProps>`
  214. ${getListItemStyles}
  215. display: flex;
  216. `;
  217. const MenuLink = styled(Link, {
  218. shouldForwardProp: p => ['isActive', 'disabled'].includes(p) === false,
  219. })<MenuListItemProps>`
  220. ${getListItemStyles}
  221. `;
  222. export default MenuItem;