diff --git a/packages/vkui/src/components/TabbarItem/TabbarItem.test.tsx b/packages/vkui/src/components/TabbarItem/TabbarItem.test.tsx index f1e2deef47..5b1fe927ed 100644 --- a/packages/vkui/src/components/TabbarItem/TabbarItem.test.tsx +++ b/packages/vkui/src/components/TabbarItem/TabbarItem.test.tsx @@ -1,7 +1,12 @@ import { render, screen } from '@testing-library/react'; import { Icon28NewsfeedOutline } from '@vkontakte/icons'; +import { + AppRootContext, + DEFAULT_APP_ROOT_CONTEXT_VALUE, +} from '../../components/AppRoot/AppRootContext'; import { baselineComponent, userEvent } from '../../testing/utils'; import { TabbarItem } from './TabbarItem'; +import styles from '../../styles/focusVisible.module.css'; describe('TabbarItem', () => { baselineComponent((props) => ( @@ -39,4 +44,51 @@ describe('TabbarItem', () => { await userEvent.click(screen.getByTestId('test')); expect(cb).toHaveBeenCalledTimes(1); }); + + function renderTabbarItemForFocus({ withKeyboardInput }: { withKeyboardInput: boolean }) { + const onFocusStub = jest.fn(); + const onBlurStub = jest.fn(); + + return { + onFocusStub, + onBlurStub, + ...render( + + , + , + ), + }; + } + + it('shows focus visible on focus with keyboard', async () => { + jest.useFakeTimers(); + + const component = renderTabbarItemForFocus({ withKeyboardInput: true }); + + await userEvent.tab(); + expect(screen.getByRole('presentation')).toHaveClass(styles['-focus-visible--focused']); + + await userEvent.tab(); + expect(screen.getByRole('presentation')).not.toHaveClass(styles['-focus-visible--focused']); + + expect(component.onFocusStub).toHaveBeenCalledTimes(1); + expect(component.onBlurStub).toHaveBeenCalledTimes(1); + }); + + it('does not show focus visible on focus without keyboard', async () => { + jest.useFakeTimers(); + + const component = renderTabbarItemForFocus({ withKeyboardInput: false }); + + await userEvent.click(screen.getByTestId('test')); + expect(screen.getByRole('presentation')).not.toHaveClass(styles['-focus-visible--focused']); + + await userEvent.tab(); + expect(screen.getByRole('presentation')).not.toHaveClass(styles['-focus-visible--focused']); + + expect(component.onFocusStub).toHaveBeenCalledTimes(1); + expect(component.onBlurStub).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/vkui/src/components/TabbarItem/TabbarItem.tsx b/packages/vkui/src/components/TabbarItem/TabbarItem.tsx index ae1d0030ca..d570d207d0 100644 --- a/packages/vkui/src/components/TabbarItem/TabbarItem.tsx +++ b/packages/vkui/src/components/TabbarItem/TabbarItem.tsx @@ -2,7 +2,10 @@ import * as React from 'react'; import { classNames, hasReactNode, noop } from '@vkontakte/vkjs'; +import { useFocusVisible } from '../../hooks/useFocusVisible'; +import { useFocusVisibleClassName } from '../../hooks/useFocusVisibleClassName'; import { usePlatform } from '../../hooks/usePlatform'; +import { callMultiple } from '../../lib/callMultiple'; import { COMMON_WARNINGS, warnOnce } from '../../lib/warnOnce'; import type { HasComponent, HasRootRef } from '../../types'; import { RootComponent } from '../RootComponent/RootComponent'; @@ -38,6 +41,8 @@ export const TabbarItem = ({ href, Component = href ? 'a' : 'button', disabled, + onFocus: onFocusProp, + onBlur: onBlurProp, ...restProps }: TabbarItemProps): React.ReactNode => { const platform = usePlatform(); @@ -50,11 +55,22 @@ export const TabbarItem = ({ } } + const { + focusVisible, + onFocus: handleFocusVisibleOnFocus, + onBlur: handleFocusVisibleOnBlur, + } = useFocusVisible(); + const focusVisibleClassNames = useFocusVisibleClassName({ + focusVisible, + }); + return (