Merge branch 'textarea-counter' into 'develop'

Textarea: add a character counter

See merge request soapbox-pub/soapbox!2373
develop^2
Alex Gleason 2023-03-30 00:30:39 +00:00
commit 8e6dfe6395
4 zmienionych plików z 43 dodań i 15 usunięć

Wyświetl plik

@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Reactions: Support custom emoji reactions - Reactions: Support custom emoji reactions
- Compatbility: Support Mastodon v2 timeline filters. - Compatbility: Support Mastodon v2 timeline filters.
- Posts: Support dislikes on Friendica. - Posts: Support dislikes on Friendica.
- UI: added a character counter to some textareas.
### Changed ### Changed
- Posts: truncate Nostr pubkeys in reply mentions. - Posts: truncate Nostr pubkeys in reply mentions.

Wyświetl plik

@ -1,5 +1,9 @@
import clsx from 'clsx'; import clsx from 'clsx';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import Stack from '../stack/stack';
import Text from '../text/text';
interface ITextarea extends Pick<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'maxLength' | 'onChange' | 'onKeyDown' | 'onPaste' | 'required' | 'disabled' | 'rows' | 'readOnly'> { interface ITextarea extends Pick<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'maxLength' | 'onChange' | 'onKeyDown' | 'onPaste' | 'required' | 'disabled' | 'rows' | 'readOnly'> {
/** Put the cursor into the input on mount. */ /** Put the cursor into the input on mount. */
@ -28,6 +32,8 @@ interface ITextarea extends Pick<React.TextareaHTMLAttributes<HTMLTextAreaElemen
isResizeable?: boolean isResizeable?: boolean
/** Textarea theme. */ /** Textarea theme. */
theme?: 'default' | 'transparent' theme?: 'default' | 'transparent'
/** Whether to display a character counter below the textarea. */
withCounter?: boolean
} }
/** Textarea with custom styles. */ /** Textarea with custom styles. */
@ -40,8 +46,11 @@ const Textarea = React.forwardRef(({
maxRows = 10, maxRows = 10,
minRows = 1, minRows = 1,
theme = 'default', theme = 'default',
maxLength,
value,
...props ...props
}: ITextarea, ref: React.ForwardedRef<HTMLTextAreaElement>) => { }: ITextarea, ref: React.ForwardedRef<HTMLTextAreaElement>) => {
const length = value?.length || 0;
const [rows, setRows] = useState<number>(autoGrow ? 1 : 4); const [rows, setRows] = useState<number>(autoGrow ? 1 : 4);
const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => { const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
@ -70,20 +79,35 @@ const Textarea = React.forwardRef(({
}; };
return ( return (
<textarea <Stack space={1.5}>
{...props} <textarea
ref={ref} {...props}
rows={rows} value={value}
onChange={handleChange} ref={ref}
className={clsx('block w-full rounded-md text-gray-900 placeholder:text-gray-600 dark:text-gray-100 dark:placeholder:text-gray-600 sm:text-sm', { rows={rows}
'bg-white dark:bg-transparent shadow-sm border-gray-400 dark:border-gray-800 dark:ring-1 dark:ring-gray-800 focus:ring-primary-500 focus:border-primary-500 dark:focus:ring-primary-500 dark:focus:border-primary-500': onChange={handleChange}
theme === 'default', className={clsx('block w-full rounded-md text-gray-900 placeholder:text-gray-600 dark:text-gray-100 dark:placeholder:text-gray-600 sm:text-sm', {
'bg-transparent border-0 focus:border-0 focus:ring-0': theme === 'transparent', 'bg-white dark:bg-transparent shadow-sm border-gray-400 dark:border-gray-800 dark:ring-1 dark:ring-gray-800 focus:ring-primary-500 focus:border-primary-500 dark:focus:ring-primary-500 dark:focus:border-primary-500':
'font-mono': isCodeEditor, theme === 'default',
'text-red-600 border-red-600': hasError, 'bg-transparent border-0 focus:border-0 focus:ring-0': theme === 'transparent',
'resize-none': !isResizeable, 'font-mono': isCodeEditor,
})} 'text-red-600 border-red-600': hasError,
/> 'resize-none': !isResizeable,
})}
/>
{maxLength && (
<div className='text-right rtl:text-left'>
<Text size='xs' theme={maxLength - length < 0 ? 'danger' : 'muted'}>
<FormattedMessage
id='textarea.counter.label'
defaultMessage='{count} characters remaining'
values={{ count: maxLength - length }}
/>
</Text>
</div>
)}
</Stack>
); );
}, },
); );

Wyświetl plik

@ -28,6 +28,7 @@ const messages = defineMessages({
newsletter: { id: 'registration.newsletter', defaultMessage: 'Subscribe to newsletter.' }, newsletter: { id: 'registration.newsletter', defaultMessage: 'Subscribe to newsletter.' },
needsConfirmationHeader: { id: 'confirmations.register.needs_confirmation.header', defaultMessage: 'Confirmation needed' }, needsConfirmationHeader: { id: 'confirmations.register.needs_confirmation.header', defaultMessage: 'Confirmation needed' },
needsApprovalHeader: { id: 'confirmations.register.needs_approval.header', defaultMessage: 'Approval needed' }, needsApprovalHeader: { id: 'confirmations.register.needs_approval.header', defaultMessage: 'Approval needed' },
reasonHint: { id: 'registration.reason_hint', defaultMessage: 'This will help us review your application' },
}); });
interface IRegistrationForm { interface IRegistrationForm {
@ -296,13 +297,14 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
{needsApproval && ( {needsApproval && (
<FormGroup <FormGroup
labelText={<FormattedMessage id='registration.reason' defaultMessage='Why do you want to join?' />} labelText={<FormattedMessage id='registration.reason' defaultMessage='Why do you want to join?' />}
hintText={<FormattedMessage id='registration.reason_hint' defaultMessage='This will help us review your application' />}
> >
<Textarea <Textarea
name='reason' name='reason'
placeholder={intl.formatMessage(messages.reasonHint)}
maxLength={500} maxLength={500}
onChange={onInputChange} onChange={onInputChange}
value={params.get('reason', '')} value={params.get('reason', '')}
autoGrow
required required
/> />
</FormGroup> </FormGroup>

Wyświetl plik

@ -1481,6 +1481,7 @@
"tabs_bar.profile": "Profile", "tabs_bar.profile": "Profile",
"tabs_bar.search": "Search", "tabs_bar.search": "Search",
"tabs_bar.settings": "Settings", "tabs_bar.settings": "Settings",
"textarea.counter.label": "{count} characters remaining",
"theme_editor.Reset": "Reset", "theme_editor.Reset": "Reset",
"theme_editor.export": "Export theme", "theme_editor.export": "Export theme",
"theme_editor.import": "Import theme", "theme_editor.import": "Import theme",