@wordpress/components
Version:
UI components for WordPress.
246 lines (214 loc) • 6.21 kB
text/typescript
/**
* WordPress dependencies
*/
import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
import { useResizeObserver } from '@wordpress/compose';
const noop = () => {};
export type Axis = 'x' | 'y';
export const POSITIONS = {
bottom: 'bottom',
corner: 'corner',
} as const;
export type Position = ( typeof POSITIONS )[ keyof typeof POSITIONS ];
interface UseResizeLabelProps {
/** The label value. */
label?: string;
/** Element to be rendered for resize listening events. */
resizeListener: JSX.Element;
}
interface UseResizeLabelArgs {
axis?: Axis;
fadeTimeout: number;
onResize: ( data: { width: number | null; height: number | null } ) => void;
position: Position;
showPx: boolean;
}
/**
* Custom hook that manages resize listener events. It also provides a label
* based on current resize width x height values.
*
* @param props
* @param props.axis Only shows the label corresponding to the axis.
* @param props.fadeTimeout Duration (ms) before deactivating the resize label.
* @param props.onResize Callback when a resize occurs. Provides { width, height } callback.
* @param props.position Adjusts label value.
* @param props.showPx Whether to add `PX` to the label.
*
* @return Properties for hook.
*/
export function useResizeLabel( {
axis,
fadeTimeout = 180,
onResize = noop,
position = POSITIONS.bottom,
showPx = false,
}: UseResizeLabelArgs ): UseResizeLabelProps {
/*
* The width/height values derive from this special useResizeObserver hook.
* This custom hook uses the ResizeObserver API to listen for resize events.
*/
const [ resizeListener, sizes ] = useResizeObserver();
/*
* Indicates if the x/y axis is preferred.
* If set, we will avoid resetting the moveX and moveY values.
* This will allow for the preferred axis values to persist in the label.
*/
const isAxisControlled = !! axis;
/*
* The moveX and moveY values are used to track whether the label should
* display width, height, or width x height.
*/
const [ moveX, setMoveX ] = useState( false );
const [ moveY, setMoveY ] = useState( false );
/*
* Cached dimension values to check for width/height updates from the
* sizes property from useResizeAware()
*/
const { width, height } = sizes;
const heightRef = useRef( height );
const widthRef = useRef( width );
/*
* This timeout is used with setMoveX and setMoveY to determine of
* both width and height values have changed at (roughly) the same time.
*/
const moveTimeoutRef = useRef< number >();
const debounceUnsetMoveXY = useCallback( () => {
const unsetMoveXY = () => {
/*
* If axis is controlled, we will avoid resetting the moveX and moveY values.
* This will allow for the preferred axis values to persist in the label.
*/
if ( isAxisControlled ) {
return;
}
setMoveX( false );
setMoveY( false );
};
if ( moveTimeoutRef.current ) {
window.clearTimeout( moveTimeoutRef.current );
}
moveTimeoutRef.current = window.setTimeout( unsetMoveXY, fadeTimeout );
}, [ fadeTimeout, isAxisControlled ] );
useEffect( () => {
/*
* On the initial render of useResizeAware, the height and width values are
* null. They are calculated then set using via an internal useEffect hook.
*/
const isRendered = width !== null || height !== null;
if ( ! isRendered ) {
return;
}
const didWidthChange = width !== widthRef.current;
const didHeightChange = height !== heightRef.current;
if ( ! didWidthChange && ! didHeightChange ) {
return;
}
/*
* After the initial render, the useResizeAware will set the first
* width and height values. We'll sync those values with our
* width and height refs. However, we shouldn't render our Tooltip
* label on this first cycle.
*/
if ( width && ! widthRef.current && height && ! heightRef.current ) {
widthRef.current = width;
heightRef.current = height;
return;
}
/*
* After the first cycle, we can track width and height changes.
*/
if ( didWidthChange ) {
setMoveX( true );
widthRef.current = width;
}
if ( didHeightChange ) {
setMoveY( true );
heightRef.current = height;
}
onResize( { width, height } );
debounceUnsetMoveXY();
}, [ width, height, onResize, debounceUnsetMoveXY ] );
const label = getSizeLabel( {
axis,
height,
moveX,
moveY,
position,
showPx,
width,
} );
return {
label,
resizeListener,
};
}
interface GetSizeLabelArgs {
axis?: Axis;
height: number | null;
moveX: boolean;
moveY: boolean;
position: Position;
showPx: boolean;
width: number | null;
}
/**
* Gets the resize label based on width and height values (as well as recent changes).
*
* @param props
* @param props.axis Only shows the label corresponding to the axis.
* @param props.height Height value.
* @param props.moveX Recent width (x axis) changes.
* @param props.moveY Recent width (y axis) changes.
* @param props.position Adjusts label value.
* @param props.showPx Whether to add `PX` to the label.
* @param props.width Width value.
*
* @return The rendered label.
*/
function getSizeLabel( {
axis,
height,
moveX = false,
moveY = false,
position = POSITIONS.bottom,
showPx = false,
width,
}: GetSizeLabelArgs ): string | undefined {
if ( ! moveX && ! moveY ) {
return undefined;
}
/*
* Corner position...
* We want the label to appear like width x height.
*/
if ( position === POSITIONS.corner ) {
return `${ width } x ${ height }`;
}
/*
* Other POSITIONS...
* The label will combine both width x height values if both
* values have recently been changed.
*
* Otherwise, only width or height will be displayed.
* The `PX` unit will be added, if specified by the `showPx` prop.
*/
const labelUnit = showPx ? ' px' : '';
if ( axis ) {
if ( axis === 'x' && moveX ) {
return `${ width }${ labelUnit }`;
}
if ( axis === 'y' && moveY ) {
return `${ height }${ labelUnit }`;
}
}
if ( moveX && moveY ) {
return `${ width } x ${ height }`;
}
if ( moveX ) {
return `${ width }${ labelUnit }`;
}
if ( moveY ) {
return `${ height }${ labelUnit }`;
}
return undefined;
}