Skip to content

Commit

Permalink
Add EuiDelayHide component (#412)
Browse files Browse the repository at this point in the history
  • Loading branch information
sorenlouv authored Feb 16, 2018
1 parent 3b84dd9 commit f19218f
Show file tree
Hide file tree
Showing 9 changed files with 277 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ reports/
tmp/
dist/
lib/
.vscode/
.DS_Store
.eslintcache
.yo-rc.json
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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**
Expand All @@ -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 `<EuiHealth>` 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**

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 @@ -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';

Expand Down Expand Up @@ -226,6 +229,7 @@ const components = [
CodeExample,
ColorPickerExample,
ContextMenuExample,
DelayHideExample,
DescriptionListExample,
ErrorBoundaryExample,
ExpressionExample,
Expand Down
55 changes: 55 additions & 0 deletions src-docs/src/views/delay_hide/delay_hide.js
Original file line number Diff line number Diff line change
@@ -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 (
<Fragment>
<EuiFlexItem>
<EuiFormRow>
<EuiCheckbox
id="dummy-id"
checked={this.state.hide}
onChange={this.onChangeHide}
label="Hide child"
/>
</EuiFormRow>
<EuiFormRow label="Minimum duration">
<EuiFieldNumber
value={this.state.minimumDuration}
onChange={this.onChangeMinimumDuration}
/>
</EuiFormRow>

<EuiFormRow label="Child to render">
<EuiDelayHide
hide={this.state.hide}
minimumDuration={this.state.minimumDuration}
render={() => <EuiLoadingSpinner size="m"/>}
/>
</EuiFormRow>
</EuiFlexItem>
</Fragment>
);
}
}
38 changes: 38 additions & 0 deletions src-docs/src/views/delay_hide/delay_hide_example.js
Original file line number Diff line number Diff line change
@@ -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: (
<p>
<EuiCode>EuiDelayHide</EuiCode> 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.
</p>
),
props: { EuiDelayHide },
demo: <DelayHide />
}
]
};
63 changes: 63 additions & 0 deletions src/components/delay_hide/delay_hide.js
Original file line number Diff line number Diff line change
@@ -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();
}
}
109 changes: 109 additions & 0 deletions src/components/delay_hide/delay_hide.test.js
Original file line number Diff line number Diff line change
@@ -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(
<EuiDelayHide
hide={false}
render={() => <div>Hello World</div>}
/>
);
});

test('it should be visible initially', async () => {
wrapper.setProps({ hide: true });
expect(wrapper.html()).toEqual('<div>Hello World</div>');
});

test('it should be visible after 900ms', () => {
wrapper.setProps({ hide: true });
jest.advanceTimersByTime(900);
expect(wrapper.html()).toEqual('<div>Hello World</div>');
});

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('<div>Hello World</div>');
});

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('<div>Hello World</div>');

wrapper.setProps({ hide: true });
expect(wrapper.html()).toEqual(null);
});
});

describe('when EuiDelayHide is hidden initially', () => {
let wrapper;
beforeEach(() => {
jest.useFakeTimers();
wrapper = mount(
<EuiDelayHide hide={true} render={() => <div>Hello World</div>} />
);
});

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('<div>Hello World</div>');
});

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('<div>Hello World</div>');

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(
<EuiDelayHide
hide={false}
minimumDuration={2000}
render={() => <div>Hello World</div>}
/>
);
wrapper.setProps({ hide: true });
});

test('it should be visible initially', async () => {
expect(wrapper.html()).toEqual('<div>Hello World</div>');
});

test('it should be visible after 1900ms', () => {
jest.advanceTimersByTime(1900);
expect(wrapper.html()).toEqual('<div>Hello World</div>');
});

test('it should be hidden after 2100ms', () => {
jest.advanceTimersByTime(2100);
expect(wrapper.html()).toEqual(null);
});
});
1 change: 1 addition & 0 deletions src/components/delay_hide/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { EuiDelayHide } from './delay_hide';
4 changes: 4 additions & 0 deletions src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ export {
EuiContextMenuItem,
} from './context_menu';

export {
EuiDelayHide
} from './delay_hide';

export {
EuiDescriptionList,
EuiDescriptionListTitle,
Expand Down

0 comments on commit f19218f

Please sign in to comment.