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 ( -