Merge branch 'polls' into 'develop'

Polls improvements from Mastodon

See merge request soapbox-pub/soapbox-fe!815
merge-requests/816/merge
Alex Gleason 3 years ago
commit 057a87cd48

@ -74,8 +74,9 @@ export function normalizePoll(poll) {
const emojiMap = makeEmojiMap(normalPoll); const emojiMap = makeEmojiMap(normalPoll);
normalPoll.options = poll.options.map(option => ({ normalPoll.options = poll.options.map((option, index) => ({
...option, ...option,
voted: poll.own_votes && poll.own_votes.includes(index),
title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap), title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap),
})); }));

@ -10,9 +10,12 @@ import spring from 'react-motion/lib/spring';
import escapeTextContentForBrowser from 'escape-html'; import escapeTextContentForBrowser from 'escape-html';
import emojify from 'soapbox/features/emoji/emoji'; import emojify from 'soapbox/features/emoji/emoji';
import RelativeTimestamp from './relative_timestamp'; import RelativeTimestamp from './relative_timestamp';
import Icon from 'soapbox/components/icon';
const messages = defineMessages({ const messages = defineMessages({
closed: { id: 'poll.closed', defaultMessage: 'Closed' }, closed: { id: 'poll.closed', defaultMessage: 'Closed' },
voted: { id: 'poll.voted', defaultMessage: 'You voted for this answer' },
votes: { id: 'poll.votes', defaultMessage: '{votes, plural, one {# vote} other {# votes}}' },
}); });
const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => { const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
@ -34,9 +37,7 @@ class Poll extends ImmutablePureComponent {
selected: {}, selected: {},
}; };
handleOptionChange = e => { _toggleOption = value => {
const { target: { value } } = e;
if (this.props.poll.get('multiple')) { if (this.props.poll.get('multiple')) {
const tmp = { ...this.state.selected }; const tmp = { ...this.state.selected };
if (tmp[value]) { if (tmp[value]) {
@ -50,8 +51,20 @@ class Poll extends ImmutablePureComponent {
tmp[value] = true; tmp[value] = true;
this.setState({ selected: tmp }); this.setState({ selected: tmp });
} }
}
handleOptionChange = ({ target: { value } }) => {
this._toggleOption(value);
}; };
handleOptionKeyPress = (e) => {
if (e.key === 'Enter' || e.key === ' ') {
this._toggleOption(e.target.getAttribute('data-index'));
e.stopPropagation();
e.preventDefault();
}
}
handleVote = () => { handleVote = () => {
if (this.props.disabled) { if (this.props.disabled) {
return; return;
@ -68,12 +81,13 @@ class Poll extends ImmutablePureComponent {
this.props.dispatch(fetchPoll(this.props.poll.get('id'))); this.props.dispatch(fetchPoll(this.props.poll.get('id')));
}; };
renderOption(option, optionIndex) { renderOption(option, optionIndex, showResults) {
const { poll, disabled } = this.props; const { poll, disabled, intl } = this.props;
const percent = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100;
const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count')); const percent = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100;
const active = !!this.state.selected[`${optionIndex}`]; const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') >= other.get('votes_count'));
const showResults = poll.get('voted') || poll.get('expired'); const active = !!this.state.selected[`${optionIndex}`];
const voted = option.get('voted') || (poll.get('own_votes') && poll.get('own_votes').includes(optionIndex));
let titleEmojified = option.get('title_emojified'); let titleEmojified = option.get('title_emojified');
if (!titleEmojified) { if (!titleEmojified) {
@ -101,8 +115,21 @@ class Poll extends ImmutablePureComponent {
disabled={disabled} disabled={disabled}
/> />
{!showResults && <span className={classNames('poll__input', { checkbox: poll.get('multiple'), active })} />} {!showResults && (
{showResults && <span className='poll__number'>{Math.round(percent)}%</span>} <span
className={classNames('poll__input', { checkbox: poll.get('multiple'), active })}
tabIndex='0'
role={poll.get('multiple') ? 'checkbox' : 'radio'}
onKeyPress={this.handleOptionKeyPress}
aria-checked={active}
aria-label={option.get('title')}
data-index={optionIndex}
/>
)}
{showResults && <span className='poll__number' title={intl.formatMessage(messages.votes, { votes: option.get('votes_count') })}>
{!!voted && <Icon src={require('@tabler/icons/icons/check.svg')} className='poll__vote__mark' title={intl.formatMessage(messages.voted)} />}
{Math.round(percent)}%
</span>}
<span dangerouslySetInnerHTML={{ __html: titleEmojified }} /> <span dangerouslySetInnerHTML={{ __html: titleEmojified }} />
</label> </label>
@ -120,11 +147,12 @@ class Poll extends ImmutablePureComponent {
const timeRemaining = poll.get('expired') ? intl.formatMessage(messages.closed) : <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />; const timeRemaining = poll.get('expired') ? intl.formatMessage(messages.closed) : <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />;
const showResults = poll.get('voted') || poll.get('expired'); const showResults = poll.get('voted') || poll.get('expired');
const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item); const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item);
const voted = poll.get('own_votes').size > 0;
return ( return (
<div className='poll'> <div className={classNames('poll', { voted })}>
<ul> <ul>
{poll.get('options').map((option, i) => this.renderOption(option, i))} {poll.get('options').map((option, i) => this.renderOption(option, i, showResults))}
</ul> </ul>
<div className='poll__footer'> <div className='poll__footer'>

@ -16,10 +16,11 @@ const messages = defineMessages({
add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add a choice' }, add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add a choice' },
remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this choice' }, remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this choice' },
poll_duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll duration' }, poll_duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll duration' },
switchToMultiple: { id: 'compose_form.poll.switch_to_multiple', defaultMessage: 'Change poll to allow multiple choices' },
switchToSingle: { id: 'compose_form.poll.switch_to_single', defaultMessage: 'Change poll to allow for a single choice' },
minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' }, minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' }, hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' }, days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
hint: { id: 'compose_form.poll.type.hint', defaultMessage: 'Click to toggle poll type. Radio button (default) is single. Checkbox is multiple.' },
}); });
@injectIntl @injectIntl
@ -63,6 +64,12 @@ class Option extends React.PureComponent {
this.props.onClearSuggestions(); this.props.onClearSuggestions();
} }
handleCheckboxKeypress = e => {
if (e.key === 'Enter' || e.key === ' ') {
this.handleToggleMultiple(e);
}
}
onSuggestionsFetchRequested = (token) => { onSuggestionsFetchRequested = (token) => {
this.props.onFetchSuggestions(token); this.props.onFetchSuggestions(token);
} }
@ -80,9 +87,11 @@ class Option extends React.PureComponent {
<span <span
className={classNames('poll__input', { checkbox: isPollMultiple })} className={classNames('poll__input', { checkbox: isPollMultiple })}
onClick={this.handleToggleMultiple} onClick={this.handleToggleMultiple}
onKeyPress={this.handleCheckboxKeypress}
role='button' role='button'
tabIndex='0' tabIndex='0'
title={intl.formatMessage(messages.hint)} title={intl.formatMessage(isPollMultiple ? messages.switchToMultiple : messages.switchToSingle)}
aria-label={intl.formatMessage(isPollMultiple ? messages.switchToMultiple : messages.switchToSingle)}
/> />
<AutosuggestInput <AutosuggestInput

@ -96,6 +96,23 @@
border-color: $valid-value-color; border-color: $valid-value-color;
background: $valid-value-color; background: $valid-value-color;
} }
&:active,
&:focus,
&:hover {
border-width: 4px;
background: none;
}
&::-moz-focus-inner {
outline: 0 !important;
border: 0;
}
&:focus,
&:active {
outline: 0 !important;
}
} }
&__number { &__number {
@ -106,6 +123,18 @@
text-align: right; text-align: right;
} }
&.voted &__number {
width: 52px;
padding-left: 8px;
flex: 0 0 52px;
}
&__vote__mark {
float: left;
color: var(--highlight-text-color);
line-height: 18px;
}
&__footer { &__footer {
padding-top: 6px; padding-top: 6px;
padding-bottom: 5px; padding-bottom: 5px;

Loading…
Cancel
Save