UNPKG

@gravityforms/components

Version:

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

524 lines (492 loc) 15 kB
import { React, classnames, ReactCalendar, PropTypes } from '@gravityforms/libraries'; import { IdProvider, useFocusTrap, useIdContext, usePopup, useStateWithDep, } from '@gravityforms/react-utils'; import { spacerClasses, deepMerge, getClosest } from '@gravityforms/utils'; import { LeftArrow, RightArrow, DoubleLeftArrow, DoubleRightArrow } from './Icons'; import Button from '../../elements/Button'; const { forwardRef, useRef, useEffect, Fragment } = React; /** * @constant * @type {object} * @description Default attributes for the ReactCalendar component. * * @since 4.4.0 * * @property {string|JSX.Element} nextLabel The next button label. * @property {string|JSX.Element} next2Label The next2 button label. * @property {Function} onChange The onChange event handler. * @property {string|JSX.Element} prevLabel The previous button label. * @property {string|JSX.Element} prev2Label The previous2 button label. */ const defaultCalendarAttributes = { maxDetail: 'month', nextLabel: <RightArrow />, next2Label: <DoubleRightArrow />, onActiveStartDateChange: () => {}, onChange: () => {}, onViewChange: () => {}, prevLabel: <LeftArrow />, prev2Label: <DoubleLeftArrow />, }; /** * @function getTodayStartOfDay * @description Get the date object of the start of day for today. * * @since 4.4.0 * * @return {Date} The date object of the start of day for today. */ const getTodayStartOfDay = () => { const date = new Date(); date.setHours( 0, 0, 0, 0 ); return date; }; /** * @function getTodayEndOfDay * @description Get the date object of the end of day for today. * * @since 4.4.0 * * @return {Date} The date object of the end of day for today. */ const getTodayEndOfDay = () => { const date = new Date(); date.setHours( 23, 59, 59, 999 ); return date; }; /** * @function getStart * @description Get the start date based on range type. * * @since 4.4.0 * * @param {string} rangeType The range type, one of `century`, `decade`, `year`, or `month`. * @param {Date} date The date object. * * @return {Date} The start of range type date object. */ const getStart = ( rangeType, date ) => { let year = date.getFullYear(); let month = 0; switch ( rangeType ) { case 'century': year = year + ( ( -year + 1 ) % 100 ); break; case 'decade': year = year + ( ( -year + 1 ) % 10 ); break; case 'year': break; case 'month': month = date.getMonth(); break; default: throw new Error( `Invalid rangeType: ${ rangeType }` ); } const begin = new Date(); begin.setFullYear( year, month, 1 ); begin.setHours( 0, 0, 0, 0 ); return begin; }; /** * @module CalendarComponent * @description A calendar component that uses the ReactCalendar component. * * @since 4.4.0 * * @param {object} props Component props. * @param {object} ref Ref to the component. * * @return {JSX.Element} The calendar component. */ const CalendarComponent = forwardRef( ( props, ref ) => { const { calendarAttributes, calendarClasses, closeOnChange, customAttributes, customClasses, onAfterClose, onAfterOpen, onClose, onOpen, onResetClick, onTodayClick, resetAttributes, resetClasses, showResetButton, showTodayButton, spacing, todayAttributes, todayClasses, triggerAttributes, triggerClasses, withTrigger, } = props; const [ activeStartDate, setActiveStartDate ] = useStateWithDep( calendarAttributes.activeStartDate || null ); const [ value, setValue ] = useStateWithDep( calendarAttributes.value || null ); const [ view, setView ] = useStateWithDep( calendarAttributes.view || null ); const id = useIdContext(); // Refs const calendarRef = useRef( null ); const triggerRef = useRef( null ); const { closePopup, openPopup, handleEscKeyDown, popupHide, popupOpen, popupReveal, } = usePopup( { customClickOutsideLogic: ( event ) => { const clickIsTile = event.target.classList.contains( 'react-calendar__tile' ); const clickInCalendar = [ '.react-calendar__tile', '.react-calendar__month-view', '.react-calendar__year-view', '.react-calendar__decade-view', '.react-calendar__century-view', ].reduce( ( carry, selector ) => { if ( carry ) { return carry; } const closest = getClosest( event.target, selector ); if ( closest ) { return true; } return false; }, false ); return clickIsTile || clickInCalendar; }, onAfterClose, onAfterOpen, onClose, onOpen, popupRef: calendarRef, triggerRef, } ); const mergedCalendarAttributes = deepMerge( defaultCalendarAttributes, calendarAttributes ); const maxDetail = mergedCalendarAttributes.maxDetail || 'month'; const trapRef = useFocusTrap( popupOpen ); const { onKeyDown: calendarWrapperOnKeyDown = () => {}, ...restCalendarWrapperAttributes } = customAttributes; const calendarWrapperProps = { className: classnames( { 'gform-calendar': true, 'gform-calendar--with-trigger': withTrigger, ...( withTrigger ? {} : spacerClasses( spacing ) ), }, customClasses ), ...restCalendarWrapperAttributes, }; if ( withTrigger ) { calendarWrapperProps.onKeyDown = ( event ) => { handleEscKeyDown( event ); calendarWrapperOnKeyDown( event ); }; } const adjustCalendarPosition = () => { if ( triggerRef.current && calendarRef.current ) { calendarRef.current.style.marginInlineStart = 0; const calendarRect = calendarRef.current.getBoundingClientRect(); const viewportWidth = window.innerWidth; let marginInlineStart = 0; if ( ( calendarRect.right + 20 ) - viewportWidth >= 0 ) { marginInlineStart = -( ( calendarRect.right + 20 ) - viewportWidth ); } calendarRef.current.style.marginInlineStart = `${ marginInlineStart }px`; } }; useEffect( () => { if ( withTrigger && popupOpen ) { adjustCalendarPosition(); window.addEventListener( 'resize', adjustCalendarPosition ); return () => { window.removeEventListener( 'resize', adjustCalendarPosition ); }; } }, [ withTrigger, popupOpen ] ); const resetControl = ( args ) => { if ( activeStartDate ) { setActiveStartDate( null ); } setView( args.view ); }; const calendarProps = deepMerge( defaultCalendarAttributes, { className: classnames( [ 'gform-calendar__calendar', ], calendarClasses ), ...mergedCalendarAttributes, activeStartDate, onActiveStartDateChange: ( args ) => { if ( [ 'drillDown', 'drillUp' ].includes( args.action ) ) { return; } resetControl( args ); mergedCalendarAttributes.onActiveStartDateChange( args ); }, onChange: ( newValue, event ) => { setValue( newValue ); mergedCalendarAttributes.onChange( newValue, event ); if ( closeOnChange ) { closePopup(); } }, onViewChange: ( args ) => { resetControl( args ); mergedCalendarAttributes.onViewChange( args ); }, value, view, } ); const todayButtonProps = { label: 'Today', customClasses: classnames( 'gform-calendar__today-button', todayClasses ), onClick: () => { const today = new Date(); const newValue = calendarProps.selectRange ? [ getTodayStartOfDay(), getTodayEndOfDay() ] : today; setActiveStartDate( today ); setView( maxDetail ); onTodayClick(); calendarProps.onChange( newValue ); if ( view !== maxDetail ) { calendarProps.onViewChange( { action: 'drillDown', activeStartDate: getStart( maxDetail, today ), value: today, view: maxDetail, } ); } if ( closeOnChange ) { closePopup(); } }, size: 'size-height-s', type: 'white', ...todayAttributes, }; const resetButtonProps = { label: 'Reset', customClasses: classnames( 'gform-calendar__reset-button', resetClasses ), onClick: () => { const newValue = calendarProps.selectRange ? [] : null; onResetClick(); calendarProps.onChange( newValue ); if ( closeOnChange ) { closePopup(); } }, size: 'size-height-s', type: 'white', ...resetAttributes, }; const { ariaId: triggerAriaId = `${ id }-trigger-aria`, ariaText: triggerAriaText = '', customAttributes: triggerCustomAttributes = {}, id: triggerId = `${ id }-trigger`, onClick: triggerOnClick = () => {}, onKeyDown: triggerOnKeyDown = () => {}, title: triggerTitle = '', ...restTriggerAttributes } = triggerAttributes; const triggerProps = { className: classnames( 'gform-calendar__trigger', triggerClasses ), customAttributes: { 'aria-expanded': popupOpen ? 'true' : 'false', 'aria-haspopup': 'dialog', 'aria-labelledby': triggerTitle ? undefined : `${ triggerAriaId } ${ triggerId }`, id: triggerId, onKeyDown: ( event ) => { handleEscKeyDown( event ); triggerOnKeyDown( event ); }, title: triggerTitle || undefined, ...triggerCustomAttributes, }, onClick: ( event ) => { triggerOnClick( event ); if ( popupOpen ) { closePopup(); } else { openPopup(); } }, size: 'size-height-m', type: 'white', ...restTriggerAttributes, }; const outerWrapperProps = { className: classnames( { 'gform-calendar__wrapper': true, 'gform-calendar__wrapper--open': popupOpen, 'gform-calendar__wrapper--hide': popupHide, 'gform-calendar__wrapper--reveal': popupReveal, ...( withTrigger ? spacerClasses( spacing ) : {} ), } ), ref: trapRef, }; const OuterWrapper = withTrigger ? 'div' : Fragment; const showFooter = showTodayButton || showResetButton; return ( <OuterWrapper { ...( OuterWrapper === 'div' ? outerWrapperProps : {} ) }> { withTrigger && ( <> <span className="gform-visually-hidden" id={ triggerAriaId } > { triggerAriaText } </span> <Button { ...triggerProps } ref={ triggerRef } /> </> ) } <div { ...calendarWrapperProps } ref={ ( node ) => { calendarRef.current = node; if ( typeof ref === 'function' ) { ref( node ); } else if ( ref ) { ref.current = node; } } }> <ReactCalendar.Calendar { ...calendarProps } /> { showFooter && ( <div className="gform-calendar__footer"> { showTodayButton && <Button { ...todayButtonProps } /> } { showResetButton && <Button { ...resetButtonProps } /> } </div> ) } </div> </OuterWrapper> ); } ); /** * @module Calendar * @description A calendar component with id wrapper. The calendar is run in controlled mode to allow for the today button to work. * * @since 4.4.0 * * @param {object} props Component props. * @param {object} props.calendarAttributes Custom attributes for the calendar. * @param {string|Array|object} props.calendarClasses Custom classes for the calendar. * @param {boolean} props.closeOnChange Close the calendar when the value changes. * @param {object} props.customAttributes Custom attributes for the component. * @param {string|Array|object} props.customClasses Custom classes for the component. * @param {string} props.id The id for the component. * @param {Function} props.onAfterClose The after close event handler. * @param {Function} props.onAfterOpen The after open event handler. * @param {Function} props.onClose The close event handler. * @param {Function} props.onOpen The open event handler. * @param {Function} props.onResetClick The click event handler for the reset button. * @param {Function} props.onTodayClick The click event handler for the today button. * @param {object} props.resetAttributes Custom attributes for the reset button. * @param {string|Array|object} props.resetClasses Custom classes for the reset button. * @param {boolean} props.showResetButton Whether to show the reset button or not. * @param {boolean} props.showTodayButton Whether to show the today button or not. * @param {string|number|Array|object} props.spacing The spacing for the component, as a string, number, array, or object. * @param {object} props.todayAttributes Custom attributes for the today button. * @param {string|Array|object} props.todayClasses Custom classes for the today button. * @param {object} props.triggerAttributes Custom attributes for the trigger button. * @param {string|Array|object} props.triggerClasses Custom classes for the trigger button. * @param {boolean} props.withTrigger Whether to show the trigger button or not. * * @return {JSX.Element} The Calendar component. * * @example * import Calendar from '@gravityforms/components/react/admin/modules/Calendar'; * * return <Calendar />; * */ const Calendar = forwardRef( ( props, ref ) => { const defaultProps = { calendarAttributes: {}, calendarClasses: [], closeOnChange: true, customAttributes: {}, customClasses: [], id: '', onAfterClose: () => {}, onAfterOpen: () => {}, onClose: () => {}, onOpen: () => {}, onResetClick: () => {}, onTodayClick: () => {}, resetAttributes: {}, resetClasses: [], showResetButton: false, showTodayButton: true, spacing: '', todayAttributes: {}, todayClasses: [], triggerAttributes: {}, triggerClasses: [], withTrigger: false, }; const combinedProps = { ...defaultProps, ...props }; const { id: idProp } = combinedProps; const idProviderProps = { id: idProp }; return ( <IdProvider { ...idProviderProps }> <CalendarComponent { ...combinedProps } ref={ ref } /> </IdProvider> ); } ); Calendar.propTypes = { calendarAttributes: PropTypes.object, calendarClasses: PropTypes.oneOfType( [ PropTypes.string, PropTypes.array, PropTypes.object, ] ), closeOnChange: PropTypes.bool, customAttributes: PropTypes.object, customClasses: PropTypes.oneOfType( [ PropTypes.string, PropTypes.array, PropTypes.object, ] ), id: PropTypes.string, onAfterClose: PropTypes.func, onAfterOpen: PropTypes.func, onClose: PropTypes.func, onOpen: PropTypes.func, onResetClick: PropTypes.func, onTodayClick: PropTypes.func, resetAttributes: PropTypes.object, resetClasses: PropTypes.oneOfType( [ PropTypes.string, PropTypes.array, PropTypes.object, ] ), showResetButton: PropTypes.bool, showTodayButton: PropTypes.bool, spacing: PropTypes.oneOfType( [ PropTypes.string, PropTypes.number, PropTypes.array, PropTypes.object, ] ), todayAttributes: PropTypes.object, todayClasses: PropTypes.oneOfType( [ PropTypes.string, PropTypes.array, PropTypes.object, ] ), triggerAttributes: PropTypes.object, triggerClasses: PropTypes.oneOfType( [ PropTypes.string, PropTypes.array, PropTypes.object, ] ), withTrigger: PropTypes.bool, }; export default Calendar;