From c2a4de7ed380cbe0fdfd989d88e8481ca89515f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Thu, 15 Feb 2018 17:42:08 +0100 Subject: [PATCH 1/7] Add EuiDelayHide component --- .gitignore | 1 + src/components/delay_hide/delay_hide.js | 56 ++++++++++++ src/components/delay_hide/delay_hide.test.js | 90 ++++++++++++++++++++ src/components/delay_hide/index.js | 1 + 4 files changed, 148 insertions(+) create mode 100644 src/components/delay_hide/delay_hide.js create mode 100644 src/components/delay_hide/delay_hide.test.js create mode 100644 src/components/delay_hide/index.js diff --git a/.gitignore b/.gitignore index 4b7db111a85..33b09b838bd 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ reports/ tmp/ dist/ lib/ +.vscode/ .DS_Store .eslintcache .yo-rc.json diff --git a/src/components/delay_hide/delay_hide.js b/src/components/delay_hide/delay_hide.js new file mode 100644 index 00000000000..7ca079f26e5 --- /dev/null +++ b/src/components/delay_hide/delay_hide.js @@ -0,0 +1,56 @@ +import { Component } from 'react'; +import PropTypes from 'prop-types'; + +export class EuiDelayHide extends Component { + static propTypes = { + hide: PropTypes.bool, + minimumDuration: PropTypes.number, + render: PropTypes.func.isRequired + }; + + constructor(props) { + super(props); + + this.state = { + hide: this.props.hide + }; + + this.lastRenderedTime = this.props.hide ? 0 : Date.now(); + this.shouldRender = false; + } + + componentWillReceiveProps(props) { + const { hide, minimumDuration = 1000 } = props; + clearTimeout(this.timeout); + + const visibleDuration = Date.now() - this.lastRenderedTime; + const timeRemaining = minimumDuration - visibleDuration; + if (hide && timeRemaining > 0) { + this.shouldRender = false; + this.setStateDelayed(timeRemaining); + } else { + this.shouldRender = true; + this.lastRenderedTime = Date.now(); + this.setState({ hide }); + } + } + + setStateDelayed = timeRemaining => { + this.timeout = setTimeout(() => { + this.shouldRender = true; + this.setState({ hide: true }); + }, timeRemaining); + }; + + shouldComponentUpdate() { + return this.shouldRender; + } + + render() { + if (this.state.hide) { + return null; + } + + return this.props.render(); + } +} diff --git a/src/components/delay_hide/delay_hide.test.js b/src/components/delay_hide/delay_hide.test.js new file mode 100644 index 00000000000..069ca73b0de --- /dev/null +++ b/src/components/delay_hide/delay_hide.test.js @@ -0,0 +1,90 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { EuiDelayHide } from './index'; + +describe('when EuiDelayHide is visible initially and is changed to hidden', () => { + let wrapper; + beforeEach(() => { + jest.useFakeTimers(); + wrapper = mount( +
Hello World
} /> + ); + wrapper.setProps({ hide: true }); + }); + + test('it should be visible initially', async () => { + expect(wrapper.html()).toEqual('
Hello World
'); + }); + + test('it should be visible after 500ms', () => { + jest.advanceTimersByTime(500); + expect(wrapper.html()).toEqual('
Hello World
'); + }); + + test('it should be hidden after 1500ms', () => { + jest.advanceTimersByTime(1500); + expect(wrapper.html()).toEqual(null); + }); +}); + +describe('when EuiDelayHide is hidden initially', () => { + let wrapper; + beforeEach(() => { + jest.useFakeTimers(); + wrapper = mount( +
Hello World
} /> + ); + }); + + test('it should be hidden initially', async () => { + expect(wrapper.html()).toEqual(null); + }); + + test('it should become visible immediately after prop change', async () => { + wrapper.setProps({ hide: false }); + expect(wrapper.html()).toEqual('
Hello World
'); + }); + + test('it should become visible immediately after prop change but not become hidden until after 1000ms', async () => { + wrapper.setProps({ hide: false }); + expect(wrapper.html()).toEqual('
Hello World
'); + + wrapper.setProps({ hide: true }); + jest.advanceTimersByTime(500); + + expect(wrapper.html()).toEqual('
Hello World
'); + + jest.advanceTimersByTime(1000); + + expect(wrapper.html()).toEqual(null); + }); +}); + +describe('when EuiDelayHide is visible initially and has a minimumDuration of 2000ms ', () => { + let wrapper; + beforeEach(() => { + jest.useFakeTimers(); + wrapper = mount( +
Hello World
} + /> + ); + wrapper.setProps({ hide: true }); + }); + + test('it should be visible initially', async () => { + expect(wrapper.html()).toEqual('
Hello World
'); + }); + + test('it should be visible after 1500ms', () => { + jest.advanceTimersByTime(1500); + expect(wrapper.html()).toEqual('
Hello World
'); + }); + + test('it should be hidden after 2500ms', () => { + jest.advanceTimersByTime(2500); + expect(wrapper.html()).toEqual(null); + }); +}); diff --git a/src/components/delay_hide/index.js b/src/components/delay_hide/index.js new file mode 100644 index 00000000000..09ab4d7bc5a --- /dev/null +++ b/src/components/delay_hide/index.js @@ -0,0 +1 @@ +export { EuiDelayHide } from './delay_hide'; From 5e15d3559ef33ea7fcffec0004c4880e729d3cb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Thu, 15 Feb 2018 17:55:10 +0100 Subject: [PATCH 2/7] Updated changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e4a5a30c25..20051bf4d3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # [`master`](https://github.com/elastic/eui/tree/master) +- Added `EuiDelayHide` component. [#412](https://github.com/elastic/eui/pull/412) - Decreased overall size of checkbox, radio, and switches as well as better styles for the different states. ([#407](https://github.com/elastic/eui/pull/407)) **Bug fixes** @@ -21,7 +22,7 @@ - Added importAction and exportAction icons ([#394](https://github.com/elastic/eui/pull/394)) - Added `EuiCard` for UI patterns that need an icon/image, title and description with some sort of action. ([#380](https://github.com/elastic/eui/pull/380)) - Add TypeScript definitions for the `` component. ([#403](https://github.com/elastic/eui/pull/403)) -- Added `SearchBar` component - introduces a simple yet rich query language to search for objects + search box and filter controls to construct/manipulate it. ([#379](https://github.com/elastic/eui/pull/379)) +- Added `SearchBar` component - introduces a simple yet rich query language to search for objects + search box and filter controls to construct/manipulate it. ([#379](https://github.com/elastic/eui/pull/379)) **Bug fixes** From b5ca7efec4cd32d1a7641859832263efa640fb6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Thu, 15 Feb 2018 18:53:25 +0100 Subject: [PATCH 3/7] Added docs --- src-docs/src/routes.js | 4 ++ src-docs/src/views/delay_hide/delay_hide.js | 52 +++++++++++++++++++ .../views/delay_hide/delay_hide_example.js | 38 ++++++++++++++ src/components/index.js | 4 ++ 4 files changed, 98 insertions(+) create mode 100644 src-docs/src/views/delay_hide/delay_hide.js create mode 100644 src-docs/src/views/delay_hide/delay_hide_example.js diff --git a/src-docs/src/routes.js b/src-docs/src/routes.js index 2f5814d0e49..b4378746a96 100644 --- a/src-docs/src/routes.js +++ b/src-docs/src/routes.js @@ -64,6 +64,9 @@ import { ColorPickerExample } import { ContextMenuExample } from './views/context_menu/context_menu_example'; +import { DelayHideExample } + from './views/delay_hide/delay_hide_example'; + import { DescriptionListExample } from './views/description_list/description_list_example'; @@ -226,6 +229,7 @@ const components = [ CodeExample, ColorPickerExample, ContextMenuExample, + DelayHideExample, DescriptionListExample, ErrorBoundaryExample, ExpressionExample, diff --git a/src-docs/src/views/delay_hide/delay_hide.js b/src-docs/src/views/delay_hide/delay_hide.js new file mode 100644 index 00000000000..5da006f32b3 --- /dev/null +++ b/src-docs/src/views/delay_hide/delay_hide.js @@ -0,0 +1,52 @@ +import React, { Component } from 'react'; +import { EuiDelayHide } from '../../../../src/components/delay_hide'; +import { EuiFlexItem } from '../../../../src/components/flex/flex_item'; +import { EuiCheckbox } from '../../../../src/components/form/checkbox/checkbox'; +import { EuiFormRow } from '../../../../src/components/form/form_row/form_row'; +import { EuiFieldNumber } from '../../../../src/components/form/field_number'; + +export default class extends Component { + state = { + minimumDuration: 1000, + hide: false + }; + + onChangeMinimumDuration = event => { + this.setState({ minimumDuration: parseInt(event.target.value, 10) }); + }; + + onChangeHide = event => { + this.setState({ hide: event.target.checked }); + }; + + render() { + return ( +
+ + + + + + + + + +
Hello world
} + /> +
+
+
+ ); + } +} diff --git a/src-docs/src/views/delay_hide/delay_hide_example.js b/src-docs/src/views/delay_hide/delay_hide_example.js new file mode 100644 index 00000000000..071574de763 --- /dev/null +++ b/src-docs/src/views/delay_hide/delay_hide_example.js @@ -0,0 +1,38 @@ +import React from 'react'; +import DelayHide from './delay_hide'; +import { GuideSectionTypes } from '../../components'; +import { EuiCode, EuiDelayHide } from '../../../../src/components'; +import { renderToHtml } from '../../services'; + +const delayHideSource = require('!!raw-loader!./delay_hide'); +const delayHideHtml = renderToHtml(DelayHide); + +export const DelayHideExample = { + title: 'DelayHide', + sections: [ + { + title: 'DelayHide', + source: [ + { + type: GuideSectionTypes.JS, + code: delayHideSource + }, + { + type: GuideSectionTypes.HTML, + code: delayHideHtml + } + ], + text: ( +

+ DelayHide is a component for conditionally toggling + the visibility of a child component. It will ensure that the child is + visible for at least 1000ms (default). This avoids UI glitches that + are common with loading spinners and other elements that are rendered + conditionally and potentially for a short amount of time. +

+ ), + props: { EuiDelayHide }, + demo: + } + ] +}; diff --git a/src/components/index.js b/src/components/index.js index 9d42605900e..243e2a5bf0d 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -53,6 +53,10 @@ export { EuiContextMenuItem, } from './context_menu'; +export { + EuiDelayHide +} from './delay_hide'; + export { EuiDescriptionList, EuiDescriptionListTitle, From e6511d50c0be5124a731a41ba402b8270cf7d582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Thu, 15 Feb 2018 19:00:09 +0100 Subject: [PATCH 4/7] Minor tweak --- src/components/delay_hide/delay_hide.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/delay_hide/delay_hide.js b/src/components/delay_hide/delay_hide.js index 7ca079f26e5..cf68323e06e 100644 --- a/src/components/delay_hide/delay_hide.js +++ b/src/components/delay_hide/delay_hide.js @@ -19,8 +19,7 @@ export class EuiDelayHide extends Component { this.shouldRender = false; } - componentWillReceiveProps(props) { - const { hide, minimumDuration = 1000 } = props; + componentWillReceiveProps({ hide, minimumDuration = 1000 }) { clearTimeout(this.timeout); const visibleDuration = Date.now() - this.lastRenderedTime; From 1261c8b93f3fd5fcd3011cb5b91de57c55c26138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Thu, 15 Feb 2018 21:55:14 +0100 Subject: [PATCH 5/7] Fix issue with component calling setState after unmounting --- src/components/delay_hide/delay_hide.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/delay_hide/delay_hide.js b/src/components/delay_hide/delay_hide.js index cf68323e06e..1f2b5b978cf 100644 --- a/src/components/delay_hide/delay_hide.js +++ b/src/components/delay_hide/delay_hide.js @@ -45,6 +45,10 @@ export class EuiDelayHide extends Component { return this.shouldRender; } + componentWillUnmount() { + clearTimeout(this.timeout); + } + render() { if (this.state.hide) { return null; From 65d536d7c624cf6e0fa6e0bf49602bc249e678df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Fri, 16 Feb 2018 02:37:32 +0100 Subject: [PATCH 6/7] Address feedback --- src-docs/src/views/delay_hide/delay_hide.js | 21 ++++---- .../views/delay_hide/delay_hide_example.js | 2 +- src/components/delay_hide/delay_hide.js | 32 ++++++----- src/components/delay_hide/delay_hide.test.js | 53 +++++++++++++------ 4 files changed, 67 insertions(+), 41 deletions(-) diff --git a/src-docs/src/views/delay_hide/delay_hide.js b/src-docs/src/views/delay_hide/delay_hide.js index 5da006f32b3..ecb3b554654 100644 --- a/src-docs/src/views/delay_hide/delay_hide.js +++ b/src-docs/src/views/delay_hide/delay_hide.js @@ -1,9 +1,12 @@ -import React, { Component } from 'react'; -import { EuiDelayHide } from '../../../../src/components/delay_hide'; -import { EuiFlexItem } from '../../../../src/components/flex/flex_item'; -import { EuiCheckbox } from '../../../../src/components/form/checkbox/checkbox'; -import { EuiFormRow } from '../../../../src/components/form/form_row/form_row'; -import { EuiFieldNumber } from '../../../../src/components/form/field_number'; +import React, { Component, Fragment } from 'react'; +import { + EuiDelayHide, + EuiFlexItem, + EuiCheckbox, + EuiFormRow, + EuiFieldNumber, + EuiLoadingSpinner +} from '../../../../src/components'; export default class extends Component { state = { @@ -21,7 +24,7 @@ export default class extends Component { render() { return ( -
+
Hello world
} + render={() => } />
-
+ ); } } diff --git a/src-docs/src/views/delay_hide/delay_hide_example.js b/src-docs/src/views/delay_hide/delay_hide_example.js index 071574de763..32ffe87c8e4 100644 --- a/src-docs/src/views/delay_hide/delay_hide_example.js +++ b/src-docs/src/views/delay_hide/delay_hide_example.js @@ -24,7 +24,7 @@ export const DelayHideExample = { ], text: (

- DelayHide is a component for conditionally toggling + EuiDelayHide is a component for conditionally toggling the visibility of a child component. It will ensure that the child is visible for at least 1000ms (default). This avoids UI glitches that are common with loading spinners and other elements that are rendered diff --git a/src/components/delay_hide/delay_hide.js b/src/components/delay_hide/delay_hide.js index 1f2b5b978cf..52aa8cce1a3 100644 --- a/src/components/delay_hide/delay_hide.js +++ b/src/components/delay_hide/delay_hide.js @@ -8,6 +8,11 @@ export class EuiDelayHide extends Component { render: PropTypes.func.isRequired }; + static defaultProps = { + hide: false, + minimumDuration: 1000 + }; + constructor(props) { super(props); @@ -16,35 +21,34 @@ export class EuiDelayHide extends Component { }; this.lastRenderedTime = this.props.hide ? 0 : Date.now(); - this.shouldRender = false; } - componentWillReceiveProps({ hide, minimumDuration = 1000 }) { + getTimeRemaining(minimumDuration) { + const visibleDuration = Date.now() - this.lastRenderedTime; + return minimumDuration - visibleDuration; + } + + componentWillReceiveProps(nextProps) { clearTimeout(this.timeout); + const timeRemaining = this.getTimeRemaining(nextProps.minimumDuration); - const visibleDuration = Date.now() - this.lastRenderedTime; - const timeRemaining = minimumDuration - visibleDuration; - if (hide && timeRemaining > 0) { - this.shouldRender = false; + if (nextProps.hide && timeRemaining > 0) { this.setStateDelayed(timeRemaining); } else { - this.shouldRender = true; - this.lastRenderedTime = Date.now(); - this.setState({ hide }); + if (this.state.hide && !nextProps.hide) { + this.lastRenderedTime = Date.now(); + } + + this.setState({ hide: nextProps.hide }); } } setStateDelayed = timeRemaining => { this.timeout = setTimeout(() => { - this.shouldRender = true; this.setState({ hide: true }); }, timeRemaining); }; - shouldComponentUpdate() { - return this.shouldRender; - } - componentWillUnmount() { clearTimeout(this.timeout); } diff --git a/src/components/delay_hide/delay_hide.test.js b/src/components/delay_hide/delay_hide.test.js index 069ca73b0de..778b6ef1ee2 100644 --- a/src/components/delay_hide/delay_hide.test.js +++ b/src/components/delay_hide/delay_hide.test.js @@ -2,27 +2,49 @@ import React from 'react'; import { mount } from 'enzyme'; import { EuiDelayHide } from './index'; -describe('when EuiDelayHide is visible initially and is changed to hidden', () => { +describe('when EuiDelayHide is visible initially', () => { let wrapper; beforeEach(() => { jest.useFakeTimers(); wrapper = mount( -

Hello World
} /> +
Hello World
} + /> ); - wrapper.setProps({ hide: true }); }); test('it should be visible initially', async () => { + wrapper.setProps({ hide: true }); expect(wrapper.html()).toEqual('
Hello World
'); }); - test('it should be visible after 500ms', () => { - jest.advanceTimersByTime(500); + test('it should be visible after 900ms', () => { + wrapper.setProps({ hide: true }); + jest.advanceTimersByTime(900); + expect(wrapper.html()).toEqual('
Hello World
'); + }); + + test('it should be hidden after 1100ms', () => { + wrapper.setProps({ hide: true }); + jest.advanceTimersByTime(1100); + expect(wrapper.html()).toEqual(null); + }); + + test('it should be visible after 1100ms regardless of prop changes in-between', () => { + wrapper.setProps({ hide: true }); + wrapper.setProps({ hide: false }); + jest.advanceTimersByTime(1100); expect(wrapper.html()).toEqual('
Hello World
'); }); - test('it should be hidden after 1500ms', () => { - jest.advanceTimersByTime(1500); + test('it should hide immediately after prop change, if it has been displayed for 1100ms', () => { + const currentTime = Date.now(); + jest.advanceTimersByTime(1100); + jest.spyOn(Date, 'now').mockReturnValue(currentTime + 1100); + expect(wrapper.html()).toEqual('
Hello World
'); + + wrapper.setProps({ hide: true }); expect(wrapper.html()).toEqual(null); }); }); @@ -45,17 +67,14 @@ describe('when EuiDelayHide is hidden initially', () => { expect(wrapper.html()).toEqual('
Hello World
'); }); - test('it should become visible immediately after prop change but not become hidden until after 1000ms', async () => { + test('it should be visible for at least 1100ms before hiding', async () => { wrapper.setProps({ hide: false }); - expect(wrapper.html()).toEqual('
Hello World
'); - wrapper.setProps({ hide: true }); - jest.advanceTimersByTime(500); + jest.advanceTimersByTime(900); expect(wrapper.html()).toEqual('
Hello World
'); - jest.advanceTimersByTime(1000); - + jest.advanceTimersByTime(200); expect(wrapper.html()).toEqual(null); }); }); @@ -78,13 +97,13 @@ describe('when EuiDelayHide is visible initially and has a minimumDuration of 20 expect(wrapper.html()).toEqual('
Hello World
'); }); - test('it should be visible after 1500ms', () => { - jest.advanceTimersByTime(1500); + test('it should be visible after 1900ms', () => { + jest.advanceTimersByTime(1900); expect(wrapper.html()).toEqual('
Hello World
'); }); - test('it should be hidden after 2500ms', () => { - jest.advanceTimersByTime(2500); + test('it should be hidden after 2100ms', () => { + jest.advanceTimersByTime(2100); expect(wrapper.html()).toEqual(null); }); }); From 67b2a23c68de1d71e6e6823fa2dafa790195aa89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Fri, 16 Feb 2018 02:44:18 +0100 Subject: [PATCH 7/7] Increased example duration to 3000 --- src-docs/src/views/delay_hide/delay_hide.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-docs/src/views/delay_hide/delay_hide.js b/src-docs/src/views/delay_hide/delay_hide.js index ecb3b554654..dc42d1e7e99 100644 --- a/src-docs/src/views/delay_hide/delay_hide.js +++ b/src-docs/src/views/delay_hide/delay_hide.js @@ -10,7 +10,7 @@ import { export default class extends Component { state = { - minimumDuration: 1000, + minimumDuration: 3000, hide: false };