feat(polls) Redesign (#12838)
Convert files to TS Move styles from SCSS to JSS Implement redesign
This commit is contained in:
parent
921f3ee8cd
commit
4f34a576d0
350
css/_polls.scss
350
css/_polls.scss
|
@ -1,353 +1,3 @@
|
|||
.poll-dialog {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
|
||||
h1, span, li, strong {
|
||||
color: #bce;
|
||||
}
|
||||
ol {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-question-field {
|
||||
padding: 8px 16px;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid #525252;
|
||||
}
|
||||
|
||||
.poll-header {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.poll-creator {
|
||||
color: #C2C2C2;
|
||||
font-weight: 600;
|
||||
margin: 4px 0 16px 0;
|
||||
}
|
||||
|
||||
.poll-answer-container {
|
||||
display: flex;
|
||||
padding: 4px;
|
||||
background: #3D3D3D;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
@media (max-width: 580px) {
|
||||
&> span {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
svg {
|
||||
margin-top: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.poll-answer-field-list, .poll-answer-list, .poll-result-list {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.poll-answer-field-list {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
ol.poll-result-list {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.poll-result-list > li {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.poll-answer-field {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
margin-bottom: 16;
|
||||
|
||||
}
|
||||
|
||||
.poll-answer-field:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.poll-create-option-row {
|
||||
display: flex;
|
||||
margin-bottom: 4;
|
||||
}
|
||||
|
||||
// Needed to override atlaskit default blue color
|
||||
.poll-create-container .jsYMHu {
|
||||
background: #292929;
|
||||
border-color: #808090;
|
||||
color: #fff // #808090
|
||||
}
|
||||
|
||||
.poll-add-button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.poll-remove-option-button {
|
||||
background: 0 0;
|
||||
border: none;
|
||||
color: #E04757;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.poll-create-add-option {
|
||||
border: none;
|
||||
background-color: #292929;
|
||||
padding: 3px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.poll-icon-button, .poll-drag-handle {
|
||||
.jitsi-icon svg {
|
||||
fill: #929292;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-drag-handle {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: grab;
|
||||
padding-left: 8;
|
||||
padding-top: 8px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.poll-question {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.poll-answer-voters {
|
||||
font-weight: lighter;
|
||||
list-style-type: none;
|
||||
border: #616161 solid 1px;
|
||||
border-radius: 3px;
|
||||
padding: 2px 6px;
|
||||
margin: 4px 0px 12px;
|
||||
background-color: #616161;
|
||||
}
|
||||
|
||||
.poll-answer-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.poll-answer-vote-name {
|
||||
flex-shrink: 1;
|
||||
overflow-wrap: anywhere
|
||||
}
|
||||
|
||||
.poll-answer-vote-count-container{
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.poll-answer-vote-count {
|
||||
margin-left: 10px;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.poll-answer-short-results{
|
||||
display: flex;
|
||||
min-width: 10em;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.poll-bar-container, .poll-bar {
|
||||
border-radius: 3px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.poll-bar-container {
|
||||
background-color: #616161;
|
||||
max-width: 160px;
|
||||
margin-top: 3px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.poll-bar {
|
||||
background-color: #246FE5;
|
||||
}
|
||||
|
||||
.poll-message-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.poll-notice {
|
||||
font-weight: 100;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.poll-show-details {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-result-links {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
a.poll-detail-link, a.poll-change-vote-link {
|
||||
color: #669AEC;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: #669AEC;
|
||||
}
|
||||
|
||||
&:visited {
|
||||
color: #669AEC;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.polls-pane-content {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pane-content{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.empty-pane-icon {
|
||||
width: 50%;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.empty-pane-icon svg {
|
||||
fill: #3D3D3D;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.empty-pane-message {
|
||||
color: #fff;
|
||||
padding: 0 16px;
|
||||
text-align: center;
|
||||
|
||||
}
|
||||
|
||||
.poll-results, .poll-answer {
|
||||
background: #292929;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #666666;
|
||||
margin: 16px;
|
||||
padding: 16px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.poll-results {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.poll-answer {
|
||||
|
||||
h1, strong ,span {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
button > span {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-create-label {
|
||||
color: #C2C2C2;
|
||||
display: flex;
|
||||
font-weight: 400;
|
||||
margin-bottom: 4;
|
||||
}
|
||||
|
||||
.expandable-input{
|
||||
line-height: 18px;
|
||||
resize: none;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
border: 1px solid #666666;
|
||||
background-color: #141414;
|
||||
color: #FFF;
|
||||
border-radius: 6px;
|
||||
padding: 10px 16px;
|
||||
}
|
||||
|
||||
#polls-panel {
|
||||
height: calc(100% - 119px);
|
||||
}
|
||||
|
||||
.poll-container {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
height: calc(100% - 88px);
|
||||
line-height: 20px;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
|
||||
& > * + *:not(.ignore-child) {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 580px) {
|
||||
height: calc(100% - 102px);
|
||||
}
|
||||
}
|
||||
|
||||
.poll-create-header {
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
margin: 20px 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.poll-create-container {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.poll-create-footer {
|
||||
background-color: #141414;
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
width: calc(100% - 32px);
|
||||
}
|
||||
|
||||
.poll-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px 16px 16px;
|
||||
}
|
||||
|
||||
.poll-answer-footer {
|
||||
padding: 8px 0 0 0;
|
||||
}
|
||||
|
|
|
@ -85,6 +85,7 @@ export interface IJitsiConference {
|
|||
sendFaceLandmarks: (faceLandmarks: FaceLandmarks) => void;
|
||||
sendFeedback: Function;
|
||||
sendLobbyMessage: Function;
|
||||
sendMessage: Function;
|
||||
sessionId: string;
|
||||
setDesktopSharingFrameRate: Function;
|
||||
setDisplayName: Function;
|
||||
|
|
|
@ -21,6 +21,7 @@ interface IProps extends IInputProps {
|
|||
name?: string;
|
||||
onKeyPress?: (e: React.KeyboardEvent) => void;
|
||||
readOnly?: boolean;
|
||||
required?: boolean;
|
||||
textarea?: boolean;
|
||||
type?: 'text' | 'email' | 'number' | 'password';
|
||||
}
|
||||
|
@ -148,6 +149,7 @@ const Input = React.forwardRef<any, IProps>(({
|
|||
onKeyPress,
|
||||
placeholder,
|
||||
readOnly = false,
|
||||
required,
|
||||
textarea = false,
|
||||
type = 'text',
|
||||
value
|
||||
|
@ -178,6 +180,7 @@ const Input = React.forwardRef<any, IProps>(({
|
|||
error && 'error', clearable && styles.clearableInput, icon && 'icon-input') }
|
||||
disabled = { disabled }
|
||||
{ ...(id ? { id } : {}) }
|
||||
maxLength = { maxLength }
|
||||
maxRows = { maxRows }
|
||||
minRows = { minRows }
|
||||
name = { name }
|
||||
|
@ -186,6 +189,7 @@ const Input = React.forwardRef<any, IProps>(({
|
|||
placeholder = { placeholder }
|
||||
readOnly = { readOnly }
|
||||
ref = { ref }
|
||||
required = { required }
|
||||
value = { value } />
|
||||
) : (
|
||||
<input
|
||||
|
@ -202,6 +206,7 @@ const Input = React.forwardRef<any, IProps>(({
|
|||
placeholder = { placeholder }
|
||||
readOnly = { readOnly }
|
||||
ref = { ref }
|
||||
required = { required }
|
||||
type = { type }
|
||||
value = { value } />
|
||||
)}
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
// @flow
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import type { AbstractComponent } from 'react';
|
||||
import React, { ComponentType, FormEvent, useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { createPollEvent, sendAnalytics } from '../../analytics';
|
||||
import { createPollEvent } from '../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../analytics/functions';
|
||||
import { IReduxState } from '../../app/types';
|
||||
import { COMMAND_NEW_POLL } from '../constants';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of inheriting component.
|
||||
*/
|
||||
type InputProps = {
|
||||
setCreateMode: boolean => void,
|
||||
setCreateMode: (mode: boolean) => void;
|
||||
};
|
||||
|
||||
/*
|
||||
|
@ -20,16 +19,15 @@ type InputProps = {
|
|||
* concrete implementations (web/native).
|
||||
**/
|
||||
export type AbstractProps = InputProps & {
|
||||
answers: Array<string>,
|
||||
question: string,
|
||||
setQuestion: string => void,
|
||||
setAnswer: (number, string) => void,
|
||||
addAnswer: ?number => void,
|
||||
moveAnswer: (number, number) => void,
|
||||
removeAnswer: number => void,
|
||||
onSubmit: Function,
|
||||
isSubmitDisabled: boolean,
|
||||
t: Function,
|
||||
addAnswer: (index?: number) => void;
|
||||
answers: Array<string>;
|
||||
isSubmitDisabled: boolean;
|
||||
onSubmit: (event?: FormEvent<HTMLFormElement>) => void;
|
||||
question: string;
|
||||
removeAnswer: (index: number) => void;
|
||||
setAnswer: (index: number, value: string) => void;
|
||||
setQuestion: (question: string) => void;
|
||||
t: Function;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -39,7 +37,7 @@ export type AbstractProps = InputProps & {
|
|||
* @param {React.AbstractComponent} Component - The concrete component.
|
||||
* @returns {React.AbstractComponent}
|
||||
*/
|
||||
const AbstractPollCreate = (Component: AbstractComponent<AbstractProps>) => (props: InputProps) => {
|
||||
const AbstractPollCreate = (Component: ComponentType<AbstractProps>) => (props: InputProps) => {
|
||||
|
||||
const { setCreateMode } = props;
|
||||
|
||||
|
@ -51,25 +49,15 @@ const AbstractPollCreate = (Component: AbstractComponent<AbstractProps>) => (pro
|
|||
answers[i] = answer;
|
||||
|
||||
setAnswers([ ...answers ]);
|
||||
});
|
||||
}, [ answers ]);
|
||||
|
||||
const addAnswer = useCallback((i: ?number) => {
|
||||
const addAnswer = useCallback((i?: number) => {
|
||||
const newAnswers = [ ...answers ];
|
||||
|
||||
sendAnalytics(createPollEvent('option.added'));
|
||||
newAnswers.splice(typeof i === 'number' ? i : answers.length, 0, '');
|
||||
setAnswers(newAnswers);
|
||||
});
|
||||
|
||||
const moveAnswer = useCallback((i, j) => {
|
||||
const newAnswers = [ ...answers ];
|
||||
const answer = answers[i];
|
||||
|
||||
sendAnalytics(createPollEvent('option.moved'));
|
||||
newAnswers.splice(i, 1);
|
||||
newAnswers.splice(j, 0, answer);
|
||||
setAnswers(newAnswers);
|
||||
});
|
||||
}, [ answers ]);
|
||||
|
||||
const removeAnswer = useCallback(i => {
|
||||
if (answers.length <= 2) {
|
||||
|
@ -80,9 +68,9 @@ const AbstractPollCreate = (Component: AbstractComponent<AbstractProps>) => (pro
|
|||
sendAnalytics(createPollEvent('option.removed'));
|
||||
newAnswers.splice(i, 1);
|
||||
setAnswers(newAnswers);
|
||||
});
|
||||
}, [ answers ]);
|
||||
|
||||
const conference = useSelector(state => state['features/base/conference'].conference);
|
||||
const conference = useSelector((state: IReduxState) => state['features/base/conference'].conference);
|
||||
|
||||
const onSubmit = useCallback(ev => {
|
||||
if (ev) {
|
||||
|
@ -95,7 +83,7 @@ const AbstractPollCreate = (Component: AbstractComponent<AbstractProps>) => (pro
|
|||
return;
|
||||
}
|
||||
|
||||
conference.sendMessage({
|
||||
conference?.sendMessage({
|
||||
type: COMMAND_NEW_POLL,
|
||||
pollId: Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(36),
|
||||
question,
|
||||
|
@ -118,7 +106,6 @@ const AbstractPollCreate = (Component: AbstractComponent<AbstractProps>) => (pro
|
|||
addAnswer = { addAnswer }
|
||||
answers = { answers }
|
||||
isSubmitDisabled = { isSubmitDisabled }
|
||||
moveAnswer = { moveAnswer }
|
||||
onSubmit = { onSubmit }
|
||||
question = { question }
|
||||
removeAnswer = { removeAnswer }
|
|
@ -1,13 +1,11 @@
|
|||
// @flow
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import type { AbstractComponent } from 'react';
|
||||
import React, { ComponentType, useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { createPollEvent, sendAnalytics } from '../../analytics';
|
||||
import { getParticipantDisplayName } from '../../base/participants';
|
||||
import { getParticipantById } from '../../base/participants/functions';
|
||||
import { createPollEvent } from '../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../analytics/functions';
|
||||
import { IReduxState } from '../../app/types';
|
||||
import { getParticipantById, getParticipantDisplayName } from '../../base/participants/functions';
|
||||
import { useBoundSelector } from '../../base/util/hooks';
|
||||
import { setVoteChanging } from '../actions';
|
||||
import { getPoll } from '../functions';
|
||||
|
@ -20,28 +18,28 @@ type InputProps = {
|
|||
/**
|
||||
* ID of the poll to display.
|
||||
*/
|
||||
pollId: string,
|
||||
pollId: string;
|
||||
};
|
||||
|
||||
export type AnswerInfo = {
|
||||
name: string,
|
||||
percentage: number,
|
||||
voters?: Array<{ id: number, name: string }>,
|
||||
voterCount: number
|
||||
name: string;
|
||||
percentage: number;
|
||||
voterCount: number;
|
||||
voters?: Array<{ id: string; name: string; } | undefined>;
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link AbstractPollResults}.
|
||||
*/
|
||||
export type AbstractProps = {
|
||||
answers: Array<AnswerInfo>,
|
||||
changeVote: Function,
|
||||
creatorName: string,
|
||||
showDetails: boolean,
|
||||
question: string,
|
||||
t: Function,
|
||||
toggleIsDetailed: Function,
|
||||
haveVoted: boolean,
|
||||
answers: Array<AnswerInfo>;
|
||||
changeVote: (e: React.MouseEvent) => void;
|
||||
creatorName: string;
|
||||
haveVoted: boolean;
|
||||
question: string;
|
||||
showDetails: boolean;
|
||||
t: Function;
|
||||
toggleIsDetailed: (e: React.MouseEvent) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -51,18 +49,18 @@ export type AbstractProps = {
|
|||
* @param {React.AbstractComponent} Component - The concrete component.
|
||||
* @returns {React.AbstractComponent}
|
||||
*/
|
||||
const AbstractPollResults = (Component: AbstractComponent<AbstractProps>) => (props: InputProps) => {
|
||||
const AbstractPollResults = (Component: ComponentType<AbstractProps>) => (props: InputProps) => {
|
||||
const { pollId } = props;
|
||||
|
||||
const pollDetails = useSelector(getPoll(pollId));
|
||||
const participant = useBoundSelector(getParticipantById, pollDetails.senderId);
|
||||
const reduxState = useSelector(state => state);
|
||||
const reduxState = useSelector((state: IReduxState) => state);
|
||||
|
||||
const [ showDetails, setShowDetails ] = useState(false);
|
||||
const toggleIsDetailed = useCallback(() => {
|
||||
sendAnalytics(createPollEvent('vote.detailsViewed'));
|
||||
setShowDetails(!showDetails);
|
||||
});
|
||||
setShowDetails(details => !details);
|
||||
}, []);
|
||||
|
||||
const answers: Array<AnswerInfo> = useMemo(() => {
|
||||
const allVoters = new Set();
|
||||
|
@ -79,7 +77,7 @@ const AbstractPollResults = (Component: AbstractComponent<AbstractProps>) => (pr
|
|||
const nrOfVotersPerAnswer = answer.voters ? Object.keys(answer.voters).length : 0;
|
||||
const percentage = allVoters.size > 0 ? Math.round(nrOfVotersPerAnswer / allVoters.size * 100) : 0;
|
||||
|
||||
let voters = null;
|
||||
let voters;
|
||||
|
||||
if (showDetails && answer.voters) {
|
||||
const answerVoters = answer.voters?.length ? [ ...answer.voters ] : Object.keys({ ...answer.voters });
|
|
@ -1,7 +1,4 @@
|
|||
// @flow
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import type { AbstractComponent } from 'react';
|
||||
import React, { ComponentType, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
/*
|
||||
|
@ -9,10 +6,10 @@ import { useTranslation } from 'react-i18next';
|
|||
* concrete implementations (web/native).
|
||||
**/
|
||||
export type AbstractProps = {
|
||||
createMode: boolean,
|
||||
onCreate: void => void,
|
||||
setCreateMode: boolean => void,
|
||||
t: Function,
|
||||
createMode: boolean;
|
||||
onCreate: () => void;
|
||||
setCreateMode: (mode: boolean) => void;
|
||||
t: Function;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -22,7 +19,7 @@ export type AbstractProps = {
|
|||
* @param {React.AbstractComponent} Component - The concrete component.
|
||||
* @returns {React.AbstractComponent}
|
||||
*/
|
||||
const AbstractPollsPane = (Component: AbstractComponent<AbstractProps>) => () => {
|
||||
const AbstractPollsPane = (Component: ComponentType<AbstractProps>) => () => {
|
||||
|
||||
const [ createMode, setCreateMode ] = useState(false);
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||
import Button from '../../../base/ui/components/web/Button';
|
||||
import Checkbox from '../../../base/ui/components/web/Checkbox';
|
||||
import { BUTTON_TYPES } from '../../../base/ui/constants.web';
|
||||
|
@ -10,8 +10,40 @@ import AbstractPollAnswer, { AbstractProps } from '../AbstractPollAnswer';
|
|||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
container: {
|
||||
margin: '24px',
|
||||
padding: '16px',
|
||||
backgroundColor: theme.palette.ui02,
|
||||
borderRadius: '8px'
|
||||
},
|
||||
header: {
|
||||
marginBottom: '24px'
|
||||
},
|
||||
question: {
|
||||
...withPixelLineHeight(theme.typography.heading6),
|
||||
color: theme.palette.text01,
|
||||
marginBottom: '8px'
|
||||
},
|
||||
creator: {
|
||||
...withPixelLineHeight(theme.typography.bodyShortRegular),
|
||||
color: theme.palette.text02
|
||||
},
|
||||
answerList: {
|
||||
listStyleType: 'none',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
marginBottom: '24px'
|
||||
},
|
||||
answer: {
|
||||
display: 'flex',
|
||||
marginBottom: '16px'
|
||||
},
|
||||
footer: {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end'
|
||||
},
|
||||
buttonMargin: {
|
||||
marginRight: theme.spacing(2)
|
||||
marginRight: theme.spacing(3)
|
||||
}
|
||||
};
|
||||
});
|
||||
|
@ -27,23 +59,23 @@ const PollAnswer = ({
|
|||
t
|
||||
}: AbstractProps) => {
|
||||
const { changingVote } = poll;
|
||||
const { classes: styles } = useStyles();
|
||||
const { classes } = useStyles();
|
||||
|
||||
return (
|
||||
<div className = 'poll-answer'>
|
||||
<div className = 'poll-header'>
|
||||
<div className = 'poll-question'>
|
||||
<span>{ poll.question }</span>
|
||||
<div className = { classes.container }>
|
||||
<div className = { classes.header }>
|
||||
<div className = { classes.question }>
|
||||
{ poll.question }
|
||||
</div>
|
||||
<div className = 'poll-creator'>
|
||||
<div className = { classes.creator }>
|
||||
{ t('polls.by', { name: creatorName }) }
|
||||
</div>
|
||||
</div>
|
||||
<ol className = 'poll-answer-list'>
|
||||
<ul className = { classes.answerList }>
|
||||
{
|
||||
poll.answers.map((answer: any, index: number) => (
|
||||
<li
|
||||
className = 'poll-answer-container'
|
||||
className = { classes.answer }
|
||||
key = { index }>
|
||||
<Checkbox
|
||||
checked = { checkBoxStates[index] }
|
||||
|
@ -54,19 +86,17 @@ const PollAnswer = ({
|
|||
</li>
|
||||
))
|
||||
}
|
||||
</ol>
|
||||
<div className = 'poll-footer poll-answer-footer' >
|
||||
</ul>
|
||||
<div className = { classes.footer } >
|
||||
<Button
|
||||
accessibilityLabel = { t('polls.answer.skip') }
|
||||
className = { styles.buttonMargin }
|
||||
fullWidth = { true }
|
||||
className = { classes.buttonMargin }
|
||||
labelKey = { 'polls.answer.skip' }
|
||||
onClick = { changingVote ? skipChangeVote : skipAnswer }
|
||||
type = { BUTTON_TYPES.SECONDARY } />
|
||||
<Button
|
||||
accessibilityLabel = { t('polls.answer.submit') }
|
||||
disabled = { isSubmitAnswerDisabled(checkBoxStates) }
|
||||
fullWidth = { true }
|
||||
labelKey = { 'polls.answer.submit' }
|
||||
onClick = { submitAnswer } />
|
||||
</div>
|
||||
|
|
|
@ -1,21 +1,62 @@
|
|||
/* eslint-disable lines-around-comment */
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
// @ts-ignore
|
||||
import { Tooltip } from '../../../base/tooltip';
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||
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 { ANSWERS_LIMIT, CHAR_LIMIT } from '../../constants';
|
||||
// @ts-ignore
|
||||
import AbstractPollCreate from '../AbstractPollCreate';
|
||||
// @ts-ignore
|
||||
import type { AbstractProps } from '../AbstractPollCreate';
|
||||
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: {
|
||||
...withPixelLineHeight(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: {
|
||||
...withPixelLineHeight(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(2)
|
||||
marginRight: theme.spacing(3)
|
||||
}
|
||||
};
|
||||
});
|
||||
|
@ -32,7 +73,7 @@ const PollCreate = ({
|
|||
setQuestion,
|
||||
t
|
||||
}: AbstractProps) => {
|
||||
const { classes: styles } = useStyles();
|
||||
const { classes } = useStyles();
|
||||
|
||||
/*
|
||||
* This ref stores the Array of answer input fields, allowing us to focus on them.
|
||||
|
@ -139,76 +180,54 @@ const PollCreate = ({
|
|||
}
|
||||
}, [ answers, addAnswer, removeAnswer, requestFocus ]);
|
||||
|
||||
const autogrow = (ev: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const el = ev.target;
|
||||
|
||||
el.style.height = '1px';
|
||||
el.style.height = `${el.scrollHeight + 2}px`;
|
||||
};
|
||||
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
return (<form
|
||||
className = 'polls-pane-content'
|
||||
className = { classes.container }
|
||||
onSubmit = { onSubmit }>
|
||||
<div className = 'poll-create-container poll-container'>
|
||||
<div className = 'poll-create-header'>
|
||||
<div className = { classes.createContainer }>
|
||||
<div className = { classes.header }>
|
||||
{ t('polls.create.create') }
|
||||
</div>
|
||||
<div className = 'poll-question-field'>
|
||||
<span className = 'poll-create-label'>
|
||||
{ t('polls.create.pollQuestion') }
|
||||
</span>
|
||||
<textarea
|
||||
<div className = { classes.questionContainer }>
|
||||
<Input
|
||||
autoFocus = { true }
|
||||
className = 'expandable-input'
|
||||
label = { t('polls.create.pollQuestion') }
|
||||
maxLength = { CHAR_LIMIT }
|
||||
onChange = { ev => setQuestion(ev.target.value) }
|
||||
onInput = { autogrow }
|
||||
onKeyDown = { onQuestionKeyDown }
|
||||
onChange = { setQuestion }
|
||||
onKeyPress = { onQuestionKeyDown }
|
||||
placeholder = { t('polls.create.questionPlaceholder') }
|
||||
required = { true }
|
||||
rows = { 1 }
|
||||
textarea = { true }
|
||||
value = { question } />
|
||||
</div>
|
||||
<ol className = 'poll-answer-field-list'>
|
||||
<ol className = { classes.answerList }>
|
||||
{answers.map((answer: any, i: number) =>
|
||||
(<li
|
||||
className = 'poll-answer-field'
|
||||
className = { classes.answer }
|
||||
key = { i }>
|
||||
<span className = 'poll-create-label'>
|
||||
{ t('polls.create.pollOption', { index: i + 1 })}
|
||||
</span>
|
||||
<div className = 'poll-create-option-row'>
|
||||
<textarea
|
||||
className = 'expandable-input'
|
||||
maxLength = { CHAR_LIMIT }
|
||||
onChange = { ev => setAnswer(i, ev.target.value) }
|
||||
onInput = { autogrow }
|
||||
onKeyDown = { ev => onAnswerKeyDown(i, ev) }
|
||||
placeholder = { t('polls.create.answerPlaceholder', { index: i + 1 }) }
|
||||
ref = { r => registerFieldRef(i, r) }
|
||||
required = { true }
|
||||
rows = { 1 }
|
||||
value = { answer } />
|
||||
</div>
|
||||
<Input
|
||||
label = { t('polls.create.pollOption', { index: i + 1 }) }
|
||||
maxLength = { CHAR_LIMIT }
|
||||
onChange = { val => setAnswer(i, val) }
|
||||
onKeyPress = { ev => onAnswerKeyDown(i, ev) }
|
||||
placeholder = { t('polls.create.answerPlaceholder', { index: i + 1 }) }
|
||||
ref = { r => registerFieldRef(i, r) }
|
||||
textarea = { true }
|
||||
value = { answer } />
|
||||
|
||||
{ answers.length > 2
|
||||
&& <Tooltip content = { t('polls.create.removeOption') }>
|
||||
<button
|
||||
className = 'poll-remove-option-button'
|
||||
onClick = { () => removeAnswer(i) }
|
||||
type = 'button'>
|
||||
{ t('polls.create.removeOption') }
|
||||
</button>
|
||||
</Tooltip>}
|
||||
&& <button
|
||||
className = { classes.removeOption }
|
||||
onClick = { () => removeAnswer(i) }
|
||||
type = 'button'>
|
||||
{ t('polls.create.removeOption') }
|
||||
</button>}
|
||||
</li>)
|
||||
)}
|
||||
</ol>
|
||||
<div className = 'poll-add-button'>
|
||||
<div className = { classes.addButtonContainer }>
|
||||
<Button
|
||||
accessibilityLabel = { t('polls.create.addOption') }
|
||||
disabled = { answers.length >= ANSWERS_LIMIT }
|
||||
fullWidth = { true }
|
||||
labelKey = { 'polls.create.addOption' }
|
||||
onClick = { () => {
|
||||
addAnswer();
|
||||
|
@ -217,23 +236,20 @@ const PollCreate = ({
|
|||
type = { BUTTON_TYPES.SECONDARY } />
|
||||
</div>
|
||||
</div>
|
||||
<div className = 'poll-footer poll-create-footer'>
|
||||
<div className = { classes.footer }>
|
||||
<Button
|
||||
accessibilityLabel = { t('polls.create.cancel') }
|
||||
className = { styles.buttonMargin }
|
||||
fullWidth = { true }
|
||||
className = { classes.buttonMargin }
|
||||
labelKey = { 'polls.create.cancel' }
|
||||
onClick = { () => setCreateMode(false) }
|
||||
type = { BUTTON_TYPES.SECONDARY } />
|
||||
<Button
|
||||
accessibilityLabel = { t('polls.create.send') }
|
||||
disabled = { isSubmitDisabled }
|
||||
fullWidth = { true }
|
||||
isSubmit = { true }
|
||||
labelKey = { 'polls.create.send' } />
|
||||
</div>
|
||||
</form>);
|
||||
|
||||
};
|
||||
|
||||
/*
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { PollAnswer, PollResults } from '..';
|
||||
import { shouldShowResults } from '../../functions';
|
||||
|
||||
import PollAnswer from './PollAnswer';
|
||||
import PollResults from './PollResults';
|
||||
|
||||
type Props = {
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Id of the poll.
|
||||
*/
|
||||
pollId: string,
|
||||
pollId: string;
|
||||
|
||||
}
|
||||
|
||||
const PollItem = React.forwardRef<Props, HTMLElement>(({ pollId }: Props, ref) => {
|
||||
const PollItem = React.forwardRef<HTMLDivElement, IProps>(({ pollId }: IProps, ref) => {
|
||||
const showResults = useSelector(shouldShowResults(pollId));
|
||||
|
||||
return (
|
|
@ -1,84 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import AbstractPollResults from '../AbstractPollResults';
|
||||
import type { AbstractProps } from '../AbstractPollResults';
|
||||
|
||||
|
||||
/**
|
||||
* Component that renders the poll results.
|
||||
*
|
||||
* @param {Props} props - The passed props.
|
||||
* @returns {React.Node}
|
||||
*/
|
||||
const PollResults = (props: AbstractProps) => {
|
||||
const {
|
||||
answers,
|
||||
changeVote,
|
||||
creatorName,
|
||||
haveVoted,
|
||||
showDetails,
|
||||
question,
|
||||
t,
|
||||
toggleIsDetailed
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className = 'poll-results'>
|
||||
<div className = 'poll-header'>
|
||||
<div className = 'poll-question'>
|
||||
<strong>{ question }</strong>
|
||||
</div>
|
||||
<div className = 'poll-creator'>
|
||||
{ t('polls.by', { name: creatorName }) }
|
||||
</div>
|
||||
</div>
|
||||
<ol className = 'poll-result-list'>
|
||||
{answers.map(({ name, percentage, voters, voterCount }, index) =>
|
||||
(<li key = { index }>
|
||||
<div className = 'poll-answer-header'>
|
||||
<span className = 'poll-answer-vote-name' >{name}</span>
|
||||
</div>
|
||||
<div className = 'poll-answer-short-results'>
|
||||
<span className = 'poll-bar-container'>
|
||||
<div
|
||||
className = 'poll-bar'
|
||||
style = {{ width: `${percentage}%` }} />
|
||||
</span>
|
||||
<div className = 'poll-answer-vote-count-container'>
|
||||
<span className = 'poll-answer-vote-count'>({voterCount}) {percentage}%</span>
|
||||
</div>
|
||||
</div>
|
||||
{ showDetails && voters && voterCount > 0
|
||||
&& <ul className = 'poll-answer-voters'>
|
||||
{voters.map(voter =>
|
||||
<li key = { voter.id }>{voter.name}</li>
|
||||
)}
|
||||
</ul>}
|
||||
</li>)
|
||||
)}
|
||||
</ol>
|
||||
<div className = { 'poll-result-links' }>
|
||||
<a
|
||||
className = { 'poll-detail-link' }
|
||||
onClick = { toggleIsDetailed }>
|
||||
{showDetails ? t('polls.results.hideDetailedResults') : t('polls.results.showDetailedResults')}
|
||||
</a>
|
||||
<a
|
||||
className = { 'poll-change-vote-link' }
|
||||
onClick = { changeVote }>
|
||||
{haveVoted ? t('polls.results.changeVote') : t('polls.results.vote')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
/*
|
||||
* We apply AbstractPollResults to fill in the AbstractProps common
|
||||
* to both the web and native implementations.
|
||||
*/
|
||||
// eslint-disable-next-line new-cap
|
||||
export default AbstractPollResults(PollResults);
|
|
@ -0,0 +1,177 @@
|
|||
import React from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||
import AbstractPollResults, { AbstractProps } from '../AbstractPollResults';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
container: {
|
||||
margin: '24px',
|
||||
padding: '16px',
|
||||
backgroundColor: theme.palette.ui02,
|
||||
borderRadius: '8px'
|
||||
},
|
||||
header: {
|
||||
marginBottom: '16px'
|
||||
},
|
||||
question: {
|
||||
...withPixelLineHeight(theme.typography.heading6),
|
||||
color: theme.palette.text01,
|
||||
marginBottom: '8px'
|
||||
},
|
||||
creator: {
|
||||
...withPixelLineHeight(theme.typography.bodyShortRegular),
|
||||
color: theme.palette.text02
|
||||
},
|
||||
resultList: {
|
||||
listStyleType: 'none',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
|
||||
'& li': {
|
||||
marginBottom: '16px'
|
||||
}
|
||||
},
|
||||
answerName: {
|
||||
display: 'flex',
|
||||
flexShrink: 1,
|
||||
overflowWrap: 'anywhere',
|
||||
...withPixelLineHeight(theme.typography.bodyShortRegular),
|
||||
color: theme.palette.text01,
|
||||
marginBottom: '4px'
|
||||
},
|
||||
answerResultContainer: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
minWidth: '10em'
|
||||
},
|
||||
barContainer: {
|
||||
backgroundColor: theme.palette.ui03,
|
||||
borderRadius: '4px',
|
||||
height: '6px',
|
||||
maxWidth: '160px',
|
||||
width: '158px',
|
||||
flexGrow: 1,
|
||||
marginTop: '2px'
|
||||
},
|
||||
bar: {
|
||||
height: '6px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: theme.palette.action01
|
||||
},
|
||||
voteCount: {
|
||||
flex: 1,
|
||||
textAlign: 'right',
|
||||
...withPixelLineHeight(theme.typography.bodyShortBold),
|
||||
color: theme.palette.text01
|
||||
},
|
||||
voters: {
|
||||
margin: 0,
|
||||
marginTop: '4px',
|
||||
listStyleType: 'none',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: theme.palette.ui03,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
padding: '8px 16px',
|
||||
|
||||
'& li': {
|
||||
...withPixelLineHeight(theme.typography.bodyShortRegular),
|
||||
color: theme.palette.text01,
|
||||
margin: 0,
|
||||
marginBottom: '2px',
|
||||
|
||||
'&:last-of-type': {
|
||||
marginBottom: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
buttonsContainer: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
|
||||
'& button': {
|
||||
border: 0,
|
||||
backgroundColor: 'transparent',
|
||||
...withPixelLineHeight(theme.typography.bodyShortRegular),
|
||||
color: theme.palette.link01
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Component that renders the poll results.
|
||||
*
|
||||
* @param {Props} props - The passed props.
|
||||
* @returns {React.Node}
|
||||
*/
|
||||
const PollResults = ({
|
||||
answers,
|
||||
changeVote,
|
||||
creatorName,
|
||||
haveVoted,
|
||||
showDetails,
|
||||
question,
|
||||
t,
|
||||
toggleIsDetailed
|
||||
}: AbstractProps) => {
|
||||
const { classes } = useStyles();
|
||||
|
||||
return (
|
||||
<div className = { classes.container }>
|
||||
<div className = { classes.header }>
|
||||
<div className = { classes.question }>
|
||||
{question}
|
||||
</div>
|
||||
<div className = { classes.creator }>
|
||||
{t('polls.by', { name: creatorName })}
|
||||
</div>
|
||||
</div>
|
||||
<ul className = { classes.resultList }>
|
||||
{answers.map(({ name, percentage, voters, voterCount }, index) =>
|
||||
(<li key = { index }>
|
||||
<div className = { classes.answerName }>
|
||||
{name}
|
||||
</div>
|
||||
<div className = { classes.answerResultContainer }>
|
||||
<span className = { classes.barContainer }>
|
||||
<div
|
||||
className = { classes.bar }
|
||||
style = {{ width: `${percentage}%` }} />
|
||||
</span>
|
||||
<div className = { classes.voteCount }>
|
||||
{voterCount}({percentage}%)
|
||||
</div>
|
||||
</div>
|
||||
{showDetails && voters && voterCount > 0
|
||||
&& <ul className = { classes.voters }>
|
||||
{voters.map(voter =>
|
||||
<li key = { voter?.id }>{voter?.name}</li>
|
||||
)}
|
||||
</ul>}
|
||||
</li>)
|
||||
)}
|
||||
</ul>
|
||||
<div className = { classes.buttonsContainer }>
|
||||
<button
|
||||
onClick = { toggleIsDetailed }>
|
||||
{showDetails ? t('polls.results.hideDetailedResults') : t('polls.results.showDetailedResults')}
|
||||
</button>
|
||||
<button
|
||||
onClick = { changeVote }>
|
||||
{haveVoted ? t('polls.results.changeVote') : t('polls.results.vote')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
* We apply AbstractPollResults to fill in the AbstractProps common
|
||||
* to both the web and native implementations.
|
||||
*/
|
||||
// eslint-disable-next-line new-cap
|
||||
export default AbstractPollResults(PollResults);
|
|
@ -1,58 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
|
||||
import { Icon, IconMessage } from '../../../base/icons';
|
||||
import { browser } from '../../../base/lib-jitsi-meet';
|
||||
|
||||
import PollItem from './PollItem';
|
||||
|
||||
const PollsList = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const polls = useSelector(state => state['features/polls'].polls);
|
||||
const pollListEndRef = useRef(null);
|
||||
|
||||
const scrollToBottom = useCallback(() => {
|
||||
if (pollListEndRef.current) {
|
||||
// Safari does not support options
|
||||
const param = browser.isSafari()
|
||||
? false : {
|
||||
behavior: 'smooth',
|
||||
block: 'end',
|
||||
inline: 'nearest'
|
||||
};
|
||||
|
||||
pollListEndRef.current.scrollIntoView(param);
|
||||
}
|
||||
}, [ pollListEndRef.current ]);
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [ polls ]);
|
||||
|
||||
const listPolls = Object.keys(polls);
|
||||
|
||||
return (
|
||||
<>
|
||||
{listPolls.length === 0
|
||||
? <div className = 'pane-content'>
|
||||
<Icon
|
||||
className = 'empty-pane-icon'
|
||||
src = { IconMessage } />
|
||||
<span className = 'empty-pane-message'>{t('polls.results.empty')}</span>
|
||||
</div>
|
||||
: listPolls.map((id, index) => (
|
||||
<PollItem
|
||||
key = { id }
|
||||
pollId = { id }
|
||||
ref = { listPolls.length - 1 === index ? pollListEndRef : null } />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PollsList;
|
|
@ -0,0 +1,89 @@
|
|||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconMessage } from '../../../base/icons/svg';
|
||||
import { browser } from '../../../base/lib-jitsi-meet';
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||
|
||||
import PollItem from './PollItem';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
container: {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
emptyIcon: {
|
||||
width: '100px',
|
||||
padding: '16px',
|
||||
|
||||
'& svg': {
|
||||
width: '100%',
|
||||
height: 'auto'
|
||||
}
|
||||
},
|
||||
emptyMessage: {
|
||||
...withPixelLineHeight(theme.typography.bodyLongBold),
|
||||
color: theme.palette.text02,
|
||||
padding: '0 24px',
|
||||
textAlign: 'center'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const PollsList = () => {
|
||||
const { t } = useTranslation();
|
||||
const { classes, theme } = useStyles();
|
||||
|
||||
const polls = useSelector((state: IReduxState) => state['features/polls'].polls);
|
||||
const pollListEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const scrollToBottom = useCallback(() => {
|
||||
if (pollListEndRef.current) {
|
||||
// Safari does not support options
|
||||
const param = browser.isSafari()
|
||||
? false : {
|
||||
behavior: 'smooth' as const,
|
||||
block: 'end' as const,
|
||||
inline: 'nearest' as const
|
||||
};
|
||||
|
||||
pollListEndRef.current.scrollIntoView(param);
|
||||
}
|
||||
}, [ pollListEndRef.current ]);
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [ polls ]);
|
||||
|
||||
const listPolls = Object.keys(polls);
|
||||
|
||||
return (
|
||||
<>
|
||||
{listPolls.length === 0
|
||||
? <div className = { classes.container }>
|
||||
<Icon
|
||||
className = { classes.emptyIcon }
|
||||
color = { theme.palette.icon03 }
|
||||
src = { IconMessage } />
|
||||
<span className = { classes.emptyMessage }>{t('polls.results.empty')}</span>
|
||||
</div>
|
||||
: listPolls.map((id, index) => (
|
||||
<PollItem
|
||||
key = { id }
|
||||
pollId = { id }
|
||||
ref = { listPolls.length - 1 === index ? pollListEndRef : null } />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PollsList;
|
|
@ -1,27 +1,42 @@
|
|||
/* eslint-disable lines-around-comment */
|
||||
import React from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import Button from '../../../base/ui/components/web/Button';
|
||||
// @ts-ignore
|
||||
import AbstractPollsPane from '../AbstractPollsPane';
|
||||
// @ts-ignore
|
||||
import type { AbstractProps } from '../AbstractPollsPane';
|
||||
import AbstractPollsPane, { AbstractProps } from '../AbstractPollsPane';
|
||||
|
||||
import PollCreate from './PollCreate';
|
||||
// @ts-ignore
|
||||
import PollsList from './PollsList';
|
||||
|
||||
const useStyles = makeStyles()(() => {
|
||||
return {
|
||||
container: {
|
||||
height: '100%',
|
||||
position: 'relative'
|
||||
},
|
||||
listContainer: {
|
||||
height: 'calc(100% - 88px)',
|
||||
overflowY: 'auto'
|
||||
},
|
||||
footer: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
padding: '24px',
|
||||
width: '100%',
|
||||
boxSizing: 'border-box'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const PollsPane = (props: AbstractProps) => {
|
||||
const { createMode, onCreate, setCreateMode, t } = props;
|
||||
const PollsPane = ({ createMode, onCreate, setCreateMode, t }: AbstractProps) => {
|
||||
const { classes } = useStyles();
|
||||
|
||||
return createMode
|
||||
? <PollCreate setCreateMode = { setCreateMode } />
|
||||
: <div className = 'polls-pane-content'>
|
||||
<div className = { 'poll-container' } >
|
||||
: <div className = { classes.container }>
|
||||
<div className = { classes.listContainer } >
|
||||
<PollsList />
|
||||
</div>
|
||||
<div className = 'poll-footer poll-create-footer'>
|
||||
<div className = { classes.footer }>
|
||||
<Button
|
||||
accessibilityLabel = { t('polls.create.create') }
|
||||
autoFocus = { true }
|
||||
|
|
Loading…
Reference in New Issue