@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
JavaScript
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;