(
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,
diff --git a/app/soapbox/features/compose/components/poll-form.tsx b/app/soapbox/features/compose/components/poll-form.tsx
index 0f8e87387..4939b9e3f 100644
--- a/app/soapbox/features/compose/components/poll-form.tsx
+++ b/app/soapbox/features/compose/components/poll-form.tsx
@@ -1,24 +1,23 @@
'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, HStack, Stack, Text } 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' },
+ 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' },
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' },
+ 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}}' },
@@ -26,7 +25,6 @@ const messages = defineMessages({
interface IOption {
index: number
- isPollMultiple?: boolean
maxChars: number
numOptions: number
onChange(index: number, value: string): void
@@ -35,7 +33,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 +40,6 @@ interface IOption {
const Option = (props: IOption) => {
const {
index,
- isPollMultiple,
maxChars,
numOptions,
onChange,
@@ -51,7 +47,6 @@ const Option = (props: IOption) => {
onFetchSuggestions,
onRemove,
onRemovePoll,
- onToggleMultiple,
suggestions,
title,
} = props;
@@ -68,20 +63,20 @@ const Option = (props: IOption) => {
}
};
- const handleToggleMultiple = (event: React.MouseEvent
| React.KeyboardEvent) => {
- event.preventDefault();
- event.stopPropagation();
+ // const handleToggleMultiple = (event: React.MouseEvent | React.KeyboardEvent) => {
+ // event.preventDefault();
+ // event.stopPropagation();
- onToggleMultiple();
- };
+ // onToggleMultiple();
+ // };
const onSuggestionsClearRequested = () => onClearSuggestions();
- const handleCheckboxKeypress = (event: React.KeyboardEvent) => {
- if (event.key === 'Enter' || event.key === ' ') {
- handleToggleMultiple(event);
- }
- };
+ // const handleCheckboxKeypress = (event: React.KeyboardEvent) => {
+ // if (event.key === 'Enter' || event.key === ' ') {
+ // handleToggleMultiple(event);
+ // }
+ // };
const onSuggestionsFetchRequested = (token: string) => onFetchSuggestions(token);
@@ -92,17 +87,11 @@ const Option = (props: IOption) => {
};
return (
-
-
+
-
-
-
-
+ {index > 1 && (
+
+
+
+ )}
+
);
};
@@ -156,27 +143,21 @@ const PollForm = (props: IPollForm) => {
...filteredProps
} = props;
- const intl = useIntl();
-
const pollLimits = useAppSelector((state) => state.instance.getIn(['configuration', 'polls']) as any);
const maxOptions = pollLimits.get('max_options');
const maxOptionChars = pollLimits.get('max_characters_per_option');
const handleAddOption = () => onAddOption('');
-
- const handleSelectDuration = (event: React.ChangeEvent) =>
- onChangeSettings(event.target.value, isMultiple);
-
- const handleToggleMultiple = () => onChangeSettings(expiresIn, !isMultiple);
-
+ const handleSelectDuration = (value: number) => onChangeSettings(value, isMultiple);
+ // const handleToggleMultiple = () => onChangeSettings(expiresIn, !isMultiple);
if (!options) {
return null;
}
return (
-
-
+
+
{options.map((title: string, i: number) => (
-
- {options.size < maxOptions && (
-
- )}
+
+
-
-
-
+ {options.size < maxOptions && (
+
+ )}
+
+
+
+
+ {/* Duration */}
+
+ Duration
+
+
+
+
+ {/* Remove Poll */}
+
+
+
+
);
};
diff --git a/app/soapbox/features/compose/components/polls/__tests__/duration-selector.test.tsx b/app/soapbox/features/compose/components/polls/__tests__/duration-selector.test.tsx
new file mode 100644
index 000000000..cf689ab43
--- /dev/null
+++ b/app/soapbox/features/compose/components/polls/__tests__/duration-selector.test.tsx
@@ -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('', () => {
+ it('defaults to 2 days', () => {
+ const handler = jest.fn();
+ render();
+
+ 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();
+
+ 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();
+
+ 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();
+
+ 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();
+
+ 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
+ });
+ });
+});
diff --git a/app/soapbox/features/compose/components/polls/duration-selector.tsx b/app/soapbox/features/compose/components/polls/duration-selector.tsx
new file mode 100644
index 000000000..491530d22
--- /dev/null
+++ b/app/soapbox/features/compose/components/polls/duration-selector.tsx
@@ -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(2);
+ const [hours, setHours] = useState(0);
+ const [minutes, setMinutes] = useState(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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default DurationSelector;
diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json
index 74957a487..9f3894ac5 100644
--- a/app/soapbox/locales/en.json
+++ b/app/soapbox/locales/en.json
@@ -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",
diff --git a/app/styles/polls.scss b/app/styles/polls.scss
index 231dbf195..2d0dd4142 100644
--- a/app/styles/polls.scss
+++ b/app/styles/polls.scss
@@ -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);