Merge branch 'input-rtl' into 'main'
Add RTL support to Input and Textarea, delete AutosuggestTextarea Closes #1558 See merge request soapbox-pub/soapbox!2798
This commit is contained in:
commit
04214c5075
11 changed files with 30 additions and 368 deletions
|
@ -152,7 +152,6 @@
|
|||
"react-sparklines": "^1.7.0",
|
||||
"react-sticky-box": "^2.0.0",
|
||||
"react-swipeable-views": "^0.14.0",
|
||||
"react-textarea-autosize": "^8.3.4",
|
||||
"react-virtuoso": "^4.3.11",
|
||||
"redux": "^4.1.1",
|
||||
"redux-immutable": "^4.0.0",
|
||||
|
|
|
@ -7,7 +7,6 @@ import AutosuggestEmoji from 'soapbox/components/autosuggest-emoji';
|
|||
import Icon from 'soapbox/components/icon';
|
||||
import { Input, Portal } from 'soapbox/components/ui';
|
||||
import AutosuggestAccount from 'soapbox/features/compose/components/autosuggest-account';
|
||||
import { isRtl } from 'soapbox/utils/rtl';
|
||||
import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions';
|
||||
|
||||
import type { Menu, MenuItem } from 'soapbox/components/dropdown-menu';
|
||||
|
@ -264,15 +263,9 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
|
|||
render() {
|
||||
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength, menu, theme } = this.props;
|
||||
const { suggestionsHidden } = this.state;
|
||||
const style: React.CSSProperties = { direction: 'ltr' };
|
||||
|
||||
const visible = !suggestionsHidden && (!suggestions.isEmpty() || (menu && value));
|
||||
|
||||
// TODO: convert to functional component and use `useLocale()` hook instead of checking placeholder text.
|
||||
if (isRtl(value) || (!value && placeholder && isRtl(placeholder))) {
|
||||
style.direction = 'rtl';
|
||||
}
|
||||
|
||||
return [
|
||||
<div key='input' className='relative w-full'>
|
||||
<label className='sr-only'>{placeholder}</label>
|
||||
|
@ -291,7 +284,6 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
|
|||
onKeyUp={onKeyUp}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
style={style}
|
||||
aria-autocomplete='list'
|
||||
id={id}
|
||||
maxLength={maxLength}
|
||||
|
|
|
@ -1,288 +0,0 @@
|
|||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import Textarea from 'react-textarea-autosize';
|
||||
|
||||
import { Portal } from 'soapbox/components/ui';
|
||||
import AutosuggestAccount from 'soapbox/features/compose/components/autosuggest-account';
|
||||
import { isRtl } from 'soapbox/utils/rtl';
|
||||
import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions';
|
||||
|
||||
import AutosuggestEmoji from './autosuggest-emoji';
|
||||
|
||||
import type { List as ImmutableList } from 'immutable';
|
||||
import type { Emoji } from 'soapbox/features/emoji';
|
||||
|
||||
interface IAutosuggesteTextarea {
|
||||
id?: string;
|
||||
value: string;
|
||||
suggestions: ImmutableList<string>;
|
||||
disabled: boolean;
|
||||
placeholder: string;
|
||||
onSuggestionSelected: (tokenStart: number, token: string | null, value: string | undefined) => void;
|
||||
onSuggestionsClearRequested: () => void;
|
||||
onSuggestionsFetchRequested: (token: string | number) => void;
|
||||
onChange: React.ChangeEventHandler<HTMLTextAreaElement>;
|
||||
onKeyUp?: React.KeyboardEventHandler<HTMLTextAreaElement>;
|
||||
onKeyDown?: React.KeyboardEventHandler<HTMLTextAreaElement>;
|
||||
onPaste: (files: FileList) => void;
|
||||
autoFocus: boolean;
|
||||
onFocus: () => void;
|
||||
onBlur?: () => void;
|
||||
condensed?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea> {
|
||||
|
||||
textarea: HTMLTextAreaElement | null = null;
|
||||
|
||||
static defaultProps = {
|
||||
autoFocus: true,
|
||||
};
|
||||
|
||||
state = {
|
||||
suggestionsHidden: true,
|
||||
focused: false,
|
||||
selectedSuggestion: 0,
|
||||
lastToken: null,
|
||||
tokenStart: 0,
|
||||
};
|
||||
|
||||
onChange: React.ChangeEventHandler<HTMLTextAreaElement> = (e) => {
|
||||
const [tokenStart, token] = textAtCursorMatchesToken(
|
||||
e.target.value,
|
||||
e.target.selectionStart,
|
||||
['@', ':', '#'],
|
||||
);
|
||||
|
||||
if (token !== null && this.state.lastToken !== token) {
|
||||
this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart });
|
||||
this.props.onSuggestionsFetchRequested(token);
|
||||
} else if (token === null) {
|
||||
this.setState({ lastToken: null });
|
||||
this.props.onSuggestionsClearRequested();
|
||||
}
|
||||
|
||||
this.props.onChange(e);
|
||||
};
|
||||
|
||||
onKeyDown: React.KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
|
||||
const { suggestions, disabled } = this.props;
|
||||
const { selectedSuggestion, suggestionsHidden } = this.state;
|
||||
|
||||
if (disabled) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.which === 229 || (e as any).isComposing) {
|
||||
// Ignore key events during text composition
|
||||
// e.key may be a name of the physical key even in this case (e.x. Safari / Chrome on Mac)
|
||||
return;
|
||||
}
|
||||
|
||||
switch (e.key) {
|
||||
case 'Escape':
|
||||
if (suggestions.size === 0 || suggestionsHidden) {
|
||||
document.querySelector('.ui')?.parentElement?.focus();
|
||||
} else {
|
||||
e.preventDefault();
|
||||
this.setState({ suggestionsHidden: true });
|
||||
}
|
||||
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
if (suggestions.size > 0 && !suggestionsHidden) {
|
||||
e.preventDefault();
|
||||
this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
|
||||
}
|
||||
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
if (suggestions.size > 0 && !suggestionsHidden) {
|
||||
e.preventDefault();
|
||||
this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
|
||||
}
|
||||
|
||||
break;
|
||||
case 'Enter':
|
||||
case 'Tab':
|
||||
// Select suggestion
|
||||
if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (e.defaultPrevented || !this.props.onKeyDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onKeyDown(e);
|
||||
};
|
||||
|
||||
onBlur = () => {
|
||||
this.setState({ suggestionsHidden: true, focused: false });
|
||||
|
||||
if (this.props.onBlur) {
|
||||
this.props.onBlur();
|
||||
}
|
||||
};
|
||||
|
||||
onFocus = () => {
|
||||
this.setState({ focused: true });
|
||||
|
||||
if (this.props.onFocus) {
|
||||
this.props.onFocus();
|
||||
}
|
||||
};
|
||||
|
||||
onSuggestionClick: React.MouseEventHandler<HTMLDivElement> = (e) => {
|
||||
const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index') as any);
|
||||
e.preventDefault();
|
||||
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
|
||||
this.textarea?.focus();
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps: IAutosuggesteTextarea, nextState: any) {
|
||||
// Skip updating when only the lastToken changes so the
|
||||
// cursor doesn't jump around due to re-rendering unnecessarily
|
||||
const lastTokenUpdated = this.state.lastToken !== nextState.lastToken;
|
||||
const valueUpdated = this.props.value !== nextProps.value;
|
||||
|
||||
if (lastTokenUpdated && !valueUpdated) {
|
||||
return false;
|
||||
} else {
|
||||
// https://stackoverflow.com/a/35962835
|
||||
return super.shouldComponentUpdate!.bind(this)(nextProps, nextState, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: IAutosuggesteTextarea, prevState: any) {
|
||||
const { suggestions } = this.props;
|
||||
if (suggestions !== prevProps.suggestions && suggestions.size > 0 && prevState.suggestionsHidden && prevState.focused) {
|
||||
this.setState({ suggestionsHidden: false });
|
||||
}
|
||||
}
|
||||
|
||||
setTextarea: React.Ref<HTMLTextAreaElement> = (c) => {
|
||||
this.textarea = c;
|
||||
};
|
||||
|
||||
onPaste: React.ClipboardEventHandler<HTMLTextAreaElement> = (e) => {
|
||||
if (e.clipboardData && e.clipboardData.files.length === 1) {
|
||||
this.props.onPaste(e.clipboardData.files);
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
renderSuggestion = (suggestion: string | Emoji, i: number) => {
|
||||
const { selectedSuggestion } = this.state;
|
||||
let inner, key;
|
||||
|
||||
if (typeof suggestion === 'object') {
|
||||
inner = <AutosuggestEmoji emoji={suggestion} />;
|
||||
key = suggestion.id;
|
||||
} else if (suggestion[0] === '#') {
|
||||
inner = suggestion;
|
||||
key = suggestion;
|
||||
} else {
|
||||
inner = <AutosuggestAccount id={suggestion} />;
|
||||
key = suggestion;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
key={key}
|
||||
data-index={i}
|
||||
className={clsx({
|
||||
'px-4 py-2.5 text-sm text-gray-700 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gray-100 dark:focus:bg-primary-800 group': true,
|
||||
'bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800': i === selectedSuggestion,
|
||||
})}
|
||||
onMouseDown={this.onSuggestionClick}
|
||||
>
|
||||
{inner}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
setPortalPosition() {
|
||||
if (!this.textarea) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { top, height, left, width } = this.textarea.getBoundingClientRect();
|
||||
|
||||
return {
|
||||
top: top + height,
|
||||
left,
|
||||
width,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, children, condensed, id } = this.props;
|
||||
const { suggestionsHidden } = this.state;
|
||||
const style = { direction: 'ltr', minRows: 10 };
|
||||
|
||||
// TODO: convert to functional component and use `useLocale()` hook instead of checking placeholder text.
|
||||
if (isRtl(value) || (!value && placeholder && isRtl(placeholder))) {
|
||||
style.direction = 'rtl';
|
||||
}
|
||||
|
||||
return [
|
||||
<div key='textarea'>
|
||||
<div className='relative'>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{placeholder}</span>
|
||||
|
||||
<Textarea
|
||||
ref={this.setTextarea}
|
||||
className={clsx('w-full resize-none border-0 px-0 text-gray-800 transition-[min-height] placeholder:text-gray-600 focus:border-0 focus:shadow-none focus:ring-0 motion-reduce:transition-none dark:bg-transparent dark:text-white dark:placeholder:text-gray-600', {
|
||||
'min-h-[40px]': condensed,
|
||||
'min-h-[100px]': !condensed,
|
||||
})}
|
||||
id={id}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
autoFocus={autoFocus}
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
onPaste={this.onPaste}
|
||||
style={style as any}
|
||||
aria-autocomplete='list'
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</div>,
|
||||
|
||||
<Portal key='portal'>
|
||||
<div
|
||||
style={this.setPortalPosition()}
|
||||
className={clsx({
|
||||
'fixed z-1000 shadow bg-white dark:bg-gray-900 rounded-lg py-1 space-y-0 dark:ring-2 dark:ring-primary-700 focus:outline-none': true,
|
||||
hidden: suggestionsHidden || suggestions.isEmpty(),
|
||||
block: !suggestionsHidden && !suggestions.isEmpty(),
|
||||
})}
|
||||
>
|
||||
{suggestions.map(this.renderSuggestion)}
|
||||
</div>
|
||||
</Portal>,
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default AutosuggestTextarea;
|
|
@ -2,6 +2,9 @@ import clsx from 'clsx';
|
|||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { useLocale } from 'soapbox/hooks';
|
||||
import { getTextDirection } from 'soapbox/utils/rtl';
|
||||
|
||||
import Icon from '../icon/icon';
|
||||
import SvgIcon from '../icon/svg-icon';
|
||||
import Tooltip from '../tooltip/tooltip';
|
||||
|
@ -45,6 +48,7 @@ interface IInput extends Pick<React.InputHTMLAttributes<HTMLInputElement>, 'maxL
|
|||
const Input = React.forwardRef<HTMLInputElement, IInput>(
|
||||
(props, ref) => {
|
||||
const intl = useIntl();
|
||||
const locale = useLocale();
|
||||
|
||||
const { type = 'text', icon, className, outerClassName, append, prepend, theme = 'normal', ...filteredProps } = props;
|
||||
|
||||
|
@ -90,10 +94,11 @@ const Input = React.forwardRef<HTMLInputElement, IInput>(
|
|||
'text-gray-600': props.disabled,
|
||||
'rounded-md bg-white dark:bg-gray-900 border-gray-400 dark:border-gray-800': theme === 'normal',
|
||||
'rounded-full bg-gray-200 border-gray-200 dark:bg-gray-800 dark:border-gray-800 focus:bg-white': theme === 'search',
|
||||
'pr-7 rtl:pl-7 rtl:pr-3': isPassword || append,
|
||||
'pr-10 rtl:pl-10 rtl:pr-3': isPassword || append,
|
||||
'pl-8': typeof icon !== 'undefined',
|
||||
'pl-16': typeof prepend !== 'undefined',
|
||||
}, className)}
|
||||
dir={typeof props.value === 'string' ? getTextDirection(props.value, { fallback: locale.direction }) : undefined}
|
||||
/>
|
||||
|
||||
{append ? (
|
||||
|
|
|
@ -2,10 +2,13 @@ import clsx from 'clsx';
|
|||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { useLocale } from 'soapbox/hooks';
|
||||
import { getTextDirection } from 'soapbox/utils/rtl';
|
||||
|
||||
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>, 'id' | 'maxLength' | 'onChange' | 'onKeyDown' | 'onPaste' | 'required' | 'disabled' | 'rows' | 'readOnly'> {
|
||||
/** Put the cursor into the input on mount. */
|
||||
autoFocus?: boolean;
|
||||
/** Allows the textarea height to grow while typing */
|
||||
|
@ -52,6 +55,7 @@ const Textarea = React.forwardRef(({
|
|||
}: ITextarea, ref: React.ForwardedRef<HTMLTextAreaElement>) => {
|
||||
const length = value?.length || 0;
|
||||
const [rows, setRows] = useState<number>(autoGrow ? 1 : 4);
|
||||
const locale = useLocale();
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
if (autoGrow) {
|
||||
|
@ -94,6 +98,7 @@ const Textarea = React.forwardRef(({
|
|||
'text-red-600 border-red-600': hasError,
|
||||
'resize-none': !isResizeable,
|
||||
})}
|
||||
dir={value?.length ? getTextDirection(value, { fallback: locale.direction }) : undefined}
|
||||
/>
|
||||
|
||||
{maxLength && (
|
||||
|
|
|
@ -41,7 +41,7 @@ const Results = ({ accountSearchResult, onSelect }: IResults) => {
|
|||
<Text weight='bold' size='sm' truncate>{account.display_name}</Text>
|
||||
{account.verified && <VerificationBadge />}
|
||||
</div>
|
||||
<Text size='sm' weight='medium' theme='muted' truncate>@{account.acct}</Text>
|
||||
<Text size='sm' weight='medium' theme='muted' direction='ltr' truncate>@{account.acct}</Text>
|
||||
</Stack>
|
||||
</HStack>
|
||||
</button>
|
||||
|
|
|
@ -14,7 +14,6 @@ import {
|
|||
uploadCompose,
|
||||
} from 'soapbox/actions/compose';
|
||||
import AutosuggestInput, { AutoSuggestion } from 'soapbox/components/autosuggest-input';
|
||||
import AutosuggestTextarea from 'soapbox/components/autosuggest-textarea';
|
||||
import { Button, HStack, Stack } from 'soapbox/components/ui';
|
||||
import EmojiPickerDropdown from 'soapbox/features/emoji/containers/emoji-picker-dropdown-container';
|
||||
import { ComposeEditor } from 'soapbox/features/ui/util/async-components';
|
||||
|
@ -82,8 +81,6 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
spoiler,
|
||||
spoiler_text: spoilerText,
|
||||
privacy,
|
||||
focusDate,
|
||||
caretPosition,
|
||||
is_submitting: isSubmitting,
|
||||
is_changing_upload:
|
||||
isChangingUpload,
|
||||
|
@ -104,7 +101,6 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
const firstRender = useRef(true);
|
||||
const formRef = useRef<HTMLDivElement>(null);
|
||||
const spoilerTextRef = useRef<AutosuggestInput>(null);
|
||||
const autosuggestTextareaRef = useRef<AutosuggestTextarea>(null);
|
||||
const editorRef = useRef<LexicalEditor>(null);
|
||||
|
||||
const { isDraggedOver } = useDraggedFiles(formRef);
|
||||
|
@ -172,11 +168,6 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
dispatch(selectComposeSuggestion(id, tokenStart, token, value, ['spoiler_text']));
|
||||
};
|
||||
|
||||
const setCursor = (start: number, end: number = start) => {
|
||||
if (!autosuggestTextareaRef.current?.textarea) return;
|
||||
autosuggestTextareaRef.current.textarea.setSelectionRange(start, end);
|
||||
};
|
||||
|
||||
const handleEmojiPick = (data: Emoji) => {
|
||||
const editor = editorRef.current;
|
||||
if (!editor) return;
|
||||
|
@ -194,18 +185,9 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
spoilerTextRef.current?.input?.focus();
|
||||
};
|
||||
|
||||
const focusTextarea = () => {
|
||||
autosuggestTextareaRef.current?.textarea?.focus();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const length = text.length;
|
||||
document.addEventListener('click', handleClick, true);
|
||||
|
||||
if (length > 0) {
|
||||
setCursor(length); // Set cursor at end
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handleClick, true);
|
||||
};
|
||||
|
@ -213,21 +195,14 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
|
||||
useEffect(() => {
|
||||
if (spoiler && firstRender.current) {
|
||||
focusTextarea();
|
||||
firstRender.current = false;
|
||||
} else if (!spoiler && prevSpoiler) {
|
||||
focusTextarea();
|
||||
//
|
||||
} else if (spoiler && !prevSpoiler) {
|
||||
focusSpoilerInput();
|
||||
}
|
||||
}, [spoiler]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof caretPosition === 'number') {
|
||||
setCursor(caretPosition);
|
||||
}
|
||||
}, [focusDate]);
|
||||
|
||||
const renderButtons = useCallback(() => (
|
||||
<HStack alignItems='center' space={2}>
|
||||
{features.media && <UploadButtonContainer composeId={id} />}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import debounce from 'lodash/debounce';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
import Textarea from 'react-textarea-autosize';
|
||||
|
||||
import { submitAccountNote } from 'soapbox/actions/account-notes';
|
||||
import { HStack, Text, Widget } from 'soapbox/components/ui';
|
||||
import { HStack, Text, Textarea, Widget } from 'soapbox/components/ui';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
|
||||
import type { Account as AccountEntity } from 'soapbox/schemas';
|
||||
|
@ -64,14 +63,17 @@ const AccountNotePanel: React.FC<IAccountNotePanel> = ({ account }) => {
|
|||
)}
|
||||
</HStack>}
|
||||
>
|
||||
<Textarea
|
||||
id={`account-note-${account.id}`}
|
||||
className='mx-[-8px] w-full resize-none rounded-md border-0 bg-transparent p-2 text-sm text-gray-800 transition-colors placeholder:text-gray-600 focus:border-0 focus:bg-white focus:shadow-none focus:ring-0 motion-reduce:transition-none dark:text-white dark:placeholder:text-gray-600 focus:dark:bg-primary-900'
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
value={value || ''}
|
||||
onChange={handleChange}
|
||||
ref={textarea}
|
||||
/>
|
||||
<div className='-mx-2'>
|
||||
<Textarea
|
||||
id={`account-note-${account.id}`}
|
||||
theme='transparent'
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
value={value || ''}
|
||||
onChange={handleChange}
|
||||
ref={textarea}
|
||||
autoGrow
|
||||
/>
|
||||
</div>
|
||||
</Widget>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -2,21 +2,19 @@ import { getLocale } from 'soapbox/actions/settings';
|
|||
|
||||
import { useAppSelector } from './useAppSelector';
|
||||
|
||||
import type { CSSProperties } from 'react';
|
||||
|
||||
/** Locales which should be presented in right-to-left. */
|
||||
const RTL_LOCALES = ['ar', 'ckb', 'fa', 'he'];
|
||||
|
||||
interface UseLocaleResult {
|
||||
locale: string;
|
||||
direction: CSSProperties['direction'];
|
||||
direction: 'ltr' | 'rtl';
|
||||
}
|
||||
|
||||
/** Get valid locale from settings. */
|
||||
const useLocale = (fallback = 'en'): UseLocaleResult => {
|
||||
const locale = useAppSelector((state) => getLocale(state, fallback));
|
||||
|
||||
const direction: CSSProperties['direction'] =
|
||||
const direction: 'ltr' | 'rtl' =
|
||||
RTL_LOCALES.includes(locale)
|
||||
? 'rtl'
|
||||
: 'ltr';
|
||||
|
|
|
@ -45,7 +45,7 @@ function isRtl(text: string, confidence = 0.3): boolean {
|
|||
|
||||
interface GetTextDirectionOpts {
|
||||
/** The default direction to return if the text is empty. */
|
||||
fallback?: 'ltr' | 'rtl';
|
||||
fallback?: 'ltr' | 'rtl' | undefined;
|
||||
/** The confidence threshold (0-1) to use when determining the direction. */
|
||||
confidence?: number;
|
||||
}
|
||||
|
|
28
yarn.lock
28
yarn.lock
|
@ -1051,7 +1051,7 @@
|
|||
dependencies:
|
||||
regenerator-runtime "^0.12.0"
|
||||
|
||||
"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.2.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
|
||||
"@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.2.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
|
||||
version "7.22.15"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.15.tgz#38f46494ccf6cf020bd4eed7124b425e83e523b8"
|
||||
integrity sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==
|
||||
|
@ -7546,15 +7546,6 @@ react-swipeable-views@^0.14.0:
|
|||
react-swipeable-views-utils "^0.14.0"
|
||||
warning "^4.0.1"
|
||||
|
||||
react-textarea-autosize@^8.3.4:
|
||||
version "8.3.4"
|
||||
resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.3.4.tgz#270a343de7ad350534141b02c9cb78903e553524"
|
||||
integrity sha512-CdtmP8Dc19xL8/R6sWvtknD/eCXkQr30dtvC4VmGInhRsfF8X/ihXCq6+9l9qbxmKRiq407/7z5fxE7cVWQNgQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.2"
|
||||
use-composed-ref "^1.3.0"
|
||||
use-latest "^1.2.1"
|
||||
|
||||
react-transition-group@^2.2.1:
|
||||
version "2.9.0"
|
||||
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d"
|
||||
|
@ -8913,23 +8904,6 @@ url-parse@^1.5.3:
|
|||
querystringify "^2.1.1"
|
||||
requires-port "^1.0.0"
|
||||
|
||||
use-composed-ref@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.3.0.tgz#3d8104db34b7b264030a9d916c5e94fbe280dbda"
|
||||
integrity sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==
|
||||
|
||||
use-isomorphic-layout-effect@^1.1.1:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb"
|
||||
integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==
|
||||
|
||||
use-latest@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/use-latest/-/use-latest-1.2.1.tgz#d13dfb4b08c28e3e33991546a2cee53e14038cf2"
|
||||
integrity sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==
|
||||
dependencies:
|
||||
use-isomorphic-layout-effect "^1.1.1"
|
||||
|
||||
use-sync-external-store@^1.0.0, use-sync-external-store@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
||||
|
|
Loading…
Reference in a new issue