Skip to content

Commit

Permalink
Dropdown menus bug (#1579)
Browse files Browse the repository at this point in the history
* add popper dropdown

* trigger toggleVisible internally

* fix hook warnings

* fix callback warning

* fix className prop position
  • Loading branch information
jschwarz2030 authored Mar 10, 2021
1 parent aa90bb9 commit bd0057b
Show file tree
Hide file tree
Showing 12 changed files with 289 additions and 71 deletions.
28 changes: 28 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"root": true,
"parser": "babel-eslint",
"plugins": ["react", "react-hooks", "jest"],
"rules": {
"no-unused-vars": "warn",
"react/display-name": "warn",
"react/jsx-key": "warn",
"react/no-unescaped-entities": "warn",
"react/jsx-no-duplicate-props": "warn",
"react/jsx-no-target-blank": "warn",
"react/no-unknown-property": "warn",
"react/prop-types": "warn"
},
"overrides": [
{
"files": ["*.js", "*.jsx"],
"parserOptions": {
"ecmaVersion": 8,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
}
}
],
"extends": ["plugin:react/recommended"]
}
10 changes: 9 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"@nivo/bar": "^0.62.0",
"@nivo/line": "^0.62.0",
"@nivo/radar": "^0.62.0",
"@popperjs/core": "^2.8.4",
"@rjsf/core": "^2.1.0",
"@turf/bbox": "^6.0.1",
"@turf/bbox-polygon": "^6.0.1",
Expand Down Expand Up @@ -70,6 +71,7 @@
"react-leaflet-bing-v2": "^5.0.1",
"react-leaflet-markercluster": "^2.0.0",
"react-onclickoutside": "^6.6.3",
"react-popper": "^2.2.4",
"react-redux": "^7.2.0",
"react-responsive": "^8.1.0",
"react-router": "^5.2.0",
Expand Down Expand Up @@ -136,5 +138,11 @@
"not dead",
"not ie <= 11",
"not op_mini all"
]
],
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
}
}
1 change: 1 addition & 0 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
</noscript>
<div id="root"></div>
<div id="external-root"></div>
<div id="dropdown"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import React from "react";
import { createStore } from "redux";
import { Provider } from "react-redux";
import { render, fireEvent, screen } from "@testing-library/react";
import {
ProjectDashboardInternal as ProjectDashboard,
defaultDashboardSetup,
} from "./ProjectDashboard.js";
import { currentErrors } from "../../../../services/Error/Error";

describe("defaultDashboardSetup", () => {
it("returns an object of dashboard settings with a name", () => {
Expand All @@ -17,7 +13,7 @@ describe("defaultDashboardSetup", () => {

describe("ProjectDashboard", () => {
it("doesn't break if only required props are provided", () => {
const { getByText, debug } = global.withProvider(
const { getByText } = global.withProvider(
<ProjectDashboard
name="project"
targets="foo"
Expand All @@ -29,7 +25,7 @@ describe("ProjectDashboard", () => {
});

it("renders a loader if loadingProject is true", () => {
const { getByTestId, debug } = global.withProvider(
const { getByTestId } = global.withProvider(
<ProjectDashboard
name="project"
targets="foo"
Expand All @@ -42,7 +38,7 @@ describe("ProjectDashboard", () => {
});

it("shows project dashboard if project is provided and loadingProject is false", () => {
const { getByTestId, debug } = global.withProvider(
const { getByTestId } = global.withProvider(
<ProjectDashboard
project={{ foo: "bar" }}
name="project"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,8 @@ export default class AutosuggestMentionTextArea extends Component {
rootProps={downshift.getRootProps({}, {suppressRefError: true})}
suppressControls
fixedMenu={true}
isVisible={show}
isVisible={Boolean(show)}
placement="bottom-start"
toggleVisible={() => _noop}
dropdownButton={dropdown => (
<textarea
Expand Down
2 changes: 1 addition & 1 deletion src/components/AutosuggestTextBox/AutosuggestTextBox.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ export default class AutosuggestTextBox extends Component {
rootProps={downshift.getRootProps({}, {suppressRefError: true})}
suppressControls
fixedMenu={this.props.fixedMenu}
isVisible={show}
isVisible={Boolean(show)}
toggleVisible={() => this.setState({highlightResult: -1})}
dropdownButton={dropdown => (
<input
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const generateMockTask = (id, challengeOwnerId) => {
};

const fetchOSMUserSuccess = () =>
new Promise((resolve, reject) => {
new Promise((resolve) => {
resolve({
displayName: "John",
username: "jdoe",
Expand Down Expand Up @@ -105,7 +105,7 @@ describe("ChallengeOwnerContactLinkInternal", () => {
});

it("renders a link to OSM if challenge owner id is missing, but project owner id is found", async () => {
const { rerender } = render(
render(
<IntlProvider locale="en">
<ContactLink
task={generateMockTask(undefined, 1)}
Expand Down
188 changes: 130 additions & 58 deletions src/components/Dropdown/Dropdown.js
Original file line number Diff line number Diff line change
@@ -1,71 +1,143 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import wrapWithClickout from 'react-clickout'
import SvgSymbol from '../SvgSymbol/SvgSymbol'
import { ExternalContext } from '../External/External'

class Dropdown extends Component {
static contextType = ExternalContext

state = {
isVisible: false,
}

toggleDropdownVisible = () => {
this.setState({isVisible: !this.state.isVisible})
}

closeDropdown = () => {
this.setState({isVisible: false})
}

handleClickout() {
if (!this.context.clickoutSuspended) {
this.closeDropdown()
import React, { useState, useEffect, useRef, useCallback } from "react";
import ReactDOM from "react-dom";
import PropTypes from "prop-types";
import classNames from "classnames";
import { usePopper } from "react-popper";
import UseEventListener from "../../hooks/UseEventListener";

const Portal = ({ children, querySelector = "#dropdown" }) => {
return ReactDOM.createPortal(children, document.querySelector(querySelector));
};

const Dropdown = ({
dropdownButton,
dropdownContent,
className,
rootProps,
innerClassName,
fixedMenu,
toggleVisible,
isVisible,
placement,
}) => {
const [active, setActive] = useState(false);
const [visible, setVisible] = useState(false);
const referenceRef = useRef();
const popperRef = useRef();

const toggle = useCallback(
(bool) => {
setActive(bool);
toggleVisible();
setTimeout(() => setVisible(bool), 1);
},
[toggleVisible]
);

const { styles, attributes, forceUpdate } = usePopper(
referenceRef.current,
popperRef.current,
{
placement: placement || "bottom-end",
modifiers: [
{
name: "preventOverflow",
options: {
rootBoundary: "viewport",
offset: [0, 10],
},
},
],
}
);

useEffect(() => {
if (active && forceUpdate) {
forceUpdate();
}
}, [active, forceUpdate]);

useEffect(() => {
if (isVisible !== undefined) {
if (isVisible && !active) {
toggle(true);
}

if (!isVisible && active) {
toggle(false);
}
}
}
}, [isVisible, active, toggle]);

render() {
const isDropdownVisible =
this.props.toggleVisible ? this.props.isVisible : this.state.isVisible
const handleDocumentClick = (event) => {
if (referenceRef.current.contains(event.target)) {
return null;
}

const renderFuncArgs = {
isDropdownVisible,
toggleDropdownVisible: this.toggleDropdownVisible,
closeDropdown: this.closeDropdown
if (popperRef.current.contains(event.target)) {
return null;
}

return (
<div className={classNames('mr-dropdown', this.props.className)} {...this.props.rootProps}>
{this.props.dropdownButton(renderFuncArgs)}
{isDropdownVisible && (
<div className={classNames("mr-dropdown__wrapper", this.props.wrapperClassName)}>
<div className="mr-dropdown__main">
<div className={classNames("mr-dropdown__inner", this.props.innerClassName, {"mr-fixed": this.props.fixedMenu})}>
{!this.props.suppressControls &&
<SvgSymbol
sym="icon-triangle"
viewBox="0 0 15 10"
className={classNames("mr-dropdown__arrow", this.props.arrowClassName)}
aria-hidden
/>
}
<div className="mr-dropdown__content">
{this.props.dropdownContent(renderFuncArgs)}
toggle(false);
};

UseEventListener("mousedown", handleDocumentClick);

const renderFuncArgs = {
isDropdownVisible: active,
toggleDropdownVisible: () => toggle(!active),
closeDropdown: () => toggle(false),
};

return (
<div data-testid="mr-dropdown" {...rootProps}>
<div ref={referenceRef} className={classNames("mr-dropdown", className)}>
{dropdownButton(renderFuncArgs)}
</div>
<Portal>
<div
ref={popperRef}
className="p-0.5 mr-z-250"
style={styles.popper}
{...attributes.popper}
>
{active && (
<div style={{ visibility: visible ? "visible" : "hidden" }}>
<div className="mr-dropdown__main">
<div
className={classNames("mr-dropdown__inner", innerClassName, {
"mr-fixed": fixedMenu,
})}
>
<div className="mr-dropdown__content">
{dropdownContent(renderFuncArgs)}
</div>
</div>
</div>
</div>
</div>
)}
</div>
)
}
}
)}
</div>
</Portal>
</div>
);
};

Dropdown.propTypes = {
dropdownButton: PropTypes.func.isRequired,
dropdownContent: PropTypes.func.isRequired,
}
className: PropTypes.string,
rootProps: PropTypes.object,
innerClassName: PropTypes.string,
fixedMenu: PropTypes.bool,
suppressControls: PropTypes.bool,
arrowClassName: PropTypes.string,
toggleVisible: PropTypes.func,
isVisible: PropTypes.bool,
placement: PropTypes.string,
};

Dropdown.defaultProps = {
toggleVisible: () => null,
};

export default wrapWithClickout(Dropdown)
export default Dropdown;
Loading

0 comments on commit bd0057b

Please sign in to comment.