kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Merge branch 'improve-polls' into 'develop'
Poll Form improvement See merge request soapbox-pub/soapbox-fe!1501environments/review-develop-3zknud/deployments/221
commit
6328b03783
|
@ -20,7 +20,7 @@ export type AutoSuggestion = string | Emoji;
|
|||
const textAtCursorMatchesToken = (str: string, caretPosition: number, searchTokens: string[]): CursorMatch => {
|
||||
let word: string;
|
||||
|
||||
const left: number = str.slice(0, caretPosition).search(/\S+$/);
|
||||
const left: number = str.slice(0, caretPosition).search(/\S+$/);
|
||||
const right: number = str.slice(caretPosition).search(/\s/);
|
||||
|
||||
if (right < 0) {
|
||||
|
@ -201,13 +201,13 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
|
|||
|
||||
if (typeof suggestion === 'object') {
|
||||
inner = <AutosuggestEmoji emoji={suggestion} />;
|
||||
key = suggestion.id;
|
||||
key = suggestion.id;
|
||||
} else if (suggestion[0] === '#') {
|
||||
inner = suggestion;
|
||||
key = suggestion;
|
||||
key = suggestion;
|
||||
} else {
|
||||
inner = <AutosuggestAccount id={suggestion} />;
|
||||
key = suggestion;
|
||||
key = suggestion;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -279,13 +279,13 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
|
|||
}
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
<div className='relative w-full'>
|
||||
<label className='sr-only'>{placeholder}</label>
|
||||
|
||||
<input
|
||||
type='text'
|
||||
className={classNames({
|
||||
'block w-full sm:text-sm dark:bg-slate-800 dark:text-white dark:placeholder:text-gray-500 focus:ring-indigo-500 focus:border-indigo-500': true,
|
||||
'block w-full sm:text-sm border-gray-300 dark:border-gray-600 dark:bg-slate-800 dark:text-white dark:placeholder:text-gray-500 focus:ring-primary-500 focus:border-primary-500': true,
|
||||
}, className)}
|
||||
ref={this.setInput}
|
||||
disabled={disabled}
|
||||
|
|
|
@ -273,11 +273,13 @@ const StatusContent: React.FC<IStatusContent> = ({ status, expanded = false, onE
|
|||
output.push(<ReadMoreButton onClick={onClick} key='read-more' />);
|
||||
}
|
||||
|
||||
if (status.poll && typeof status.poll === 'string') {
|
||||
const hasPoll = status.poll && typeof status.poll === 'string';
|
||||
|
||||
if (hasPoll) {
|
||||
output.push(<Poll id={status.poll} key='poll' status={status.url} />);
|
||||
}
|
||||
|
||||
return <>{output}</>;
|
||||
return <div className={classNames({ 'bg-gray-100 rounded-md p-4': hasPoll })}>{output}</div>;
|
||||
} else {
|
||||
const output = [
|
||||
<div
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
|
||||
import { render, screen } from '../../../../jest/test-helpers';
|
||||
import Divider from '../divider';
|
||||
|
||||
describe('<Divider />', () => {
|
||||
it('renders without text', () => {
|
||||
render(<Divider />);
|
||||
|
||||
expect(screen.queryAllByTestId('divider-text')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('renders text', () => {
|
||||
const text = 'Hello';
|
||||
render(<Divider text={text} />);
|
||||
|
||||
expect(screen.getByTestId('divider-text')).toHaveTextContent(text);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
import React from 'react';
|
||||
|
||||
interface IDivider {
|
||||
text?: string
|
||||
}
|
||||
|
||||
/** Divider */
|
||||
const Divider = ({ text }: IDivider) => (
|
||||
<div className='relative' data-testid='divider'>
|
||||
<div className='absolute inset-0 flex items-center' aria-hidden='true'>
|
||||
<div className='w-full border-t-2 border-gray-100 dark:border-slate-700 border-solid' />
|
||||
</div>
|
||||
|
||||
{text && (
|
||||
<div className='relative flex justify-center'>
|
||||
<span className='px-2 bg-white text-gray-400' data-testid='divider-text'>{text}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Divider;
|
|
@ -5,6 +5,7 @@ export { default as Checkbox } from './checkbox/checkbox';
|
|||
export { default as Column } from './column/column';
|
||||
export { default as Counter } from './counter/counter';
|
||||
export { default as Datepicker } from './datepicker/datepicker';
|
||||
export { default as Divider } from './divider/divider';
|
||||
export { default as Emoji } from './emoji/emoji';
|
||||
export { default as EmojiSelector } from './emoji-selector/emoji-selector';
|
||||
export { default as FileInput } from './file-input/file-input';
|
||||
|
|
|
@ -64,7 +64,7 @@ const Input = React.forwardRef<HTMLInputElement, IInput>(
|
|||
type={revealed ? 'text' : type}
|
||||
ref={ref}
|
||||
className={classNames({
|
||||
'dark:bg-slate-800 dark:text-white block w-full sm:text-sm border-gray-300 dark:border-gray-600 rounded-md focus:ring-indigo-500 focus:border-indigo-500':
|
||||
'dark:bg-slate-800 dark:text-white block w-full sm:text-sm border-gray-300 dark:border-gray-600 rounded-md focus:ring-primary-500 focus:border-primary-500':
|
||||
true,
|
||||
'pr-7': isPassword,
|
||||
'text-red-600 border-red-600': hasError,
|
||||
|
|
|
@ -38,6 +38,7 @@ const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u20
|
|||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What\'s on your mind?' },
|
||||
pollPlaceholder: { id: 'compose_form.poll_placeholder', defaultMessage: 'Add a poll topic...' },
|
||||
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' },
|
||||
publish: { id: 'compose_form.publish', defaultMessage: 'Post' },
|
||||
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
|
||||
|
@ -62,6 +63,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
spoilerText: PropTypes.string,
|
||||
focusDate: PropTypes.instanceOf(Date),
|
||||
caretPosition: PropTypes.number,
|
||||
hasPoll: PropTypes.bool,
|
||||
isSubmitting: PropTypes.bool,
|
||||
isChangingUpload: PropTypes.bool,
|
||||
isEditing: PropTypes.bool,
|
||||
|
@ -340,7 +342,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
|
||||
<AutosuggestTextarea
|
||||
ref={(isModalOpen && shouldCondense) ? null : this.setAutosuggestTextarea}
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
placeholder={intl.formatMessage(this.props.hasPoll ? messages.pollPlaceholder : messages.placeholder)}
|
||||
disabled={disabled}
|
||||
value={this.props.text}
|
||||
onChange={this.handleChange}
|
||||
|
|
|
@ -1,32 +1,33 @@
|
|||
'use strict';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import AutosuggestInput from 'soapbox/components/autosuggest_input';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import IconButton from 'soapbox/components/icon_button';
|
||||
import { HStack } from 'soapbox/components/ui';
|
||||
import { Button, Divider, HStack, Stack, Text, Toggle } from 'soapbox/components/ui';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import DurationSelector from './polls/duration-selector';
|
||||
|
||||
import type { AutoSuggestion } from 'soapbox/components/autosuggest_input';
|
||||
|
||||
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' },
|
||||
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' },
|
||||
option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Answer #{number}' },
|
||||
add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add an answer' },
|
||||
remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this answer' },
|
||||
pollDuration: { id: 'compose_form.poll.duration', defaultMessage: 'Duration' },
|
||||
removePoll: { id: 'compose_form.poll.remove', defaultMessage: 'Remove poll' },
|
||||
switchToMultiple: { id: 'compose_form.poll.switch_to_multiple', defaultMessage: 'Change poll to allow multiple answers' },
|
||||
switchToSingle: { id: 'compose_form.poll.switch_to_single', defaultMessage: 'Change poll to allow for a single answer' },
|
||||
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}}' },
|
||||
multiSelect: { id: 'compose_form.poll.multiselect', defaultMessage: 'Multi-Select' },
|
||||
multiSelectDetail: { id: 'compose_form.poll.multiselect_detail', defaultMessage: 'Allow users to select multiple answers' },
|
||||
});
|
||||
|
||||
interface IOption {
|
||||
index: number
|
||||
isPollMultiple?: boolean
|
||||
maxChars: number
|
||||
numOptions: number
|
||||
onChange(index: number, value: string): void
|
||||
|
@ -35,7 +36,6 @@ interface IOption {
|
|||
onRemove(index: number): void
|
||||
onRemovePoll(): void
|
||||
onSuggestionSelected(tokenStart: number, token: string, value: string, key: (string | number)[]): void
|
||||
onToggleMultiple(): void
|
||||
suggestions?: any // list
|
||||
title: string
|
||||
}
|
||||
|
@ -43,7 +43,6 @@ interface IOption {
|
|||
const Option = (props: IOption) => {
|
||||
const {
|
||||
index,
|
||||
isPollMultiple,
|
||||
maxChars,
|
||||
numOptions,
|
||||
onChange,
|
||||
|
@ -51,7 +50,6 @@ const Option = (props: IOption) => {
|
|||
onFetchSuggestions,
|
||||
onRemove,
|
||||
onRemovePoll,
|
||||
onToggleMultiple,
|
||||
suggestions,
|
||||
title,
|
||||
} = props;
|
||||
|
@ -68,21 +66,8 @@ const Option = (props: IOption) => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleToggleMultiple = (event: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
onToggleMultiple();
|
||||
};
|
||||
|
||||
const onSuggestionsClearRequested = () => onClearSuggestions();
|
||||
|
||||
const handleCheckboxKeypress = (event: React.KeyboardEvent<HTMLElement>) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
handleToggleMultiple(event);
|
||||
}
|
||||
};
|
||||
|
||||
const onSuggestionsFetchRequested = (token: string) => onFetchSuggestions(token);
|
||||
|
||||
const onSuggestionSelected = (tokenStart: number, token: string | null, value: AutoSuggestion) => {
|
||||
|
@ -92,19 +77,14 @@ const Option = (props: IOption) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<li>
|
||||
<label className='poll__text editable'>
|
||||
<span
|
||||
className={classNames('poll__input', { checkbox: isPollMultiple })}
|
||||
onClick={handleToggleMultiple}
|
||||
onKeyPress={handleCheckboxKeypress}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
title={intl.formatMessage(isPollMultiple ? messages.switchToSingle : messages.switchToMultiple)}
|
||||
aria-label={intl.formatMessage(isPollMultiple ? messages.switchToSingle : messages.switchToMultiple)}
|
||||
/>
|
||||
<HStack alignItems='center' justifyContent='between' space={4}>
|
||||
<HStack alignItems='center' space={2} grow>
|
||||
<div className='w-6'>
|
||||
<Text weight='bold'>{index + 1}.</Text>
|
||||
</div>
|
||||
|
||||
<AutosuggestInput
|
||||
className='rounded-md'
|
||||
placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
|
||||
maxLength={maxChars}
|
||||
value={title}
|
||||
|
@ -114,18 +94,16 @@ const Option = (props: IOption) => {
|
|||
onSuggestionsClearRequested={onSuggestionsClearRequested}
|
||||
onSuggestionSelected={onSuggestionSelected}
|
||||
searchTokens={[':']}
|
||||
autoFocus
|
||||
autoFocus={index === 0}
|
||||
/>
|
||||
</label>
|
||||
</HStack>
|
||||
|
||||
<div className='poll__cancel'>
|
||||
<IconButton
|
||||
title={intl.formatMessage(messages.remove_option)}
|
||||
src={require('@tabler/icons/icons/x.svg')}
|
||||
onClick={handleOptionRemove}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
{index > 1 && (
|
||||
<div>
|
||||
<Button theme='danger' size='sm' onClick={handleOptionRemove}>Delete</Button>
|
||||
</div>
|
||||
)}
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -162,21 +140,22 @@ const PollForm = (props: IPollForm) => {
|
|||
const maxOptions = pollLimits.get('max_options');
|
||||
const maxOptionChars = pollLimits.get('max_characters_per_option');
|
||||
|
||||
const handleAddOption = () => onAddOption('');
|
||||
|
||||
const handleSelectDuration = (event: React.ChangeEvent<HTMLSelectElement>) =>
|
||||
onChangeSettings(event.target.value, isMultiple);
|
||||
const handleAddOption = () => {
|
||||
// autofocus on new input
|
||||
// use streamfield
|
||||
onAddOption('');
|
||||
};
|
||||
|
||||
const handleSelectDuration = (value: number) => onChangeSettings(value, isMultiple);
|
||||
const handleToggleMultiple = () => onChangeSettings(expiresIn, !isMultiple);
|
||||
|
||||
|
||||
if (!options) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='compose-form__poll-wrapper'>
|
||||
<ul>
|
||||
<Stack space={4}>
|
||||
<Stack space={2}>
|
||||
{options.map((title: string, i: number) => (
|
||||
<Option
|
||||
title={title}
|
||||
|
@ -184,34 +163,63 @@ const PollForm = (props: IPollForm) => {
|
|||
index={i}
|
||||
onChange={onChangeOption}
|
||||
onRemove={onRemoveOption}
|
||||
isPollMultiple={isMultiple}
|
||||
onToggleMultiple={handleToggleMultiple}
|
||||
maxChars={maxOptionChars}
|
||||
numOptions={options.size}
|
||||
{...filteredProps}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<HStack className='text-black dark:text-white' space={2}>
|
||||
{options.size < maxOptions && (
|
||||
<button className='button button-secondary' onClick={handleAddOption}>
|
||||
<Icon src={require('@tabler/icons/icons/plus.svg')} />
|
||||
<FormattedMessage {...messages.add_option} />
|
||||
</button>
|
||||
)}
|
||||
<HStack space={2}>
|
||||
<div className='w-6' />
|
||||
|
||||
<select value={expiresIn} onChange={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>
|
||||
</HStack>
|
||||
</div>
|
||||
{options.size < maxOptions && (
|
||||
<Button
|
||||
theme='secondary'
|
||||
onClick={handleAddOption}
|
||||
size='sm'
|
||||
>
|
||||
<FormattedMessage {...messages.add_option} />
|
||||
</Button>
|
||||
)}
|
||||
</HStack>
|
||||
</Stack>
|
||||
|
||||
<Divider />
|
||||
|
||||
<button onClick={handleToggleMultiple} className='text-left'>
|
||||
<HStack alignItems='center' justifyContent='between'>
|
||||
<Stack>
|
||||
<Text weight='medium'>
|
||||
{intl.formatMessage(messages.multiSelect)}
|
||||
</Text>
|
||||
|
||||
<Text theme='muted' size='sm'>
|
||||
{intl.formatMessage(messages.multiSelectDetail)}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Toggle checked={isMultiple} onChange={handleToggleMultiple} />
|
||||
</HStack>
|
||||
</button>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Duration */}
|
||||
<Stack space={2}>
|
||||
<Text weight='medium'>
|
||||
{intl.formatMessage(messages.pollDuration)}
|
||||
</Text>
|
||||
|
||||
<DurationSelector onDurationChange={handleSelectDuration} />
|
||||
</Stack>
|
||||
|
||||
{/* Remove Poll */}
|
||||
<div className='text-center'>
|
||||
<Button theme='danger' size='sm' onClick={props.onRemovePoll}>
|
||||
{intl.formatMessage(messages.removePoll)}
|
||||
</Button>
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
|
||||
import { render, screen } from '../../../../../jest/test-helpers';
|
||||
import DurationSelector from '../duration-selector';
|
||||
|
||||
describe('<DurationSelector />', () => {
|
||||
it('defaults to 2 days', () => {
|
||||
const handler = jest.fn();
|
||||
render(<DurationSelector onDurationChange={handler} />);
|
||||
|
||||
expect(screen.getByTestId('duration-selector-days')).toHaveValue('2');
|
||||
expect(screen.getByTestId('duration-selector-hours')).toHaveValue('0');
|
||||
expect(screen.getByTestId('duration-selector-minutes')).toHaveValue('0');
|
||||
});
|
||||
|
||||
describe('when changing the day', () => {
|
||||
it('calls the "onDurationChange" callback', async() => {
|
||||
const handler = jest.fn();
|
||||
render(<DurationSelector onDurationChange={handler} />);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
screen.getByTestId('duration-selector-days'),
|
||||
screen.getByRole('option', { name: '1 day' }),
|
||||
);
|
||||
|
||||
expect(handler.mock.calls[0][0]).toEqual(172800); // 2 days
|
||||
expect(handler.mock.calls[1][0]).toEqual(86400); // 1 day
|
||||
});
|
||||
|
||||
it('should disable the hour/minute select if 7 days selected', async() => {
|
||||
const handler = jest.fn();
|
||||
render(<DurationSelector onDurationChange={handler} />);
|
||||
|
||||
expect(screen.getByTestId('duration-selector-hours')).not.toBeDisabled();
|
||||
expect(screen.getByTestId('duration-selector-minutes')).not.toBeDisabled();
|
||||
|
||||
await userEvent.selectOptions(
|
||||
screen.getByTestId('duration-selector-days'),
|
||||
screen.getByRole('option', { name: '7 days' }),
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('duration-selector-hours')).toBeDisabled();
|
||||
expect(screen.getByTestId('duration-selector-minutes')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when changing the hour', () => {
|
||||
it('calls the "onDurationChange" callback', async() => {
|
||||
const handler = jest.fn();
|
||||
render(<DurationSelector onDurationChange={handler} />);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
screen.getByTestId('duration-selector-hours'),
|
||||
screen.getByRole('option', { name: '1 hour' }),
|
||||
);
|
||||
|
||||
expect(handler.mock.calls[0][0]).toEqual(172800); // 2 days
|
||||
expect(handler.mock.calls[1][0]).toEqual(176400); // 2 days, 1 hour
|
||||
});
|
||||
});
|
||||
|
||||
describe('when changing the minute', () => {
|
||||
it('calls the "onDurationChange" callback', async() => {
|
||||
const handler = jest.fn();
|
||||
render(<DurationSelector onDurationChange={handler} />);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
screen.getByTestId('duration-selector-minutes'),
|
||||
screen.getByRole('option', { name: '15 minutes' }),
|
||||
);
|
||||
|
||||
expect(handler.mock.calls[0][0]).toEqual(172800); // 2 days
|
||||
expect(handler.mock.calls[1][0]).toEqual(173700); // 2 days, 1 minute
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,93 @@
|
|||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { Select } from 'soapbox/components/ui';
|
||||
|
||||
const messages = defineMessages({
|
||||
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}}' },
|
||||
});
|
||||
|
||||
interface IDurationSelector {
|
||||
onDurationChange(expiresIn: number): void
|
||||
}
|
||||
|
||||
const DurationSelector = ({ onDurationChange }: IDurationSelector) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const [days, setDays] = useState<number>(2);
|
||||
const [hours, setHours] = useState<number>(0);
|
||||
const [minutes, setMinutes] = useState<number>(0);
|
||||
|
||||
const value = useMemo(() => {
|
||||
const now: any = new Date();
|
||||
const future: any = new Date();
|
||||
now.setDate(now.getDate() + days);
|
||||
now.setMinutes(now.getMinutes() + minutes);
|
||||
now.setHours(now.getHours() + hours);
|
||||
|
||||
return (now - future) / 1000;
|
||||
}, [days, hours, minutes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (days === 7) {
|
||||
setHours(0);
|
||||
setMinutes(0);
|
||||
}
|
||||
}, [days]);
|
||||
|
||||
useEffect(() => {
|
||||
onDurationChange(value);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div className='grid grid-cols-1 gap-y-2 gap-x-2 sm:grid-cols-3'>
|
||||
<div className='sm:col-span-1'>
|
||||
<Select
|
||||
value={days}
|
||||
onChange={(event) => setDays(Number(event.target.value))}
|
||||
data-testid='duration-selector-days'
|
||||
>
|
||||
{[...Array(8).fill(undefined)].map((_, number) => (
|
||||
<option value={number} key={number}>
|
||||
{intl.formatMessage(messages.days, { number })}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className='sm:col-span-1'>
|
||||
<Select
|
||||
value={hours}
|
||||
onChange={(event) => setHours(Number(event.target.value))}
|
||||
disabled={days === 7}
|
||||
data-testid='duration-selector-hours'
|
||||
>
|
||||
{[...Array(24).fill(undefined)].map((_, number) => (
|
||||
<option value={number} key={number}>
|
||||
{intl.formatMessage(messages.hours, { number })}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className='sm:col-span-1'>
|
||||
<Select
|
||||
value={minutes}
|
||||
onChange={(event) => setMinutes(Number(event.target.value))}
|
||||
disabled={days === 7}
|
||||
data-testid='duration-selector-minutes'
|
||||
>
|
||||
{[0, 15, 30, 45].map((number) => (
|
||||
<option value={number} key={number}>
|
||||
{intl.formatMessage(messages.minutes, { number })}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DurationSelector;
|
|
@ -26,6 +26,7 @@ const mapStateToProps = state => {
|
|||
privacy: state.getIn(['compose', 'privacy']),
|
||||
focusDate: state.getIn(['compose', 'focusDate']),
|
||||
caretPosition: state.getIn(['compose', 'caretPosition']),
|
||||
hasPoll: !!state.getIn(['compose', 'poll']),
|
||||
isSubmitting: state.getIn(['compose', 'is_submitting']),
|
||||
isEditing: state.getIn(['compose', 'id']) !== null,
|
||||
isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
|
||||
|
|
|
@ -271,12 +271,12 @@
|
|||
"compose_form.markdown.unmarked": "Post markdown disabled",
|
||||
"compose_form.message": "Message",
|
||||
"compose_form.placeholder": "What's on your mind?",
|
||||
"compose_form.poll.add_option": "Add a choice",
|
||||
"compose_form.poll.add_option": "Add an answer",
|
||||
"compose_form.poll.duration": "Poll duration",
|
||||
"compose_form.poll.option_placeholder": "Choice {number}",
|
||||
"compose_form.poll.remove_option": "Remove this choice",
|
||||
"compose_form.poll.switch_to_multiple": "Change poll to allow multiple choices",
|
||||
"compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
|
||||
"compose_form.poll.option_placeholder": "Answer #{number}",
|
||||
"compose_form.poll.remove_option": "Remove this answer",
|
||||
"compose_form.poll.switch_to_multiple": "Change poll to allow multiple answers",
|
||||
"compose_form.poll.switch_to_single": "Change poll to allow for a single answer",
|
||||
"compose_form.publish": "Post",
|
||||
"compose_form.publish_loud": "{publish}!",
|
||||
"compose_form.schedule": "Schedule",
|
||||
|
|
|
@ -118,49 +118,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.compose-form__poll-wrapper {
|
||||
border-top: 1px solid var(--foreground-color);
|
||||
|
||||
ul {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.button.button-secondary {
|
||||
@apply h-auto py-1.5 px-2.5 text-primary-600 dark:text-primary-400 border-primary-600;
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.poll__text {
|
||||
flex: 0 0 auto;
|
||||
width: calc(100% - (23px + 6px));
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
@apply border border-solid border-primary-600 bg-white dark:bg-slate-800;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
outline: 0;
|
||||
font-family: inherit;
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 8px center;
|
||||
background-size: auto 16px;
|
||||
border-radius: 4px;
|
||||
padding: 6px 10px;
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.icon-button.disabled {
|
||||
color: var(--brand-color);
|
||||
}
|
||||
}
|
||||
|
||||
.muted .poll {
|
||||
color: var(--primary-text-color);
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue