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.

ContextMenuItem.tsx 6.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. import React, { ReactNode, useCallback } from 'react';
  2. import { useSelector } from 'react-redux';
  3. import { makeStyles } from 'tss-react/mui';
  4. import { showOverflowDrawer } from '../../../../toolbox/functions.web';
  5. import Icon from '../../../icons/components/Icon';
  6. import { withPixelLineHeight } from '../../../styles/functions.web';
  7. import { TEXT_OVERFLOW_TYPES } from '../../constants.any';
  8. import TextWithOverflow from './TextWithOverflow';
  9. export interface IProps {
  10. /**
  11. * Label used for accessibility.
  12. */
  13. accessibilityLabel: string;
  14. /**
  15. * The context menu item background color.
  16. */
  17. backgroundColor?: string;
  18. /**
  19. * Component children.
  20. */
  21. children?: ReactNode;
  22. /**
  23. * CSS class name used for custom styles.
  24. */
  25. className?: string;
  26. /**
  27. * Id of dom element controlled by this item. Matches aria-controls.
  28. * Useful if you need this item as a tab element.
  29. *
  30. */
  31. controls?: string;
  32. /**
  33. * Custom icon. If used, the icon prop is ignored.
  34. * Used to allow custom children instead of just the default icons.
  35. */
  36. customIcon?: ReactNode;
  37. /**
  38. * Whether or not the action is disabled.
  39. */
  40. disabled?: boolean;
  41. /**
  42. * Default icon for action.
  43. */
  44. icon?: Function;
  45. /**
  46. * Id of the action container.
  47. */
  48. id?: string;
  49. /**
  50. * Click handler.
  51. */
  52. onClick?: (e?: React.MouseEvent<any>) => void;
  53. /**
  54. * Keydown handler.
  55. */
  56. onKeyDown?: (e: React.KeyboardEvent<HTMLDivElement>) => void;
  57. /**
  58. * Keypress handler.
  59. */
  60. onKeyPress?: (e?: React.KeyboardEvent) => void;
  61. /**
  62. * Overflow type for item text.
  63. */
  64. overflowType?: TEXT_OVERFLOW_TYPES;
  65. /**
  66. * You can use this item as a tab. Defaults to button if not set.
  67. *
  68. * If no onClick handler is provided, we assume the context menu item is
  69. * not interactive and no role will be set.
  70. */
  71. role?: 'tab' | 'button' | 'menuitem';
  72. /**
  73. * Whether the item is marked as selected.
  74. */
  75. selected?: boolean;
  76. /**
  77. * TestId of the element, if any.
  78. */
  79. testId?: string;
  80. /**
  81. * Action text.
  82. */
  83. text?: string;
  84. /**
  85. * Class name for the text.
  86. */
  87. textClassName?: string;
  88. }
  89. const useStyles = makeStyles()(theme => {
  90. return {
  91. contextMenuItem: {
  92. alignItems: 'center',
  93. cursor: 'pointer',
  94. display: 'flex',
  95. minHeight: '40px',
  96. padding: '10px 16px',
  97. boxSizing: 'border-box',
  98. '& > *:not(:last-child)': {
  99. marginRight: theme.spacing(3)
  100. },
  101. '&:hover': {
  102. backgroundColor: theme.palette.ui02
  103. },
  104. '&:active': {
  105. backgroundColor: theme.palette.ui03
  106. },
  107. '&.focus-visible': {
  108. boxShadow: `inset 0 0 0 2px ${theme.palette.action01Hover}`
  109. }
  110. },
  111. selected: {
  112. borderLeft: `3px solid ${theme.palette.action01Hover}`,
  113. paddingLeft: '13px',
  114. backgroundColor: theme.palette.ui02
  115. },
  116. contextMenuItemDisabled: {
  117. pointerEvents: 'none'
  118. },
  119. contextMenuItemIconDisabled: {
  120. '& svg': {
  121. fill: `${theme.palette.text03} !important`
  122. }
  123. },
  124. contextMenuItemLabelDisabled: {
  125. color: theme.palette.text03,
  126. '&:hover': {
  127. background: 'none'
  128. },
  129. '& svg': {
  130. fill: theme.palette.text03
  131. }
  132. },
  133. contextMenuItemDrawer: {
  134. padding: '13px 16px'
  135. },
  136. contextMenuItemIcon: {
  137. '& svg': {
  138. fill: theme.palette.icon01
  139. }
  140. },
  141. text: {
  142. ...withPixelLineHeight(theme.typography.bodyShortRegular),
  143. color: theme.palette.text01
  144. },
  145. drawerText: {
  146. ...withPixelLineHeight(theme.typography.bodyShortRegularLarge)
  147. }
  148. };
  149. });
  150. const ContextMenuItem = ({
  151. accessibilityLabel,
  152. backgroundColor,
  153. children,
  154. className,
  155. controls,
  156. customIcon,
  157. disabled,
  158. id,
  159. icon,
  160. onClick,
  161. onKeyDown,
  162. onKeyPress,
  163. overflowType,
  164. role = 'button',
  165. selected,
  166. testId,
  167. text,
  168. textClassName }: IProps) => {
  169. const { classes: styles, cx } = useStyles();
  170. const _overflowDrawer: boolean = useSelector(showOverflowDrawer);
  171. const style = backgroundColor ? { backgroundColor } : {};
  172. const onKeyPressHandler = useCallback(e => {
  173. // only trigger the fallback behavior (onClick) if we dont have any explicit keyboard event handler
  174. if (onClick && !onKeyPress && !onKeyDown && (e.key === 'Enter' || e.key === ' ')) {
  175. e.preventDefault();
  176. onClick(e);
  177. }
  178. if (onKeyPress) {
  179. onKeyPress(e);
  180. }
  181. }, [ onClick, onKeyPress, onKeyDown ]);
  182. let tabIndex: undefined | 0 | -1;
  183. if (role === 'tab') {
  184. tabIndex = selected ? 0 : -1;
  185. }
  186. if (role === 'button' && !disabled) {
  187. tabIndex = 0;
  188. }
  189. return (
  190. <div
  191. aria-controls = { controls }
  192. aria-disabled = { disabled }
  193. aria-label = { accessibilityLabel }
  194. aria-selected = { role === 'tab' ? selected : undefined }
  195. className = { cx(styles.contextMenuItem,
  196. _overflowDrawer && styles.contextMenuItemDrawer,
  197. disabled && styles.contextMenuItemDisabled,
  198. selected && styles.selected,
  199. className
  200. ) }
  201. data-testid = { testId }
  202. id = { id }
  203. key = { text }
  204. onClick = { disabled ? undefined : onClick }
  205. onKeyDown = { disabled ? undefined : onKeyDown }
  206. onKeyPress = { disabled ? undefined : onKeyPressHandler }
  207. role = { onClick ? role : undefined }
  208. style = { style }
  209. tabIndex = { onClick ? tabIndex : undefined }>
  210. {customIcon ? customIcon
  211. : icon && <Icon
  212. className = { cx(styles.contextMenuItemIcon,
  213. disabled && styles.contextMenuItemIconDisabled) }
  214. size = { 20 }
  215. src = { icon } />}
  216. {text && (
  217. <TextWithOverflow
  218. className = { cx(styles.text,
  219. _overflowDrawer && styles.drawerText,
  220. disabled && styles.contextMenuItemLabelDisabled,
  221. textClassName) }
  222. overflowType = { overflowType } >
  223. {text}
  224. </TextWithOverflow>
  225. )}
  226. {children}
  227. </div>
  228. );
  229. };
  230. export default ContextMenuItem;