Merge branch 'rtl-improvements' into 'develop'
RTL improvments Closes #1246 See merge request soapbox-pub/soapbox!2028
This commit is contained in:
commit
b2676dec0d
|
@ -8,7 +8,6 @@ import AutosuggestEmoji, { Emoji } from 'soapbox/components/autosuggest-emoji';
|
||||||
import Icon from 'soapbox/components/icon';
|
import Icon from 'soapbox/components/icon';
|
||||||
import { Input } from 'soapbox/components/ui';
|
import { Input } from 'soapbox/components/ui';
|
||||||
import AutosuggestAccount from 'soapbox/features/compose/components/autosuggest-account';
|
import AutosuggestAccount from 'soapbox/features/compose/components/autosuggest-account';
|
||||||
import { isRtl } from 'soapbox/rtl';
|
|
||||||
import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions';
|
import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions';
|
||||||
|
|
||||||
import type { Menu, MenuItem } from 'soapbox/components/dropdown-menu';
|
import type { Menu, MenuItem } from 'soapbox/components/dropdown-menu';
|
||||||
|
@ -264,14 +263,8 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
|
||||||
render() {
|
render() {
|
||||||
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength, menu, theme } = this.props;
|
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength, menu, theme } = this.props;
|
||||||
const { suggestionsHidden } = this.state;
|
const { suggestionsHidden } = this.state;
|
||||||
const style: React.CSSProperties = { direction: 'ltr' };
|
|
||||||
|
|
||||||
const visible = !suggestionsHidden && (!suggestions.isEmpty() || (menu && value));
|
const visible = !suggestionsHidden && (!suggestions.isEmpty() || (menu && value));
|
||||||
|
|
||||||
if (isRtl(value)) {
|
|
||||||
style.direction = 'rtl';
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
<div key='input' className='relative w-full'>
|
<div key='input' className='relative w-full'>
|
||||||
<label className='sr-only'>{placeholder}</label>
|
<label className='sr-only'>{placeholder}</label>
|
||||||
|
@ -290,7 +283,6 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
|
||||||
onKeyUp={onKeyUp}
|
onKeyUp={onKeyUp}
|
||||||
onFocus={this.onFocus}
|
onFocus={this.onFocus}
|
||||||
onBlur={this.onBlur}
|
onBlur={this.onBlur}
|
||||||
style={style}
|
|
||||||
aria-autocomplete='list'
|
aria-autocomplete='list'
|
||||||
id={id}
|
id={id}
|
||||||
maxLength={maxLength}
|
maxLength={maxLength}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import Textarea from 'react-textarea-autosize';
|
||||||
import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions';
|
import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions';
|
||||||
|
|
||||||
import AutosuggestAccount from '../features/compose/components/autosuggest-account';
|
import AutosuggestAccount from '../features/compose/components/autosuggest-account';
|
||||||
import { isRtl } from '../rtl';
|
|
||||||
|
|
||||||
import AutosuggestEmoji, { Emoji } from './autosuggest-emoji';
|
import AutosuggestEmoji, { Emoji } from './autosuggest-emoji';
|
||||||
|
|
||||||
|
@ -227,11 +226,6 @@ class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea>
|
||||||
render() {
|
render() {
|
||||||
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, children, condensed, id } = this.props;
|
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, children, condensed, id } = this.props;
|
||||||
const { suggestionsHidden } = this.state;
|
const { suggestionsHidden } = this.state;
|
||||||
const style = { direction: 'ltr', minRows: 10 };
|
|
||||||
|
|
||||||
if (isRtl(value)) {
|
|
||||||
style.direction = 'rtl';
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
<div key='textarea'>
|
<div key='textarea'>
|
||||||
|
@ -256,7 +250,6 @@ class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea>
|
||||||
onFocus={this.onFocus}
|
onFocus={this.onFocus}
|
||||||
onBlur={this.onBlur}
|
onBlur={this.onBlur}
|
||||||
onPaste={this.onPaste}
|
onPaste={this.onPaste}
|
||||||
style={style as any}
|
|
||||||
aria-autocomplete='list'
|
aria-autocomplete='list'
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { ToastText, ToastType } from 'soapbox/toast';
|
import { ToastText, ToastType } from 'soapbox/toast';
|
||||||
|
|
||||||
|
import HStack from '../hstack/hstack';
|
||||||
import Icon from '../icon/icon';
|
import Icon from '../icon/icon';
|
||||||
|
|
||||||
const renderText = (text: ToastText) => {
|
const renderText = (text: ToastText) => {
|
||||||
|
@ -63,7 +64,7 @@ const Toast = (props: IToast) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderAction = () => {
|
const renderAction = () => {
|
||||||
const classNames = 'ml-3 mt-0.5 flex-shrink-0 rounded-full text-sm font-medium text-primary-600 dark:text-accent-blue hover:underline focus:outline-none';
|
const classNames = 'mt-0.5 flex-shrink-0 rounded-full text-sm font-medium text-primary-600 dark:text-accent-blue hover:underline focus:outline-none';
|
||||||
|
|
||||||
if (action && actionLabel) {
|
if (action && actionLabel) {
|
||||||
return (
|
return (
|
||||||
|
@ -102,43 +103,41 @@ const Toast = (props: IToast) => {
|
||||||
data-testid='toast'
|
data-testid='toast'
|
||||||
className={
|
className={
|
||||||
classNames({
|
classNames({
|
||||||
'pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white dark:bg-gray-900 shadow-lg dark:ring-2 dark:ring-gray-800': true,
|
'p-4 pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white dark:bg-gray-900 shadow-lg dark:ring-2 dark:ring-gray-800': true,
|
||||||
'animate-enter': t.visible,
|
'animate-enter': t.visible,
|
||||||
'animate-leave': !t.visible,
|
'animate-leave': !t.visible,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className='p-4'>
|
<HStack space={4} alignItems='start'>
|
||||||
<div className='flex items-start'>
|
<HStack space={3} justifyContent='between' alignItems='start' className='w-0 flex-1'>
|
||||||
<div className='flex w-0 flex-1 justify-between items-start'>
|
<HStack space={3} alignItems='start' className='w-0 flex-1'>
|
||||||
<div className='w-0 flex-1 flex items-start'>
|
<div className='flex-shrink-0'>
|
||||||
<div className='flex-shrink-0'>
|
{renderIcon()}
|
||||||
{renderIcon()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className='ml-3 pt-0.5 text-sm text-gray-900 dark:text-gray-100' data-testid='toast-message'>
|
|
||||||
{renderText(message)}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action */}
|
<p className='pt-0.5 text-sm text-gray-900 dark:text-gray-100' data-testid='toast-message'>
|
||||||
{renderAction()}
|
{renderText(message)}
|
||||||
</div>
|
</p>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
{/* Dismiss Button */}
|
{/* Action */}
|
||||||
<div className='ml-4 flex flex-shrink-0 pt-0.5'>
|
{renderAction()}
|
||||||
<button
|
</HStack>
|
||||||
type='button'
|
|
||||||
className='inline-flex rounded-md text-gray-600 dark:text-gray-600 hover:text-gray-700 dark:hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2'
|
{/* Dismiss Button */}
|
||||||
onClick={dismissToast}
|
<div className='flex flex-shrink-0 pt-0.5'>
|
||||||
data-testid='toast-dismiss'
|
<button
|
||||||
>
|
type='button'
|
||||||
<span className='sr-only'>Close</span>
|
className='inline-flex rounded-md text-gray-600 dark:text-gray-600 hover:text-gray-700 dark:hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2'
|
||||||
<Icon src={require('@tabler/icons/x.svg')} className='w-5 h-5' />
|
onClick={dismissToast}
|
||||||
</button>
|
data-testid='toast-dismiss'
|
||||||
</div>
|
>
|
||||||
|
<span className='sr-only'>Close</span>
|
||||||
|
<Icon src={require('@tabler/icons/x.svg')} className='w-5 h-5' />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</HStack>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -53,8 +53,6 @@ import ErrorBoundary from '../components/error-boundary';
|
||||||
import UI from '../features/ui';
|
import UI from '../features/ui';
|
||||||
import { store } from '../store';
|
import { store } from '../store';
|
||||||
|
|
||||||
const RTL_LOCALES = ['ar', 'ckb', 'fa', 'he'];
|
|
||||||
|
|
||||||
// Configure global functions for developers
|
// Configure global functions for developers
|
||||||
createGlobals(store);
|
createGlobals(store);
|
||||||
|
|
||||||
|
@ -211,7 +209,7 @@ const SoapboxLoad: React.FC<ISoapboxLoad> = ({ children }) => {
|
||||||
const me = useAppSelector(state => state.me);
|
const me = useAppSelector(state => state.me);
|
||||||
const account = useOwnAccount();
|
const account = useOwnAccount();
|
||||||
const swUpdating = useAppSelector(state => state.meta.swUpdating);
|
const swUpdating = useAppSelector(state => state.meta.swUpdating);
|
||||||
const locale = useLocale();
|
const { locale } = useLocale();
|
||||||
|
|
||||||
const [messages, setMessages] = useState<Record<string, string>>({});
|
const [messages, setMessages] = useState<Record<string, string>>({});
|
||||||
const [localeLoading, setLocaleLoading] = useState(true);
|
const [localeLoading, setLocaleLoading] = useState(true);
|
||||||
|
@ -262,7 +260,7 @@ interface ISoapboxHead {
|
||||||
|
|
||||||
/** Injects metadata into site head with Helmet. */
|
/** Injects metadata into site head with Helmet. */
|
||||||
const SoapboxHead: React.FC<ISoapboxHead> = ({ children }) => {
|
const SoapboxHead: React.FC<ISoapboxHead> = ({ children }) => {
|
||||||
const locale = useLocale();
|
const { locale, direction } = useLocale();
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
const soapboxConfig = useSoapboxConfig();
|
const soapboxConfig = useSoapboxConfig();
|
||||||
|
|
||||||
|
@ -281,7 +279,7 @@ const SoapboxHead: React.FC<ISoapboxHead> = ({ children }) => {
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<html lang={locale} className={classNames('h-full', { dark: darkMode })} />
|
<html lang={locale} className={classNames('h-full', { dark: darkMode })} />
|
||||||
<body className={bodyClass} dir={RTL_LOCALES.includes(locale) ? 'rtl' : undefined} />
|
<body className={bodyClass} dir={direction} />
|
||||||
{themeCss && <style id='theme' type='text/css'>{`:root{${themeCss}}`}</style>}
|
{themeCss && <style id='theme' type='text/css'>{`:root{${themeCss}}`}</style>}
|
||||||
{darkMode && <style type='text/css'>{':root { color-scheme: dark; }'}</style>}
|
{darkMode && <style type='text/css'>{':root { color-scheme: dark; }'}</style>}
|
||||||
<meta name='theme-color' content={soapboxConfig.brandColor} />
|
<meta name='theme-color' content={soapboxConfig.brandColor} />
|
||||||
|
|
|
@ -2,15 +2,38 @@ import MESSAGES from 'soapbox/locales/messages';
|
||||||
|
|
||||||
import { useSettings } from './useSettings';
|
import { useSettings } from './useSettings';
|
||||||
|
|
||||||
|
import type { CSSProperties } from 'react';
|
||||||
|
|
||||||
|
/** Locales which should be presented in right-to-left. */
|
||||||
|
const RTL_LOCALES = ['ar', 'ckb', 'fa', 'he'];
|
||||||
|
|
||||||
/** Ensure the given locale exists in our codebase */
|
/** Ensure the given locale exists in our codebase */
|
||||||
const validLocale = (locale: string): boolean => Object.keys(MESSAGES).includes(locale);
|
const validLocale = (locale: string): boolean => Object.keys(MESSAGES).includes(locale);
|
||||||
|
|
||||||
/** Get valid locale from settings. */
|
interface UseLocaleResult {
|
||||||
const useLocale = (fallback = 'en') => {
|
locale: string
|
||||||
const settings = useSettings();
|
direction: CSSProperties['direction']
|
||||||
const locale = settings.get('locale');
|
}
|
||||||
|
|
||||||
return validLocale(locale) ? locale : fallback;
|
/** Get valid locale from settings. */
|
||||||
|
const useLocale = (fallback = 'en'): UseLocaleResult => {
|
||||||
|
const settings = useSettings();
|
||||||
|
const userLocale = settings.get('locale') as unknown;
|
||||||
|
|
||||||
|
const locale =
|
||||||
|
(typeof userLocale === 'string' && validLocale(userLocale))
|
||||||
|
? userLocale
|
||||||
|
: fallback;
|
||||||
|
|
||||||
|
const direction: CSSProperties['direction'] =
|
||||||
|
RTL_LOCALES.includes(locale)
|
||||||
|
? 'rtl'
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
locale,
|
||||||
|
direction,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export { useLocale };
|
export { useLocale };
|
||||||
|
|
|
@ -137,6 +137,10 @@
|
||||||
@apply bg-primary-50 hover:bg-primary-100 dark:bg-gray-700 dark:hover:bg-gray-600 text-primary-600 dark:text-primary-400;
|
@apply bg-primary-50 hover:bg-primary-100 dark:bg-gray-700 dark:hover:bg-gray-600 text-primary-600 dark:text-primary-400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.react-datepicker__close-icon {
|
||||||
|
@apply rtl:left-0 rtl:right-auto rtl:pr-0 rtl:pl-[6px];
|
||||||
|
}
|
||||||
|
|
||||||
.react-datepicker__close-icon::after {
|
.react-datepicker__close-icon::after {
|
||||||
@apply bg-transparent text-gray-600 dark:text-gray-400 text-base;
|
@apply bg-transparent text-gray-600 dark:text-gray-400 text-base;
|
||||||
font-family: 'Font Awesome 5 Free';
|
font-family: 'Font Awesome 5 Free';
|
||||||
|
|
Loading…
Reference in New Issue