Skip to content

Commit 4502472

Browse files
#RI-2763-add common Range Filter, add tests (#661)
* #RI-2763-add common Range Filter, add tests
1 parent ad9f3a4 commit 4502472

File tree

7 files changed

+419
-324
lines changed

7 files changed

+419
-324
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import React from 'react'
2+
import { instance, mock } from 'ts-mockito'
3+
import { render, screen, fireEvent } from 'uiSrc/utils/test-utils'
4+
import RangeFilter, { Props } from './RangeFilter'
5+
6+
const mockedProps = mock<Props>()
7+
8+
const startRangeTestId = 'range-start-input'
9+
const endRangeTestId = 'range-end-input'
10+
const resetBtnTestId = 'range-filter-btn'
11+
12+
describe('RangeFilter', () => {
13+
it('should render', () => {
14+
expect(render(<RangeFilter {...instance(mockedProps)} />)).toBeTruthy()
15+
})
16+
17+
it('should call handleChangeStart onChange start range thumb', () => {
18+
const handleChangeStart = jest.fn()
19+
render(
20+
<RangeFilter
21+
{...instance(mockedProps)}
22+
handleChangeStart={handleChangeStart}
23+
start={1}
24+
end={1000}
25+
/>
26+
)
27+
const startRangeInput = screen.getByTestId(startRangeTestId)
28+
29+
fireEvent.change(
30+
startRangeInput,
31+
{ target: { value: 123 } }
32+
)
33+
expect(handleChangeStart).toBeCalledTimes(1)
34+
})
35+
36+
it('should call handleChangeEnd onChange end range thumb', () => {
37+
const handleChangeEnd = jest.fn()
38+
render(
39+
<RangeFilter
40+
{...instance(mockedProps)}
41+
handleChangeEnd={handleChangeEnd}
42+
start={1}
43+
end={100}
44+
/>
45+
)
46+
const endRangeInput = screen.getByTestId(endRangeTestId)
47+
48+
fireEvent.change(
49+
endRangeInput,
50+
{ target: { value: 15 } }
51+
)
52+
expect(handleChangeEnd).toBeCalledTimes(1)
53+
})
54+
it('should reset start and end values on press Reset buttons', () => {
55+
const handleChangeEnd = jest.fn()
56+
const handleChangeStart = jest.fn()
57+
58+
render(
59+
<RangeFilter
60+
{...instance(mockedProps)}
61+
handleChangeStart={handleChangeStart}
62+
handleChangeEnd={handleChangeEnd}
63+
start={1}
64+
end={100}
65+
min={1}
66+
max={120}
67+
/>
68+
)
69+
const resetBtn = screen.getByTestId(resetBtnTestId)
70+
71+
fireEvent.click(resetBtn)
72+
73+
expect(handleChangeEnd).toBeCalledWith(120)
74+
expect(handleChangeStart).toBeCalledWith(1)
75+
})
76+
})
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import React, { useCallback, useEffect, useRef } from 'react'
2+
import cx from 'classnames'
3+
4+
import { getFormatTime, } from 'uiSrc/utils/streamUtils'
5+
6+
import styles from './styles.module.scss'
7+
8+
const buttonString = 'Reset Filter'
9+
10+
export interface Props {
11+
max: number
12+
min: number
13+
start: number
14+
end: number
15+
handleChangeStart: (value: number) => void
16+
handleChangeEnd: (value: number) => void
17+
}
18+
19+
function usePrevious(value: any) {
20+
const ref = useRef()
21+
useEffect(() => {
22+
ref.current = value
23+
})
24+
return ref.current
25+
}
26+
27+
const RangeFilter = (props: Props) => {
28+
const { max, min, start, end, handleChangeStart, handleChangeEnd } = props
29+
30+
const getPercent = useCallback(
31+
(value) => Math.round(((value - min) / (max - min)) * 100),
32+
[min, max]
33+
)
34+
35+
const minValRef = useRef<HTMLInputElement>(null)
36+
const maxValRef = useRef<HTMLInputElement>(null)
37+
const range = useRef<HTMLInputElement>(null)
38+
39+
const prevValue = usePrevious({ max }) ?? { max: 0 }
40+
41+
const resetFilter = useCallback(
42+
() => {
43+
handleChangeStart(min)
44+
handleChangeEnd(max)
45+
},
46+
[min, max]
47+
)
48+
49+
const onChangeStart = useCallback(
50+
(event) => {
51+
const value = Math.min(+event.target.value, end - 1)
52+
handleChangeStart(value)
53+
},
54+
[end]
55+
)
56+
57+
const onChangeEnd = useCallback(
58+
(event) => {
59+
const value = Math.max(+event.target.value, start + 1)
60+
handleChangeEnd(value)
61+
},
62+
[start]
63+
)
64+
65+
useEffect(() => {
66+
if (maxValRef.current) {
67+
const minPercent = getPercent(start)
68+
const maxPercent = getPercent(+maxValRef.current.value)
69+
70+
if (range.current) {
71+
range.current.style.left = `${minPercent}%`
72+
range.current.style.width = `${maxPercent - minPercent}%`
73+
}
74+
}
75+
}, [start, getPercent])
76+
77+
useEffect(() => {
78+
if (minValRef.current) {
79+
const minPercent = getPercent(+minValRef.current.value)
80+
const maxPercent = getPercent(end)
81+
82+
if (range.current) {
83+
range.current.style.width = `${maxPercent - minPercent}%`
84+
}
85+
}
86+
}, [end, getPercent])
87+
88+
useEffect(() => {
89+
if (max && prevValue && prevValue.max !== max && end === prevValue.max) {
90+
handleChangeEnd(max)
91+
}
92+
}, [prevValue])
93+
94+
if (start === 0 && end === 0) {
95+
return (
96+
<div data-testid="mock-blank-range" className={styles.rangeWrapper}>
97+
<div className={cx(styles.sliderTrack, styles.mockRange)} />
98+
</div>
99+
)
100+
}
101+
102+
if (start === end) {
103+
return (
104+
<div data-testid="mock-fill-range" className={styles.rangeWrapper}>
105+
<div className={cx(styles.sliderRange, styles.mockRange)}>
106+
<div className={styles.sliderLeftValue}>{getFormatTime(start?.toString())}</div>
107+
<div className={styles.sliderRightValue}>{getFormatTime(end?.toString())}</div>
108+
</div>
109+
</div>
110+
)
111+
}
112+
113+
return (
114+
<>
115+
<div className={styles.rangeWrapper}>
116+
<input
117+
type="range"
118+
min={min}
119+
max={max}
120+
value={start}
121+
ref={minValRef}
122+
onChange={onChangeStart}
123+
className={cx(styles.thumb, styles.thumbZindex3)}
124+
data-testid="range-start-input"
125+
/>
126+
<input
127+
type="range"
128+
min={min}
129+
max={max}
130+
value={end}
131+
ref={maxValRef}
132+
onChange={onChangeEnd}
133+
className={cx(styles.thumb, styles.thumbZindex4)}
134+
data-testid="range-end-input"
135+
/>
136+
<div className={styles.slider}>
137+
<div className={styles.sliderTrack} />
138+
<div ref={range} className={styles.sliderRange}>
139+
<div className={styles.sliderLeftValue}>{getFormatTime(start?.toString())}</div>
140+
<div className={styles.sliderRightValue}>{getFormatTime(end?.toString())}</div>
141+
</div>
142+
</div>
143+
</div>
144+
{(start !== min || end !== max) && (
145+
<button
146+
data-testid="range-filter-btn"
147+
className={styles.resetButton}
148+
type="button"
149+
onClick={resetFilter}
150+
>
151+
{buttonString}
152+
</button>
153+
)}
154+
</>
155+
)
156+
}
157+
158+
export default RangeFilter
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
.rangeWrapper {
2+
margin: 30px 30px 26px;
3+
padding: 12px 0;
4+
}
5+
6+
.resetButton {
7+
position: absolute;
8+
right: 30px;
9+
top: 80px;
10+
z-index: 10;
11+
text-decoration: underline;
12+
color: var(--euiTextSubduedColor);
13+
&:hover,
14+
&:focus {
15+
color: var(--euiTextColor);
16+
}
17+
font: normal normal 500 13px/18px Graphik, sans-serif;
18+
}
19+
20+
.slider {
21+
position: relative;
22+
width: 100%;
23+
}
24+
25+
.sliderTrack,
26+
.sliderRange,
27+
.sliderLeftValue,
28+
.sliderRightValue {
29+
position: absolute;
30+
}
31+
32+
.sliderTrack {
33+
background-color: var(--separatorColor);
34+
width: 100%;
35+
height: 1px;
36+
margin-top: 2px;
37+
z-index: 1;
38+
}
39+
40+
.sliderRange {
41+
height: 5px;
42+
background-color: var(--euiColorPrimary);
43+
z-index: 2;
44+
}
45+
46+
.sliderLeftValue,
47+
.sliderRightValue {
48+
width: max-content;
49+
color: var(--euiColorMediumShade);
50+
font: normal normal normal 12px/18px Graphik;
51+
margin-top: 20px;
52+
}
53+
54+
.sliderLeftValue {
55+
left: 0;
56+
margin-top: -30px;
57+
}
58+
59+
.sliderRightValue {
60+
right: -4px;
61+
}
62+
63+
.mockRange {
64+
left: 30px;
65+
width: calc(100% - 56px);
66+
}
67+
68+
.thumb,
69+
.thumb::-webkit-slider-thumb {
70+
-webkit-appearance: none;
71+
-webkit-tap-highlight-color: transparent;
72+
}
73+
74+
.thumb {
75+
pointer-events: none;
76+
position: absolute;
77+
height: 0;
78+
width: calc(100% - 60px);
79+
outline: none;
80+
}
81+
82+
.thumbZindex3 {
83+
z-index: 3;
84+
}
85+
86+
.thumbZindex4 {
87+
z-index: 4;
88+
}
89+
90+
.thumb::-moz-range-thumb {
91+
width: 2px;
92+
height: 12px;
93+
background-color: var(--euiColorPrimary);
94+
border: none;
95+
cursor: pointer;
96+
margin-top: 4px;
97+
pointer-events: all;
98+
position: relative;
99+
}
100+
101+
.thumbZindex3::-moz-range-thumb {
102+
transform: translateY(-4px);
103+
}
104+
105+
.thumbZindex4::-moz-range-thumb {
106+
transform: translateY(8px);
107+
}
108+
109+
input[type='range']::-webkit-slider-thumb {
110+
width: 2px;
111+
height: 12px;
112+
background-color: var(--euiColorPrimary);
113+
border: none;
114+
cursor: pointer;
115+
margin-top: 4px;
116+
pointer-events: all;
117+
position: relative;
118+
}
119+
120+
input[type='range']:first-child::-webkit-slider-thumb {
121+
transform: translateY(-4px);
122+
}
123+
124+
input[type='range']:last-of-type::-webkit-slider-thumb {
125+
transform: translateY(8px);
126+
}

0 commit comments

Comments
 (0)