diff --git a/ui_framework/src/components/color_picker/color_picker.js b/ui_framework/src/components/color_picker/color_picker.js
index 6c7ada1a9683..41965cb48a41 100644
--- a/ui_framework/src/components/color_picker/color_picker.js
+++ b/ui_framework/src/components/color_picker/color_picker.js
@@ -1,9 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
-
import { ChromePicker } from 'react-color';
+import { KuiOutsideClickDetector } from '../outside_click_detector';
+
import { KuiColorPickerSwatch } from './color_picker_swatch';
export class KuiColorPicker extends React.Component {
@@ -12,16 +13,9 @@ export class KuiColorPicker extends React.Component {
this.state = {
showColorSelector: false,
};
- // Use this variable to differentiate between clicks on the element that should not cause the pop up
- // to close, and external clicks that should cause the pop up to close.
- this.clickedMyself = false;
}
closeColorSelector = () => {
- if (this.clickedMyself) {
- this.clickedMyself = false;
- return;
- }
this.setState({ showColorSelector: false });
};
@@ -33,21 +27,6 @@ export class KuiColorPicker extends React.Component {
this.props.onChange(color.hex);
};
- onClickRootElement = () => {
- // This prevents clicking on the element from closing it, due to the event handler on the
- // document object.
- this.clickedMyself = true;
- };
-
- componentDidMount() {
- // When the user clicks somewhere outside of the color picker, we will dismiss it.
- document.addEventListener('click', this.closeColorSelector);
- }
-
- componentWillUnmount() {
- document.removeEventListener('click', this.closeColorSelector);
- }
-
getColorLabel() {
const { color } = this.props;
const colorValue = color === null ? '(transparent)' : color;
@@ -65,30 +44,31 @@ export class KuiColorPicker extends React.Component {
const { color, className, showColorLabel } = this.props;
const classes = classNames('kuiColorPicker', className);
return (
-
+
-
- { showColorLabel ? this.getColorLabel() : null }
+
+
+ { showColorLabel ? this.getColorLabel() : null }
+
+ {
+ this.state.showColorSelector ?
+
+
+
+ : null
+ }
- {
- this.state.showColorSelector ?
-
-
-
- : null
- }
-
+
);
}
}
diff --git a/ui_framework/src/components/index.js b/ui_framework/src/components/index.js
index fe3d7d3eaf83..4e731c522824 100644
--- a/ui_framework/src/components/index.js
+++ b/ui_framework/src/components/index.js
@@ -96,6 +96,10 @@ export {
KuiModalOverlay,
} from './modal';
+export {
+ KuiOutsideClickDetector,
+} from './outside_click_detector';
+
export {
KuiPager,
KuiPagerButtonGroup,
diff --git a/ui_framework/src/components/outside_click_detector/__snapshots__/outside_click_detector.test.js.snap b/ui_framework/src/components/outside_click_detector/__snapshots__/outside_click_detector.test.js.snap
new file mode 100644
index 000000000000..6345a66dd3ab
--- /dev/null
+++ b/ui_framework/src/components/outside_click_detector/__snapshots__/outside_click_detector.test.js.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`KuiOutsideClickDetector is rendered 1`] = ``;
diff --git a/ui_framework/src/components/outside_click_detector/index.js b/ui_framework/src/components/outside_click_detector/index.js
new file mode 100644
index 000000000000..a05f19936044
--- /dev/null
+++ b/ui_framework/src/components/outside_click_detector/index.js
@@ -0,0 +1,3 @@
+export {
+ KuiOutsideClickDetector,
+} from './outside_click_detector';
diff --git a/ui_framework/src/components/outside_click_detector/outside_click_detector.js b/ui_framework/src/components/outside_click_detector/outside_click_detector.js
new file mode 100644
index 000000000000..cf5e15836a42
--- /dev/null
+++ b/ui_framework/src/components/outside_click_detector/outside_click_detector.js
@@ -0,0 +1,48 @@
+import {
+ Children,
+ cloneElement,
+ Component,
+} from 'react';
+import PropTypes from 'prop-types';
+
+export class KuiOutsideClickDetector extends Component {
+ static propTypes = {
+ children: PropTypes.node.isRequired,
+ onOutsideClick: PropTypes.func.isRequired,
+ }
+
+ onClickOutside = event => {
+ if (!this.wrapperRef) {
+ return;
+ }
+
+ if (this.wrapperRef === event.target) {
+ return;
+ }
+
+ if (this.wrapperRef.contains(event.target)) {
+ return;
+ }
+
+ this.props.onOutsideClick();
+ }
+
+ componentDidMount() {
+ document.addEventListener('click', this.onClickOutside);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('click', this.onClickOutside);
+ }
+
+ render() {
+ const props = Object.assign({}, this.props.children.props, {
+ ref: node => {
+ this.wrapperRef = node;
+ },
+ });
+
+ const child = Children.only(this.props.children);
+ return cloneElement(child, props);
+ }
+}
diff --git a/ui_framework/src/components/outside_click_detector/outside_click_detector.test.js b/ui_framework/src/components/outside_click_detector/outside_click_detector.test.js
new file mode 100644
index 000000000000..942abf51010b
--- /dev/null
+++ b/ui_framework/src/components/outside_click_detector/outside_click_detector.test.js
@@ -0,0 +1,17 @@
+import React from 'react';
+import { render } from 'enzyme';
+
+import { KuiOutsideClickDetector } from './outside_click_detector';
+
+describe('KuiOutsideClickDetector', () => {
+ test('is rendered', () => {
+ const component = render(
+ {}}>
+
+
+ );
+
+ expect(component)
+ .toMatchSnapshot();
+ });
+});
diff --git a/ui_framework/src/components/popover/popover.js b/ui_framework/src/components/popover/popover.js
index ce55ab06d937..4c2eec8e5d81 100644
--- a/ui_framework/src/components/popover/popover.js
+++ b/ui_framework/src/components/popover/popover.js
@@ -1,7 +1,9 @@
-import React, { Component } from 'react';
+import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
+import { KuiOutsideClickDetector } from '../outside_click_detector';
+
const anchorPositionToClassNameMap = {
'center': '',
'left': 'kuiPopover--anchorLeft',
@@ -10,80 +12,45 @@ const anchorPositionToClassNameMap = {
export const ANCHOR_POSITIONS = Object.keys(anchorPositionToClassNameMap);
-export class KuiPopover extends Component {
- constructor(props) {
- super(props);
+export const KuiPopover = ({
+ anchorPosition,
+ bodyClassName,
+ button,
+ isOpen,
+ children,
+ className,
+ closePopover,
+ ...rest,
+}) => {
+ const classes = classNames(
+ 'kuiPopover',
+ anchorPositionToClassNameMap[anchorPosition],
+ className,
+ {
+ 'kuiPopover-isOpen': isOpen,
+ },
+ );
- // Use this variable to differentiate between clicks on the element that should not cause the pop up
- // to close, and external clicks that should cause the pop up to close.
- this.didClickMyself = false;
- }
+ const bodyClasses = classNames('kuiPopover__body', bodyClassName);
- closePopover = () => {
- if (this.didClickMyself) {
- this.didClickMyself = false;
- return;
- }
+ const body = (
+
+ { children }
+
+ );
- this.props.closePopover();
- };
-
- onClickRootElement = () => {
- // This prevents clicking on the element from closing it, due to the event handler on the
- // document object.
- this.didClickMyself = true;
- };
-
- componentDidMount() {
- // When the user clicks somewhere outside of the color picker, we will dismiss it.
- document.addEventListener('click', this.closePopover);
- }
-
- componentWillUnmount() {
- document.removeEventListener('click', this.closePopover);
- }
-
- render() {
- const {
- anchorPosition,
- bodyClassName,
- button,
- isOpen,
- children,
- className,
- closePopover, // eslint-disable-line no-unused-vars
- ...rest,
- } = this.props;
-
- const classes = classNames(
- 'kuiPopover',
- anchorPositionToClassNameMap[anchorPosition],
- className,
- {
- 'kuiPopover-isOpen': isOpen,
- },
- );
-
- const bodyClasses = classNames('kuiPopover__body', bodyClassName);
-
- const body = (
-
- { children }
-
- );
-
- return (
+ return (
+
{ button }
{ body }
- );
- }
-}
+
+ );
+};
KuiPopover.propTypes = {
isOpen: PropTypes.bool,