diff --git a/packages/vkui/src/components/SegmentedControl/SegmentedControl.e2e-playground.tsx b/packages/vkui/src/components/SegmentedControl/SegmentedControl.e2e-playground.tsx index e32674ccb4..c15d130387 100644 --- a/packages/vkui/src/components/SegmentedControl/SegmentedControl.e2e-playground.tsx +++ b/packages/vkui/src/components/SegmentedControl/SegmentedControl.e2e-playground.tsx @@ -38,6 +38,8 @@ export const SegmentedControlPlayground = (props: ComponentPlaygroundProps) => { { label: 'google', before: , value: 'google' }, ], ], + defaultValue: ['fb'], + dir: ['ltr', 'rtl'], }, ]} > diff --git a/packages/vkui/src/components/SegmentedControl/SegmentedControl.module.css b/packages/vkui/src/components/SegmentedControl/SegmentedControl.module.css index 72ea240282..28c91ca555 100644 --- a/packages/vkui/src/components/SegmentedControl/SegmentedControl.module.css +++ b/packages/vkui/src/components/SegmentedControl/SegmentedControl.module.css @@ -32,6 +32,15 @@ inset 0 0 0 0.5px var(--vkui--color_image_border_alpha), 0 3px 8px rgba(0, 0, 0, 0.12), 0 3px 1px rgba(0, 0, 0, 0.04); + inline-size: calc(100% / var(--vkui_internal--SegmentedControl_options)); + transform: translateX(calc(var(--vkui_internal--SegmentedControl_actual_index) * 100%)); + + --vkui_internal--SegmentedControl_actual_index: 0; + --vkui_internal--SegmentedControl_options: 0; +} + +.rtl .slider { + transform: translateX(calc(-1 * var(--vkui_internal--SegmentedControl_actual_index) * 100%)); } /** diff --git a/packages/vkui/src/components/SegmentedControl/SegmentedControl.stories.tsx b/packages/vkui/src/components/SegmentedControl/SegmentedControl.stories.tsx index a3389411eb..87e309c0d0 100644 --- a/packages/vkui/src/components/SegmentedControl/SegmentedControl.stories.tsx +++ b/packages/vkui/src/components/SegmentedControl/SegmentedControl.stories.tsx @@ -8,6 +8,12 @@ const story: Meta = { component: SegmentedControl, parameters: { ...CanvasFullLayout, ...DisableCartesianParam }, args: { onChange: fn() }, + argTypes: { + role: { + control: 'select', + options: ['radiogroup', 'tablist'], + }, + }, }; export default story; @@ -30,6 +36,7 @@ export const Playground: Story = { value: 'other', }, ], + role: 'radiogroup', }, decorators: [ (Component) => ( diff --git a/packages/vkui/src/components/SegmentedControl/SegmentedControl.test.tsx b/packages/vkui/src/components/SegmentedControl/SegmentedControl.test.tsx index 1295b9d644..9a126e8b58 100644 --- a/packages/vkui/src/components/SegmentedControl/SegmentedControl.test.tsx +++ b/packages/vkui/src/components/SegmentedControl/SegmentedControl.test.tsx @@ -1,12 +1,14 @@ import { useState } from 'react'; import { fireEvent, render, screen } from '@testing-library/react'; -import { baselineComponent } from '../../testing/utils'; +import { baselineComponent, mockRtlDirection } from '../../testing/utils'; import { SegmentedControl, type SegmentedControlOptionInterface, type SegmentedControlProps, type SegmentedControlValue, } from './SegmentedControl'; +import styles from './SegmentedControl.module.css'; + const ctrl = () => screen.getByTestId('ctrl'); const option = (idx = 0) => ctrl().querySelectorAll("input[type='radio']")[idx]; @@ -151,4 +153,41 @@ describe('SegmentedControl', () => { }); }); }); + + describe('check slide rendering', () => { + const options: SegmentedControlOptionInterface[] = [ + { label: 'vk', value: 'vk', id: 'vk' }, + { label: 'ok', value: 'ok', id: 'ok' }, + { label: 'fb', value: 'fb', id: 'fb' }, + ]; + + const SegmentedControlTabsTest = (props: Omit) => ( + + ); + + it('should use correct css variables', () => { + const { container } = render(); + const slider = container.getElementsByClassName(styles.slider)[0]; + expect(slider).toHaveStyle('--vkui_internal--SegmentedControl_actual_index: 1'); + expect(slider).toHaveStyle('--vkui_internal--SegmentedControl_options: 3'); + }); + }); + + describe('check rtl', () => { + mockRtlDirection(); + + it('check rtl', () => { + const options: SegmentedControlOptionInterface[] = [ + { label: 'vk', value: 'vk', id: 'vk' }, + { label: 'ok', value: 'ok', id: 'ok' }, + { label: 'fb', value: 'fb', id: 'fb' }, + ]; + + const SegmentedControlTabsTest = (props: Omit) => ( + + ); + render(); + expect(ctrl()).toHaveClass(styles.rtl); + }); + }); }); diff --git a/packages/vkui/src/components/SegmentedControl/SegmentedControl.tsx b/packages/vkui/src/components/SegmentedControl/SegmentedControl.tsx index 2e8b28eddf..0570823bde 100644 --- a/packages/vkui/src/components/SegmentedControl/SegmentedControl.tsx +++ b/packages/vkui/src/components/SegmentedControl/SegmentedControl.tsx @@ -3,11 +3,13 @@ import * as React from 'react'; import { classNames } from '@vkontakte/vkjs'; import { useAdaptivity } from '../../hooks/useAdaptivity'; +import { useDirection } from '../../hooks/useDirection'; import { useCustomEnsuredControl } from '../../hooks/useEnsuredControl'; +import { useExternRef } from '../../hooks/useExternRef'; import { useTabsNavigation } from '../../hooks/useTabsNavigation'; import { useIsomorphicLayoutEffect } from '../../lib/useIsomorphicLayoutEffect'; import { warnOnce } from '../../lib/warnOnce'; -import type { HTMLAttributesWithRootRef } from '../../types'; +import type { CSSCustomProperties, HTMLAttributesWithRootRef } from '../../types'; import { RootComponent } from '../RootComponent/RootComponent'; import { SegmentedControlOption, @@ -57,9 +59,13 @@ export const SegmentedControl = ({ onChange: onChangeProp, value: valueProp, role = 'radiogroup', + getRootRef, ...restProps }: SegmentedControlProps): React.ReactNode => { const id = React.useId(); + const [directionRef, textDirection = 'ltr'] = useDirection(); + const rootRef = useExternRef(getRootRef, directionRef); + const isRtl = textDirection === 'rtl'; const [value, onChange] = useCustomEnsuredControl({ onChange: onChangeProp, @@ -69,7 +75,7 @@ export const SegmentedControl = ({ const { sizeY = 'none' } = useAdaptivity(); - const { tabsRef } = useTabsNavigation(role === 'tablist'); + const { tabsRef } = useTabsNavigation(role === 'tablist', isRtl); const actualIndex = options.findIndex((option) => option.value === value); @@ -79,28 +85,24 @@ export const SegmentedControl = ({ } }, [actualIndex]); - const translateX = `translateX(${100 * actualIndex}%)`; + const sliderStyle: CSSCustomProperties = { + '--vkui_internal--SegmentedControl_actual_index': String(actualIndex), + '--vkui_internal--SegmentedControl_options': String(options.length), + }; return (
- {actualIndex > -1 && ( -
- )} + {actualIndex > -1 &&
} {options.map(({ label, before, ...optionProps }) => { const selected = value === optionProps.value; const onSelect = () => onChange(optionProps.value); diff --git a/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-android-chromium-dark-1-snap.png b/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-android-chromium-dark-1-snap.png index 33e7a6f043..82742e3b08 100644 --- a/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-android-chromium-dark-1-snap.png +++ b/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-android-chromium-dark-1-snap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:40fa9812d9488dc7a5ffc02d43451500a3dacbb7ab8075f9791e7f8cfd50d4d1 -size 70096 +oid sha256:f62c12fd445e9e3deded0e00313e2d15fdd87655bb58858e726b9d33639b5dbc +size 89992 diff --git a/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-android-chromium-light-1-snap.png b/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-android-chromium-light-1-snap.png index 6e9e3e8287..e93b503520 100644 --- a/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-android-chromium-light-1-snap.png +++ b/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-android-chromium-light-1-snap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20a99de966c2404f92e96286f0e7a7255fb0715459abecbcc74d79e40a5d0ba6 -size 65814 +oid sha256:1e5c47688a90d432b767c7bec74763a4d1f8dbb2fc9f83aa6590c58e932ee928 +size 84197 diff --git a/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-ios-webkit-dark-1-snap.png b/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-ios-webkit-dark-1-snap.png index 4fd40bac03..6243aa27c1 100644 --- a/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-ios-webkit-dark-1-snap.png +++ b/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-ios-webkit-dark-1-snap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:140efca7b54f3e7dd04393d7966eeb2f7d0855dca796e9bbd47aeb3bfe186490 -size 115562 +oid sha256:6ea088eac1260627d5cb9f00665ce1914127a9bc275862348385eeb2fadd49f9 +size 148794 diff --git a/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-ios-webkit-light-1-snap.png b/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-ios-webkit-light-1-snap.png index ac7b943c34..2967963a45 100644 --- a/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-ios-webkit-light-1-snap.png +++ b/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-ios-webkit-light-1-snap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59f554332929002daa8856662a1467585169627c0f88ebc047c8175514fac634 -size 115032 +oid sha256:d703ec7669bd97212786be8c2e8537bab7d837690e312681c810209842b8621d +size 148003 diff --git a/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-vkcom-chromium-dark-1-snap.png b/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-vkcom-chromium-dark-1-snap.png index e18a6066bf..d6b73c176d 100644 --- a/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-vkcom-chromium-dark-1-snap.png +++ b/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-vkcom-chromium-dark-1-snap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2099ff674d37b061a16284af7740fe0800a7c45808f66b2a870da247e0376723 -size 46089 +oid sha256:fd9002c8b542bf2852184441990e833f5eb401267df2acf3bd4a9d2449cb54a4 +size 61333 diff --git a/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-vkcom-chromium-light-1-snap.png b/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-vkcom-chromium-light-1-snap.png index 9333ed0ce5..3754317641 100644 --- a/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-vkcom-chromium-light-1-snap.png +++ b/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-vkcom-chromium-light-1-snap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c81f58191df4bf5092307eddc4219423e10b5ab0266c14a60ff00c7e0f39466 -size 46748 +oid sha256:b2f53b02fc5cbfd8658d2d9c76be6c8bb784107928c5611602170e6077391eaa +size 62128 diff --git a/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-vkcom-firefox-dark-1-snap.png b/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-vkcom-firefox-dark-1-snap.png index e8c577ec2a..1d27cad429 100644 --- a/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-vkcom-firefox-dark-1-snap.png +++ b/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-vkcom-firefox-dark-1-snap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f1f99f6e4ae9d8587e9fc3072f922876c45160f2d50eb58ac25435523753dbdf -size 83771 +oid sha256:f54bbdf49b8ac868d081fee7f78e68636f10708072d988049003d5b8647ca273 +size 110141 diff --git a/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-vkcom-firefox-light-1-snap.png b/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-vkcom-firefox-light-1-snap.png index 0ec2eba795..cf2fe23aa0 100644 --- a/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-vkcom-firefox-light-1-snap.png +++ b/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-vkcom-firefox-light-1-snap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78f6c02da52e55187089946ae20c830d780818d2226ccdc9e02294a7f63c5bef -size 87298 +oid sha256:da1baef1c0feb57b0038355d050299be93397f3e5193965259ba106f3fee6df9 +size 113537 diff --git a/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-vkcom-webkit-dark-1-snap.png b/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-vkcom-webkit-dark-1-snap.png index dbe6abf930..6acbd62b62 100644 --- a/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-vkcom-webkit-dark-1-snap.png +++ b/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-vkcom-webkit-dark-1-snap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d96a1d97bae6d57e8610e15a96f05326906c129191ace46f8ca876283b5a075a -size 84792 +oid sha256:5205c3aa18654c626eda395566ca018af7f7fe8ead3f7ba13060ecfe3329b263 +size 113079 diff --git a/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-vkcom-webkit-light-1-snap.png b/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-vkcom-webkit-light-1-snap.png index dc49e3bf73..b111c9db07 100644 --- a/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-vkcom-webkit-light-1-snap.png +++ b/packages/vkui/src/components/SegmentedControl/__image_snapshots__/segmentedcontrol-vkcom-webkit-light-1-snap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea44e3e7990dfb1ecc88aed39b382efcfba406f02e986160d79c644b3f27450b -size 86756 +oid sha256:c78d976b74a4d182d051ef70485467ac030510c520bb6e65c9409b46c7628a82 +size 115427 diff --git a/packages/vkui/src/components/Tabs/Tabs.tsx b/packages/vkui/src/components/Tabs/Tabs.tsx index f639e0aba2..53cba6cd30 100644 --- a/packages/vkui/src/components/Tabs/Tabs.tsx +++ b/packages/vkui/src/components/Tabs/Tabs.tsx @@ -2,6 +2,8 @@ import * as React from 'react'; import { classNames } from '@vkontakte/vkjs'; +import { useDirection } from '../../hooks/useDirection'; +import { useExternRef } from '../../hooks/useExternRef'; import { usePlatform } from '../../hooks/usePlatform'; import { useTabsNavigation } from '../../hooks/useTabsNavigation'; import type { HTMLAttributesWithRootRef } from '../../types'; @@ -58,17 +60,21 @@ export const Tabs = ({ withScrollToSelectedTab, scrollBehaviorToSelectedTab = 'nearest', layoutFillMode = 'auto', + getRootRef, ...restProps }: TabsProps): React.ReactNode => { const platform = usePlatform(); + const [directionRef, textDirection = 'ltr'] = useDirection(); + const rootRef = useExternRef(getRootRef, directionRef); const isTabFlow = role === 'tablist'; const withGaps = mode === 'accent' || mode === 'secondary'; - const { tabsRef } = useTabsNavigation(isTabFlow); + const { tabsRef } = useTabsNavigation(isTabFlow, textDirection === 'rtl'); return ( { expect(document.activeElement).toBe(tab1); expect(document.activeElement).not.toBe(tab2); }); + + it('should change active tab in rtl', () => { + const { tabs, tab2, tab1 } = setupTabs(); + const { result } = renderHook(() => useTabsNavigation(true, true)); + + (result.current.tabsRef as React.MutableRefObject).current = tabs; + tab1.focus(); + + fireEvent.keyDown(document, { key: 'ArrowLeft' }); + expect(document.activeElement).toBe(tab2); + + fireEvent.keyDown(document, { key: 'ArrowRight' }); + expect(document.activeElement).toBe(tab1); + }); }); diff --git a/packages/vkui/src/hooks/useTabsNavigation.ts b/packages/vkui/src/hooks/useTabsNavigation.ts index 022b3f6761..e06555ae12 100644 --- a/packages/vkui/src/hooks/useTabsNavigation.ts +++ b/packages/vkui/src/hooks/useTabsNavigation.ts @@ -3,7 +3,7 @@ import { pressedKey } from '../lib/accessibility'; import { useDOM } from '../lib/dom'; import { useGlobalEventListener } from './useGlobalEventListener'; -export function useTabsNavigation(enabled = true) { +export function useTabsNavigation(enabled = true, isRtl = false) { const { document } = useDOM(); const tabsRef = React.useRef(null); const getTabEls = () => { @@ -41,7 +41,7 @@ export function useTabsNavigation(enabled = true) { } else if (key === 'End') { nextIndex = tabEls.length - 1; } else { - const offset = key === 'ArrowRight' ? 1 : -1; + const offset = (key === 'ArrowRight' ? 1 : -1) * (isRtl ? -1 : 1); nextIndex = currentFocusedElIndex + offset; }