2021-07-08 19:41:03 +08:00
|
|
|
import React from "react";
|
|
|
|
import PropTypes from "prop-types";
|
|
|
|
import { defineMessages, injectIntl } from "react-intl";
|
|
|
|
|
|
|
|
import Menu from "@material-ui/core/Menu";
|
|
|
|
import { Divider } from "@material-ui/core";
|
2022-02-15 22:51:51 +08:00
|
|
|
import Icon from "/imports/ui/components/common/icon/component";
|
2022-03-04 04:48:56 +08:00
|
|
|
import { SMALL_VIEWPORT_BREAKPOINT } from '/imports/ui/components/layout/enums';
|
2023-04-01 02:06:41 +08:00
|
|
|
import KEY_CODES from '/imports/utils/keyCodes';
|
2021-07-08 19:41:03 +08:00
|
|
|
|
2021-11-06 03:59:01 +08:00
|
|
|
import { ENTER } from "/imports/utils/keyCodes";
|
2021-09-14 09:50:24 +08:00
|
|
|
|
2021-11-06 03:59:01 +08:00
|
|
|
import Styled from './styles';
|
2021-07-08 19:41:03 +08:00
|
|
|
|
|
|
|
const intlMessages = defineMessages({
|
|
|
|
close: {
|
|
|
|
id: 'app.dropdown.close',
|
|
|
|
description: 'Close button label',
|
|
|
|
},
|
2023-01-04 23:24:39 +08:00
|
|
|
active: {
|
|
|
|
id: 'app.dropdown.list.item.activeLabel',
|
|
|
|
description: 'active item label',
|
|
|
|
},
|
2021-07-08 19:41:03 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
class BBBMenu extends React.Component {
|
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
|
|
|
this.state = {
|
|
|
|
anchorEl: null,
|
|
|
|
};
|
|
|
|
|
2022-05-13 21:42:19 +08:00
|
|
|
this.optsToMerge = {};
|
2021-09-02 06:17:18 +08:00
|
|
|
this.autoFocus = false;
|
|
|
|
|
2023-04-01 02:06:41 +08:00
|
|
|
this.handleKeyDown = this.handleKeyDown.bind(this);
|
2021-07-08 19:41:03 +08:00
|
|
|
this.handleClick = this.handleClick.bind(this);
|
|
|
|
this.handleClose = this.handleClose.bind(this);
|
|
|
|
}
|
|
|
|
|
2022-04-01 00:58:41 +08:00
|
|
|
componentDidUpdate() {
|
|
|
|
const { anchorEl } = this.state;
|
2023-04-01 02:06:41 +08:00
|
|
|
const { open } = this.props;
|
|
|
|
if (open === false && anchorEl) {
|
2022-04-01 00:58:41 +08:00
|
|
|
this.setState({ anchorEl: null });
|
2023-04-01 02:06:41 +08:00
|
|
|
} else if (open === true && !anchorEl) {
|
2022-04-01 00:58:41 +08:00
|
|
|
this.setState({ anchorEl: this.anchorElRef });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-01 02:06:41 +08:00
|
|
|
handleKeyDown(event) {
|
|
|
|
const { anchorEl } = this.state;
|
|
|
|
const isMenuOpen = Boolean(anchorEl);
|
|
|
|
|
|
|
|
|
|
|
|
if ([KEY_CODES.ESCAPE, KEY_CODES.TAB].includes(event.which)) {
|
|
|
|
this.handleClose();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isMenuOpen && [KEY_CODES.ARROW_UP, KEY_CODES.ARROW_DOWN].includes(event.which)) {
|
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
|
|
|
const menuItems = Array.from(document.querySelectorAll('[data-key^="menuItem-"]'));
|
|
|
|
if (menuItems.length === 0) return;
|
|
|
|
|
|
|
|
const focusedIndex = menuItems.findIndex(item => item === document.activeElement);
|
|
|
|
const nextIndex = event.which === KEY_CODES.ARROW_UP ? focusedIndex - 1 : focusedIndex + 1;
|
|
|
|
let indexToFocus = 0;
|
|
|
|
if (nextIndex < 0) {
|
|
|
|
indexToFocus = menuItems.length - 1;
|
|
|
|
} else if (nextIndex >= menuItems.length) {
|
|
|
|
indexToFocus = 0;
|
|
|
|
} else {
|
|
|
|
indexToFocus = nextIndex;
|
|
|
|
}
|
|
|
|
|
|
|
|
menuItems[indexToFocus].focus();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-07-08 19:41:03 +08:00
|
|
|
handleClick(event) {
|
2021-08-03 01:48:52 +08:00
|
|
|
this.setState({ anchorEl: event.currentTarget });
|
2021-07-08 19:41:03 +08:00
|
|
|
};
|
2021-08-03 01:48:52 +08:00
|
|
|
|
2021-09-01 04:08:26 +08:00
|
|
|
handleClose(event) {
|
2021-07-08 19:41:03 +08:00
|
|
|
const { onCloseCallback } = this.props;
|
2021-08-03 01:48:52 +08:00
|
|
|
this.setState({ anchorEl: null }, onCloseCallback());
|
2021-09-01 04:08:26 +08:00
|
|
|
|
|
|
|
if (event) {
|
|
|
|
event.persist();
|
|
|
|
|
|
|
|
if (event.type === 'click') {
|
|
|
|
setTimeout(() => {
|
|
|
|
document.activeElement.blur();
|
|
|
|
}, 0);
|
|
|
|
}
|
|
|
|
}
|
2021-07-08 19:41:03 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
makeMenuItems() {
|
2023-01-04 23:45:26 +08:00
|
|
|
const { actions, selectedEmoji, intl } = this.props;
|
2021-07-19 01:30:27 +08:00
|
|
|
|
2021-07-08 19:41:03 +08:00
|
|
|
return actions?.map(a => {
|
2023-04-28 22:35:41 +08:00
|
|
|
const { dataTest, label, onClick, key, disabled, accessKey, description, selected } = a;
|
2021-11-06 03:59:01 +08:00
|
|
|
const emojiSelected = key?.toLowerCase()?.includes(selectedEmoji?.toLowerCase());
|
2021-07-19 01:30:27 +08:00
|
|
|
|
2021-11-06 02:38:01 +08:00
|
|
|
let customStyles = {
|
2022-05-24 00:58:51 +08:00
|
|
|
paddingLeft: '16px',
|
|
|
|
paddingRight: '16px',
|
2022-05-23 22:45:53 +08:00
|
|
|
paddingTop: '12px',
|
|
|
|
paddingBottom: '12px',
|
2022-05-20 04:14:56 +08:00
|
|
|
marginLeft: '0px',
|
|
|
|
marginRight: '0px',
|
2021-11-06 02:38:01 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
if (a.customStyles) {
|
|
|
|
customStyles = { ...customStyles, ...a.customStyles };
|
|
|
|
}
|
|
|
|
|
2021-08-09 01:13:47 +08:00
|
|
|
return [
|
2021-09-02 06:17:18 +08:00
|
|
|
a.dividerTop && <Divider disabled />,
|
2021-11-06 03:59:01 +08:00
|
|
|
<Styled.BBBMenuItem
|
2021-11-11 21:36:00 +08:00
|
|
|
emoji={emojiSelected ? 'yes' : 'no'}
|
2021-08-09 01:13:47 +08:00
|
|
|
key={label}
|
2022-02-04 22:13:42 +08:00
|
|
|
data-test={dataTest}
|
2023-04-01 02:06:41 +08:00
|
|
|
data-key={`menuItem-${dataTest}`}
|
2021-08-09 01:13:47 +08:00
|
|
|
disableRipple={true}
|
|
|
|
disableGutters={true}
|
2021-08-11 10:44:34 +08:00
|
|
|
disabled={disabled}
|
2021-11-06 02:38:01 +08:00
|
|
|
style={customStyles}
|
2021-09-01 04:08:26 +08:00
|
|
|
onClick={(event) => {
|
2021-08-09 01:13:47 +08:00
|
|
|
onClick();
|
|
|
|
const close = !key.includes('setstatus') && !key.includes('back');
|
|
|
|
// prevent menu close for sub menu actions
|
2021-09-01 04:08:26 +08:00
|
|
|
if (close) this.handleClose(event);
|
2022-03-01 21:13:23 +08:00
|
|
|
event.stopPropagation();
|
2021-08-09 01:13:47 +08:00
|
|
|
}}>
|
2022-05-20 04:14:56 +08:00
|
|
|
<Styled.MenuItemWrapper>
|
2021-07-08 19:41:03 +08:00
|
|
|
{a.icon ? <Icon iconName={a.icon} key="icon" /> : null}
|
2022-10-07 01:43:58 +08:00
|
|
|
<Styled.Option aria-describedby={`${key}-option-desc`}>{label}</Styled.Option>
|
2023-01-04 23:24:39 +08:00
|
|
|
{description && <div className="sr-only" id={`${key}-option-desc`}>{`${description}${selected ? ` - ${intl.formatMessage(intlMessages.active)}` : ''}`}</div>}
|
2021-11-06 03:59:01 +08:00
|
|
|
{a.iconRight ? <Styled.IconRight iconName={a.iconRight} key="iconRight" /> : null}
|
2022-05-20 04:14:56 +08:00
|
|
|
</Styled.MenuItemWrapper>
|
2021-11-06 03:59:01 +08:00
|
|
|
</Styled.BBBMenuItem>,
|
2021-09-02 06:17:18 +08:00
|
|
|
a.divider && <Divider disabled />
|
2021-08-09 01:13:47 +08:00
|
|
|
];
|
2021-07-08 19:41:03 +08:00
|
|
|
});
|
|
|
|
}
|
2021-08-03 01:48:52 +08:00
|
|
|
|
2021-07-08 19:41:03 +08:00
|
|
|
render() {
|
|
|
|
const { anchorEl } = this.state;
|
2023-04-28 22:35:41 +08:00
|
|
|
const { accessKey, trigger, intl, customStyles, renderOtherComponents, dataTest, opts } = this.props;
|
2021-07-08 19:41:03 +08:00
|
|
|
const actionsItems = this.makeMenuItems();
|
2021-07-19 01:30:27 +08:00
|
|
|
|
2021-11-06 02:38:01 +08:00
|
|
|
let menuStyles = { zIndex: 9999 };
|
|
|
|
|
|
|
|
if (customStyles) {
|
|
|
|
menuStyles = { ...menuStyles, ...customStyles };
|
|
|
|
}
|
|
|
|
|
2021-07-08 19:41:03 +08:00
|
|
|
return (
|
2021-08-17 11:18:49 +08:00
|
|
|
<>
|
2021-09-02 06:17:18 +08:00
|
|
|
<div
|
|
|
|
onClick={(e) => {
|
|
|
|
e.persist();
|
2022-03-28 22:10:54 +08:00
|
|
|
const firefoxInputSource = !([1, 5].includes(e.nativeEvent.mozInputSource)); // 1 = mouse, 5 = touch (firefox only)
|
|
|
|
const chromeInputSource = !(['mouse', 'touch'].includes(e.nativeEvent.pointerType));
|
|
|
|
|
2022-05-13 21:42:19 +08:00
|
|
|
this.optsToMerge.autoFocus = firefoxInputSource && chromeInputSource;
|
2021-09-02 06:17:18 +08:00
|
|
|
this.handleClick(e);
|
|
|
|
}}
|
2021-09-14 09:50:24 +08:00
|
|
|
onKeyPress={(e) => {
|
|
|
|
e.persist();
|
|
|
|
if (e.which !== ENTER) return null;
|
|
|
|
this.handleClick(e);
|
|
|
|
}}
|
2023-04-01 02:06:41 +08:00
|
|
|
accessKey={accessKey}
|
2022-04-01 00:58:41 +08:00
|
|
|
ref={(ref) => this.anchorElRef = ref}
|
2021-09-02 06:17:18 +08:00
|
|
|
>
|
|
|
|
{trigger}
|
|
|
|
</div>
|
|
|
|
|
2021-07-08 19:41:03 +08:00
|
|
|
<Menu
|
2022-05-13 21:42:19 +08:00
|
|
|
{...opts}
|
|
|
|
{...this.optsToMerge}
|
2021-07-08 19:41:03 +08:00
|
|
|
anchorEl={anchorEl}
|
|
|
|
open={Boolean(anchorEl)}
|
|
|
|
onClose={this.handleClose}
|
2021-11-06 02:38:01 +08:00
|
|
|
style={menuStyles}
|
2022-01-29 03:52:22 +08:00
|
|
|
data-test={dataTest}
|
2023-04-01 02:06:41 +08:00
|
|
|
onKeyDownCapture={this.handleKeyDown}
|
2021-07-08 19:41:03 +08:00
|
|
|
>
|
|
|
|
{actionsItems}
|
2023-04-28 22:35:41 +08:00
|
|
|
{renderOtherComponents}
|
2022-03-04 04:48:56 +08:00
|
|
|
{anchorEl && window.innerWidth < SMALL_VIEWPORT_BREAKPOINT &&
|
2021-11-06 03:59:01 +08:00
|
|
|
<Styled.CloseButton
|
2021-07-08 19:41:03 +08:00
|
|
|
label={intl.formatMessage(intlMessages.close)}
|
|
|
|
size="lg"
|
|
|
|
color="default"
|
|
|
|
onClick={this.handleClose}
|
|
|
|
/>
|
|
|
|
}
|
|
|
|
</Menu>
|
2021-08-17 11:18:49 +08:00
|
|
|
</>
|
2021-07-08 19:41:03 +08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default injectIntl(BBBMenu);
|
|
|
|
|
|
|
|
BBBMenu.defaultProps = {
|
|
|
|
opts: {
|
|
|
|
id: "default-dropdown-menu",
|
2021-09-02 06:17:18 +08:00
|
|
|
autoFocus: false,
|
2021-07-08 19:41:03 +08:00
|
|
|
keepMounted: true,
|
|
|
|
transitionDuration: 0,
|
|
|
|
elevation: 3,
|
|
|
|
getContentAnchorEl: null,
|
|
|
|
fullwidth: "true",
|
|
|
|
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
2021-08-09 01:13:47 +08:00
|
|
|
transformorigin: { vertical: 'top', horizontal: 'right' },
|
2021-07-08 19:41:03 +08:00
|
|
|
},
|
2021-09-02 06:17:18 +08:00
|
|
|
onCloseCallback: () => { },
|
2021-07-08 19:41:03 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
BBBMenu.propTypes = {
|
|
|
|
intl: PropTypes.shape({
|
|
|
|
formatMessage: PropTypes.func.isRequired,
|
|
|
|
}).isRequired,
|
|
|
|
|
|
|
|
trigger: PropTypes.element.isRequired,
|
|
|
|
|
|
|
|
actions: PropTypes.arrayOf(PropTypes.shape({
|
|
|
|
key: PropTypes.string.isRequired,
|
|
|
|
label: PropTypes.string.isRequired,
|
2021-08-31 02:19:52 +08:00
|
|
|
onClick: PropTypes.func,
|
2021-07-08 19:41:03 +08:00
|
|
|
icon: PropTypes.string,
|
|
|
|
iconRight: PropTypes.string,
|
2021-09-02 06:17:18 +08:00
|
|
|
disabled: PropTypes.bool,
|
2021-07-08 19:41:03 +08:00
|
|
|
divider: PropTypes.bool,
|
2021-08-09 01:13:47 +08:00
|
|
|
dividerTop: PropTypes.bool,
|
2021-08-09 01:22:01 +08:00
|
|
|
accessKey: PropTypes.string,
|
2023-03-29 22:16:47 +08:00
|
|
|
dataTest: PropTypes.string,
|
2021-07-08 19:41:03 +08:00
|
|
|
})).isRequired,
|
|
|
|
|
|
|
|
onCloseCallback: PropTypes.func,
|
2022-01-29 03:52:22 +08:00
|
|
|
dataTest: PropTypes.string,
|
2021-07-08 19:41:03 +08:00
|
|
|
};
|