Files
jitsi-meet/react/features/polls/components/web/PollCreate.tsx
Дамян Минков 469406d7cd feat(polls): Move polls to using a component (#16406)
* squash: Renames module.

* squash: Loads polls component.

* squash: Attach needed logic when components/hosts load.

* squash: Moves to use component.

* squash: Uses json-message format with types.

* squash: Checks for polls support.

* squash: Fixes comments and moves validate polls to backend.

* squash: Fix debian build.

* fix(polls): Fixes polls in breakout rooms.

* squash: Further simplify types.

Separate type that needs to go into ljm and those used only for the UI part.
Simplify answer/voter type to be unified across operations which simplifies and its logic.

* squash: Change voters structure to be {id, name}.

* squash: Update react/features/conference/functions.any.ts

Co-authored-by: Saúl Ibarra Corretgé <saghul@jitsi.org>

* squash: Drops roomJid from messages. Uses the connection information as breakout does.

---------

Co-authored-by: Saúl Ibarra Corretgé <saghul@jitsi.org>
2025-09-25 16:46:06 -05:00

285 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import Button from '../../../base/ui/components/web/Button';
import Input from '../../../base/ui/components/web/Input';
import { BUTTON_TYPES } from '../../../base/ui/constants.web';
import { editPoll } from '../../actions';
import { ANSWERS_LIMIT, CHAR_LIMIT } from '../../constants';
import AbstractPollCreate, { AbstractProps } from '../AbstractPollCreate';
const useStyles = makeStyles()(theme => {
return {
container: {
height: '100%',
position: 'relative'
},
createContainer: {
padding: '0 24px',
height: 'calc(100% - 88px)',
overflowY: 'auto'
},
header: {
...theme.typography.heading6,
color: theme.palette.text01,
margin: '24px 0 16px'
},
questionContainer: {
paddingBottom: '24px',
borderBottom: `1px solid ${theme.palette.ui03}`
},
answerList: {
listStyleType: 'none',
margin: 0,
padding: 0
},
answer: {
marginBottom: '24px'
},
removeOption: {
...theme.typography.bodyShortRegular,
color: theme.palette.link01,
marginTop: '8px',
border: 0,
background: 'transparent'
},
addButtonContainer: {
display: 'flex'
},
footer: {
position: 'absolute',
bottom: 0,
display: 'flex',
justifyContent: 'flex-end',
padding: '24px',
width: '100%',
boxSizing: 'border-box'
},
buttonMargin: {
marginRight: theme.spacing(3)
}
};
});
const PollCreate = ({
addAnswer,
answers,
editingPoll,
editingPollId,
isSubmitDisabled,
onSubmit,
question,
removeAnswer,
setAnswer,
setCreateMode,
setQuestion,
t
}: AbstractProps) => {
const { classes } = useStyles();
const dispatch = useDispatch();
/*
* This ref stores the Array of answer input fields, allowing us to focus on them.
* This array is maintained by registerfieldRef and the useEffect below.
*/
const answerInputs = useRef<Array<HTMLInputElement>>([]);
const registerFieldRef = useCallback((i, r) => {
if (r === null) {
return;
}
answerInputs.current[i] = r;
}, [ answerInputs ]);
useEffect(() => {
answerInputs.current = answerInputs.current.slice(0, answers.length);
}, [ answers ]);
/*
* This state allows us to requestFocus asynchronously, without having to worry
* about whether a newly created input field has been rendered yet or not.
*/
const [ lastFocus, requestFocus ] = useState<number | null>(null);
useEffect(() => {
if (lastFocus === null) {
return;
}
const input = answerInputs.current[lastFocus];
if (input === undefined) {
return;
}
input.focus();
}, [ lastFocus ]);
const checkModifiers = useCallback(ev => {
// Composition events used to add accents to characters
// despite their absence from standard US keyboards,
// to build up logograms of many Asian languages
// from their base components or categories and so on.
if (ev.isComposing || ev.keyCode === 229) {
// keyCode 229 means that user pressed some button,
// but input method is still processing that.
// This is a standard behavior for some input methods
// like entering japanese or сhinese hieroglyphs.
return true;
}
// Because this isn't done automatically on MacOS
if (ev.key === 'Enter' && ev.metaKey) {
ev.preventDefault();
onSubmit();
return;
}
if (ev.ctrlKey || ev.metaKey || ev.altKey || ev.shiftKey) {
return;
}
}, []);
const onQuestionKeyDown = useCallback(ev => {
if (checkModifiers(ev)) {
return;
}
if (ev.key === 'Enter') {
requestFocus(0);
ev.preventDefault();
}
}, []);
// Called on keypress in answer fields
const onAnswerKeyDown = useCallback((i, ev) => {
if (checkModifiers(ev)) {
return;
}
if (ev.key === 'Enter') {
// We add a new option input
// only if we are on the last option input
if (i === answers.length - 1) {
addAnswer(i + 1);
}
requestFocus(i + 1);
ev.preventDefault();
} else if (ev.key === 'Backspace' && ev.target.value === '' && answers.length > 1) {
removeAnswer(i);
requestFocus(i > 0 ? i - 1 : 0);
ev.preventDefault();
} else if (ev.key === 'ArrowDown') {
if (i === answers.length - 1) {
addAnswer();
}
requestFocus(i + 1);
ev.preventDefault();
} else if (ev.key === 'ArrowUp') {
if (i === 0) {
addAnswer(0);
requestFocus(0);
} else {
requestFocus(i - 1);
}
ev.preventDefault();
}
}, [ answers, addAnswer, removeAnswer, requestFocus ]);
/* eslint-disable react/jsx-no-bind */
return (<form
className = { classes.container }
onSubmit = { onSubmit }>
<div className = { classes.createContainer }>
<div className = { classes.header }>
{ t('polls.create.create') }
</div>
<div className = { classes.questionContainer }>
<Input
autoFocus = { true }
id = 'polls-create-input'
label = { t('polls.create.pollQuestion') }
maxLength = { CHAR_LIMIT }
onChange = { setQuestion }
onKeyPress = { onQuestionKeyDown }
placeholder = { t('polls.create.questionPlaceholder') }
textarea = { true }
value = { question } />
</div>
<ol className = { classes.answerList }>
{answers.map((answer, i: number) => {
const isIdenticalAnswer = answers.slice(0, i).length === 0 ? false
: answers.slice(0, i).some(prevAnswer =>
prevAnswer.name === answer.name
&& prevAnswer.name !== '' && answer.name !== '');
return (<li
className = { classes.answer }
key = { i }>
<Input
bottomLabel = { (isIdenticalAnswer ? t('polls.errors.notUniqueOption',
{ index: i + 1 }) : '') }
error = { isIdenticalAnswer }
id = { `polls-answer-input-${i}` }
label = { t('polls.create.pollOption', { index: i + 1 }) }
maxLength = { CHAR_LIMIT }
onChange = { name => setAnswer(i, {
name
}) }
onKeyPress = { ev => onAnswerKeyDown(i, ev) }
placeholder = { t('polls.create.answerPlaceholder', { index: i + 1 }) }
ref = { r => registerFieldRef(i, r) }
textarea = { true }
value = { answer.name } />
{ answers.length > 2
&& <button
className = { classes.removeOption }
data-testid = { `remove-polls-answer-input-${i}` }
onClick = { () => removeAnswer(i) }
type = 'button'>
{ t('polls.create.removeOption') }
</button>}
</li>);
}
)}
</ol>
<div className = { classes.addButtonContainer }>
<Button
accessibilityLabel = { t('polls.create.addOption') }
disabled = { answers.length >= ANSWERS_LIMIT }
labelKey = { 'polls.create.addOption' }
onClick = { () => {
addAnswer();
requestFocus(answers.length);
} }
type = { BUTTON_TYPES.SECONDARY } />
</div>
</div>
<div className = { classes.footer }>
<Button
accessibilityLabel = { t('polls.create.cancel') }
className = { classes.buttonMargin }
labelKey = { 'polls.create.cancel' }
onClick = { () => {
setCreateMode(false);
editingPollId
&& editingPoll?.editing
&& dispatch(editPoll(editingPollId, false));
} }
type = { BUTTON_TYPES.SECONDARY } />
<Button
accessibilityLabel = { t('polls.create.save') }
disabled = { isSubmitDisabled }
isSubmit = { true }
labelKey = { 'polls.create.save' } />
</div>
</form>);
};
/*
* We apply AbstractPollCreate to fill in the AbstractProps common
* to both the web and native implementations.
*/
// eslint-disable-next-line new-cap
export default AbstractPollCreate(PollCreate);