profilingContextMenu.tsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. import {forwardRef} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Tooltip} from 'sentry/components/tooltip';
  4. import {IconCheckmark} from 'sentry/icons';
  5. import {space} from 'sentry/styles/space';
  6. interface MenuProps
  7. extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
  8. children: React.ReactNode;
  9. }
  10. const Menu = styled(
  11. forwardRef((props: MenuProps, ref: React.Ref<HTMLDivElement> | undefined) => {
  12. return <div ref={ref} role="menu" {...props} />;
  13. })
  14. )`
  15. position: absolute;
  16. font-size: ${p => p.theme.fontSizeMedium};
  17. z-index: ${p => p.theme.zIndex.dropdown};
  18. background: ${p => p.theme.backgroundElevated};
  19. border: 1px solid ${p => p.theme.border};
  20. border-radius: ${p => p.theme.borderRadius};
  21. box-shadow: ${p => p.theme.dropShadowHeavy};
  22. width: auto;
  23. min-width: 164px;
  24. overflow: auto;
  25. padding-bottom: ${space(0.5)};
  26. `;
  27. export {Menu as ProfilingContextMenu};
  28. const MenuContentContainer = styled('div')`
  29. cursor: pointer;
  30. display: flex;
  31. align-items: center;
  32. font-weight: normal;
  33. padding: 0 ${space(1)};
  34. border-radius: ${p => p.theme.borderRadius};
  35. box-sizing: border-box;
  36. background: ${p => (p.tabIndex === 0 ? p.theme.hover : undefined)};
  37. &:focus {
  38. color: ${p => p.theme.textColor};
  39. background: ${p => p.theme.hover};
  40. outline: none;
  41. }
  42. `;
  43. const MenuItemCheckboxLabel = styled('label')`
  44. display: flex;
  45. align-items: center;
  46. font-weight: normal;
  47. margin: 0;
  48. cursor: pointer;
  49. flex: 1 1 100%;
  50. `;
  51. interface MenuItemCheckboxProps
  52. extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
  53. checked?: boolean;
  54. }
  55. const MenuItemCheckbox = forwardRef(
  56. (props: MenuItemCheckboxProps, ref: React.Ref<HTMLDivElement> | undefined) => {
  57. const {children, checked, ...rest} = props;
  58. return (
  59. <MenuContentOuterContainer>
  60. <MenuContentContainer ref={ref} role="menuitem" {...rest}>
  61. <MenuItemCheckboxLabel>
  62. <MenuLeadingItem>
  63. <Input type="checkbox" checked={checked} onChange={() => void 0} />
  64. <IconCheckmark />
  65. </MenuLeadingItem>
  66. <MenuContent>{children}</MenuContent>
  67. </MenuItemCheckboxLabel>
  68. </MenuContentContainer>
  69. </MenuContentOuterContainer>
  70. );
  71. }
  72. );
  73. export {MenuItemCheckbox as ProfilingContextMenuItemCheckbox};
  74. interface MenuItemButtonProps
  75. extends React.DetailedHTMLProps<
  76. React.HTMLAttributes<HTMLButtonElement>,
  77. HTMLButtonElement
  78. > {
  79. disabled?: boolean;
  80. icon?: React.ReactNode;
  81. tooltip?: string;
  82. }
  83. const MenuItemButton = forwardRef(
  84. (props: MenuItemButtonProps, ref: React.Ref<HTMLButtonElement> | undefined) => {
  85. const {children, tooltip, ...rest} = props;
  86. return (
  87. <MenuContentOuterContainer>
  88. <Tooltip title={tooltip}>
  89. <MenuButton disabled={props.disabled} ref={ref} role="menuitem" {...rest}>
  90. {props.icon ? <MenuLeadingItem>{props.icon}</MenuLeadingItem> : null}
  91. {children}
  92. </MenuButton>
  93. </Tooltip>
  94. </MenuContentOuterContainer>
  95. );
  96. }
  97. );
  98. export {MenuItemButton as ProfilingContextMenuItemButton};
  99. const MenuButton = styled('button')`
  100. border: none;
  101. display: flex;
  102. flex: 1;
  103. align-items: center;
  104. padding: ${space(0.5)} ${space(1)};
  105. border-radius: ${p => p.theme.borderRadius};
  106. box-sizing: border-box;
  107. background: ${p => (p.tabIndex === 0 ? p.theme.hover : 'transparent')} !important;
  108. pointer-events: ${p => (p.disabled ? 'none' : undefined)};
  109. opacity: ${p => (p.disabled ? 0.7 : undefined)};
  110. &:focus {
  111. color: ${p => p.theme.textColor};
  112. background: ${p => p.theme.hover};
  113. outline: none;
  114. }
  115. svg {
  116. margin-right: ${space(0.5)};
  117. }
  118. `;
  119. const MenuLeadingItem = styled('div')`
  120. display: flex;
  121. align-items: center;
  122. height: 1.4em;
  123. width: 1em;
  124. gap: ${space(1)};
  125. padding: ${space(1)} 0;
  126. position: relative;
  127. `;
  128. const MenuContent = styled('div')`
  129. position: relative;
  130. width: 100%;
  131. display: flex;
  132. gap: ${space(0.5)};
  133. justify-content: space-between;
  134. padding: ${space(0.5)} 0;
  135. margin-left: ${space(0.5)};
  136. text-transform: capitalize;
  137. margin-bottom: 0;
  138. line-height: 1.4;
  139. white-space: nowrap;
  140. overflow: hidden;
  141. text-overflow: ellipsis;
  142. `;
  143. const Input = styled('input')`
  144. position: absolute;
  145. opacity: 0;
  146. cursor: pointer;
  147. height: 0;
  148. padding-right: ${space(1)};
  149. & + svg {
  150. position: absolute;
  151. left: 50%;
  152. top: 50%;
  153. transform: translate(-50%, -50%);
  154. width: 1em;
  155. height: 1.4em;
  156. display: none;
  157. }
  158. &:checked + svg {
  159. display: block;
  160. }
  161. `;
  162. interface MenuItemProps
  163. extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
  164. children: React.ReactNode;
  165. }
  166. const MenuItem = styled(
  167. forwardRef((props: MenuItemProps, ref: React.Ref<HTMLDivElement> | undefined) => {
  168. const {children, ...rest} = props;
  169. return (
  170. <MenuContentOuterContainer>
  171. <MenuContentContainer ref={ref} role="menuitem" {...rest}>
  172. <MenuContent>{children}</MenuContent>
  173. </MenuContentContainer>
  174. </MenuContentOuterContainer>
  175. );
  176. })
  177. )`
  178. cursor: pointer;
  179. color: ${p => p.theme.textColor};
  180. background: transparent;
  181. padding: 0 ${space(0.5)};
  182. &:focus {
  183. outline: none;
  184. }
  185. &:active: {
  186. background: transparent;
  187. }
  188. `;
  189. export {MenuItem as ProfilingContextMenuItem};
  190. const MenuContentOuterContainer = styled('div')`
  191. padding: 0 ${space(0.5)};
  192. display: flex;
  193. > span {
  194. display: flex;
  195. }
  196. > div,
  197. > span,
  198. button {
  199. flex: 1;
  200. }
  201. `;
  202. const MenuGroup = styled('div')`
  203. padding-top: 0;
  204. padding-bottom: ${space(1)};
  205. &:last-of-type {
  206. padding-bottom: 0;
  207. }
  208. `;
  209. export {MenuGroup as ProfilingContextMenuGroup};
  210. interface MenuHeadingProps
  211. extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
  212. children: React.ReactNode;
  213. }
  214. const MenuHeading = styled((props: MenuHeadingProps) => {
  215. const {children, ...rest} = props;
  216. return <div {...rest}>{children}</div>;
  217. })`
  218. text-transform: uppercase;
  219. line-height: 1.5;
  220. font-weight: 600;
  221. color: ${p => p.theme.subText};
  222. margin-bottom: 0;
  223. cursor: default;
  224. font-size: 75%;
  225. padding: ${space(0.5)} ${space(1.5)};
  226. `;
  227. export {MenuHeading as ProfilingContextMenuHeading};
  228. const Layer = styled('div')`
  229. width: 100%;
  230. height: 100%;
  231. position: absolute;
  232. left: 0;
  233. top: 0;
  234. z-index: ${p => p.theme.zIndex.dropdown - 1};
  235. `;
  236. export {Layer as ProfilingContextMenuLayer};