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.tsx 8.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
  2. import { FlatList, Platform, TextInput, View, ViewStyle } from 'react-native';
  3. import { Divider } from 'react-native-paper';
  4. import { useDispatch } from 'react-redux';
  5. import Button from '../../../base/ui/components/native/Button';
  6. import Input from '../../../base/ui/components/native/Input';
  7. import { BUTTON_TYPES } from '../../../base/ui/constants.native';
  8. import { editPoll } from '../../actions';
  9. import { ANSWERS_LIMIT, CHAR_LIMIT } from '../../constants';
  10. import AbstractPollCreate, { AbstractProps } from '../AbstractPollCreate';
  11. import { dialogStyles, pollsStyles } from './styles';
  12. const PollCreate = (props: AbstractProps) => {
  13. const {
  14. addAnswer,
  15. answers,
  16. editingPoll,
  17. editingPollId,
  18. isSubmitDisabled,
  19. onSubmit,
  20. question,
  21. removeAnswer,
  22. setAnswer,
  23. setCreateMode,
  24. setQuestion,
  25. t
  26. } = props;
  27. const answerListRef = useRef<FlatList>(null);
  28. const dispatch = useDispatch();
  29. /*
  30. * This ref stores the Array of answer input fields, allowing us to focus on them.
  31. * This array is maintained by registerFieldRef and the useEffect below.
  32. */
  33. const answerInputs = useRef<TextInput[]>([]);
  34. const registerFieldRef = useCallback((i, input) => {
  35. if (input === null) {
  36. return;
  37. }
  38. answerInputs.current[i] = input;
  39. }, [ answerInputs ]);
  40. useEffect(() => {
  41. answerInputs.current = answerInputs.current.slice(0, answers.length);
  42. }, [ answers ]);
  43. /*
  44. * This state allows us to requestFocus asynchronously, without having to worry
  45. * about whether a newly created input field has been rendered yet or not.
  46. */
  47. const [ lastFocus, requestFocus ] = useState<number | null>(null);
  48. const { PRIMARY, SECONDARY, TERTIARY } = BUTTON_TYPES;
  49. useEffect(() => {
  50. if (lastFocus === null) {
  51. return;
  52. }
  53. const input = answerInputs.current[lastFocus];
  54. if (input === undefined) {
  55. return;
  56. }
  57. input.focus();
  58. }, [ answerInputs, lastFocus ]);
  59. const onQuestionKeyDown = useCallback(() => {
  60. answerInputs.current[0].focus();
  61. }, []);
  62. // Called on keypress in answer fields
  63. const onAnswerKeyDown = useCallback((index: number, ev) => {
  64. const { key } = ev.nativeEvent;
  65. const currentText = answers[index].name;
  66. if (key === 'Backspace' && currentText === '' && answers.length > 1) {
  67. removeAnswer(index);
  68. requestFocus(index > 0 ? index - 1 : 0);
  69. }
  70. }, [ answers, addAnswer, removeAnswer, requestFocus ]);
  71. /* eslint-disable react/no-multi-comp */
  72. const createRemoveOptionButton = (onPress: () => void) => (
  73. <Button
  74. id = { t('polls.create.removeOption') }
  75. labelKey = 'polls.create.removeOption'
  76. labelStyle = { dialogStyles.optionRemoveButtonText }
  77. onClick = { onPress }
  78. style = { dialogStyles.optionRemoveButton }
  79. type = { TERTIARY } />
  80. );
  81. const pollCreateButtonsContainerStyles = Platform.OS === 'android'
  82. ? pollsStyles.pollCreateButtonsContainerAndroid : pollsStyles.pollCreateButtonsContainerIos;
  83. /* eslint-disable react/jsx-no-bind */
  84. const renderListItem = ({ index }: { index: number; }) => {
  85. const isIdenticalAnswer
  86. = answers.slice(0, index).length === 0 ? false : answers.slice(0, index).some(prevAnswer =>
  87. prevAnswer.name === answers[index].name
  88. && prevAnswer.name !== '' && answers[index].name !== '');
  89. return (
  90. <View
  91. id = 'option-container'
  92. style = { dialogStyles.optionContainer as ViewStyle }>
  93. <Input
  94. blurOnSubmit = { false }
  95. bottomLabel = { (
  96. isIdenticalAnswer ? t('polls.errors.notUniqueOption', { index: index + 1 }) : '') }
  97. error = { isIdenticalAnswer }
  98. id = { `polls-answer-input-${index}` }
  99. label = { t('polls.create.pollOption', { index: index + 1 }) }
  100. maxLength = { CHAR_LIMIT }
  101. onChange = { name => setAnswer(index,
  102. {
  103. name,
  104. voters: []
  105. }) }
  106. onKeyPress = { ev => onAnswerKeyDown(index, ev) }
  107. placeholder = { t('polls.create.answerPlaceholder', { index: index + 1 }) }
  108. // This is set to help the touch event not be propagated to any subviews.
  109. pointerEvents = { 'auto' }
  110. ref = { input => registerFieldRef(index, input) }
  111. value = { answers[index].name } />
  112. {
  113. answers.length > 2
  114. && createRemoveOptionButton(() => removeAnswer(index))
  115. }
  116. </View>
  117. );
  118. };
  119. const renderListHeaderComponent = useMemo(() => (
  120. <>
  121. <Input
  122. autoFocus = { true }
  123. blurOnSubmit = { false }
  124. customStyles = {{ container: dialogStyles.customContainer }}
  125. id = { t('polls.create.pollQuestion') }
  126. label = { t('polls.create.pollQuestion') }
  127. maxLength = { CHAR_LIMIT }
  128. onChange = { setQuestion }
  129. onSubmitEditing = { onQuestionKeyDown }
  130. placeholder = { t('polls.create.questionPlaceholder') }
  131. // This is set to help the touch event not be propagated to any subviews.
  132. pointerEvents = { 'auto' }
  133. value = { question } />
  134. <Divider style = { pollsStyles.fieldSeparator as ViewStyle } />
  135. </>
  136. ), [ question ]);
  137. return (
  138. <View style = { pollsStyles.pollCreateContainer as ViewStyle }>
  139. <View style = { pollsStyles.pollCreateSubContainer as ViewStyle }>
  140. <FlatList
  141. ListHeaderComponent = { renderListHeaderComponent }
  142. data = { answers }
  143. extraData = { answers }
  144. keyExtractor = { (item, index) => index.toString() }
  145. ref = { answerListRef }
  146. renderItem = { renderListItem } />
  147. <View style = { pollCreateButtonsContainerStyles as ViewStyle }>
  148. <Button
  149. accessibilityLabel = 'polls.create.addOption'
  150. disabled = { answers.length >= ANSWERS_LIMIT }
  151. id = { t('polls.create.addOption') }
  152. labelKey = 'polls.create.addOption'
  153. onClick = { () => {
  154. // adding and answer
  155. addAnswer();
  156. requestFocus(answers.length);
  157. } }
  158. style = { pollsStyles.pollCreateAddButton }
  159. type = { SECONDARY } />
  160. <View
  161. style = { pollsStyles.buttonRow as ViewStyle }>
  162. <Button
  163. accessibilityLabel = 'polls.create.cancel'
  164. id = { t('polls.create.cancel') }
  165. labelKey = 'polls.create.cancel'
  166. onClick = { () => {
  167. setCreateMode(false);
  168. editingPollId
  169. && editingPoll?.editing
  170. && dispatch(editPoll(editingPollId, false));
  171. } }
  172. style = { pollsStyles.pollCreateButton }
  173. type = { SECONDARY } />
  174. <Button
  175. accessibilityLabel = 'polls.create.save'
  176. disabled = { isSubmitDisabled }
  177. id = { t('polls.create.save') }
  178. labelKey = 'polls.create.save'
  179. onClick = { onSubmit }
  180. style = { pollsStyles.pollCreateButton }
  181. type = { PRIMARY } />
  182. </View>
  183. </View>
  184. </View>
  185. </View>
  186. );
  187. };
  188. /*
  189. * We apply AbstractPollCreate to fill in the AbstractProps common
  190. * to both the web and native implementations.
  191. */
  192. // eslint-disable-next-line new-cap
  193. export default AbstractPollCreate(PollCreate);