UNPKG

@gravityforms/components

Version:

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

390 lines (356 loc) 12.5 kB
import { React, classnames } from '@gravityforms/libraries'; import { spacerClasses } from '@gravityforms/utils'; import Box from '../../elements/Box'; import Input from '../../elements/Input'; import Dropdown from '../Dropdown'; const { useState, useEffect, forwardRef } = React; const NEEDS_I18N_LABEL = 'Needs i18n'; // Helper function to sort regions based on preferredRegions const sortRegions = ( regions, preferred ) => { if ( ! preferred || ! Array.isArray( preferred ) || preferred.length === 0 ) { return [ ...regions ]; } const preferredItems = []; const otherItems = []; regions.forEach( ( region ) => { const index = preferred.indexOf( region.value ); if ( index !== -1 ) { preferredItems[ index ] = region; } else { otherItems.push( region ); } } ); // Filter out any undefined slots and combine with other items return [ ...preferredItems.filter( ( item ) => item !== undefined ), ...otherItems, ]; }; const transformRegionCodes = ( inputObj ) => { const result = {}; for ( const key in inputObj ) { if ( Object.prototype.hasOwnProperty.call( inputObj, key ) ) { result[ key ] = Object.entries( inputObj[ key ] ).map( ( [ value, label ] ) => ( { value, label, } ) ); } } return result; }; /** * @module Address * @description Renders an address component. * * @since 5.5.0 * * @param {React.ReactNode|React.ReactNode[]} [children] - React element children * @param {Record<string, any>} [customAttributes] - Custom attributes for the component * @param {string|string[]|Record<string, boolean>} [customClasses] - Custom classes for the component * @param {Record<string, any>} [countryDropdownAttributes] - Attributes for the country dropdown * @param {string|string[]|Record<string, boolean>} [countryDropdownClasses] - Classes for the country dropdown * @param {Record<string, any>} [defaultData] - Default data for the component state * @param {string[]} [disabledFields] - Fields to disable in the component. Use the state keys: lineOne, lineTwo, city, state, postalCode, country * @param {Record<string, any>} [initialData] - Initial data for the component state * @param {Record<string, any>} [i18n] - Internationalization settings * @param {function} [onChange] - Callback for when the component changes, returns the state * @param {boolean} [parseRegionCodes] - Whether to parse region codes for the component * @param {Record<string, any>} [preferredRegions] - Preferred regions for the component: countries, usStates, caProvinces * @param {Record<string, any>} [regionCodes] - Region codes for the component * @param {string|number|string[]|Record<string, any>} [spacing=''] - The spacing for the component * @param {Record<string, any>} [stateDropdownAttributes] - Attributes for the state/province dropdown * @param {string|string[]|Record<string, boolean>} [stateDropdownClasses] - Classes for the state/province dropdown * @param {boolean} [useSelectPlaceholders] - Whether to use select placeholders for the dropdowns * @param {React.RefObject<HTMLElement>|null} ref - Ref to the component * * @return {JSX.Element} The Address component. * * @example * import Address from '@gravityforms/components/react/admin/modules/Address'; * * // Basic usage with all fields * <Address * regionCodes={{ * countries: [{ value: 'US', label: 'United States' }, { value: 'CA', label: 'Canada' }], * usStates: [{ value: 'CA', label: 'California' }, { value: 'NY', label: 'New York' }], * caProvinces: [{ value: 'ON', label: 'Ontario' }, { value: 'QC', label: 'Quebec' }] * }} * onChange={(data) => console.log(data)} * /> * * // With preferred regions and initial data * <Address * regionCodes={{ * countries: [{ value: 'US', label: 'United States' }, { value: 'CA', label: 'Canada' }], * usStates: [{ value: 'CA', label: 'California' }, { value: 'NY', label: 'New York' }], * caProvinces: [{ value: 'ON', label: 'Ontario' }, { value: 'QC', label: 'Quebec' }] * }} * preferredRegions={{ * countries: ['CA', 'US'], * usStates: ['NY'], * caProvinces: ['QC'] * }} * initialData={{ * country: 'US', * state: 'CA', * city: 'San Francisco' * }} * /> * * // With disabled fields * <Address * regionCodes={{ * countries: [{ value: 'US', label: 'United States' }, { value: 'CA', label: 'Canada' }] * }} * disabledFields={['lineTwo', 'state']} * initialData={{ country: 'US' }} * /> * * // With custom classes and spacing * <Address * regionCodes={{ * countries: [{ value: 'US', label: 'United States' }] * }} * customClasses={['custom-address-class']} * spacing={4} * customAttributes={{ 'data-test': 'address-component' }} * > * <div>Additional content</div> * </Address> */ const Address = forwardRef( ( { children = null, customAttributes = {}, customClasses = [], countryDropdownAttributes = {}, countryDropdownClasses = [], defaultData = {}, disabledFields = [], initialData = {}, i18n = {}, onChange = () => {}, parseRegionCodes = false, preferredRegions = {}, regionCodes = {}, spacing = '', stateDropdownAttributes = {}, stateDropdownClasses = [], useSelectPlaceholders = true, }, ref ) => { const getInitialValue = ( field ) => { if ( disabledFields.includes( field ) ) { return undefined; } return initialData[ field ] || defaultData[ field ] || ''; }; const [ addressData, setAddressData ] = useState( () => ( [ 'lineOne', 'lineTwo', 'city', 'state', 'postalCode', 'country' ].reduce( ( carry, key ) => ( { ...carry, [ key ]: getInitialValue( key ), } ), {} ) ) ); const { countries = [], usStates = [], caProvinces = [] } = parseRegionCodes ? transformRegionCodes( regionCodes ) : regionCodes; const sortedCountries = sortRegions( countries, preferredRegions.countries ); const sortedUsStates = sortRegions( usStates, preferredRegions.usStates ); const sortedCaProvinces = sortRegions( caProvinces, preferredRegions.caProvinces ); useEffect( () => { const filteredData = Object.fromEntries( Object.entries( addressData ).filter( ( [ key ] ) => ! disabledFields.includes( key ) ) ); onChange( filteredData ); }, [ addressData ] ); const handleInputChange = ( field ) => ( value ) => { if ( ! disabledFields.includes( field ) ) { setAddressData( ( prev ) => ( { ...prev, [ field ]: value, } ) ); } }; const handleDropdownChange = ( field ) => ( event, regionCode ) => { if ( ! disabledFields.includes( field ) ) { let options = []; if ( field === 'country' ) { options = sortedCountries; } else if ( field === 'state' ) { if ( addressData.country === 'US' ) { options = sortedUsStates; } else if ( addressData.country === 'CA' ) { options = sortedCaProvinces; } } const selected = options.find( ( opt ) => opt.value === regionCode.value ); setAddressData( ( prev ) => ( { ...prev, [ field ]: selected ? selected.value : regionCode.value, ...( field === 'country' && ! disabledFields.includes( 'state' ) && { state: '' } ), } ) ); } }; const getStateLabelPlaceholder = () => { if ( addressData.country === 'US' ) { return { label: i18n.stateLabel || NEEDS_I18N_LABEL, placeholder: i18n.statePlaceholder || NEEDS_I18N_LABEL, }; } if ( addressData.country === 'CA' ) { return { label: i18n.provinceLabel || NEEDS_I18N_LABEL, placeholder: i18n.provincePlaceholder || NEEDS_I18N_LABEL, }; } return { label: i18n.stateProvinceLabel || NEEDS_I18N_LABEL, placeholder: i18n.stateProvincePlaceholder || NEEDS_I18N_LABEL, }; }; const getPostalCodeLabelPlaceholder = () => { if ( addressData.country === 'US' ) { return { label: i18n.zipCodeLabel || NEEDS_I18N_LABEL, placeholder: i18n.zipCodePlaceholder || NEEDS_I18N_LABEL, }; } if ( addressData.country === 'CA' ) { return { label: i18n.postalCodeLabel || NEEDS_I18N_LABEL, placeholder: i18n.postalCodePlaceholder || NEEDS_I18N_LABEL, }; } return { label: i18n.zipPostalCodeLabel || NEEDS_I18N_LABEL, placeholder: i18n.zipPostalCodePlaceholder || NEEDS_I18N_LABEL, }; }; const renderInputField = ( key ) => { if ( disabledFields.includes( key ) ) { return null; } let label = i18n[ `${ key }Label` ] || NEEDS_I18N_LABEL; let placeholder = i18n[ `${ key }Placeholder` ] || NEEDS_I18N_LABEL; if ( key === 'state' ) { ( { label, placeholder } = getStateLabelPlaceholder() ); } else if ( key === 'postalCode' ) { ( { label, placeholder } = getPostalCodeLabelPlaceholder() ); } return <Input key={ key } labelAttributes={ { label } } value={ addressData[ key ] } onChange={ handleInputChange( key ) } placeholder={ placeholder } spacing={ 3 } />; }; const renderCountryField = () => { if ( disabledFields.includes( 'country' ) ) { return null; } if ( sortedCountries.length === 0 ) { return renderInputField( 'country' ); } const listItems = useSelectPlaceholders ? [ { label: i18n.countrySelectPlaceholder || NEEDS_I18N_LABEL, value: '', }, ...sortedCountries ] : sortedCountries; const countryDropdownProps = { controlled: true, hasSearch: true, i18n, label: i18n.countryLabel || NEEDS_I18N_LABEL, listItems, onChange: handleDropdownChange( 'country' ), popoverMaxHeight: 300, size: 'r', value: addressData.country, ...countryDropdownAttributes, className: classnames( { 'gform-address__country-dropdown': true, ...countryDropdownClasses, } ), }; return ( <Dropdown { ...countryDropdownProps } /> ); }; const renderStateProvinceField = () => { if ( disabledFields.includes( 'state' ) ) { return null; } if ( addressData.country === 'US' && sortedUsStates.length > 0 ) { const listItems = useSelectPlaceholders ? [ { label: i18n.stateSelectPlaceholder || NEEDS_I18N_LABEL, value: '', }, ...sortedUsStates ] : sortedUsStates; const stateDropdownProps = { controlled: true, hasSearch: true, i18n, label: i18n.stateLabel || NEEDS_I18N_LABEL, listItems, onChange: handleDropdownChange( 'state' ), popoverMaxHeight: 300, size: 'r', value: addressData.state, ...stateDropdownAttributes, className: classnames( { 'gform-address__state-province-dropdown': true, ...stateDropdownClasses, } ), }; return ( <Dropdown { ...stateDropdownProps } /> ); } if ( addressData.country === 'CA' && sortedCaProvinces.length > 0 ) { const listItems = useSelectPlaceholders ? [ { label: i18n.provinceSelectPlaceholder || NEEDS_I18N_LABEL, value: '', }, ...sortedCaProvinces ] : sortedCaProvinces; const stateDropdownProps = { controlled: true, hasSearch: true, i18n, label: i18n.provinceLabel || NEEDS_I18N_LABEL, listItems, onChange: handleDropdownChange( 'state' ), popoverMaxHeight: 300, size: 'r', value: addressData.state, ...stateDropdownAttributes, className: classnames( { 'gform-address__state-province-dropdown': true, ...stateDropdownClasses, } ), }; return ( <Dropdown { ...stateDropdownProps } /> ); } return renderInputField( 'state' ); }; const attributes = { className: classnames( { 'gform-address': true, ...spacerClasses( spacing ), }, customClasses ), ref, ...customAttributes, }; return ( <div { ...attributes }> { renderInputField( 'lineOne' ) } { renderInputField( 'lineTwo' ) } <Box display="flex" customClasses={ [ 'gform-address__row' ] }> { renderInputField( 'city' ) } { renderStateProvinceField() } </Box> <Box display="flex" customClasses={ [ 'gform-address__row' ] }> { renderInputField( 'postalCode' ) } { renderCountryField() } </Box> { children } </div> ); } ); Address.displayName = 'Address'; export default Address;