Merge branch 'filter-bar-animation' into 'develop'

Filter bar tab indicator animation

See merge request soapbox-pub/soapbox-fe!954
strip-front-mentions
marcin mikołajczak 2022-01-06 18:17:04 +00:00
commit afa1ae2fc1
6 zmienionych plików z 306 dodań i 160 usunięć

Wyświetl plik

@ -0,0 +1,156 @@
import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { debounce } from 'lodash';
export default class FilterBar extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
items: PropTypes.array.isRequired,
active: PropTypes.string,
className: PropTypes.string,
};
state = {
mounted: false,
};
componentDidMount() {
document.addEventListener('keydown', this.handleKeyDown, false);
window.addEventListener('resize', this.handleResize, { passive: true });
const { left, width } = this.getActiveTabIndicationSize();
this.setState({ mounted: true, left, width });
}
componentWillUnmount() {
document.removeEventListener('keydown', this.handleKeyDown, false);
document.removeEventListener('resize', this.handleResize, false);
}
handleResize = debounce(() => {
this.setState(this.getActiveTabIndicationSize());
}, 300, {
trailing: true,
});
componentDidUpdate(prevProps) {
if (this.props.active !== prevProps.active) {
this.setState(this.getActiveTabIndicationSize());
}
}
setRef = c => {
this.node = c;
}
setFocusRef = c => {
this.focusedItem = c;
}
handleKeyDown = e => {
const items = Array.from(this.node.getElementsByTagName('a'));
const index = items.indexOf(document.activeElement);
let element = null;
switch(e.key) {
case 'ArrowRight':
element = items[index+1] || items[0];
break;
case 'ArrowLeft':
element = items[index-1] || items[items.length-1];
break;
}
if (element) {
element.focus();
e.preventDefault();
e.stopPropagation();
}
}
handleItemKeyPress = e => {
if (e.key === 'Enter' || e.key === ' ') {
this.handleClick(e);
}
}
handleClick = e => {
const i = Number(e.currentTarget.getAttribute('data-index'));
const { action, to } = this.props.items[i];
if (typeof action === 'function') {
e.preventDefault();
action(e);
} else if (to) {
e.preventDefault();
this.context.router.history.push(to);
}
}
getActiveTabIndicationSize() {
const { active, items } = this.props;
if (!active || !this.node) return { width: null };
const index = items.findIndex(({ name }) => name === active);
const elements = Array.from(this.node.getElementsByTagName('a'));
const element = elements[index];
if (!element) return { width: null };
const left = element.offsetLeft;
const { width } = element.getBoundingClientRect();
return { left, width };
}
renderActiveTabIndicator() {
const { left, width } = this.state;
return (
<div className='filter-bar__active' style={{ left, width }} />
);
}
renderItem(option, i) {
if (option === null) {
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
}
const { name, text, href, to, title } = option;
return (
<a
key={name}
href={href || to || '#'}
role='button'
tabIndex='0'
ref={i === 0 ? this.setFocusRef : null}
onClick={this.handleClick}
onKeyPress={this.handleItemKeyPress}
data-index={i}
title={title}
>
{text}
</a>
);
}
render() {
const { className, items } = this.props;
const { mounted } = this.state;
return (
<div className={classNames('filter-bar', className)} ref={this.setRef}>
{mounted && this.renderActiveTabIndicator()}
{items.map((option, i) => this.renderItem(option, i))}
</div>
);
}
}

Wyświetl plik

@ -2,18 +2,26 @@ import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl } from 'react-intl';
import AccountContainer from '../../../containers/account_container';
import StatusContainer from '../../../containers/status_container';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Hashtag from '../../../components/hashtag';
import FilterBar from '../../search/components/filter_bar';
import ScrollableList from 'soapbox/components/scrollable_list';
import PlaceholderAccount from 'soapbox/features/placeholder/components/placeholder_account';
import PlaceholderHashtag from 'soapbox/features/placeholder/components/placeholder_hashtag';
import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status';
import Pullable from 'soapbox/components/pullable';
import FilterBar from 'soapbox/components/filter_bar';
export default class SearchResults extends ImmutablePureComponent {
const messages = defineMessages({
accounts: { id: 'search_results.accounts', defaultMessage: 'People' },
statuses: { id: 'search_results.statuses', defaultMessage: 'Posts' },
hashtags: { id: 'search_results.hashtags', defaultMessage: 'Hashtags' },
});
export default @injectIntl
class SearchResults extends ImmutablePureComponent {
static propTypes = {
value: PropTypes.string,
@ -25,12 +33,37 @@ export default class SearchResults extends ImmutablePureComponent {
features: PropTypes.object.isRequired,
suggestions: ImmutablePropTypes.list,
trends: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired,
};
handleLoadMore = () => this.props.expandSearch(this.props.selectedFilter);
handleSelectFilter = newActiveFilter => this.props.selectFilter(newActiveFilter);
renderFilterBar() {
const { intl, selectedFilter } = this.props;
const items = [
{
text: intl.formatMessage(messages.accounts),
action: () => this.handleSelectFilter('accounts'),
name: 'accounts',
},
{
text: intl.formatMessage(messages.statuses),
action: () => this.handleSelectFilter('statuses'),
name: 'statuses',
},
{
text: intl.formatMessage(messages.hashtags),
action: () => this.handleSelectFilter('hashtags'),
name: 'hashtags',
},
];
return <FilterBar className='search__filter-bar' items={items} active={selectedFilter} />;
}
render() {
const { value, results, submitted, selectedFilter, suggestions, trends } = this.props;
@ -105,7 +138,7 @@ export default class SearchResults extends ImmutablePureComponent {
return (
<>
<FilterBar selectedFilter={selectedFilter} selectFilter={this.handleSelectFilter} />
{this.renderFilterBar()}
{noResultsMessage || (
<Pullable>

Wyświetl plik

@ -1,9 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { defineMessages, injectIntl } from 'react-intl';
import Icon from 'soapbox/components/icon';
import FilterBar from 'soapbox/components/filter_bar';
const tooltips = defineMessages({
const messages = defineMessages({
all: { id: 'notifications.filter.all', defaultMessage: 'All' },
mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' },
favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Likes' },
boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Reposts' },
@ -14,7 +16,7 @@ const tooltips = defineMessages({
});
export default @injectIntl
class FilterBar extends React.PureComponent {
class NotificationFilterBar extends React.PureComponent {
static propTypes = {
selectFilter: PropTypes.func.isRequired,
@ -30,90 +32,67 @@ class FilterBar extends React.PureComponent {
render() {
const { selectedFilter, advancedMode, supportsEmojiReacts, intl } = this.props;
const renderedElement = !advancedMode ? (
<div className='notification__filter-bar'>
<button
className={selectedFilter === 'all' ? 'active' : ''}
onClick={this.onClick('all')}
>
<FormattedMessage
id='notifications.filter.all'
defaultMessage='All'
/>
</button>
<button
className={selectedFilter === 'mention' ? 'active' : ''}
onClick={this.onClick('mention')}
>
<FormattedMessage
id='notifications.filter.mentions'
defaultMessage='Mentions'
/>
</button>
</div>
) : (
<div className='notification__filter-bar'>
<button
className={selectedFilter === 'all' ? 'active' : ''}
onClick={this.onClick('all')}
>
<FormattedMessage
id='notifications.filter.all'
defaultMessage='All'
/>
</button>
<button
className={selectedFilter === 'mention' ? 'active' : ''}
onClick={this.onClick('mention')}
title={intl.formatMessage(tooltips.mentions)}
>
<Icon src={require('@tabler/icons/icons/at.svg')} />
</button>
<button
className={selectedFilter === 'favourite' ? 'active' : ''}
onClick={this.onClick('favourite')}
title={intl.formatMessage(tooltips.favourites)}
>
<Icon src={require('@tabler/icons/icons/thumb-up.svg')} />
</button>
{supportsEmojiReacts && <button
className={selectedFilter === 'pleroma:emoji_reaction' ? 'active' : ''}
onClick={this.onClick('pleroma:emoji_reaction')}
title={intl.formatMessage(tooltips.emoji_reacts)}
>
<Icon src={require('@tabler/icons/icons/mood-smile.svg')} />
</button>}
<button
className={selectedFilter === 'reblog' ? 'active' : ''}
onClick={this.onClick('reblog')}
title={intl.formatMessage(tooltips.boosts)}
>
<Icon src={require('feather-icons/dist/icons/repeat.svg')} />
</button>
<button
className={selectedFilter === 'poll' ? 'active' : ''}
onClick={this.onClick('poll')}
title={intl.formatMessage(tooltips.polls)}
>
<Icon src={require('@tabler/icons/icons/chart-bar.svg')} />
</button>
<button
className={selectedFilter === 'follow' ? 'active' : ''}
onClick={this.onClick('follow')}
title={intl.formatMessage(tooltips.follows)}
>
<Icon src={require('@tabler/icons/icons/user-plus.svg')} />
</button>
<button
className={selectedFilter === 'move' ? 'active' : ''}
onClick={this.onClick('move')}
title={intl.formatMessage(tooltips.moves)}
>
<Icon src={require('feather-icons/dist/icons/briefcase.svg')} />
</button>
</div>
);
return renderedElement;
const items = [
{
text: intl.formatMessage(messages.all),
action: this.onClick('all'),
name: 'all',
},
];
if (!advancedMode) {
items.push({
text: intl.formatMessage(messages.mentions),
action: this.onClick('mention'),
name: 'mention',
});
} else {
items.push({
text: <Icon src={require('@tabler/icons/icons/at.svg')} />,
title: intl.formatMessage(messages.mentions),
action: this.onClick('mention'),
name: 'mention',
});
items.push({
text: <Icon src={require('@tabler/icons/icons/thumb-up.svg')} />,
title: intl.formatMessage(messages.favourites),
action: this.onClick('favourite'),
name: 'favourite',
});
if (supportsEmojiReacts) items.push({
text: <Icon src={require('@tabler/icons/icons/mood-smile.svg')} />,
title: intl.formatMessage(messages.emoji_reacts),
action: this.onClick('pleroma:emoji_reaction'),
name: 'pleroma:emoji_reaction',
});
items.push({
text: <Icon src={require('feather-icons/dist/icons/repeat.svg')} />,
title: intl.formatMessage(messages.boosts),
action: this.onClick('reblog'),
name: 'reblog',
});
items.push({
text: <Icon src={require('@tabler/icons/icons/chart-bar.svg')} />,
title: intl.formatMessage(messages.polls),
action: this.onClick('poll'),
name: 'poll',
});
items.push({
text: <Icon src={require('@tabler/icons/icons/user-plus.svg')} />,
title: intl.formatMessage(messages.follows),
action: this.onClick('follow'),
name: 'follow',
});
items.push({
text: <Icon src={require('feather-icons/dist/icons/briefcase.svg')} />,
title: intl.formatMessage(messages.moves),
action: this.onClick('move'),
name: 'move',
});
}
return <FilterBar key={advancedMode} className='notification__filter-bar' items={items} active={selectedFilter} />;
}
}

Wyświetl plik

@ -1,53 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, injectIntl } from 'react-intl';
export default @injectIntl
class FilterBar extends React.PureComponent {
static propTypes = {
selectFilter: PropTypes.func.isRequired,
selectedFilter: PropTypes.string.isRequired,
};
onClick(searchType) {
return () => this.props.selectFilter(searchType);
}
render() {
const { selectedFilter } = this.props;
return (
<div className='search__filter-bar'>
<button
className={selectedFilter === 'accounts' ? 'active' : ''}
onClick={this.onClick('accounts')}
>
<FormattedMessage
id='search_results.accounts'
defaultMessage='People'
/>
</button>
<button
className={selectedFilter === 'statuses' ? 'active' : ''}
onClick={this.onClick('statuses')}
>
<FormattedMessage
id='search_results.statuses'
defaultMessage='Posts'
/>
</button>
<button
className={selectedFilter === 'hashtags' ? 'active' : ''}
onClick={this.onClick('hashtags')}
>
<FormattedMessage
id='search_results.hashtags'
defaultMessage='Hashtags'
/>
</button>
</div>
);
}
}

Wyświetl plik

@ -9,9 +9,11 @@ import LoadingIndicator from 'soapbox/components/loading_indicator';
import AccountContainer from 'soapbox/containers/account_container';
import ScrollableList from 'soapbox/components/scrollable_list';
import { fetchFavourites, fetchReactions } from 'soapbox/actions/interactions';
import FilterBar from 'soapbox/components/filter_bar';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
all: { id: 'reactions.all', defaultMessage: 'All' },
});
const mapStateToProps = (state, props) => {
@ -62,6 +64,29 @@ class ReactionsModal extends React.PureComponent {
this.setState({ reaction });
};
renderFilterBar() {
const { intl, reactions } = this.props;
const { reaction } = this.state;
const items = [
{
text: intl.formatMessage(messages.all),
action: this.handleFilterChange(''),
name: 'all',
},
];
reactions.forEach(reaction => items.push(
{
text: `${reaction.name} ${reaction.count}`,
action: this.handleFilterChange(reaction.name),
name: reaction.name,
},
));
return <FilterBar className='reaction__filter-bar' items={items} active={reaction || 'all'} />;
}
render() {
const { intl, reactions } = this.props;
const { reaction } = this.state;
@ -78,14 +103,7 @@ class ReactionsModal extends React.PureComponent {
const emptyMessage = <FormattedMessage id='status.reactions.empty' defaultMessage='No one has reacted to this post yet. When someone does, they will show up here.' />;
body = (<>
{
reactions.size > 0 && (
<div className='reaction__filter-bar'>
<button className={!reaction ? 'active' : ''} onClick={this.handleFilterChange('')}>All</button>
{reactions?.filter(reaction => reaction.count).map(reaction => <button key={reaction.name} className={this.state.reaction === reaction.name ? 'active' : ''} onClick={this.handleFilterChange(reaction.name)}>{reaction.name} {reaction.count}</button>)}
</div>
)
}
{reactions.size > 0 && this.renderFilterBar()}
<ScrollableList
scrollKey='reactions'
emptyMessage={emptyMessage}

Wyświetl plik

@ -636,10 +636,8 @@ article:last-child > .domain {
}
}
.notification__filter-bar,
.search__filter-bar,
.account__section-headline,
.reaction__filter-bar {
.filter-bar,
.account__section-headline {
border-bottom: 1px solid var(--brand-color--faint);
cursor: default;
display: flex;
@ -684,15 +682,30 @@ article:last-child > .domain {
background-color: var(--accent-color);
}
}
}
button .svg-icon {
width: 18px;
height: 18px;
margin: 0 auto;
.svg-icon {
width: 18px;
height: 18px;
margin: 0 auto;
}
}
}
.filter-bar {
position: relative;
&__active {
position: absolute;
height: 3px;
bottom: 0;
background-color: var(--accent-color);
}
}
.no-reduce-motion .filter-bar__active {
transition: all 0.3s;
}
.reaction__filter-bar {
overflow-x: auto;
overflow-y: hidden;