Skip to content

Commit

Permalink
feat(popover-container): add ClickAwayWrapper to close popover when c…
Browse files Browse the repository at this point in the history
…lick outside detected

Adds the `ClickAwayWrapper` to enable the `PopoverContainer` to close when a `mousedown` event is
detected from outside of the wrapper boundaries

fix #5017, fix #5158
  • Loading branch information
edleeks87 committed Jun 10, 2022
1 parent 091f0a4 commit 5ba1522
Show file tree
Hide file tree
Showing 3 changed files with 226 additions and 77 deletions.
94 changes: 54 additions & 40 deletions src/components/popover-container/popover-container.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import Icon from "../icon";
import createGuid from "../../__internal__/utils/helpers/guid";
import { filterStyledSystemPaddingProps } from "../../style/utils";
import ClickAwayWrapper from "../../__internal__/click-away-wrapper";

const paddingPropTypes = filterStyledSystemPaddingProps(
styledSystemPropTypes.space
Expand All @@ -37,6 +38,7 @@ const PopoverContainer = ({
const isControlled = open !== undefined;
const [isOpenInternal, setIsOpenInternal] = useState(false);

const ref = useRef();
const closeButtonRef = useRef();
const openButtonRef = useRef();
const guid = useRef(createGuid());
Expand Down Expand Up @@ -84,49 +86,61 @@ const PopoverContainer = ({
ariaLabel: closeButtonAriaLabel,
};

const handleClickAway = (e) => {
if (!isControlled) setIsOpenInternal(false);
if (onClose) onClose(e);
};

return (
<PopoverContainerWrapperStyle
data-component="popover-container"
role="region"
aria-labelledby={popoverContainerId}
<ClickAwayWrapper
targets={[ref]}
handleClickAway={handleClickAway}
eventTypeId="mousedown"
>
{renderOpenComponent(renderOpenComponentProps)}
<Transition
in={isOpen}
timeout={{ exit: 300 }}
appear
mountOnEnter
unmountOnExit
nodeRef={popoverContentNodeRef}
<PopoverContainerWrapperStyle
data-component="popover-container"
role="region"
aria-labelledby={popoverContainerId}
ref={ref}
>
{(state) => (
<PopoverContainerContentStyle
data-element="popover-container-content"
role="dialog"
animationState={state}
position={position}
shouldCoverButton={shouldCoverButton}
aria-labelledby={popoverContainerId}
aria-label={containerAriaLabel}
aria-describedby={ariaDescribedBy}
p="16px 24px"
ref={popoverContentNodeRef}
{...filterStyledSystemPaddingProps(rest)}
>
<PopoverContainerHeaderStyle>
<PopoverContainerTitleStyle
id={popoverContainerId}
data-element="popover-container-title"
>
{title}
</PopoverContainerTitleStyle>
{renderCloseComponent(renderCloseComponentProps)}
</PopoverContainerHeaderStyle>
{children}
</PopoverContainerContentStyle>
)}
</Transition>
</PopoverContainerWrapperStyle>
{renderOpenComponent(renderOpenComponentProps)}
<Transition
in={isOpen}
timeout={{ exit: 300 }}
appear
mountOnEnter
unmountOnExit
nodeRef={popoverContentNodeRef}
>
{(state) => (
<PopoverContainerContentStyle
data-element="popover-container-content"
role="dialog"
animationState={state}
position={position}
shouldCoverButton={shouldCoverButton}
aria-labelledby={popoverContainerId}
aria-label={containerAriaLabel}
aria-describedby={ariaDescribedBy}
p="16px 24px"
ref={popoverContentNodeRef}
{...filterStyledSystemPaddingProps(rest)}
>
<PopoverContainerHeaderStyle>
<PopoverContainerTitleStyle
id={popoverContainerId}
data-element="popover-container-title"
>
{title}
</PopoverContainerTitleStyle>
{renderCloseComponent(renderCloseComponentProps)}
</PopoverContainerHeaderStyle>
{children}
</PopoverContainerContentStyle>
)}
</Transition>
</PopoverContainerWrapperStyle>
</ClickAwayWrapper>
);
};

Expand Down
92 changes: 92 additions & 0 deletions src/components/popover-container/popover-container.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -617,3 +617,95 @@ describe("PopoverContainerContentStyle", () => {
});
});
});

describe("open state when click event triggered", () => {
it("should close the container when uncontrolled and target is outside wrapper element", () => {
const wrapper = render({});
act(() => {
wrapper.find(PopoverContainerOpenIcon).props().onAction();
});
expect(wrapper.update().find(PopoverContainerOpenIcon).prop("id")).toBe(
undefined
);
document.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
expect(wrapper.update().find(PopoverContainerOpenIcon).prop("id")).toBe(
"PopoverContainer_guid-123"
);
});

it("should not close the container when uncontrolled and target is inside wrapper element", () => {
const wrapper = render({});
act(() => {
wrapper.find(PopoverContainerOpenIcon).props().onAction();
});
expect(wrapper.update().find(PopoverContainerOpenIcon).prop("id")).toBe(
undefined
);
document.dispatchEvent(
new CustomEvent("click", {
detail: {
enzymeTestingTarget: wrapper?.find(PopoverContainer).getDOMNode(),
},
})
);
expect(wrapper.update().find(PopoverContainerOpenIcon).prop("id")).toBe(
undefined
);
});

it("should close the container when controlled and target is outside wrapper element", () => {
const onCloseFn = jest.fn();
const MockWrapper = () => {
const [open, setOpen] = React.useState(true);

return (
<PopoverContainer
title="PopoverContainerSettings"
open={open}
onClose={(e) => {
setOpen(false);
onCloseFn(e);
}}
/>
);
};
const wrapper = mount(<MockWrapper />);

expect(wrapper.update().find(PopoverContainer).prop("open")).toBe(true);
act(() => {
document.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
});
expect(wrapper.update().find(PopoverContainer).prop("open")).toBe(false);
expect(onCloseFn).toHaveBeenCalled();
});

it("should not close the container when controlled and target is inside wrapper element", () => {
const onCloseFn = jest.fn();
const MockWrapper = () => {
const [open, setOpen] = React.useState(true);

return (
<PopoverContainer
title="PopoverContainerSettings"
open={open}
onClose={(e) => {
setOpen(false);
onCloseFn(e);
}}
/>
);
};
const wrapper = mount(<MockWrapper />);

expect(wrapper.update().find(PopoverContainer).prop("open")).toBe(true);
document.dispatchEvent(
new CustomEvent("click", {
detail: {
enzymeTestingTarget: wrapper?.find(PopoverContainer).getDOMNode(),
},
})
);
expect(wrapper.update().find(PopoverContainer).prop("open")).toBe(true);
expect(onCloseFn).not.toHaveBeenCalled();
});
});
117 changes: 80 additions & 37 deletions src/components/popover-container/popover-container.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import Link from "../link";
import Pill from "../pill";
import Badge from "../badge";
import StyledSystemProps from "../../../.storybook/utils/styled-system-props";
import isChromatic from "../../../.storybook/isChromatic";

export const isOpenForChromatic = isChromatic();

<Meta title="Popover Container" />

Expand Down Expand Up @@ -47,13 +50,22 @@ import PopoverContainer from "carbon-react/lib/components/popover-container";

Use the `title` prop to set a title within the `PopoverContainer`.

**Please note you should supply an `onClose` when controlling the `open` state in order for the `PopoverContainer` to close when clicking outside of the wrapper element**

<Canvas>
<Story name="title" parameters={{ info: { disable: true } }}>
<div style={{ height: 100 }}>
<PopoverContainer title="With a title" open={true}>
Contents
</PopoverContainer>
</div>
{() => {
const [open, setOpen] = useState(isOpenForChromatic);
const onOpen = () => setOpen(true);
const onClose = () => setOpen(false);
return (
<div style={{ height: 100 }}>
<PopoverContainer title="With a title" open={open} onClose={onClose} onOpen={onOpen}>
Contents
</PopoverContainer>
</div>
);
}}
</Story>
</Canvas>

Expand All @@ -62,27 +74,45 @@ Use the `title` prop to set a title within the `PopoverContainer`.
Use the `position` prop to open the `PopoverContainer` to the left, this is useful if your open button is to the right
of the screen.

**Please note you should supply an `onClose` when controlling the `open` state in order for the `PopoverContainer` to close when clicking outside of the wrapper element**

<Canvas>
<Story name="position" parameters={{ info: { disable: true } }}>
<div style={{ height: 150, float: "right", clear: "right" }}>
<PopoverContainer title="Right Aligned" position="left" open={true}>
Contents
</PopoverContainer>
</div>
{() => {
const [open, setOpen] = useState(isOpenForChromatic);
const onOpen = () => setOpen(true);
const onClose = () => setOpen(false);
return (
<div style={{ height: 150, float: "right", clear: "right" }}>
<PopoverContainer title="Right Aligned" position="left" open={open} onClose={onClose} onOpen={onOpen}>
Contents
</PopoverContainer>
</div>
);
}}
</Story>
</Canvas>

### Cover Button

Use the `shouldCoverButton` prop to hide the open button when the `PopoverContainer` is open.

**Please note you should supply an `onClose` when controlling the `open` state in order for the `PopoverContainer` to close when clicking outside of the wrapper element**

<Canvas>
<Story name="cover button" parameters={{ info: { disable: true } }}>
<div style={{ height: 100 }}>
<PopoverContainer title="Cover Button" shouldCoverButton open={true}>
Contents
</PopoverContainer>
</div>
{() => {
const [open, setOpen] = useState(isOpenForChromatic);
const onOpen = () => setOpen(true);
const onClose = () => setOpen(false);
return (
<div style={{ height: 100 }}>
<PopoverContainer title="Cover Button" shouldCoverButton open={open} onClose={onClose} onOpen={onOpen}>
Contents
</PopoverContainer>
</div>
);
}}
</Story>
</Canvas>

Expand Down Expand Up @@ -118,6 +148,8 @@ Use the `renderOpenComponent` and `renderCloseComponent` to render your own open

You can use the `open`, `onOpen` and `onClose` props to control the open state of the `PopoverContainer`.

**Please note you should supply an `onClose` when controlling the `open` state in order for the `PopoverContainer` to close when clicking outside of the wrapper element**

<Canvas>
<Story name="controlled" parameters={{ info: { disable: true } }}>
{() => {
Expand Down Expand Up @@ -149,31 +181,40 @@ You can use the `open`, `onOpen` and `onClose` props to control the open state o

You can easily use many different components to create your own composition.

**Please note you should supply an `onClose` when controlling the `open` state in order for the `PopoverContainer` to close when clicking outside of the wrapper element**

<Canvas>
<Story name="complex" parameters={{ info: { disable: true } }}>
<div style={{ height: 330 }}>
<PopoverContainer title="Popover Container Title" open={true}>
<Link>This is example link text</Link>
<div style={{ padding: "25px 0 15px 0" }}>
<Button>Small</Button>
<Button ml={2}>Compact</Button>
{() => {
const [open, setOpen] = useState(isOpenForChromatic);
const onOpen = () => setOpen(true);
const onClose = () => setOpen(false);
return (
<div style={{ height: 330 }}>
<PopoverContainer title="Popover Container Title" open={open} onOpen={onOpen} onClose={onClose}>
<Link>This is example link text</Link>
<div style={{ padding: "25px 0 15px 0" }}>
<Button>Small</Button>
<Button ml={2}>Compact</Button>
</div>
<DraggableContainer>
<DraggableItem key="1" id={1}>
<Checkbox name="one" label="Draggable Label One" />
</DraggableItem>
<DraggableItem key="2" id={2}>
<Checkbox name="two" label="Draggable Label Two" />
</DraggableItem>
<DraggableItem key="3" id={3}>
<Checkbox name="three" label="Draggable Label Three" />
</DraggableItem>
<DraggableItem key="4" id={4}>
<Checkbox name="four" label="Draggable Label Four" />
</DraggableItem>
</DraggableContainer>
</PopoverContainer>
</div>
<DraggableContainer>
<DraggableItem key="1" id={1}>
<Checkbox name="one" label="Draggable Label One" />
</DraggableItem>
<DraggableItem key="2" id={2}>
<Checkbox name="two" label="Draggable Label Two" />
</DraggableItem>
<DraggableItem key="3" id={3}>
<Checkbox name="three" label="Draggable Label Three" />
</DraggableItem>
<DraggableItem key="4" id={4}>
<Checkbox name="four" label="Draggable Label Four" />
</DraggableItem>
</DraggableContainer>
</PopoverContainer>
</div>
);
}}
</Story>
</Canvas>

Expand All @@ -182,6 +223,8 @@ You can easily use many different components to create your own composition.
If you want to use the `PopoverContainer` to create for example `Filter` component.
You can do it easly in this way:

**Please note you should supply an `onClose` when controlling the `open` state in order for the `PopoverContainer` to close when clicking outside of the wrapper element**

<Canvas>
<Story name="filter" parameters={{ info: { disable: true } }}>
{() => {
Expand Down

0 comments on commit 5ba1522

Please sign in to comment.