-
Notifications
You must be signed in to change notification settings - Fork 21
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
Product Block Editor: Map SelectWithTextInput
and DateTime
inputs to the custom product blocks
#2178
Product Block Editor: Map SelectWithTextInput
and DateTime
inputs to the custom product blocks
#2178
Changes from all commits
f3dee6e
57e1128
1fe2fe5
e05bbb3
1015760
abd3492
0965ef9
b94f33a
776ffb8
d8252b8
b916bd3
0fb9cf0
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,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" | ||
] | ||
} |
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; | ||
} | ||
} | ||
Comment on lines
+24
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need validation for the datetime? I wasn't able to set an invalid date time in the inputs. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't test all browsers, but Chrome and FireFox both allow entering date by keyboard, which may result in an invalid value. Kapture.2024-01-03.at.11.23.23.mp4"請輸入有效的日期" and "請輸入有效的時間" mean "Please enter a valid date" and "Please enter a valid time". |
||
|
||
/** | ||
* 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> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
.invisibleLabelAndTooltip { | ||
label { | ||
visibility: hidden; | ||
} | ||
} |
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" | ||
] | ||
} |
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 } ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💅 Can we add a bit of documentation of what There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added JSDoc for all custom blocks by d8252b8. |
||
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 ); | ||
} | ||
Comment on lines
+69
to
+73
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💅 This can be reduced to:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I lead toward keeping it as it is. |
||
}; | ||
|
||
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> | ||
); | ||
} |
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.
❓ Can we elaborate or reword this explanation? Maybe my lack of knowledge of the concepts but not sure if I understand what those "derived values" are. I assume is just the data itself?
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.
In the React ecosystem, the derived value/state is commonly referred to the value calculated based on another value. I'm guessing that it probably comes from the wording in the React API
getDerivedStateFromProps
.Added in 0965ef9.