Skip to content

Commit

Permalink
Merge pull request #3675 from Sage/FE-2546-select-component-popper
Browse files Browse the repository at this point in the history
feat(select): add poperjs positioning mechanism - FE-2546
  • Loading branch information
mkrds authored Feb 18, 2021
2 parents 0c37bfd + 133ea2a commit e29bb21
Show file tree
Hide file tree
Showing 13 changed files with 274 additions and 210 deletions.
2 changes: 1 addition & 1 deletion cypress/support/step-definitions/button-steps.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Then("Button as a sibling background color is {string}", (color) => {
});

When("I click on {string}", (element) => {
buttonDataComponentIFrame(element).click();
buttonDataComponentIFrame(element).click({ force: true });
});

When("I click on {string} as a sibling", (element) => {
Expand Down
2 changes: 1 addition & 1 deletion cypress/support/step-definitions/select-steps.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ When("I focus openOnFocus Select input", () => {
});

When("I click openOnFocus Select input", () => {
openOnFocusID().click();
openOnFocusID().click({ force: true });
});

Then("{string} Select list is opened", (name) => {
Expand Down
82 changes: 52 additions & 30 deletions src/__internal__/popover/popover.component.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useRef } from "react";
import React, { useEffect, useLayoutEffect, useRef } from "react";
import ReactDOM from "react-dom";
import PropTypes from "prop-types";
import { createPopper } from "@popperjs/core";
Expand Down Expand Up @@ -36,8 +36,9 @@ const Popover = ({
modifiers,
}) => {
const elementDOM = useRef();
if (!elementDOM.current) {
if (!elementDOM.current && !disablePortal) {
elementDOM.current = document.createElement("div");
document.body.appendChild(elementDOM.current);
}
const popperInstance = useRef();
const popperRef = useRef();
Expand All @@ -54,41 +55,62 @@ const Popover = ({
popperElementRef = popperRef;
}

/* istanbul ignore next */
const observer = useRef(
new ResizeObserver(() => {
if (popperInstance.current) {
popperInstance.current.update();
}
})
);

useEffect(() => {
popperInstance.current = createPopper(
reference.current,
popperElementRef.current,
{
placement,
onFirstUpdate,
modifiers: [
alignSameWidthPopper,
{
name: "computeStyles",
options: {
gpuAcceleration: false,
const observerRef = observer.current;
const referenceRef = reference.current;
observer.current.observe(referenceRef);

return () => {
observerRef.unobserve(referenceRef);
observerRef.disconnect();
};
}, [reference]);

useLayoutEffect(() => {
if (reference.current) {
popperInstance.current = createPopper(
reference.current,
popperElementRef.current,
{
placement,
onFirstUpdate,
modifiers: [
alignSameWidthPopper,
{
name: "computeStyles",
options: {
gpuAcceleration: false,
},
},
},
...(modifiers || []),
],
}
);
...(modifiers || []),
],
}
);
}

return () => {
popperInstance.current.destroy();
popperInstance.current = null;
if (popperInstance.current) {
popperInstance.current.destroy();
popperInstance.current = null;
}
};
}, [modifiers, onFirstUpdate, placement, popperElementRef, reference]);

// eslint-disable-next-line consistent-return
useEffect(() => {
if (!disablePortal) {
const portalElement = elementDOM.current;
document.body.appendChild(portalElement);
return () => {
document.body.removeChild(portalElement);
};
}
return () => {
if (!disablePortal) {
document.body.removeChild(elementDOM.current);
}
};
}, [disablePortal]);

if (disablePortal) {
Expand Down Expand Up @@ -124,7 +146,7 @@ Popover.propTypes = {
modifiers: PropTypes.array,
// Optional onFirstUpdate funcition, for more information go to:
// https://popper.js.org/docs/v2/constructors/#modifiers
onFirstUpdate: PropTypes.array,
onFirstUpdate: PropTypes.func,
// When true, children are not rendered in portal
disablePortal: PropTypes.bool,
// Reference element, children will be positioned in relation to this element - should be a ref
Expand Down
11 changes: 8 additions & 3 deletions src/__internal__/popover/popover.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { useCallback, useState } from "react";
import ReactDOM from "react-dom";
import { mount } from "enzyme";
import { createPopper } from "@popperjs/core";
Expand All @@ -8,9 +8,14 @@ import Popover, { alignSameWidthPopoverFunction } from "./popover.component";
jest.mock("@popperjs/core");

const Component = (props) => {
const ref = React.createRef();
const [ref, setRef] = useState({});

const setRefCallback = useCallback((reference) => {
setRef({ current: reference });
}, []);

return (
<div ref={ref} id="popover-container">
<div ref={setRefCallback} id="popover-container">
<Popover placement="bottom-start" {...props} reference={ref}>
<div id="popover-children" />
</Popover>
Expand Down
2 changes: 2 additions & 0 deletions src/__spec_helper__/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Enzyme from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import { setup } from "./mock-match-media";
import setupResizeObserverMock from "./mock-resize-observer";

setupResizeObserverMock();
setup();
Enzyme.configure({ adapter: new Adapter() });
14 changes: 14 additions & 0 deletions src/__spec_helper__/mock-resize-observer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const setupResizeObserverMock = () => {
if (!global.window) {
return;
}
global.window.ResizeObserver =
global.window.ResizeObserver ||
jest.fn().mockImplementation(() => ({
disconnect: jest.fn(),
observe: jest.fn(),
unobserve: jest.fn(),
}));
};

export default setupResizeObserverMock;
25 changes: 8 additions & 17 deletions src/components/action-popover/action-popover-item.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import createGuid from "../../utils/helpers/guid";
import ActionPopoverContext from "./action-popover-context";

const INTERVAL = 150;
const DEFER_TIMEOUT = 30;

const MenuItem = ({
children,
Expand All @@ -43,7 +42,6 @@ const MenuItem = ({
const [isLeftAligned, setIsLeftAligned] = useState(true);
const submenuRef = useRef();
const ref = useRef();
const refTimer = useRef();
const mouseEnterTimer = useRef();
const mouseLeaveTimer = useRef();
const { spacing } = theme;
Expand All @@ -64,23 +62,16 @@ const MenuItem = ({
}
}, [submenu, spacing, placement]);

const setRef = useCallback(
(element) => {
clearTimeout(refTimer.current);
refTimer.current = setTimeout(() => {
ref.current = element;
alignSubmenu();
if (focusItem && element) {
element.focus();
}
}, DEFER_TIMEOUT);
},
[alignSubmenu, focusItem]
);
useEffect(() => {
alignSubmenu();

if (focusItem && ref.current) {
ref.current.focus();
}
}, [alignSubmenu, focusItem]);

useEffect(() => {
return function cleanup() {
clearTimeout(refTimer.current);
clearTimeout(mouseEnterTimer.current);
clearTimeout(mouseLeaveTimer.current);
};
Expand Down Expand Up @@ -199,7 +190,7 @@ const MenuItem = ({
return (
<div
{...rest}
ref={setRef}
ref={ref}
onClick={onClick}
onKeyDown={onKeyDown}
type="button"
Expand Down
49 changes: 23 additions & 26 deletions src/components/select/multi-select/multi-select.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ const MultiSelect = React.forwardRef(
const [selectedValue, setSelectedValue] = useState([]);
const [highlightedValue, setHighlightedValue] = useState("");
const [filterText, setFilterText] = useState("");
const [repositionTrigger, setRepositionTrigger] = useState(false);
const [placeholderOverride, setPlaceholderOverride] = useState();

const setOpen = useCallback(() => {
Expand Down Expand Up @@ -99,6 +98,25 @@ const MultiSelect = React.forwardRef(
[children, setOpen]
);

const removeSelectedValue = useCallback(
(index) => {
setSelectedValue((previousValue) => {
isClickTriggeredBySelect.current = true;
if (previousValue.length === 0) {
return previousValue;
}
const newValue = [...previousValue];
newValue.splice(index, 1);
if (isControlled.current && onChange) {
onChange(createCustomEvent(newValue));
return newValue;
}
return newValue;
});
},
[createCustomEvent, onChange]
);

const handleTextboxKeydown = useCallback(
(event) => {
const { key } = event;
Expand Down Expand Up @@ -136,7 +154,10 @@ const MultiSelect = React.forwardRef(
const notInContainer =
containerRef.current && !containerRef.current.contains(event.target);

if (notInContainer && !isClickTriggeredBySelect.current) {
const notInList =
listboxRef.current && !listboxRef.current.contains(event.target);

if (notInContainer && notInList && !isClickTriggeredBySelect.current) {
setTextValue("");
setFilterText("");
setHighlightedValue("");
Expand Down Expand Up @@ -229,10 +250,6 @@ const MultiSelect = React.forwardRef(
if (!isControlled.current && onChange) {
onChange(createCustomEvent(selectedValue));
}

setRepositionTrigger((previousValue) => {
return !previousValue;
});
}, [createCustomEvent, onChange, selectedValue]);

function handleTextboxClick(event) {
Expand Down Expand Up @@ -379,25 +396,6 @@ const MultiSelect = React.forwardRef(
});
}

function removeSelectedValue(index) {
setSelectedValue((previousValue) => {
if (previousValue.length === 0) {
return previousValue;
}

const newValue = [...previousValue];
newValue.splice(index, 1);

if (isControlled.current && onChange) {
onChange(createCustomEvent(newValue));

return previousValue;
}

return newValue;
});
}

function assignInput(input) {
setTextboxRef(input.current);

Expand Down Expand Up @@ -450,7 +448,6 @@ const MultiSelect = React.forwardRef(
filterText={filterText}
highlightedValue={highlightedValue}
noResultsMessage={noResultsMessage}
repositionTrigger={repositionTrigger}
disablePortal={disablePortal}
isLoading={isLoading}
>
Expand Down
Loading

0 comments on commit e29bb21

Please sign in to comment.