Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

PollCreate.tsx 8.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. /* eslint-disable lines-around-comment */
  2. import React, { useCallback, useEffect, useRef, useState } from 'react';
  3. import { makeStyles } from 'tss-react/mui';
  4. // @ts-ignore
  5. import { Tooltip } from '../../../base/tooltip';
  6. import Button from '../../../base/ui/components/web/Button';
  7. import { BUTTON_TYPES } from '../../../base/ui/constants.web';
  8. import { ANSWERS_LIMIT, CHAR_LIMIT } from '../../constants';
  9. // @ts-ignore
  10. import AbstractPollCreate from '../AbstractPollCreate';
  11. // @ts-ignore
  12. import type { AbstractProps } from '../AbstractPollCreate';
  13. const useStyles = makeStyles()(theme => {
  14. return {
  15. buttonMargin: {
  16. marginRight: theme.spacing(2)
  17. }
  18. };
  19. });
  20. const PollCreate = ({
  21. addAnswer,
  22. answers,
  23. isSubmitDisabled,
  24. onSubmit,
  25. question,
  26. removeAnswer,
  27. setAnswer,
  28. setCreateMode,
  29. setQuestion,
  30. t
  31. }: AbstractProps) => {
  32. const { classes: styles } = useStyles();
  33. /*
  34. * This ref stores the Array of answer input fields, allowing us to focus on them.
  35. * This array is maintained by registerfieldRef and the useEffect below.
  36. */
  37. const answerInputs = useRef<Array<HTMLInputElement>>([]);
  38. const registerFieldRef = useCallback((i, r) => {
  39. if (r === null) {
  40. return;
  41. }
  42. answerInputs.current[i] = r;
  43. }, [ answerInputs ]);
  44. useEffect(() => {
  45. answerInputs.current = answerInputs.current.slice(0, answers.length);
  46. }, [ answers ]);
  47. /*
  48. * This state allows us to requestFocus asynchronously, without having to worry
  49. * about whether a newly created input field has been rendered yet or not.
  50. */
  51. const [ lastFocus, requestFocus ] = useState<number | null>(null);
  52. useEffect(() => {
  53. if (lastFocus === null) {
  54. return;
  55. }
  56. const input = answerInputs.current[lastFocus];
  57. if (input === undefined) {
  58. return;
  59. }
  60. input.focus();
  61. }, [ lastFocus ]);
  62. const checkModifiers = useCallback(ev => {
  63. // Composition events used to add accents to characters
  64. // despite their absence from standard US keyboards,
  65. // to build up logograms of many Asian languages
  66. // from their base components or categories and so on.
  67. if (ev.isComposing || ev.keyCode === 229) {
  68. // keyCode 229 means that user pressed some button,
  69. // but input method is still processing that.
  70. // This is a standard behavior for some input methods
  71. // like entering japanese or сhinese hieroglyphs.
  72. return true;
  73. }
  74. // Because this isn't done automatically on MacOS
  75. if (ev.key === 'Enter' && ev.metaKey) {
  76. ev.preventDefault();
  77. onSubmit();
  78. return;
  79. }
  80. if (ev.ctrlKey || ev.metaKey || ev.altKey || ev.shiftKey) {
  81. return;
  82. }
  83. }, []);
  84. const onQuestionKeyDown = useCallback(ev => {
  85. if (checkModifiers(ev)) {
  86. return;
  87. }
  88. if (ev.key === 'Enter') {
  89. requestFocus(0);
  90. ev.preventDefault();
  91. }
  92. }, []);
  93. // Called on keypress in answer fields
  94. const onAnswerKeyDown = useCallback((i, ev) => {
  95. if (checkModifiers(ev)) {
  96. return;
  97. }
  98. if (ev.key === 'Enter') {
  99. // We add a new option input
  100. // only if we are on the last option input
  101. if (i === answers.length - 1) {
  102. addAnswer(i + 1);
  103. }
  104. requestFocus(i + 1);
  105. ev.preventDefault();
  106. } else if (ev.key === 'Backspace' && ev.target.value === '' && answers.length > 1) {
  107. removeAnswer(i);
  108. requestFocus(i > 0 ? i - 1 : 0);
  109. ev.preventDefault();
  110. } else if (ev.key === 'ArrowDown') {
  111. if (i === answers.length - 1) {
  112. addAnswer();
  113. }
  114. requestFocus(i + 1);
  115. ev.preventDefault();
  116. } else if (ev.key === 'ArrowUp') {
  117. if (i === 0) {
  118. addAnswer(0);
  119. requestFocus(0);
  120. } else {
  121. requestFocus(i - 1);
  122. }
  123. ev.preventDefault();
  124. }
  125. }, [ answers, addAnswer, removeAnswer, requestFocus ]);
  126. const autogrow = (ev: React.ChangeEvent<HTMLTextAreaElement>) => {
  127. const el = ev.target;
  128. el.style.height = '1px';
  129. el.style.height = `${el.scrollHeight + 2}px`;
  130. };
  131. /* eslint-disable react/jsx-no-bind */
  132. return (<form
  133. className = 'polls-pane-content'
  134. onSubmit = { onSubmit }>
  135. <div className = 'poll-create-container poll-container'>
  136. <div className = 'poll-create-header'>
  137. { t('polls.create.create') }
  138. </div>
  139. <div className = 'poll-question-field'>
  140. <span className = 'poll-create-label'>
  141. { t('polls.create.pollQuestion') }
  142. </span>
  143. <textarea
  144. autoFocus = { true }
  145. className = 'expandable-input'
  146. maxLength = { CHAR_LIMIT }
  147. onChange = { ev => setQuestion(ev.target.value) }
  148. onInput = { autogrow }
  149. onKeyDown = { onQuestionKeyDown }
  150. placeholder = { t('polls.create.questionPlaceholder') }
  151. required = { true }
  152. rows = { 1 }
  153. value = { question } />
  154. </div>
  155. <ol className = 'poll-answer-field-list'>
  156. {answers.map((answer: any, i: number) =>
  157. (<li
  158. className = 'poll-answer-field'
  159. key = { 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. rows = { 1 }
  174. value = { answer } />
  175. </div>
  176. { answers.length > 2
  177. && <Tooltip content = { t('polls.create.removeOption') }>
  178. <button
  179. className = 'poll-remove-option-button'
  180. onClick = { () => removeAnswer(i) }
  181. type = 'button'>
  182. { t('polls.create.removeOption') }
  183. </button>
  184. </Tooltip>}
  185. </li>)
  186. )}
  187. </ol>
  188. <div className = 'poll-add-button'>
  189. <Button
  190. accessibilityLabel = { t('polls.create.addOption') }
  191. disabled = { answers.length >= ANSWERS_LIMIT }
  192. fullWidth = { true }
  193. labelKey = { 'polls.create.addOption' }
  194. onClick = { () => {
  195. addAnswer();
  196. requestFocus(answers.length);
  197. } }
  198. type = { BUTTON_TYPES.SECONDARY } />
  199. </div>
  200. </div>
  201. <div className = 'poll-footer poll-create-footer'>
  202. <Button
  203. accessibilityLabel = { t('polls.create.cancel') }
  204. className = { styles.buttonMargin }
  205. fullWidth = { true }
  206. labelKey = { 'polls.create.cancel' }
  207. onClick = { () => setCreateMode(false) }
  208. type = { BUTTON_TYPES.SECONDARY } />
  209. <Button
  210. accessibilityLabel = { t('polls.create.send') }
  211. disabled = { isSubmitDisabled }
  212. fullWidth = { true }
  213. isSubmit = { true }
  214. labelKey = { 'polls.create.send' } />
  215. </div>
  216. </form>);
  217. };
  218. /*
  219. * We apply AbstractPollCreate to fill in the AbstractProps common
  220. * to both the web and native implementations.
  221. */
  222. // eslint-disable-next-line new-cap
  223. export default AbstractPollCreate(PollCreate);