Skip to content

Commit

Permalink
[UI Framework] Add KuiOutsideClickDetector (elastic#13521)
Browse files Browse the repository at this point in the history
* Add KuiOutsideClickDetector.
* Convert KuiColorPicker and KuiPopover to use KuiOutsideClickDetector.
  • Loading branch information
cjcenizal authored and chrisronline committed Dec 1, 2017
1 parent f0a3811 commit 0c7cec4
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 112 deletions.
68 changes: 24 additions & 44 deletions ui_framework/src/components/color_picker/color_picker.js
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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 });
};

Expand All @@ -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;
Expand All @@ -65,30 +44,31 @@ export class KuiColorPicker extends React.Component {
const { color, className, showColorLabel } = this.props;
const classes = classNames('kuiColorPicker', className);
return (
<div
className={classes}
data-test-subj={this.props['data-test-subj']}
onClick={this.onClickRootElement}
>
<KuiOutsideClickDetector onOutsideClick={this.closeColorSelector}>
<div
className="kuiColorPicker__preview"
onClick={this.toggleColorSelector}
className={classes}
data-test-subj={this.props['data-test-subj']}
>
<KuiColorPickerSwatch color={color} aria-label={this.props['aria-label']} />
{ showColorLabel ? this.getColorLabel() : null }
<div
className="kuiColorPicker__preview"
onClick={this.toggleColorSelector}
>
<KuiColorPickerSwatch color={color} aria-label={this.props['aria-label']} />
{ showColorLabel ? this.getColorLabel() : null }
</div>
{
this.state.showColorSelector ?
<div className="kuiColorPickerPopUp" data-test-subj="colorPickerPopup">
<ChromePicker
color={color ? color : '#ffffff'}
disableAlpha={true}
onChange={this.handleColorSelection}
/>
</div>
: null
}
</div>
{
this.state.showColorSelector ?
<div className="kuiColorPickerPopUp" data-test-subj="colorPickerPopup">
<ChromePicker
color={color ? color : '#ffffff'}
disableAlpha={true}
onChange={this.handleColorSelection}
/>
</div>
: null
}
</div>
</KuiOutsideClickDetector>
);
}
}
Expand Down
4 changes: 4 additions & 0 deletions ui_framework/src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ export {
KuiModalOverlay,
} from './modal';

export {
KuiOutsideClickDetector,
} from './outside_click_detector';

export {
KuiPager,
KuiPagerButtonGroup,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`KuiOutsideClickDetector is rendered 1`] = `<div />`;
3 changes: 3 additions & 0 deletions ui_framework/src/components/outside_click_detector/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export {
KuiOutsideClickDetector,
} from './outside_click_detector';
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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(
<KuiOutsideClickDetector onOutsideClick={() => {}}>
<div />
</KuiOutsideClickDetector>
);

expect(component)
.toMatchSnapshot();
});
});
103 changes: 35 additions & 68 deletions ui_framework/src/components/popover/popover.js
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -10,80 +12,45 @@ const anchorPositionToClassNameMap = {

export const ANCHOR_POSITIONS = Object.keys(anchorPositionToClassNameMap);

export class KuiPopover extends Component {
constructor(props) {
super(props);

// 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;
}

closePopover = () => {
if (this.didClickMyself) {
this.didClickMyself = false;
return;
}

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 = (
<div className={bodyClasses}>
{ children }
</div>
);

return (
export const KuiPopover = ({
anchorPosition,
bodyClassName,
button,
isOpen,
children,
className,
closePopover,
...rest,
}) => {
const classes = classNames(
'kuiPopover',
anchorPositionToClassNameMap[anchorPosition],
className,
{
'kuiPopover-isOpen': isOpen,
},
);

const bodyClasses = classNames('kuiPopover__body', bodyClassName);

const body = (
<div className={bodyClasses}>
{ children }
</div>
);

return (
<KuiOutsideClickDetector onOutsideClick={closePopover}>
<div
onClick={this.onClickRootElement}
className={classes}
{...rest}
>
{ button }
{ body }
</div>
);
}
}
</KuiOutsideClickDetector>
);
};

KuiPopover.propTypes = {
isOpen: PropTypes.bool,
Expand Down

0 comments on commit 0c7cec4

Please sign in to comment.