-
Notifications
You must be signed in to change notification settings - Fork 39
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
fix: fix flickering when expanding/collapsing sections #595
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,89 +1,81 @@ | ||
'use strict'; | ||
|
||
import React, {Component} from 'react'; | ||
import React, {useContext, useLayoutEffect, useState} from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import classNames from 'classnames'; | ||
import {isEmpty, isFunction} from 'lodash'; | ||
import {Card, Disclosure} from '@gravity-ui/uikit'; | ||
import {Disclosure} from '@gravity-ui/uikit'; | ||
import {MeasurementContext} from './measurement-context'; | ||
|
||
export default class Details extends Component { | ||
static propTypes = { | ||
type: PropTypes.oneOf(['text', 'image']), | ||
title: PropTypes.oneOfType([PropTypes.element, PropTypes.string]).isRequired, | ||
content: PropTypes.oneOfType([PropTypes.func, PropTypes.string, PropTypes.element, PropTypes.array]).isRequired, | ||
extendClassNames: PropTypes.oneOfType([PropTypes.array, PropTypes.string]), | ||
onClick: PropTypes.func, | ||
asHtml: PropTypes.bool | ||
}; | ||
|
||
state = {isOpened: false}; | ||
|
||
handleClick = () => { | ||
this.setState((state, props) => { | ||
const newState = {isOpened: !state.isOpened}; | ||
export default function Details(props) { | ||
const [isOpened, setIsOpened] = useState(false); | ||
|
||
if (props.onClick) { | ||
props.onClick(newState); | ||
} | ||
|
||
return newState; | ||
}); | ||
}; | ||
const handleClick = () => { | ||
const newIsOpened = !isOpened; | ||
|
||
stopPropagation = (e) => { | ||
e.stopPropagation(); | ||
setIsOpened(newIsOpened); | ||
props.onClick?.({isOpened: newIsOpened}); | ||
}; | ||
|
||
_getContent() { | ||
const content = this.props.content; | ||
const getContent = () => { | ||
const {content} = props; | ||
|
||
return isFunction(content) ? content() : content; | ||
} | ||
}; | ||
|
||
_renderContent() { | ||
if (!this.state.isOpened) { | ||
const renderContent = () => { | ||
if (!isOpened) { | ||
return null; | ||
} | ||
|
||
const children = this.props.asHtml ? null : this._getContent(); | ||
const extraProps = this.props.asHtml ? {dangerouslySetInnerHTML: {__html: this._getContent()}} : {}; | ||
const children = props.asHtml ? null : getContent(); | ||
const extraProps = props.asHtml ? {dangerouslySetInnerHTML: {__html: getContent()}} : {}; | ||
|
||
return <div className='details__content' {...extraProps}> | ||
{children} | ||
</div>; | ||
} | ||
}; | ||
|
||
render() { | ||
const {type, title, content, extendClassNames} = this.props; | ||
const className = classNames( | ||
'details', | ||
extendClassNames | ||
); | ||
const {title, content, extendClassNames} = props; | ||
const className = classNames( | ||
'details', | ||
extendClassNames | ||
); | ||
|
||
return ( | ||
isEmpty(content) && !isFunction(content) ? ( | ||
<div className={className}> | ||
{title} | ||
</div> | ||
) : ( | ||
<Disclosure className={className} onUpdate={this.handleClick} | ||
size='l'> | ||
<Disclosure.Summary> | ||
{(props, defaultButton) => ( | ||
<div className={classNames(className, 'details__summary')} aria-controls={props.ariaControls} onClick={props.onClick} id={props.id}> | ||
<div className="details__expand-button" onClick={this.stopPropagation}> | ||
{defaultButton} | ||
</div> | ||
{title} | ||
const {measure} = useContext(MeasurementContext); | ||
useLayoutEffect(() => { | ||
measure?.(); | ||
}, [isOpened]); | ||
|
||
return ( | ||
isEmpty(content) && !isFunction(content) ? ( | ||
<div className={className}> | ||
{title} | ||
</div> | ||
) : ( | ||
<Disclosure className={className} onUpdate={handleClick} size='l' expanded={isOpened}> | ||
<Disclosure.Summary> | ||
{(props, defaultButton) => ( | ||
<div className={classNames(className, 'details__summary')} aria-controls={props.ariaControls} onClick={props.onClick} id={props.id}> | ||
<div className="details__expand-button" onClick={e => e.stopPropagation()}> | ||
{defaultButton} | ||
</div> | ||
)} | ||
</Disclosure.Summary> | ||
{type === 'image' ? this._renderContent() : | ||
<Card className='details__card' view='filled'> | ||
{this._renderContent()} | ||
</Card>} | ||
</Disclosure> | ||
) | ||
); | ||
} | ||
{title} | ||
</div> | ||
)} | ||
</Disclosure.Summary> | ||
{renderContent()} | ||
</Disclosure> | ||
) | ||
); | ||
} | ||
|
||
Details.propTypes = { | ||
id: PropTypes.string, | ||
ariaControls: PropTypes.arrayOf(PropTypes.string), | ||
title: PropTypes.oneOfType([PropTypes.element, PropTypes.string]).isRequired, | ||
content: PropTypes.oneOfType([PropTypes.func, PropTypes.string, PropTypes.element, PropTypes.array]).isRequired, | ||
extendClassNames: PropTypes.oneOfType([PropTypes.array, PropTypes.string]), | ||
onClick: PropTypes.func, | ||
asHtml: PropTypes.bool | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import {createContext} from 'react'; | ||
|
||
export const MeasurementContext = createContext({measure: () => {}}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can't figure out how it works and fixes the problem There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Intro: react-virtualized needs to know exact sizes of each element for correct positioning. To let react-virtualized know the exact dimensions of element, you should use Problem (how it was before): when we expand section, it immediately renders expanded section. At this point react-virtualized knows nothing about expanded section, because Solution (how it is now): we use Since there are many components that need to be able to call Now, everything looks 100% correct and user never sees intermediate state. However there's definitely some room for optimisation in terms of performance. If you click on expand/collapse all under the devtools CPU x6 slower throttling, it takes around 5s to render. That's because measuring happens tens of times instead of just once before final render. I wasn't able to fix this quickly, and without throttling everything works fine, so decided to keep it as is. |
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.
why don't you need this condition now?
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.
We wrap components in Card directly where we need it and pass already wrapped components as children.
The Details component shouldn't be aware of anything regarding its children IMO. It shouldn't care what it will render, it just receives
ReactNode
and renders it as-is. That makes logic a lot easier to understand and reduces complexity. That's from the software design point of view.Another reason why I got rid of it, because it causes another visual glitch — upon collapsing Meta/History, you'd first see a frame where Disclosure contains empty card and only afterwards it collapses completely.