diff --git a/docs/app/Components/ComponentDoc/ComponentExample/ComponentExample.js b/docs/app/Components/ComponentDoc/ComponentExample/ComponentExample.js index a2529856b7..70bdb3e4f8 100644 --- a/docs/app/Components/ComponentDoc/ComponentExample/ComponentExample.js +++ b/docs/app/Components/ComponentDoc/ComponentExample/ComponentExample.js @@ -166,6 +166,7 @@ class ComponentExample extends Component { const LODASH = require('lodash') const REACT = require('react') const SEMANTIC_UI_REACT = require('semantic-ui-react') + let WIREFRAME let COMMON /* eslint-enable no-unused-vars */ @@ -191,6 +192,8 @@ class ComponentExample extends Component { if (module === 'COMMON') { const componentPath = examplePath.split(__PATH_SEP__).splice(0, 2).join(__PATH_SEP__) COMMON = require(`docs/app/Examples/${componentPath}/common`) + } else if (module === 'WIREFRAME') { + WIREFRAME = require('docs/app/Examples/behaviors/Visibility/Wireframe').default } const constStatements = [] @@ -420,8 +423,10 @@ class ComponentExample extends Component { {exampleElement} - {this.renderJSX()} - {this.renderHTML()} + + {this.renderJSX()} + {this.renderHTML()} + ) diff --git a/docs/app/Examples/behaviors/Visibility/Settings/CallbackFrequencyExample.js b/docs/app/Examples/behaviors/Visibility/Settings/CallbackFrequencyExample.js deleted file mode 100644 index 94bc3a1848..0000000000 --- a/docs/app/Examples/behaviors/Visibility/Settings/CallbackFrequencyExample.js +++ /dev/null @@ -1,66 +0,0 @@ -import React, { Component } from 'react' -import { Button, Checkbox, Divider, Grid, Segment, Visibility } from 'semantic-ui-react' - -import Wireframe from '../Wireframe' - -export default class VisibilityExample extends Component { - state = { - continuous: false, - log: [], - once: true, - } - - updateLog = eventName => () => this.setState({ log: [eventName, ...this.state.log] }) - - clearLog = () => this.setState({ log: [] }) - - toggleOnce = () => this.setState({ once: !this.state.once }) - - toggleContinuous = () => this.setState({ continuous: !this.state.continuous }) - - render() { - const { continuous, log, once } = this.state - - return ( - - - - - - - - - - - - - - - - Event Log - -
-                {log.map((e, i) => 

{e} fired

)} -
-
-
-
-
- ) - } -} diff --git a/docs/app/Examples/behaviors/Visibility/Settings/GroupedCallbacksExample.js b/docs/app/Examples/behaviors/Visibility/Settings/GroupedCallbacksExample.js deleted file mode 100644 index 7e78225358..0000000000 --- a/docs/app/Examples/behaviors/Visibility/Settings/GroupedCallbacksExample.js +++ /dev/null @@ -1,63 +0,0 @@ -import React, { Component } from 'react' -import { Button, Checkbox, Divider, Grid, Segment, Visibility } from 'semantic-ui-react' -import Wireframe from '../Wireframe' - -class VisibilityExample extends Component { - state = { - continuous: false, - log: [], - once: true, - } - - updateLog = eventName => () => this.setState({ log: [eventName, ...this.state.log] }) - - clearLog = () => this.setState({ log: [] }) - - toggleOnce = () => this.setState({ once: !this.state.once }) - - toggleContinuous = () => this.setState({ continuous: !this.state.continuous }) - - render() { - const { continuous, log, once } = this.state - - return ( - - - - - - - - - - - - - - - - Event Log - -
-                {log.map((e, i) => 

{e} fired

)} -
-
-
-
-
- ) - } -} - -export default VisibilityExample diff --git a/docs/app/Examples/behaviors/Visibility/Settings/VisibilityExampleCallbackFrequency.js b/docs/app/Examples/behaviors/Visibility/Settings/VisibilityExampleCallbackFrequency.js new file mode 100644 index 0000000000..f2aaa0e5b3 --- /dev/null +++ b/docs/app/Examples/behaviors/Visibility/Settings/VisibilityExampleCallbackFrequency.js @@ -0,0 +1,79 @@ +import React, { Component } from 'react' +import { Button, Checkbox, Divider, Grid, Label, Segment, Sticky, Visibility } from 'semantic-ui-react' + +import Wireframe from '../Wireframe' + +export default class VisibilityExampleCallbackFrequency extends Component { + state = { + continuous: false, + log: [], + logCount: 0, + once: true, + } + + handleContextRef = contextRef => this.setState({ contextRef }) + + updateLog = eventName => () => this.setState(({ + log: [ + `${new Date().toLocaleTimeString()}: ${eventName}`, + ...this.state.log, + ].slice(0, 20), + logCount: this.state.logCount + 1, + })) + + clearLog = () => this.setState({ log: [], logCount: 0 }) + + toggleOnce = () => this.setState({ once: !this.state.once }) + + toggleContinuous = () => this.setState({ continuous: !this.state.continuous }) + + render() { + const { continuous, contextRef, log, logCount, once } = this.state + + return ( +
+ + + + + + + + + + + + + + + + + + Event Log + + +
{log.map((e, i) => 
{e}
)}
+
+
+
+
+
+
+ ) + } +} diff --git a/docs/app/Examples/behaviors/Visibility/Settings/VisibilityExampleGroupedCallbacks.js b/docs/app/Examples/behaviors/Visibility/Settings/VisibilityExampleGroupedCallbacks.js new file mode 100644 index 0000000000..9652bfb6a6 --- /dev/null +++ b/docs/app/Examples/behaviors/Visibility/Settings/VisibilityExampleGroupedCallbacks.js @@ -0,0 +1,75 @@ +import React, { Component } from 'react' +import { Button, Checkbox, Divider, Grid, Label, Segment, Sticky, Visibility } from 'semantic-ui-react' + +import Wireframe from '../Wireframe' + +export default class VisibilityExampleGroupedCallbacks extends Component { + state = { + continuous: false, + log: [], + logCount: 0, + once: true, + } + + handleContextRef = contextRef => this.setState({ contextRef }) + + updateLog = eventName => () => this.setState(({ + log: [ + `${new Date().toLocaleTimeString()}: ${eventName}`, + ...this.state.log, + ].slice(0, 20), + logCount: this.state.logCount + 1, + })) + + clearLog = () => this.setState({ log: [], logCount: 0 }) + + toggleOnce = () => this.setState({ once: !this.state.once }) + + toggleContinuous = () => this.setState({ continuous: !this.state.continuous }) + + render() { + const { continuous, contextRef, log, logCount, once } = this.state + + return ( +
+ + + + + + + + + + + + + + + + + + Event Log + + +
{log.map((e, i) => 
{e}
)}
+
+
+
+
+
+
+ ) + } +} diff --git a/docs/app/Examples/behaviors/Visibility/Settings/VisibilityExampleOffset.js b/docs/app/Examples/behaviors/Visibility/Settings/VisibilityExampleOffset.js new file mode 100644 index 0000000000..6b7a9c823e --- /dev/null +++ b/docs/app/Examples/behaviors/Visibility/Settings/VisibilityExampleOffset.js @@ -0,0 +1,66 @@ +import React, { Component } from 'react' +import { Grid, Sticky, Table, Visibility } from 'semantic-ui-react' + +import Wireframe from '../Wireframe' + +export default class VisibilityExampleOffset extends Component { + state = { + calculations: { + topPassed: false, + bottomPassed: false, + topVisible: false, + bottomVisible: false, + }, + } + + handleContextRef = contextRef => this.setState({ contextRef }) + + handleUpdate = (e, { calculations }) => this.setState({ calculations }) + + render() { + const { calculations, contextRef } = this.state + + return ( +
+ + + + + + + + + + + + + Calculation + Value + + + + + topVisible + {calculations.topVisible.toString()} + + + bottomVisible + {calculations.bottomVisible.toString()} + + + topPassed + {calculations.topPassed.toString()} + + + bottomPassed + {calculations.bottomPassed.toString()} + + +
+
+
+
+
+ ) + } +} diff --git a/docs/app/Examples/behaviors/Visibility/Settings/index.js b/docs/app/Examples/behaviors/Visibility/Settings/index.js index c68dbd1dfc..7c21456f3a 100644 --- a/docs/app/Examples/behaviors/Visibility/Settings/index.js +++ b/docs/app/Examples/behaviors/Visibility/Settings/index.js @@ -3,24 +3,32 @@ import React from 'react' import ComponentExample from 'docs/app/Components/ComponentDoc/ComponentExample' import ExampleSection from 'docs/app/Components/ComponentDoc/ExampleSection' -const VisibilityExample = () => ( +const VisibilitySettingsExamples = () => ( + ) -export default VisibilityExample +export default VisibilitySettingsExamples diff --git a/docs/app/Examples/behaviors/Visibility/Types/VisibilityExample.js b/docs/app/Examples/behaviors/Visibility/Types/VisibilityExample.js deleted file mode 100644 index 78f66398b7..0000000000 --- a/docs/app/Examples/behaviors/Visibility/Types/VisibilityExample.js +++ /dev/null @@ -1,101 +0,0 @@ -import React, { Component } from 'react' -import { Grid, Table, Visibility } from 'semantic-ui-react' -import Wireframe from '../Wireframe' - -class VisibilityExample extends Component { - state = { - calculations: { - height: 0, - width: 0, - topPassed: false, - bottomPassed: false, - pixelsPassed: 0, - percentagePassed: 0, - topVisible: false, - bottomVisible: false, - fits: false, - passing: false, - onScreen: false, - offScreen: false, - }, - } - - handleUpdate = (e, { calculations }) => this.setState({ calculations }) - - render() { - const { calculations } = this.state - - return ( - - - - - - - - - - - - Calculation - Value - - - - - pixelsPassed - {calculations.pixelsPassed.toFixed()}px - - - percentagePassed - {(calculations.percentagePassed * 100).toFixed()}% - - - fits - {calculations.fits.toString()} - - - width - {calculations.width.toFixed()}px - - - height - {calculations.height.toFixed()}px - - - onScreen - {calculations.onScreen.toString()} - - - offScreen - {calculations.offScreen.toString()} - - - passing - {calculations.passing.toString()} - - - topVisible - {calculations.topVisible.toString()} - - - bottomVisible - {calculations.bottomVisible.toString()} - - - topPassed - {calculations.topPassed.toString()} - - - bottomPassed - {calculations.bottomPassed.toString()} - - -
-
-
- ) - } -} - -export default VisibilityExample diff --git a/docs/app/Examples/behaviors/Visibility/Types/VisibilityExampleVisibility.js b/docs/app/Examples/behaviors/Visibility/Types/VisibilityExampleVisibility.js new file mode 100644 index 0000000000..418c85f676 --- /dev/null +++ b/docs/app/Examples/behaviors/Visibility/Types/VisibilityExampleVisibility.js @@ -0,0 +1,106 @@ +import React, { Component } from 'react' +import { Grid, Sticky, Table, Visibility } from 'semantic-ui-react' + +import Wireframe from '../Wireframe' + +export default class VisibilityExampleVisibility extends Component { + state = { + calculations: { + height: 0, + width: 0, + topPassed: false, + bottomPassed: false, + pixelsPassed: 0, + percentagePassed: 0, + topVisible: false, + bottomVisible: false, + fits: false, + passing: false, + onScreen: false, + offScreen: false, + }, + } + + handleContextRef = contextRef => this.setState({ contextRef }) + + handleUpdate = (e, { calculations }) => this.setState({ calculations }) + + render() { + const { calculations, contextRef } = this.state + + return ( +
+ + + + + + + + + + + + + Calculation + Value + + + + + pixelsPassed + {calculations.pixelsPassed.toFixed()}px + + + percentagePassed + {(calculations.percentagePassed * 100).toFixed()}% + + + fits + {calculations.fits.toString()} + + + width + {calculations.width.toFixed()}px + + + height + {calculations.height.toFixed()}px + + + onScreen + {calculations.onScreen.toString()} + + + offScreen + {calculations.offScreen.toString()} + + + passing + {calculations.passing.toString()} + + + topVisible + {calculations.topVisible.toString()} + + + bottomVisible + {calculations.bottomVisible.toString()} + + + topPassed + {calculations.topPassed.toString()} + + + bottomPassed + {calculations.bottomPassed.toString()} + + +
+
+
+
+
+ ) + } +} diff --git a/docs/app/Examples/behaviors/Visibility/Types/index.js b/docs/app/Examples/behaviors/Visibility/Types/index.js index ceed463a2b..52ff021199 100644 --- a/docs/app/Examples/behaviors/Visibility/Types/index.js +++ b/docs/app/Examples/behaviors/Visibility/Types/index.js @@ -3,14 +3,14 @@ import React from 'react' import ComponentExample from 'docs/app/Components/ComponentDoc/ComponentExample' import ExampleSection from 'docs/app/Components/ComponentDoc/ExampleSection' -const VisibilityExample = () => ( +const VisibilityTypesExamples = () => ( ) -export default VisibilityExample +export default VisibilityTypesExamples diff --git a/src/behaviors/Visibility/Visibility.d.ts b/src/behaviors/Visibility/Visibility.d.ts index 1ce1d60b90..9d6b24abf8 100644 --- a/src/behaviors/Visibility/Visibility.d.ts +++ b/src/behaviors/Visibility/Visibility.d.ts @@ -53,6 +53,12 @@ export interface VisibilityProps { */ onBottomVisibleReverse?: (nothing: null, data: VisibilityEventData) => void; + /** + * Value that context should be adjusted in pixels. Useful for making content appear below content fixed to the + * page. + */ + offset?: number | string | Array; + /** When set to false a callback will occur each time an element passes the threshold for a condition. */ once?: boolean; diff --git a/src/behaviors/Visibility/Visibility.js b/src/behaviors/Visibility/Visibility.js index ef41889e02..3c4d9c5882 100644 --- a/src/behaviors/Visibility/Visibility.js +++ b/src/behaviors/Visibility/Visibility.js @@ -7,6 +7,7 @@ import { getElementType, getUnhandledProps, META, + normalizeOffset, isBrowser, } from '../../lib' @@ -65,6 +66,19 @@ export default class Visibility extends Component { */ onBottomVisibleReverse: PropTypes.func, + /** + * Value that context should be adjusted in pixels. Useful for making content appear below content fixed to the + * page. + */ + offset: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + PropTypes.arrayOf([ + PropTypes.number, + PropTypes.string, + ]), + ]), + /** When set to false a callback will occur each time an element passes the threshold for a condition. */ once: PropTypes.bool, @@ -147,6 +161,7 @@ export default class Visibility extends Component { static defaultProps = { context: isBrowser ? window : null, continuous: false, + offset: [0, 0], once: true, } @@ -280,16 +295,18 @@ export default class Visibility extends Component { handleRef = c => (this.ref = c) handleUpdate = () => { + const { offset } = this.props const { bottom, height, top, width } = this.ref.getBoundingClientRect() + const [topOffset, bottomOffset] = normalizeOffset(offset) - const topPassed = top < 0 - const bottomPassed = bottom < 0 + const topPassed = top < topOffset + const bottomPassed = bottom < bottomOffset const pixelsPassed = bottomPassed ? 0 : Math.max(top * -1, 0) const percentagePassed = pixelsPassed / height - const bottomVisible = bottom >= 0 && bottom <= window.innerHeight - const topVisible = top >= 0 && top <= window.innerHeight + const bottomVisible = bottom >= bottomOffset && bottom <= window.innerHeight + const topVisible = top >= topOffset && top <= window.innerHeight const fits = topVisible && bottomVisible const passing = topPassed && !bottomPassed diff --git a/src/lib/index.js b/src/lib/index.js index eea44556e1..4778c777c9 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -39,6 +39,7 @@ export * as SUI from './SUI' export { default as keyboardKey } from './keyboardKey' export { numberToWordMap, numberToWord } from './numberToWord' +export normalizeOffset from './normalizeOffset' export normalizeTransitionDuration from './normalizeTransitionDuration' export { default as objectDiff } from './objectDiff' export shallowEqual from './shallowEqual' diff --git a/src/lib/normalizeOffset.js b/src/lib/normalizeOffset.js new file mode 100644 index 0000000000..861d02f90b --- /dev/null +++ b/src/lib/normalizeOffset.js @@ -0,0 +1,6 @@ +/** + * Normalizes the offset value. + * @param {number|array} value The value to normalize. + * @returns {number} + */ +export default value => ((typeof value === 'number' || typeof value === 'string') ? [value, value] : value) diff --git a/test/specs/behaviors/Visibility/Visibility-test.js b/test/specs/behaviors/Visibility/Visibility-test.js index a9da20a139..244d372f4d 100644 --- a/test/specs/behaviors/Visibility/Visibility-test.js +++ b/test/specs/behaviors/Visibility/Visibility-test.js @@ -1,3 +1,4 @@ +import _ from 'lodash' import React from 'react' import Visibility from 'src/behaviors/Visibility' @@ -200,6 +201,41 @@ describe('Visibility', () => { }) }) + describe('offset', () => { + _.each(_.filter(expectations, 'callback'), (expectation) => { + it(`fires ${expectation.name} when offset is number`, () => { + const callback = sandbox.spy() + const opts = { [expectation.callback]: callback } + + const offset = 10 + const falseCond = _.map(expectation.false[0], value => value + offset) + const trueCond = _.map(expectation.true[0], value => value + offset) + + wrapperMount() + mockScroll(...trueCond) + mockScroll(...falseCond) + + callback.should.have.been.calledOnce() + }) + + it(`fires ${expectation.name} when offset is array`, () => { + const callback = sandbox.spy() + const opts = { [expectation.callback]: callback } + + const bottomOffset = 20 + const topOffset = 10 + const falseCond = [expectation.false[0][0] + topOffset, expectation.false[0][1] + bottomOffset] + const trueCond = [expectation.true[0][0] + topOffset, expectation.true[0][1] + bottomOffset] + + wrapperMount() + mockScroll(...trueCond) + mockScroll(...falseCond) + + callback.should.have.been.calledOnce() + }) + }) + }) + describe('onPassed', () => { it('fires callback when pixels passed', () => { const onPassed = {