diff --git a/packages/vkui/src/components/Popover/Popover.module.css b/packages/vkui/src/components/Popover/Popover.module.css index ba02465322..db489eb47f 100644 --- a/packages/vkui/src/components/Popover/Popover.module.css +++ b/packages/vkui/src/components/Popover/Popover.module.css @@ -1,9 +1,5 @@ .Popover { position: relative; - animation: popover-fade-in 0.2s ease; - background: var(--vkui--color_background_modal); - border-radius: var(--vkui--size_border_radius--regular); - box-shadow: var(--vkui--elevation3); } /* Создаём "Safe Zone" */ @@ -17,12 +13,8 @@ position: relative; } -@keyframes popover-fade-in { - from { - opacity: 0; - } - - to { - opacity: 1; - } +.Popover__in--withStyling { + background-color: var(--vkui--color_background_modal); + border-radius: var(--vkui--size_border_radius--regular); + box-shadow: var(--vkui--elevation3); } diff --git a/packages/vkui/src/components/Popover/Popover.stories.tsx b/packages/vkui/src/components/Popover/Popover.stories.tsx index d55ec38ff6..abf1792deb 100644 --- a/packages/vkui/src/components/Popover/Popover.stories.tsx +++ b/packages/vkui/src/components/Popover/Popover.stories.tsx @@ -1,14 +1,20 @@ import * as React from 'react'; import { Meta, StoryObj } from '@storybook/react'; +import { Icon16Clear, Icon28AddOutline, Icon28DeleteOutline } from '@vkontakte/icons'; import { DisableCartesianParam } from '../../storybook/constants'; +import { getAvatarUrl } from '../../testing/mock'; +import { Avatar } from '../Avatar/Avatar'; import { Button } from '../Button/Button'; +import { CellButton } from '../CellButton/CellButton'; import { Checkbox } from '../Checkbox/Checkbox'; import { Div } from '../Div/Div'; import { FormItem } from '../FormItem/FormItem'; import { FormLayout } from '../FormLayout/FormLayout'; +import { Group } from '../Group/Group'; +import { IconButton } from '../IconButton/IconButton'; import { Input } from '../Input/Input'; import { Text } from '../Typography/Text/Text'; -import { Popover, PopoverProps } from './Popover'; +import { Popover, type PopoverOnShownChange, type PopoverProps } from './Popover'; const story: Meta = { title: 'Poppers/Popover', @@ -23,8 +29,10 @@ type Story = StoryObj; export const Playground: Story = { render: (args) => ( Привет @@ -32,53 +40,179 @@ export const Playground: Story = { } {...args} > - + ), }; export const Example: Story = { render: function Render() { - const [shown, setShown] = React.useState(true); - - return ( - <> + const PopoverWithTriggerHover = () => { + return ( Привет } > - + + ); + }; + const PopoverWithTriggerClick = () => { + return ( ( + + } onClick={onClose}> + Добавить + + } + mode="danger" + onClick={onClose} + > + Удалить + + + )} + > + + + ); + }; + + const PopoverWithTriggerFocus = () => { + return ( + ( - - + + - - + + Согласен - + + )} + > + + + ); + }; + + const PopoverWithAllTriggers = () => { + return ( + + + } > - + - + ); + }; + + const PopoverWithTriggerManual = () => { + const [shown, setShown] = React.useState(false); + + const handleShownChange: PopoverOnShownChange = React.useCallback((value, reason) => { + if (!value) { + switch (reason) { + case 'callback': + case 'escape-key': + case 'click-outside': + setShown(false); + break; + default: + break; + } + } + }, []); + + return ( + ( +
+
+ + + +
+
+ The cake +
+ is +
a lie +
+
+ )} + onShownChange={handleShownChange} + > + +
+ ); + }; + + return ( +
+ + + + + +
); }, }; diff --git a/packages/vkui/src/components/Popover/Popover.test.tsx b/packages/vkui/src/components/Popover/Popover.test.tsx new file mode 100644 index 0000000000..d285468e8f --- /dev/null +++ b/packages/vkui/src/components/Popover/Popover.test.tsx @@ -0,0 +1,51 @@ +import * as React from 'react'; +import { render } from '@testing-library/react'; +import { baselineComponent, waitForFloatingPosition } from '../../testing/utils'; +import { Popover, type PopoverProps } from './Popover'; + +describe(Popover, () => { + baselineComponent((props) => ( + +
Test
+
+ )); + + it('should provide zIndex to popover element', async () => { + const result = render( + +
Target
+
, + ); + await waitForFloatingPosition(); + expect(result.getByTestId('popover').parentElement).toHaveStyle('z-index: 100500'); + }); + + it('should injects aria-expanded attr to target element if correct role provided', async () => { + const Fixture = ({ shown }: PopoverProps) => ( + 1} + > +
+ Target +
+
+ ); + const result = render(); + await waitForFloatingPosition(); + expect(result.getByTestId('target')).toHaveAttribute('aria-expanded', 'true'); + result.rerender(); + await waitForFloatingPosition(); + expect(result.getByTestId('target')).toHaveAttribute('aria-expanded', 'false'); + }); +}); diff --git a/packages/vkui/src/components/Popover/Popover.tsx b/packages/vkui/src/components/Popover/Popover.tsx index 88a04cf3ae..0ab4973961 100644 --- a/packages/vkui/src/components/Popover/Popover.tsx +++ b/packages/vkui/src/components/Popover/Popover.tsx @@ -1,202 +1,175 @@ import * as React from 'react'; import { classNames } from '@vkontakte/vkjs'; -import { useEventListener } from '../../hooks/useEventListener'; -import { useExternRef } from '../../hooks/useExternRef'; -import { useGlobalEventListener } from '../../hooks/useGlobalEventListener'; import { usePatchChildren } from '../../hooks/usePatchChildren'; -import { useTimeout } from '../../hooks/useTimeout'; -import { useDOM } from '../../lib/dom'; -import { FocusTrap, FocusTrapProps } from '../FocusTrap/FocusTrap'; -import { Popper, PopperCommonProps } from '../Popper/Popper'; +import { injectAriaExpandedPropByRole } from '../../lib/accessibility'; +import { createPortal } from '../../lib/createPortal'; +import { animationFadeClassNames, transformOriginClassNames } from '../../lib/cssAnimation'; +import { getDocumentBody } from '../../lib/dom'; +import { + type OnShownChange, + useFloatingWithInteractions, + type UseFloatingWithInteractionsProps, + type UseFloatingWithInteractionsReturn, +} from '../../lib/floating'; +import type { HTMLAttributesWithRootRef } from '../../types'; +import { FocusTrap } from '../FocusTrap/FocusTrap'; import styles from './Popover.module.css'; +export type PopoverOnShownChange = OnShownChange; + +export type PopoverContentRenderProp = ( + props: Pick, +) => React.ReactNode; + export interface PopoverProps - extends Omit< - PopperCommonProps, - 'arrow' | 'arrowClassName' | 'arrowHeight' | 'arrowPadding' | 'ArrowIcon' | 'content' - >, - Pick { + extends UseFloatingWithInteractionsProps, + Omit< + HTMLAttributesWithRootRef, + keyof UseFloatingWithInteractionsProps | 'content' + > { /** - * Механика вызова всплывающего окна. - * - * - `"click"` – показывается/скрывается только при нажатии. - * - `"hover"` – помимо нажатия, будет показываться/скрывается при наведении/отведении мыши. + * Содержимое всплывающего окна. * - * > ⚠️`"hover"` на тач-устройствах будет работать как `"click"`, с одним лишь нюансом, что не будет закрываться - * > при повторном нажатии на целевой элемент. Для закрытия необходимо нажать на область вне целевого элемента - * > и выпадающего окна. - */ - action?: 'click' | 'hover'; - /** - * Если передан, то всплывающее окно будет показано/скрыто в зависимости от значения свойства. + * При передаче контента в виде [render prop](https://react.dev/reference/react/cloneElement#passing-data-with-a-render-prop), + * в аргументе функции можно получить метод `onClose`, с помощью которого можно программно закрывать + * всплывающее окно. */ - shown?: boolean; + content?: React.ReactNode | PopoverContentRenderProp; /** - * Количество миллисекунд, после которых произойдёт показ всплывающего окна. + * Целевой элемент. Всплывающее окно появится возле него. * - * > Используется только для `action="hover"` при наведении/отведении мыши. + * > ⚠️ Если это пользовательский компонент, то он должен: + * > 1. предоставлять параметры либо `getRootRef`, либо `ref` (cм. `React.forwardRef()`) для получения ссылки на DOM-узел; + * > 2. принимать DOM атрибуты и события. */ - showDelay?: number; + children?: React.ReactElement; /** - * Количество миллисекунд, после которых произойдёт скрытие всплывающего окна. - * - * > Используется только для `action="hover"` при наведении/отведении мыши. + * Нужно ли при навигации с клавиатуры авто-фокусироваться на всплывающий элемент. */ - hideDelay?: number; + autoFocus?: boolean; /** - * Содержимое всплывающего окна. + * Нужно ли после закрытия всплывающего элемента возвращать фокус на предыдущий активный элемент. */ - content?: React.ReactNode; + restoreFocus?: boolean; /** - * Целевой элемент. Всплывающее окно появится возле него. + * Отключает у всплывающего элемента стилизацию по умолчанию, а именно: + * - background + * - border-radius + * - box-shadow * - * > ⚠️ Если это пользовательский компонент, то он должен предоставлять параметры либо `getRootRef`, либо `ref` для получения ссылки на DOM-узел. + * Используется в случае, если необходимо стилизовать по своему. */ - children?: React.ReactElement; + noStyling?: boolean; + /** + * Перебивает zIndex заданный по умолчанию. + */ + zIndex?: number | string; /** - * Вызывается при каждом изменении видимости всплывающего окна. + * По умолчанию используется document.body. */ - onShownChange?(shown: boolean): void; + usePortal?: boolean | Element | DocumentFragment; } /** * @see https://vkcom.github.io/VKUI/#/Popover */ export const Popover = ({ - action = 'click', - shown: shownProp, - showDelay = 150, - hideDelay = 150, - offsetDistance = 8, + // UsePopoverProps + placement: expectedPlacement = 'bottom-start', + trigger = 'click', content, - children, - style: styleProp, - className, - getRootRef, + hoverDelay = 150, + offsetByMainAxis = 8, + offsetByCrossAxis = 0, + disabled, + disableInteractive, + disableCloseOnClickOutside, + disableCloseOnEscKey, + // uncontrolled + defaultShown = false, + // controlled + shown: shownProp, onShownChange, - restoreFocus = true, - ...restProps -}: PopoverProps) => { - const { document } = useDOM(); - - const hoverable = action === 'hover'; - const hovered = React.useRef(false); - const [computedShown, setComputedShown] = React.useState(shownProp || false); - const [dropdownNode, setPopperNode] = React.useState(null); - const shown = typeof shownProp === 'boolean' ? shownProp : computedShown; + // Для createPortal + usePortal = true, - const patchedPopperRef = useExternRef(setPopperNode, getRootRef); - - const [childRef, child] = usePatchChildren(children); - - const setShown = (value: boolean) => { - if (typeof shownProp !== 'boolean') { - setComputedShown(value); - } - typeof onShownChange === 'function' && onShownChange(value); - }; - - const showTimeout = useTimeout(() => setShown(true), showDelay); - - const hideTimeout = useTimeout(() => setShown(false), hideDelay); - - const handleTargetEnter = () => { - hovered.current = true; - hideTimeout.clear(); - showTimeout.set(); - }; - - const handleTargetClick = () => { - if (hovered.current && shown) { - return; - } - setShown(!shown); - }; - - const handleTargetLeave = () => { - hovered.current = false; - showTimeout.clear(); - hideTimeout.set(); - }; - - const handleContentKeyDownEscape = () => { - setShown(false); - }; - - const handleOutsideClick = (e: MouseEvent) => { - if ( - dropdownNode && - !childRef.current?.contains(e.target as Node) && - !dropdownNode.contains(e.target as Node) - ) { - setShown(false); - } - }; - - useGlobalEventListener(document, 'click', handleOutsideClick, { - capture: true, - passive: true, + // FocusTrapProps + autoFocus = true, + restoreFocus = true, + className, + children, + noStyling = false, + zIndex = 'var(--vkui--z_index_popout)', + // a11y + role, + ...restPopoverProps +}: PopoverProps) => { + const { + placement, + shown, + willBeHide, + refs, + referenceProps, + floatingProps, + onClose, + onRestoreFocus, + onEscapeKeyDown, + } = useFloatingWithInteractions({ + placement: expectedPlacement, + trigger, + hoverDelay, + offsetByMainAxis, + offsetByCrossAxis, + disabled, + disableInteractive, + disableCloseOnClickOutside, + disableCloseOnEscKey, + defaultShown, + shown: shownProp, + onShownChange, }); - const targetEnterListener = useEventListener('mouseenter', handleTargetEnter); - const targetClickEvent = useEventListener('click', handleTargetClick); - const targetLeaveListener = useEventListener('mouseleave', handleTargetLeave); - - React.useEffect(() => { - if (!childRef.current) { - return; - } - targetClickEvent.add(childRef.current); - }, [childRef, targetClickEvent]); - - React.useEffect(() => { - if (!childRef.current) { - return; - } - - if (hoverable) { - targetEnterListener.add(childRef.current); - targetLeaveListener.add(childRef.current); - } + const [childRef, child] = usePatchChildren( + children, + injectAriaExpandedPropByRole(referenceProps, shown, role), + refs.setReference, + ); - return () => { - targetEnterListener.remove(); - targetLeaveListener.remove(); - }; - }, [childRef, hoverable, targetEnterListener, targetLeaveListener]); + let popover: React.ReactNode = null; + if (shown) { + floatingProps.style.zIndex = String(zIndex); + popover = ( +
+ + {typeof content === 'function' ? content({ onClose }) : content} + +
+ ); + } return ( {child} - {shown && ( - ( - - {content} - - )} - onMouseOver={hoverable ? hideTimeout.clear : undefined} - onMouseOut={hoverable ? handleTargetLeave : undefined} - /> - )} + {usePortal && popover + ? createPortal( + popover, + typeof usePortal !== 'boolean' ? usePortal : getDocumentBody(childRef.current), + ) + : popover} ); }; diff --git a/packages/vkui/src/components/Popover/Readme.md b/packages/vkui/src/components/Popover/Readme.md index 99c10e9a36..bd4edeb389 100644 --- a/packages/vkui/src/components/Popover/Readme.md +++ b/packages/vkui/src/components/Popover/Readme.md @@ -1,32 +1,98 @@ -> **Важно** -> -> Это нестабильный компонент. Его API может меняться в рамках одной мажорной версии. [Подробнее про нестабильные компоненты](https://vkcom.github.io/VKUI/#/Unstable). +Утилитарный компонент, предназначенный для отрисовки части пользовательского интерфейса в выпадающем окне. -Может открываться при клике или наведении мыши на `children`. Ограничений по содержимому нет. Предназначен для -отрисовки части интерфейса в выпадающем окне. +Может открываться при следующих DOM событиях на переданном `children`: -```jsx { "props": { "layout": false, "iframe": true } } -const [shown, setShown] = React.useState(true); +- при нажатии; +- при наведении; +- при фокусе; -return ( - +или при комбинация этих событий. + +Также есть ручной режим, где компонент будет показываться только программно. Подробнее в примерах на этой странице. + +## Цифровая доступность (a11y) + +Старайтесь сопровождать элемент текстовым описанием для корректной работы скринридеров. Для этого +необходимо вручную передавать некоторые параметры. +
+ +- У всплывающего элемента обязательно должен быть указан [`role`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles). + Зачастую это либо [`"tooltip"`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tooltip_role), + либо [`"menu"`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/menu_role), + либо [`"dialog"`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/dialog_role). +- У целевого элемента, в зависимости от `role` у всплывающего элемента, должны быть заданы атрибуты + `aria-*`. Какие именно можно ознакомиться в документации конкретного `role`. + +> **Исключение:** `aria-expanded` компонент выставляет самостоятельно в зависимости от `role`, +> поэтому об этом атрибуте можно не беспокоиться. + +Примеры ниже соблюдают хорошие практики по a11y, вы можете ориентироваться на них. + +````jsx { "props": { "layout": false, "iframe": true } } +const PopoverWithTriggerHover = () => { + return ( Привет } > - + + ); +}; +const PopoverWithTriggerClick = () => { + return ( ( +
+ } onClick={onClose}> + Добавить + + } + mode="danger" + onClick={onClose} + > + Удалить + +
+ )} + > + +
+ ); +}; + +const PopoverWithTriggerFocus = () => { + return ( + ( @@ -38,13 +104,111 @@ return ( Согласен - + + )} + > + + + ); +}; + +const PopoverWithAllTriggers = () => { + return ( + + + + } + > + + + ); +}; + +const PopoverWithTriggerManual = () => { + const [shown, setShown] = React.useState(false); + + // Если вы используете TypeScript, то можете импортировать тип функции: + // + // ```ts + // import type { PopoverOnShownChange } from '@vkontakte/vkui'; + // + // const handleShownChange: PopoverOnShownChange = React.useCallback(() => {}, []); + // ``` + const handleShownChange = React.useCallback((value, reason) => { + if (!value) { + switch (reason) { + case 'callback': + case 'escape-key': + case 'click-outside': + setShown(false); + break; + default: + break; } + } + }, []); + + return ( + ( +
+
+ + + +
+
+ The cake +
+ is +
a lie +
+
+ )} + onShownChange={handleShownChange} > - +
-
-); -``` + ); +}; + +const Playground = () => { + return ( +
+ + + + + +
+ ); +}; + +; +```` diff --git a/packages/vkui/src/index.ts b/packages/vkui/src/index.ts index bd6712fee6..3c4f4bbe9b 100644 --- a/packages/vkui/src/index.ts +++ b/packages/vkui/src/index.ts @@ -2,6 +2,8 @@ import './styles/constants.css'; import './styles/adaptivity.module.css'; import './styles/dynamicTokens.css'; import './styles/focusVisible.module.css'; +import './styles/animationFades.module.css'; +import './styles/transformOriginByPlacement.module.css'; export { AppRoot } from './components/AppRoot/AppRoot'; export type { AppRootProps, SafeAreaInsets } from './components/AppRoot/AppRoot'; @@ -328,6 +330,12 @@ export { LocaleProvider } from './components/LocaleProvider/LocaleProvider'; export type { LocaleProviderProps } from './components/LocaleProvider/LocaleProvider'; export { PlatformProvider } from './components/PlatformProvider/PlatformProvider'; export type { PlatformProviderProps } from './components/PlatformProvider/PlatformProvider'; +export { Popover } from './components/Popover/Popover'; +export type { + PopoverProps, + PopoverOnShownChange, + PopoverContentRenderProp, +} from './components/Popover/Popover'; /** * HOCs @@ -393,9 +401,6 @@ export type { TransitionContextProps } from './components/NavTransitionContext/N export { ChipsSelect as unstable_ChipsSelect } from './components/ChipsSelect/ChipsSelect'; export type { ChipsSelectProps as unstable_ChipsSelectProps } from './components/ChipsSelect/ChipsSelect'; -export { Popover as unstable_Popover } from './components/Popover/Popover'; -export type { PopoverProps as unstable_PopoverProps } from './components/Popover/Popover'; - export { TextTooltip as unstable_TextTooltip } from './components/TextTooltip/TextTooltip'; export type { TextTooltipProps as unstable_TextTooltipProps } from './components/TextTooltip/TextTooltip'; diff --git a/packages/vkui/src/lib/accessibility.test.ts b/packages/vkui/src/lib/accessibility.test.ts new file mode 100644 index 0000000000..f685ccdc06 --- /dev/null +++ b/packages/vkui/src/lib/accessibility.test.ts @@ -0,0 +1,18 @@ +import { injectAriaExpandedPropByRole } from './accessibility'; + +describe(injectAriaExpandedPropByRole, () => { + it.each(['menu', 'application', 'tab', 'menuitem', 'treeitem', 'gridcell'])( + 'should injects aria-expanded attribute to provided props when role="%s"', + (role) => { + expect(injectAriaExpandedPropByRole({}, true, role)).toEqual({ 'aria-expanded': true }); + expect(injectAriaExpandedPropByRole({}, false, role)).toEqual({ 'aria-expanded': false }); + }, + ); + + it('should not injects aria-expanded attribute to props', () => { + expect(injectAriaExpandedPropByRole({}, true)).toEqual({}); + expect(injectAriaExpandedPropByRole({}, false)).toEqual({}); + expect(injectAriaExpandedPropByRole({}, true, 'alert')).toEqual({}); + expect(injectAriaExpandedPropByRole({}, false, 'alert')).toEqual({}); + }); +}); diff --git a/packages/vkui/src/lib/accessibility.ts b/packages/vkui/src/lib/accessibility.ts index 91d1c54b56..a3282fc713 100644 --- a/packages/vkui/src/lib/accessibility.ts +++ b/packages/vkui/src/lib/accessibility.ts @@ -120,7 +120,7 @@ export function shouldTriggerClickOnEnterOrSpace( el.isContentEditable !== true && tagName !== 'INPUT' && tagName !== 'TEXTAREA' && - (role === 'button' || role === 'link'); + (role === 'button' || role === 'link' || role === 'menuitem'); const isNativeAnchorEl = tagName === 'A' && el.hasAttribute('href'); const keyPressed = pressedKey(e); @@ -133,3 +133,25 @@ export function shouldTriggerClickOnEnterOrSpace( (keyPressed === Keys.ENTER && !isNativeAnchorEl)) ); } + +/** + * @see https://doka.guide/a11y/aria-expanded/ + */ +export const injectAriaExpandedPropByRole = ( + props: React.ComponentProps, + state: boolean, + role?: React.AriaRole, +) => { + switch (role) { + case 'menu': + case 'application': + case 'tab': + case 'menuitem': + case 'treeitem': + case 'gridcell': + props['aria-expanded'] = state; + return props; + default: + return props; + } +}; diff --git a/packages/vkui/src/lib/cssAnimation/fades.ts b/packages/vkui/src/lib/cssAnimation/fades.ts new file mode 100644 index 0000000000..1ef9aa75e0 --- /dev/null +++ b/packages/vkui/src/lib/cssAnimation/fades.ts @@ -0,0 +1,6 @@ +import styles from '../../styles/animationFades.module.css'; // eslint-disable-line import/order + +export const animationFadeClassNames = { + in: styles['-anim-fade-in'], + out: styles['-anim-fade-out'], +} as const; diff --git a/packages/vkui/src/lib/cssAnimation/index.ts b/packages/vkui/src/lib/cssAnimation/index.ts new file mode 100644 index 0000000000..7278a62d8d --- /dev/null +++ b/packages/vkui/src/lib/cssAnimation/index.ts @@ -0,0 +1,2 @@ +export { animationFadeClassNames } from './fades'; +export { transformOriginClassNames } from './transformOrigin'; diff --git a/packages/vkui/src/lib/cssAnimation/transformOrigin.ts b/packages/vkui/src/lib/cssAnimation/transformOrigin.ts new file mode 100644 index 0000000000..dc59968051 --- /dev/null +++ b/packages/vkui/src/lib/cssAnimation/transformOrigin.ts @@ -0,0 +1,17 @@ +import type { Placement } from '../floating'; +import styles from '../../styles/transformOriginByPlacement.module.css'; // eslint-disable-line import/order + +export const transformOriginClassNames: Record = { + 'top': styles['-anim-transform-origin-top'], + 'top-start': styles['-anim-transform-origin-top-start'], + 'top-end': styles['-anim-transform-origin-top-end'], + 'right': styles['-anim-transform-origin-right'], + 'right-start': styles['-anim-transform-origin-right-start'], + 'right-end': styles['-anim-transform-origin-right-end'], + 'bottom': styles['-anim-transform-origin-bottom'], + 'bottom-start': styles['-anim-transform-origin-bottom-start'], + 'bottom-end': styles['-anim-transform-origin-bottom-end'], + 'left': styles['-anim-transform-origin-left'], + 'left-start': styles['-anim-transform-origin-left-start'], + 'left-end': styles['-anim-transform-origin-left-end'], +}; diff --git a/packages/vkui/src/styles/animationFades.module.css b/packages/vkui/src/styles/animationFades.module.css new file mode 100644 index 0000000000..d0db74c267 --- /dev/null +++ b/packages/vkui/src/styles/animationFades.module.css @@ -0,0 +1,36 @@ +/** + * [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) { + .-anim-fade-in { + animation: anim-fade-in 0.1s ease-in forwards; + } + + .-anim-fade-out { + animation: anim-fade-out 0.1s ease-out forwards; + } +} + +@keyframes anim-fade-in { + from { + opacity: 0; + transform: scale(0.8); + } + + to { + opacity: 1; + transform: scale(1); + } +} +@keyframes anim-fade-out { + from { + opacity: 1; + } + + to { + opacity: 0; + } +} diff --git a/packages/vkui/src/styles/transformOriginByPlacement.module.css b/packages/vkui/src/styles/transformOriginByPlacement.module.css new file mode 100644 index 0000000000..4038e69acf --- /dev/null +++ b/packages/vkui/src/styles/transformOriginByPlacement.module.css @@ -0,0 +1,47 @@ +.-anim-transform-origin-top { + transform-origin: bottom center; +} + +.-anim-transform-origin-top-start { + transform-origin: bottom left; +} + +.-anim-transform-origin-top-end { + transform-origin: bottom right; +} + +.-anim-transform-origin-right { + transform-origin: left center; +} + +.-anim-transform-origin-right-start { + transform-origin: left top; +} + +.-anim-transform-origin-right-end { + transform-origin: left bottom; +} + +.-anim-transform-origin-bottom { + transform-origin: top center; +} + +.-anim-transform-origin-bottom-start { + transform-origin: top left; +} + +.-anim-transform-origin-bottom-end { + transform-origin: top right; +} + +.-anim-transform-origin-left { + transform-origin: right center; +} + +.-anim-transform-origin-left-start { + transform-origin: right top; +} + +.-anim-transform-origin-left-end { + transform-origin: right bottom; +}