Filter bar animation
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
cd4e33a8ed
commit
b06596bbb6
|
@ -0,0 +1,145 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
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);
|
||||
this.setState({ mounted: true });
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this.handleKeyDown, false);
|
||||
}
|
||||
|
||||
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;
|
||||
case 'Tab':
|
||||
if (e.shiftKey) {
|
||||
element = items[index-1] || items[items.length-1];
|
||||
} else {
|
||||
element = items[index+1] || items[0];
|
||||
}
|
||||
break;
|
||||
case 'Home':
|
||||
element = items[0];
|
||||
break;
|
||||
case 'End':
|
||||
element = 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);
|
||||
}
|
||||
}
|
||||
|
||||
renderActiveTabIndicator() {
|
||||
const { active, items } = this.props;
|
||||
|
||||
if (!active || !this.node) return null;
|
||||
|
||||
const index = items.findIndex(({ name }) => name === active);
|
||||
const elements = Array.from(this.node.getElementsByTagName('a'));
|
||||
const element = elements[index];
|
||||
|
||||
if (!element) return null;
|
||||
|
||||
const offsetLeft = element.offsetLeft;
|
||||
const { width } = element.getBoundingClientRect();
|
||||
|
||||
return (
|
||||
<div className='filter-bar__active' style={{ left: offsetLeft, width }} />
|
||||
);
|
||||
}
|
||||
|
||||
renderItem(option, i) {
|
||||
if (option === null) {
|
||||
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
|
||||
}
|
||||
|
||||
const { text, href, to, title } = option;
|
||||
|
||||
return (
|
||||
<a
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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} />;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,9 +13,11 @@ import AccountContainer from '../../containers/account_container';
|
|||
import Column from '../ui/components/column';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
import { makeGetStatus } from '../../selectors';
|
||||
import FilterBar from 'soapbox/components/filter_bar';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.reactions', defaultMessage: 'Reactions' },
|
||||
all: { id: 'reactions.all', defaultMessage: 'All' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
|
@ -82,8 +84,31 @@ class Reactions extends ImmutablePureComponent {
|
|||
this.context.router.history.replace(`/@${username}/posts/${statusId}/reactions/${reaction}`);
|
||||
};
|
||||
|
||||
renderFilterBar() {
|
||||
const { intl, params, reactions } = this.props;
|
||||
const { reaction } = params;
|
||||
|
||||
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, params, reactions, accounts, status } = this.props;
|
||||
const { intl, reactions, accounts, status } = this.props;
|
||||
|
||||
if (!accounts) {
|
||||
return (
|
||||
|
@ -105,14 +130,7 @@ class Reactions extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<Column heading={intl.formatMessage(messages.heading)}>
|
||||
{
|
||||
reactions.size > 0 && (
|
||||
<div className='reaction__filter-bar'>
|
||||
<button className={!params.reaction ? 'active' : ''} onClick={this.handleFilterChange('')}>All</button>
|
||||
{reactions?.filter(reaction => reaction.count).map(reaction => <button key={reaction.name} className={params.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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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,12 +682,24 @@ 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);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue