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/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** 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..dc42d1e7e99 --- /dev/null +++ b/src-docs/src/views/delay_hide/delay_hide.js @@ -0,0 +1,55 @@ +import React, { Component, Fragment } from 'react'; +import { + EuiDelayHide, + EuiFlexItem, + EuiCheckbox, + EuiFormRow, + EuiFieldNumber, + EuiLoadingSpinner +} from '../../../../src/components'; + +export default class extends Component { + state = { + minimumDuration: 3000, + hide: false + }; + + onChangeMinimumDuration = event => { + this.setState({ minimumDuration: parseInt(event.target.value, 10) }); + }; + + onChangeHide = event => { + this.setState({ hide: event.target.checked }); + }; + + render() { + return ( + + + + + + + + + + + } + /> + + + + ); + } +} 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..32ffe87c8e4 --- /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: ( +

+ 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 + conditionally and potentially for a short amount of time. +

+ ), + props: { EuiDelayHide }, + demo: + } + ] +}; diff --git a/src/components/delay_hide/delay_hide.js b/src/components/delay_hide/delay_hide.js new file mode 100644 index 00000000000..52aa8cce1a3 --- /dev/null +++ b/src/components/delay_hide/delay_hide.js @@ -0,0 +1,63 @@ +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 + }; + + static defaultProps = { + hide: false, + minimumDuration: 1000 + }; + + constructor(props) { + super(props); + + this.state = { + hide: this.props.hide + }; + + this.lastRenderedTime = this.props.hide ? 0 : Date.now(); + } + + getTimeRemaining(minimumDuration) { + const visibleDuration = Date.now() - this.lastRenderedTime; + return minimumDuration - visibleDuration; + } + + componentWillReceiveProps(nextProps) { + clearTimeout(this.timeout); + const timeRemaining = this.getTimeRemaining(nextProps.minimumDuration); + + if (nextProps.hide && timeRemaining > 0) { + this.setStateDelayed(timeRemaining); + } else { + if (this.state.hide && !nextProps.hide) { + this.lastRenderedTime = Date.now(); + } + + this.setState({ hide: nextProps.hide }); + } + } + + setStateDelayed = timeRemaining => { + this.timeout = setTimeout(() => { + this.setState({ hide: true }); + }, timeRemaining); + }; + + componentWillUnmount() { + clearTimeout(this.timeout); + } + + 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..778b6ef1ee2 --- /dev/null +++ b/src/components/delay_hide/delay_hide.test.js @@ -0,0 +1,109 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { EuiDelayHide } from './index'; + +describe('when EuiDelayHide is visible initially', () => { + let wrapper; + beforeEach(() => { + jest.useFakeTimers(); + wrapper = mount( +
Hello World
} + /> + ); + }); + + test('it should be visible initially', async () => { + wrapper.setProps({ hide: true }); + expect(wrapper.html()).toEqual('
Hello World
'); + }); + + 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 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); + }); +}); + +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 be visible for at least 1100ms before hiding', async () => { + wrapper.setProps({ hide: false }); + wrapper.setProps({ hide: true }); + jest.advanceTimersByTime(900); + + expect(wrapper.html()).toEqual('
Hello World
'); + + jest.advanceTimersByTime(200); + 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 1900ms', () => { + jest.advanceTimersByTime(1900); + expect(wrapper.html()).toEqual('
Hello World
'); + }); + + test('it should be hidden after 2100ms', () => { + jest.advanceTimersByTime(2100); + 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'; 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,