diff --git a/packages/vkui/src/components/Avatar/Avatar.tsx b/packages/vkui/src/components/Avatar/Avatar.tsx
index 46ad6f3880..1cb02d4cf3 100644
--- a/packages/vkui/src/components/Avatar/Avatar.tsx
+++ b/packages/vkui/src/components/Avatar/Avatar.tsx
@@ -81,20 +81,32 @@ export const Avatar = ({
className,
gradientColor,
initials,
- fallbackIcon,
+ fallbackIcon: fallbackIconProp,
children,
...restProps
}: AvatarProps) => {
const gradientName =
typeof gradientColor === 'number' ? COLORS_NUMBER_TO_TEXT_MAP[gradientColor] : gradientColor;
const isGradientNotCustom = gradientName && gradientName !== 'custom';
- const rewrittenFallbackIcon = initials ? undefined : fallbackIcon;
+
+ const fallbackIcon = initials ? (
+
+ {initials}
+
+ ) : (
+ fallbackIconProp
+ );
return (
- {initials && (
-
- {initials}
-
- )}
{children}
);
diff --git a/packages/vkui/src/components/Clickable/Clickable.module.css b/packages/vkui/src/components/Clickable/Clickable.module.css
index 0ab7a57da9..82a36b2ecf 100644
--- a/packages/vkui/src/components/Clickable/Clickable.module.css
+++ b/packages/vkui/src/components/Clickable/Clickable.module.css
@@ -1,8 +1,3 @@
.Clickable__host {
cursor: pointer;
}
-
-.Clickable__host:focus,
-.Clickable__host:focus-visible {
- outline: none;
-}
diff --git a/packages/vkui/src/components/Clickable/Clickable.tsx b/packages/vkui/src/components/Clickable/Clickable.tsx
index 3901ade557..8b55f8e72d 100644
--- a/packages/vkui/src/components/Clickable/Clickable.tsx
+++ b/packages/vkui/src/components/Clickable/Clickable.tsx
@@ -1,14 +1,16 @@
import * as React from 'react';
import { classNames } from '@vkontakte/vkjs';
import { useFocusVisible } from '../../hooks/useFocusVisible';
+import {
+ type FocusVisibleModeProps,
+ useFocusVisibleClassName,
+} from '../../hooks/useFocusVisibleClassName';
import { callMultiple } from '../../lib/callMultiple';
-import { FocusVisible, FocusVisibleMode } from '../FocusVisible/FocusVisible';
import { RootComponent, RootComponentProps } from '../RootComponent/RootComponent';
import styles from './Clickable.module.css';
-export interface ClickableProps extends RootComponentProps {
+export interface ClickableProps extends RootComponentProps, FocusVisibleModeProps {
baseClassName?: string;
- focusVisibleMode?: FocusVisibleMode;
}
/**
@@ -31,16 +33,16 @@ const RealClickable = ({
...restProps
}: ClickableProps) => {
const { focusVisible, onBlur, onFocus } = useFocusVisible();
+ const focusVisibleClassNames = useFocusVisibleClassName({ focusVisible, mode: focusVisibleMode });
return (
{children}
-
);
};
diff --git a/packages/vkui/src/components/CustomSelect/__image_snapshots__/customselect-no-max-height-vkcom-chromium-light-1-snap.png b/packages/vkui/src/components/CustomSelect/__image_snapshots__/customselect-no-max-height-vkcom-chromium-light-1-snap.png
index 7f33ab3d36..60c7c92aa5 100644
--- a/packages/vkui/src/components/CustomSelect/__image_snapshots__/customselect-no-max-height-vkcom-chromium-light-1-snap.png
+++ b/packages/vkui/src/components/CustomSelect/__image_snapshots__/customselect-no-max-height-vkcom-chromium-light-1-snap.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f8cd15401d934dfc8a349b6b8f79533c842c03ffeb7da65215d7da396e5db13d
-size 29582
+oid sha256:a2e9ee4b0ba468a5137e331ec8991da95f0b580341255991cef4b28806deaa6c
+size 29562
diff --git a/packages/vkui/src/components/CustomSelect/__image_snapshots__/customselect-no-max-height-vkcom-firefox-light-1-snap.png b/packages/vkui/src/components/CustomSelect/__image_snapshots__/customselect-no-max-height-vkcom-firefox-light-1-snap.png
index 201f6eb2c1..f84d86bd7f 100644
--- a/packages/vkui/src/components/CustomSelect/__image_snapshots__/customselect-no-max-height-vkcom-firefox-light-1-snap.png
+++ b/packages/vkui/src/components/CustomSelect/__image_snapshots__/customselect-no-max-height-vkcom-firefox-light-1-snap.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1f1a7aaf01ca229081920f0b4f5ad89f47ce1ae8722d2d150f1899f287ad56c2
-size 27641
+oid sha256:cd94b4a6533356c597eb94327ecf489954a6a97f5c299bc631121b04464b6e8f
+size 27671
diff --git a/packages/vkui/src/components/CustomSelect/__image_snapshots__/customselect-no-max-height-vkcom-webkit-light-1-snap.png b/packages/vkui/src/components/CustomSelect/__image_snapshots__/customselect-no-max-height-vkcom-webkit-light-1-snap.png
index 84aac61de2..ab8ba8dd03 100644
--- a/packages/vkui/src/components/CustomSelect/__image_snapshots__/customselect-no-max-height-vkcom-webkit-light-1-snap.png
+++ b/packages/vkui/src/components/CustomSelect/__image_snapshots__/customselect-no-max-height-vkcom-webkit-light-1-snap.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c314baacbd7c6ffd76a992ab307f43fac84d015bfabf82a150f6f64c8e02e437
-size 18915
+oid sha256:a676f3cea5738a4f33e25126680f387684cf30cad38e16edab175155776cf68c
+size 18911
diff --git a/packages/vkui/src/components/FocusVisible/FocusVisible.module.css b/packages/vkui/src/components/FocusVisible/FocusVisible.module.css
deleted file mode 100644
index 65784e08f8..0000000000
--- a/packages/vkui/src/components/FocusVisible/FocusVisible.module.css
+++ /dev/null
@@ -1,98 +0,0 @@
-.FocusVisible {
- visibility: hidden;
- position: absolute;
- border-radius: inherit;
- user-select: none;
- pointer-events: none;
- overflow: hidden;
- top: var(--vkui_internal--focus_visible_distance, 0);
- left: var(--vkui_internal--focus_visible_distance, 0);
- right: var(--vkui_internal--focus_visible_distance, 0);
- bottom: var(--vkui_internal--focus_visible_distance, 0);
-}
-
-.FocusVisible--visible {
- visibility: visible;
-}
-
-/* Необходимо перебить селектор `.Tappable > *` */
-.FocusVisible.FocusVisible {
- position: absolute;
-}
-
-.FocusVisible {
- --vkui_internal--focus_visible_thin: 2px;
-}
-
-.FocusVisible--thin {
- --vkui_internal--focus_visible_thin: var(--vkui--size_border--regular);
-}
-
-.FocusVisible--mode-inside,
-.FocusVisible--mode-outline {
- border-color: var(--vkui--color_stroke_accent);
- border-width: var(--vkui_internal--focus_visible_thin);
- border-style: solid;
- box-sizing: border-box;
-}
-
-.FocusVisible--mode-outline {
- --vkui_internal--focus_visible_distance: 0;
-}
-
-.FocusVisible--mode-inside {
- --vkui_internal--focus_visible_distance: 2px;
-}
-
-.FocusVisible--mode-outside {
- box-shadow: 0 0 0 var(--vkui_internal--focus_visible_thin) var(--vkui--color_stroke_accent);
-
- --vkui_internal--focus_visible_distance: -2px;
-}
-
-/**
- * [a11y]
- * add animation for browsers that support prefers-reduced-motion
- * so that users with vestibular motion disorders have no problem
- * navigating accessible vkui apps via keyboard
- */
-@media (prefers-reduced-motion: no-preference) {
- .FocusVisible--visible.FocusVisible--mode-inside {
- animation: animation-focus-visible 0.15s ease-in-out forwards;
- animation-delay: 0.01ms;
- will-change: top, left, bottom, right;
-
- --vkui_internal--focus_visible_distance: 4px;
- }
-
- .FocusVisible--visible.FocusVisible--mode-outside {
- animation-name: animation-focus-visible-outside;
-
- --vkui_internal--focus_visible_distance: 0;
- }
-}
-
-@keyframes animation-focus-visible {
- 0% {
- }
-
- 100% {
- top: 0;
- left: 0;
- bottom: 0;
- right: 0;
- will-change: auto;
- }
-}
-@keyframes animation-focus-visible-outside {
- 0% {
- }
-
- 100% {
- top: -2px;
- left: -2px;
- bottom: -2px;
- right: -2px;
- will-change: auto;
- }
-}
diff --git a/packages/vkui/src/components/FocusVisible/FocusVisible.tsx b/packages/vkui/src/components/FocusVisible/FocusVisible.tsx
deleted file mode 100644
index 5d74cab195..0000000000
--- a/packages/vkui/src/components/FocusVisible/FocusVisible.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import * as React from 'react';
-import { classNames } from '@vkontakte/vkjs';
-import { HasRootRef } from '../../types';
-import { RootComponent } from '../RootComponent/RootComponent';
-import styles from './FocusVisible.module.css';
-
-const stylesMode = {
- inside: styles['FocusVisible--mode-inside'],
- outside: styles['FocusVisible--mode-outside'],
- outline: styles['FocusVisible--mode-outline'],
-};
-
-export type FocusVisibleMode = 'inside' | 'outside' | 'outline';
-
-export interface FocusVisibleProps extends HasRootRef {
- visible: boolean | undefined;
- mode: FocusVisibleMode;
- thin?: boolean;
-}
-
-/**
- * @see https://vkcom.github.io/VKUI/#/FocusVisible
- */
-export const FocusVisible = ({ visible, mode, thin, ...restProps }: FocusVisibleProps) => (
-
-);
diff --git a/packages/vkui/src/components/FormField/FormField.module.css b/packages/vkui/src/components/FormField/FormField.module.css
index f5c3d6ec52..b4d3dd995b 100644
--- a/packages/vkui/src/components/FormField/FormField.module.css
+++ b/packages/vkui/src/components/FormField/FormField.module.css
@@ -8,7 +8,6 @@
-webkit-tap-highlight-color: transparent;
isolation: isolate;
border-radius: var(--vkui--size_border_radius--regular);
- outline: none;
}
.FormField--sizeY-compact {
@@ -86,9 +85,9 @@
}
/**
- * [start]
* CMP:
* FormItem
+ * [start]
*/
:global(.vkuiInternalFormItem--status-error) .FormField__border,
.FormField--status-error .FormField__border {
@@ -123,11 +122,6 @@
z-index: var(--vkui_internal--z_index_form_field_border_hover);
}
-/* stylelint-disable-next-line @project-tools/stylelint-atomic, selector-max-universal */
-.FormField *:focus {
- outline: none;
-}
-
/**
* CMP:
* ModalCardBase
@@ -207,3 +201,13 @@
border-bottom-left-radius: var(--vkui--size_border_radius--regular);
border-bottom-right-radius: var(--vkui--size_border_radius--regular);
}
+
+/**
+ * useFocusVisibleClassName()
+ */
+/* increase specificity for selects */
+.FormField--focus-visible.FormField--focus-visible.FormField--focus-visible {
+ outline: var(--vkui_internal--outline);
+ outline-width: var(--vkui--size_border--regular);
+ outline-offset: calc(-1 * var(--vkui--size_border--regular));
+}
diff --git a/packages/vkui/src/components/FormField/FormField.tsx b/packages/vkui/src/components/FormField/FormField.tsx
index f3d0965858..9ecd99e93e 100644
--- a/packages/vkui/src/components/FormField/FormField.tsx
+++ b/packages/vkui/src/components/FormField/FormField.tsx
@@ -2,10 +2,10 @@ import * as React from 'react';
import { classNames } from '@vkontakte/vkjs';
import { useAdaptivity } from '../../hooks/useAdaptivity';
import { useExternRef } from '../../hooks/useExternRef';
+import { useFocusVisibleClassName } from '../../hooks/useFocusVisibleClassName';
import { useFocusWithin } from '../../hooks/useFocusWithin';
import { SizeType } from '../../lib/adaptivity';
import { HasComponent, HasRootRef } from '../../types';
-import { FocusVisible } from '../FocusVisible/FocusVisible';
import styles from './FormField.module.css';
const sizeYClassNames = {
@@ -65,10 +65,15 @@ export const FormField = ({
...restProps
}: FormFieldOwnProps) => {
const elRef = useExternRef(getRootRef);
- const focusWithin = useFocusWithin(elRef);
const { sizeY = 'none' } = useAdaptivity();
const [hover, setHover] = React.useState(false);
+ const focusWithin = useFocusWithin(elRef);
+ const focusVisibleClassNames = useFocusVisibleClassName({
+ focusVisible: focusWithin,
+ mode: styles['FormField--focus-visible'],
+ });
+
const handleMouseEnter = (e: MouseEvent) => {
e.stopPropagation();
setHover(true);
@@ -92,6 +97,7 @@ export const FormField = ({
sizeY !== SizeType.REGULAR && sizeYClassNames[sizeY],
disabled && styles['FormField--disabled'],
!disabled && hover && styles['FormField--hover'],
+ focusVisibleClassNames,
className,
)}
>
@@ -103,7 +109,6 @@ export const FormField = ({
)}
-
);
};
diff --git a/packages/vkui/src/components/Image/Image.e2e-playground.tsx b/packages/vkui/src/components/Image/Image.e2e-playground.tsx
index 84c233f2f1..bd45d6efa3 100644
--- a/packages/vkui/src/components/Image/Image.e2e-playground.tsx
+++ b/packages/vkui/src/components/Image/Image.e2e-playground.tsx
@@ -86,3 +86,38 @@ export const ImagePlayground = (props: ComponentPlaygroundProps) => {
);
};
+
+export const ImageFocusVisiblePlayground = (props: ComponentPlaygroundProps) => (
+
+ {(props: ImageProps) => null} {...props} />}
+
+);
+
+export const ImageFocusVisibleOverlayPlayground = (props: ComponentPlaygroundProps) => (
+
+
+
+
+ ,
+ ],
+ },
+ ]}
+ >
+ {(props: ImageProps) => }
+
+);
diff --git a/packages/vkui/src/components/Image/Image.e2e.tsx b/packages/vkui/src/components/Image/Image.e2e.tsx
index 76a8c73b31..61c9de33f9 100644
--- a/packages/vkui/src/components/Image/Image.e2e.tsx
+++ b/packages/vkui/src/components/Image/Image.e2e.tsx
@@ -1,8 +1,44 @@
import * as React from 'react';
import { test } from '@vkui-e2e/test';
-import { ImagePlayground } from './Image.e2e-playground';
+import { Platform } from '../../lib/platform';
+import {
+ ImageFocusVisibleOverlayPlayground,
+ ImageFocusVisiblePlayground,
+ ImagePlayground,
+} from './Image.e2e-playground';
test('Image', async ({ mount, expectScreenshotClippedToContent, componentPlaygroundProps }) => {
await mount();
await expectScreenshotClippedToContent();
});
+
+test.describe('Image', () => {
+ test.use({
+ onlyForPlatforms: [Platform.ANDROID],
+ onlyForAppearances: ['light'],
+ });
+
+ test('State: Focus Visible', async ({
+ mount,
+ page,
+ expectScreenshotClippedToContent,
+ componentPlaygroundProps,
+ }) => {
+ await mount();
+ await page.emulateMedia({ reducedMotion: 'reduce' });
+ await page.keyboard.press('Tab');
+ await expectScreenshotClippedToContent();
+ });
+
+ test('State: Focus Visible (overlay)', async ({
+ mount,
+ page,
+ expectScreenshotClippedToContent,
+ componentPlaygroundProps,
+ }) => {
+ await mount();
+ await page.emulateMedia({ reducedMotion: 'reduce' });
+ await page.keyboard.press('Tab');
+ await expectScreenshotClippedToContent();
+ });
+});
diff --git a/packages/vkui/src/components/Image/__image_snapshots__/image-state-focus-visible-android-chromium-light-1-snap.png b/packages/vkui/src/components/Image/__image_snapshots__/image-state-focus-visible-android-chromium-light-1-snap.png
new file mode 100644
index 0000000000..f67091b4e0
--- /dev/null
+++ b/packages/vkui/src/components/Image/__image_snapshots__/image-state-focus-visible-android-chromium-light-1-snap.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8e7c82f23b07e386f1755c873e1aa92e6704591510955dcfb96d51c4521bc447
+size 3920
diff --git a/packages/vkui/src/components/Image/__image_snapshots__/image-state-focus-visible-overlay-android-chromium-light-1-snap.png b/packages/vkui/src/components/Image/__image_snapshots__/image-state-focus-visible-overlay-android-chromium-light-1-snap.png
new file mode 100644
index 0000000000..6c88ce74fb
--- /dev/null
+++ b/packages/vkui/src/components/Image/__image_snapshots__/image-state-focus-visible-overlay-android-chromium-light-1-snap.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:93fa2ba6ec99b89e51dfcd3985e667a09b5cc4477dee2f43ed8f1fe31a4d0f05
+size 5027
diff --git a/packages/vkui/src/components/ImageBase/ImageBase.module.css b/packages/vkui/src/components/ImageBase/ImageBase.module.css
index 010d8b3c87..9d85f3e7d4 100644
--- a/packages/vkui/src/components/ImageBase/ImageBase.module.css
+++ b/packages/vkui/src/components/ImageBase/ImageBase.module.css
@@ -31,6 +31,7 @@
.ImageBase__img {
position: absolute;
+ z-index: var(--vkui_internal--z_index_image_base_img);
top: 0;
left: 0;
display: block;
@@ -49,7 +50,7 @@
.ImageBase__fallback {
position: absolute;
- /* Расчитываем на ценитрирование через родительский `display: flex` */
+ /* Расcчитываем на центрирование через родительский `display: flex` */
top: auto;
left: auto;
}
diff --git a/packages/vkui/src/components/ImageBase/ImageBaseOverlay/ImageBaseOverlay.module.css b/packages/vkui/src/components/ImageBase/ImageBaseOverlay/ImageBaseOverlay.module.css
index 71653d8890..46d2150e05 100644
--- a/packages/vkui/src/components/ImageBase/ImageBaseOverlay/ImageBaseOverlay.module.css
+++ b/packages/vkui/src/components/ImageBase/ImageBaseOverlay/ImageBaseOverlay.module.css
@@ -20,11 +20,6 @@
opacity: 1;
}
-.ImageBaseOverlay--focus-visible {
- opacity: 1;
- box-shadow: inset 0 0 0 2px var(--vkui--color_stroke_accent);
-}
-
.ImageBaseOverlay--theme-light {
color: var(--vkui--color_icon_accent);
background-color: var(--vkui--color_avatar_overlay_inverse_alpha);
diff --git a/packages/vkui/src/components/ImageBase/ImageBaseOverlay/ImageBaseOverlay.tsx b/packages/vkui/src/components/ImageBase/ImageBaseOverlay/ImageBaseOverlay.tsx
index 2bda370e96..d19da1cebb 100644
--- a/packages/vkui/src/components/ImageBase/ImageBaseOverlay/ImageBaseOverlay.tsx
+++ b/packages/vkui/src/components/ImageBase/ImageBaseOverlay/ImageBaseOverlay.tsx
@@ -2,6 +2,7 @@ import * as React from 'react';
import { classNames } from '@vkontakte/vkjs';
import { useAdaptivityHasPointer } from '../../../hooks/useAdaptivityHasPointer';
import { useAppearance } from '../../../hooks/useAppearance';
+import { focusVisiblePresetModeClassNames } from '../../../hooks/useFocusVisibleClassName';
import { Tappable } from '../../Tappable/Tappable';
import { ImageBaseContext } from '../context';
import type { ImageBaseExpectedIconProps } from '../types';
@@ -79,7 +80,10 @@ export const ImageBaseOverlay = ({
)}
hasHover={visibility === 'on-hover'}
hoverMode={visibility === 'on-hover' ? styles['ImageBaseOverlay--visible'] : undefined}
- focusVisibleMode={styles['ImageBaseOverlay--focus-visible']}
+ focusVisibleMode={classNames(
+ focusVisiblePresetModeClassNames['inside'],
+ styles['ImageBaseOverlay--visible'],
+ )}
hasActive={false}
onClick={onClick}
>
diff --git a/packages/vkui/src/components/Link/Link.e2e-playground.tsx b/packages/vkui/src/components/Link/Link.e2e-playground.tsx
new file mode 100644
index 0000000000..337f6d68af
--- /dev/null
+++ b/packages/vkui/src/components/Link/Link.e2e-playground.tsx
@@ -0,0 +1,17 @@
+import * as React from 'react';
+import { ComponentPlayground, type ComponentPlaygroundProps } from '@vkui-e2e/playground-helpers';
+import { Link, type LinkProps } from './Link';
+
+export const LinkFocusVisiblePlayground = (props: ComponentPlaygroundProps) => (
+
+ {(props: LinkProps) => (
+
+ Нажимая «Продолжить», вы принимаете{' '}
+
+ пользовательское соглашение
+
+ ...
+
+ )}
+
+);
diff --git a/packages/vkui/src/components/Link/Link.e2e.tsx b/packages/vkui/src/components/Link/Link.e2e.tsx
new file mode 100644
index 0000000000..4ab5871a0e
--- /dev/null
+++ b/packages/vkui/src/components/Link/Link.e2e.tsx
@@ -0,0 +1,23 @@
+import * as React from 'react';
+import { test } from '@vkui-e2e/test';
+import { Platform } from '../../lib/platform';
+import { LinkFocusVisiblePlayground } from './Link.e2e-playground';
+
+test.describe('Link', () => {
+ test.use({
+ onlyForPlatforms: [Platform.ANDROID],
+ onlyForAppearances: ['light'],
+ });
+
+ test('State: Focus Visible', async ({
+ mount,
+ page,
+ expectScreenshotClippedToContent,
+ componentPlaygroundProps,
+ }) => {
+ await mount();
+ await page.emulateMedia({ reducedMotion: 'reduce' });
+ await page.keyboard.press('Tab');
+ await expectScreenshotClippedToContent();
+ });
+});
diff --git a/packages/vkui/src/components/Link/Link.module.css b/packages/vkui/src/components/Link/Link.module.css
index f836ce8e0e..7ed691c56a 100644
--- a/packages/vkui/src/components/Link/Link.module.css
+++ b/packages/vkui/src/components/Link/Link.module.css
@@ -21,17 +21,6 @@
}
}
-.Link--focus-visible {
- /**
- * На момент v4.33.0, реализация не подошла, т.к. текст может быть многострочным.
- * Поэтому используем свой класс и применяем `outline`.
- *
- * `!important` – чтобы перебить глобальное обнуление `outline` на `:focus`.
- */
- /* stylelint-disable-next-line declaration-no-important */
- outline: 2px solid var(--vkui--color_stroke_accent) !important;
-}
-
.Link--has-visited:visited {
color: var(--vkui--color_text_link_visited);
}
diff --git a/packages/vkui/src/components/Link/Link.tsx b/packages/vkui/src/components/Link/Link.tsx
index 528f004b41..4c7bff0143 100644
--- a/packages/vkui/src/components/Link/Link.tsx
+++ b/packages/vkui/src/components/Link/Link.tsx
@@ -21,7 +21,7 @@ export const Link = ({ hasVisited, children, className, ...restProps }: LinkProp
className={classNames(styles['Link'], hasVisited && styles['Link--has-visited'], className)}
hasHover={false}
activeMode="opacity"
- focusVisibleMode={styles['Link--focus-visible']}
+ focusVisibleMode="outside"
>
{children}
diff --git a/packages/vkui/src/components/Link/__image_snapshots__/link-state-focus-visible-android-chromium-light-1-snap.png b/packages/vkui/src/components/Link/__image_snapshots__/link-state-focus-visible-android-chromium-light-1-snap.png
new file mode 100644
index 0000000000..756a681e7a
--- /dev/null
+++ b/packages/vkui/src/components/Link/__image_snapshots__/link-state-focus-visible-android-chromium-light-1-snap.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e1d605231604aa87f70d78fe3c3f7bf775d308572d0bea8be9198ad2d04f0450
+size 6436
diff --git a/packages/vkui/src/components/Search/Search.module.css b/packages/vkui/src/components/Search/Search.module.css
index 9e79e66d5e..c9e84b365b 100644
--- a/packages/vkui/src/components/Search/Search.module.css
+++ b/packages/vkui/src/components/Search/Search.module.css
@@ -98,7 +98,7 @@
}
.Search__nativeInput:focus {
- outline: none;
+ outline: var(--vkui_internal--outline-reset);
}
.Search--has-after .Search__nativeInput {
diff --git a/packages/vkui/src/components/SegmentedControl/SegmentedControlOption/SegmentedControlOption.module.css b/packages/vkui/src/components/SegmentedControl/SegmentedControlOption/SegmentedControlOption.module.css
index c084281e62..bfa9ad4bf8 100644
--- a/packages/vkui/src/components/SegmentedControl/SegmentedControlOption/SegmentedControlOption.module.css
+++ b/packages/vkui/src/components/SegmentedControl/SegmentedControlOption/SegmentedControlOption.module.css
@@ -16,7 +16,7 @@
flex: 1;
}
-.SegmentedControlOption:not(.SegmentedControlOption--checked):hover {
+.SegmentedControlOption:hover {
opacity: 0.5;
}
diff --git a/packages/vkui/src/components/SegmentedControl/SegmentedControlOption/SegmentedControlOption.tsx b/packages/vkui/src/components/SegmentedControl/SegmentedControlOption/SegmentedControlOption.tsx
index 30beaf0cd8..3430617c54 100644
--- a/packages/vkui/src/components/SegmentedControl/SegmentedControlOption/SegmentedControlOption.tsx
+++ b/packages/vkui/src/components/SegmentedControl/SegmentedControlOption/SegmentedControlOption.tsx
@@ -1,9 +1,9 @@
import * as React from 'react';
import { classNames, hasReactNode } from '@vkontakte/vkjs';
import { useFocusVisible } from '../../../hooks/useFocusVisible';
+import { useFocusVisibleClassName } from '../../../hooks/useFocusVisibleClassName';
import { callMultiple } from '../../../lib/callMultiple';
import { HasRef, HasRootRef } from '../../../types';
-import { FocusVisible } from '../../FocusVisible/FocusVisible';
import { Headline } from '../../Typography/Headline/Headline';
import { VisuallyHidden } from '../../VisuallyHidden/VisuallyHidden';
import styles from './SegmentedControlOption.module.css';
@@ -28,12 +28,14 @@ export const SegmentedControlOption = ({
...restProps
}: SegmentedControlOptionProps) => {
const { focusVisible, onBlur, onFocus } = useFocusVisible();
+ const focusVisibleClassNames = useFocusVisibleClassName({ focusVisible });
return (
);
};
diff --git a/packages/vkui/src/components/Slider/Slider.e2e.tsx b/packages/vkui/src/components/Slider/Slider.e2e.tsx
index 5793687d0c..2f7c34f9ea 100644
--- a/packages/vkui/src/components/Slider/Slider.e2e.tsx
+++ b/packages/vkui/src/components/Slider/Slider.e2e.tsx
@@ -28,6 +28,7 @@ test.describe('Slider with Tooltip', () => {
max={30}
/>,
);
+ await page.emulateMedia({ reducedMotion: 'reduce' });
await page.keyboard.press('Tab');
await expectScreenshotClippedToContent();
});
@@ -50,6 +51,8 @@ test.describe('keyboard events', () => {
{...componentPlaygroundProps}
/>,
);
+ await page.emulateMedia({ reducedMotion: 'reduce' });
+
const locator = page.getByRole('slider');
const [startSlider, endSlider] = await locator.all();
diff --git a/packages/vkui/src/components/Slider/SliderThumb/SliderThumb.module.css b/packages/vkui/src/components/Slider/SliderThumb/SliderThumb.module.css
index 4f0979c38a..f981497662 100644
--- a/packages/vkui/src/components/Slider/SliderThumb/SliderThumb.module.css
+++ b/packages/vkui/src/components/Slider/SliderThumb/SliderThumb.module.css
@@ -4,13 +4,15 @@
width: var(--vkui_internal--slider_thumb_size);
height: var(--vkui_internal--slider_thumb_size);
border-radius: 50%;
- border: 0.5px solid var(--vkui--color_separator_primary_alpha);
+ border: var(--vkui--size_border--regular) solid var(--vkui--color_separator_primary_alpha);
background: var(--vkui--color_background_contrast);
box-shadow: var(--vkui--elevation3);
user-select: none;
}
-.SliderThumb--focused {
+.SliderThumb--focus-visible {
+ outline: var(--vkui_internal--outline);
+ outline-offset: calc(-1 * var(--vkui--size_border--regular));
z-index: 2;
}
diff --git a/packages/vkui/src/components/Slider/SliderThumb/SliderThumb.tsx b/packages/vkui/src/components/Slider/SliderThumb/SliderThumb.tsx
index 323535cede..74e9b327a2 100644
--- a/packages/vkui/src/components/Slider/SliderThumb/SliderThumb.tsx
+++ b/packages/vkui/src/components/Slider/SliderThumb/SliderThumb.tsx
@@ -3,6 +3,7 @@ import { classNames } from '@vkontakte/vkjs';
import { useBooleanState } from '../../../hooks/useBooleanState';
import { useExternRef } from '../../../hooks/useExternRef';
import { useFocusVisible } from '../../../hooks/useFocusVisible';
+import { useFocusVisibleClassName } from '../../../hooks/useFocusVisibleClassName';
import {
arrowMiddleware,
convertFloatingDataToReactCSSProperties,
@@ -12,7 +13,6 @@ import {
useFloating,
} from '../../../lib/floating';
import type { HasDataAttribute, HasRootRef } from '../../../types';
-import { FocusVisible } from '../../FocusVisible/FocusVisible';
import { TooltipBase } from '../../TooltipBase/TooltipBase';
import styles from './SliderThumb.module.css';
@@ -33,6 +33,10 @@ export const SliderThumb = ({
...restProps
}: SliderThumbProps) => {
const { focusVisible, onBlur, onFocus } = useFocusVisible(false);
+ const focusVisibleClassNames = useFocusVisibleClassName({
+ focusVisible,
+ mode: styles['SliderThumb--focus-visible'],
+ });
const [arrowRef, setArrowRef] = React.useState(null);
const memoizedMiddlewares = React.useMemo(() => {
@@ -89,11 +93,7 @@ export const SliderThumb = ({
ref={handleRootRef}
onMouseEnter={setHoveredTrue}
onMouseLeave={setHoveredFalse}
- className={classNames(
- styles['SliderThumb'],
- focusVisible && styles['SliderThumb--focused'],
- className,
- )}
+ className={classNames(styles['SliderThumb'], focusVisibleClassNames, className)}
>
-
{shouldShowTooltip && (
{
);
};
+
+export const SwitchFocusVisiblePlayground = (props: ComponentPlaygroundProps) => (
+
+ {(props: SwitchProps) => (
+
+
+
+ )}
+
+);
diff --git a/packages/vkui/src/components/Switch/Switch.e2e.tsx b/packages/vkui/src/components/Switch/Switch.e2e.tsx
index 6c29379708..49bd4fe3f0 100644
--- a/packages/vkui/src/components/Switch/Switch.e2e.tsx
+++ b/packages/vkui/src/components/Switch/Switch.e2e.tsx
@@ -1,8 +1,28 @@
import * as React from 'react';
import { test } from '@vkui-e2e/test';
-import { SwitchPlayground } from './Switch.e2e-playground';
+import { Platform } from '../../lib/platform';
+import { SwitchFocusVisiblePlayground, SwitchPlayground } from './Switch.e2e-playground';
test('Switch', async ({ mount, expectScreenshotClippedToContent, componentPlaygroundProps }) => {
await mount();
await expectScreenshotClippedToContent();
});
+
+test.describe('Switch', () => {
+ test.use({
+ onlyForPlatforms: [Platform.ANDROID],
+ onlyForAppearances: ['light'],
+ });
+
+ test('State: Focus Visible', async ({
+ mount,
+ page,
+ expectScreenshotClippedToContent,
+ componentPlaygroundProps,
+ }) => {
+ await mount();
+ await page.emulateMedia({ reducedMotion: 'reduce' });
+ await page.keyboard.press('Tab');
+ await expectScreenshotClippedToContent();
+ });
+});
diff --git a/packages/vkui/src/components/Switch/Switch.tsx b/packages/vkui/src/components/Switch/Switch.tsx
index ec9f6250fa..0639480ae5 100644
--- a/packages/vkui/src/components/Switch/Switch.tsx
+++ b/packages/vkui/src/components/Switch/Switch.tsx
@@ -2,12 +2,12 @@ import * as React from 'react';
import { classNames } from '@vkontakte/vkjs';
import { useAdaptivity } from '../../hooks/useAdaptivity';
import { useFocusVisible } from '../../hooks/useFocusVisible';
+import { useFocusVisibleClassName } from '../../hooks/useFocusVisibleClassName';
import { usePlatform } from '../../hooks/usePlatform';
import { SizeType } from '../../lib/adaptivity';
import { callMultiple } from '../../lib/callMultiple';
import { Platform } from '../../lib/platform';
import { HasRef, HasRootRef } from '../../types';
-import { FocusVisible } from '../FocusVisible/FocusVisible';
import { VisuallyHidden } from '../VisuallyHidden/VisuallyHidden';
import styles from './Switch.module.css';
@@ -28,6 +28,7 @@ export const Switch = ({ style, className, getRootRef, getRef, ...restProps }: S
const platform = usePlatform();
const { sizeY = 'none' } = useAdaptivity();
const { focusVisible, onBlur, onFocus } = useFocusVisible();
+ const focusVisibleClassNames = useFocusVisibleClassName({ focusVisible, mode: 'outside' });
return (
-
);
};
diff --git a/packages/vkui/src/components/Switch/__image_snapshots__/switch-state-focus-visible-android-chromium-light-1-snap.png b/packages/vkui/src/components/Switch/__image_snapshots__/switch-state-focus-visible-android-chromium-light-1-snap.png
new file mode 100644
index 0000000000..beee35e158
--- /dev/null
+++ b/packages/vkui/src/components/Switch/__image_snapshots__/switch-state-focus-visible-android-chromium-light-1-snap.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1344c4dd5b8b5a8f5e11be5948b43c3658142915deba78815c5cf6f14ecae617
+size 1254
diff --git a/packages/vkui/src/components/TabbarItem/TabbarItem.module.css b/packages/vkui/src/components/TabbarItem/TabbarItem.module.css
index 58256531bf..5c0fa5dad5 100644
--- a/packages/vkui/src/components/TabbarItem/TabbarItem.module.css
+++ b/packages/vkui/src/components/TabbarItem/TabbarItem.module.css
@@ -5,7 +5,7 @@
color: var(--vkui--color_tabbar_text_inactive);
text-decoration: none;
border: 0;
- outline: none;
+ outline: var(--vkui_internal--outline-reset);
padding: 0;
background: transparent;
height: var(--vkui_internal--tabbar_height);
diff --git a/packages/vkui/src/components/Tappable/Tappable.e2e-playground.tsx b/packages/vkui/src/components/Tappable/Tappable.e2e-playground.tsx
index 7e791de0cf..a40ed7ce2a 100644
--- a/packages/vkui/src/components/Tappable/Tappable.e2e-playground.tsx
+++ b/packages/vkui/src/components/Tappable/Tappable.e2e-playground.tsx
@@ -1,5 +1,6 @@
import * as React from 'react';
import { ComponentPlayground, type ComponentPlaygroundProps } from '@vkui-e2e/playground-helpers';
+import { type FocusVisibleMode } from '../../hooks/useFocusVisibleClassName';
import { AdaptivityProvider } from '../AdaptivityProvider/AdaptivityProvider';
import { Tappable, type TappableProps } from './Tappable';
@@ -29,3 +30,31 @@ export const TappablePlayground = (props: ComponentPlaygroundProps) => {
);
};
+
+interface TappableFocusVisiblePlaygroundProps extends ComponentPlaygroundProps {
+ mode?: FocusVisibleMode;
+}
+
+const TappableFocusVisible = (props: TappableProps) => (
+
+
+ Tappable:focus-visible
+
+
+);
+
+export const TappableFocusVisiblePlayground = ({
+ mode = 'inside',
+ ...props
+}: TappableFocusVisiblePlaygroundProps) => (
+
+ {TappableFocusVisible}
+
+);
diff --git a/packages/vkui/src/components/Tappable/Tappable.e2e.tsx b/packages/vkui/src/components/Tappable/Tappable.e2e.tsx
index c482422d78..0ebad66670 100644
--- a/packages/vkui/src/components/Tappable/Tappable.e2e.tsx
+++ b/packages/vkui/src/components/Tappable/Tappable.e2e.tsx
@@ -1,8 +1,32 @@
import * as React from 'react';
import { test } from '@vkui-e2e/test';
-import { TappablePlayground } from './Tappable.e2e-playground';
+import { Platform } from '../../lib/platform';
+import { TappableFocusVisiblePlayground, TappablePlayground } from './Tappable.e2e-playground';
test('Tappable', async ({ mount, expectScreenshotClippedToContent, componentPlaygroundProps }) => {
await mount();
await expectScreenshotClippedToContent();
});
+
+test.describe('Tappable', () => {
+ test.describe('State: Focus Visible', () => {
+ test.use({
+ onlyForPlatforms: [Platform.ANDROID],
+ onlyForAppearances: ['light'],
+ });
+
+ ['inside', 'outside'].forEach((mode) => {
+ test(`focusVisibleMode="${mode}"`, async ({
+ mount,
+ page,
+ expectScreenshotClippedToContent,
+ componentPlaygroundProps,
+ }) => {
+ await mount();
+ await page.emulateMedia({ reducedMotion: 'reduce' });
+ await page.keyboard.press('Tab');
+ await expectScreenshotClippedToContent();
+ });
+ });
+ });
+});
diff --git a/packages/vkui/src/components/Tappable/Tappable.module.css b/packages/vkui/src/components/Tappable/Tappable.module.css
index 412c150970..0d75a7339a 100644
--- a/packages/vkui/src/components/Tappable/Tappable.module.css
+++ b/packages/vkui/src/components/Tappable/Tappable.module.css
@@ -4,6 +4,7 @@
cursor: default;
border-radius: var(--vkui--size_border_radius--regular);
transition: background-color 0.15s ease-out, opacity 0.15s ease-out;
+ outline: var(--vkui_internal--outline-reset);
-webkit-tap-highlight-color: transparent;
}
@@ -31,11 +32,6 @@ https://github.com/VKCOM/VKUI/pull/3641
.Tappable[disabled],
.Tappable[aria-disabled='true'] {
cursor: default;
-}
-
-.Tappable:focus,
-.Tappable:focus-visible,
-.Tappable--focus-visible {
outline: none;
}
diff --git a/packages/vkui/src/components/Tappable/Tappable.tsx b/packages/vkui/src/components/Tappable/Tappable.tsx
index 634d590558..f056cff7ff 100644
--- a/packages/vkui/src/components/Tappable/Tappable.tsx
+++ b/packages/vkui/src/components/Tappable/Tappable.tsx
@@ -6,6 +6,10 @@ import { useAdaptivityHasPointer } from '../../hooks/useAdaptivityHasPointer';
import { useBooleanState } from '../../hooks/useBooleanState';
import { useExternRef } from '../../hooks/useExternRef';
import { useFocusVisible } from '../../hooks/useFocusVisible';
+import {
+ type FocusVisibleModeProps,
+ useFocusVisibleClassName,
+} from '../../hooks/useFocusVisibleClassName';
import { usePlatform } from '../../hooks/usePlatform';
import { useTimeout } from '../../hooks/useTimeout';
import { shouldTriggerClickOnEnterOrSpace } from '../../lib/accessibility';
@@ -22,7 +26,6 @@ import {
HasRootRef,
LiteralUnion,
} from '../../types';
-import { FocusVisible, FocusVisibleMode } from '../FocusVisible/FocusVisible';
import { Touch, TouchEvent, TouchProps } from '../Touch/Touch';
import TouchRootContext from '../Touch/TouchContext';
import styles from './Tappable.module.css';
@@ -64,7 +67,8 @@ export interface TappableProps
HasRootRef,
HasComponent,
HasChildren,
- Pick {
+ Pick,
+ FocusVisibleModeProps {
/**
* Длительность показа active-состояния
*/
@@ -94,10 +98,6 @@ export interface TappableProps
* Стиль подсветки hover-состояния. Если передать произвольную строку, она добавится как css-класс во время hover
*/
hoverMode?: LiteralUnion;
- /**
- * Стиль аутлайна focus visible. Если передать произвольную строку, она добавится как css-класс во время focus-visible
- */
- focusVisibleMode?: LiteralUnion;
onEnter?(outputEvent: MouseEvent): void;
onLeave?(outputEvent: MouseEvent): void;
/**
@@ -241,7 +241,6 @@ export const Tappable = ({
Component !== 'a' && Component !== 'button' && Component !== 'label' && !props.contentEditable;
const isPresetHoverMode = isPresetStateMode(hoverMode);
const isPresetActiveMode = isPresetStateMode(activeMode);
- const isPresetFocusVisibleMode = ['inside', 'outside'].includes(focusVisibleMode);
const [activity, { start, stop, delayStart }] = useActivity(hasActive, activeEffectDelay);
const active = activity === TapState.active || activity === TapState.exiting;
@@ -324,6 +323,11 @@ export const Tappable = ({
stop(activeDuration >= 100 ? 0 : activeEffectDelay - activeDuration);
}
+ const focusVisibleClassNames = useFocusVisibleClassName({
+ focusVisible: !props.disabled && focusVisible,
+ mode: focusVisibleMode,
+ });
+
const classes = classNames(
className,
styles['Tappable'],
@@ -334,11 +338,10 @@ export const Tappable = ({
hasActive && styles['Tappable--hasActive'],
hasHover && hovered && !isPresetHoverMode && hoverMode,
hasActive && activated && !isPresetActiveMode && activeMode,
- focusVisible && !isPresetFocusVisibleMode && focusVisibleMode,
hasHover && hovered && isPresetHoverMode && stylesHoverMode[hoverMode],
hasActive && activated && isPresetActiveMode && stylesActiveMode[activeMode],
- focusVisible && styles['Tappable--focus-visible'],
borderRadiusMode === 'inherit' && styles['Tappable--borderRadiusInherit'],
+ focusVisibleClassNames,
);
const handlers: RootComponentProps = {
@@ -384,9 +387,6 @@ export const Tappable = ({
{((hasHover && hoverMode === 'background') || (hasActive && activeMode === 'background')) && (
)}
- {!props.disabled && isPresetFocusVisibleMode && (
-
- )}
);
};
diff --git a/packages/vkui/src/components/Tappable/__image_snapshots__/tappable-state-focus-visible-focusvisiblemode-inside-android-chromium-light-1-snap.png b/packages/vkui/src/components/Tappable/__image_snapshots__/tappable-state-focus-visible-focusvisiblemode-inside-android-chromium-light-1-snap.png
new file mode 100644
index 0000000000..82842b333d
--- /dev/null
+++ b/packages/vkui/src/components/Tappable/__image_snapshots__/tappable-state-focus-visible-focusvisiblemode-inside-android-chromium-light-1-snap.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:deff8ae90a86b08325a96f1b781e7ed85bc5ef1e50bd98391c8ea4483632990d
+size 5127
diff --git a/packages/vkui/src/components/Tappable/__image_snapshots__/tappable-state-focus-visible-focusvisiblemode-outside-android-chromium-light-1-snap.png b/packages/vkui/src/components/Tappable/__image_snapshots__/tappable-state-focus-visible-focusvisiblemode-outside-android-chromium-light-1-snap.png
new file mode 100644
index 0000000000..c657f1924a
--- /dev/null
+++ b/packages/vkui/src/components/Tappable/__image_snapshots__/tappable-state-focus-visible-focusvisiblemode-outside-android-chromium-light-1-snap.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:13ec2bd5a2e5a1b1d13293939a4ffba206e818e282d3a4d9ae83c3e359c0574f
+size 5711
diff --git a/packages/vkui/src/components/Textarea/Textarea.e2e-playground.tsx b/packages/vkui/src/components/Textarea/Textarea.e2e-playground.tsx
index 5e72f05260..693a07b412 100644
--- a/packages/vkui/src/components/Textarea/Textarea.e2e-playground.tsx
+++ b/packages/vkui/src/components/Textarea/Textarea.e2e-playground.tsx
@@ -5,6 +5,7 @@ import { SizeType } from '../../vkui';
import { AdaptivityProvider } from '../AdaptivityProvider/AdaptivityProvider';
import { AppRoot } from '../AppRoot/AppRoot';
import { AppearanceProvider } from '../AppearanceProvider/AppearanceProvider';
+import { Div } from '../Div/Div';
import { Textarea, type TextareaProps } from './Textarea';
export const TextareaPlayground = (props: ComponentPlaygroundProps) => {
@@ -41,9 +42,7 @@ export const TextareaPlayground = (props: ComponentPlaygroundProps) => {
);
};
-export const TextareaTestFitSizeToContentPlayground = ({
- appearance,
-}: ComponentPlaygroundProps) => {
+export const TextareaStatePlayground = ({ appearance }: ComponentPlaygroundProps) => {
return (
-
+
+
+
diff --git a/packages/vkui/src/components/Textarea/Textarea.e2e.tsx b/packages/vkui/src/components/Textarea/Textarea.e2e.tsx
index 74846c1494..c772b0c830 100644
--- a/packages/vkui/src/components/Textarea/Textarea.e2e.tsx
+++ b/packages/vkui/src/components/Textarea/Textarea.e2e.tsx
@@ -1,9 +1,7 @@
import * as React from 'react';
import { test } from '@vkui-e2e/test';
-import {
- TextareaPlayground,
- TextareaTestFitSizeToContentPlayground,
-} from './Textarea.e2e-playground';
+import { Platform } from '../../lib/platform';
+import { TextareaPlayground, TextareaStatePlayground } from './Textarea.e2e-playground';
test('Textarea', async ({ mount, expectScreenshotClippedToContent, componentPlaygroundProps }) => {
await mount();
@@ -11,13 +9,18 @@ test('Textarea', async ({ mount, expectScreenshotClippedToContent, componentPlay
});
test.describe('Textarea', () => {
+ test.use({
+ onlyForPlatforms: [Platform.ANDROID],
+ onlyForAppearances: ['light'],
+ });
+
test('fits size to content', async ({
mount,
page,
expectScreenshotClippedToContent,
componentPlaygroundProps,
}) => {
- await mount();
+ await mount();
await page.locator('#textarea').fill('1\n2\n3\n4\n5\n6\n7\n8');
@@ -33,4 +36,16 @@ test.describe('Textarea', () => {
await expectScreenshotClippedToContent();
});
+
+ test('State: Focus Visible', async ({
+ mount,
+ page,
+ expectScreenshotClippedToContent,
+ componentPlaygroundProps,
+ }) => {
+ await mount();
+ await page.emulateMedia({ reducedMotion: 'reduce' });
+ await page.keyboard.press('Tab');
+ await expectScreenshotClippedToContent();
+ });
});
diff --git a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-android-chromium-dark-1-snap.png b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-android-chromium-dark-1-snap.png
index c6620c3fb8..35cc00ba91 100644
--- a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-android-chromium-dark-1-snap.png
+++ b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-android-chromium-dark-1-snap.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e053b3bd53ab67a95ae0216393aeedb3319f616f46ceffdb8eab43ea302ca369
-size 62306
+oid sha256:e83bdc38b87ca55beaffe89266c3c6968456abf0a8ac9baa28b0c28838a7dc0a
+size 62259
diff --git a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-android-chromium-light-1-snap.png b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-android-chromium-light-1-snap.png
index dcb0129bb7..f4b3159993 100644
--- a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-android-chromium-light-1-snap.png
+++ b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-android-chromium-light-1-snap.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9d33ce1a470cb89675b444613f4d3fbd2777e9b509bf70a2b85b1287865006c2
-size 62303
+oid sha256:00b2429cb4709aac8a9fbc7401efb7cc968eebc26c63e2d3a1db9f958848bae6
+size 62335
diff --git a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-android-chromium-dark-1-snap.png b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-android-chromium-dark-1-snap.png
deleted file mode 100644
index 830b0d9472..0000000000
--- a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-android-chromium-dark-1-snap.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:1dd4c4ed9fbe1ca63457eefd792e3dd4b97669e8119abd57f2c06651e3329a6c
-size 3428
diff --git a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-android-chromium-dark-2-snap.png b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-android-chromium-dark-2-snap.png
deleted file mode 100644
index 9e9679e839..0000000000
--- a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-android-chromium-dark-2-snap.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:2864852a32314996f322383253074fbfb634b8cb23d0b5bf78536c106d93afd5
-size 1195
diff --git a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-android-chromium-light-1-snap.png b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-android-chromium-light-1-snap.png
index 830b0d9472..474f7215b4 100644
--- a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-android-chromium-light-1-snap.png
+++ b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-android-chromium-light-1-snap.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1dd4c4ed9fbe1ca63457eefd792e3dd4b97669e8119abd57f2c06651e3329a6c
-size 3428
+oid sha256:0d5d1fcf742f0dae678fa7b3eee91ac0e8d105dc92982b0f41436ceeb23a95e9
+size 3608
diff --git a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-android-chromium-light-2-snap.png b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-android-chromium-light-2-snap.png
index 9e9679e839..cf516bfaa4 100644
--- a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-android-chromium-light-2-snap.png
+++ b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-android-chromium-light-2-snap.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2864852a32314996f322383253074fbfb634b8cb23d0b5bf78536c106d93afd5
-size 1195
+oid sha256:5b7ba5e15c85e1abcf41d03f0d7e9b434ef24d644d6cc187cb3dda9a99ffdd00
+size 1330
diff --git a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-ios-webkit-dark-1-snap.png b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-ios-webkit-dark-1-snap.png
deleted file mode 100644
index cbdefcef68..0000000000
--- a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-ios-webkit-dark-1-snap.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:4a08dba2b6155e54b93a92f00628beabde95ec743fd55ceb8f2ec1f09bad8d81
-size 3150
diff --git a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-ios-webkit-dark-2-snap.png b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-ios-webkit-dark-2-snap.png
deleted file mode 100644
index 6001f3d80b..0000000000
--- a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-ios-webkit-dark-2-snap.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:9a44b0f91667269116d5110a0b0c2d3d56f468487bfff2fc79d0f0e4d264c490
-size 1105
diff --git a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-ios-webkit-light-1-snap.png b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-ios-webkit-light-1-snap.png
deleted file mode 100644
index cbdefcef68..0000000000
--- a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-ios-webkit-light-1-snap.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:4a08dba2b6155e54b93a92f00628beabde95ec743fd55ceb8f2ec1f09bad8d81
-size 3150
diff --git a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-ios-webkit-light-2-snap.png b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-ios-webkit-light-2-snap.png
deleted file mode 100644
index 6001f3d80b..0000000000
--- a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-ios-webkit-light-2-snap.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:9a44b0f91667269116d5110a0b0c2d3d56f468487bfff2fc79d0f0e4d264c490
-size 1105
diff --git a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-chromium-dark-1-snap.png b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-chromium-dark-1-snap.png
deleted file mode 100644
index 92085c6828..0000000000
--- a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-chromium-dark-1-snap.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:17acb14d82494d979092dacbedf430fc30ca5a46452f003287baa10b37715ca6
-size 3435
diff --git a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-chromium-dark-2-snap.png b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-chromium-dark-2-snap.png
deleted file mode 100644
index 8057cac504..0000000000
--- a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-chromium-dark-2-snap.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:943f404d6fd21860919b4a7301d5f10d1ed35b39a7d54a82811796299f108c7a
-size 1198
diff --git a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-chromium-light-1-snap.png b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-chromium-light-1-snap.png
deleted file mode 100644
index 92085c6828..0000000000
--- a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-chromium-light-1-snap.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:17acb14d82494d979092dacbedf430fc30ca5a46452f003287baa10b37715ca6
-size 3435
diff --git a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-chromium-light-2-snap.png b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-chromium-light-2-snap.png
deleted file mode 100644
index 8057cac504..0000000000
--- a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-chromium-light-2-snap.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:943f404d6fd21860919b4a7301d5f10d1ed35b39a7d54a82811796299f108c7a
-size 1198
diff --git a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-firefox-dark-1-snap.png b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-firefox-dark-1-snap.png
deleted file mode 100644
index 580563b0fe..0000000000
--- a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-firefox-dark-1-snap.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:0b98ec737ef57c5c381d1cf36006a89a10388e48d367838d26c3f412afcc06c3
-size 4466
diff --git a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-firefox-dark-2-snap.png b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-firefox-dark-2-snap.png
deleted file mode 100644
index d598b5f761..0000000000
--- a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-firefox-dark-2-snap.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:140a57eea0a631ac4ddad4085cb112f4a61aee01069df40f390dbff5c8ccba5e
-size 1516
diff --git a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-firefox-light-1-snap.png b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-firefox-light-1-snap.png
deleted file mode 100644
index 580563b0fe..0000000000
--- a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-firefox-light-1-snap.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:0b98ec737ef57c5c381d1cf36006a89a10388e48d367838d26c3f412afcc06c3
-size 4466
diff --git a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-firefox-light-2-snap.png b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-firefox-light-2-snap.png
deleted file mode 100644
index d598b5f761..0000000000
--- a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-firefox-light-2-snap.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:140a57eea0a631ac4ddad4085cb112f4a61aee01069df40f390dbff5c8ccba5e
-size 1516
diff --git a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-webkit-dark-1-snap.png b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-webkit-dark-1-snap.png
deleted file mode 100644
index ef5215c56d..0000000000
--- a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-webkit-dark-1-snap.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:fe7fcba7fd75a40c6165753ae1783478ba981e1ff2d95783ad9f3056698a378a
-size 3146
diff --git a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-webkit-dark-2-snap.png b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-webkit-dark-2-snap.png
deleted file mode 100644
index 827b780775..0000000000
--- a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-webkit-dark-2-snap.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:525a79a1ae585e7745aefd8c388987a06eb20330883be4aca09bd11167a35df8
-size 1095
diff --git a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-webkit-light-1-snap.png b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-webkit-light-1-snap.png
deleted file mode 100644
index ef5215c56d..0000000000
--- a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-webkit-light-1-snap.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:fe7fcba7fd75a40c6165753ae1783478ba981e1ff2d95783ad9f3056698a378a
-size 3146
diff --git a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-webkit-light-2-snap.png b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-webkit-light-2-snap.png
deleted file mode 100644
index 827b780775..0000000000
--- a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-fits-size-to-content-vkcom-webkit-light-2-snap.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:525a79a1ae585e7745aefd8c388987a06eb20330883be4aca09bd11167a35df8
-size 1095
diff --git a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-state-focus-visible-android-chromium-light-1-snap.png b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-state-focus-visible-android-chromium-light-1-snap.png
new file mode 100644
index 0000000000..86c15c714f
--- /dev/null
+++ b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-state-focus-visible-android-chromium-light-1-snap.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:745d662f7282b63baf0c63f5a2d5f0692feeee990a118553a29c9a7279ee45a7
+size 955
diff --git a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-vkcom-chromium-dark-1-snap.png b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-vkcom-chromium-dark-1-snap.png
index 2784003c70..3bbf06d143 100644
--- a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-vkcom-chromium-dark-1-snap.png
+++ b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-vkcom-chromium-dark-1-snap.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0b24a9c9bf0174f770c8adfccc33c5105e1c81586b5791b63aa52e61ac37ae74
-size 55472
+oid sha256:2e57cb954a0b9e7fb7c4e617223c9e35a554061fda9dada75d1327bfe6187040
+size 55411
diff --git a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-vkcom-chromium-light-1-snap.png b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-vkcom-chromium-light-1-snap.png
index 53dd27da1c..8f29cafc62 100644
--- a/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-vkcom-chromium-light-1-snap.png
+++ b/packages/vkui/src/components/Textarea/__image_snapshots__/textarea-vkcom-chromium-light-1-snap.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0623df1662dadffd4e8e29196bfbdfc5c4755ae266fb20d1cdb4f989e1eb4c94
-size 55404
+oid sha256:8d41ccdd63afa156604dd0fc8878ac6735000eead30c9197e38b77f62d601ff8
+size 55309
diff --git a/packages/vkui/src/components/WriteBar/WriteBar.module.css b/packages/vkui/src/components/WriteBar/WriteBar.module.css
index 95389c2659..66eb81bd78 100644
--- a/packages/vkui/src/components/WriteBar/WriteBar.module.css
+++ b/packages/vkui/src/components/WriteBar/WriteBar.module.css
@@ -45,7 +45,7 @@
}
.WriteBar__textarea:focus {
- outline: none;
+ outline: var(--vkui_internal--outline-reset);
}
.WriteBar__inlineAfter {
diff --git a/packages/vkui/src/hooks/useFocusVisibleClassName.test.ts b/packages/vkui/src/hooks/useFocusVisibleClassName.test.ts
new file mode 100644
index 0000000000..9336204c18
--- /dev/null
+++ b/packages/vkui/src/hooks/useFocusVisibleClassName.test.ts
@@ -0,0 +1,46 @@
+import { act, renderHook } from '@testing-library/react-hooks';
+import { classNames } from '@vkontakte/vkjs';
+import {
+ focusVisiblePresetModeClassNames as modeClassNames,
+ useFocusVisibleClassName,
+ type UseFocusVisibleClassNameProps,
+} from './useFocusVisibleClassName';
+import styles from '../styles/focusVisible.module.css';
+
+const focusedClasses = classNames(styles['-focus-visible'], styles['-focus-visible--focused']);
+
+const test = (hookConfig: UseFocusVisibleClassNameProps, resultClassNameString: string) => {
+ const { result } = renderHook(() => useFocusVisibleClassName(hookConfig));
+
+ act(() => {
+ expect(result.current).toEqual(resultClassNameString);
+ });
+};
+
+describe('useFocusVisibleClassName()', () => {
+ it('focusVisible: false returns proper class', () => {
+ test({ focusVisible: false }, styles['-focus-visible']);
+ });
+
+ it('focusVisible: true (default mode) returns proper classes', () => {
+ test({ focusVisible: true }, classNames(focusedClasses, modeClassNames['inside']));
+ });
+
+ it('focusVisible: true (preset mode: outside) returns proper classes', () => {
+ test(
+ { focusVisible: true, mode: 'outside' },
+ classNames(focusedClasses, modeClassNames['outside']),
+ );
+ });
+
+ it('focusVisible: true (custom mode: customClass) returns proper classes', () => {
+ test({ focusVisible: true, mode: 'customClass' }, classNames(focusedClasses, 'customClass'));
+ });
+
+ it('focusVisible: true (complex mode: customClass, inside) returns proper classes', () => {
+ test(
+ { focusVisible: true, mode: classNames('customClass', modeClassNames['inside']) },
+ classNames(focusedClasses, 'customClass', modeClassNames['inside']),
+ );
+ });
+});
diff --git a/packages/vkui/src/hooks/useFocusVisibleClassName.ts b/packages/vkui/src/hooks/useFocusVisibleClassName.ts
new file mode 100644
index 0000000000..28cd1154a8
--- /dev/null
+++ b/packages/vkui/src/hooks/useFocusVisibleClassName.ts
@@ -0,0 +1,48 @@
+import { classNames } from '@vkontakte/vkjs';
+import { LiteralUnion } from '../types';
+import styles from '../styles/focusVisible.module.css';
+
+export const focusVisiblePresetModeClassNames = {
+ inside: styles['-focus-visible--mode-inside'],
+ outside: styles['-focus-visible--mode-outside'],
+};
+
+type FocusVisiblePresetMode = keyof typeof focusVisiblePresetModeClassNames;
+
+export type FocusVisibleMode = LiteralUnion;
+
+const isPresetMode = (mode: FocusVisibleMode): mode is FocusVisiblePresetMode =>
+ mode === 'inside' || mode === 'outside';
+
+export interface FocusVisibleModeProps {
+ /**
+ * Стиль аутлайна focus visible. Если передать произвольную строку, она добавится как css-класс при :focus-visible
+ */
+ focusVisibleMode?: FocusVisibleMode;
+}
+
+export interface UseFocusVisibleClassNameProps {
+ focusVisible?: boolean;
+ mode?: FocusVisibleMode;
+}
+
+/**
+ * Используется для проброса классов состояния :focus-visible в компонент.
+ *
+ * Рулит исключительно классами. Чтобы определить, есть ли фокусное состояние,
+ * используйте хуки `useFocusVisible()` и `useFocusWithin()`.
+ */
+export function useFocusVisibleClassName({
+ focusVisible = false,
+ mode = 'inside',
+}: UseFocusVisibleClassNameProps) {
+ const modeClassName = isPresetMode(mode) ? focusVisiblePresetModeClassNames[mode] : mode;
+
+ const focusVisibleClassNames = classNames(
+ styles['-focus-visible'],
+ focusVisible && styles['-focus-visible--focused'],
+ focusVisible && modeClassName,
+ );
+
+ return focusVisibleClassNames;
+}
diff --git a/packages/vkui/src/index.ts b/packages/vkui/src/index.ts
index b14b9b79fd..da5f216083 100644
--- a/packages/vkui/src/index.ts
+++ b/packages/vkui/src/index.ts
@@ -3,6 +3,7 @@ import './lib/polyfills';
import './styles/constants.css';
import './styles/adaptivity.module.css';
import './styles/dynamicTokens.css';
+import './styles/focusVisible.module.css';
export { AppRoot } from './components/AppRoot/AppRoot';
export type { AppRootProps } from './components/AppRoot/AppRoot';
diff --git a/packages/vkui/src/styles/constants.css b/packages/vkui/src/styles/constants.css
index 4de7c35054..ab2f8d4059 100644
--- a/packages/vkui/src/styles/constants.css
+++ b/packages/vkui/src/styles/constants.css
@@ -8,6 +8,20 @@
var(--vkui--size_base_padding_horizontal--regular) - var(--vkui--spacing_size_s)
);
+ /**
+ * [a11y][focus visible]
+ */
+ --vkui_internal--outline_width: 2px;
+ --vkui_internal--outline: var(--vkui_internal--outline_width, 2px) solid
+ var(--vkui--color_stroke_accent);
+ /**
+ * [a11y][windows high contrast mode]
+ *
+ * windows compatible outline reset
+ * @see https://benmyers.dev/blog/whcm-outlines/
+ */
+ --vkui_internal--outline-reset: var(--vkui_internal--outline_width) solid transparent;
+
/**
* "Safe Zone" добавляет невидимую интерактивную область, по которой пользователь будет вводить мышкой и тем самым
* компонент будет оставаться активным.
@@ -57,6 +71,7 @@
--vkui_internal--z_index_form_field_side: 6;
/* z_index ImageBase isolate */
+ --vkui_internal--z_index_image_base_img: -1;
--vkui_internal--z_index_image_base_overlay: 0;
--vkui_internal--z_index_image_base_border: 1;
--vkui_internal--z_index_image_base_badge: 2;
diff --git a/packages/vkui/src/styles/focusVisible.module.css b/packages/vkui/src/styles/focusVisible.module.css
new file mode 100644
index 0000000000..66ff75acf5
--- /dev/null
+++ b/packages/vkui/src/styles/focusVisible.module.css
@@ -0,0 +1,65 @@
+/**
+ * Утилитарные классы на замену отдельному компоненту для имитации
+ * :focus-visible состояния.
+ */
+.-focus-visible {
+ --vkui_internal--outline_width: 2px;
+}
+
+/* stylelint-disable-next-line @project-tools/stylelint-atomic, selector-max-universal */
+.-focus-visible:focus,
+.-focus-visible:focus-visible,
+.-focus-visible *:focus,
+.-focus-visible *:focus-visible {
+ outline: none;
+}
+
+.-focus-visible.-focus-visible--mode-outside {
+ --vkui_internal--outline_offset: var(--vkui_internal--outline_width);
+}
+
+.-focus-visible.-focus-visible--mode-inside {
+ --vkui_internal--outline_offset: calc(-1 * var(--vkui_internal--outline_width));
+}
+
+.-focus-visible.-focus-visible--focused.-focus-visible--mode-inside,
+.-focus-visible.-focus-visible--focused.-focus-visible--mode-outside {
+ outline: var(--vkui_internal--outline);
+ outline-offset: var(--vkui_internal--outline_offset);
+}
+
+/**
+ * [a11y][progressive enhancement]
+ * @see https://github.com/VKCOM/VKUI/issues/2703
+ *
+ * TODO [a11y]: перенести комментарий в задачу по ссылке выше
+ *
+ * add animation for browsers that support prefers-reduced-motion
+ * without support there are no animations; this way, users with
+ * vestibular motion disorders are not affected
+ *
+ * добавляем анимации для браузеров с поддержкой prefers-reduced-motion
+ * без поддержки анимаций нет вообще; так юзеры с проблемами с
+ * вестибулярным аппаратом могут пользоваться приложениями на vkui с
+ * клавиатуры без спецэффектов
+ */
+@media (prefers-reduced-motion: no-preference) {
+ .-focus-visible.-focus-visible--focused.-focus-visible--mode-inside,
+ .-focus-visible.-focus-visible--focused.-focus-visible--mode-outside {
+ outline-offset: 0;
+ animation: animation-outline-offset 0.1s ease-in-out 0.01s forwards;
+ }
+
+ .-focus-visible.-focus-visible--focused.-focus-visible--mode-inside {
+ outline-offset: calc(-2 * var(--vkui_internal--outline_width));
+ }
+}
+
+@keyframes animation-outline-offset {
+ 0% {
+ }
+
+ 100% {
+ outline-offset: var(--vkui_internal--outline_offset);
+ }
+}
diff --git a/styleguide/config.js b/styleguide/config.js
index aec0631ede..70fa33d494 100644
--- a/styleguide/config.js
+++ b/styleguide/config.js
@@ -97,7 +97,7 @@ const baseConfig = {
borderRadius: 'none',
},
'& textarea:focus': {
- outline: 0,
+ outline: `2px solid transparent`,
borderColor: `none !important`,
},
},