mastodon-glitch/app/javascript/flavours/glitch/features/compose/components/dropdown.jsx

244 lines
5.6 KiB
JavaScript

// Package imports.
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import classNames from 'classnames';
import Overlay from 'react-overlays/Overlay';
// Components.
import { IconButton } from 'flavours/glitch/components/icon_button';
import DropdownMenu from './dropdown_menu';
// The component.
export default class ComposerOptionsDropdown extends PureComponent {
static propTypes = {
isUserTouching: PropTypes.func,
disabled: PropTypes.bool,
icon: PropTypes.string,
items: PropTypes.arrayOf(PropTypes.shape({
icon: PropTypes.string,
meta: PropTypes.string,
name: PropTypes.string.isRequired,
text: PropTypes.string,
})).isRequired,
onModalOpen: PropTypes.func,
onModalClose: PropTypes.func,
title: PropTypes.string,
value: PropTypes.string,
onChange: PropTypes.func,
container: PropTypes.func,
renderItemContents: PropTypes.func,
closeOnChange: PropTypes.bool,
};
static defaultProps = {
closeOnChange: true,
};
state = {
open: false,
openedViaKeyboard: undefined,
placement: 'bottom',
};
// Toggles opening and closing the dropdown.
handleToggle = ({ type }) => {
const { onModalOpen } = this.props;
const { open } = this.state;
if (this.props.isUserTouching && this.props.isUserTouching()) {
if (open) {
this.props.onModalClose();
} else {
const modal = this.handleMakeModal();
if (modal && onModalOpen) {
onModalOpen(modal);
}
}
} else {
if (open && this.activeElement) {
this.activeElement.focus({ preventScroll: true });
}
this.setState({ open: !open, openedViaKeyboard: type !== 'click' });
}
};
handleKeyDown = (e) => {
switch (e.key) {
case 'Escape':
this.handleClose();
break;
}
};
handleMouseDown = () => {
if (!this.state.open) {
this.activeElement = document.activeElement;
}
};
handleButtonKeyDown = (e) => {
switch(e.key) {
case ' ':
case 'Enter':
this.handleMouseDown();
break;
}
};
handleKeyPress = (e) => {
switch(e.key) {
case ' ':
case 'Enter':
this.handleToggle(e);
e.stopPropagation();
e.preventDefault();
break;
}
};
handleClose = () => {
if (this.state.open && this.activeElement) {
this.activeElement.focus({ preventScroll: true });
}
this.setState({ open: false });
};
handleItemClick = (e) => {
const {
items,
onChange,
onModalClose,
closeOnChange,
} = this.props;
const i = Number(e.currentTarget.getAttribute('data-index'));
const { name } = items[i];
e.preventDefault(); // Prevents focus from changing
if (closeOnChange) onModalClose();
onChange(name);
};
// Creates an action modal object.
handleMakeModal = () => {
const {
items,
onChange,
onModalOpen,
onModalClose,
value,
} = this.props;
// Required props.
if (!(onChange && onModalOpen && onModalClose && items)) {
return null;
}
// The object.
return {
renderItemContents: this.props.renderItemContents,
onClick: this.handleItemClick,
actions: items.map(
({
name,
...rest
}) => ({
...rest,
active: value && name === value,
name,
}),
),
};
};
setTargetRef = c => {
this.target = c;
};
findTarget = () => {
return this.target;
};
handleOverlayEnter = (state) => {
this.setState({ placement: state.placement });
};
// Rendering.
render () {
const {
disabled,
title,
icon,
items,
onChange,
value,
container,
renderItemContents,
closeOnChange,
} = this.props;
const { open, placement } = this.state;
const active = value && items.findIndex(({ name }) => name === value) === (placement === 'bottom' ? 0 : (items.length - 1));
return (
<div
className={classNames('privacy-dropdown', placement, { active: open })}
onKeyDown={this.handleKeyDown}
ref={this.setTargetRef}
>
<div className={classNames('privacy-dropdown__value', { active })}>
<IconButton
active={open}
className='privacy-dropdown__value-icon'
disabled={disabled}
icon={icon}
inverted
onClick={this.handleToggle}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleButtonKeyDown}
onKeyPress={this.handleKeyPress}
size={18}
style={{
height: null,
lineHeight: '27px',
}}
title={title}
/>
</div>
<Overlay
containerPadding={20}
placement={placement}
show={open}
flip
target={this.findTarget}
container={container}
popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}
>
{({ props, placement }) => (
<div {...props}>
<div className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}>
<DropdownMenu
items={items}
renderItemContents={renderItemContents}
onChange={onChange}
onClose={this.handleClose}
value={value}
openedViaKeyboard={this.state.openedViaKeyboard}
closeOnChange={closeOnChange}
/>
</div>
</div>
)}
</Overlay>
</div>
);
}
}