bigbluebutton-Github/bigbluebutton-html5/imports/ui/components/common/menu/component.jsx

322 lines
10 KiB
React
Raw Normal View History

2021-07-08 19:41:03 +08:00
import React from "react";
import PropTypes from "prop-types";
import { defineMessages, injectIntl } from "react-intl";
2023-05-03 03:24:06 +08:00
import { Divider } from "@mui/material";
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';
import KEY_CODES from '/imports/utils/keyCodes';
import MenuSkeleton from './skeleton';
import GenericContentItem from '/imports/ui/components/generic-content/generic-content-item/component';
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 = {};
this.autoFocus = false;
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);
}
componentDidUpdate() {
const { anchorEl } = this.state;
const { open } = this.props;
if (open === false && anchorEl) {
this.setState({ anchorEl: null });
} else if (open === true && !anchorEl) {
this.setState({ anchorEl: this.anchorElRef });
}
}
handleKeyDown(event) {
const { anchorEl } = this.state;
const { isHorizontal } = this.props;
const isMenuOpen = Boolean(anchorEl);
const previousKey = isHorizontal ? KEY_CODES.ARROW_LEFT : KEY_CODES.ARROW_UP;
const nextKey = isHorizontal ? KEY_CODES.ARROW_RIGHT : KEY_CODES.ARROW_DOWN;
if ([KEY_CODES.ESCAPE, KEY_CODES.TAB].includes(event.which)) {
this.handleClose();
return;
}
if (isMenuOpen && [previousKey, nextKey].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 === previousKey ? 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) {
this.setState({ anchorEl: event.currentTarget });
2021-07-08 19:41:03 +08:00
};
handleClose(event) {
2021-07-08 19:41:03 +08:00
const { onCloseCallback } = this.props;
this.setState({ anchorEl: null }, onCloseCallback());
if (event) {
event.persist();
if (event.type === 'click') {
setTimeout(() => {
document.activeElement.blur();
}, 0);
}
}
2021-07-08 19:41:03 +08:00
};
makeMenuItems() {
const { actions, selectedEmoji, intl, isHorizontal, isEmoji, isMobile, roundButtons, keepOpen } = this.props;
2021-07-08 19:41:03 +08:00
return actions?.map(a => {
const { dataTest, label, onClick, key, disabled,
description, selected, textColor, isToggle, loading,
isTitle, titleActions, contentFunction } = a;
2021-11-06 03:59:01 +08:00
const emojiSelected = key?.toLowerCase()?.includes(selectedEmoji?.toLowerCase());
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',
marginLeft: '0px',
marginRight: '0px',
};
if (a.customStyles) {
customStyles = { ...customStyles, ...a.customStyles };
}
if (loading) {
return (
2024-05-20 22:09:01 +08:00
<MenuSkeleton key={label} />
);
}
return [
(!a.isSeparator && onClick) && (
<Styled.BBBMenuItem
emoji={emojiSelected ? 'yes' : 'no'}
key={label}
data-test={dataTest}
data-key={`menuItem-${dataTest}`}
disableRipple={true}
disableGutters={true}
disabled={disabled}
style={customStyles}
$roundButtons={roundButtons}
2024-04-24 01:56:23 +08:00
$isToggle={isToggle}
onClick={(event) => {
onClick(event);
const close = !keepOpen && !key?.includes('setstatus') && !key?.includes('back');
// prevent menu close for sub menu actions
if (close) this.handleClose(event);
event.stopPropagation();
}}>
<Styled.MenuItemWrapper
isMobile={isMobile}
isEmoji={isEmoji}
>
{a.icon ? <Icon iconName={a.icon} key="icon" /> : null}
<Styled.Option hasIcon={!!(a.icon)} isHorizontal={isHorizontal} isMobile={isMobile} aria-describedby={`${key}-option-desc`}>{label}</Styled.Option>
{description && <div className="sr-only" id={`${key}-option-desc`}>{`${description}${selected ? ` - ${intl.formatMessage(intlMessages.active)}` : ''}`}</div>}
{a.iconRight ? <Styled.IconRight iconName={a.iconRight} key="iconRight" /> : null}
</Styled.MenuItemWrapper>
</Styled.BBBMenuItem>
),
(!onClick && !a.isSeparator) && (
<Styled.BBBMenuInformation
key={a.key}
isTitle={isTitle}
isGenericContent={!!contentFunction}
>
<Styled.MenuItemWrapper
hasSpaceBetween={isTitle && titleActions}
>
{!contentFunction ? (
<>
{a.icon ? <Icon color={textColor} iconName={a.icon} key="icon" /> : null}
<Styled.Option hasIcon={!!(a.icon)} isTitle={isTitle} textColor={textColor} isHorizontal={isHorizontal} isMobile={isMobile} aria-describedby={`${key}-option-desc`}>{label}</Styled.Option>
{a.iconRight ? <Styled.IconRight color={textColor} iconName={a.iconRight} key="iconRight" /> : null}
{(isTitle && titleActions?.length > 0) ? (
2024-10-03 06:09:07 +08:00
titleActions.map((item, index) => (
<Styled.TitleAction
2024-10-03 06:09:07 +08:00
key={item.id || index}
tooltipplacement="right"
size="md"
onClick={item.onClick}
circle
tooltipLabel={item.tooltip}
hideLabel
icon={item.icon}
/>
))
) : null}
</>
) : (
<GenericContentItem
width="100%"
renderFunction={contentFunction}
/>
)}
</Styled.MenuItemWrapper>
</Styled.BBBMenuInformation>
),
a.isSeparator && <Divider disabled />
];
}) ?? [];
2021-07-08 19:41:03 +08:00
}
2021-07-08 19:41:03 +08:00
render() {
const { anchorEl } = this.state;
2023-06-27 22:08:49 +08:00
const {
trigger,
intl,
customStyles,
dataTest,
opts,
accessKey,
open,
renderOtherComponents,
customAnchorEl,
hasRoundedCorners,
overrideMobileStyles,
isHorizontal,
2023-06-27 22:08:49 +08:00
} = this.props;
2021-07-08 19:41:03 +08:00
const actionsItems = this.makeMenuItems();
const roundedCornersStyles = { borderRadius: '3rem' };
2023-05-16 03:21:27 +08:00
let menuStyles = { zIndex: 999 };
if (customStyles) {
menuStyles = { ...menuStyles, ...customStyles };
}
if (isHorizontal) {
const horizontalStyles = { display: 'flex' };
menuStyles = { ...menuStyles, ...horizontalStyles};
}
2021-07-08 19:41:03 +08:00
return (
<>
<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;
this.handleClick(e);
}}
onKeyPress={(e) => {
e.persist();
2023-05-03 03:24:06 +08:00
if (e.which !== KEY_CODES.ENTER) return null;
this.handleClick(e);
}}
accessKey={accessKey}
ref={(ref) => this.anchorElRef = ref}
2023-05-03 03:24:06 +08:00
role="button"
2023-05-10 10:15:08 +08:00
tabIndex={-1}
>
{trigger}
</div>
<Styled.MenuWrapper
2022-05-13 21:42:19 +08:00
{...opts}
{...this.optsToMerge}
2023-06-14 01:51:42 +08:00
anchorEl={customAnchorEl ? customAnchorEl : anchorEl}
2021-07-08 19:41:03 +08:00
open={Boolean(anchorEl)}
onClose={this.handleClose}
style={menuStyles}
2022-01-29 03:52:22 +08:00
data-test={dataTest}
onKeyDownCapture={this.handleKeyDown}
2023-08-14 21:47:23 +08:00
$isHorizontal={isHorizontal}
2023-06-27 22:08:49 +08:00
PaperProps={{
style: hasRoundedCorners ? roundedCornersStyles : {},
className: overrideMobileStyles ? 'override-mobile-styles' : 'MuiPaper-root-mobile',
}}
2021-07-08 19:41:03 +08:00
>
{actionsItems}
{renderOtherComponents}
2023-06-27 22:08:49 +08:00
{!overrideMobileStyles && 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}
/>
}
</Styled.MenuWrapper>
</>
2021-07-08 19:41:03 +08:00
);
}
}
BBBMenu.defaultProps = {
opts: {
id: "default-dropdown-menu",
autoFocus: false,
2021-07-08 19:41:03 +08:00
keepMounted: true,
transitionDuration: 0,
elevation: 3,
2023-05-03 03:24:06 +08:00
getcontentanchorel: null,
2021-07-08 19:41:03 +08:00
fullwidth: "true",
anchorOrigin: { vertical: 'top', horizontal: 'right' },
transformorigin: { vertical: 'top', horizontal: 'right' },
2021-07-08 19:41:03 +08:00
},
onCloseCallback: () => { },
2023-05-03 03:24:06 +08:00
dataTest: '',
2021-07-08 19:41:03 +08:00
};
BBBMenu.propTypes = {
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}).isRequired,
trigger: PropTypes.element.isRequired,
actions: PropTypes.array.isRequired,
2021-07-08 19:41:03 +08:00
onCloseCallback: PropTypes.func,
2022-01-29 03:52:22 +08:00
dataTest: PropTypes.string,
2023-05-03 03:24:06 +08:00
open: PropTypes.bool,
customStyles: PropTypes.object,
opts: PropTypes.object,
accessKey: PropTypes.string,
2021-07-08 19:41:03 +08:00
};
2023-05-03 03:24:06 +08:00
export default injectIntl(BBBMenu);