You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

DropdownMenu.tsx 9.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2023 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. import { css } from '@emotion/react';
  21. import styled from '@emotion/styled';
  22. import classNames from 'classnames';
  23. import React from 'react';
  24. import tw from 'twin.macro';
  25. import { INPUT_SIZES } from '../helpers/constants';
  26. import { translate } from '../helpers/l10n';
  27. import { themeBorder, themeColor, themeContrast } from '../helpers/theme';
  28. import { InputSizeKeys, ThemedProps } from '../types/theme';
  29. import Checkbox from './Checkbox';
  30. import { ClipboardBase } from './clipboard';
  31. import { BaseLink, LinkProps } from './Link';
  32. import NavLink from './NavLink';
  33. import RadioButton from './RadioButton';
  34. import Tooltip from './Tooltip';
  35. interface Props extends React.HtmlHTMLAttributes<HTMLMenuElement> {
  36. children?: React.ReactNode;
  37. className?: string;
  38. innerRef?: React.Ref<HTMLUListElement>;
  39. maxHeight?: string;
  40. size?: InputSizeKeys;
  41. }
  42. export function DropdownMenu({
  43. children,
  44. className,
  45. innerRef,
  46. maxHeight = 'inherit',
  47. size = 'small',
  48. ...menuProps
  49. }: Props) {
  50. return (
  51. <DropdownMenuWrapper
  52. className={classNames('dropdown-menu', className)}
  53. ref={innerRef}
  54. role="menu"
  55. style={{ '--inputSize': INPUT_SIZES[size], maxHeight }}
  56. {...menuProps}
  57. >
  58. {children}
  59. </DropdownMenuWrapper>
  60. );
  61. }
  62. interface ListItemProps {
  63. children?: React.ReactNode;
  64. className?: string;
  65. innerRef?: React.Ref<HTMLLIElement>;
  66. onFocus?: VoidFunction;
  67. onPointerEnter?: VoidFunction;
  68. onPointerLeave?: VoidFunction;
  69. }
  70. type ItemLinkProps = Omit<ListItemProps, 'innerRef'> &
  71. Pick<LinkProps, 'disabled' | 'icon' | 'onClick' | 'to'> & {
  72. innerRef?: React.Ref<HTMLAnchorElement>;
  73. };
  74. export function ItemLink(props: ItemLinkProps) {
  75. const { children, className, disabled, icon, onClick, innerRef, to, ...liProps } = props;
  76. return (
  77. <li {...liProps}>
  78. <ItemLinkStyled
  79. className={classNames(className, { disabled })}
  80. disabled={disabled}
  81. icon={icon}
  82. onClick={onClick}
  83. ref={innerRef}
  84. role="menuitem"
  85. showExternalIcon={false}
  86. to={to}
  87. >
  88. {children}
  89. </ItemLinkStyled>
  90. </li>
  91. );
  92. }
  93. interface ItemNavLinkProps extends ItemLinkProps {
  94. end?: boolean;
  95. }
  96. export function ItemNavLink(props: ItemNavLinkProps) {
  97. const { children, className, disabled, end, icon, onClick, innerRef, to, ...liProps } = props;
  98. return (
  99. <li {...liProps}>
  100. <ItemNavLinkStyled
  101. className={classNames(className, { disabled })}
  102. disabled={disabled}
  103. end={end}
  104. onClick={onClick}
  105. ref={innerRef}
  106. role="menuitem"
  107. to={to}
  108. >
  109. {icon}
  110. {children}
  111. </ItemNavLinkStyled>
  112. </li>
  113. );
  114. }
  115. interface ItemButtonProps extends ListItemProps {
  116. disabled?: boolean;
  117. icon?: React.ReactNode;
  118. onClick: React.MouseEventHandler<HTMLButtonElement>;
  119. }
  120. export function ItemButton(props: ItemButtonProps) {
  121. const { children, className, disabled, icon, innerRef, onClick, ...liProps } = props;
  122. return (
  123. <li ref={innerRef} role="none" {...liProps}>
  124. <ItemButtonStyled className={className} disabled={disabled} onClick={onClick} role="menuitem">
  125. {icon}
  126. {children}
  127. </ItemButtonStyled>
  128. </li>
  129. );
  130. }
  131. export const ItemDangerButton = styled(ItemButton)`
  132. --color: ${themeContrast('dropdownMenuDanger')};
  133. `;
  134. interface ItemCheckboxProps extends ListItemProps {
  135. checked: boolean;
  136. disabled?: boolean;
  137. id?: string;
  138. onCheck: (checked: boolean, id?: string) => void;
  139. }
  140. export function ItemCheckbox(props: ItemCheckboxProps) {
  141. const { checked, children, className, disabled, id, innerRef, onCheck, onFocus, ...liProps } =
  142. props;
  143. return (
  144. <li ref={innerRef} role="none" {...liProps}>
  145. <ItemCheckboxStyled
  146. checked={checked}
  147. className={classNames(className, { disabled })}
  148. disabled={disabled}
  149. id={id}
  150. onCheck={onCheck}
  151. onFocus={onFocus}
  152. >
  153. {children}
  154. </ItemCheckboxStyled>
  155. </li>
  156. );
  157. }
  158. interface ItemRadioButtonProps extends ListItemProps {
  159. checked: boolean;
  160. disabled?: boolean;
  161. onCheck: (value: string) => void;
  162. value: string;
  163. }
  164. export function ItemRadioButton(props: ItemRadioButtonProps) {
  165. const { checked, children, className, disabled, innerRef, onCheck, value, ...liProps } = props;
  166. return (
  167. <li ref={innerRef} role="none" {...liProps}>
  168. <ItemRadioButtonStyled
  169. checked={checked}
  170. className={classNames(className, { disabled })}
  171. disabled={disabled}
  172. onCheck={onCheck}
  173. value={value}
  174. >
  175. {children}
  176. </ItemRadioButtonStyled>
  177. </li>
  178. );
  179. }
  180. interface ItemCopyProps {
  181. children?: React.ReactNode;
  182. className?: string;
  183. copyValue: string;
  184. }
  185. export function ItemCopy(props: ItemCopyProps) {
  186. const { children, className, copyValue } = props;
  187. return (
  188. <ClipboardBase>
  189. {({ setCopyButton, copySuccess }) => (
  190. <Tooltip overlay={translate('copied_action')} visible={copySuccess}>
  191. <li role="none">
  192. <ItemButtonStyled
  193. className={className}
  194. data-clipboard-text={copyValue}
  195. ref={setCopyButton}
  196. role="menuitem"
  197. >
  198. {children}
  199. </ItemButtonStyled>
  200. </li>
  201. </Tooltip>
  202. )}
  203. </ClipboardBase>
  204. );
  205. }
  206. interface ItemDownloadProps extends ListItemProps {
  207. download: string;
  208. href: string;
  209. }
  210. export function ItemDownload(props: ItemDownloadProps) {
  211. const { children, className, download, href, innerRef, ...liProps } = props;
  212. return (
  213. <li ref={innerRef} role="none" {...liProps}>
  214. <ItemDownloadStyled
  215. className={className}
  216. download={download}
  217. href={href}
  218. rel="noopener noreferrer"
  219. role="menuitem"
  220. target="_blank"
  221. >
  222. {children}
  223. </ItemDownloadStyled>
  224. </li>
  225. );
  226. }
  227. export const ItemHeaderHighlight = styled.span`
  228. color: ${themeContrast('searchHighlight')};
  229. font-weight: 600;
  230. `;
  231. export const ItemHeader = styled.li`
  232. background-color: ${themeColor('dropdownMenuHeader')};
  233. color: ${themeContrast('dropdownMenuHeader')};
  234. ${tw`sw-py-2 sw-px-3`}
  235. `;
  236. ItemHeader.defaultProps = { className: 'dropdown-menu-header', role: 'menuitem' };
  237. export const ItemDivider = styled.li`
  238. height: 1px;
  239. background-color: ${themeColor('popupBorder')};
  240. ${tw`sw-my-1 sw--mx-2`}
  241. ${tw`sw-overflow-hidden`};
  242. `;
  243. ItemDivider.defaultProps = { role: 'separator' };
  244. const DropdownMenuWrapper = styled.ul`
  245. background-color: ${themeColor('dropdownMenu')};
  246. color: ${themeContrast('dropdownMenu')};
  247. width: var(--inputSize);
  248. list-style: none;
  249. ${tw`sw-flex sw-flex-col`}
  250. ${tw`sw-box-border`};
  251. ${tw`sw-min-w-input-small`}
  252. ${tw`sw-py-2`}
  253. ${tw`sw-body-sm`}
  254. &:focus {
  255. outline: none;
  256. }
  257. `;
  258. const itemStyle = (props: ThemedProps) => css`
  259. color: var(--color);
  260. background-color: ${themeColor('dropdownMenu')(props)};
  261. border: none;
  262. border-bottom: none;
  263. text-decoration: none;
  264. transition: none;
  265. ${tw`sw-flex sw-items-center`}
  266. ${tw`sw-body-sm`}
  267. ${tw`sw-box-border`}
  268. ${tw`sw-w-full`}
  269. ${tw`sw-text-left`}
  270. ${tw`sw-py-2 sw-px-3`}
  271. ${tw`sw-truncate`};
  272. ${tw`sw-cursor-pointer`}
  273. &.active,
  274. &:active,
  275. &.active:active,
  276. &:hover,
  277. &.active:hover {
  278. color: var(--color);
  279. background-color: ${themeColor('dropdownMenuHover')(props)};
  280. text-decoration: none;
  281. outline: none;
  282. border: none;
  283. border-bottom: none;
  284. }
  285. &:focus,
  286. &:focus-within,
  287. &.active:focus,
  288. &.active:focus-within {
  289. color: var(--color);
  290. background-color: ${themeColor('dropdownMenuFocus')(props)};
  291. text-decoration: none;
  292. outline: ${themeBorder('focus', 'dropdownMenuFocusBorder')(props)};
  293. outline-offset: -4px;
  294. border: none;
  295. border-bottom: none;
  296. }
  297. &:disabled,
  298. &.disabled {
  299. color: ${themeContrast('dropdownMenuDisabled')(props)};
  300. background-color: ${themeColor('dropdownMenuDisabled')(props)};
  301. pointer-events: none !important;
  302. ${tw`sw-cursor-not-allowed`};
  303. }
  304. & > svg {
  305. ${tw`sw-mr-2`}
  306. }
  307. `;
  308. const ItemNavLinkStyled = styled(NavLink)`
  309. --color: ${themeContrast('dropdownMenu')};
  310. ${itemStyle};
  311. `;
  312. const ItemLinkStyled = styled(BaseLink)`
  313. --color: ${themeContrast('dropdownMenu')};
  314. ${itemStyle}
  315. `;
  316. const ItemButtonStyled = styled.button`
  317. --color: ${themeContrast('dropdownMenu')};
  318. ${itemStyle}
  319. `;
  320. const ItemDownloadStyled = styled.a`
  321. --color: ${themeContrast('dropdownMenu')};
  322. ${itemStyle}
  323. `;
  324. const ItemCheckboxStyled = styled(Checkbox)`
  325. --color: ${themeContrast('dropdownMenu')};
  326. ${itemStyle}
  327. `;
  328. const ItemRadioButtonStyled = styled(RadioButton)`
  329. --color: ${themeContrast('dropdownMenu')};
  330. ${itemStyle}
  331. `;