Refactor StatusActionButton

next-interactions
Alex Gleason 3 years ago
parent 82130a1612
commit bd98842434
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7

@ -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,27 +356,38 @@ 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 (
<>
<IconButton
disabled={disabled}
className={classNames({
'text-gray-400 hover:text-gray-600': true,
'text-gray-600': open,
})}
title={title}
src={src}
aria-pressed={pressed}
text={text}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleButtonKeyDown}
onKeyPress={this.handleKeyPress}
ref={this.setTargetRef}
/>
{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({
'text-gray-400 hover:text-gray-600': true,
'text-gray-600': open,
})}
title={title}
src={src}
aria-pressed={pressed}
text={text}
onClick={this.handleClick}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleButtonKeyDown}
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 */

@ -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,47 +587,30 @@ 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),
action: this.handleReblogClick,
icon: require('@tabler/icons/icons/repeat.svg'),
},
{
text: intl.formatMessage(messages.quotePost),
action: this.handleQuoteClick,
icon: require('@tabler/icons/icons/quote.svg'),
},
];
reblogButton = (
<DropdownMenuContainer
items={reblogMenu}
disabled={!publicStatus}
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}
/>
);
}
const reblogMenu = [
{
text: intl.formatMessage(status.reblogged ? messages.cancel_reblog_private : messages.reblog),
action: this.handleReblogClick,
icon: require('@tabler/icons/icons/repeat.svg'),
},
{
text: intl.formatMessage(messages.quotePost),
action: this.handleQuoteClick,
icon: require('@tabler/icons/icons/quote.svg'),
},
];
const reblogButton = (
<StatusActionButton
icon={reblogIcon}
color='success'
disabled={!publicStatus}
title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)}
active={status.reblogged}
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>
{reblogButton}
{reblogCount > 0 && (
<StatusActionCounter count={reblogCount} />
)}
</StatusAction>
{features.quotePosts && me ? (
<DropdownMenuContainer items={reblogMenu} onShiftClick={this.handleReblogClick}>
{reblogButton}
</DropdownMenuContainer>
) : (
reblogButton
)}
{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),
})}
title={meEmojiTitle}
src={require('@tabler/icons/icons/heart.svg')}
iconClassName={classNames({
'fill-accent-300': Boolean(meEmojiReact),
})}
// emoji={meEmojiReact}
onClick={this.handleLikeButtonClick}
<Hoverable
component={(
<EmojiSelector
onReact={this.handleReact}
focused={emojiSelectorFocused}
onUnfocus={handleEmojiSelectorUnfocus}
/>
</Hoverable>
{emojiReactCount > 0 && (
(features.exposableReactions && !features.emojiReacts) ? (
<StatusActionCounter count={emojiReactCount} />
) : (
<StatusActionCounter count={emojiReactCount} />
)
)}
</div>
>
<StatusActionButton
title={meEmojiTitle}
icon={require('@tabler/icons/icons/thumb-up.svg')}
color='accent'
onClick={this.handleLikeButtonClick}
active={Boolean(meEmojiReact)}
count={emojiReactCount}
/>
</Hoverable>
): (
<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…
Cancel
Save