Rudimentary notifications dropdown

This commit is contained in:
Alex Gleason 2021-04-22 16:10:37 -05:00
parent bbd4aa4f8b
commit 324f698e58
No known key found for this signature in database
GPG key ID: 7211D1F99744FBB7
4 changed files with 141 additions and 35 deletions

View 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>
);
}
}

View file

@ -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>
);
}

View file

@ -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'>

View file

@ -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;
}
}