Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Popper): add prop onReferenceHiddenChanged #8126

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,11 @@ export const OnboardingTooltip = ({

let tooltip: React.ReactPortal | null = null;
if (shown) {
const floatingStyle = convertFloatingDataToReactCSSProperties(
positionStrategy,
floatingDataX,
floatingDataY,
);
const floatingStyle = convertFloatingDataToReactCSSProperties({
strategy: positionStrategy,
x: floatingDataX,
y: floatingDataY,
});

tooltip = createPortal(
<>
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 @@ -53,6 +53,7 @@ type AllowedFloatingComponentProps = Pick<
| 'usePortal'
| 'sameWidth'
| 'hideWhenReferenceHidden'
| 'onReferenceHiddenChange'
| 'disabled'
| 'disableInteractive'
| 'disableCloseOnClickOutside'
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 @@ -43,6 +43,7 @@ export const usePopover = <ElementType extends HTMLElement = HTMLElement>({
offsetByCrossAxis = 0,
sameWidth,
hideWhenReferenceHidden,
onReferenceHiddenChange,
disabled,
disableInteractive,
disableCloseOnClickOutside,
Expand Down Expand Up @@ -177,6 +178,7 @@ export const usePopover = <ElementType extends HTMLElement = HTMLElement>({
trigger,
strategy,
hoverDelay,
onReferenceHiddenChange,
closeAfterClick,
disabled,
disableInteractive,
Expand Down
17 changes: 11 additions & 6 deletions packages/vkui/src/components/Popper/Popper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
usePlacementChangeCallback,
type VirtualElement,
} from '../../lib/floating';
import { useReferenceHiddenChangeCallback } from '../../lib/floating/useReferenceHiddenChangeCallback';
import { useIsomorphicLayoutEffect } from '../../lib/useIsomorphicLayoutEffect';
import type { HTMLAttributesWithRootRef } from '../../types';
import { AppRootPortal } from '../AppRoot/AppRootPortal';
Expand Down Expand Up @@ -46,6 +47,7 @@ type AllowedFloatingComponentProps = Pick<
| 'onShownChange'
| 'defaultShown'
| 'hideWhenReferenceHidden'
| 'onReferenceHiddenChange'
| 'sameWidth'
| 'zIndex'
| 'strategy'
Expand Down Expand Up @@ -116,6 +118,7 @@ export const Popper = ({
children,
usePortal = true,
onPlacementChange,
onReferenceHiddenChange,
zIndex,
style,
...restProps
Expand Down Expand Up @@ -157,6 +160,8 @@ export const Popper = ({

usePlacementChangeCallback(placementProp, resolvedPlacement, onPlacementChange);

useReferenceHiddenChangeCallback(middlewareData.hide, onReferenceHiddenChange);

const { arrow: arrowCoords } = middlewareData;

const handleRootRef = useExternRef<HTMLDivElement>(refs.setFloating, getRootRef);
Expand All @@ -178,13 +183,13 @@ export const Popper = ({
style={mergeStyle(dropdownStyle, style)}
baseClassName={styles.host}
getRootRef={handleRootRef}
baseStyle={convertFloatingDataToReactCSSProperties(
floatingPositionStrategy,
floatingDataX,
floatingDataY,
sameWidth ? null : undefined,
baseStyle={convertFloatingDataToReactCSSProperties({
strategy: floatingPositionStrategy,
x: floatingDataX,
y: floatingDataY,
initialWidth: sameWidth ? null : undefined,
middlewareData,
)}
})}
>
{arrow && (
<FloatingArrow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,11 @@ export const SliderThumb = ({
<TooltipBase
appearance="neutral"
getRootRef={refs.setFloating}
style={convertFloatingDataToReactCSSProperties(
floatingPositionStrategy,
floatingDataX,
floatingDataY,
)}
style={convertFloatingDataToReactCSSProperties({
strategy: floatingPositionStrategy,
x: floatingDataX,
y: floatingDataY,
})}
arrowProps={{
coords: arrowCoords,
placement: resolvedPlacement,
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 @@ -18,6 +18,7 @@ type AllowedFloatingComponentProps = Pick<
| 'defaultShown'
| 'onShownChange'
| 'hideWhenReferenceHidden'
| 'onReferenceHiddenChange'
| 'children'
| 'zIndex'
| 'usePortal'
Expand Down
2 changes: 2 additions & 0 deletions packages/vkui/src/components/Tooltip/useTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const useTooltip = ({
hideWhenReferenceHidden,
disableFlipMiddleware = false,
disableTriggerOnFocus = false,
onReferenceHiddenChange,

// useFloatingWithInteractions
defaultShown,
Expand Down Expand Up @@ -146,6 +147,7 @@ export const useTooltip = ({
shown: shownProp,
onShownChange,
trigger: disableTriggerOnFocus ? 'hover' : ['hover', 'focus'],
onReferenceHiddenChange,
hoverDelay,
closeAfterClick: !disableCloseAfterClick,
disableInteractive: !enableInteractive,
Expand Down
5 changes: 5 additions & 0 deletions packages/vkui/src/hooks/useFloatingElement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
usePlacementChangeCallback,
} from '../lib/floating';
import { type ReferenceProps } from '../lib/floating/useFloatingWithInteractions/types';
import { useReferenceHiddenChangeCallback } from '../lib/floating/useReferenceHiddenChangeCallback';
import { useExternRef } from './useExternRef';
import { useGlobalEscKeyDown } from './useGlobalEscKeyDown';

Expand Down Expand Up @@ -41,6 +42,7 @@ export type UseFloatingElementProps<
> = Omit<UseFloatingMiddlewaresBootstrapOptions, 'arrowRef'> &
Omit<UseFloatingWithInteractionsProps, 'placement'> & {
onPlacementChange?: OnPlacementChange;
onReferenceHiddenChange?: (hidden: boolean) => void;
renderFloatingComponent: RenderFloatingComponentFn<FloatingElement>;
remapReferenceProps?: RemapReferencePropsFn<ReferenceElement>;
externalFloatingElementRef?: React.Ref<FloatingElement>;
Expand Down Expand Up @@ -81,6 +83,7 @@ export const useFloatingElement = <
onShownChange,
onShownChanged,
strategy,
onReferenceHiddenChange,

onPlacementChange,

Expand Down Expand Up @@ -139,6 +142,8 @@ export const useFloatingElement = <

usePlacementChangeCallback(placement, resolvedPlacement, onPlacementChange);

useReferenceHiddenChangeCallback(middlewareData.hide, onReferenceHiddenChange);

const component = renderFloatingComponent({
shown,
willBeHide,
Expand Down
17 changes: 13 additions & 4 deletions packages/vkui/src/lib/floating/functions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,13 @@ describe('floating/functions', () => {
left: 10,
width: 'max-content',
};
expect(convertFloatingDataToReactCSSProperties('absolute', 10, 10)).toEqual({
expect(
convertFloatingDataToReactCSSProperties({ strategy: 'absolute', x: 10, y: 10 }),
).toEqual({
position: 'absolute',
...expectedCSSProperties,
});
expect(convertFloatingDataToReactCSSProperties('fixed', 10, 10)).toEqual({
expect(convertFloatingDataToReactCSSProperties({ strategy: 'fixed', x: 10, y: 10 })).toEqual({
position: 'fixed',
...expectedCSSProperties,
});
Expand All @@ -61,13 +63,20 @@ describe('floating/functions', () => {
left: 0,
width: 'max-content',
};
expect(convertFloatingDataToReactCSSProperties('absolute', 0, 0)).toEqual(
expect(convertFloatingDataToReactCSSProperties({ strategy: 'absolute', x: 0, y: 0 })).toEqual(
expectedCSSProperties,
);
});

it('should ignore `width` property if `initialWidth` prop is null', () => {
expect(convertFloatingDataToReactCSSProperties('absolute', 0, 0, null)).toEqual({
expect(
convertFloatingDataToReactCSSProperties({
strategy: 'absolute',
x: 0,
y: 0,
initialWidth: null,
}),
).toEqual({
position: 'absolute',
top: 0,
right: 'auto',
Expand Down
22 changes: 15 additions & 7 deletions packages/vkui/src/lib/floating/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,26 @@ export function getAutoPlacementAlign(placement: AutoPlacementType): 'start' | '
return align === 'start' || align === 'end' ? align : null;
}

export type ConvertFloatingDataArgs = {
strategy: FloatingPositionStrategy;
x: UseFloatingData['x'];
y: UseFloatingData['y'];
initialWidth?: React.CSSProperties['width'] | null;
middlewareData?: UseFloatingData['middlewareData'];
};

/**
* Note: не используем `translate3d`, чтобы в лишний раз не выносить в отдельный слой и не занимать память в GPU.
*
* см. https://floating-ui.com/docs/react#positioning
*/
export function convertFloatingDataToReactCSSProperties(
strategy: FloatingPositionStrategy,
x: UseFloatingData['x'],
y: UseFloatingData['y'],
initialWidth: React.CSSProperties['width'] | null = 'max-content',
middlewareData?: UseFloatingData['middlewareData'],
): React.CSSProperties {
export function convertFloatingDataToReactCSSProperties({
strategy,
x,
y,
initialWidth = 'max-content',
middlewareData,
}: ConvertFloatingDataArgs): React.CSSProperties {
const styles: React.CSSProperties = {
position: strategy,
top: y,
Expand Down
7 changes: 7 additions & 0 deletions packages/vkui/src/lib/floating/types/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,11 @@ export interface FloatingComponentProps
* чтобы всплывающий элемент вместился в эту область видимости.
*/
onPlacementChange?: OnPlacementChange;
/**
* При использовании свойства `hideWhenReferenceHidden` колбэк срабатывает, когда
* целевой элемент вышел за область видимости(hidden=true) и вернулся обратно(hidden=false).
*
* > При первом появлении всплывающего элемента колбэк будет вызван со значением `false`
*/
onReferenceHiddenChange?: (hidden: boolean) => void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -329,13 +329,12 @@ export const useFloatingWithInteractions = <T extends HTMLElement = HTMLElement>
}, [triggerOnHover, triggerOnFocus, triggerOnClick]);

if (shownFinalState) {
floatingPropsRef.current.style = convertFloatingDataToReactCSSProperties(
floatingPropsRef.current.style = convertFloatingDataToReactCSSProperties({
strategy,
x,
y,
undefined,
middlewareData,
);
});

if (disableInteractive) {
floatingPropsRef.current.style.pointerEvents = 'none';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { renderHook } from '@testing-library/react';
import { useReferenceHiddenChangeCallback } from './useReferenceHiddenChangeCallback';

type HookArguments = Parameters<typeof useReferenceHiddenChangeCallback>;
type RenderHookProps = {
hideMiddleware: HookArguments[0];
onReferenceHiddenChange: HookArguments[1];
};

describe('usePlacementChangeCallback', () => {
it('calls callback on initial render when initial placement differ from resolvedPlacement', () => {
const onReferenceHiddenChangeStub = jest.fn();

const defaultProps: RenderHookProps = {
hideMiddleware: {
referenceHidden: false,
},
onReferenceHiddenChange: onReferenceHiddenChangeStub,
};

const { rerender } = renderHook<void, RenderHookProps>(
({ hideMiddleware, onReferenceHiddenChange }) =>
useReferenceHiddenChangeCallback(hideMiddleware, onReferenceHiddenChange),
{
initialProps: defaultProps,
},
);

expect(onReferenceHiddenChangeStub).not.toHaveBeenCalled();

rerender({ ...defaultProps, hideMiddleware: { referenceHidden: true } });

expect(onReferenceHiddenChangeStub).toHaveBeenNthCalledWith(1, true);

rerender({ ...defaultProps, hideMiddleware: { referenceHidden: false } });

expect(onReferenceHiddenChangeStub).toHaveBeenNthCalledWith(2, false);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as React from 'react';
import { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect';
import { type UseFloatingData } from './types/common';
import { type FloatingComponentProps } from './types/component';

export function useReferenceHiddenChangeCallback(
hideMiddleware: UseFloatingData['middlewareData']['hide'],
onReferenceHiddenChange: FloatingComponentProps['onReferenceHiddenChange'],
) {
const prevHiddenRef = React.useRef<boolean | undefined>(hideMiddleware?.referenceHidden);
React.useEffect(() => {
prevHiddenRef.current = hideMiddleware?.referenceHidden;
});

useIsomorphicLayoutEffect(
function checkHiddenChanged() {
if (!onReferenceHiddenChange) {
return;
}
if (hideMiddleware?.referenceHidden !== prevHiddenRef.current) {
onReferenceHiddenChange(hideMiddleware?.referenceHidden || false);
}
},
[hideMiddleware?.referenceHidden, onReferenceHiddenChange],
);
}