UNPKG

@wordpress/components

Version:
246 lines (214 loc) 6.21 kB
/** * 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; }