about summary refs log tree commit diff
path: root/app/javascript/flavours/glitch
diff options
context:
space:
mode:
authorThibaut Girka <thib@sitedethib.com>2019-03-06 12:30:11 +0100
committerThibG <thib@sitedethib.com>2019-03-06 23:56:53 +0100
commit8fe86cebaa30e77e50c8223a1ff83759dbd7ca62 (patch)
treeb82bd62f5d7dd41e6c70361e93ffcaa9b0114e74 /app/javascript/flavours/glitch
parent3e5a0bc8250b3dc806e97e8370c319c40fc5ea28 (diff)
[Glitch] Port polls creation UI from upstream
Diffstat (limited to 'app/javascript/flavours/glitch')
-rw-r--r--app/javascript/flavours/glitch/actions/compose.js50
-rw-r--r--app/javascript/flavours/glitch/actions/statuses.js6
-rw-r--r--app/javascript/flavours/glitch/features/composer/index.js52
-rw-r--r--app/javascript/flavours/glitch/features/composer/options/index.js35
-rw-r--r--app/javascript/flavours/glitch/features/composer/poll_form/components/poll_form.js121
-rw-r--r--app/javascript/flavours/glitch/features/composer/poll_form/index.js29
-rw-r--r--app/javascript/flavours/glitch/reducers/compose.js35
-rw-r--r--app/javascript/flavours/glitch/styles/components/composer.scss7
-rw-r--r--app/javascript/flavours/glitch/styles/polls.scss88
9 files changed, 403 insertions, 20 deletions
diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js
index fc32277b2..21ed6225c 100644
--- a/app/javascript/flavours/glitch/actions/compose.js
+++ b/app/javascript/flavours/glitch/actions/compose.js
@@ -55,6 +55,13 @@ export const COMPOSE_UPLOAD_CHANGE_FAIL        = 'COMPOSE_UPLOAD_UPDATE_FAIL';
 
 export const COMPOSE_DOODLE_SET        = 'COMPOSE_DOODLE_SET';
 
+export const COMPOSE_POLL_ADD             = 'COMPOSE_POLL_ADD';
+export const COMPOSE_POLL_REMOVE          = 'COMPOSE_POLL_REMOVE';
+export const COMPOSE_POLL_OPTION_ADD      = 'COMPOSE_POLL_OPTION_ADD';
+export const COMPOSE_POLL_OPTION_CHANGE   = 'COMPOSE_POLL_OPTION_CHANGE';
+export const COMPOSE_POLL_OPTION_REMOVE   = 'COMPOSE_POLL_OPTION_REMOVE';
+export const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE';
+
 const messages = defineMessages({
   uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
 });
@@ -144,6 +151,7 @@ export function submitCompose(routerHistory) {
       sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0),
       spoiler_text: spoilerText,
       visibility: getState().getIn(['compose', 'privacy']),
+      poll: getState().getIn(['compose', 'poll'], null),
     }, {
       headers: {
         'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
@@ -504,3 +512,45 @@ export function insertEmojiCompose(position, emoji) {
     emoji,
   };
 };
+
+export function addPoll() {
+  return {
+    type: COMPOSE_POLL_ADD,
+  };
+};
+
+export function removePoll() {
+  return {
+    type: COMPOSE_POLL_REMOVE,
+  };
+};
+
+export function addPollOption(title) {
+  return {
+    type: COMPOSE_POLL_OPTION_ADD,
+    title,
+  };
+};
+
+export function changePollOption(index, title) {
+  return {
+    type: COMPOSE_POLL_OPTION_CHANGE,
+    index,
+    title,
+  };
+};
+
+export function removePollOption(index) {
+  return {
+    type: COMPOSE_POLL_OPTION_REMOVE,
+    index,
+  };
+};
+
+export function changePollSettings(expiresIn, isMultiple) {
+  return {
+    type: COMPOSE_POLL_SETTINGS_CHANGE,
+    expiresIn,
+    isMultiple,
+  };
+};
diff --git a/app/javascript/flavours/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js
index 13ce782e6..4eabc4be0 100644
--- a/app/javascript/flavours/glitch/actions/statuses.js
+++ b/app/javascript/flavours/glitch/actions/statuses.js
@@ -80,7 +80,11 @@ export function redraft(status) {
 
 export function deleteStatus(id, router, withRedraft = false) {
   return (dispatch, getState) => {
-    const status = getState().getIn(['statuses', id]);
+    let status = getState().getIn(['statuses', id]);
+
+    if (status.get('poll')) {
+      status = status.set('poll', getState().getIn(['polls', status.get('poll')]));
+    }
 
     dispatch(deleteStatusRequest(id));
 
diff --git a/app/javascript/flavours/glitch/features/composer/index.js b/app/javascript/flavours/glitch/features/composer/index.js
index ec0e405a4..9d2e0b3da 100644
--- a/app/javascript/flavours/glitch/features/composer/index.js
+++ b/app/javascript/flavours/glitch/features/composer/index.js
@@ -31,6 +31,7 @@ import {
   openModal,
 } from 'flavours/glitch/actions/modal';
 import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
+import { addPoll, removePoll } from 'flavours/glitch/actions/compose';
 
 //  Components.
 import ComposerOptions from './options';
@@ -39,6 +40,7 @@ import ComposerReply from './reply';
 import ComposerSpoiler from './spoiler';
 import ComposerTextarea from './textarea';
 import ComposerUploadForm from './upload_form';
+import ComposerPollForm from './poll_form';
 import ComposerWarning from './warning';
 import ComposerHashtagWarning from './hashtag_warning';
 import ComposerDirectWarning from './direct_warning';
@@ -102,6 +104,7 @@ function mapStateToProps (state) {
     suggestions: state.getIn(['compose', 'suggestions']),
     text: state.getIn(['compose', 'text']),
     anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
+    poll: state.getIn(['compose', 'poll']),
     spoilersAlwaysOn: spoilersAlwaysOn,
     mediaDescriptionConfirmation: state.getIn(['local_settings', 'confirm_missing_media_description']),
     preselectOnReply: state.getIn(['local_settings', 'preselect_on_reply']),
@@ -134,6 +137,15 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
   onChangeVisibility(value) {
     dispatch(changeComposeVisibility(value));
   },
+  onTogglePoll() {
+    dispatch((_, getState) => {
+      if (getState().getIn(['compose', 'poll'])) {
+        dispatch(removePoll());
+      } else {
+        dispatch(addPoll());
+      }
+    });
+  },
   onClearSuggestions() {
     dispatch(clearComposeSuggestions());
   },
@@ -394,6 +406,7 @@ class Composer extends React.Component {
       isUploading,
       layout,
       media,
+      poll,
       onCancelReply,
       onChangeAdvancedOption,
       onChangeDescription,
@@ -401,6 +414,7 @@ class Composer extends React.Component {
       onChangeSpoilerness,
       onChangeText,
       onChangeVisibility,
+      onTogglePoll,
       onClearSuggestions,
       onCloseModal,
       onFetchSuggestions,
@@ -463,30 +477,38 @@ class Composer extends React.Component {
           suggestions={suggestions}
           value={text}
         />
-        {isUploading || media && media.size ? (
-          <ComposerUploadForm
-            intl={intl}
-            media={media}
-            onChangeDescription={onChangeDescription}
-            onOpenFocalPointModal={onOpenFocalPointModal}
-            onRemove={onUndoUpload}
-            progress={progress}
-            uploading={isUploading}
-            handleRef={handleRefUploadForm}
-          />
-        ) : null}
+        <div className='compose-form__modifiers'>
+          {isUploading || media && media.size ? (
+            <ComposerUploadForm
+              intl={intl}
+              media={media}
+              onChangeDescription={onChangeDescription}
+              onOpenFocalPointModal={onOpenFocalPointModal}
+              onRemove={onUndoUpload}
+              progress={progress}
+              uploading={isUploading}
+              handleRef={handleRefUploadForm}
+            />
+          ) : null}
+          {!!poll && (
+            <ComposerPollForm />
+          )}
+        </div>
         <ComposerOptions
           acceptContentTypes={acceptContentTypes}
           advancedOptions={advancedOptions}
           disabled={isSubmitting}
-          full={media ? media.size >= 4 || media.some(
-            item => item.get('type') === 'video'
-          ) : false}
+          allowMedia={!poll && (media ? media.size < 4 && !media.some(
+              item => item.get('type') === 'video'
+            ) : true)}
           hasMedia={media && !!media.size}
+          allowPoll={!(media && !!media.size)}
+          hasPoll={!!poll}
           intl={intl}
           onChangeAdvancedOption={onChangeAdvancedOption}
           onChangeSensitivity={onChangeSensitivity}
           onChangeVisibility={onChangeVisibility}
+          onTogglePoll={onTogglePoll}
           onDoodleOpen={onOpenDoodleModal}
           onModalClose={onCloseModal}
           onModalOpen={onOpenActionsModal}
diff --git a/app/javascript/flavours/glitch/features/composer/options/index.js b/app/javascript/flavours/glitch/features/composer/options/index.js
index 5b4a7444c..80ac1b63a 100644
--- a/app/javascript/flavours/glitch/features/composer/options/index.js
+++ b/app/javascript/flavours/glitch/features/composer/options/index.js
@@ -98,6 +98,14 @@ const messages = defineMessages({
     defaultMessage: 'Upload a file',
     id: 'compose.attach.upload',
   },
+  add_poll: {
+    defaultMessage: 'Add a poll',
+    id: 'poll_button.add_poll',
+  },
+  remove_poll: {
+    defaultMessage: 'Remove poll',
+    id: 'poll_button.remove_poll',
+  },
 });
 
 //  Handlers.
@@ -160,12 +168,15 @@ export default class ComposerOptions extends React.PureComponent {
       acceptContentTypes,
       advancedOptions,
       disabled,
-      full,
+      allowMedia,
       hasMedia,
+      allowPoll,
+      hasPoll,
       intl,
       onChangeAdvancedOption,
       onChangeSensitivity,
       onChangeVisibility,
+      onTogglePoll,
       onModalClose,
       onModalOpen,
       onToggleSpoiler,
@@ -209,7 +220,7 @@ export default class ComposerOptions extends React.PureComponent {
       <div className='composer--options'>
         <input
           accept={acceptContentTypes}
-          disabled={disabled || full}
+          disabled={disabled || !allowMedia}
           key={resetFileKey}
           onChange={handleChangeFiles}
           ref={handleRefFileElement}
@@ -218,7 +229,7 @@ export default class ComposerOptions extends React.PureComponent {
           {...hiddenComponent}
         />
         <Dropdown
-          disabled={disabled || full}
+          disabled={disabled || !allowMedia}
           icon='paperclip'
           items={[
             {
@@ -237,6 +248,19 @@ export default class ComposerOptions extends React.PureComponent {
           onModalOpen={onModalOpen}
           title={intl.formatMessage(messages.attach)}
         />
+        <IconButton
+          active={hasPoll}
+          disabled={disabled || !allowPoll}
+          icon='tasks'
+          inverted
+          onClick={onTogglePoll}
+          size={18}
+          style={{
+            height: null,
+            lineHeight: null,
+          }}
+          title={intl.formatMessage(hasPoll ? messages.remove_poll : messages.add_poll)}
+        />
         <Motion
           defaultStyle={{ scale: 0.87 }}
           style={{
@@ -329,12 +353,15 @@ ComposerOptions.propTypes = {
   acceptContentTypes: PropTypes.string,
   advancedOptions: ImmutablePropTypes.map,
   disabled: PropTypes.bool,
-  full: PropTypes.bool,
+  allowMedia: PropTypes.bool,
   hasMedia: PropTypes.bool,
+  allowPoll: PropTypes.bool,
+  hasPoll: PropTypes.bool,
   intl: PropTypes.object.isRequired,
   onChangeAdvancedOption: PropTypes.func,
   onChangeSensitivity: PropTypes.func,
   onChangeVisibility: PropTypes.func,
+  onTogglePoll: PropTypes.func,
   onDoodleOpen: PropTypes.func,
   onModalClose: PropTypes.func,
   onModalOpen: PropTypes.func,
diff --git a/app/javascript/flavours/glitch/features/composer/poll_form/components/poll_form.js b/app/javascript/flavours/glitch/features/composer/poll_form/components/poll_form.js
new file mode 100644
index 000000000..c329b04cd
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/composer/poll_form/components/poll_form.js
@@ -0,0 +1,121 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import IconButton from 'flavours/glitch/components/icon_button';
+import Icon from 'flavours/glitch/components/icon';
+import classNames from 'classnames';
+
+const messages = defineMessages({
+  option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Choice {number}' },
+  add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add a choice' },
+  remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this choice' },
+  poll_duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll duration' },
+  minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
+  hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
+  days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
+});
+
+@injectIntl
+class Option extends React.PureComponent {
+
+  static propTypes = {
+    title: PropTypes.string.isRequired,
+    index: PropTypes.number.isRequired,
+    isPollMultiple: PropTypes.bool,
+    onChange: PropTypes.func.isRequired,
+    onRemove: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  handleOptionTitleChange = e => {
+    this.props.onChange(this.props.index, e.target.value);
+  };
+
+  handleOptionRemove = () => {
+    this.props.onRemove(this.props.index);
+  };
+
+  render () {
+    const { isPollMultiple, title, index, intl } = this.props;
+
+    return (
+      <li>
+        <label className='poll__text editable'>
+          <span className={classNames('poll__input', { checkbox: isPollMultiple })} />
+
+          <input
+            type='text'
+            placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
+            maxlength={25}
+            value={title}
+            onChange={this.handleOptionTitleChange}
+          />
+        </label>
+
+        <div className='poll__cancel'>
+          <IconButton disabled={index <= 1} title={intl.formatMessage(messages.remove_option)} icon='times' onClick={this.handleOptionRemove} />
+        </div>
+      </li>
+    );
+  }
+
+}
+
+export default
+@injectIntl
+class PollForm extends ImmutablePureComponent {
+
+  static propTypes = {
+    options: ImmutablePropTypes.list,
+    expiresIn: PropTypes.number,
+    isMultiple: PropTypes.bool,
+    onChangeOption: PropTypes.func.isRequired,
+    onAddOption: PropTypes.func.isRequired,
+    onRemoveOption: PropTypes.func.isRequired,
+    onChangeSettings: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
+  };
+
+  handleAddOption = () => {
+    this.props.onAddOption('');
+  };
+
+  handleSelectDuration = e => {
+    this.props.onChangeSettings(e.target.value, this.props.isMultiple);
+  };
+
+  render () {
+    const { options, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl } = this.props;
+
+    if (!options) {
+      return null;
+    }
+
+    return (
+      <div className='compose-form__poll-wrapper'>
+        <ul>
+          {options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} />)}
+        </ul>
+
+        <div className='poll__footer'>
+          {options.size < 4 && (
+            <button className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' /> <FormattedMessage {...messages.add_option} /></button>
+          )}
+
+          <select value={expiresIn} onChange={this.handleSelectDuration}>
+            <option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>
+            <option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option>
+            <option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option>
+            <option value={21600}>{intl.formatMessage(messages.hours, { number: 6 })}</option>
+            <option value={86400}>{intl.formatMessage(messages.days, { number: 1 })}</option>
+            <option value={259200}>{intl.formatMessage(messages.days, { number: 3 })}</option>
+            <option value={604800}>{intl.formatMessage(messages.days, { number: 7 })}</option>
+          </select>
+        </div>
+      </div>
+    );
+  }
+
+}
diff --git a/app/javascript/flavours/glitch/features/composer/poll_form/index.js b/app/javascript/flavours/glitch/features/composer/poll_form/index.js
new file mode 100644
index 000000000..5232c3b31
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/composer/poll_form/index.js
@@ -0,0 +1,29 @@
+import { connect } from 'react-redux';
+import PollForm from './components/poll_form';
+import { addPollOption, removePollOption, changePollOption, changePollSettings } from '../../../actions/compose';
+
+const mapStateToProps = state => ({
+  options: state.getIn(['compose', 'poll', 'options']),
+  expiresIn: state.getIn(['compose', 'poll', 'expires_in']),
+  isMultiple: state.getIn(['compose', 'poll', 'multiple']),
+});
+
+const mapDispatchToProps = dispatch => ({
+  onAddOption(title) {
+    dispatch(addPollOption(title));
+  },
+
+  onRemoveOption(index) {
+    dispatch(removePollOption(index));
+  },
+
+  onChangeOption(index, title) {
+    dispatch(changePollOption(index, title));
+  },
+
+  onChangeSettings(expiresIn, isMultiple) {
+    dispatch(changePollSettings(expiresIn, isMultiple));
+  },
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(PollForm);
diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js
index 7281cbd61..a79b0dd24 100644
--- a/app/javascript/flavours/glitch/reducers/compose.js
+++ b/app/javascript/flavours/glitch/reducers/compose.js
@@ -31,6 +31,12 @@ import {
   COMPOSE_UPLOAD_CHANGE_FAIL,
   COMPOSE_DOODLE_SET,
   COMPOSE_RESET,
+  COMPOSE_POLL_ADD,
+  COMPOSE_POLL_REMOVE,
+  COMPOSE_POLL_OPTION_ADD,
+  COMPOSE_POLL_OPTION_CHANGE,
+  COMPOSE_POLL_OPTION_REMOVE,
+  COMPOSE_POLL_SETTINGS_CHANGE,
 } from 'flavours/glitch/actions/compose';
 import { TIMELINE_DELETE } from 'flavours/glitch/actions/timelines';
 import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
@@ -70,6 +76,7 @@ const initialState = ImmutableMap({
   is_changing_upload: false,
   progress: 0,
   media_attachments: ImmutableList(),
+  poll: null,
   suggestion_token: null,
   suggestions: ImmutableList(),
   default_advanced_options: ImmutableMap({
@@ -94,6 +101,12 @@ const initialState = ImmutableMap({
   }),
 });
 
+const initialPoll = ImmutableMap({
+  options: ImmutableList(['', '']),
+  expires_in: 24 * 3600,
+  multiple: false,
+});
+
 function statusToTextMentions(state, status) {
   let set = ImmutableOrderedSet([]);
 
@@ -140,6 +153,7 @@ function clearAll(state) {
     map.set('privacy', state.get('default_privacy'));
     map.set('sensitive', false);
     map.update('media_attachments', list => list.clear());
+    map.set('poll', null);
     map.set('idempotencyKey', uuid());
   });
 };
@@ -336,6 +350,7 @@ export default function compose(state = initialState, action) {
       map.set('spoiler', false);
       map.set('spoiler_text', '');
       map.set('privacy', state.get('default_privacy'));
+      map.set('poll', null);
       map.update(
         'advanced_options',
         map => map.mergeWith(overwrite, state.get('default_advanced_options'))
@@ -424,7 +439,27 @@ export default function compose(state = initialState, action) {
         map.set('spoiler', false);
         map.set('spoiler_text', '');
       }
+
+      if (action.status.get('poll')) {
+        map.set('poll', ImmutableMap({
+          options: action.status.getIn(['poll', 'options']).map(x => x.get('title')),
+          multiple: action.status.getIn(['poll', 'multiple']),
+          expires_in: 24 * 3600,
+        }));
+      }
     });
+  case COMPOSE_POLL_ADD:
+    return state.set('poll', initialPoll);
+  case COMPOSE_POLL_REMOVE:
+    return state.set('poll', null);
+  case COMPOSE_POLL_OPTION_ADD:
+    return state.updateIn(['poll', 'options'], options => options.push(action.title));
+  case COMPOSE_POLL_OPTION_CHANGE:
+    return state.setIn(['poll', 'options', action.index], action.title);
+  case COMPOSE_POLL_OPTION_REMOVE:
+    return state.updateIn(['poll', 'options'], options => options.delete(action.index));
+  case COMPOSE_POLL_SETTINGS_CHANGE:
+    return state.update('poll', poll => poll.set('expires_in', action.expiresIn).set('multiple', action.isMultiple));
   default:
     return state;
   }
diff --git a/app/javascript/flavours/glitch/styles/components/composer.scss b/app/javascript/flavours/glitch/styles/components/composer.scss
index fa24cabf2..e14775e44 100644
--- a/app/javascript/flavours/glitch/styles/components/composer.scss
+++ b/app/javascript/flavours/glitch/styles/components/composer.scss
@@ -368,6 +368,13 @@
   }
 }
 
+.compose-form__modifiers {
+  color: $inverted-text-color;
+  font-family: inherit;
+  font-size: 14px;
+  background: $simple-background-color;
+}
+
 .composer--options {
   padding: 10px;
   background: darken($simple-background-color, 8%);
diff --git a/app/javascript/flavours/glitch/styles/polls.scss b/app/javascript/flavours/glitch/styles/polls.scss
index ce324b36e..4f8c94d83 100644
--- a/app/javascript/flavours/glitch/styles/polls.scss
+++ b/app/javascript/flavours/glitch/styles/polls.scss
@@ -37,9 +37,34 @@
       display: none;
     }
 
+    input[type=text] {
+      display: block;
+      box-sizing: border-box;
+      flex: 1 1 auto;
+      width: 20px;
+      font-size: 14px;
+      color: $inverted-text-color;
+      display: block;
+      outline: 0;
+      font-family: inherit;
+      background: $simple-background-color;
+      border: 1px solid darken($simple-background-color, 14%);
+      border-radius: 4px;
+      padding: 6px 10px;
+
+      &:focus {
+        border-color: $highlight-text-color;
+      }
+    }
+
     &.selectable {
       cursor: pointer;
     }
+
+    &.editable {
+      display: flex;
+      align-items: center;
+    }
   }
 
   &__input {
@@ -49,6 +74,7 @@
     box-sizing: border-box;
     width: 18px;
     height: 18px;
+    flex: 0 0 auto;
     margin-right: 10px;
     top: -1px;
     border-radius: 50%;
@@ -102,3 +128,65 @@
     font-size: 14px;
   }
 }
+
+.compose-form__poll-wrapper {
+  border-top: 1px solid darken($simple-background-color, 8%);
+
+  ul {
+    padding: 10px;
+  }
+
+  .poll__footer {
+    border-top: 1px solid darken($simple-background-color, 8%);
+    padding: 10px;
+    display: flex;
+    align-items: center;
+
+    button,
+    select {
+      flex: 1 1 50%;
+    }
+  }
+
+  .button.button-secondary {
+    font-size: 14px;
+    font-weight: 400;
+    padding: 6px 10px;
+    height: auto;
+    line-height: inherit;
+    color: $action-button-color;
+    border-color: $action-button-color;
+    margin-right: 5px;
+  }
+
+  li {
+    display: flex;
+    align-items: center;
+
+    .poll__text {
+      flex: 0 0 auto;
+      width: calc(100% - (23px + 6px));
+      margin-right: 6px;
+    }
+  }
+
+  select {
+    appearance: none;
+    box-sizing: border-box;
+    font-size: 14px;
+    color: $inverted-text-color;
+    display: inline-block;
+    width: auto;
+    outline: 0;
+    font-family: inherit;
+    background: $simple-background-color url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(darken($simple-background-color, 14%))}'/></svg>") no-repeat right 8px center / auto 16px;
+    border: 1px solid darken($simple-background-color, 14%);
+    border-radius: 4px;
+    padding: 6px 10px;
+    padding-right: 30px;
+  }
+
+  .icon-button.disabled {
+    color: darken($simple-background-color, 14%);
+  }
+}