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 7.3KB

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