Refactor StatusActionButton
This commit is contained in:
parent
82130a1612
commit
bd98842434
6 changed files with 174 additions and 195 deletions
|
@ -223,12 +223,12 @@ const RouterDropdownMenu = withRouter(DropdownMenu);
|
|||
|
||||
export interface IDropdown extends RouteComponentProps {
|
||||
icon?: string,
|
||||
src: string,
|
||||
src?: string,
|
||||
items: Menu,
|
||||
size?: number,
|
||||
active?: boolean,
|
||||
pressed?: boolean,
|
||||
title: string,
|
||||
title?: string,
|
||||
disabled?: boolean,
|
||||
status?: Status,
|
||||
isUserTouching?: () => boolean,
|
||||
|
@ -245,6 +245,7 @@ export interface IDropdown extends RouteComponentProps {
|
|||
openedViaKeyboard?: boolean,
|
||||
text?: string,
|
||||
onShiftClick?: React.EventHandler<React.MouseEvent | React.KeyboardEvent>,
|
||||
children?: JSX.Element,
|
||||
}
|
||||
|
||||
interface IDropdownState {
|
||||
|
@ -355,11 +356,21 @@ class Dropdown extends React.PureComponent<IDropdown, IDropdownState> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { src, items, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard = false, pressed, text } = this.props;
|
||||
const { src = require('@tabler/icons/icons/dots.svg'), items, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard = false, pressed, text, children } = this.props;
|
||||
const open = this.state.id === openDropdownId;
|
||||
|
||||
return (
|
||||
<>
|
||||
{children ? (
|
||||
React.cloneElement(children, {
|
||||
disabled,
|
||||
onClick: this.handleClick,
|
||||
onMouseDown: this.handleMouseDown,
|
||||
onKeyDown: this.handleButtonKeyDown,
|
||||
onKeyPress: this.handleKeyPress,
|
||||
ref: this.setTargetRef,
|
||||
})
|
||||
) : (
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
className={classNames({
|
||||
|
@ -376,6 +387,7 @@ class Dropdown extends React.PureComponent<IDropdown, IDropdownState> {
|
|||
onKeyPress={this.handleKeyPress}
|
||||
ref={this.setTargetRef}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>
|
||||
<RouterDropdownMenu items={items} onClose={this.handleClose} openedViaKeyboard={openedViaKeyboard} />
|
||||
|
|
|
@ -3,7 +3,7 @@ import React, { useState, useRef } from 'react';
|
|||
import { usePopper } from 'react-popper';
|
||||
|
||||
interface IHoverable {
|
||||
component: React.Component,
|
||||
component: JSX.Element,
|
||||
}
|
||||
|
||||
/** Wrapper to render a given component when hovered */
|
81
app/soapbox/components/status-action-button.tsx
Normal file
81
app/soapbox/components/status-action-button.tsx
Normal file
|
@ -0,0 +1,81 @@
|
|||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import InlineSVG from 'react-inlinesvg';
|
||||
|
||||
import { Text } from 'soapbox/components/ui';
|
||||
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
||||
|
||||
const COLORS = {
|
||||
accent: 'text-accent-300 hover:text-accent-300 dark:hover:text-accent-300',
|
||||
success: 'text-success-600 hover:text-success-600 dark:hover:text-success-600',
|
||||
'': '',
|
||||
};
|
||||
|
||||
const FILL_COLORS = {
|
||||
accent: 'fill-accent-300 hover:fill-accent-300',
|
||||
'': '',
|
||||
};
|
||||
|
||||
type Color = keyof typeof COLORS;
|
||||
type FillColor = keyof typeof FILL_COLORS;
|
||||
|
||||
interface IStatusActionCounter {
|
||||
count: number,
|
||||
}
|
||||
|
||||
/** Action button numerical counter, eg "5" likes */
|
||||
const StatusActionCounter: React.FC<IStatusActionCounter> = ({ count = 0 }): JSX.Element => {
|
||||
return (
|
||||
<Text size='xs' weight='semibold' theme='inherit'>
|
||||
{shortNumberFormat(count)}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
interface IStatusActionButton extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
iconClassName?: string,
|
||||
icon: string,
|
||||
count?: number,
|
||||
active?: boolean,
|
||||
color?: Color,
|
||||
fill?: FillColor,
|
||||
}
|
||||
|
||||
const StatusActionButton = React.forwardRef((props: IStatusActionButton, ref: React.ForwardedRef<HTMLButtonElement>): JSX.Element => {
|
||||
const { icon, className, iconClassName, active, color = '', fill = '', count = 0, ...filteredProps } = props;
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
type='button'
|
||||
className={classNames(
|
||||
'group flex items-center p-1 space-x-0.5 rounded-full',
|
||||
'text-gray-400 hover:text-gray-600 dark:hover:text-white',
|
||||
'bg-white dark:bg-transparent',
|
||||
{
|
||||
[COLORS[color]]: active,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
{...filteredProps}
|
||||
>
|
||||
<InlineSVG
|
||||
src={icon}
|
||||
className={classNames(
|
||||
'p-1 rounded-full box-content',
|
||||
'group-focus:outline-none group-focus:ring-2 group-focus:ring-offset-2 dark:ring-offset-0 group-focus:ring-primary-500',
|
||||
{
|
||||
[FILL_COLORS[fill]]: active,
|
||||
},
|
||||
iconClassName,
|
||||
)}
|
||||
/>
|
||||
|
||||
{(count || null) && (
|
||||
<StatusActionCounter count={count} />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
export default StatusActionButton;
|
|
@ -1,4 +1,3 @@
|
|||
import classNames from 'classnames';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import React from 'react';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
@ -8,11 +7,8 @@ import { withRouter } from 'react-router-dom';
|
|||
|
||||
import { simpleEmojiReact } from 'soapbox/actions/emoji_reacts';
|
||||
import EmojiSelector from 'soapbox/components/emoji_selector';
|
||||
import {
|
||||
StatusAction,
|
||||
StatusActionButton,
|
||||
StatusActionCounter,
|
||||
} from 'soapbox/components/ui/status/status-action-button';
|
||||
import Hoverable from 'soapbox/components/hoverable';
|
||||
import StatusActionButton from 'soapbox/components/status-action-button';
|
||||
import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container';
|
||||
import { isUserTouching } from 'soapbox/is_mobile';
|
||||
import { getReactForStatus, reduceEmoji } from 'soapbox/utils/emoji_reacts';
|
||||
|
@ -20,8 +16,6 @@ import { getFeatures } from 'soapbox/utils/features';
|
|||
|
||||
import { openModal } from '../actions/modals';
|
||||
|
||||
import { IconButton, Hoverable } from './ui';
|
||||
|
||||
import type { History } from 'history';
|
||||
import type { AnyAction, Dispatch } from 'redux';
|
||||
import type { Menu } from 'soapbox/components/dropdown_menu';
|
||||
|
@ -171,7 +165,7 @@ class StatusActionBar extends ImmutablePureComponent<IStatusActionBar, IStatusAc
|
|||
}
|
||||
}
|
||||
|
||||
handleLikeButtonClick = () => {
|
||||
handleLikeButtonClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
const { features } = this.props;
|
||||
|
||||
const reactForStatus = getReactForStatus(this.props.status, this.props.allowedEmoji);
|
||||
|
@ -186,6 +180,8 @@ class StatusActionBar extends ImmutablePureComponent<IStatusActionBar, IStatusAc
|
|||
} else {
|
||||
this.handleReact(meEmojiReact);
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
handleReact = (emoji: string): void => {
|
||||
|
@ -204,13 +200,15 @@ class StatusActionBar extends ImmutablePureComponent<IStatusActionBar, IStatusAc
|
|||
};
|
||||
}
|
||||
|
||||
handleFavouriteClick: React.EventHandler<React.MouseEvent> = () => {
|
||||
handleFavouriteClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
const { me, onFavourite, onOpenUnauthorizedModal, status } = this.props;
|
||||
if (me) {
|
||||
onFavourite(status);
|
||||
} else {
|
||||
onOpenUnauthorizedModal('FAVOURITE');
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
handleBookmarkClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
|
@ -589,9 +587,6 @@ class StatusActionBar extends ImmutablePureComponent<IStatusActionBar, IStatusAc
|
|||
reblogIcon = require('@tabler/icons/icons/lock.svg');
|
||||
}
|
||||
|
||||
let reblogButton;
|
||||
|
||||
if (me && features.quotePosts) {
|
||||
const reblogMenu = [
|
||||
{
|
||||
text: intl.formatMessage(status.reblogged ? messages.cancel_reblog_private : messages.reblog),
|
||||
|
@ -605,31 +600,17 @@ class StatusActionBar extends ImmutablePureComponent<IStatusActionBar, IStatusAc
|
|||
},
|
||||
];
|
||||
|
||||
reblogButton = (
|
||||
<DropdownMenuContainer
|
||||
items={reblogMenu}
|
||||
const reblogButton = (
|
||||
<StatusActionButton
|
||||
icon={reblogIcon}
|
||||
color='success'
|
||||
disabled={!publicStatus}
|
||||
title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)}
|
||||
active={status.reblogged}
|
||||
pressed={status.reblogged}
|
||||
title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)}
|
||||
src={reblogIcon}
|
||||
onShiftClick={this.handleReblogClick}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
reblogButton = (
|
||||
<IconButton
|
||||
disabled={!publicStatus}
|
||||
className={classNames({
|
||||
'text-gray-400 group-hover:text-gray-600 dark:group-hover:text-white': !status.reblogged,
|
||||
'text-success-600 group-hover:text-success-600': status.reblogged,
|
||||
})}
|
||||
title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)}
|
||||
src={reblogIcon}
|
||||
onClick={this.handleReblogClick}
|
||||
count={reblogCount}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!status.in_reply_to_id) {
|
||||
replyTitle = intl.formatMessage(messages.reply);
|
||||
|
@ -648,55 +629,40 @@ class StatusActionBar extends ImmutablePureComponent<IStatusActionBar, IStatusAc
|
|||
count={replyCount}
|
||||
/>
|
||||
|
||||
<StatusAction>
|
||||
{features.quotePosts && me ? (
|
||||
<DropdownMenuContainer items={reblogMenu} onShiftClick={this.handleReblogClick}>
|
||||
{reblogButton}
|
||||
{reblogCount > 0 && (
|
||||
<StatusActionCounter count={reblogCount} />
|
||||
</DropdownMenuContainer>
|
||||
) : (
|
||||
reblogButton
|
||||
)}
|
||||
</StatusAction>
|
||||
|
||||
{features.emojiReacts ? (
|
||||
<div
|
||||
ref={this.setRef}
|
||||
className='group flex relative items-center space-x-0.5 p-1 rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500'
|
||||
>
|
||||
<Hoverable
|
||||
component={(
|
||||
<EmojiSelector
|
||||
onReact={this.handleReact}
|
||||
focused={emojiSelectorFocused}
|
||||
onUnfocus={handleEmojiSelectorUnfocus}
|
||||
/> as any
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<IconButton
|
||||
className={classNames({
|
||||
'text-gray-400 group-hover:text-gray-600 dark:group-hover:text-white': !meEmojiReact,
|
||||
'text-accent-300 group-hover:text-accent-300': Boolean(meEmojiReact),
|
||||
})}
|
||||
<StatusActionButton
|
||||
title={meEmojiTitle}
|
||||
src={require('@tabler/icons/icons/heart.svg')}
|
||||
iconClassName={classNames({
|
||||
'fill-accent-300': Boolean(meEmojiReact),
|
||||
})}
|
||||
// emoji={meEmojiReact}
|
||||
icon={require('@tabler/icons/icons/thumb-up.svg')}
|
||||
color='accent'
|
||||
onClick={this.handleLikeButtonClick}
|
||||
active={Boolean(meEmojiReact)}
|
||||
count={emojiReactCount}
|
||||
/>
|
||||
</Hoverable>
|
||||
|
||||
{emojiReactCount > 0 && (
|
||||
(features.exposableReactions && !features.emojiReacts) ? (
|
||||
<StatusActionCounter count={emojiReactCount} />
|
||||
) : (
|
||||
<StatusActionCounter count={emojiReactCount} />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
): (
|
||||
<StatusActionButton
|
||||
title={intl.formatMessage(messages.favourite)}
|
||||
icon={require('@tabler/icons/icons/heart.svg')}
|
||||
onClick={this.handleLikeButtonClick}
|
||||
color='accent'
|
||||
fill='accent'
|
||||
onClick={this.handleFavouriteClick}
|
||||
active={Boolean(meEmojiReact)}
|
||||
count={favouriteCount}
|
||||
/>
|
||||
|
@ -710,14 +676,12 @@ class StatusActionBar extends ImmutablePureComponent<IStatusActionBar, IStatusAc
|
|||
/>
|
||||
)}
|
||||
|
||||
<StatusAction>
|
||||
<DropdownMenuContainer
|
||||
items={menu}
|
||||
<DropdownMenuContainer items={menu} status={status}>
|
||||
<StatusActionButton
|
||||
title={intl.formatMessage(messages.more)}
|
||||
status={status}
|
||||
src={require('@tabler/icons/icons/dots.svg')}
|
||||
icon={require('@tabler/icons/icons/dots.svg')}
|
||||
/>
|
||||
</StatusAction>
|
||||
</DropdownMenuContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ export { default as EmojiSelector } from './emoji-selector/emoji-selector';
|
|||
export { default as Form } from './form/form';
|
||||
export { default as FormActions } from './form-actions/form-actions';
|
||||
export { default as FormGroup } from './form-group/form-group';
|
||||
export { default as Hoverable } from './hoverable/hoverable';
|
||||
export { default as HStack } from './hstack/hstack';
|
||||
export { default as Icon } from './icon/icon';
|
||||
export { default as IconButton } from './icon-button/icon-button';
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
import { IconButton } from 'soapbox/components/ui';
|
||||
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
||||
|
||||
interface IStatusActionCounter {
|
||||
count: number,
|
||||
className?: string,
|
||||
}
|
||||
|
||||
/** Action button numerical counter, eg "5" likes */
|
||||
const StatusActionCounter: React.FC<IStatusActionCounter> = ({ count = 0, className }): JSX.Element => {
|
||||
return (
|
||||
<span className={classNames('text-xs font-semibold text-gray-400 group-hover:text-gray-600 dark:group-hover:text-white', className)}>
|
||||
{shortNumberFormat(count)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
interface IStatusAction {
|
||||
title?: string,
|
||||
}
|
||||
|
||||
/** Status action container element */
|
||||
const StatusAction: React.FC<IStatusAction> = ({ title, children }) => {
|
||||
return (
|
||||
<div title={title} className='group flex items-center space-x-0.5 p-1 rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500'>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface IStatusActionButton {
|
||||
icon: string,
|
||||
onClick: () => void,
|
||||
count?: number,
|
||||
active?: boolean,
|
||||
title?: string,
|
||||
}
|
||||
|
||||
/** Action button (eg "Like") for a Status */
|
||||
const StatusActionButton: React.FC<IStatusActionButton> = ({ icon, title, active = false, onClick, count = 0 }): JSX.Element => {
|
||||
|
||||
const handleClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
onClick();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
return (
|
||||
<StatusAction title={title}>
|
||||
<IconButton
|
||||
title={title}
|
||||
src={icon}
|
||||
onClick={handleClick}
|
||||
className={classNames('text-gray-400 group-hover:text-gray-600 dark:group-hover:text-white', {
|
||||
'text-accent-300 group-hover:text-accent-300 dark:group-hover:text-accent-300': active,
|
||||
// TODO: repost button
|
||||
// 'text-success-600 hover:text-success-600': active,
|
||||
})}
|
||||
iconClassName={classNames({
|
||||
'fill-accent-300': active,
|
||||
})}
|
||||
/>
|
||||
|
||||
{(count || null) && (
|
||||
<StatusActionCounter
|
||||
className={classNames({ 'text-accent-300 group-hover:text-accent-300': active })}
|
||||
count={count}
|
||||
/>
|
||||
)}
|
||||
</StatusAction>
|
||||
);
|
||||
};
|
||||
|
||||
export { StatusAction, StatusActionButton, StatusActionCounter };
|
Loading…
Reference in a new issue