diff --git a/packages/shared/index.js b/packages/shared/index.js index 3604d26f..a0519f3e 100644 --- a/packages/shared/index.js +++ b/packages/shared/index.js @@ -50,7 +50,7 @@ export { default as Translation } from './src/Translation'; export { default as TranslationFragment } from './src/TranslationFragment'; export { default as Duration } from './src/Duration'; -export { default as SliderLabels } from './src/SliderLabels'; +export { default as SliderLabels } from './src/forms/SliderLabels'; export { default as OfflineScreen } from './src/offlineScreen/OfflineScreen'; export { default as StatusbarBackground, diff --git a/packages/shared/src/SliderLabels.js b/packages/shared/src/SliderLabels.js deleted file mode 100644 index fbc0cf11..00000000 --- a/packages/shared/src/SliderLabels.js +++ /dev/null @@ -1,209 +0,0 @@ -// @flow - -import * as React from 'react'; -import { defaultTokens } from '@kiwicom/mobile-orbit'; -import { View } from 'react-native'; - -import type { OnLayout } from '../types/Events'; -import Text from './Text'; -import StyleSheet from './PlatformStyleSheet'; -import Price from './Price'; -import type { TranslationType } from '../types/Translation'; - -type Props = {| - +startLabel: TranslationType | React.Element, - +startValue: number, - +endLabel?: TranslationType | React.Element, - +endValue?: number, - +max: number, - +min: number, -|}; - -type State = {| - width: number, - labelStartWidth: number, - labelStartAtMax: boolean, - labelEndWidth: number, - paddingLeft: number, - paddingRight: number, -|}; - -export default class SliderLabels extends React.Component { - constructor(props: Props) { - super(props); - this.state = { - width: 0, - labelStartWidth: 0, - labelStartAtMax: false, - labelEndWidth: 0, - paddingLeft: 0, - paddingRight: 0, - }; - } - - componentDidMount() { - if (this.props.endValue) { - requestAnimationFrame(this.setPaddingForTwoLabels); - } else { - requestAnimationFrame(this.setPaddingForOneLabel); - } - } - - getMaxPadding = (gap: number): number => { - return Math.floor( - this.state.width - - this.state.labelStartWidth - - this.state.labelEndWidth - - gap, - ); - }; - - calculateMarkerStartOffset = (): number => { - const { min, max, startValue } = this.props; - - let val; - if (startValue > max) { - val = max; - } else if (startValue < min) { - val = min; - } else { - val = startValue; - } - - return Math.round((val / (max - min)) * this.state.width); - }; - - calculateMarkerEndOffset = (): number => { - const { min, max, endValue } = this.props; - - if (!endValue) { - return 0; - } - - let val; - if (endValue > max) { - val = max; - } else if (endValue < min) { - val = min; - } else { - val = endValue; - } - - const w = (val / (max - min)) * this.state.width; - return Math.round(this.state.width - w); - }; - - getStartLabelOffset = (): number => { - const startMarkerOffset = this.calculateMarkerStartOffset(); - const startLabelHalf = this.state.labelStartWidth / 2; - - return startMarkerOffset < startLabelHalf - ? 0 - : startMarkerOffset - startLabelHalf; - }; - - isBelowMaxPadding = (value: number, gap: number = 0): boolean => { - const maxPadding = this.getMaxPadding(gap); - return value + gap < maxPadding; - }; - - setPaddingForTwoLabels = (): void => { - const startLabelOffset = this.getStartLabelOffset(); - const endMarkerOffset = this.calculateMarkerEndOffset(); - const endLabelHalf = this.state.labelEndWidth / 2; - const endLabelOffset = - endMarkerOffset < endLabelHalf ? 0 : endMarkerOffset - endLabelHalf; - - const isBelowMaxPadding = this.isBelowMaxPadding( - startLabelOffset + endLabelOffset, - 10, - ); - - const hasOffsetChanged = - this.state.paddingLeft !== startLabelOffset || - this.state.paddingRight !== endLabelOffset; - - if (isBelowMaxPadding && hasOffsetChanged) { - this.setState({ - paddingLeft: startLabelOffset, - paddingRight: endLabelOffset, - }); - } - - requestAnimationFrame(this.setPaddingForTwoLabels); - }; - - setPaddingForOneLabel = (): void => { - const startLabelOffset = this.getStartLabelOffset(); - const isBelowMaxPadding = this.isBelowMaxPadding(startLabelOffset); - const hasOffsetChanged = this.state.paddingLeft !== startLabelOffset; - - if (isBelowMaxPadding) { - if (hasOffsetChanged) { - this.setState({ - paddingLeft: startLabelOffset, - }); - } - - if (this.state.labelStartAtMax) { - this.setState({ labelStartAtMax: false }); - } - } else if (this.state.labelStartAtMax === false) { - this.setState({ labelStartAtMax: true }); - } - - requestAnimationFrame(this.setPaddingForOneLabel); - }; - - saveFullWidth = (e: OnLayout) => { - this.setState({ width: Math.floor(e.nativeEvent.layout.width) }); - }; - - saveLabelStartWidth = (e: OnLayout) => { - this.setState({ labelStartWidth: Math.floor(e.nativeEvent.layout.width) }); - }; - - saveLabelEndWidth = (e: OnLayout) => { - this.setState({ labelEndWidth: Math.floor(e.nativeEvent.layout.width) }); - }; - - render() { - return ( - - - {this.props.startLabel} - - {this.props.endLabel && ( - - {this.props.endLabel} - - )} - - ); - } -} - -const styles = StyleSheet.create({ - sliderLabels: { - width: '100%', - flexDirection: 'row', - marginTop: 10, - marginBottom: 5, - }, - label: { - fontSize: 14, - color: defaultTokens.paletteBlueNormal, - }, -}); diff --git a/packages/shared/src/__tests__/SliderLabels.test.js b/packages/shared/src/__tests__/SliderLabels.test.js deleted file mode 100644 index 9c32c8b0..00000000 --- a/packages/shared/src/__tests__/SliderLabels.test.js +++ /dev/null @@ -1,149 +0,0 @@ -// @flow strict - -import * as React from 'react'; -import renderer from 'react-test-renderer'; -import ShallowRenderer from 'react-test-renderer/shallow'; - -import Translation from '../Translation'; -import SliderLabels from '../SliderLabels'; - -const shallowRenderer = new ShallowRenderer(); - -it('one label', () => { - expect( - shallowRenderer.render( - } - startValue={46} - max={1000} - min={1} - />, - ), - ).toMatchSnapshot(); -}); - -it('two labels', () => { - expect( - shallowRenderer.render( - } - startValue={46} - endLabel={} - endValue={850} - max={1000} - min={1} - />, - ), - ).toMatchSnapshot(); -}); - -const requestAF = global.requestAnimationFrame; -beforeEach(async () => (global.requestAnimationFrame = jest.fn())); // eslint-disable-line require-await -afterEach(() => (global.requestAnimationFrame = requestAF)); - -it('startValue in the middle', () => { - const wrapper = renderer - .create( - } - startValue={3} - />, - ) - .getInstance(); - - wrapper.saveFullWidth({ nativeEvent: { layout: { width: 500 } } }); - wrapper.saveLabelStartWidth({ nativeEvent: { layout: { width: 20 } } }); - wrapper.saveLabelEndWidth({ nativeEvent: { layout: { width: 20 } } }); - - expect(wrapper.state.labelStartAtMax).toBe(false); - wrapper.setPaddingForOneLabel(); - expect(wrapper.state.labelStartAtMax).toBe(false); -}); - -it('startValue reaches max value', () => { - const wrapper = renderer - .create( - } - startValue={5} - />, - ) - .getInstance(); - - wrapper.saveFullWidth({ nativeEvent: { layout: { width: 500 } } }); - wrapper.saveLabelStartWidth({ nativeEvent: { layout: { width: 20 } } }); - - expect(wrapper.state.labelStartAtMax).toBe(false); - wrapper.setPaddingForOneLabel(); - expect(wrapper.state.labelStartAtMax).toBe(true); -}); - -it('startValue goes beyond max value', () => { - const wrapper = renderer - .create( - } - startValue={10} - />, - ) - .getInstance(); - - wrapper.saveFullWidth({ nativeEvent: { layout: { width: 500 } } }); - wrapper.saveLabelStartWidth({ nativeEvent: { layout: { width: 20 } } }); - - expect(wrapper.state.labelStartAtMax).toBe(false); - wrapper.setPaddingForOneLabel(); - expect(wrapper.state.labelStartAtMax).toBe(true); -}); - -it('Both sides have padding', () => { - const wrapper = renderer - .create( - } - startValue={8} - endLabel={} - endValue={16} - />, - ) - .getInstance(); - - wrapper.saveFullWidth({ nativeEvent: { layout: { width: 500 } } }); - wrapper.saveLabelStartWidth({ nativeEvent: { layout: { width: 20 } } }); - wrapper.saveLabelEndWidth({ nativeEvent: { layout: { width: 20 } } }); - - wrapper.setPaddingForTwoLabels(); - expect(wrapper.state.paddingLeft).not.toBe(0); - expect(wrapper.state.paddingRight).not.toBe(0); -}); - -it('No padding should be set', () => { - const wrapper = renderer - .create( - } - startValue={1} - endLabel={} - endValue={100} - />, - ) - .getInstance(); - - wrapper.saveFullWidth({ nativeEvent: { layout: { width: 500 } } }); - wrapper.saveLabelStartWidth({ nativeEvent: { layout: { width: 20 } } }); - wrapper.saveLabelEndWidth({ nativeEvent: { layout: { width: 20 } } }); - - wrapper.setPaddingForTwoLabels(); - expect(wrapper.state.paddingLeft).toBe(0); - expect(wrapper.state.paddingRight).toBe(0); -}); diff --git a/packages/shared/src/forms/SliderLabels.js b/packages/shared/src/forms/SliderLabels.js new file mode 100644 index 00000000..eba40788 --- /dev/null +++ b/packages/shared/src/forms/SliderLabels.js @@ -0,0 +1,105 @@ +// @flow + +import * as React from 'react'; +import { defaultTokens } from '@kiwicom/mobile-orbit'; +import { View } from 'react-native'; + +import type { OnLayout } from '../../types/Events'; +import Text from '../Text'; +import StyleSheet from '../PlatformStyleSheet'; +import Price from '../Price'; +import type { TranslationType } from '../../types/Translation'; +import useCalculateSliderPositions from './useCalculateSliderPositions'; + +type Props = {| + +startLabel: TranslationType | React.Element, + +startValue: number, + +endLabel?: TranslationType | React.Element, + +endValue?: number, + +max: number, + +min: number, +|}; + +export default function SliderLabels(props: Props) { + const [width, setWidth] = React.useState(0); + const [labelStartWidth, setLabelStartWidth] = React.useState(0); + const [labelStartAtMax, setLabelStartAtMax] = React.useState(false); + const [labelEndWidth, setLabelEndWidth] = React.useState(0); + const [paddingLeft, setPaddingLeft] = React.useState(0); + const [paddingRight, setPaddingRight] = React.useState(0); + + useCalculateSliderPositions({ + labelEndWidth, + labelStartAtMax, + labelStartWidth, + paddingLeft, + paddingRight, + props, + width, + setPaddingLeft, + setLabelStartAtMax, + setPaddingRight, + min: props.min, + max: props.max, + endValue: props.endValue, + startValue: props.startValue, + }); + + function saveFullWidth(e: OnLayout) { + setWidth(Math.floor(e.nativeEvent.layout.width)); + } + + function saveLabelStartWidth(e: OnLayout) { + setLabelStartWidth(Math.floor(e.nativeEvent.layout.width)); + } + + function saveLabelEndWidth(e: OnLayout) { + setLabelEndWidth(Math.floor(e.nativeEvent.layout.width)); + } + + return ( + + + {props.startLabel} + + {props.endLabel && ( + + {props.endLabel} + + )} + + ); +} + +const styles = StyleSheet.create({ + sliderLabels: { + width: '100%', + flexDirection: 'row', + marginTop: 10, + marginBottom: 5, + justifyContent: 'space-between', + }, + sliderLabelsEnd: { + justifyContent: 'flex-end', + }, + label: { + fontSize: 14, + color: defaultTokens.paletteBlueNormal, + }, +}); diff --git a/packages/shared/src/forms/__tests__/SliderLabels.test.js b/packages/shared/src/forms/__tests__/SliderLabels.test.js new file mode 100644 index 00000000..fa394c3e --- /dev/null +++ b/packages/shared/src/forms/__tests__/SliderLabels.test.js @@ -0,0 +1,179 @@ +// @flow strict + +import * as React from 'react'; +import renderer from 'react-test-renderer'; +import ShallowRenderer from 'react-test-renderer/shallow'; + +import Translation from '../../Translation'; +import SliderLabels from '../SliderLabels'; + +const shallowRenderer = new ShallowRenderer(); + +it('renders with one label', () => { + expect( + shallowRenderer.render( + } + startValue={46} + max={1000} + min={1} + />, + ), + ).toMatchSnapshot(); +}); + +it('renders with two labels', () => { + expect( + shallowRenderer.render( + } + startValue={46} + endLabel={} + endValue={850} + max={1000} + min={1} + />, + ), + ).toMatchSnapshot(); +}); + +it('should have startValue in the middle', () => { + const wrapper = renderer.create( + } + startValue={3} + />, + ); + const viewContainer = wrapper.root.findByProps({ + testID: 'sliderLabelsContainer', + }); + const startLabel = wrapper.root.findByProps({ + testID: 'startLabelContainer', + }); + + renderer.act(() => { + viewContainer.props.onLayout({ + nativeEvent: { layout: { width: 500 } }, + }); + startLabel.props.onLayout({ nativeEvent: { layout: { width: 20 } } }); + }); + + expect(startLabel.props.style).toMatchInlineSnapshot(` + Object { + "transform": Array [ + Object { + "translateX": 365, + }, + ], + } + `); +}); + +it('should display the label at the end', () => { + const wrapper = renderer.create( + } + startValue={5} + />, + ); + + const viewContainer = wrapper.root.findByProps({ + testID: 'sliderLabelsContainer', + }); + const startLabel = wrapper.root.findByProps({ + testID: 'startLabelContainer', + }); + + renderer.act(() => { + viewContainer.props.onLayout({ + nativeEvent: { layout: { width: 500 } }, + }); + startLabel.props.onLayout({ nativeEvent: { layout: { width: 20 } } }); + }); + + expect(viewContainer.props.style[1].justifyContent).toBe('flex-end'); +}); + +it('should have translateX on both labels', () => { + const wrapper = renderer.create( + } + startValue={8} + endLabel={} + endValue={16} + />, + ); + + const viewContainer = wrapper.root.findByProps({ + testID: 'sliderLabelsContainer', + }); + const startLabel = wrapper.root.findByProps({ + testID: 'startLabelContainer', + }); + const endLabel = wrapper.root.findByProps({ + testID: 'endLabelContainer', + }); + + renderer.act(() => { + viewContainer.props.onLayout({ nativeEvent: { layout: { width: 500 } } }); + startLabel.props.onLayout({ nativeEvent: { layout: { width: 20 } } }); + endLabel.props.onLayout({ nativeEvent: { layout: { width: 20 } } }); + }); + + expect(startLabel.props.style).toMatchInlineSnapshot(` + Object { + "transform": Array [ + Object { + "translateX": 201, + }, + ], + } + `); + expect(endLabel.props.style).toMatchInlineSnapshot(` + Object { + "transform": Array [ + Object { + "translateX": -69, + }, + ], + } + `); +}); + +it('should have no translateX', () => { + const wrapper = renderer.create( + } + startValue={1} + endLabel={} + endValue={100} + />, + ); + + const viewContainer = wrapper.root.findByProps({ + testID: 'sliderLabelsContainer', + }); + const startLabel = wrapper.root.findByProps({ + testID: 'startLabelContainer', + }); + const endLabel = wrapper.root.findByProps({ + testID: 'endLabelContainer', + }); + + renderer.act(() => { + viewContainer.props.onLayout({ nativeEvent: { layout: { width: 500 } } }); + startLabel.props.onLayout({ nativeEvent: { layout: { width: 20 } } }); + endLabel.props.onLayout({ nativeEvent: { layout: { width: 20 } } }); + }); + + expect(startLabel.props.style.transform[0].translateX).toBe(0); + expect(endLabel.props.style.transform[0].translateX).toBe(-0); +}); diff --git a/packages/shared/src/__tests__/__snapshots__/SliderLabels.test.js.snap b/packages/shared/src/forms/__tests__/__snapshots__/SliderLabels.test.js.snap similarity index 66% rename from packages/shared/src/__tests__/__snapshots__/SliderLabels.test.js.snap rename to packages/shared/src/forms/__tests__/__snapshots__/SliderLabels.test.js.snap index b774908f..23e75023 100644 --- a/packages/shared/src/__tests__/__snapshots__/SliderLabels.test.js.snap +++ b/packages/shared/src/forms/__tests__/__snapshots__/SliderLabels.test.js.snap @@ -1,26 +1,34 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`one label 1`] = ` +exports[`renders with one label 1`] = ` `; -exports[`two labels 1`] = ` +exports[`renders with two labels 1`] = ` void, + +setLabelStartAtMax: boolean => void, + +setPaddingRight: number => void, + +min: number, + +max: number, + +endValue: ?number, + +startValue: number, +|}; + +export default function useCalculateSliderPositions({ + labelEndWidth, + labelStartAtMax, + labelStartWidth, + paddingLeft, + paddingRight, + props, + width, + setPaddingLeft, + setLabelStartAtMax, + setPaddingRight, + min, + max, + endValue, + startValue, +}: Props) { + React.useEffect(() => { + function getOffset(input: number) { + let val; + + if (input > max) { + val = max; + } else if (input < min) { + val = min; + } else { + val = input; + } + return Math.round((val / (max - min)) * width); + } + + function calculateMarkerEndOffset(): number { + if (!endValue) { + return 0; + } + + const offset = getOffset(endValue); + return Math.round(width - offset); + } + + function calculateMarkerStartOffset(): number { + return getOffset(startValue); + } + + function isBelowMaxPadding(value: number, gap: number = 0): boolean { + const maxPadding = getMaxPadding(gap); + return value + gap < maxPadding; + } + + function setPaddingForOneLabel(): void { + const startLabelOffset = getStartLabelOffset(); + const hasOffsetChanged = paddingLeft !== startLabelOffset; + + if (isBelowMaxPadding(startLabelOffset)) { + if (hasOffsetChanged) { + setPaddingLeft(startLabelOffset); + } + + if (labelStartAtMax) { + setLabelStartAtMax(false); + } + } else if (labelStartAtMax === false) { + setLabelStartAtMax(true); + } + } + + function getMaxPadding(gap: number): number { + return Math.floor(width - labelStartWidth - labelEndWidth - gap); + } + + function getStartLabelOffset(): number { + const startMarkerOffset = calculateMarkerStartOffset(); + const startLabelHalf = labelStartWidth / 2; + + return startMarkerOffset < startLabelHalf + ? 0 + : startMarkerOffset - startLabelHalf; + } + + function setPaddingForTwoLabels(): void { + const startLabelOffset = getStartLabelOffset(); + const endMarkerOffset = calculateMarkerEndOffset(); + const endLabelHalf = labelEndWidth / 2; + const endLabelOffset = + endMarkerOffset < endLabelHalf ? 0 : endMarkerOffset - endLabelHalf; + + const hasOffsetChanged = + paddingLeft !== startLabelOffset || paddingRight !== endLabelOffset; + + if ( + isBelowMaxPadding(startLabelOffset + endLabelOffset, 10) && + hasOffsetChanged + ) { + setPaddingLeft(startLabelOffset); + setPaddingRight(endLabelOffset); + } + } + + if (props.endValue) { + setPaddingForTwoLabels(); + } else { + setPaddingForOneLabel(); + } + }, [ + endValue, + labelEndWidth, + labelStartAtMax, + labelStartWidth, + max, + min, + paddingLeft, + paddingRight, + props, + setLabelStartAtMax, + setPaddingLeft, + setPaddingRight, + startValue, + width, + ]); +}