Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT] Menu component with Radix-ui popover element #2014

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
554 changes: 553 additions & 1 deletion package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@
}
},
"dependencies": {
"@radix-ui/react-popover": "^1.0.7",
"@xmldom/xmldom": "^0.8.10",
"classnames": "^2.3.2",
"commonmark": "^0.30.0",
Expand Down
25 changes: 1 addition & 24 deletions src/renderer/assets/styles/components/dropdown.css
Original file line number Diff line number Diff line change
Expand Up @@ -27,32 +27,9 @@
border-color: var(--color-primary);
border-style: solid;
border-radius: 5px;
margin-top: 20px;
position: relative;

&::after {
content: " ";
position: absolute;
top: -10px;
left: 16px;
width: 0;
height: 0;
border-style: solid;
border-width: 0 7.5px 10px 7.5px;
border-color: transparent transparent var(--color-primary) transparent;
}

&.dropdown_right::after {
left: auto;
right: 34px;
}

&.dropdown_publication::after {
left: auto;
right: 25px;
}

& > * {
& > * {
text-decoration: none;
color: black;
padding: 0.7rem;
Expand Down
161 changes: 23 additions & 138 deletions src/renderer/common/components/menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,166 +6,51 @@
// ==LICENSE-END==

import * as React from "react";
import * as ReactDOM from "react-dom";
import { v4 as uuidv4 } from "uuid";

import MenuButton from "./MenuButton";
import MenuContent from "./MenuContent";
import { connect } from "react-redux";
import { ILibraryRootState } from "readium-desktop/renderer/library/redux/states";
import { DialogTypeName } from "readium-desktop/common/models/dialog";
import * as stylesDropDown from "readium-desktop/renderer/assets/styles/components/dropdown.css";
import * as Popover from "@radix-ui/react-popover";

// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface IBaseProps {
button: React.ReactElement;
content: React.ReactElement;
dir: string; // Direction of menu: right or left
focusMenuButton?: (ref: React.RefObject<HTMLElement>, currentMenuId: string) => void;
}

// IProps may typically extend:
// RouteComponentProps
// ReturnType<typeof mapStateToProps>
// ReturnType<typeof mapDispatchToProps>
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface IProps extends IBaseProps, ReturnType<typeof mapStateToProps> {
interface IProps extends React.PropsWithChildren<IBaseProps> {
}

// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface IState {
contentStyle: React.CSSProperties;
menuOpen: boolean;
}

class Menu extends React.Component<IProps, IState> {

private backFocusMenuButtonRef: React.RefObject<HTMLElement>;
private contentRef: HTMLDivElement;
private menuId: string;

constructor(props: IProps) {
super(props);

this.backFocusMenuButtonRef = React.createRef<HTMLElement>();

this.state = {
contentStyle: {},
menuOpen: false,
};
this.menuId = "menu-" + uuidv4();
this.doBackFocusMenuButton = this.doBackFocusMenuButton.bind(this);
this.setBackFocusMenuButton = this.setBackFocusMenuButton.bind(this);
this.toggleOpenMenu = this.toggleOpenMenu.bind(this);
}
// FIXME / TODO
// do back focus not implemented with Radix popover menu

public componentDidUpdate(oldProps: IProps, oldState: IState) {
if (this.state.menuOpen && !oldState.menuOpen) {
this.refreshStyle();
}
if (oldProps.infoDialogIsOpen === true &&
oldProps.infoDialogIsOpen !== this.props.infoDialogIsOpen) {
this.doBackFocusMenuButton();
}
}
class Menu extends React.Component<IProps, IState> {

public render(): React.ReactElement<{}> {
const { button, dir, content } = this.props;
const contentStyle = this.state.contentStyle;
return (
<>
<MenuButton
menuId={this.menuId}
open={this.state.menuOpen}
toggle={this.toggleOpenMenu}
setBackFocusMenuButton={this.setBackFocusMenuButton}
>
{button}
</MenuButton>
{ this.state.menuOpen ?
<MenuContent
id={this.menuId}
open={this.state.menuOpen}
dir={dir}
menuStyle={contentStyle}
toggle={this.toggleOpenMenu}
setContentRef={(ref) => { this.contentRef = ref; }}
doBackFocusMenuButton={this.doBackFocusMenuButton}
>
<span onClick={() => setTimeout(this.toggleOpenMenu, 1)}>
{content}
</span>
</MenuContent>
: <></>
}
</>
<Popover.Root>
<Popover.Trigger asChild>
<button>
{this.props.button}
</button>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content className="PopoverContent" sideOffset={5}>
<div className={stylesDropDown.dropdown_menu}>
{this.props.children}
</div>
<Popover.Arrow className="PopoverArrow" aria-hidden/>
</Popover.Content>
</Popover.Portal>
</Popover.Root>
);
}

private toggleOpenMenu() {
this.setState({ menuOpen: !this.state.menuOpen });
}

private offset(el: HTMLElement) {
const rect = el.getBoundingClientRect();
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const right = window.innerWidth - (rect.right + 19) - scrollLeft;
return {
top: rect.top + scrollTop,
left: rect.left + scrollLeft,
right,
};
}

private refreshStyle() {
if (!this.backFocusMenuButtonRef?.current || !this.contentRef) {
return;
}
const contentStyle: React.CSSProperties = {
position: "absolute",
};

// calculate vertical position of the menu
const button = this.backFocusMenuButtonRef.current;
const buttonRect = button.getBoundingClientRect();
const bottomPos = window.innerHeight - buttonRect.bottom;
const contentElement = ReactDOM.findDOMNode(this.contentRef) as HTMLElement;
const contentHeight = contentElement.getBoundingClientRect().height;

if (bottomPos < contentHeight) {
contentStyle.top = Math.round(this.offset(button).top - contentHeight) + "px";
} else {
contentStyle.top = Math.round(this.offset(button).top + buttonRect.height) + "px";
}

if (this.props.dir === "right") {
contentStyle.right = Math.round(this.offset(button).right) + "px";
} else {
contentStyle.left = Math.round(this.offset(button).left) + "px";
}

this.setState({ contentStyle });
}

private setBackFocusMenuButton(currentRef: React.RefObject<HTMLElement>, currentMenuId: string) {
if (currentRef?.current && this.menuId === currentMenuId) {
this.backFocusMenuButtonRef = currentRef;
}
}

private doBackFocusMenuButton() {
if (this.backFocusMenuButtonRef?.current) {
this.backFocusMenuButtonRef.current.focus();
}
}
}

const mapStateToProps = (state: ILibraryRootState, _props: IBaseProps) => {
return {
infoDialogIsOpen: state.dialog.open
&& (state.dialog.type === DialogTypeName.PublicationInfoOpds
|| state.dialog.type === DialogTypeName.PublicationInfoLib
|| state.dialog.type === DialogTypeName.DeletePublicationConfirm),
};
};

export default connect(mapStateToProps)(Menu);
export default (Menu);
62 changes: 0 additions & 62 deletions src/renderer/common/components/menu/MenuButton.tsx

This file was deleted.

83 changes: 0 additions & 83 deletions src/renderer/common/components/menu/MenuContent.tsx

This file was deleted.

Loading