-
Notifications
You must be signed in to change notification settings - Fork 851
Forms: Add 'Other' option support to radio fields #46461
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Changes from all commits
6d0ea28
08a81c2
355cb8e
2dd23ec
70466fa
9994fb0
f0b6b2d
a133718
0c959bd
088a593
f832086
27ef08f
9b7419b
d766a98
819502d
fc70ea4
1b37d79
335e1e9
0b36ec9
b7f94a4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| Significance: minor | ||
| Type: added | ||
|
|
||
| Add 'Other' option support for radio fields with custom text input, including ARIA accessibility, proper validation, and metadata storage for form submissions | ||
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1225,3 +1225,27 @@ | |
| } | ||
| } | ||
| } | ||
|
|
||
| /* "Other" option text input styles */ | ||
| .jetpack-other-text-input-wrapper { | ||
| margin-left: 1.5em; | ||
|
||
| } | ||
|
|
||
| .jetpack-other-text-input { | ||
| width: 100%; | ||
| max-width: 400px; | ||
| padding: 0.5em; | ||
| border: var(--jetpack--contact-form--border, 1px solid #8c8f94); | ||
| border-radius: var(--jetpack--contact-form--border-radius, 0); | ||
| font-size: var(--jetpack--contact-form--font-size, 16px); | ||
|
|
||
| &:focus { | ||
| outline: 2px solid; | ||
| outline-offset: 2px; | ||
| } | ||
|
|
||
| &:disabled { | ||
| opacity: 0.5; | ||
| cursor: not-allowed; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,14 @@ | ||||||||||
| import { RichText, store as blockEditorStore, useBlockProps } from '@wordpress/block-editor'; | ||||||||||
| import { | ||||||||||
| InspectorControls, | ||||||||||
| RichText, | ||||||||||
| store as blockEditorStore, | ||||||||||
| useBlockProps, | ||||||||||
| } from '@wordpress/block-editor'; | ||||||||||
| import { ToggleControl, PanelBody, VisuallyHidden } from '@wordpress/components'; | ||||||||||
| import { useDispatch, useSelect } from '@wordpress/data'; | ||||||||||
| import { useState } from '@wordpress/element'; | ||||||||||
| import { __ } from '@wordpress/i18n'; | ||||||||||
| import clsx from 'clsx'; | ||||||||||
| import { useSyncedAttributes } from '../shared/hooks/use-synced-attributes.js'; | ||||||||||
| import { ALLOWED_FORMATS } from '../shared/util/constants.js'; | ||||||||||
| import useEnter from './use-enter.js'; | ||||||||||
|
|
@@ -17,14 +25,23 @@ const getLabelOrFallback = ( label, placeholder ) => { | |||||||||
| return label ?? placeholder; | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| const OptionEdit = ( { attributes, clientId, context, name, setAttributes, mergeBlocks } ) => { | ||||||||||
| const OptionEdit = ( { | ||||||||||
| attributes, | ||||||||||
| clientId, | ||||||||||
| context, | ||||||||||
| isSelected, | ||||||||||
| mergeBlocks, | ||||||||||
| name, | ||||||||||
| setAttributes, | ||||||||||
| } ) => { | ||||||||||
| const { | ||||||||||
| 'jetpack/field-default-value': defaultValue, | ||||||||||
| 'jetpack/field-options-type': type = 'checkbox', | ||||||||||
| 'jetpack/field-required': required, | ||||||||||
| 'jetpack/field-share-attributes': isSynced, | ||||||||||
| } = context; | ||||||||||
| const { hideInput, label, isStandalone, requiredText, placeholder } = attributes; | ||||||||||
| const { hideInput, label, isStandalone, requiredText, placeholder, isOther, otherPlaceholder } = | ||||||||||
| attributes; | ||||||||||
|
|
||||||||||
| useSyncedAttributes( name, isSynced, SYNCED_ATTRIBUTE_KEYS, attributes, setAttributes ); | ||||||||||
|
|
||||||||||
|
|
@@ -37,6 +54,19 @@ const OptionEdit = ( { attributes, clientId, context, name, setAttributes, merge | |||||||||
| [ clientId ] | ||||||||||
| ); | ||||||||||
|
|
||||||||||
| const isParentSelected = useSelect( | ||||||||||
| select => { | ||||||||||
| const { getBlockRootClientId, getSelectedBlockClientId } = select( blockEditorStore ); | ||||||||||
| const parentClientId = getBlockRootClientId( clientId ); | ||||||||||
| if ( ! parentClientId ) { | ||||||||||
| return false; | ||||||||||
| } | ||||||||||
| const selectedBlockClientId = getSelectedBlockClientId(); | ||||||||||
| return selectedBlockClientId === parentClientId; | ||||||||||
| }, | ||||||||||
| [ clientId ] | ||||||||||
| ); | ||||||||||
|
|
||||||||||
| const onRemove = () => { | ||||||||||
| if ( siblingsCount <= 1 ) { | ||||||||||
| return; | ||||||||||
|
|
@@ -45,6 +75,8 @@ const OptionEdit = ( { attributes, clientId, context, name, setAttributes, merge | |||||||||
| removeBlock( clientId ); | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| const [ isFocusedOtherPlaceholder, setIsFocusedOtherPlaceholder ] = useState( false ); | ||||||||||
|
|
||||||||||
| const blockProps = useBlockProps( { | ||||||||||
| className: `jetpack-field-option field-option-${ type }`, | ||||||||||
| } ); | ||||||||||
|
|
@@ -55,7 +87,11 @@ const OptionEdit = ( { attributes, clientId, context, name, setAttributes, merge | |||||||||
| const isPreviewMode = useSelect( select => { | ||||||||||
| return select( blockEditorStore ).getSettings().isPreviewMode; | ||||||||||
| }, [] ); | ||||||||||
| const placeholderValue = placeholder !== '' ? placeholder : __( 'Add option…', 'jetpack-forms' ); | ||||||||||
| const emptyPlaceholder = isOther | ||||||||||
| ? __( 'Other', 'jetpack-forms' ) | ||||||||||
| : __( 'Add option…', 'jetpack-forms' ); | ||||||||||
| const placeholderValue = placeholder !== '' ? placeholder : emptyPlaceholder; | ||||||||||
|
|
||||||||||
| // The label value to use for the RichText field must manually fall back to the | ||||||||||
| // placeholder to be rendered in previews. | ||||||||||
| const labelValue = isPreviewMode ? getLabelOrFallback( label, placeholderValue ) : label; | ||||||||||
|
|
@@ -65,59 +101,102 @@ const OptionEdit = ( { attributes, clientId, context, name, setAttributes, merge | |||||||||
| // to allow for custom required text. | ||||||||||
| if ( isStandalone ) { | ||||||||||
| return ( | ||||||||||
| <div { ...blockProps }> | ||||||||||
| { ! hideInput && ( | ||||||||||
| <input | ||||||||||
| className="jetpack-field-option__checkbox" | ||||||||||
| checked={ !! defaultValue } | ||||||||||
| onChange={ noop } | ||||||||||
| type={ type } | ||||||||||
| /> | ||||||||||
| ) } | ||||||||||
| <div className="jetpack-field-option__label-wrapper"> | ||||||||||
| <RichText | ||||||||||
| ref={ useEnterRef } | ||||||||||
| identifier="label" | ||||||||||
| tagName="div" | ||||||||||
| className="wp-block" | ||||||||||
| value={ labelValue } | ||||||||||
| placeholder={ placeholderValue } | ||||||||||
| __unstableDisableFormats | ||||||||||
| onChange={ newLabel => setAttributes( { label: newLabel } ) } | ||||||||||
| onRemove={ onRemove } | ||||||||||
| /> | ||||||||||
| { required && ( | ||||||||||
| <RichText | ||||||||||
| ref={ useEnterRequiredRef } | ||||||||||
| allowedFormats={ ALLOWED_FORMATS } | ||||||||||
| className="required" | ||||||||||
| onChange={ value => setAttributes( { requiredText: value } ) } | ||||||||||
| tagName="span" | ||||||||||
| value={ requiredText || __( '(required)', 'jetpack-forms' ) } | ||||||||||
| withoutInteractiveFormatting | ||||||||||
| <> | ||||||||||
| <div { ...blockProps }> | ||||||||||
| { ! hideInput && ( | ||||||||||
| <input | ||||||||||
| className="jetpack-field-option__checkbox" | ||||||||||
| checked={ !! defaultValue } | ||||||||||
| onChange={ noop } | ||||||||||
| type={ type } | ||||||||||
| /> | ||||||||||
| ) } | ||||||||||
|
|
||||||||||
| <div className={ clsx( 'jetpack-field-option__label-wrapper', { 'is-other': isOther } ) }> | ||||||||||
| <RichText | ||||||||||
| ref={ useEnterRef } | ||||||||||
| identifier="label" | ||||||||||
| tagName="div" | ||||||||||
| className="wp-block" | ||||||||||
| value={ labelValue } | ||||||||||
| placeholder={ placeholderValue } | ||||||||||
| __unstableDisableFormats | ||||||||||
| onChange={ newLabel => setAttributes( { label: newLabel } ) } | ||||||||||
| onRemove={ onRemove } | ||||||||||
| /> | ||||||||||
|
|
||||||||||
| { required && ( | ||||||||||
| <RichText | ||||||||||
| ref={ useEnterRequiredRef } | ||||||||||
| allowedFormats={ ALLOWED_FORMATS } | ||||||||||
| className="required" | ||||||||||
| onChange={ value => setAttributes( { requiredText: value } ) } | ||||||||||
| tagName="span" | ||||||||||
| value={ requiredText || __( '(required)', 'jetpack-forms' ) } | ||||||||||
| withoutInteractiveFormatting | ||||||||||
| /> | ||||||||||
| ) } | ||||||||||
| </div> | ||||||||||
| </div> | ||||||||||
| </div> | ||||||||||
| </> | ||||||||||
| ); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| return ( | ||||||||||
| <li { ...blockProps }> | ||||||||||
| <input type={ type } className="jetpack-option__type" tabIndex="-1" /> | ||||||||||
| <RichText | ||||||||||
| ref={ useEnterRef } | ||||||||||
| identifier="label" | ||||||||||
| tagName="div" | ||||||||||
| className="wp-block" | ||||||||||
| onMerge={ mergeBlocks } | ||||||||||
| value={ labelValue } | ||||||||||
| placeholder={ __( 'Add option…', 'jetpack-forms' ) } | ||||||||||
| __unstableDisableFormats | ||||||||||
| onChange={ newLabel => setAttributes( { label: newLabel } ) } | ||||||||||
| onRemove={ onRemove } | ||||||||||
| /> | ||||||||||
| </li> | ||||||||||
| <> | ||||||||||
| <InspectorControls> | ||||||||||
| <PanelBody | ||||||||||
| title={ __( 'Settings', 'jetpack-forms' ) } | ||||||||||
| className="jetpack-contact-form__panel" | ||||||||||
| > | ||||||||||
| <ToggleControl | ||||||||||
| key="allowOther" | ||||||||||
| label={ __( '"Other" option', 'jetpack-forms' ) } | ||||||||||
| checked={ !! isOther } | ||||||||||
| onChange={ toggleValue => { | ||||||||||
| setAttributes( { isOther: toggleValue } ); | ||||||||||
| } } | ||||||||||
| help={ __( | ||||||||||
| 'Show as "Other" option with a text input field below it.', | ||||||||||
| 'jetpack-forms' | ||||||||||
| ) } | ||||||||||
| __nextHasNoMarginBottom={ true } | ||||||||||
| /> | ||||||||||
| </PanelBody> | ||||||||||
| </InspectorControls> | ||||||||||
| <li { ...blockProps }> | ||||||||||
| <input type={ type } className="jetpack-option__type" tabIndex="-1" /> | ||||||||||
| <RichText | ||||||||||
| ref={ useEnterRef } | ||||||||||
| identifier="label" | ||||||||||
| tagName="div" | ||||||||||
| className="wp-block" | ||||||||||
| value={ labelValue } | ||||||||||
| placeholder={ __( 'Add option…', 'jetpack-forms' ) } | ||||||||||
| __unstableDisableFormats | ||||||||||
| onChange={ newLabel => setAttributes( { label: newLabel } ) } | ||||||||||
| onMerge={ mergeBlocks } | ||||||||||
| onRemove={ onRemove } | ||||||||||
| /> | ||||||||||
| </li> | ||||||||||
| { isOther && ( isSelected || isParentSelected ) && ( | ||||||||||
| <li className="jetpack-other-text-input-wrapper is-visible"> | ||||||||||
| <VisuallyHidden as="label" htmlFor={ `${ clientId }-other-text` }> | ||||||||||
| { otherPlaceholder || __( 'Please specify…', 'jetpack-forms' ) } | ||||||||||
| </VisuallyHidden> | ||||||||||
| <input | ||||||||||
| id={ `${ clientId }-other-text` } | ||||||||||
| className="grunion-field jetpack-field__input" | ||||||||||
| onChange={ event => setAttributes( { otherPlaceholder: event.target.value } ) } | ||||||||||
| onFocus={ () => setIsFocusedOtherPlaceholder( true ) } | ||||||||||
| onBlur={ () => setIsFocusedOtherPlaceholder( false ) } | ||||||||||
| type="text" | ||||||||||
| value={ isFocusedOtherPlaceholder ? otherPlaceholder : '' } | ||||||||||
| placeholder={ otherPlaceholder } | ||||||||||
|
Comment on lines
+194
to
+195
|
||||||||||
| value={ isFocusedOtherPlaceholder ? otherPlaceholder : '' } | |
| placeholder={ otherPlaceholder } | |
| value={ otherPlaceholder } | |
| placeholder={ otherPlaceholder || __( 'Please specify…', 'jetpack-forms' ) } |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -62,6 +62,14 @@ const settings = { | |||||||||
| type: 'boolean', | ||||||||||
| default: false, | ||||||||||
| }, | ||||||||||
| isOther: { | ||||||||||
| type: 'boolean', | ||||||||||
| default: false, | ||||||||||
| }, | ||||||||||
| otherPlaceholder: { | ||||||||||
| type: 'string', | ||||||||||
| default: __( 'Please specify…', 'jetpack-forms' ), | ||||||||||
| }, | ||||||||||
|
Comment on lines
+69
to
+72
|
||||||||||
| otherPlaceholder: { | |
| type: 'string', | |
| default: __( 'Please specify…', 'jetpack-forms' ), | |
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changelog filename "add-other-option-to-dropdown" mentions "dropdown" but the feature is actually for radio fields, not dropdown/select fields. The filename should be "add-other-option-to-radio-fields" or similar to accurately reflect the feature scope.