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

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