-
Notifications
You must be signed in to change notification settings - Fork 843
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add EuiDelayHide component #412
Conversation
@cjcenizal I was not sure where this component belonged, so added it to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👏 This is awesome man! I had a few minor suggestions about the docs and some questions about the desired behavior.
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'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we import all of these from the main components
module? I have a regex which will replace that with @elastic/eui
in the code example.
import {
EuiDelayHide,
EuiFlexItem,
EuiCheckbox,
EuiFormRow,
EuiFieldNumber,
} from '../../../../src/components';
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure I'll do that. But it seems to give some problems to vscode (and probably static analysis tools in general). Eg. I can "go to definition" when I use
import { EuiCheckbox } from '../../../../src/components/form/checkbox/checkbox';
, but not with
import { EuiDelayHide} from '../../../../src/components';
.
This makes it a bit harder to navigate the codebase. Also: automatic imports will import using the first.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I hear you. Though I'm not sure we should optimize the code for any particular editor... I use SublimeText myself so I don't get the benefits vscode is giving you. In the absence of a standard editor, I think it's better to optimize for no editor, i.e. make the code as readable as possible when viewed as regular text.
|
||
render() { | ||
return ( | ||
<div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we import React, { Fragment } from 'react';
at the top of the file and replace this div with a <Fragment>
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
], | ||
text: ( | ||
<p> | ||
<EuiCode>DelayHide</EuiCode> is a component for conditionally toggling |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we change this to <EuiCode>EuiDelayHide</EuiCode>
to use the full component name?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
}; | ||
|
||
this.lastRenderedTime = this.props.hide ? 0 : Date.now(); | ||
this.shouldRender = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe instead of setting these variables here, we should extract the logic in componentWillReceiveProps
into an instance method and invoke it either here in the constructor or in componentWillMount
? I think this is a corner case, but it seems the most "correct".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fine with that. I'm not 100% sure how you want it to be - can you show it with code? :)
clearTimeout(this.timeout); | ||
|
||
const visibleDuration = Date.now() - this.lastRenderedTime; | ||
const timeRemaining = minimumDuration - visibleDuration; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to resume the countdown instead of resetting it entirely? In other words, why not debounce the hiding action?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand you correctly, I think the current implementation is correct.
In our case there might be several loading events with a few ms between them. If each takes 500ms, and they have 10ms between them, we should show the loading spinner for 1520ms (500ms + 10ms + 500ms + 10ms + 500ms) and then hide it immediately after the last loading event finishes.
If we reset the timer between each event the loading spinner will be displayed for 2020ms (500ms + 10ms + 500ms + 10 + 1000ms).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, you know what, you're right. I completely forgot that the whole point of this component is to show the waiting feedback for a bare minimum amount of time.
}; | ||
|
||
shouldComponentUpdate() { | ||
return this.shouldRender; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to use shouldComponentUpdate
here? What's it doing for us?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No - will remove it again.
I was of the impression that it would avoid re-rendering the children but it happens anyway so ¯_(ツ)_/¯
describe('when EuiDelayHide is visible initially and is changed to hidden', () => { | ||
let wrapper; | ||
beforeEach(() => { | ||
jest.useFakeTimers(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't know this existed! I'm going to use this to shave time off our other tests. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great! One gotcha I ran into, is that it doesn't do anything about Date.now
and similar date methods. It's just setTimeout and setInterval.
); | ||
}); | ||
|
||
test('it should be hidden initially', async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should it be hidden initially? I think it should behave the same as setting hide
to true, and should apply the delay. If the consumer really doesn't want it visible, they should just not render it at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it should behave the same as setting hide to true, and should apply the delay
I think if the initial value is hide=true
there is no reason to display it. The whole point of this component is to avoid "UI glitching". If an element is hidden from the start, it won't need the delay.
I think this is very different from a component changing visibility from visible to hidden (which is where we want the delay).
It would also add an extra burden on the consumer. As it is now I can render the global loading spinner with a prop from redux (isLoading
is a reduced value combining all loading states in the entire app):
export default ({ isLoading }) => {
return (
<EuiDelayHide
hide={!isLoading}
render={<EuiProgress size="xs" position="fixed" />}
/>
);
};
If EuiDelayHide
was to always render children on init I would have to keep a flag, to avoid rendering the loading spinner initially:
let isInitialLoad = true;
export default ({ isLoading }) => {
if (!isLoading && isInitialLoad) {
return null;
}
isInitialLoad = false;
return (
<EuiDelayHide
hide={!isLoading}
render={<EuiProgress size="xs" position="fixed" />}
/>
);
};
I think we should wait a little with this but if it becomes a requested feature we can add a flag, eg leading
delay (liked lodash.debounce
has it).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should wait a little with this but if it becomes a request feature we can add a flag, eg leading delay (liked lodash.debounce has it).
Sounds good!
<EuiDelayHide | ||
hide={this.state.hide} | ||
minimumDuration={this.state.minimumDuration} | ||
render={() => <div>Hello world</div>} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use an instance of <EuiLoadingSpinner size="m"/>
instead of the div, just to make the use case a little more realistic?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea.
Ouch, that looks nasty. Good catch. Can you reproduce it? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for all the feedback!
|
||
render() { | ||
return ( | ||
<div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
<EuiDelayHide | ||
hide={this.state.hide} | ||
minimumDuration={this.state.minimumDuration} | ||
render={() => <div>Hello world</div>} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea.
], | ||
text: ( | ||
<p> | ||
<EuiCode>DelayHide</EuiCode> is a component for conditionally toggling |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
}; | ||
|
||
this.lastRenderedTime = this.props.hide ? 0 : Date.now(); | ||
this.shouldRender = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fine with that. I'm not 100% sure how you want it to be - can you show it with code? :)
clearTimeout(this.timeout); | ||
|
||
const visibleDuration = Date.now() - this.lastRenderedTime; | ||
const timeRemaining = minimumDuration - visibleDuration; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand you correctly, I think the current implementation is correct.
In our case there might be several loading events with a few ms between them. If each takes 500ms, and they have 10ms between them, we should show the loading spinner for 1520ms (500ms + 10ms + 500ms + 10ms + 500ms) and then hide it immediately after the last loading event finishes.
If we reset the timer between each event the loading spinner will be displayed for 2020ms (500ms + 10ms + 500ms + 10 + 1000ms).
}; | ||
|
||
shouldComponentUpdate() { | ||
return this.shouldRender; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No - will remove it again.
I was of the impression that it would avoid re-rendering the children but it happens anyway so ¯_(ツ)_/¯
describe('when EuiDelayHide is visible initially and is changed to hidden', () => { | ||
let wrapper; | ||
beforeEach(() => { | ||
jest.useFakeTimers(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great! One gotcha I ran into, is that it doesn't do anything about Date.now
and similar date methods. It's just setTimeout and setInterval.
); | ||
}); | ||
|
||
test('it should be hidden initially', async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it should behave the same as setting hide to true, and should apply the delay
I think if the initial value is hide=true
there is no reason to display it. The whole point of this component is to avoid "UI glitching". If an element is hidden from the start, it won't need the delay.
I think this is very different from a component changing visibility from visible to hidden (which is where we want the delay).
It would also add an extra burden on the consumer. As it is now I can render the global loading spinner with a prop from redux (isLoading
is a reduced value combining all loading states in the entire app):
export default ({ isLoading }) => {
return (
<EuiDelayHide
hide={!isLoading}
render={<EuiProgress size="xs" position="fixed" />}
/>
);
};
If EuiDelayHide
was to always render children on init I would have to keep a flag, to avoid rendering the loading spinner initially:
let isInitialLoad = true;
export default ({ isLoading }) => {
if (!isLoading && isInitialLoad) {
return null;
}
isInitialLoad = false;
return (
<EuiDelayHide
hide={!isLoading}
render={<EuiProgress size="xs" position="fixed" />}
/>
);
};
I think we should wait a little with this but if it becomes a requested feature we can add a flag, eg leading
delay (liked lodash.debounce
has it).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! Thanks for doing this @sqren !
283f814
to
67b2a23
Compare
@cjcenizal I don't have write access. Will you merge? |
@sqren I just double checked and all Elastic employees should have write access. I just gave you admin access though, just to be sure. |
@cjcenizal What is the process for bumping eui version? Should I do that? And what about publishing? |
@sqren Sorry about that. It looks like the master branch access was locked down to only administrators for some reason. I removed that restriction so anyone with write access can contribute. |
Closes #382
This component will conditionally show/hide a child component. It will ensure that the child is visible for at least 1000ms (default). This avoids UI glitches with loading spinners and other elements that are rendered conditionally.
EuiDelayHide
uses a render props to render the child component.TODO: