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 (