Skip to content

Commit

Permalink
feat(Popper): add prop onReferenceHiddenChanged (#8126)
Browse files Browse the repository at this point in the history
* feat(Popper): add prop getFloatingElementHiddenStyles

* fix: fix after merge conficts

* feat(Popper, Popover, Tooltip): add prop `onReferenceHiddenChanged` and remove prop `getFloatingElementHiddenStyles`

* fix: return old style calculation

* fix(Popover, Popper, Tooltip): rename `onReferenceHiddenChanged` to `onReferenceHiddenChange`. Add prop JSDoc

* fix: fix jsdoc

Co-authored-by: Inomdzhon Mirdzhamolov <[email protected]>

---------

Co-authored-by: Inomdzhon Mirdzhamolov <[email protected]>
  • Loading branch information
EldarMuhamethanov and inomdzhon authored Feb 4, 2025
1 parent 0e4317a commit e07de46
Show file tree
Hide file tree
Showing 14 changed files with 133 additions and 30 deletions.
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
10 changes: 5 additions & 5 deletions packages/vkui/src/components/Slider/SliderThumb/SliderThumb.tsx
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
6 changes: 6 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,10 @@ export interface FloatingComponentProps
* чтобы всплывающий элемент вместился в эту область видимости.
*/
onPlacementChange?: OnPlacementChange;
/**
* Событие скрытия / раскрытия компонента при использовании свойства `hideWhenReferenceHidden`.
*
* > Стоит иметь ввиду, что событие также будет вызвано и при новом рендере компонента
*/
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);
});
});
26 changes: 26 additions & 0 deletions packages/vkui/src/lib/floating/useReferenceHiddenChangeCallback.ts
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],
);
}

0 comments on commit e07de46

Please sign in to comment.