Rudimentary notifications dropdown
This commit is contained in:
parent
bbd4aa4f8b
commit
324f698e58
73
app/soapbox/components/dropdown_element.js
Normal file
73
app/soapbox/components/dropdown_element.js
Normal file
|
@ -0,0 +1,73 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Motion from '../features/ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
|
||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||
|
||||
export default class DropdownElement extends React.PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
style: PropTypes.object,
|
||||
placement: PropTypes.string,
|
||||
arrowOffsetLeft: PropTypes.string,
|
||||
arrowOffsetTop: PropTypes.string,
|
||||
openedViaKeyboard: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
style: {},
|
||||
placement: 'bottom',
|
||||
};
|
||||
|
||||
state = {
|
||||
mounted: false,
|
||||
};
|
||||
|
||||
handleDocumentClick = e => {
|
||||
if (this.node && !this.node.contains(e.target)) {
|
||||
this.props.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('click', this.handleDocumentClick, false);
|
||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
this.setState({ mounted: true });
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('click', this.handleDocumentClick, false);
|
||||
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.node = c;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children, style, placement, arrowOffsetLeft, arrowOffsetTop } = this.props;
|
||||
const { mounted } = this.state;
|
||||
return (
|
||||
<Motion defaultStyle={{ opacity: 0, scaleX: 1, scaleY: 1 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
|
||||
{({ opacity, scaleX, scaleY }) => (
|
||||
// It should not be transformed when mounting because the resulting
|
||||
// size will be used to determine the coordinate of the menu by
|
||||
// react-overlays
|
||||
<div className={`dropdown-menu ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}>
|
||||
<div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -3,11 +3,8 @@ import PropTypes from 'prop-types';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import IconButton from './icon_button';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import Motion from '../features/ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
import DropdownElement from './dropdown_element';
|
||||
|
||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||
let id = 0;
|
||||
|
||||
class DropdownMenu extends React.PureComponent {
|
||||
|
@ -31,28 +28,13 @@ class DropdownMenu extends React.PureComponent {
|
|||
placement: 'bottom',
|
||||
};
|
||||
|
||||
state = {
|
||||
mounted: false,
|
||||
};
|
||||
|
||||
handleDocumentClick = e => {
|
||||
if (this.node && !this.node.contains(e.target)) {
|
||||
this.props.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('click', this.handleDocumentClick, false);
|
||||
document.addEventListener('keydown', this.handleKeyDown, false);
|
||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
if (this.focusedItem && this.props.openedViaKeyboard) this.focusedItem.focus();
|
||||
this.setState({ mounted: true });
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('click', this.handleDocumentClick, false);
|
||||
document.removeEventListener('keydown', this.handleKeyDown, false);
|
||||
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
|
@ -163,22 +145,13 @@ class DropdownMenu extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { items, style, placement, arrowOffsetLeft, arrowOffsetTop } = this.props;
|
||||
const { mounted } = this.state;
|
||||
const { items, ...props } = this.props;
|
||||
return (
|
||||
<Motion defaultStyle={{ opacity: 0, scaleX: 1, scaleY: 1 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
|
||||
{({ opacity, scaleX, scaleY }) => (
|
||||
// It should not be transformed when mounting because the resulting
|
||||
// size will be used to determine the coordinate of the menu by
|
||||
// react-overlays
|
||||
<div className={`dropdown-menu ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}>
|
||||
<div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
|
||||
<ul>
|
||||
{items.map((option, i) => this.renderItem(option, i))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
<DropdownElement {...props}>
|
||||
<ul>
|
||||
{items.map((option, i) => this.renderItem(option, i))}
|
||||
</ul>
|
||||
</DropdownElement>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -15,6 +15,9 @@ import Icon from '../../../components/icon';
|
|||
import ThemeToggle from '../../ui/components/theme_toggle_container';
|
||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import { isStaff } from 'soapbox/utils/accounts';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import DropdownElement from 'soapbox/components/dropdown_element';
|
||||
import Notifications from 'soapbox/features/notifications';
|
||||
|
||||
const messages = defineMessages({
|
||||
post: { id: 'tabs_bar.post', defaultMessage: 'Post' },
|
||||
|
@ -37,6 +40,7 @@ class TabsBar extends React.PureComponent {
|
|||
|
||||
state = {
|
||||
collapsed: false,
|
||||
notificationsOpen: false,
|
||||
}
|
||||
|
||||
static contextTypes = {
|
||||
|
@ -52,6 +56,24 @@ class TabsBar extends React.PureComponent {
|
|||
return pathname === '/' || pathname.startsWith('/timeline/');
|
||||
}
|
||||
|
||||
setNotifBtnRef = c => {
|
||||
this.notifBtn = c;
|
||||
}
|
||||
|
||||
getNotifBtn = () => {
|
||||
return this.notifBtn;
|
||||
};
|
||||
|
||||
handleNotificationsClick = e => {
|
||||
if (window.innerWidth <= 1190) return;
|
||||
this.setState({ notificationsOpen: !this.state.notificationsOpen });
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
handleCloseNotifications = e => {
|
||||
this.setState({ notificationsOpen: false });
|
||||
}
|
||||
|
||||
getNavLinks() {
|
||||
const { intl: { formatMessage }, logo, account, dashboardCount, notificationCount, chatsCount } = this.props;
|
||||
let links = [];
|
||||
|
@ -69,7 +91,14 @@ class TabsBar extends React.PureComponent {
|
|||
</NavLink>);
|
||||
if (account) {
|
||||
links.push(
|
||||
<NavLink key='notifications' className='tabs-bar__link' to='/notifications' data-preview-title-id='column.notifications'>
|
||||
<NavLink
|
||||
key='notifications'
|
||||
className='tabs-bar__link'
|
||||
to='/notifications'
|
||||
data-preview-title-id='column.notifications'
|
||||
onClick={this.handleNotificationsClick}
|
||||
innerRef={this.setNotifBtnRef}
|
||||
>
|
||||
<IconWithCounter icon='bell' count={notificationCount} />
|
||||
<span><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></span>
|
||||
</NavLink>);
|
||||
|
@ -115,6 +144,14 @@ class TabsBar extends React.PureComponent {
|
|||
<div className='tabs-bar__container'>
|
||||
<div className='tabs-bar__split tabs-bar__split--left'>
|
||||
{this.getNavLinks()}
|
||||
|
||||
<Overlay show={this.state.notificationsOpen} placement='bottom' target={this.getNotifBtn}>
|
||||
<DropdownElement onClose={this.handleCloseNotifications}>
|
||||
<div className='dropdown-menu__notifications'>
|
||||
<Notifications />
|
||||
</div>
|
||||
</DropdownElement>
|
||||
</Overlay>
|
||||
</div>
|
||||
<div className='tabs-bar__split tabs-bar__split--right'>
|
||||
<div className='tabs-bar__search-container'>
|
||||
|
|
|
@ -157,3 +157,26 @@
|
|||
.dropdown__icon {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.dropdown-menu__notifications {
|
||||
overflow: hidden;
|
||||
|
||||
.column {
|
||||
padding: 4px !important;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.notification__filter-bar button {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.column-header__wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.slist {
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
max-height: 200px;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue