Skip to content

Commit ba6b9c4

Browse files
authoredDec 26, 2024··
feat(Slider): fix rtl view (#8098)
1 parent b658bd5 commit ba6b9c4

15 files changed

+153
-36
lines changed
 

‎packages/vkui/src/components/Slider/Slider.e2e-playground.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,17 @@ export const SliderPlayground = (props: ComponentPlaygroundProps) => {
1515
min: [-10],
1616
max: [10],
1717
value: [0],
18+
dir: ['ltr', 'rtl'],
1819
},
1920
{
2021
multiple: [true],
2122
defaultValue: [[20, 80]],
2223
},
24+
{
25+
multiple: [true],
26+
defaultValue: [[30, 90]],
27+
dir: ['rtl'],
28+
},
2329
{
2430
defaultValue: [50],
2531
$adaptivity: 'y',

‎packages/vkui/src/components/Slider/Slider.module.css

+22-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
position: relative;
33
cursor: pointer;
44
block-size: var(--vkui_internal--slider_thumb_size);
5+
6+
--vkui_internal--Slider_start_value: 0;
7+
--vkui_internal--Slider_end_value: 0;
58
}
69

710
.disabled {
@@ -24,8 +27,14 @@
2427
}
2528

2629
.trackFill {
27-
inline-size: auto;
2830
background: var(--vkui--color_background_accent);
31+
inline-size: calc(var(--vkui_internal--Slider_start_value) * 1%);
32+
}
33+
34+
.multiple .trackFill {
35+
inline-size: auto;
36+
inset-inline-start: calc(var(--vkui_internal--Slider_start_value) * 1%);
37+
inset-inline-end: calc((100 - var(--vkui_internal--Slider_end_value)) * 1%);
2938
}
3039

3140
.thumbs {
@@ -40,6 +49,18 @@
4049
transform: translate(-50%, -50%);
4150
}
4251

52+
.rtl .thumb {
53+
transform: translate(50%, -50%);
54+
}
55+
56+
.thumbStart {
57+
inset-inline-start: calc(var(--vkui_internal--Slider_start_value) * 1%);
58+
}
59+
60+
.thumbEnd {
61+
inset-inline-start: calc(var(--vkui_internal--Slider_end_value) * 1%);
62+
}
63+
4364
.sizeL {
4465
--vkui_internal--slider_thumb_size: 28px;
4566
}

‎packages/vkui/src/components/Slider/Slider.test.tsx

+62
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
baselineComponent,
88
fakeTimers,
99
mockRect,
10+
mockRtlDirection,
1011
userEvent,
1112
waitForFloatingPosition,
1213
} from '../../testing/utils';
@@ -239,6 +240,67 @@ describe(Slider, () => {
239240
});
240241
});
241242

243+
describe('check rtl view', () => {
244+
mockRtlDirection();
245+
246+
it('moves start', async () => {
247+
render(<Slider />);
248+
const slider = screen.getByRole('slider');
249+
250+
fireEvent.mouseDown(slider);
251+
252+
fireEvent.mouseMove(slider, pointerPos(40));
253+
expect(slider).toHaveValue('60');
254+
255+
fireEvent.mouseMove(slider, pointerPos(50));
256+
expect(slider).toHaveValue('50');
257+
258+
fireEvent.mouseUp(slider);
259+
});
260+
261+
it('moves start (multiple)', async () => {
262+
render(
263+
<Slider
264+
multiple
265+
defaultValue={[30, 70]}
266+
startThumbTestId="startSlider"
267+
endThumbTestId="endSlider"
268+
/>,
269+
);
270+
const startSlider = screen.getByTestId('startSlider');
271+
const endSlider = screen.getByTestId('endSlider');
272+
273+
fireEvent.mouseDown(startSlider);
274+
275+
fireEvent.mouseMove(startSlider, pointerPos(40));
276+
expect(startSlider).toHaveValue('60');
277+
expect(endSlider).toHaveValue('70');
278+
279+
fireEvent.mouseMove(startSlider, pointerPos(50));
280+
expect(startSlider).toHaveValue('50');
281+
expect(endSlider).toHaveValue('70');
282+
283+
fireEvent.mouseUp(startSlider);
284+
});
285+
286+
it('moves end (multiple)', async () => {
287+
render(<Slider multiple defaultValue={[30, 70]} />);
288+
const [startSlider, endSlider] = screen.getAllByRole('slider');
289+
290+
fireEvent.mouseDown(endSlider);
291+
292+
fireEvent.mouseMove(endSlider, pointerPos(40));
293+
expect(startSlider).toHaveValue('30');
294+
expect(endSlider).toHaveValue('60');
295+
296+
fireEvent.mouseMove(endSlider, pointerPos(50));
297+
expect(startSlider).toHaveValue('30');
298+
expect(endSlider).toHaveValue('50');
299+
300+
fireEvent.mouseUp(endSlider);
301+
});
302+
});
303+
242304
describe('with tooltip', () => {
243305
fakeTimers();
244306

‎packages/vkui/src/components/Slider/Slider.tsx

+23-15
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import * as React from 'react';
44
import { classNames } from '@vkontakte/vkjs';
55
import { clamp } from '../../helpers/math';
6+
import { mergeStyle } from '../../helpers/mergeStyle';
67
import { useAdaptivity } from '../../hooks/useAdaptivity';
8+
import { useDirection } from '../../hooks/useDirection';
79
import { useExternRef } from '../../hooks/useExternRef';
8-
import type { HTMLAttributesWithRootRef } from '../../types';
10+
import type { CSSCustomProperties, HTMLAttributesWithRootRef } from '../../types';
911
import { type CustomTouchEvent, type CustomTouchEventHandler, Touch } from '../Touch/Touch';
1012
import { SliderThumb } from './SliderThumb/SliderThumb';
1113
import {
@@ -101,9 +103,12 @@ export const Slider = ({
101103
onChange,
102104
withTooltip,
103105
size = 'l',
106+
style: styleProp,
104107
...restProps
105108
}: SliderProps | SliderMultipleProps): React.ReactNode => {
106109
const { sizeY = 'none' } = useAdaptivity();
110+
const [directionRef, textDirection = 'ltr'] = useDirection();
111+
const isRtl = textDirection === 'rtl';
107112

108113
const isControlled = valueProp !== undefined;
109114
const [localValue, setValue] = React.useState(defaultValue);
@@ -163,7 +168,10 @@ export const Slider = ({
163168
// @ts-expect-error: TS2345 в VKUITouchEvent плохо описаны типы. `target` это просто `EventTarget`.
164169
const foundDraggingType = getDraggingTypeByTargetDataset(event.originalEvent.target);
165170

166-
const nextStartX = event.startX - nextContainerX;
171+
let nextStartX = event.startX - nextContainerX;
172+
if (isRtl) {
173+
nextStartX = nextContainerWidth - nextStartX;
174+
}
167175
const nextValue = offsetToValue(nextStartX, nextContainerWidth, min, max, step);
168176
const nextDragging = snapDirection(value, nextValue, foundDraggingType);
169177

@@ -205,7 +213,7 @@ export const Slider = ({
205213
const { startX, containerWidth, dragging } = gesture;
206214

207215
const { shiftX = 0 } = event;
208-
const nextStartX = startX + shiftX;
216+
const nextStartX = startX + (isRtl ? -shiftX : shiftX);
209217
const nextValue = offsetToValue(nextStartX, containerWidth, min, max, step);
210218

211219
changeValue(updateInternalStateValue(value, nextValue, min, max, dragging), event);
@@ -231,6 +239,11 @@ export const Slider = ({
231239
);
232240
};
233241

242+
const style: CSSCustomProperties = {
243+
'--vkui_internal--Slider_start_value': String(startValueInPercent),
244+
'--vkui_internal--Slider_end_value': String(endReversedValueInPercent),
245+
};
246+
234247
return (
235248
<Touch
236249
data-value={multiple ? `${startValue},${endValue}` : startValue}
@@ -240,27 +253,23 @@ export const Slider = ({
240253
disabled && styles.disabled,
241254
sizeY !== 'regular' && sizeYClassNames[sizeY],
242255
sizeClassNames[size],
256+
multiple && styles.multiple,
257+
isRtl && styles.rtl,
243258
className,
244259
)}
260+
style={mergeStyle(styleProp, style)}
261+
getRootRef={directionRef}
245262
onStart={disabled ? undefined : handlePointerStart}
246263
onMove={disabled ? undefined : handlePointerMove}
247264
onEnd={disabled ? undefined : handlePointerEnd}
248265
>
249266
<div className={styles.track} />
250-
<div
251-
className={styles.trackFill}
252-
style={
253-
multiple
254-
? { left: `${startValueInPercent}%`, right: `${100 - endReversedValueInPercent}%` }
255-
: { width: `${startValueInPercent}%` }
256-
}
257-
/>
267+
<div className={styles.trackFill} />
258268
<div ref={thumbsContainerRef} className={styles.thumbs}>
259269
<SliderThumb
260270
data-type="start"
261-
className={styles.thumb}
271+
className={classNames(styles.thumb, styles.thumbStart)}
262272
style={{
263-
left: `${startValueInPercent}%`,
264273
// Меняем местами порядок слоёв, иначе, при достижении `start` и `end` 100%, `end` будет перекрывать `start`.
265274
zIndex: multiple && startValueInPercent >= 50 ? 2 : undefined,
266275
}}
@@ -284,8 +293,7 @@ export const Slider = ({
284293
{multiple && (
285294
<SliderThumb
286295
data-type="end"
287-
className={styles.thumb}
288-
style={{ left: `${endReversedValueInPercent}%` }}
296+
className={classNames(styles.thumb, styles.thumbEnd)}
289297
withTooltip={withTooltip}
290298
inputProps={{
291299
'data-type': 'end',

‎packages/vkui/src/testing/utils.tsx

+20
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,26 @@ export function fakeTimers() {
4242
});
4343
}
4444

45+
export function mockRtlDirection() {
46+
const originalGetComputedStyle = window.getComputedStyle;
47+
48+
let getComputedStyleMock: ReturnType<typeof jest.spyOn> | null = null;
49+
beforeEach(() => {
50+
/**
51+
* Мокаем получение direction
52+
*/
53+
getComputedStyleMock = jest.spyOn(window, 'getComputedStyle').mockImplementation((e) => {
54+
return {
55+
...originalGetComputedStyle(e),
56+
direction: 'rtl',
57+
};
58+
});
59+
});
60+
afterEach(() => {
61+
getComputedStyleMock.mockRestore();
62+
});
63+
}
64+
4565
export const imgOnlyAttributes: ImgOnlyAttributes = {
4666
alt: 'test',
4767
crossOrigin: 'anonymous',

0 commit comments

Comments
 (0)
Please sign in to comment.