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.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  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 onGrab = useCallback((i, ev) => {
  102. if (ev.button !== 0) {
  103. return;
  104. }
  105. setGrabbing(i);
  106. window.addEventListener('mouseup', () => {
  107. setGrabbing(_grabbing => {
  108. requestFocus(_grabbing);
  109. return null;
  110. });
  111. }, { once: true });
  112. });
  113. const onMouseOver = useCallback(i => {
  114. if (grabbing !== null && grabbing !== i) {
  115. moveAnswer(grabbing, i);
  116. setGrabbing(i);
  117. }
  118. });
  119. const autogrow = ev => {
  120. const el = ev.target;
  121. el.style.height = '1px';
  122. el.style.height = `${el.scrollHeight + 2}px`;
  123. };
  124. /* eslint-disable react/jsx-no-bind */
  125. return (<form
  126. className = 'polls-pane-content'
  127. onSubmit = { onSubmit }>
  128. <div className = 'poll-create-container poll-container'>
  129. <div className = 'poll-create-header'>
  130. { t('polls.create.create') }
  131. </div>
  132. <div className = 'poll-question-field'>
  133. <span className = 'poll-create-label'>
  134. { t('polls.create.pollQuestion') }
  135. </span>
  136. <textarea
  137. autoFocus = { true }
  138. className = 'expandable-input'
  139. maxLength = { CHAR_LIMIT }
  140. onChange = { ev => setQuestion(ev.target.value) }
  141. onInput = { autogrow }
  142. onKeyDown = { onQuestionKeyDown }
  143. placeholder = { t('polls.create.questionPlaceholder') }
  144. required = { true }
  145. row = '1'
  146. value = { question } />
  147. </div>
  148. <ol className = 'poll-answer-field-list'>
  149. {answers.map((answer, i) =>
  150. (<li
  151. className = { `poll-answer-field${grabbing === i ? ' poll-dragged' : ''}` }
  152. key = { i }
  153. onMouseOver = { () => onMouseOver(i) }>
  154. <span className = 'poll-create-label'>
  155. { t('polls.create.pollOption', { index: i + 1 })}
  156. </span>
  157. <div className = 'poll-create-option-row'>
  158. <textarea
  159. className = 'expandable-input'
  160. maxLength = { CHAR_LIMIT }
  161. onChange = { ev => setAnswer(i, ev.target.value) }
  162. onInput = { autogrow }
  163. onKeyDown = { ev => onAnswerKeyDown(i, ev) }
  164. placeholder = { t('polls.create.answerPlaceholder', { index: i + 1 }) }
  165. ref = { r => registerFieldRef(i, r) }
  166. required = { true }
  167. row = { 1 }
  168. value = { answer } />
  169. <button
  170. className = 'poll-drag-handle'
  171. onMouseDown = { ev => onGrab(i, ev) }
  172. tabIndex = '-1'
  173. type = 'button'>
  174. <Icon src = { IconMenu } />
  175. </button>
  176. </div>
  177. { answers.length > 2
  178. && <Tooltip content = { t('polls.create.removeOption') }>
  179. <button
  180. className = 'poll-remove-option-button'
  181. onClick = { () => removeAnswer(i) }
  182. type = 'button'>
  183. { t('polls.create.removeOption') }
  184. </button>
  185. </Tooltip>}
  186. </li>)
  187. )}
  188. </ol>
  189. <div className = 'poll-add-button'>
  190. <button
  191. aria-label = { 'Add option' }
  192. className = { 'poll-secondary-button' }
  193. onClick = { () => {
  194. addAnswer();
  195. requestFocus(answers.length);
  196. } }
  197. type = 'button' >
  198. <span>{t('polls.create.addOption')}</span>
  199. </button>
  200. </div>
  201. </div>
  202. <div className = 'poll-footer'>
  203. <button
  204. aria-label = { t('polls.create.cancel') }
  205. className = 'poll-small-secondary-button'
  206. onClick = { () => setCreateMode(false) }
  207. type = 'button' >
  208. <span>{t('polls.create.cancel')}</span>
  209. </button>
  210. <button
  211. aria-label = { t('polls.create.send') }
  212. className = 'poll-small-primary-button'
  213. disabled = { isSubmitDisabled }
  214. type = 'submit' >
  215. <span>{t('polls.create.send')}</span>
  216. </button>
  217. </div>
  218. </form>);
  219. };
  220. /*
  221. * We apply AbstractPollCreate to fill in the AbstractProps common
  222. * to both the web and native implementations.
  223. */
  224. // eslint-disable-next-line new-cap
  225. export default AbstractPollCreate(PollCreate);