Skip to content
Open
32 changes: 32 additions & 0 deletions packages/vkui/src/components/Popover/Popover.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,37 @@ export const Example: Story = {
);
};

const PopoverWithTriggerLongPress = () => {
return (
<Popover
noStyling
trigger="long-press"
id="menupopup-longpress"
role="dialog"
aria-labelledby="menubutton"
content={({ onClose }) => (
<Group>
<CellButton role="menuitem" before={<Icon28AddOutline />} onClick={onClose}>
Добавить
</CellButton>
<CellButton
role="menuitem"
before={<Icon28DeleteOutline />}
appearance="negative"
onClick={onClose}
>
Удалить
</CellButton>
</Group>
)}
>
<Button id="menubutton" aria-controls="menupopup" mode="outline">
Долго удерживай нажатие на мне
</Button>
</Popover>
);
};

const PopoverWithTriggerFocus = () => {
return (
<Popover
Expand Down Expand Up @@ -204,6 +235,7 @@ export const Example: Story = {
<Flex margin="auto" direction="column" align="start" gap="2xl">
<PopoverWithTriggerHover />
<PopoverWithTriggerClick />
<PopoverWithTriggerLongPress />
<PopoverWithTriggerFocus />
<PopoverWithAllTriggers />
<PopoverWithTriggerManual />
Expand Down
1 change: 1 addition & 0 deletions packages/vkui/src/components/Popover/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ type AllowedFloatingComponentProps = Pick<
| 'customMiddlewares'
| 'strategy'
| 'disableFocusTrap'
| 'longPressDelay'
>;

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/vkui/src/components/Popover/usePopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const usePopover = <ElementType extends HTMLElement = HTMLElement>({
disableFlipMiddleware = false,
disableShiftMiddleware = false,
trigger = 'click',
longPressDelay,
strategy,
content,
hoverDelay = 150,
Expand Down Expand Up @@ -199,6 +200,7 @@ export const usePopover = <ElementType extends HTMLElement = HTMLElement>({
customMiddlewares,

trigger,
longPressDelay,
strategy,
hoverDelay,
onReferenceHiddenChange,
Expand Down
4 changes: 4 additions & 0 deletions packages/vkui/src/components/Tooltip/Tooltip.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { Button } from '../Button/Button';
import { Tooltip, type TooltipProps } from './Tooltip';
import { useTooltip } from './useTooltip';

vi.mock('../../hooks/useHoverSupported', () => ({
useHoverSupported: vi.fn(() => true),
}));

describe(Tooltip, () => {
baselineComponent((props) => (
<Tooltip shown description="test" {...props}>
Expand Down
1 change: 1 addition & 0 deletions packages/vkui/src/components/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type AllowedFloatingComponentProps = Pick<
| 'disableFlipMiddleware'
| 'disableShiftMiddleware'
| 'strategy'
| 'longPressDelay'
| 'overflowPadding'
>;

Expand Down
20 changes: 18 additions & 2 deletions packages/vkui/src/components/Tooltip/useTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ import {
useFloatingElement,
type UseFloatingElementProps,
} from '../../hooks/useFloatingElement';
import { useHoverSupported } from '../../hooks/useHoverSupported';
import { animationFadeClassNames } from '../../lib/animation';
import { getArrowCoordsByMiddlewareData, sizeMiddleware } from '../../lib/floating';
import { type ReferenceProps } from '../../lib/floating/useFloatingWithInteractions/types';
import {
type ReferenceProps,
type TriggerType,
} from '../../lib/floating/useFloatingWithInteractions/types';
import { AppRootPortal } from '../AppRoot/AppRootPortal';
import { TooltipBase } from '../TooltipBase/TooltipBase';
import { type TooltipProps } from './Tooltip';
Expand Down Expand Up @@ -54,6 +58,7 @@ export const useTooltip = ({
onShownChange,
hoverDelay = 150,
strategy,
longPressDelay,

// инверсированные св-ва для useFloatingWithInteractions
enableInteractive = false,
Expand All @@ -77,6 +82,16 @@ export const useTooltip = ({
const generatedId = React.useId();
const tooltipId = idProp || generatedId;

const hoverSupported = useHoverSupported();

const trigger: TriggerType = (() => {
if (hoverSupported) {
return disableTriggerOnFocus ? 'hover' : ['hover', 'focus'];
} else {
return 'long-press';
}
})();

const renderFloatingComponent = useCallback(
({
shown,
Expand Down Expand Up @@ -159,12 +174,13 @@ export const useTooltip = ({
defaultShown,
shown: shownProp,
onShownChange,
trigger: disableTriggerOnFocus ? 'hover' : ['hover', 'focus'],
trigger,
onReferenceHiddenChange,
hoverDelay,
closeAfterClick: !disableCloseAfterClick,
disableInteractive: !enableInteractive,
strategy,
longPressDelay,

onPlacementChange,

Expand Down
102 changes: 102 additions & 0 deletions packages/vkui/src/hooks/useBooleanRef.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import * as React from 'react';
import { act, render } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { useBooleanRef } from './useBooleanRef';

const HookTester = React.forwardRef<
{
value: boolean;
setValue: (v: boolean) => void;
setTrue: () => void;
setFalse: () => void;
toggle: () => void;
} & Record<string, any>,
{ defaultValue?: boolean }
>((props, ref) => {
const api = useBooleanRef(props.defaultValue);
React.useImperativeHandle(ref, () => api, [api]);
return null;
});
HookTester.displayName = 'HookTester';

describe('useBooleanRef', () => {
it('defaults to false when no argument provided', () => {
const ref = React.createRef<any>();
render(<HookTester ref={ref} />);
expect(ref.current).toBeTruthy();
expect(ref.current.value).toBe(false);
});

it('respects the default value (true)', () => {
const ref = React.createRef<any>();
render(<HookTester ref={ref} defaultValue={true} />);
expect(ref.current.value).toBe(true);
});

it('setValue sets the value to true and false', () => {
const ref = React.createRef<any>();
render(<HookTester ref={ref} />);
act(() => {
ref.current.setValue(true);
});
expect(ref.current.value).toBe(true);

act(() => {
ref.current.setValue(false);
});
expect(ref.current.value).toBe(false);
});

it('setTrue sets value to true and setFalse sets value to false', () => {
const ref = React.createRef<any>();
render(<HookTester ref={ref} defaultValue={false} />);

act(() => {
ref.current.setTrue();
});
expect(ref.current.value).toBe(true);

act(() => {
ref.current.setFalse();
});
expect(ref.current.value).toBe(false);
});

it('toggle flips the value', () => {
const ref = React.createRef<any>();
render(<HookTester ref={ref} defaultValue={false} />);

act(() => {
ref.current.toggle();
});
expect(ref.current.value).toBe(true);

act(() => {
ref.current.toggle();
});
expect(ref.current.value).toBe(false);
});

it('combination of operations works correctly', () => {
const ref = React.createRef<any>();
render(<HookTester ref={ref} defaultValue={true} />);

// start true
expect(ref.current.value).toBe(true);

act(() => {
ref.current.setFalse();
});
expect(ref.current.value).toBe(false);

act(() => {
ref.current.toggle();
});
expect(ref.current.value).toBe(true);

act(() => {
ref.current.setValue(false);
});
expect(ref.current.value).toBe(false);
});
});
34 changes: 34 additions & 0 deletions packages/vkui/src/hooks/useBooleanRef.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as React from 'react';

export const useBooleanRef = (defaultValue = false) => {
const valueRef = React.useRef(defaultValue);

const setValue = React.useCallback((value: boolean) => {
valueRef.current = value;
}, []);

const setTrue = React.useCallback(() => {
valueRef.current = true;
}, []);

const setFalse = React.useCallback(() => {
valueRef.current = false;
}, []);

const toggle = React.useCallback(() => {
valueRef.current = !valueRef.current;
}, []);

return React.useMemo(
() => ({
setValue,
setTrue,
setFalse,
toggle,
get value() {
return valueRef.current;
},
}),
[setFalse, setTrue, setValue, toggle],
);
};
2 changes: 2 additions & 0 deletions packages/vkui/src/hooks/useFloatingElement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export const useFloatingElement = <
onShownChange,
onShownChanged,
strategy,
longPressDelay,
onReferenceHiddenChange,

onPlacementChange,
Expand Down Expand Up @@ -130,6 +131,7 @@ export const useFloatingElement = <
strategy,
placement: strictPlacement,
trigger,
longPressDelay,
hoverDelay,
closeAfterClick,
disabled,
Expand Down
10 changes: 10 additions & 0 deletions packages/vkui/src/hooks/useHoverSupported.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useMediaQuery } from './useMediaQuery';

// '(hover: hover)' — проверяет, может ли первичный ввод поддерживать hover (обычно мышь).
// '(any-hover: hover)' — проверяет, поддерживает ли любой ввод hover (полезно для гибридных устройств).
// '(pointer: fine)' — указывает, что есть «тонкий» точный указатель (мышь, трекпад).
const HOVER_SUPPORTED_QUERY = '(hover: hover), (any-hover: hover), (pointer: fine)';

export const useHoverSupported = () => {
return useMediaQuery(HOVER_SUPPORTED_QUERY);
};
Loading