-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2178 from woocommerce/add/product-block-editor-cu…
…stom-select-with-text-and-date-time Product Block Editor: Map `SelectWithTextInput` and `DateTime` inputs to the custom product blocks
- Loading branch information
Showing
17 changed files
with
474 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
{ | ||
"$schema": "https://schemas.wp.org/trunk/block.json", | ||
"apiVersion": 2, | ||
"name": "google-listings-and-ads/product-date-time-field", | ||
"title": "Product date and time fields", | ||
"textdomain": "google-listings-and-ads", | ||
"attributes": { | ||
"_templateBlockHideConditions": { | ||
"type": "array" | ||
}, | ||
"label": { | ||
"type": "string" | ||
}, | ||
"tooltip": { | ||
"type": "string" | ||
}, | ||
"property": { | ||
"type": "string", | ||
"__experimentalRole": "content" | ||
} | ||
}, | ||
"supports": { | ||
"html": false, | ||
"inserter": false, | ||
"lock": false | ||
}, | ||
"usesContext": [ | ||
"postType" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { date as formatDate, getDate } from '@wordpress/date'; | ||
import { useWooBlockProps } from '@woocommerce/block-templates'; | ||
import { useState, useRef } from '@wordpress/element'; | ||
import { | ||
__experimentalUseProductEntityProp as useProductEntityProp, | ||
__experimentalTextControl as TextControl, | ||
useValidation, | ||
} from '@woocommerce/product-editor'; | ||
import { Flex, FlexBlock } from '@wordpress/components'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import styles from './editor.module.scss'; | ||
|
||
/** | ||
* @typedef {import('../types.js').ProductEditorBlockContext} ProductEditorBlockContext | ||
* @typedef {import('../types.js').ProductBasicAttributes} ProductBasicAttributes | ||
*/ | ||
|
||
async function resolveValidationMessage( inputRef ) { | ||
const input = inputRef.current; | ||
|
||
if ( ! input.validity.valid ) { | ||
return input.validationMessage; | ||
} | ||
} | ||
|
||
/** | ||
* Custom block for editing a given product data with date and time fields. | ||
* | ||
* @param {Object} props React props. | ||
* @param {ProductBasicAttributes} props.attributes | ||
* @param {ProductEditorBlockContext} props.context | ||
*/ | ||
export default function Edit( { attributes, context } ) { | ||
const { property } = attributes; | ||
const blockProps = useWooBlockProps( attributes ); | ||
const [ value, setValue ] = useProductEntityProp( property, { | ||
postType: context.postType, | ||
fallbackValue: '', | ||
} ); | ||
const dateInputRef = useRef( null ); | ||
const timeInputRef = useRef( null ); | ||
|
||
// Refer to the "Derived value for initialization" in README for why these `useState` | ||
// can initialize directly. | ||
const [ date, setDate ] = useState( () => | ||
value ? formatDate( 'Y-m-d', value ) : '' | ||
); | ||
const [ time, setTime ] = useState( () => | ||
value ? formatDate( 'H:i', value ) : '' | ||
); | ||
|
||
const setNextValue = ( nextDate, nextTime ) => { | ||
let nextValue = ''; | ||
|
||
// It allows `nextTime` to be an empty string and fall back to '00:00:00'. | ||
if ( nextDate ) { | ||
// The date and time values are strings presented in the site timezone. | ||
// Normalize them to date string in UTC timezone in ISO 8601 format. | ||
const isoDateString = `${ nextDate }T${ nextTime || '00:00:00' }`; | ||
const dateInSiteTimezone = getDate( isoDateString ); | ||
nextValue = formatDate( 'c', dateInSiteTimezone, 'UTC' ); | ||
} | ||
|
||
setDate( nextDate ); | ||
setTime( nextTime ); | ||
|
||
if ( value !== nextValue ) { | ||
setValue( nextValue ); | ||
} | ||
}; | ||
|
||
const dateValidation = useValidation( `${ property }-date`, () => | ||
resolveValidationMessage( dateInputRef ) | ||
); | ||
|
||
const timeValidation = useValidation( `${ property }-time`, () => | ||
resolveValidationMessage( timeInputRef ) | ||
); | ||
|
||
return ( | ||
<div { ...blockProps }> | ||
<Flex align="flex-start"> | ||
<FlexBlock> | ||
<TextControl | ||
ref={ dateInputRef } | ||
type="date" | ||
pattern="\d{4}-\d{2}-\d{2}" | ||
label={ attributes.label } | ||
tooltip={ attributes.tooltip } | ||
value={ date } | ||
error={ dateValidation.error } | ||
onChange={ ( nextDate ) => | ||
setNextValue( nextDate, time ) | ||
} | ||
onBlur={ dateValidation.validate } | ||
/> | ||
</FlexBlock> | ||
<FlexBlock> | ||
<TextControl | ||
// The invisible chars in the label and tooltip are to maintain the space between | ||
// the <label> and <input> as the same in the sibling <TextControl>. Also, it uses | ||
// CSS to keep its space but is invisible. | ||
className={ styles.invisibleLabelAndTooltip } | ||
label=" " | ||
tooltip=" " | ||
ref={ timeInputRef } | ||
type="time" | ||
pattern="[0-9]{2}:[0-9]{2}" | ||
value={ time } | ||
error={ timeValidation.error } | ||
onChange={ ( nextTime ) => | ||
setNextValue( date, nextTime ) | ||
} | ||
onBlur={ timeValidation.validate } | ||
/> | ||
</FlexBlock> | ||
</Flex> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
.invisibleLabelAndTooltip { | ||
label { | ||
visibility: hidden; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
{ | ||
"$schema": "https://schemas.wp.org/trunk/block.json", | ||
"apiVersion": 2, | ||
"name": "google-listings-and-ads/product-select-with-text-field", | ||
"title": "Product select with text field", | ||
"textdomain": "google-listings-and-ads", | ||
"attributes": { | ||
"_templateBlockHideConditions": { | ||
"type": "array" | ||
}, | ||
"label": { | ||
"type": "string" | ||
}, | ||
"tooltip": { | ||
"type": "string" | ||
}, | ||
"property": { | ||
"type": "string", | ||
"__experimentalRole": "content" | ||
}, | ||
"options": { | ||
"type": "array", | ||
"items": { | ||
"type": "object" | ||
}, | ||
"default": [] | ||
}, | ||
"customInputValue": { | ||
"type": "string" | ||
} | ||
}, | ||
"supports": { | ||
"html": false, | ||
"inserter": false, | ||
"lock": false | ||
}, | ||
"usesContext": [ | ||
"postType" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { useWooBlockProps } from '@woocommerce/block-templates'; | ||
import { SelectControl } from '@wordpress/components'; | ||
import { useState } from '@wordpress/element'; | ||
import { | ||
__experimentalUseProductEntityProp as useProductEntityProp, | ||
__experimentalTextControl as TextControl, | ||
} from '@woocommerce/product-editor'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { Label } from '../components'; | ||
|
||
/** | ||
* @typedef {import('../types.js').ProductEditorBlockContext} ProductEditorBlockContext | ||
* @typedef {import('../types.js').ProductBasicAttributes} ProductBasicAttributes | ||
*/ | ||
|
||
/** | ||
* @typedef {Object} SpecificAttributes | ||
* @property {string} customInputValue The value to be used in the selection option that shows the text field. | ||
* @property {import('@wordpress/components').SelectControl.Option} [options=[]] The options to be shown in the select field. | ||
* | ||
* @typedef {ProductBasicAttributes & SpecificAttributes} ProductSelectWithTextFieldAttributes | ||
*/ | ||
|
||
const FALLBACK_VALUE = ''; | ||
|
||
/** | ||
* Custom block for editing a given product data with a select field or a text field. | ||
* The text field is used to enter a custom value and is shown when selecting the option | ||
* that has the same value as the given `customInputValue`. | ||
* | ||
* @param {Object} props React props. | ||
* @param {ProductSelectWithTextFieldAttributes} props.attributes | ||
* @param {ProductEditorBlockContext} props.context | ||
*/ | ||
export default function Edit( { attributes, context } ) { | ||
const { options, customInputValue } = attributes; | ||
|
||
const blockProps = useWooBlockProps( attributes ); | ||
const [ value, setValue ] = useProductEntityProp( attributes.property, { | ||
postType: context.postType, | ||
fallbackValue: FALLBACK_VALUE, | ||
} ); | ||
|
||
// Refer to the "Derived value for initialization" in README for why this `useState` | ||
// can initialize directly. | ||
const [ optionValue, setOptionValue ] = useState( () => { | ||
// Empty string is a valid option value. | ||
const initValue = value ?? FALLBACK_VALUE; | ||
const selectedOption = options.find( | ||
( option ) => option.value === initValue | ||
); | ||
|
||
return selectedOption?.value ?? customInputValue; | ||
} ); | ||
|
||
const isSelectedCustomInput = optionValue === customInputValue; | ||
|
||
const [ text, setText ] = useState( isSelectedCustomInput ? value : '' ); | ||
|
||
const handleSelectionChange = ( nextOptionValue ) => { | ||
setOptionValue( nextOptionValue ); | ||
|
||
if ( nextOptionValue === customInputValue ) { | ||
setValue( text ); | ||
} else { | ||
setValue( nextOptionValue ); | ||
} | ||
}; | ||
|
||
const handleTextChange = ( nextText ) => { | ||
setText( nextText ); | ||
setValue( nextText ); | ||
}; | ||
|
||
return ( | ||
<div { ...blockProps }> | ||
<SelectControl | ||
label={ | ||
<Label | ||
label={ attributes.label } | ||
tooltip={ attributes.tooltip } | ||
/> | ||
} | ||
options={ options } | ||
value={ optionValue } | ||
onChange={ handleSelectionChange } | ||
/> | ||
{ isSelectedCustomInput && ( | ||
<TextControl | ||
type="text" | ||
value={ text } | ||
onChange={ handleTextChange } | ||
/> | ||
) } | ||
</div> | ||
); | ||
} |
Oops, something went wrong.