UNPKG

@gravityforms/components

Version:

UI components for use in Gravity Forms development. Both React and vanilla js flavors.

1,031 lines (953 loc) 35.3 kB
import { React, SimpleBar, classnames, PropTypes } from '@gravityforms/libraries'; import { useId, useStateWithDep, ConditionalWrapper, useFocusTrap, } from '@gravityforms/react-utils'; import { sprintf } from '@gravityforms/utils'; import Button from '../../elements/Button'; import Heading from '../../elements/Heading'; import Text from '../../elements/Text'; import { ESCAPE, ARROW_LEFT, ARROW_RIGHT } from '../../utils/keymap'; const NEEDS_I18N_LABEL = 'Needs i18n'; const { forwardRef, useState, useRef, useEffect, useCallback } = React; /** * @module Flyout * @description A flyout component in react. * * @since 1.1.18 * * @param {object} props Component props. * @param {JSX.Element} props.afterContent Any custom content to be placed after the body of the flyout. * @param {number} props.animationDelay Total runtime of close animation. Synchronize with css if modifying the built-in 250 ms delay. * @param {JSX.Element} props.beforeContent Any custom content to be placed before the header of the flyout. * @param {JSX.Element} props.children React element children in the flyout body. * @param {object} props.closeButtonCustomAttributes Custom attributes for the close button. * @param {boolean} props.closeOnMaskClick Whether to close if the background mask is clicked. * @param {string} props.componentsPrefix Component system prefix, e.g. `gform-admin` or `gravitycrm-admin`. * @param {object} props.customAttributes Custom attributes for the component. * @param {string|Array|object} props.customBodyClasses Custom classes for the flyout body as array. * @param {string|Array|object} props.customClasses Custom classes for the component. * @param {string|Array|object} props.customInnerBodyClasses Custom classes for the flyout inner_body as array. * @param {string|Array|object} props.customMaskClasses Custom classes for the mask as array. * @param {string} props.description Subheading description for the flyout. * @param {string} props.desktopWidth Width for the flyout on desktop. * @param {string} props.direction Flyout direction, left or right. * @param {boolean} props.expandable Whether to enable the ability to allow expanding to a larger width. * @param {object} props.expandableButtonCustomAttributes Custom attributes for the expandable button. * @param {string} props.expandableWidthDesktop Width to expand to if expanded on desktop. * @param {JSX.Element} props.footerChildrenLeft React element children in the flyout footer on the left. * @param {JSX.Element} props.footerChildrenRight React element children in the flyout footer on the right. * @param {boolean} props.footerIsFixed Whether or not the footer is fixed. * @param {JSX.Element} props.headerChildrenLeft React element children in the flyout header on the left. * @param {JSX.Element} props.headerChildrenRight React element children in the flyout header on the right. * @param {object} props.headerDescriptionCustomAttributes Custom attributes for the header description. * @param {object} props.headerHeadingCustomAttributes Custom attributes for the header heading. * @param {boolean} props.headerIsFixed Whether or not the header is fixed. * @param {object} props.i18n Translated strings for the UI. * @param {string} props.iconPrefix The prefix for the icon library to be used. * @param {object} props.icons Icons for the UI. * @param {string} props.id Flyout id. * @param {boolean} props.isExpanded Prop to control whether the flyout is expanded. * @param {boolean} props.isOpen Prop to control whether the dialog is open. * @param {boolean} props.isPinned Prop to control whether the flyout is pinned. * @param {boolean} props.maskBlur Whether to blur behind the mask for the flyout. * @param {Array} props.maskClickExcludeButtons Array of button codes that will not close the flyout on mask click, 0 = left click, 1 = middle click, 2 = right click, 3 = back button, 4 = forward button, 5 = X1, 6 = X2. * @param {string} props.maskTheme Mask background color scheme: `none`, `light` or `dark` * @param {number} props.maxWidth Max width in pixels for the flyout. * @param {number} props.mobileBreakpoint Mobile breakpoint in pixels for the flyout. * @param {string} props.mobileWidth Width for the flyout on mobile. * @param {string} props.offset Top offset for the flyout. * @param {boolean} props.offsetWPAdminBar Whether to offset the flyout from the WordPress admin bar. * @param {boolean} props.offsetWPAdminMenu Whether to offset the flyout from the WordPress admin menu. * @param {Function} props.onClose Function to fire on flyout close. * @param {Function} props.onExpandedChange Callback fired when expanded state changes. * @param {Function} props.onOpen Function to fire on flyout open. * @param {Function} props.onPinnedChange Callback fired when pinned state changes. * @param {Function} props.onPinnedWidthChange Callback fired when pinned width changes via drag. * @param {boolean} props.pinnable Whether to enable the ability to allow pinning of the flyout. * @param {string} props.pinnedActiveContentSelector Selector for the pinned, active content, to scroll into view. * @param {object} props.pinnedButtonCustomAttributes Custom attributes for the pinning button. * @param {string} props.pinnedContentSelector Selector for the content that needs the pinned flyout width offset. * @param {number} props.pinnedContentMinWidth Min width in pixels for the pinned content. * @param {string} props.pinnedDragHandleAriaValueText Template for the aria-valuetext attribute. Use %d for width value, e.g. "Flyout width %d pixels". * @param {string} props.pinnedDragHandleLabel Label for the pinned drag handle. * @param {number} props.pinnedDragHandleKeyboardStep Step size for the pinned drag handle when using keyboard. * @param {number} props.pinnedDragHandleKeyboardStepLarge Step size for the pinned drag handle when using keyboard and shift key is pressed. * @param {number} props.pinnedDefaultWidth Default width in pixels when pinned. * @param {number} props.pinnedMaxWidth Max width in pixels when pinned. * @param {number} props.pinnedMinWidth Min width in pixels when pinned. * @param {number} props.pinnedWidth Current pinned width in pixels. Used for persistent state. * @param {string} props.position The position of the flyout, `absolute` or `fixed`. * @param {boolean} props.resetScrollOnOpen Whether to reset scroll on open. * @param {boolean} props.showDivider Whether or not to show the divider border below title. * @param {boolean} props.simplebar Whether or not to use SimpleBar on the content. * @param {string} props.title The title of the flyout. * @param {number} props.zIndex z-index of the flyout. * @param {object|null} ref Ref to the component. * * @return {JSX.Element} The Flyout component. * * @example * import Flyout from '@gravityforms/components/react/admin/modules/Flyout'; * * // Basic usage * return ( * <Flyout direction="right" title="Flyout title"> * { children } * </Flyout> * ); * * @example * // With external state management * return ( * <Flyout * direction="right" * isExpanded={ expandedState } * isOpen={ openState } * isPinned={ pinnedState } * onClose={ () => setOpenState( false ) } * onExpandedChange={ ( val ) => setExpandedState( val ) } * onPinnedChange={ ( val ) => setPinnedState( val ) } * title="Flyout title" * > * { children } * </Flyout> * ); * */ const Flyout = forwardRef( ( { afterContent = null, animationDelay = 250, beforeContent = null, children = null, closeButtonCustomAttributes = { customClasses: [], icon: 'delete', iconPrefix: 'gravity-component-icon', label: '', size: 'size-xs', title: '', type: 'round', }, closeOnMaskClick = true, componentsPrefix = 'gform-admin', customAttributes = {}, customBodyClasses = [], customClasses = [], customInnerBodyClasses = [], customMaskClasses = [], description = '', desktopWidth = '0', direction = '', expandable = false, expandableButtonCustomAttributes = { customClasses: [], size: 'size-xs', type: 'round', }, expandableWidthDesktop = 'min(90%, 1085px)', footerChildrenLeft = null, footerChildrenRight = null, footerIsFixed = false, headerChildrenLeft = null, headerChildrenRight = null, headerDescriptionCustomAttributes = {}, headerHeadingCustomAttributes = {}, headerIsFixed = false, i18n = {}, iconPrefix = 'gravity-component-icon', icons = { iconClose: 'delete', iconCollapse: 'close-expand', iconExpand: 'expand', iconPin: 'pin', iconUnpin: 'unpin', }, id: defaultId = '', isExpanded = false, isOpen = false, isPinned = false, maskBlur = false, maskClickExcludeButtons = [], maskTheme = 'none', maxWidth = 0, mobileBreakpoint = 0, mobileWidth = '100%', offset = '0px', offsetWPAdminBar = false, offsetWPAdminMenu = false, onClose = () => {}, onExpandedChange = () => {}, onOpen = () => {}, onPinnedChange = () => {}, onPinnedWidthChange = () => {}, pinnable = false, pinnedActiveContentSelector = '', pinnedButtonCustomAttributes = { customClasses: [], size: 'size-xs', type: 'round', }, pinnedContentMinWidth = 350, pinnedContentSelector = '', pinnedDragHandleAriaValueText = '', pinnedDragHandleLabel = '', pinnedDragHandleKeyboardStep = 10, pinnedDragHandleKeyboardStepLarge = 50, pinnedDefaultWidth = 500, pinnedMaxWidth = 1000, pinnedMinWidth = 350, pinnedWidth = null, position = 'fixed', resetScrollOnOpen = false, showDivider = true, simplebar = false, title = '', zIndex = 10, }, ref ) => { const [ animationReady, setAnimationReady ] = useState( false ); const [ animationActive, setAnimationActive ] = useState( false ); const [ flyoutActive, setFlyoutActive ] = useStateWithDep( isOpen ); const [ flyoutExpanded, setFlyoutExpanded ] = useStateWithDep( isExpanded ); const [ flyoutPinned, setFlyoutPinned ] = useStateWithDep( isPinned ); const [ isDragging, setIsDragging ] = useState( false ); const [ isKeyboardResizing, setIsKeyboardResizing ] = useState( false ); const initialPinnedWidth = pinnedWidth ?? pinnedDefaultWidth; const effectivePinnedMin = Math.min( pinnedMinWidth, pinnedMaxWidth ); const clampedInitialPinnedWidth = Math.min( Math.max( initialPinnedWidth, effectivePinnedMin ), pinnedMaxWidth ); const [ currentPinnedWidth, setCurrentPinnedWidth ] = useStateWithDep( clampedInitialPinnedWidth ); const trapRef = useFocusTrap( flyoutActive ); const simplebarRef = useRef( null ); const bodyRef = useRef( null ); const dragHandleRef = useRef( null ); const dragStartX = useRef( 0 ); const dragStartWidth = useRef( 0 ); const keyboardResizeTimeout = useRef( null ); const flyoutRef = useRef( null ); const id = useId( defaultId ); const isInitialFlyoutActiveRef = useRef( true ); const isRtl = document.documentElement.dir === 'rtl' || getComputedStyle( document.documentElement ).direction === 'rtl'; const shouldInvertDragDirection = ( direction === 'left' ) !== isRtl; /** * @description Callback ref to merge internal flyout ref with forwarded ref. * * @since 6.0.10 * * @param {HTMLElement|null} node The DOM node. * * @return {void} */ const setFlyoutRef = useCallback( ( node ) => { flyoutRef.current = node; if ( typeof ref === 'function' ) { ref( node ); } else if ( ref ) { ref.current = node; } }, [ ref ] ); /** * @description Clamps a width value between min and max. * * @since 6.0.10 * * @param {number} width Width to clamp. * * @return {number} Clamped width. */ const clampWidth = useCallback( ( width ) => { const effectiveMin = Math.min( pinnedMinWidth, pinnedMaxWidth ); return Math.min( Math.max( width, effectiveMin ), pinnedMaxWidth ); }, [ pinnedMinWidth, pinnedMaxWidth ] ); /** * @description Handles mouse move during drag. * * @since 6.0.10 * * @param {MouseEvent} event Mouse event. * * @return {void} */ const handleDragMove = useCallback( ( event ) => { if ( ! isDragging ) { return; } const deltaX = shouldInvertDragDirection ? event.clientX - dragStartX.current : dragStartX.current - event.clientX; const newWidth = clampWidth( dragStartWidth.current + deltaX ); setCurrentPinnedWidth( newWidth ); }, [ isDragging, shouldInvertDragDirection, clampWidth, setCurrentPinnedWidth ] ); /** * @description Handles mouse up to end drag. * * @since 6.0.10 * * @return {void} */ const handleDragEnd = useCallback( () => { if ( ! isDragging ) { return; } setIsDragging( false ); if ( dragHandleRef.current ) { dragHandleRef.current.setAttribute( 'data-dragging', 'false' ); } document.body.style.cursor = ''; document.body.style.userSelect = ''; onPinnedWidthChange( currentPinnedWidth ); }, [ isDragging, currentPinnedWidth, onPinnedWidthChange ] ); // Set up and clean up drag event listeners useEffect( () => { if ( isDragging ) { document.addEventListener( 'mousemove', handleDragMove ); document.addEventListener( 'mouseup', handleDragEnd ); return () => { document.removeEventListener( 'mousemove', handleDragMove ); document.removeEventListener( 'mouseup', handleDragEnd ); }; } }, [ isDragging, handleDragMove, handleDragEnd ] ); // Scroll active content into view when pinned useEffect( () => { if ( ! flyoutPinned || ! flyoutActive || ! pinnedActiveContentSelector ) { return; } const pinButton = flyoutRef.current?.querySelector( '.gform-flyout__pin' ); const isPinButtonVisible = pinButton && getComputedStyle( pinButton ).display !== 'none'; if ( ! isPinButtonVisible ) { return; } const activeElement = document.querySelector( pinnedActiveContentSelector ); if ( activeElement ) { activeElement.scrollIntoView( { behavior: 'smooth', block: 'nearest' } ); } }, [ flyoutPinned, flyoutActive, pinnedActiveContentSelector ] ); // Update drag handle aria attributes when flyout width changes useEffect( () => { if ( ! flyoutPinned || ! flyoutActive || ! flyoutRef.current || ! dragHandleRef.current ) { return; } const resizeObserver = new ResizeObserver( ( entries ) => { for ( const entry of entries ) { const observedWidth = Math.round( entry.contentRect.width ); dragHandleRef.current.setAttribute( 'aria-valuenow', String( observedWidth ) ); dragHandleRef.current.setAttribute( 'aria-valuetext', sprintf( pinnedDragHandleAriaValueText, observedWidth ) ); } } ); resizeObserver.observe( flyoutRef.current ); return () => { resizeObserver.disconnect(); }; }, [ flyoutPinned, flyoutActive, pinnedDragHandleAriaValueText ] ); useEffect( () => { // WHY: Skip running the open/close side-effects on initial mount. if ( isInitialFlyoutActiveRef.current ) { isInitialFlyoutActiveRef.current = false; return; } if ( flyoutActive ) { showFlyout(); } else if ( ! flyoutActive ) { closeFlyout(); } }, [ flyoutActive ] ); const handleEscapeRequest = ( e ) => { if ( e.key !== ESCAPE ) { return; } e.stopPropagation(); closeFlyout(); }; const pointerDownOrigin = useRef( null ); const handlePointerDown = ( event ) => { pointerDownOrigin.current = event.target; }; const handlePointerUp = ( event ) => { if ( pointerDownOrigin.current === event.target && event.target.classList.contains( 'gform-flyout__mask' ) && closeOnMaskClick && ! maskClickExcludeButtons.includes( event.button ) && flyoutActive ) { event.stopPropagation(); setFlyoutActive( false ); } pointerDownOrigin.current = null; }; const maskThemeValue = flyoutPinned ? 'none' : maskTheme; const maskProps = { className: classnames( { 'gform-flyout__mask': true, 'gform-flyout--anim-in-ready': animationReady, 'gform-flyout--anim-in-active': animationReady && animationActive, [ `gform-flyout__mask--position-${ position }` ]: true, [ `gform-flyout__mask--theme-${ maskThemeValue }` ]: true, 'gform-flyout__mask--blur': maskBlur && ! flyoutPinned, 'gform-flyout--offset-wpadmin-bar': offsetWPAdminBar, 'gform-flyout--offset-wpadmin-menu': offsetWPAdminMenu, 'gform-flyout--expandable': expandable, 'gform-flyout--expanded': flyoutExpanded && ! flyoutPinned, 'gform-flyout--pinnable': pinnable, 'gform-flyout--pinned': flyoutPinned, }, customMaskClasses ), id: `${ id }-mask`, onPointerDown: handlePointerDown, onPointerUp: handlePointerUp, style: { zIndex, }, }; const componentProps = { className: classnames( { 'gform-flyout': true, 'gform-flyout--anim-in-ready': animationReady, 'gform-flyout--anim-in-active': animationReady && animationActive, [ `gform-flyout--${ direction }` ]: true, [ `gform-flyout--${ position }` ]: true, 'gform-flyout--divider': showDivider, 'gform-flyout--no-divider': ! showDivider, 'gform-flyout--no-description': ! description, 'gform-flyout--scroll-simplebar': simplebar, 'gform-flyout--scroll-native': ! simplebar, 'gform-flyout--header-footer-fixed': headerIsFixed && footerIsFixed, 'gform-flyout--header-fixed': headerIsFixed, 'gform-flyout--footer-fixed': footerIsFixed, 'gform-flyout--dragging': isDragging || isKeyboardResizing, }, customClasses ), id, onKeyDown: handleEscapeRequest, ...customAttributes, }; /** * @function showFlyout * @description Opens the flyout and fires the `onOpen` function if passed in. * * @since 1.1.18 * * @return {void} */ const showFlyout = () => { setAnimationReady( true ); setTimeout( () => { setAnimationActive( true ); if ( resetScrollOnOpen ) { if ( simplebar && simplebarRef.current ) { const scrollElement = simplebarRef.current.getScrollElement(); if ( scrollElement ) { scrollElement.scrollTop = 0; } } else if ( headerIsFixed && bodyRef.current ) { bodyRef.current.scrollTop = 0; } else if ( ref && ref.current ) { ref.current.scrollTop = 0; } } onOpen(); }, 25 ); }; /** * @function closeFlyout * @description Closes the flyout and fires the `onClose` function if passed in. * * @since 1.1.18 * * @return {void} */ const closeFlyout = () => { setAnimationActive( false ); setTimeout( () => { setAnimationReady( false ); onClose(); }, animationDelay ); }; /** * @function getHeader * @description Returns the header of the flyout that contains the title, description, children, and close button. * * @since 1.1.18 * * @return {JSX.Element} */ const getHeader = () => { let buttonType = 'unstyled'; if ( closeButtonCustomAttributes.type === 'round' ) { buttonType = 'white'; } else if ( closeButtonCustomAttributes.type === 'simplified' ) { buttonType = 'simplified'; } const closeButtonProps = { circular: closeButtonCustomAttributes.type === 'round', customAttributes: { ...( i18n?.titleClose || closeButtonCustomAttributes.title ? { title: i18n?.titleClose || closeButtonCustomAttributes.title, } : {} ), }, icon: icons?.iconClose || closeButtonCustomAttributes.icon, iconPrefix: closeButtonCustomAttributes.iconPrefix || iconPrefix, label: i18n?.labelClose || closeButtonCustomAttributes.label || NEEDS_I18N_LABEL, onClick: () => setFlyoutActive( false ), size: 'size-height-s', type: buttonType, ...closeButtonCustomAttributes, customClasses: classnames( { 'gform-flyout__close': true, }, closeButtonCustomAttributes.customClasses || [], ), }; const pinnedButtonProps = { circular: pinnedButtonCustomAttributes.type === 'round', iconPrefix, size: 'size-height-s', type: buttonType, ...pinnedButtonCustomAttributes, customAttributes: { title: flyoutPinned ? ( i18n?.titleUnpin || NEEDS_I18N_LABEL ) : ( i18n?.titlePin || NEEDS_I18N_LABEL ), }, icon: flyoutPinned ? icons?.iconUnpin : icons?.iconPin, label: flyoutPinned ? ( i18n?.labelUnpin || NEEDS_I18N_LABEL ) : ( i18n?.labelPin || NEEDS_I18N_LABEL ), onClick: ( event ) => { const updatedPinnedState = ! flyoutPinned; setFlyoutPinned( updatedPinnedState ); onPinnedChange( updatedPinnedState, event ); }, customClasses: classnames( { 'gform-flyout__pin': true, }, pinnedButtonCustomAttributes.customClasses || [], ), }; const expandableButtonProps = { circular: expandableButtonCustomAttributes.type === 'round', iconPrefix, size: 'size-height-s', type: buttonType, ...expandableButtonCustomAttributes, customAttributes: { title: flyoutExpanded ? ( i18n?.titleCollapse || NEEDS_I18N_LABEL ) : ( i18n?.titleExpand || NEEDS_I18N_LABEL ), disabled: flyoutPinned, }, icon: flyoutExpanded ? icons?.iconCollapse : icons?.iconExpand, label: flyoutExpanded ? ( i18n?.labelCollapse || NEEDS_I18N_LABEL ) : ( i18n?.labelExpand || NEEDS_I18N_LABEL ), onClick: ( event ) => { const updatedExpandedState = ! flyoutExpanded; setFlyoutExpanded( updatedExpandedState ); onExpandedChange( updatedExpandedState, event ); }, customClasses: classnames( { 'gform-flyout__expander': true, }, expandableButtonCustomAttributes.customClasses || [], ), }; const headerProps = { className: classnames( { 'gform-flyout__head': true, } ), }; const headingProps = { customClasses: classnames( { 'gform-flyout__title': true, } ), content: title, size: 'display-xs', tagName: 'h3', weight: 'SemiBold', ...headerHeadingCustomAttributes, }; const descriptionProps = { customClasses: classnames( { 'gform-flyout__desc': true, } ), content: description, ...headerDescriptionCustomAttributes, }; return ( <header { ...headerProps }> <div className="gform-flyout__head-left"> { title && <Heading { ...headingProps } /> } { description && <Text { ...descriptionProps } /> } { headerChildrenLeft } </div> <div className="gform-flyout__head-right"> { headerChildrenRight } { pinnable && <Button { ...pinnedButtonProps } /> } { expandable && <Button { ...expandableButtonProps } /> } { <Button { ...closeButtonProps } /> } </div> </header> ); }; /** * @function getBody * @description Returns the body that wrap the children of the flyout. * * @since 1.1.18 * * @return {JSX.Element} */ const getBody = () => { const bodyProps = { className: classnames( { 'gform-flyout__body': true, }, customBodyClasses ), }; const innerBodyProps = { className: classnames( { 'gform-flyout__body-inner': true, }, customInnerBodyClasses ), }; return ( <ConditionalWrapper condition={ simplebar && headerIsFixed && footerIsFixed } wrapper={ ( ch ) => <SimpleBar className="gform-flyout__simplebar" ref={ simplebarRef }>{ ch }</SimpleBar> } > <div { ...bodyProps } ref={ bodyRef } > <div { ...innerBodyProps } > { children } </div> </div> </ConditionalWrapper> ); }; /** * @function getFooter * @description Returns the footer of the flyout that contains optional children. * * @since 5.7.4 * * @return {JSX.Element} */ const getFooter = () => { if ( ! footerChildrenLeft && ! footerChildrenRight ) { return null; } const footerProps = { className: classnames( { 'gform-flyout__footer': true, } ), }; return ( <footer { ...footerProps }> { footerChildrenLeft && ( <div className="gform-flyout__footer-left"> { footerChildrenLeft } </div> ) } { footerChildrenRight && ( <div className="gform-flyout__footer-right"> { footerChildrenRight } </div> ) } </footer> ); }; /** * @function handleDragStart * @description Handles mouse down to start drag. * * @since 6.0.10 * * @param {MouseEvent} event Mouse event. * * @return {void} */ const handleDragStart = ( event ) => { event.preventDefault(); setIsDragging( true ); dragStartX.current = event.clientX; dragStartWidth.current = currentPinnedWidth; if ( dragHandleRef.current ) { dragHandleRef.current.setAttribute( 'data-dragging', 'true' ); } document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; }; /** * @function handleDragKeyDown * @description Handles keyboard navigation for the drag handle. * * @since 6.0.10 * * @param {KeyboardEvent} event Keyboard event. * * @return {void} */ const handleDragKeyDown = ( event ) => { const step = event.shiftKey ? pinnedDragHandleKeyboardStepLarge : pinnedDragHandleKeyboardStep; let newWidth = currentPinnedWidth; const increaseKey = shouldInvertDragDirection ? ARROW_RIGHT : ARROW_LEFT; const decreaseKey = shouldInvertDragDirection ? ARROW_LEFT : ARROW_RIGHT; switch ( event.key ) { case increaseKey: newWidth = clampWidth( currentPinnedWidth + step ); event.preventDefault(); break; case decreaseKey: newWidth = clampWidth( currentPinnedWidth - step ); event.preventDefault(); break; default: return; } setIsKeyboardResizing( true ); if ( dragHandleRef.current ) { dragHandleRef.current.setAttribute( 'data-dragging', 'true' ); } if ( keyboardResizeTimeout.current ) { clearTimeout( keyboardResizeTimeout.current ); } keyboardResizeTimeout.current = setTimeout( () => { setIsKeyboardResizing( false ); if ( dragHandleRef.current ) { dragHandleRef.current.setAttribute( 'data-dragging', 'false' ); } }, 150 ); setCurrentPinnedWidth( newWidth ); onPinnedWidthChange( newWidth ); }; /** * @function getPinnedDraggableHandle * @description Returns the draggable handle for the pinned flyout. * * @since 6.0.2 * * @return {JSX.Element} The draggable handle element. */ const getPinnedDraggableHandle = () => { const handleStyle = { zIndex, }; return ( <div className="gform-flyout__pinned-drag-handle-wrapper" style={ handleStyle }> <div ref={ dragHandleRef } className="gform-flyout__pinned-drag-handle" data-dragging={ isDragging ? 'true' : 'false' } role="slider" aria-label={ pinnedDragHandleLabel } aria-valuemin={ pinnedMinWidth } aria-valuemax={ pinnedMaxWidth } aria-valuenow={ currentPinnedWidth } aria-valuetext={ sprintf( pinnedDragHandleAriaValueText, currentPinnedWidth ) } tabIndex="0" onMouseDown={ handleDragStart } onKeyDown={ handleDragKeyDown } /> </div> ); }; /** * @function getCSS * @description Returns the CSS used to handle the width and mobile media query. * * @since 1.1.18 * * @return {string} The CSS for the flyout. */ const getCSS = () => { let css = `#${ id } { max-width: ${ maxWidth ? `${ maxWidth }px` : 'none' }; width: ${ mobileWidth }; z-index: ${ zIndex } } #${ id }-mask { --gform-admin-flyout-top-offset: ${ offset }; } `; if ( mobileBreakpoint ) { css += ` @media only screen and (min-width: ${ mobileBreakpoint }px) { #${ id } { width: ${ desktopWidth }; } } `; } if ( expandable ) { if ( mobileBreakpoint ) { css += ` .gform-flyout--expandable #${ id } .gform-flyout__expander { display: none; } `; css += ` @media only screen and (min-width: ${ mobileBreakpoint }px) { .gform-flyout--expandable #${ id } .gform-flyout__expander { display: inherit; } .gform-flyout--expanded #${ id } { width: ${ expandableWidthDesktop }; } } `; } } if ( pinnable ) { const pinnedContentPaddingProp = direction === 'left' ? 'inline-start' : 'inline-end'; css += ` .${ componentsPrefix }:has(#${ id }-mask.gform-flyout--pinnable) { --${ componentsPrefix }-flyout-pinned-content-min-width: ${ pinnedContentMinWidth }px; /* Flyout constraints */ --${ componentsPrefix }-flyout-pinned-min-width: ${ pinnedMinWidth }px; --${ componentsPrefix }-flyout-pinned-max-width: min(${ pinnedMaxWidth }px, calc(100vw - var(--${ componentsPrefix }-flyout-pinned-content-min-width) - var(--gform-admin-wp-admin-menu-offset))); --${ componentsPrefix }-flyout-pinned-width: ${ currentPinnedWidth }px; --${ componentsPrefix }-flyout-pinned-clamped-width: clamp( var(--${ componentsPrefix }-flyout-pinned-min-width), var(--${ componentsPrefix }-flyout-pinned-width), var(--${ componentsPrefix }-flyout-pinned-max-width) ); /* Content constraints */ --${ componentsPrefix }-flyout-pinned-content-offset: max( var(--${ componentsPrefix }-flyout-pinned-content-min-width), var(--${ componentsPrefix }-flyout-pinned-clamped-width) ); } `; if ( mobileBreakpoint ) { css += ` .gform-flyout--pinnable #${ id } .gform-flyout__pin, .gform-flyout--pinnable #${ id } .gform-flyout__pinned-drag-handle-wrapper { display: none; } `; css += ` @media only screen and (min-width: ${ mobileBreakpoint }px) { .gform-flyout--pinnable #${ id } .gform-flyout__pin { display: inherit; } .gform-flyout--pinnable #${ id } .gform-flyout__pinned-drag-handle-wrapper { display: block; } .gform-flyout--pinned #${ id } { width: var(--${ componentsPrefix }-flyout-pinned-clamped-width) !important; } .${ componentsPrefix }:has(#${ id }-mask.gform-flyout--anim-in-active.gform-flyout--pinned) ${ pinnedContentSelector } { padding-${ pinnedContentPaddingProp }: var(--${ componentsPrefix }-flyout-pinned-content-offset) !important; } } `; } } return css; }; return ( <div { ...maskProps } ref={ trapRef }> <article { ...componentProps } ref={ setFlyoutRef } > <ConditionalWrapper condition={ simplebar && ! ( headerIsFixed && footerIsFixed ) } wrapper={ ( ch ) => <SimpleBar className="gform-flyout__simplebar" ref={ simplebarRef }>{ ch }</SimpleBar> } > { beforeContent } { getHeader() } { getBody() } { getFooter() } { afterContent } </ConditionalWrapper> { pinnable && flyoutPinned && getPinnedDraggableHandle() } </article> <style> { getCSS() } </style> </div> ); } ); Flyout.propTypes = { afterContent: PropTypes.node, animationDelay: PropTypes.number, beforeContent: PropTypes.node, children: PropTypes.oneOfType( [ PropTypes.arrayOf( PropTypes.node ), PropTypes.node, ] ), closeButtonCustomAttributes: PropTypes.object, closeOnMaskClick: PropTypes.bool, componentsPrefix: PropTypes.string, customAttributes: PropTypes.object, customBodyClasses: PropTypes.oneOfType( [ PropTypes.string, PropTypes.array, PropTypes.object, ] ), customClasses: PropTypes.oneOfType( [ PropTypes.string, PropTypes.array, PropTypes.object, ] ), customInnerBodyClasses: PropTypes.oneOfType( [ PropTypes.string, PropTypes.array, PropTypes.object, ] ), description: PropTypes.string, desktopWidth: PropTypes.string, direction: PropTypes.string, expandable: PropTypes.bool, expandableButtonCustomAttributes: PropTypes.object, expandableWidthDesktop: PropTypes.string, footerChildrenLeft: PropTypes.oneOfType( [ PropTypes.arrayOf( PropTypes.node ), PropTypes.node, ] ), footerChildrenRight: PropTypes.oneOfType( [ PropTypes.arrayOf( PropTypes.node ), PropTypes.node, ] ), footerIsFixed: PropTypes.bool, headerChildrenLeft: PropTypes.oneOfType( [ PropTypes.arrayOf( PropTypes.node ), PropTypes.node, ] ), headerChildrenRight: PropTypes.oneOfType( [ PropTypes.arrayOf( PropTypes.node ), PropTypes.node, ] ), headerDescriptionCustomAttributes: PropTypes.object, headerHeadingCustomAttributes: PropTypes.object, headerIsFixed: PropTypes.bool, i18n: PropTypes.object, iconPrefix: PropTypes.string, icons: PropTypes.object, id: PropTypes.string, isExpanded: PropTypes.bool, isOpen: PropTypes.bool, isPinned: PropTypes.bool, maskBlur: PropTypes.bool, maskClickExcludeButtons: PropTypes.array, maskTheme: PropTypes.string, maxWidth: PropTypes.number, mobileBreakpoint: PropTypes.number, mobileWidth: PropTypes.string, offset: PropTypes.string, offsetWPAdminBar: PropTypes.bool, offsetWPAdminMenu: PropTypes.bool, onClose: PropTypes.func, onExpandedChange: PropTypes.func, onOpen: PropTypes.func, onPinnedChange: PropTypes.func, onPinnedWidthChange: PropTypes.func, pinnable: PropTypes.bool, pinnedActiveContentSelector: PropTypes.string, pinnedButtonCustomAttributes: PropTypes.object, pinnedContentSelector: PropTypes.string, pinnedContentMinWidth: PropTypes.number, pinnedDragHandleLabel: PropTypes.string, pinnedDragHandleKeyboardStep: PropTypes.number, pinnedDragHandleKeyboardStepLarge: PropTypes.number, pinnedDefaultWidth: PropTypes.number, pinnedMaxWidth: PropTypes.number, pinnedMinWidth: PropTypes.number, pinnedWidth: PropTypes.number, position: PropTypes.oneOf( [ 'absolute', 'fixed' ] ), resetScrollOnOpen: PropTypes.bool, showDivider: PropTypes.bool, simplebar: PropTypes.bool, title: PropTypes.string, zIndex: PropTypes.number, }; Flyout.displayName = 'Flyout'; export default Flyout;