Compose form design changes
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
@ -22,9 +22,10 @@ interface IDropdownMenuItem {
|
||||
index: number;
|
||||
item: MenuItem | null;
|
||||
onClick?(): void;
|
||||
autoFocus?: boolean;
|
||||
}
|
||||
|
||||
const DropdownMenuItem = ({ index, item, onClick }: IDropdownMenuItem) => {
|
||||
const DropdownMenuItem = ({ index, item, onClick, autoFocus }: IDropdownMenuItem) => {
|
||||
const history = useHistory();
|
||||
|
||||
const itemRef = useRef<HTMLAnchorElement>(null);
|
||||
@ -63,7 +64,7 @@ const DropdownMenuItem = ({ index, item, onClick }: IDropdownMenuItem) => {
|
||||
useEffect(() => {
|
||||
const firstItem = index === 0;
|
||||
|
||||
if (itemRef.current && firstItem) {
|
||||
if (itemRef.current && (autoFocus ? firstItem : item?.active)) {
|
||||
itemRef.current.focus({ preventScroll: true });
|
||||
}
|
||||
}, [itemRef.current, index]);
|
||||
|
||||
@ -262,6 +262,8 @@ const DropdownMenu = (props: IDropdownMenu) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const autoFocus = !items.some((item) => item?.active);
|
||||
|
||||
return (
|
||||
<>
|
||||
{children ? (
|
||||
@ -309,6 +311,7 @@ const DropdownMenu = (props: IDropdownMenu) => {
|
||||
item={item}
|
||||
index={idx}
|
||||
onClick={handleClose}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@ -8,19 +8,16 @@ import { useButtonStyles } from './useButtonStyles';
|
||||
|
||||
import type { ButtonSizes, ButtonThemes } from './useButtonStyles';
|
||||
|
||||
interface IButton {
|
||||
interface IButton extends Pick<
|
||||
React.ComponentProps<'button'>,
|
||||
'children' | 'className' | 'disabled' | 'onClick' | 'onMouseDown' | 'onKeyDown' | 'title' | 'type'
|
||||
> {
|
||||
/** Whether this button expands the width of its container. */
|
||||
block?: boolean;
|
||||
/** Elements inside the <button> */
|
||||
children?: React.ReactNode;
|
||||
/** Extra class names for the button. */
|
||||
className?: string;
|
||||
/** Prevent the button from being clicked. */
|
||||
disabled?: boolean;
|
||||
/** URL to an SVG icon to render inside the button. */
|
||||
icon?: string;
|
||||
/** Action when the button is clicked. */
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
/** URL to an SVG icon to render inside the button next to the text. */
|
||||
secondaryIcon?: string;
|
||||
/** A predefined button size. */
|
||||
size?: ButtonSizes;
|
||||
/** Text inside the button. Takes precedence over `children`. */
|
||||
@ -29,26 +26,24 @@ interface IButton {
|
||||
to?: string;
|
||||
/** Styles the button visually with a predefined theme. */
|
||||
theme?: ButtonThemes;
|
||||
/** Whether this button should submit a form by default. */
|
||||
type?: 'button' | 'submit';
|
||||
}
|
||||
|
||||
/** Customizable button element with various themes. */
|
||||
const Button = React.forwardRef<HTMLButtonElement, IButton>((props, ref): JSX.Element => {
|
||||
const {
|
||||
block = false,
|
||||
children,
|
||||
disabled = false,
|
||||
icon,
|
||||
onClick,
|
||||
size = 'md',
|
||||
text,
|
||||
theme = 'secondary',
|
||||
to,
|
||||
type = 'button',
|
||||
className,
|
||||
} = props;
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, IButton>(({
|
||||
block = false,
|
||||
children,
|
||||
disabled = false,
|
||||
icon,
|
||||
secondaryIcon,
|
||||
onClick,
|
||||
size = 'md',
|
||||
text,
|
||||
theme = 'secondary',
|
||||
to,
|
||||
type = 'button',
|
||||
className,
|
||||
...props
|
||||
}, ref): JSX.Element => {
|
||||
const body = text || children;
|
||||
|
||||
const themeClass = useButtonStyles({
|
||||
@ -58,14 +53,6 @@ const Button = React.forwardRef<HTMLButtonElement, IButton>((props, ref): JSX.El
|
||||
size,
|
||||
});
|
||||
|
||||
const renderIcon = () => {
|
||||
if (!icon) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Icon src={icon} className='h-4 w-4' />;
|
||||
};
|
||||
|
||||
const handleClick: React.MouseEventHandler<HTMLButtonElement> = React.useCallback((event) => {
|
||||
if (onClick && !disabled) {
|
||||
onClick(event);
|
||||
@ -74,18 +61,21 @@ const Button = React.forwardRef<HTMLButtonElement, IButton>((props, ref): JSX.El
|
||||
|
||||
const renderButton = () => (
|
||||
<button
|
||||
className={clsx('space-x-2 rtl:space-x-reverse', themeClass, className)}
|
||||
{...props}
|
||||
className={clsx('rtl:space-x-reverse', themeClass, className)}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
ref={ref}
|
||||
type={type}
|
||||
data-testid='button'
|
||||
>
|
||||
{renderIcon()}
|
||||
{icon ? <Icon src={icon} className='h-4 w-4' /> : null}
|
||||
|
||||
{body && (
|
||||
<span>{body}</span>
|
||||
)}
|
||||
|
||||
{secondaryIcon ? <Icon src={secondaryIcon} className='h-4 w-4' /> : null}
|
||||
</button>
|
||||
);
|
||||
|
||||
|
||||
@ -11,14 +11,14 @@ const themes = {
|
||||
danger: 'border-transparent bg-danger-100 dark:bg-danger-900 text-danger-600 dark:text-danger-200 hover:bg-danger-600 hover:text-gray-100 dark:hover:text-gray-100 dark:hover:bg-danger-500 focus:ring-danger-500',
|
||||
transparent: 'border-transparent bg-transparent text-primary-600 dark:text-accent-blue dark:bg-transparent hover:bg-gray-200 dark:hover:bg-gray-800/50',
|
||||
outline: 'border-gray-100 border-2 bg-transparent text-gray-100 hover:bg-white/10',
|
||||
muted: 'border border-solid bg-transparent border-gray-400 dark:border-gray-800 hover:border-primary-300 dark:hover:border-primary-700 focus:border-primary-500 text-gray-900 dark:text-gray-100 focus:ring-primary-500',
|
||||
muted: 'border border-solid bg-transparent border-gray-400 dark:border-gray-800 hover:border-primary-300 dark:hover:border-primary-700 focus:border-primary-500 text-gray-800 dark:text-gray-100 focus:ring-primary-500',
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
xs: 'px-3 py-1 text-xs',
|
||||
sm: 'px-3 py-1.5 text-xs leading-4',
|
||||
md: 'px-4 py-2 text-sm',
|
||||
lg: 'px-6 py-3 text-base',
|
||||
xs: 'gap-x-1.5 px-2 py-1 text-xs',
|
||||
sm: 'gap-x-2 px-3 py-1.5 text-xs leading-4',
|
||||
md: 'gap-x-2 px-4 py-2 text-sm',
|
||||
lg: 'gap-x-2 px-6 py-3 text-base',
|
||||
};
|
||||
|
||||
type ButtonSizes = keyof typeof sizes
|
||||
|
||||
@ -26,7 +26,7 @@ import WarningContainer from '../containers/warning-container';
|
||||
import { $createEmojiNode } from '../editor/nodes/emoji-node';
|
||||
import { countableText } from '../util/counter';
|
||||
|
||||
import MarkdownButton from './markdown-button';
|
||||
import ContentTypeButton from './content-type-button';
|
||||
import PollButton from './poll-button';
|
||||
import PollForm from './polls/poll-form';
|
||||
import PrivacyDropdown from './privacy-dropdown';
|
||||
@ -197,10 +197,8 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||
{features.media && <UploadButtonContainer composeId={id} />}
|
||||
<EmojiPickerDropdown onPickEmoji={handleEmojiPick} condensed={shouldCondense} />
|
||||
{features.polls && <PollButton composeId={id} />}
|
||||
{features.privacyScopes && !group && !groupId && <PrivacyDropdown composeId={id} />}
|
||||
{features.scheduledStatuses && <ScheduleButton composeId={id} />}
|
||||
{features.spoilers && <SpoilerButton composeId={id} />}
|
||||
{features.richText && <MarkdownButton composeId={id} />}
|
||||
</HStack>
|
||||
), [features, id]);
|
||||
|
||||
@ -240,6 +238,11 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||
publishText = intl.formatMessage(messages.schedule);
|
||||
}
|
||||
|
||||
const selectButtons = [];
|
||||
|
||||
if (features.privacyScopes && !group && !groupId) selectButtons.push(<PrivacyDropdown composeId={id} />);
|
||||
if (features.richText) selectButtons.push(<ContentTypeButton composeId={id} />);
|
||||
|
||||
return (
|
||||
<Stack className='w-full' space={4} ref={formRef} onClick={handleClick} element='form' onSubmit={handleSubmit}>
|
||||
{scheduledStatusCount > 0 && !event && !group && (
|
||||
@ -269,6 +272,12 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||
|
||||
{!shouldCondense && !event && !group && <ReplyMentions composeId={id} />}
|
||||
|
||||
{!!selectButtons && (
|
||||
<HStack space={2} wrap className='-mb-2'>
|
||||
{selectButtons}
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Suspense>
|
||||
<ComposeEditor
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { changeComposeContentType } from 'soapbox/actions/compose';
|
||||
import { useAppDispatch, useCompose } from 'soapbox/hooks';
|
||||
|
||||
import ComposeFormButton from './compose-form-button';
|
||||
|
||||
const messages = defineMessages({
|
||||
marked: { id: 'compose_form.markdown.marked', defaultMessage: 'Post markdown enabled' },
|
||||
unmarked: { id: 'compose_form.markdown.unmarked', defaultMessage: 'Post markdown disabled' },
|
||||
});
|
||||
|
||||
interface IMarkdownButton {
|
||||
composeId: string;
|
||||
}
|
||||
|
||||
const MarkdownButton: React.FC<IMarkdownButton> = ({ composeId }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const active = useCompose(composeId).content_type === 'text/markdown';
|
||||
|
||||
const onClick = () => dispatch(changeComposeContentType(composeId, active ? 'text/plain' : 'text/markdown'));
|
||||
|
||||
return (
|
||||
<ComposeFormButton
|
||||
icon={require('@tabler/icons/outline/markdown.svg')}
|
||||
title={intl.formatMessage(active ? messages.marked : messages.unmarked)}
|
||||
active={active}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
export { MarkdownButton as default };
|
||||
@ -9,7 +9,7 @@ import Overlay from 'react-overlays/lib/Overlay';
|
||||
import { changeComposeVisibility } from 'soapbox/actions/compose';
|
||||
import { closeModal, openModal } from 'soapbox/actions/modals';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import { IconButton } from 'soapbox/components/ui';
|
||||
import { Button } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useCompose } from 'soapbox/hooks';
|
||||
import { userTouching } from 'soapbox/is-mobile';
|
||||
|
||||
@ -277,12 +277,12 @@ const PrivacyDropdown: React.FC<IPrivacyDropdown> = ({
|
||||
active: valueOption && options.indexOf(valueOption) === 0,
|
||||
})}
|
||||
>
|
||||
<IconButton
|
||||
className={clsx({
|
||||
'text-gray-600 hover:text-gray-700 dark:hover:text-gray-500': !open,
|
||||
'text-primary-500 hover:text-primary-600 dark:text-primary-500 dark:hover:text-primary-400': open,
|
||||
})}
|
||||
src={valueOption?.icon}
|
||||
<Button
|
||||
theme='muted'
|
||||
size='xs'
|
||||
text={valueOption?.text}
|
||||
icon={valueOption?.icon}
|
||||
secondaryIcon={require('@tabler/icons/outline/chevron-down.svg')}
|
||||
title={intl.formatMessage(messages.change_privacy)}
|
||||
onClick={handleToggle}
|
||||
onMouseDown={handleMouseDown}
|
||||
|
||||
@ -85,6 +85,7 @@ const messages = defineMessages({
|
||||
privacy_followers_only: { id: 'preferences.options.privacy_followers_only', defaultMessage: 'Followers-only' },
|
||||
content_type_plaintext: { id: 'preferences.options.content_type_plaintext', defaultMessage: 'Plain text' },
|
||||
content_type_markdown: { id: 'preferences.options.content_type_markdown', defaultMessage: 'Markdown' },
|
||||
content_type_html: { id: 'preferences.options.content_type_html', defaultMessage: 'HTML' },
|
||||
});
|
||||
|
||||
const Preferences = () => {
|
||||
@ -116,6 +117,7 @@ const Preferences = () => {
|
||||
const defaultContentTypeOptions = React.useMemo(() => ({
|
||||
'text/plain': intl.formatMessage(messages.content_type_plaintext),
|
||||
'text/markdown': intl.formatMessage(messages.content_type_markdown),
|
||||
'text/html': intl.formatMessage(messages.content_type_html),
|
||||
}), []);
|
||||
|
||||
return (
|
||||
|
||||
@ -440,13 +440,12 @@
|
||||
"compose_event.tabs.pending": "Manage requests",
|
||||
"compose_event.update": "Update",
|
||||
"compose_event.upload_banner": "Upload event banner",
|
||||
"compose_form.content_type.change": "Change content type",
|
||||
"compose_form.direct_message_warning": "This post will only be sent to the mentioned users.",
|
||||
"compose_form.event_placeholder": "Post to this event",
|
||||
"compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.",
|
||||
"compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.",
|
||||
"compose_form.lock_disclaimer.lock": "locked",
|
||||
"compose_form.markdown.marked": "Post markdown enabled",
|
||||
"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 an answer",
|
||||
@ -1162,6 +1161,7 @@
|
||||
"preferences.fields.unfollow_modal_label": "Show confirmation dialog before unfollowing someone",
|
||||
"preferences.hints.demetricator": "Decrease social media anxiety by hiding all numbers from the site.",
|
||||
"preferences.notifications.advanced": "Show all notification categories",
|
||||
"preferences.options.content_type_html": "HTML",
|
||||
"preferences.options.content_type_markdown": "Markdown",
|
||||
"preferences.options.content_type_plaintext": "Plain text",
|
||||
"preferences.options.privacy_followers_only": "Followers-only",
|
||||
|
||||
Reference in New Issue
Block a user