Skip to content

Commit 02f9f06

Browse files
authored
feat(Visibility): add updateOn prop (Semantic-Org#2791)
* feat(Visibility): add `updateOn` prop * test(Visibility): add more tests
1 parent 12a3731 commit 02f9f06

File tree

8 files changed

+337
-155
lines changed

8 files changed

+337
-155
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import React, { Component } from 'react'
2+
import { Checkbox, Grid, Segment, Sticky, Table, Visibility } from 'semantic-ui-react'
3+
4+
import Wireframe from '../Wireframe'
5+
6+
export default class VisibilityExampleUpdateOn extends Component {
7+
state = {
8+
calculations: {
9+
topVisible: false,
10+
bottomVisible: false,
11+
},
12+
showWireframe: true,
13+
}
14+
15+
handleContextRef = (contextRef) => {
16+
if (!this.state.contextRef) this.setState({ contextRef })
17+
}
18+
19+
handleUpdate = (e, { calculations }) => this.setState({ calculations })
20+
21+
handleWireframe = (e, { checked }) => this.setState({ showWireframe: checked })
22+
23+
render() {
24+
const { calculations, contextRef, showWireframe } = this.state
25+
26+
return (
27+
<div ref={this.handleContextRef}>
28+
<Grid columns={2}>
29+
<Grid.Column>
30+
{showWireframe ? <Wireframe /> : null}
31+
32+
<Visibility offset={[10, 10]} onUpdate={this.handleUpdate} updateOn='repaint'>
33+
<Segment>
34+
It's a tricky <code>Segment</code>
35+
</Segment>
36+
</Visibility>
37+
</Grid.Column>
38+
39+
<Grid.Column>
40+
<Sticky context={contextRef}>
41+
<Segment>
42+
<Checkbox
43+
checked={showWireframe}
44+
label='Show Wifeframe'
45+
onChange={this.handleWireframe}
46+
toggle
47+
/>
48+
49+
<Table basic='very' celled>
50+
<Table.Header>
51+
<Table.Row>
52+
<Table.HeaderCell>Calculation</Table.HeaderCell>
53+
<Table.HeaderCell>Value</Table.HeaderCell>
54+
</Table.Row>
55+
</Table.Header>
56+
<Table.Body>
57+
<Table.Row>
58+
<Table.Cell>topVisible</Table.Cell>
59+
<Table.Cell>{calculations.topVisible.toString()}</Table.Cell>
60+
</Table.Row>
61+
<Table.Row>
62+
<Table.Cell>bottomVisible</Table.Cell>
63+
<Table.Cell>{calculations.bottomVisible.toString()}</Table.Cell>
64+
</Table.Row>
65+
</Table.Body>
66+
</Table>
67+
</Segment>
68+
</Sticky>
69+
</Grid.Column>
70+
</Grid>
71+
</div>
72+
)
73+
}
74+
}

docs/app/Examples/behaviors/Visibility/Settings/index.js

+18
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from 'react'
2+
import { Message } from 'semantic-ui-react'
23

34
import ComponentExample from 'docs/app/Components/ComponentDoc/ComponentExample'
45
import ExampleSection from 'docs/app/Components/ComponentDoc/ExampleSection'
@@ -28,6 +29,23 @@ const VisibilitySettingsExamples = () => (
2829
description='You can specify callbacks that occur after different percentages or pixels of an element are passed.'
2930
examplePath='behaviors/Visibility/Settings/VisibilityExampleGroupedCallbacks'
3031
/>
32+
<ComponentExample
33+
title='Update on'
34+
description={
35+
<span>
36+
You can specify <code>updateOn='repaint'</code>, it will allow to update and fire
37+
callbacks on browser repaint (animation frames).
38+
</span>
39+
}
40+
examplePath='behaviors/Visibility/Settings/VisibilityExampleUpdateOn'
41+
>
42+
<Message warning>
43+
By default <code>Visibility</code> handles events only on browser events. It means that if
44+
you will hide a large block an event will not be triggered and <code>Visibility</code> will
45+
not perform calculations. This problem can be easily solved with{' '}
46+
<code>updateOn='repaint'</code>.
47+
</Message>
48+
</ComponentExample>
3149
</ExampleSection>
3250
)
3351

src/behaviors/Visibility/Visibility.d.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export interface VisibilityProps {
5757
* Value that context should be adjusted in pixels. Useful for making content appear below content fixed to the
5858
* page.
5959
*/
60-
offset?: number | string | Array<number|string>;
60+
offset?: number | string | Array<number | string>;
6161

6262
/** When set to false a callback will occur each time an element passes the threshold for a condition. */
6363
once?: boolean;
@@ -136,6 +136,13 @@ export interface VisibilityProps {
136136
* @param {object} data - All props.
137137
*/
138138
onUpdate?: (nothing: null, data: VisibilityEventData) => void;
139+
140+
/**
141+
* Allows to choose the mode of the position calculations:
142+
* - `events` - (default) update and fire callbacks only on scroll/resize events
143+
* - `repaint` - update and fire callbacks on browser repaint (animation frames)
144+
*/
145+
updateOn?: 'events' | 'repaint';
139146
}
140147

141148
export interface VisibilityCalculations {

src/behaviors/Visibility/Visibility.js

+40-16
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,7 @@ export default class Visibility extends Component {
7474
offset: PropTypes.oneOfType([
7575
PropTypes.number,
7676
PropTypes.string,
77-
PropTypes.arrayOf(PropTypes.oneOfType([
78-
PropTypes.number,
79-
PropTypes.string,
80-
])),
77+
PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])),
8178
]),
8279

8380
/** When set to false a callback will occur each time an element passes the threshold for a condition. */
@@ -157,13 +154,21 @@ export default class Visibility extends Component {
157154
* @param {object} data - All props.
158155
*/
159156
onUpdate: PropTypes.func,
157+
158+
/**
159+
* Allows to choose the mode of the position calculations:
160+
* - `events` - (default) update and fire callbacks only on scroll/resize events
161+
* - `repaint` - update and fire callbacks on browser repaint (animation frames)
162+
*/
163+
updateOn: PropTypes.oneOf(['events', 'repaint']),
160164
}
161165

162166
static defaultProps = {
163167
context: isBrowser() ? window : null,
164168
continuous: false,
165169
offset: [0, 0],
166170
once: true,
171+
updateOn: 'events',
167172
}
168173

169174
static _meta = {
@@ -188,24 +193,27 @@ export default class Visibility extends Component {
188193
// Lifecycle
189194
// ----------------------------------------
190195

191-
componentWillReceiveProps({ continuous, once, context }) {
192-
const cleanHappened = continuous !== this.props.continuous || once !== this.props.once
196+
componentWillReceiveProps({ continuous, once, context, updateOn }) {
197+
const cleanHappened =
198+
continuous !== this.props.continuous ||
199+
once !== this.props.once ||
200+
updateOn !== this.props.updateOn
193201

194202
// Heads up! We should clean up array of happened callbacks, if values of these props are changed
195203
if (cleanHappened) this.firedCallbacks = []
196204

197-
if (this.props.context !== context) {
205+
if (context !== this.props.context || updateOn !== this.props.updateOn) {
198206
this.unattachHandlers(this.props.context)
199-
this.attachHandlers(context)
207+
this.attachHandlers(context, updateOn)
200208
}
201209
}
202210

203211
componentDidMount() {
204212
if (!isBrowser()) return
205-
const { context, fireOnMount } = this.props
213+
const { context, fireOnMount, updateOn } = this.props
206214

207215
this.pageYOffset = window.pageYOffset
208-
this.attachHandlers(context)
216+
this.attachHandlers(context, updateOn)
209217

210218
if (fireOnMount) this.update()
211219
}
@@ -214,21 +222,30 @@ export default class Visibility extends Component {
214222
const { context } = this.props
215223

216224
this.unattachHandlers(context)
217-
if (this.frameId) cancelAnimationFrame(this.frameId)
218225
}
219226

220-
attachHandlers(context) {
221-
if (context) {
222-
eventStack.sub('resize', this.handleUpdate, { target: context })
223-
eventStack.sub('scroll', this.handleUpdate, { target: context })
227+
attachHandlers(context, updateOn) {
228+
if (updateOn === 'events') {
229+
if (context) {
230+
eventStack.sub('resize', this.handleUpdate, { target: context })
231+
eventStack.sub('scroll', this.handleUpdate, { target: context })
232+
}
233+
234+
return
224235
}
236+
237+
// Heads up!
238+
// We will deal with `repaint` there
239+
this.handleUpdate()
225240
}
226241

227242
unattachHandlers(context) {
228243
if (context) {
229244
eventStack.unsub('resize', this.handleUpdate, { target: context })
230245
eventStack.unsub('scroll', this.handleUpdate, { target: context })
231246
}
247+
248+
if (this.frameId) cancelAnimationFrame(this.frameId)
232249
}
233250

234251
// ----------------------------------------
@@ -308,6 +325,7 @@ export default class Visibility extends Component {
308325
onTopVisibleReverse,
309326
onOffScreen,
310327
onOnScreen,
328+
updateOn,
311329
} = this.props
312330
const forward = {
313331
bottomPassed: { callback: onBottomPassed, name: 'onBottomPassed' },
@@ -333,6 +351,8 @@ export default class Visibility extends Component {
333351
// Heads up! Reverse callbacks should be fired first
334352
_.forEach(reverse, (data, value) => this.fire(data, value, true))
335353
_.forEach(forward, (data, value) => this.fire(data, value))
354+
355+
if (updateOn === 'repaint') this.handleUpdate()
336356
}
337357

338358
// ----------------------------------------
@@ -392,6 +412,10 @@ export default class Visibility extends Component {
392412
const ElementType = getElementType(Visibility, this.props)
393413
const rest = getUnhandledProps(Visibility, this.props)
394414

395-
return <ElementType {...rest} ref={this.handleRef}>{children}</ElementType>
415+
return (
416+
<ElementType {...rest} ref={this.handleRef}>
417+
{children}
418+
</ElementType>
419+
)
396420
}
397421
}

test/specs/addons/Responsive/Responsive-test.js

+8-22
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,13 @@ describe('Responsive', () => {
1111
rendersContent: false,
1212
})
1313

14-
let requestAnimationFrame
15-
16-
before(() => {
17-
requestAnimationFrame = window.requestAnimationFrame
18-
window.requestAnimationFrame = fn => fn()
19-
})
20-
21-
after(() => {
22-
window.requestAnimationFrame = requestAnimationFrame
14+
beforeEach(() => {
15+
sandbox.stub(window, 'requestAnimationFrame').callsArg(0)
2316
})
2417

2518
describe('children', () => {
2619
it('renders by default', () => {
27-
shallow(<Responsive />)
28-
.should.be.present()
20+
shallow(<Responsive />).should.be.present()
2921
})
3022
})
3123

@@ -67,9 +59,7 @@ describe('Responsive', () => {
6759
const getWidth = () => 500
6860
const wrapper = shallow(<Responsive getWidth={getWidth} />)
6961

70-
wrapper
71-
.state('width')
72-
.should.equal(500)
62+
wrapper.state('width').should.equal(500)
7363
})
7464

7565
it('is called on resize', () => {
@@ -87,28 +77,24 @@ describe('Responsive', () => {
8777
describe('maxWidth', () => {
8878
it('renders when fits', () => {
8979
sandbox.stub(window, 'innerWidth').value(Responsive.onlyMobile.maxWidth)
90-
shallow(<Responsive {...Responsive.onlyMobile}>Show me!</Responsive>)
91-
.should.not.be.blank()
80+
shallow(<Responsive {...Responsive.onlyMobile}>Show me!</Responsive>).should.not.be.blank()
9281
})
9382

9483
it('do not render when not fits', () => {
9584
sandbox.stub(window, 'innerWidth').value(Responsive.onlyTablet.maxWidth)
96-
shallow(<Responsive {...Responsive.onlyMobile}>Hide me!</Responsive>)
97-
.should.be.blank()
85+
shallow(<Responsive {...Responsive.onlyMobile}>Hide me!</Responsive>).should.be.blank()
9886
})
9987
})
10088

10189
describe('minWidth', () => {
10290
it('renders when fits', () => {
10391
sandbox.stub(window, 'innerWidth').value(Responsive.onlyMobile.minWidth)
104-
shallow(<Responsive {...Responsive.onlyMobile}>Show me!</Responsive>)
105-
.should.not.be.blank()
92+
shallow(<Responsive {...Responsive.onlyMobile}>Show me!</Responsive>).should.not.be.blank()
10693
})
10794

10895
it('do not render when not fits', () => {
10996
sandbox.stub(window, 'innerWidth').value(Responsive.onlyTablet.minWidth)
110-
shallow(<Responsive {...Responsive.onlyMobile}>Hide me!</Responsive>)
111-
.should.be.blank()
97+
shallow(<Responsive {...Responsive.onlyMobile}>Hide me!</Responsive>).should.be.blank()
11298
})
11399
})
114100

0 commit comments

Comments
 (0)