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.

PollCreate.js 8.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. // @flow
  2. import React, { useCallback, useEffect, useRef, useState } from 'react';
  3. import { Icon, IconMenu } from '../../../base/icons';
  4. import { Tooltip } from '../../../base/tooltip';
  5. import { CHAR_LIMIT } from '../../constants';
  6. import AbstractPollCreate from '../AbstractPollCreate';
  7. import type { AbstractProps } from '../AbstractPollCreate';
  8. const PollCreate = (props: AbstractProps) => {
  9. const {
  10. addAnswer,
  11. answers,
  12. isSubmitDisabled,
  13. moveAnswer,
  14. onSubmit,
  15. question,
  16. removeAnswer,
  17. setAnswer,
  18. setCreateMode,
  19. setQuestion,
  20. t
  21. } = props;
  22. /*
  23. * This ref stores the Array of answer input fields, allowing us to focus on them.
  24. * This array is maintained by registerfieldRef and the useEffect below.
  25. */
  26. const answerInputs = useRef([]);
  27. const registerFieldRef = useCallback((i, r) => {
  28. if (r === null) {
  29. return;
  30. }
  31. answerInputs.current[i] = r;
  32. }, [ answerInputs ]);
  33. useEffect(() => {
  34. answerInputs.current = answerInputs.current.slice(0, answers.length);
  35. }, [ answers ]);
  36. /*
  37. * This state allows us to requestFocus asynchronously, without having to worry
  38. * about whether a newly created input field has been rendered yet or not.
  39. */
  40. const [ lastFocus, requestFocus ] = useState(null);
  41. useEffect(() => {
  42. if (lastFocus === null) {
  43. return;
  44. }
  45. const input = answerInputs.current[lastFocus];
  46. if (input === undefined) {
  47. return;
  48. }
  49. input.focus();
  50. }, [ lastFocus ]);
  51. const checkModifiers = useCallback(ev => {
  52. // Because this isn't done automatically on MacOS
  53. if (ev.key === 'Enter' && ev.metaKey) {
  54. ev.preventDefault();
  55. onSubmit();
  56. return;
  57. }
  58. if (ev.ctrlKey || ev.metaKey || ev.altKey || ev.shiftKey) {
  59. return;
  60. }
  61. });
  62. const onQuestionKeyDown = useCallback(ev => {
  63. if (checkModifiers(ev)) {
  64. return;
  65. }
  66. if (ev.key === 'Enter') {
  67. requestFocus(0);
  68. ev.preventDefault();
  69. }
  70. });
  71. // Called on keypress in answer fields
  72. const onAnswerKeyDown = useCallback((i, ev) => {
  73. if (checkModifiers(ev)) {
  74. return;
  75. }
  76. if (ev.key === 'Enter') {
  77. addAnswer(i + 1);
  78. requestFocus(i + 1);
  79. ev.preventDefault();
  80. } else if (ev.key === 'Backspace' && ev.target.value === '' && answers.length > 1) {
  81. removeAnswer(i);
  82. requestFocus(i > 0 ? i - 1 : 0);
  83. ev.preventDefault();
  84. } else if (ev.key === 'ArrowDown') {
  85. if (i === answers.length - 1) {
  86. addAnswer();
  87. }
  88. requestFocus(i + 1);
  89. ev.preventDefault();
  90. } else if (ev.key === 'ArrowUp') {
  91. if (i === 0) {
  92. addAnswer(0);
  93. requestFocus(0);
  94. } else {
  95. requestFocus(i - 1);
  96. }
  97. ev.preventDefault();
  98. }
  99. }, [ answers, addAnswer, removeAnswer, requestFocus ]);
  100. const [ grabbing, setGrabbing ] = useState(null);
  101. const interchangeHeights = (i, j) => {
  102. const h = answerInputs.current[i].scrollHeight;
  103. answerInputs.current[i].style.height = `${answerInputs.current[j].scrollHeight}px`;
  104. answerInputs.current[j].style.height = `${h}px`;
  105. };
  106. const onGrab = useCallback((i, ev) => {
  107. if (ev.button !== 0) {
  108. return;
  109. }
  110. setGrabbing(i);
  111. window.addEventListener('mouseup', () => {
  112. setGrabbing(_grabbing => {
  113. requestFocus(_grabbing);
  114. return null;
  115. });
  116. }, { once: true });
  117. });
  118. const onMouseOver = useCallback(i => {
  119. if (grabbing !== null && grabbing !== i) {
  120. interchangeHeights(i, grabbing);
  121. moveAnswer(grabbing, i);
  122. setGrabbing(i);
  123. }
  124. });
  125. const autogrow = ev => {
  126. const el = ev.target;
  127. el.style.height = '1px';
  128. el.style.height = `${el.scrollHeight + 2}px`;
  129. };
  130. /* eslint-disable react/jsx-no-bind */
  131. return (<form
  132. className = 'polls-pane-content'
  133. onSubmit = { onSubmit }>
  134. <div className = 'poll-create-container poll-container'>
  135. <div className = 'poll-create-header'>
  136. { t('polls.create.create') }
  137. </div>
  138. <div className = 'poll-question-field'>
  139. <span className = 'poll-create-label'>
  140. { t('polls.create.pollQuestion') }
  141. </span>
  142. <textarea
  143. autoFocus = { true }
  144. className = 'expandable-input'
  145. maxLength = { CHAR_LIMIT }
  146. onChange = { ev => setQuestion(ev.target.value) }
  147. onInput = { autogrow }
  148. onKeyDown = { onQuestionKeyDown }
  149. placeholder = { t('polls.create.questionPlaceholder') }
  150. required = { true }
  151. row = '1'
  152. value = { question } />
  153. </div>
  154. <ol className = 'poll-answer-field-list'>
  155. {answers.map((answer, i) =>
  156. (<li
  157. className = { `poll-answer-field${grabbing === i ? ' poll-dragged' : ''}` }
  158. key = { i }
  159. onMouseOver = { () => onMouseOver(i) }>
  160. <span className = 'poll-create-label'>
  161. { t('polls.create.pollOption', { index: i + 1 })}
  162. </span>
  163. <div className = 'poll-create-option-row'>
  164. <textarea
  165. className = 'expandable-input'
  166. maxLength = { CHAR_LIMIT }
  167. onChange = { ev => setAnswer(i, ev.target.value) }
  168. onInput = { autogrow }
  169. onKeyDown = { ev => onAnswerKeyDown(i, ev) }
  170. placeholder = { t('polls.create.answerPlaceholder', { index: i + 1 }) }
  171. ref = { r => registerFieldRef(i, r) }
  172. required = { true }
  173. row = { 1 }
  174. value = { answer } />
  175. <button
  176. className = 'poll-drag-handle'
  177. onMouseDown = { ev => onGrab(i, ev) }
  178. tabIndex = '-1'
  179. type = 'button'>
  180. <Icon src = { IconMenu } />
  181. </button>
  182. </div>
  183. { answers.length > 2
  184. && <Tooltip content = { t('polls.create.removeOption') }>
  185. <button
  186. className = 'poll-remove-option-button'
  187. onClick = { () => removeAnswer(i) }
  188. type = 'button'>
  189. { t('polls.create.removeOption') }
  190. </button>
  191. </Tooltip>}
  192. </li>)
  193. )}
  194. </ol>
  195. <div className = 'poll-add-button'>
  196. <button
  197. aria-label = { 'Add option' }
  198. className = 'poll-button poll-button-secondary'
  199. onClick = { () => {
  200. addAnswer();
  201. requestFocus(answers.length);
  202. } }
  203. type = 'button' >
  204. <span>{t('polls.create.addOption')}</span>
  205. </button>
  206. </div>
  207. </div>
  208. <div className = 'poll-footer poll-create-footer'>
  209. <button
  210. aria-label = { t('polls.create.cancel') }
  211. className = 'poll-button poll-button-secondary poll-button-short'
  212. onClick = { () => setCreateMode(false) }
  213. type = 'button' >
  214. <span>{t('polls.create.cancel')}</span>
  215. </button>
  216. <button
  217. aria-label = { t('polls.create.send') }
  218. className = 'poll-button poll-button-primary poll-button-short'
  219. disabled = { isSubmitDisabled }
  220. type = 'submit' >
  221. <span>{t('polls.create.send')}</span>
  222. </button>
  223. </div>
  224. </form>);
  225. };
  226. /*
  227. * We apply AbstractPollCreate to fill in the AbstractProps common
  228. * to both the web and native implementations.
  229. */
  230. // eslint-disable-next-line new-cap
  231. export default AbstractPollCreate(PollCreate);