Skip to content

Commit 9ad91b6

Browse files
committed
Avoid reflow on selectbox updates
1 parent c134f40 commit 9ad91b6

20 files changed

+240
-270
lines changed

.babelrc

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,5 @@
44
"@babel/typescript",
55
["@babel/preset-env", { "modules": false }]
66
],
7-
"plugins": ["@babel/plugin-proposal-class-properties"],
8-
"env": {
9-
"production": {
10-
"plugins": [
11-
[
12-
"transform-react-remove-prop-types",
13-
{
14-
"removeImport": true
15-
}
16-
]
17-
]
18-
}
19-
}
7+
"plugins": ["@babel/plugin-proposal-class-properties"]
208
}

example/src/App.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import React, { createRef, Component } from 'react'
22

33
import { TAlbumItem } from './sample-data'
44
import { SelectableGroup } from '../../src'
5-
import Counters from './Counters'
6-
import List from './List'
5+
import { Counters } from './Counters'
6+
import { List } from './List'
77

88
type TAppProps = {
99
items: TAlbumItem[]
@@ -47,10 +47,11 @@ class App extends Component<TAppProps, TAppState> {
4747
}
4848

4949
handleSelectionFinish = selectedItems => {
50+
console.log('Handle selection finish', selectedItems.length)
5051
this.countersRef.current.handleSelectionFinish(selectedItems)
5152
}
5253

53-
handleSelectedItemUnmount = (unmountedItem, selectedItems) => {
54+
handleSelectedItemUnmount = (_unmountedItem, selectedItems) => {
5455
console.log('hadneleSelectedItemUnmount')
5556
this.countersRef.current.handleSelectionFinish(selectedItems)
5657
}

example/src/Card.tsx

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,33 @@
1-
import React, { Component } from 'react'
1+
import React from 'react'
22

33
import { createSelectable, TSelectableItemProps } from '../../src'
4-
import Label from './Label'
4+
import { Label } from './Label'
55

6-
type TAlbumProps = TSelectableItemProps & {
6+
type TAlbumProps = {
77
player: string
88
year: number
99
}
1010

1111
const DISABLED_CARD_YEARS = [10, 22, 27, 54, 82, 105, 150]
1212

13-
class Card extends Component<TAlbumProps> {
14-
render() {
15-
const { selectableRef, isSelected, isSelecting, player, year } = this.props
13+
export const Card = createSelectable<TAlbumProps>((props: TSelectableItemProps & TAlbumProps) => {
14+
const { selectableRef, isSelected, isSelecting, player, year } = props
1615

17-
const classNames = [
18-
'item',
19-
DISABLED_CARD_YEARS.includes(year) && 'not-selectable',
20-
isSelecting && 'selecting',
21-
isSelected && 'selected'
22-
]
23-
.filter(Boolean)
24-
.join(' ')
16+
const classNames = [
17+
'item',
18+
DISABLED_CARD_YEARS.includes(year) && 'not-selectable',
19+
isSelecting && 'selecting',
20+
isSelected && 'selected'
21+
]
22+
.filter(Boolean)
23+
.join(' ')
2524

26-
return (
27-
<div ref={selectableRef} className={classNames}>
28-
<div className="tick">+</div>
29-
<h2>{player}</h2>
30-
<small>{year}</small>
31-
<Label isSelected={isSelected} isSelecting={isSelecting} />
32-
</div>
33-
)
34-
}
35-
}
36-
37-
export default createSelectable(Card)
25+
return (
26+
<div ref={selectableRef} className={classNames}>
27+
<div className="tick">+</div>
28+
<h2>{player}</h2>
29+
<small>{year}</small>
30+
<Label isSelected={isSelected} isSelecting={isSelecting} />
31+
</div>
32+
)
33+
})

example/src/Counters.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { Component } from 'react'
22

3-
class Counters extends Component {
3+
export class Counters extends Component {
44
state = {
55
selectedItems: [],
66
selectingItems: []
@@ -34,5 +34,3 @@ class Counters extends Component {
3434
)
3535
}
3636
}
37-
38-
export default Counters

example/src/Label.tsx

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,18 @@
1-
import React, { Component } from 'react'
1+
import React from 'react'
22

33
type TLabelProps = {
44
isSelecting: boolean
55
isSelected: boolean
66
}
77

8-
class Label extends Component<TLabelProps> {
9-
render() {
10-
const { isSelecting, isSelected } = this.props
8+
export function Label(props: TLabelProps) {
9+
const { isSelecting, isSelected } = props
1110

12-
return (
13-
<div className="card-label">
14-
Selecting: <span>{`${isSelecting}`}</span>
15-
<br />
16-
Selected: <span>{`${isSelected}`}</span>
17-
</div>
18-
)
19-
}
11+
return (
12+
<div className="card-label">
13+
Selecting: <span>{`${isSelecting}`}</span>
14+
<br />
15+
Selected: <span>{`${isSelected}`}</span>
16+
</div>
17+
)
2018
}
21-
22-
export default Label

example/src/List.tsx

Lines changed: 22 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,32 @@
1-
import React, { Component } from 'react'
1+
import React, { memo } from 'react'
22

33
import { TAlbumItem } from './sample-data'
44
import { DeselectAll, SelectAll } from '../../src'
5-
import SelectableCard from './Card'
5+
import { Card } from './Card'
66

77
type TListProps = {
88
items: TAlbumItem[]
99
}
1010

11-
class List extends Component<TListProps> {
12-
shouldComponentUpdate(nextProps: TListProps) {
13-
return nextProps.items !== this.props.items
14-
}
11+
export const List = memo((props: TListProps) => {
12+
const { items } = props
1513

16-
render() {
17-
const { items } = this.props
18-
19-
return (
20-
<div>
21-
<p className="not-selectable">Press ESC to clear selection</p>
22-
<div className="button-container">
23-
<SelectAll component="button" type="button" className="btn">
24-
Select all
25-
</SelectAll>
26-
<DeselectAll component="button" type="button" className="btn">
27-
Clear selection
28-
</DeselectAll>
29-
</div>
30-
<div className="albums">
31-
{items.map(item => (
32-
<SelectableCard key={item.year} player={item.player} year={item.year} />
33-
))}
34-
</div>
14+
return (
15+
<div>
16+
<p className="not-selectable">Press ESC to clear selection</p>
17+
<div className="button-container">
18+
<SelectAll component="button" type="button" className="btn">
19+
Select all
20+
</SelectAll>
21+
<DeselectAll component="button" type="button" className="btn">
22+
Clear selection
23+
</DeselectAll>
3524
</div>
36-
)
37-
}
38-
}
39-
40-
export default List
25+
<div className="albums">
26+
{items.map(item => (
27+
<Card key={item.year} player={item.player} year={item.year} />
28+
))}
29+
</div>
30+
</div>
31+
)
32+
})

package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@
8080
"@typescript-eslint/eslint-plugin": "^2.17.0",
8181
"@typescript-eslint/parser": "^2.17.0",
8282
"babel-loader": "^8.0.6",
83-
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
8483
"cross-env": "^7.0.0",
8584
"eslint": "6.8.0",
8685
"eslint-config-airbnb": "18.0.1",
@@ -93,7 +92,6 @@
9392
"husky": "^4.2.3",
9493
"lint-staged": "^10.0.7",
9594
"prettier": "^1.18.2",
96-
"prop-types": "^15.7.2",
9795
"react": "^16.8.6",
9896
"react-dom": "^16.8.6",
9997
"release-it": "^12.6.1",

src/CreateSelectable.tsx

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
import React, { Component, ComponentType } from 'react'
2-
import { bool } from 'prop-types'
32

43
import { getBoundsForNode, TComputedBounds, TGetBoundsForNodeArgs } from './utils'
5-
import { TSelectableItemState } from './Selectable.types'
6-
import SelectableGroupContext from './Context'
4+
import { TSelectableItemState, TSelectableItemProps } from './Selectable.types'
5+
import { SelectableGroupContext } from './SelectableGroup.context'
76

8-
const createSelectable = (WrappedComponent: ComponentType<any>) =>
9-
class SelectableItem extends Component<any, TSelectableItemState> {
10-
static contextType = SelectableGroupContext
7+
type TAddedProps = Partial<Pick<TSelectableItemProps, 'isSelected'>>
118

12-
static propTypes = {
13-
isSelected: bool
14-
}
9+
export const createSelectable = <T extends any>(
10+
WrappedComponent: ComponentType<TSelectableItemProps & T>
11+
): ComponentType<T & TAddedProps> =>
12+
class SelectableItem extends Component<T & TAddedProps, TSelectableItemState> {
13+
static contextType = SelectableGroupContext
1514

1615
static defaultProps = {
1716
isSelected: false
@@ -27,16 +26,16 @@ const createSelectable = (WrappedComponent: ComponentType<any>) =>
2726
bounds: TComputedBounds[] | null = null
2827

2928
componentDidMount() {
30-
this.registerSelectable()
29+
this.updateBounds()
30+
this.context.selectable.register(this)
3131
}
3232

3333
componentWillUnmount() {
3434
this.context.selectable.unregister(this)
3535
}
3636

37-
registerSelectable = (containerScroll?: TGetBoundsForNodeArgs) => {
37+
updateBounds = (containerScroll?: TGetBoundsForNodeArgs) => {
3838
this.bounds = getBoundsForNode(this.node!, containerScroll)
39-
this.context.selectable.register(this)
4039
}
4140

4241
getSelectableRef = (ref: HTMLElement | null) => {
@@ -49,5 +48,3 @@ const createSelectable = (WrappedComponent: ComponentType<any>) =>
4948
)
5049
}
5150
}
52-
53-
export default createSelectable

src/DeselectAll.tsx

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
import React, { Component, ReactNode } from 'react'
2-
import { ReactComponentLike } from 'prop-types'
1+
import React, { Component, FunctionComponent, ReactNode } from 'react'
32

4-
import SelectableGroupContext from './Context'
3+
import { SelectableGroupContext } from './SelectableGroup.context'
54

6-
type TDeselectAllButton = {
5+
type TDeselectAllProps = {
76
children: ReactNode
8-
component?: ReactComponentLike
7+
component?: string | FunctionComponent
98
className?: string
109
[key: string]: any
1110
}
1211

13-
class DeselectAllButton extends Component<TDeselectAllButton> {
12+
export class DeselectAll extends Component<TDeselectAllProps> {
1413
static contextType = SelectableGroupContext
1514

1615
root: HTMLDivElement | null = null
@@ -24,7 +23,8 @@ class DeselectAllButton extends Component<TDeselectAllButton> {
2423
}
2524

2625
render() {
27-
const { component: ButtonComponent = 'div', children, className, ...rest } = this.props
26+
const { component = 'div', children, className, ...rest } = this.props
27+
const ButtonComponent = component as FunctionComponent<any>
2828

2929
return (
3030
<ButtonComponent
@@ -38,5 +38,3 @@ class DeselectAllButton extends Component<TDeselectAllButton> {
3838
)
3939
}
4040
}
41-
42-
export default DeselectAllButton

src/SelectAll.tsx

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
import React, { Component, ReactNode } from 'react'
2-
import { ReactComponentLike } from 'prop-types'
1+
import React, { Component, ReactNode, FunctionComponent } from 'react'
32

4-
import SelectableGroupContext from './Context'
3+
import { SelectableGroupContext } from './SelectableGroup.context'
54

6-
type TSelectAllButton = {
5+
type TSelectAllProps = {
76
children: ReactNode
8-
component?: ReactComponentLike
7+
component?: string | FunctionComponent
98
className?: string
109
[key: string]: any
1110
}
1211

13-
class SelectAllButton extends Component<TSelectAllButton> {
12+
export class SelectAll extends Component<TSelectAllProps> {
1413
static contextType = SelectableGroupContext
1514

1615
root: HTMLDivElement | null = null
@@ -24,7 +23,8 @@ class SelectAllButton extends Component<TSelectAllButton> {
2423
}
2524

2625
render() {
27-
const { component: ButtonComponent = 'div', children, className = '', ...rest } = this.props
26+
const { component = 'div', children, className = '', ...rest } = this.props
27+
const ButtonComponent = component as FunctionComponent<any>
2828

2929
return (
3030
<ButtonComponent
@@ -38,5 +38,3 @@ class SelectAllButton extends Component<TSelectAllButton> {
3838
)
3939
}
4040
}
41-
42-
export default SelectAllButton

0 commit comments

Comments
 (0)