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 { export interface IDropdown extends RouteComponentProps {
icon?: string, icon?: string,
src: string, src?: string,
items: Menu, items: Menu,
size?: number, size?: number,
active?: boolean, active?: boolean,
pressed?: boolean, pressed?: boolean,
title: string, title?: string,
disabled?: boolean, disabled?: boolean,
status?: Status, status?: Status,
isUserTouching?: () => boolean, isUserTouching?: () => boolean,
@ -245,6 +245,7 @@ export interface IDropdown extends RouteComponentProps {
openedViaKeyboard?: boolean, openedViaKeyboard?: boolean,
text?: string, text?: string,
onShiftClick?: React.EventHandler<React.MouseEvent | React.KeyboardEvent>, onShiftClick?: React.EventHandler<React.MouseEvent | React.KeyboardEvent>,
children?: JSX.Element,
} }
interface IDropdownState { interface IDropdownState {
@ -355,27 +356,38 @@ class Dropdown extends React.PureComponent<IDropdown, IDropdownState> {
} }
render() { 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; const open = this.state.id === openDropdownId;
return ( return (
<> <>
<IconButton {children ? (
disabled={disabled} React.cloneElement(children, {
className={classNames({ disabled,
'text-gray-400 hover:text-gray-600': true, onClick: this.handleClick,
'text-gray-600': open, onMouseDown: this.handleMouseDown,
})} onKeyDown: this.handleButtonKeyDown,
title={title} onKeyPress: this.handleKeyPress,
src={src} ref: this.setTargetRef,
aria-pressed={pressed} })
text={text} ) : (
onClick={this.handleClick} <IconButton
onMouseDown={this.handleMouseDown} disabled={disabled}
onKeyDown={this.handleButtonKeyDown} className={classNames({
onKeyPress={this.handleKeyPress} 'text-gray-400 hover:text-gray-600': true,
ref={this.setTargetRef} '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}> <Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>
<RouterDropdownMenu items={items} onClose={this.handleClose} openedViaKeyboard={openedViaKeyboard} /> <RouterDropdownMenu items={items} onClose={this.handleClose} openedViaKeyboard={openedViaKeyboard} />

@ -3,7 +3,7 @@ import React, { useState, useRef } from 'react';
import { usePopper } from 'react-popper'; import { usePopper } from 'react-popper';
interface IHoverable { interface IHoverable {
component: React.Component, component: JSX.Element,
} }
/** Wrapper to render a given component when hovered */ /** 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 { List as ImmutableList } from 'immutable';
import React from 'react'; import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component'; 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 { simpleEmojiReact } from 'soapbox/actions/emoji_reacts';
import EmojiSelector from 'soapbox/components/emoji_selector'; import EmojiSelector from 'soapbox/components/emoji_selector';
import { import Hoverable from 'soapbox/components/hoverable';
StatusAction, import StatusActionButton from 'soapbox/components/status-action-button';
StatusActionButton,
StatusActionCounter,
} from 'soapbox/components/ui/status/status-action-button';
import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container'; import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container';
import { isUserTouching } from 'soapbox/is_mobile'; import { isUserTouching } from 'soapbox/is_mobile';
import { getReactForStatus, reduceEmoji } from 'soapbox/utils/emoji_reacts'; import { getReactForStatus, reduceEmoji } from 'soapbox/utils/emoji_reacts';
@ -20,8 +16,6 @@ import { getFeatures } from 'soapbox/utils/features';
import { openModal } from '../actions/modals'; import { openModal } from '../actions/modals';
import { IconButton, Hoverable } from './ui';
import type { History } from 'history'; import type { History } from 'history';
import type { AnyAction, Dispatch } from 'redux'; import type { AnyAction, Dispatch } from 'redux';
import type { Menu } from 'soapbox/components/dropdown_menu'; 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 { features } = this.props;
const reactForStatus = getReactForStatus(this.props.status, this.props.allowedEmoji); const reactForStatus = getReactForStatus(this.props.status, this.props.allowedEmoji);
@ -186,6 +180,8 @@ class StatusActionBar extends ImmutablePureComponent<IStatusActionBar, IStatusAc
} else { } else {
this.handleReact(meEmojiReact); this.handleReact(meEmojiReact);
} }
e.stopPropagation();
} }
handleReact = (emoji: string): void => { 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; const { me, onFavourite, onOpenUnauthorizedModal, status } = this.props;
if (me) { if (me) {
onFavourite(status); onFavourite(status);
} else { } else {
onOpenUnauthorizedModal('FAVOURITE'); onOpenUnauthorizedModal('FAVOURITE');
} }
e.stopPropagation();
} }
handleBookmarkClick: React.EventHandler<React.MouseEvent> = (e) => { handleBookmarkClick: React.EventHandler<React.MouseEvent> = (e) => {
@ -589,47 +587,30 @@ class StatusActionBar extends ImmutablePureComponent<IStatusActionBar, IStatusAc
reblogIcon = require('@tabler/icons/icons/lock.svg'); reblogIcon = require('@tabler/icons/icons/lock.svg');
} }
let reblogButton; const reblogMenu = [
{
if (me && features.quotePosts) { text: intl.formatMessage(status.reblogged ? messages.cancel_reblog_private : messages.reblog),
const reblogMenu = [ action: this.handleReblogClick,
{ icon: require('@tabler/icons/icons/repeat.svg'),
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'),
text: intl.formatMessage(messages.quotePost), },
action: this.handleQuoteClick, ];
icon: require('@tabler/icons/icons/quote.svg'),
}, const reblogButton = (
]; <StatusActionButton
icon={reblogIcon}
reblogButton = ( color='success'
<DropdownMenuContainer disabled={!publicStatus}
items={reblogMenu} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)}
disabled={!publicStatus} active={status.reblogged}
active={status.reblogged} onClick={this.handleReblogClick}
pressed={status.reblogged} count={reblogCount}
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}
/>
);
}
if (!status.in_reply_to_id) { if (!status.in_reply_to_id) {
replyTitle = intl.formatMessage(messages.reply); replyTitle = intl.formatMessage(messages.reply);
@ -648,55 +629,40 @@ class StatusActionBar extends ImmutablePureComponent<IStatusActionBar, IStatusAc
count={replyCount} count={replyCount}
/> />
<StatusAction> {features.quotePosts && me ? (
{reblogButton} <DropdownMenuContainer items={reblogMenu} onShiftClick={this.handleReblogClick}>
{reblogCount > 0 && ( {reblogButton}
<StatusActionCounter count={reblogCount} /> </DropdownMenuContainer>
)} ) : (
</StatusAction> reblogButton
)}
{features.emojiReacts ? ( {features.emojiReacts ? (
<div <Hoverable
ref={this.setRef} component={(
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' <EmojiSelector
> onReact={this.handleReact}
<Hoverable focused={emojiSelectorFocused}
component={( onUnfocus={handleEmojiSelectorUnfocus}
<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>
{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 <StatusActionButton
title={intl.formatMessage(messages.favourite)} title={intl.formatMessage(messages.favourite)}
icon={require('@tabler/icons/icons/heart.svg')} icon={require('@tabler/icons/icons/heart.svg')}
onClick={this.handleLikeButtonClick} color='accent'
fill='accent'
onClick={this.handleFavouriteClick}
active={Boolean(meEmojiReact)} active={Boolean(meEmojiReact)}
count={favouriteCount} count={favouriteCount}
/> />
@ -710,14 +676,12 @@ class StatusActionBar extends ImmutablePureComponent<IStatusActionBar, IStatusAc
/> />
)} )}
<StatusAction> <DropdownMenuContainer items={menu} status={status}>
<DropdownMenuContainer <StatusActionButton
items={menu}
title={intl.formatMessage(messages.more)} title={intl.formatMessage(messages.more)}
status={status} icon={require('@tabler/icons/icons/dots.svg')}
src={require('@tabler/icons/icons/dots.svg')}
/> />
</StatusAction> </DropdownMenuContainer>
</div> </div>
); );
} }

@ -7,7 +7,6 @@ export { default as EmojiSelector } from './emoji-selector/emoji-selector';
export { default as Form } from './form/form'; export { default as Form } from './form/form';
export { default as FormActions } from './form-actions/form-actions'; export { default as FormActions } from './form-actions/form-actions';
export { default as FormGroup } from './form-group/form-group'; 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 HStack } from './hstack/hstack';
export { default as Icon } from './icon/icon'; export { default as Icon } from './icon/icon';
export { default as IconButton } from './icon-button/icon-button'; 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