Skip to content

Commit

Permalink
Uses new WindowEvent component for Flyout "close on ESC" (#1127)
Browse files Browse the repository at this point in the history
* Adds new EuiWindowEvent component

* Switches flyout close on ESC to window event

* Removes old event listener on Flyout

* Adds EuiWindowEvent example to docs

* Updates changelog for EuiWindowEvent and Flyout changes

* Adds more clarification to WindowEvent example

* Moves WindowEvent to services folder

* Updates changelog to group changes correctly

* Adds more about how to use the EuiWindowEvent in example docs

* Rewrites examples for WindowEvent docs

* Adds EuiWindowEvent props to docs
  • Loading branch information
jasonrhodes authored Aug 22, 2018
1 parent 91dbf4e commit 658d538
Show file tree
Hide file tree
Showing 12 changed files with 418 additions and 3 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

- Added `zIndexAdjustment` to `EuiPopover` which allows tweaking the popover content's `z-index` ([#1097](https://github.com/elastic/eui/pull/1097))
- Added new `EuiSuperSelect` component and `hasArrow` prop to `EuiPopover` ([#921](https://github.com/elastic/eui/pull/921))
- Added a new `EuiWindowEvent` component for declarative, safe management of `window` event listeners ([#1127](https://github.com/elastic/eui/pull/1127))
- Changed `Flyout` component to close on ESC keypress even if the flyout does not have focus, using new Window Event component ([#1127](https://github.com/elastic/eui/pull/1127))

**Bug fixes**

Expand Down
4 changes: 4 additions & 0 deletions src-docs/src/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,9 @@ import { ToolTipExample }
import { ToggleExample }
from './views/toggle/toggle_example';

import { WindowEventExample }
from './views/window_event/window_event_example';

import { XYChartExample }
from './views/series_chart/series_chart_example';

Expand Down Expand Up @@ -395,6 +398,7 @@ const navigation = [{
ToggleExample,
UtilityClassesExample,
MutationObserverExample,
WindowEventExample,
].map(example => createExample(example)),
}, {
name: 'Package',
Expand Down
31 changes: 31 additions & 0 deletions src-docs/src/views/window_event/basic_window_event.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';

import {
EuiModal,
EuiModalBody,
EuiModalHeader,
EuiModalHeaderTitle,
EuiOverlayMask
} from '../../../../src/components';

import { ModalExample } from './modal_example_container';

const BasicModal = ({ onClose }) => (
<EuiOverlayMask>
<EuiModal
onClose={onClose}
style={{ width: '800px' }}
>
<EuiModalHeader>
<EuiModalHeaderTitle >
Example modal
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<p>This modal closes when you press ESC, using a window event listener.</p>
</EuiModalBody>
</EuiModal>
</EuiOverlayMask>
);

export const BasicWindowEvent = () => <ModalExample modal={BasicModal} />;
50 changes: 50 additions & 0 deletions src-docs/src/views/window_event/modal_example_container.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React, { Component } from 'react';
import {
EuiButton,
} from '../../../../src/components';

import {
EuiWindowEvent,
} from '../../../../src/services';

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

this.state = {
open: false
};

this.open = this.open.bind(this);
this.close = this.close.bind(this);
this.closeOnEscape = this.closeOnEscape.bind(this);
}

open() {
this.setState({ open: true });
}

close() {
if (this.state.open) {
this.setState({ open: false });
}
}

closeOnEscape({ key }) {
if (key === 'Escape') {
this.close();
}
}

render() {
const { modal: Modal, buttonText = 'Open Modal' } = this.props;
const button = <EuiButton onClick={this.open}>{buttonText}</EuiButton>;

return (
<div>
<EuiWindowEvent event="keydown" handler={this.closeOnEscape} />
{this.state.open ? <Modal onClose={this.close} /> : button}
</div>
);
}
}
43 changes: 43 additions & 0 deletions src-docs/src/views/window_event/mouse_position.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React, { Component } from 'react';

import {
EuiSwitch,
EuiDescriptionList,
EuiSpacer
} from '../../../../src/components';

import {
EuiWindowEvent,
} from '../../../../src/services';

export class MousePosition extends Component {

state = {
tracking: false,
coordinates: {}
};

onSwitchChange = () => this.setState((state) => ({ tracking: !state.tracking }));

onMouseMove = ({ clientX, clientY }) => this.setState({ coordinates: { clientX, clientY } });

render() {
const listItems = [
{ title: 'Position X', description: this.state.coordinates.clientX || '??' },
{ title: 'Position Y', description: this.state.coordinates.clientY || '??' }
];
return (
<div>
<EuiSwitch
label="Track mouse position"
checked={this.state.tracking}
onChange={this.onSwitchChange}
/>
{this.state.tracking ? <EuiWindowEvent event="mousemove" handler={this.onMouseMove} /> : null}
<EuiSpacer size="l" />
<EuiDescriptionList listItems={listItems} />
<EuiSpacer size="xxl" />
</div>
);
}
}
65 changes: 65 additions & 0 deletions src-docs/src/views/window_event/window_event_conflict.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React from 'react';

import {
EuiModal,
EuiModalBody,
EuiModalHeader,
EuiModalHeaderTitle,
EuiOverlayMask,
EuiFieldText,
EuiSpacer
} from '../../../../src/components';

import { ModalExample } from './modal_example_container';

class ConflictModal extends React.Component {

constructor(props) {
super(props);

this.state = {
inputValue: ''
};
}

updateInputValue = e => this.setState({ inputValue: e.target.value });

clearInputValueOnEscape = e => {
if (e.key === 'Escape') {
this.setState({ inputValue: '' });
e.stopPropagation();
}
}

render() {
return (
<EuiOverlayMask>
<EuiModal
onClose={this.props.onClose}
style={{ width: '800px' }}
>
<EuiModalHeader>
<EuiModalHeaderTitle >
Example modal
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiFieldText
value={this.state.inputValue}
onChange={this.updateInputValue}
onKeyDown={this.clearInputValueOnEscape}
/>
<EuiSpacer size="s" />
<p>While typing in this field, ESC will clear the field.</p>
<EuiSpacer size="l" />
<p>Otherwise, the event bubbles up to the window and ESC closes the modal.</p>
</EuiModalBody>
</EuiModal>
</EuiOverlayMask>
);
}
}

export const WindowEventConflict = () => (
<ModalExample modal={ConflictModal} buttonText="Open Modal with Conflicting Listener" />
);
117 changes: 117 additions & 0 deletions src-docs/src/views/window_event/window_event_example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import React from 'react';

import { renderToHtml } from '../../services';

import {
GuideSectionTypes,
} from '../../components';

import {
EuiWindowEvent
} from '../../../../src/services';

import {
EuiCode,
EuiCallOut,
EuiSpacer
} from '../../../../src/components';

import { BasicWindowEvent } from './basic_window_event';
const basicSource = require('!!raw-loader!./basic_window_event');
const basicHtml = renderToHtml(BasicWindowEvent);

import { WindowEventConflict } from './window_event_conflict';
const conflictSource = require('!!raw-loader!./window_event_conflict');
const conflictHtml = renderToHtml(WindowEventConflict);

import { MousePosition } from './mouse_position';
const mousePositionSource = require('!!raw-loader!./mouse_position');
const mousePositionHtml = renderToHtml(MousePosition);

export const WindowEventExample = {
title: 'Window Events',
sections: [
{
title: 'Basic example: closing a modal on escape',
source: [{
type: GuideSectionTypes.JS,
code: basicSource,
}, {
type: GuideSectionTypes.HTML,
code: basicHtml,
}],
text: (
<div>
<p>
Use an <EuiCode>EuiWindowEvent</EuiCode> to safely and declaratively manage adding and auto-removing event listeners
to the <EuiCode>window</EuiCode>. This is preferable to setting up your own window event listeners because it will remove
old listeners when your component unmounts, preventing you from accidentally leaving them around forever.
</p>
<p>
This modal example registers a listener on the <EuiCode>keydown</EuiCode> event and listens for ESC key presses,
which closes the open modal.
</p>
</div>
),
components: { EuiWindowEvent },
props: { EuiWindowEvent },
demo: <BasicWindowEvent />,
},
{
title: 'Avoiding event conflicts',
source: [{
type: GuideSectionTypes.JS,
code: conflictSource,
}, {
type: GuideSectionTypes.HTML,
code: conflictHtml,
}],
text: (
<div>
<EuiCallOut
title="Be careful with global listeners"
color="warning"
iconType="alert"
>
<p>
Since window event listeners are global, they can conflict with other event listeners if you aren&apos;t careful.
</p>
</EuiCallOut>
<EuiSpacer />
<p>
The safest and best way to avoid these conflicts is to use <EuiCode>event.stopPropagation()</EuiCode> at the
lowest, most specific level where you are responding to a DOM event. This will prevent the event from bubbling
up to the window, and the <EuiCode>WindowEvent</EuiCode> listener will never be triggered, avoiding the conflict.
</p>
</div>
),
components: { EuiWindowEvent },
demo: <WindowEventConflict />,
},
{
title: 'Tracking mouse position',
source: [{
type: GuideSectionTypes.JS,
code: mousePositionSource,
}, {
type: GuideSectionTypes.HTML,
code: mousePositionHtml,
}],
text: (
<div>
<p>
For some DOM events, you have to listen on the window. One example of this is tracking <em>mouse position</em>. Below,
when you click the toggle switch, your mouse position is tracked. When you toggle off, tracking stops.
</p>
<p>
If you were manually attaching window listeners, you might forget to remove the listener and be silently
responding to mouse events in the background for the life of your app. The <EuiCode>WindowEvent</EuiCode> component
manages that unmount/unregister process for you.
</p>
</div>
),
components: { EuiWindowEvent },
demo: <MousePosition />,
}
],
};
5 changes: 2 additions & 3 deletions src/components/flyout/flyout.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import classnames from 'classnames';
import PropTypes from 'prop-types';
import FocusTrap from 'focus-trap-react';

import { keyCodes } from '../../services';
import { keyCodes, EuiWindowEvent } from '../../services';

import { EuiOverlayMask } from '../overlay_mask';
import { EuiButtonIcon } from '../button';
Expand All @@ -20,7 +20,6 @@ export class EuiFlyout extends Component {
onKeyDown = event => {
if (event.keyCode === keyCodes.ESCAPE) {
event.preventDefault();
event.stopPropagation();
this.props.onClose();
}
};
Expand Down Expand Up @@ -72,7 +71,6 @@ export class EuiFlyout extends Component {
}}
className={classes}
tabIndex={0}
onKeyDown={this.onKeyDown}
style={newStyle || style}
{...rest}
>
Expand All @@ -90,6 +88,7 @@ export class EuiFlyout extends Component {

return (
<span>
<EuiWindowEvent event="keydown" handler={this.onKeyDown} />
{optionalOverlay}
{/* Trap focus even when ownFocus={false}, otherwise closing the flyout won't return focus
to the originating button */}
Expand Down
4 changes: 4 additions & 0 deletions src/services/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,7 @@ export {
calculatePopoverPosition,
findPopoverPosition,
} from './popover';

export {
EuiWindowEvent
} from './window_event';
1 change: 1 addition & 0 deletions src/services/window_event/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as EuiWindowEvent } from './window_event';
Loading

0 comments on commit 658d538

Please sign in to comment.