popper.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. import {Dispatch, RefObject, SetStateAction, useEffect, useState} from 'react';
  2. import {Popper} from 'react-popper';
  3. import styled from '@emotion/styled';
  4. import Code from 'docs-ui/components/code';
  5. import SelectField from 'sentry/components/forms/selectField';
  6. import space from 'sentry/styles/space';
  7. import BooleanField from 'sentry/views/settings/components/forms/booleanField';
  8. import {iconProps} from './data';
  9. import IconSample from './sample';
  10. import {ExtendedIconData, SelectedIcon} from './searchPanel';
  11. type Props = {
  12. icon: ExtendedIconData;
  13. setSelectedIcon: Dispatch<SetStateAction<SelectedIcon>>;
  14. boxRef: RefObject<HTMLDivElement>;
  15. };
  16. const IconPopper = ({icon, setSelectedIcon, boxRef}: Props) => {
  17. /**
  18. * Editable icon props
  19. */
  20. const [size, setSize] = useState(iconProps.size.default);
  21. const [direction, setDirection] = useState(
  22. icon.defaultProps?.direction ?? iconProps.direction.default
  23. );
  24. const [type, setType] = useState(icon.defaultProps?.type ?? iconProps.type.default);
  25. const [isCircled, setIsCircled] = useState(icon.defaultProps?.isCircled ?? false);
  26. const [isSolid, setIsSolid] = useState(icon.defaultProps?.isSolid ?? false);
  27. /**
  28. * Generate and update code sample based on prop states
  29. */
  30. const getCodeSample = () => {
  31. return `<Icon${icon.name} color="gray500" size="${size}"${
  32. isCircled ? ' isCircled' : ' '
  33. }${isSolid ? ' isSolid' : ' '} />`;
  34. };
  35. const [codeSample, setCodeSample] = useState(getCodeSample());
  36. useEffect(() => {
  37. setCodeSample(getCodeSample());
  38. }, [size, isCircled, isSolid]);
  39. /**
  40. * Deselect icon box on outside click
  41. */
  42. const clickAwayHandler = e => {
  43. if (e.target !== boxRef && !boxRef?.contains?.(e.target)) {
  44. setSelectedIcon({group: '', icon: ''});
  45. }
  46. };
  47. useEffect(() => {
  48. document.addEventListener('click', clickAwayHandler);
  49. return () => document.removeEventListener('click', clickAwayHandler);
  50. }, []);
  51. return (
  52. <Popper
  53. placement="bottom"
  54. modifiers={{
  55. offset: {offset: '0, 10'},
  56. flip: {enabled: false},
  57. }}
  58. >
  59. {({ref: popperRef, style, placement}) => {
  60. return (
  61. <Wrap
  62. ref={popperRef}
  63. style={style}
  64. data-placement={placement}
  65. /**
  66. * Prevent click event from propagating up to <IconInfoBox />,
  67. * otherwise it would trigger setSelectedIcon and deselect the icon box.
  68. */
  69. onClick={e => e.stopPropagation()}
  70. >
  71. <SampleWrap>
  72. <IconSample
  73. name={icon.name}
  74. size={size}
  75. color="gray500"
  76. {...(icon.additionalProps?.includes('type') ? {type} : {})}
  77. {...(icon.additionalProps?.includes('direction') ? {direction} : {})}
  78. {...(isCircled ? {isCircled} : {})}
  79. {...(isSolid ? {isSolid} : {})}
  80. />
  81. </SampleWrap>
  82. <PropsWrap>
  83. <SelectorWrap>
  84. <SelectorLabel>Size</SelectorLabel>
  85. <StyledSelectField
  86. name="size"
  87. defaultValue={iconProps.size.default}
  88. choices={iconProps.size.options}
  89. onChange={value => setSize(value as string)}
  90. clearable={false}
  91. />
  92. </SelectorWrap>
  93. {icon.additionalProps?.includes('direction') && (
  94. <SelectorWrap>
  95. <SelectorLabel>Direction</SelectorLabel>
  96. <StyledSelectField
  97. name="type"
  98. defaultValue={direction}
  99. choices={iconProps.direction.options}
  100. onChange={value => setDirection(value as string)}
  101. clearable={false}
  102. />
  103. </SelectorWrap>
  104. )}
  105. {icon.additionalProps?.includes('type') && (
  106. <SelectorWrap>
  107. <SelectorLabel>Type</SelectorLabel>
  108. <StyledSelectField
  109. name="type"
  110. defaultValue={type}
  111. choices={iconProps.type.options}
  112. onChange={value => setType(value as string)}
  113. clearable={false}
  114. />
  115. </SelectorWrap>
  116. )}
  117. {icon.additionalProps?.includes('isCircled') && (
  118. <StyledBooleanField
  119. name="isCircled"
  120. label="Circled"
  121. value={isCircled}
  122. onChange={value => setIsCircled(value)}
  123. />
  124. )}
  125. {icon.additionalProps?.includes('isSolid') && (
  126. <StyledBooleanField
  127. name="isSolid"
  128. label="Solid"
  129. value={isSolid}
  130. onChange={value => setIsSolid(value)}
  131. />
  132. )}
  133. </PropsWrap>
  134. <Code className="language-jsx">{codeSample}</Code>
  135. </Wrap>
  136. );
  137. }}
  138. </Popper>
  139. );
  140. };
  141. export default IconPopper;
  142. const Wrap = styled('div')`
  143. text-align: left;
  144. max-width: 20rem;
  145. padding: ${space(2)};
  146. padding-bottom: 0;
  147. background: ${p => p.theme.background};
  148. border: solid 1px ${p => p.theme.border};
  149. box-shadow: ${p => p.theme.dropShadowHeavy};
  150. border-radius: ${p => p.theme.borderRadius};
  151. z-index: ${p => p.theme.zIndex.modal};
  152. cursor: initial;
  153. `;
  154. const SampleWrap = styled('div')`
  155. width: 100%;
  156. height: 4rem;
  157. display: flex;
  158. justify-content: center;
  159. align-items: center;
  160. margin: 0 auto ${space(3)};
  161. `;
  162. const PropsWrap = styled('div')``;
  163. const SelectorWrap = styled('div')`
  164. display: flex;
  165. align-items: center;
  166. justify-content: space-between;
  167. &:not(:first-of-type) {
  168. padding-top: ${space(2)};
  169. }
  170. &:not(:last-of-type) {
  171. padding-bottom: ${space(2)};
  172. border-bottom: solid 1px ${p => p.theme.innerBorder};
  173. }
  174. `;
  175. const SelectorLabel = styled('p')`
  176. margin-bottom: 0;
  177. `;
  178. const StyledSelectField = styled(SelectField)`
  179. width: 50%;
  180. padding-left: 10px;
  181. `;
  182. const StyledBooleanField = styled(BooleanField)`
  183. padding-left: 0;
  184. `;