From 43288a8f76aa64f32c0811f232b839471c482988 Mon Sep 17 00:00:00 2001 From: Andrey Medvedev Date: Fri, 19 Jan 2024 11:44:43 +0300 Subject: [PATCH] fix(Switch): Show focus in android talkback (#6365) (#6405) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Действительно, на Android в Talkback не видно фокуса при фокусировании на инпуте, потому что мы прячем этот инпут с помощью `VisuallyHidden` компонента. Как вариант можно убрать использование VisuallyHidden и спрятать компонент позади визульной части. Изменения: - Добавил модификатор для VisuallyHidden, когда речь идет о `Component=input`. Явно задал размер равный размеру родительского элемента, чтобы выделение визуально было подобно размеру switch. Но изменение затронуло не только Switch, но и другие компоненты, в которых ``. - добавил `role='switch'` - добавил `aria-checked`. Так как значение этого аттрибута должно соответствовать значению инпута, то добавил переменную состояния. - обновил пример в Storybook, добавив историю с испольованием SimpleCell чтобы как-то учесть пожелания из https://github.com/VKCOM/VKUI/issues/4931 --- .../src/components/Switch/Switch.stories.tsx | 12 +++ .../src/components/Switch/Switch.test.tsx | 82 +++++++++++++++++++ .../vkui/src/components/Switch/Switch.tsx | 31 ++++++- .../VisuallyHidden/VisuallyHidden.module.css | 12 +++ .../VisuallyHidden/VisuallyHidden.test.tsx | 16 ++++ .../VisuallyHidden/VisuallyHidden.tsx | 10 ++- 6 files changed, 161 insertions(+), 2 deletions(-) diff --git a/packages/vkui/src/components/Switch/Switch.stories.tsx b/packages/vkui/src/components/Switch/Switch.stories.tsx index a46b361dc9..65a31423e7 100644 --- a/packages/vkui/src/components/Switch/Switch.stories.tsx +++ b/packages/vkui/src/components/Switch/Switch.stories.tsx @@ -1,4 +1,6 @@ +import * as React from 'react'; import { Meta, StoryObj } from '@storybook/react'; +import { SimpleCell } from '../../components/SimpleCell/SimpleCell'; import { CanvasFullLayout, DisableCartesianParam } from '../../storybook/constants'; import { Switch, SwitchProps } from './Switch'; @@ -18,3 +20,13 @@ export default story; type Story = StoryObj; export const Playground: Story = {}; + +export const WithSimpleCellLabel: Story = { + render: function Render(args) { + return ( + }> + Комментарии к записям + + ); + }, +}; diff --git a/packages/vkui/src/components/Switch/Switch.test.tsx b/packages/vkui/src/components/Switch/Switch.test.tsx index 277593dd7d..9c51f176f2 100644 --- a/packages/vkui/src/components/Switch/Switch.test.tsx +++ b/packages/vkui/src/components/Switch/Switch.test.tsx @@ -1,4 +1,6 @@ import * as React from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { baselineComponent } from '../../testing/utils'; import { VisuallyHidden } from '../VisuallyHidden/VisuallyHidden'; import { Switch } from './Switch'; @@ -12,4 +14,84 @@ describe('Switch', () => { )); + + it('(Uncontrolled) shows checked state', async () => { + const { rerender } = render(); + + const component = screen.getByRole('switch'); + if (!component) { + throw new Error('Can not find component'); + } + + expect(component.checked).toBeFalsy(); + expect(component.getAttribute('aria-checked')).toBe('false'); + + fireEvent.click(component); + + expect(component.checked).toBeTruthy(); + expect(component.getAttribute('aria-checked')).toBe('true'); + + rerender(); + + const defaultCheckedComponent = screen.getByTestId('switch'); + if (!defaultCheckedComponent) { + throw new Error('Can not find component'); + } + + expect(defaultCheckedComponent.checked).toBeTruthy(); + expect(defaultCheckedComponent.getAttribute('aria-checked')).toBe('true'); + + fireEvent.click(defaultCheckedComponent); + + expect(defaultCheckedComponent.checked).toBeFalsy(); + expect(defaultCheckedComponent.getAttribute('aria-checked')).toBe('false'); + + rerender(); + + const disabledSwitch = screen.getByTestId('switch'); + if (!disabledSwitch) { + throw new Error('Can not find component'); + } + expect(disabledSwitch.checked).toBeFalsy(); + expect(disabledSwitch.getAttribute('aria-checked')).toBe('false'); + + jest.useFakeTimers(); + userEvent.click(disabledSwitch); + + expect(disabledSwitch.checked).toBeFalsy(); + expect(disabledSwitch.getAttribute('aria-checked')).toBe('false'); + }); + + it('(Controlled) shows checked state', () => { + function ControlledSwitch() { + const [checked, setChecked] = React.useState(false); + return ( + + + + + ); + } + render(); + + const switchComponent = screen.getByRole('switch'); + if (!switchComponent) { + throw new Error('Can not find component'); + } + + expect(switchComponent.checked).toBeFalsy(); + expect(switchComponent.getAttribute('aria-checked')).toBe('false'); + + fireEvent.click(switchComponent); + + expect(switchComponent.checked).toBeFalsy(); + expect(switchComponent.getAttribute('aria-checked')).toBe('false'); + + fireEvent.click(screen.getByRole('button')); + + expect(switchComponent.checked).toBeTruthy(); + expect(switchComponent.getAttribute('aria-checked')).toBe('true'); + }); }); diff --git a/packages/vkui/src/components/Switch/Switch.tsx b/packages/vkui/src/components/Switch/Switch.tsx index 0639480ae5..cf0399b2d5 100644 --- a/packages/vkui/src/components/Switch/Switch.tsx +++ b/packages/vkui/src/components/Switch/Switch.tsx @@ -24,12 +24,37 @@ export interface SwitchProps /** * @see https://vkcom.github.io/VKUI/#/Switch */ -export const Switch = ({ style, className, getRootRef, getRef, ...restProps }: SwitchProps) => { +export const Switch = ({ + style, + className, + getRootRef, + getRef, + checked: checkedProp, + ...restProps +}: SwitchProps) => { const platform = usePlatform(); const { sizeY = 'none' } = useAdaptivity(); const { focusVisible, onBlur, onFocus } = useFocusVisible(); const focusVisibleClassNames = useFocusVisibleClassName({ focusVisible, mode: 'outside' }); + const [localUncontrolledChecked, setLocalUncontrolledChecked] = React.useState( + Boolean(restProps.defaultChecked), + ); + const isControlled = checkedProp !== undefined; + + const syncUncontrolledCheckedStateOnClick = React.useCallback( + (e: React.MouseEvent) => { + if (isControlled) { + return; + } + + const switchTarget = e.target as HTMLInputElement; + setLocalUncontrolledChecked(switchTarget.checked); + }, + [isControlled], + ); + + const ariaCheckedState = isControlled ? checkedProp : localUncontrolledChecked; return (