Skip to content

Commit

Permalink
feat(SegmentedControl): fix rtl view (#8092)
Browse files Browse the repository at this point in the history
* feat(SegmentedControl): fix rtl view

* feat(SegmentedControl): make calulating in css

* fix(useTabsNavigation): fix navigation by arrow in rtl
  • Loading branch information
EldarMuhamethanov authored Jan 15, 2025
1 parent 83d64a6 commit 02d004d
Show file tree
Hide file tree
Showing 18 changed files with 116 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export const SegmentedControlPlayground = (props: ComponentPlaygroundProps) => {
{ label: 'google', before: <Icon24LogoGoogle />, value: 'google' },
],
],
defaultValue: ['fb'],
dir: ['ltr', 'rtl'],
},
]}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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%));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ const story: Meta<SegmentedControlProps> = {
component: SegmentedControl,
parameters: { ...CanvasFullLayout, ...DisableCartesianParam },
args: { onChange: fn() },
argTypes: {
role: {
control: 'select',
options: ['radiogroup', 'tablist'],
},
},
};

export default story;
Expand All @@ -30,6 +36,7 @@ export const Playground: Story = {
value: 'other',
},
],
role: 'radiogroup',
},
decorators: [
(Component) => (
Expand Down
Original file line number Diff line number Diff line change
@@ -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];

Expand Down Expand Up @@ -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<SegmentedControlProps, 'options' | 'role'>) => (
<SegmentedControl data-testid="ctrl" {...props} role="radiogroup" options={options} />
);

it('should use correct css variables', () => {
const { container } = render(<SegmentedControlTabsTest defaultValue="ok" />);
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<SegmentedControlProps, 'options' | 'role'>) => (
<SegmentedControl data-testid="ctrl" {...props} role="radiogroup" options={options} />
);
render(<SegmentedControlTabsTest />);
expect(ctrl()).toHaveClass(styles.rtl);
});
});
});
28 changes: 15 additions & 13 deletions packages/vkui/src/components/SegmentedControl/SegmentedControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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);

Expand All @@ -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 (
<RootComponent
{...restProps}
getRootRef={rootRef}
baseClassName={classNames(
styles.host,
sizeY !== 'compact' && sizeYClassNames[sizeY],
size === 'l' && styles.sizeL,
isRtl && styles.rtl,
)}
>
<div role={role} ref={tabsRef} className={styles.in}>
{actualIndex > -1 && (
<div
aria-hidden
className={styles.slider}
style={{
width: `${100 / options.length}%`,
transform: translateX,
}}
/>
)}
{actualIndex > -1 && <div aria-hidden className={styles.slider} style={sliderStyle} />}
{options.map(({ label, before, ...optionProps }) => {
const selected = value === optionProps.value;
const onSelect = () => onChange(optionProps.value);
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 7 additions & 1 deletion packages/vkui/src/components/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 (
<RootComponent
{...restProps}
getRootRef={rootRef}
baseClassName={classNames(
styles.host,
'vkuiInternalTabs',
Expand Down
14 changes: 14 additions & 0 deletions packages/vkui/src/hooks/useTabsNavigation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,18 @@ describe('useTabsNavigation', () => {
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<HTMLDivElement>).current = tabs;
tab1.focus();

fireEvent.keyDown(document, { key: 'ArrowLeft' });
expect(document.activeElement).toBe(tab2);

fireEvent.keyDown(document, { key: 'ArrowRight' });
expect(document.activeElement).toBe(tab1);
});
});
4 changes: 2 additions & 2 deletions packages/vkui/src/hooks/useTabsNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLDivElement>(null);
const getTabEls = () => {
Expand Down Expand Up @@ -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;
}

Expand Down

0 comments on commit 02d004d

Please sign in to comment.