-
Notifications
You must be signed in to change notification settings - Fork 9
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
Ux-157 FormElement api simplification #663
Changes from 7 commits
1d5a359
8539d9a
e05071f
de5ee5c
a28ab6f
659f47e
0e1933b
9a33dfa
3bfbab4
79569f4
22ba49b
c4b52d9
6de6997
934aa9e
d3562fe
1451e3f
3ac7b93
350731c
7a5c581
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 |
---|---|---|
|
@@ -94,20 +94,16 @@ import Input from "@paprika/input"; | |
|
||
<FormElement label="Name"> | ||
<FormElement.Content> | ||
{({ idForLabel, ariaDescribedBy }) => ( | ||
<Input | ||
id={idForLabel} | ||
onChange={handleChange} | ||
value={value} | ||
placeholder="Form placeholder" | ||
aria-describedby={ariaDescribedBy} | ||
aria-required={hasRequiredLabel} | ||
hasError={false} | ||
isDisabled={isDisabled} | ||
isReadOnly={isReadOnly} | ||
size={size} | ||
/> | ||
)} | ||
<Input | ||
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. is this a breaking change or our FormElements still support the renderFunction? 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 will be a breaking change. I don't think we should encourage using the renderFunction as an acceptable api moving forward |
||
onChange={handleChange} | ||
value={value} | ||
placeholder="Form placeholder" | ||
aria-required={hasRequiredLabel} | ||
hasError={false} | ||
isDisabled={isDisabled} | ||
isReadOnly={isReadOnly} | ||
size={size} | ||
/> | ||
</FormElement.Content> | ||
</FormElement>; | ||
``` | ||
|
@@ -120,11 +116,9 @@ import FormElement from "@paprika/form-element"; | |
|
||
<FormElement label="Name"> | ||
<FormElement.Content> | ||
{({ idForLabel, ariaDescribedBy }) => ( | ||
<DatePicker onError={() => {}} hasError={Boolean(errorText.length)} id={idForLabel} onChange={() => {}}> | ||
<DatePicker.Input aria-describedby={ariaDescribedBy} /> | ||
</DatePicker> | ||
)} | ||
<DatePicker onError={() => {}} hasError={Boolean(errorText.length)} onChange={() => {}}> | ||
<DatePicker.Input /> | ||
</DatePicker> | ||
</FormElement.Content> | ||
</FormElement>; | ||
``` | ||
|
@@ -136,16 +130,12 @@ import FormElement from "@paprika/form-element"; | |
|
||
<FormElement hasRequiredLabel={hasRequiredLabel} label="Name"> | ||
<FormElement.Content> | ||
{({ idForLabel, ariaDescribedBy }) => ( | ||
<input | ||
aria-required={hasRequiredLabel} | ||
aria-describedby={ariaDescribedBy} | ||
aria-invalid={Boolean(errorText.length)} | ||
disabled={isDisabled} | ||
id={idForLabel} | ||
readOnly={isReadOnly} | ||
/> | ||
)} | ||
<input | ||
aria-required={hasRequiredLabel} | ||
aria-invalid={Boolean(errorText.length)} | ||
disabled={isDisabled} | ||
readOnly={isReadOnly} | ||
/> | ||
</FormElement.Content> | ||
</FormElement>; | ||
``` | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
import React from "react"; | ||
import PropTypes from "prop-types"; | ||
import { RefOf } from "@paprika/helpers/lib/customPropTypes"; | ||
import extractChildren from "@paprika/helpers/lib/extractChildren"; | ||
import * as sc from "./Content.styles"; | ||
|
||
const propTypes = { | ||
|
@@ -10,24 +11,132 @@ const propTypes = { | |
refLabel: RefOf(), | ||
/** Used for aria-describedby on the FormElement */ | ||
ariaDescribedBy: PropTypes.string, | ||
/** Used when form Elements are nested and is automatically appended to aria-describedby */ | ||
wrapperAriaDescribedBy: PropTypes.string, | ||
}; | ||
|
||
const defaultProps = { | ||
idForLabel: null, | ||
ariaDescribedBy: null, | ||
refLabel: null, | ||
wrapperAriaDescribedBy: "", | ||
}; | ||
|
||
const supportedComponentNames = { | ||
Input: "Input", | ||
Checkbox: "Checkbox", | ||
RadioGroup: "Radio.Group", | ||
DatePicker: "DatePicker", | ||
ListBox: "ListBox", | ||
FormElement: "FormElement", | ||
TimePicker: "TimePicker", | ||
}; | ||
|
||
function Content(props) { | ||
const { children, idForLabel, refLabel, ariaDescribedBy, ...moreProps } = props; | ||
let isLabelRefSet = false; | ||
const { children, idForLabel, refLabel, wrapperAriaDescribedBy, ...moreProps } = props; | ||
const ariaDescribedBy = `${props.ariaDescribedBy} ${wrapperAriaDescribedBy}`; | ||
|
||
const extractedChildren = extractChildren(children, Object.values(supportedComponentNames)); | ||
|
||
const getIdProp = () => { | ||
// ensure the id for lable is only set on one element | ||
const idProp = !isLabelRefSet ? { id: idForLabel } : null; | ||
|
||
if (idProp) { | ||
isLabelRefSet = true; | ||
} | ||
return idProp; | ||
}; | ||
|
||
const cloneWithAriaDescribedByAndLabelId = input => | ||
React.cloneElement(input, { | ||
"aria-describedby": ariaDescribedBy, | ||
...getIdProp(), | ||
}); | ||
|
||
const cloneWithAriaDescribedBy = item => | ||
React.cloneElement(item, { | ||
"aria-describedby": ariaDescribedBy, | ||
}); | ||
|
||
const getAccessibleElement = (elements, cloneFn) => | ||
Array.isArray(elements) ? elements.map(cloneFn) : cloneFn(elements); | ||
|
||
const renderInput = extractedInputs => getAccessibleElement(extractedInputs, cloneWithAriaDescribedByAndLabelId); | ||
|
||
const renderCheckboxes = extractedCheckboxes => getAccessibleElement(extractedCheckboxes, cloneWithAriaDescribedBy); | ||
|
||
const renderRadioGroup = extractedRadioGroup => { | ||
const radioElements = extractedRadioGroup.props.children.map(cloneWithAriaDescribedBy); | ||
return React.cloneElement(extractedRadioGroup, { | ||
children: radioElements, | ||
}); | ||
}; | ||
|
||
const renderTimePicker = extractedTimePicker => cloneWithAriaDescribedByAndLabelId(extractedTimePicker); | ||
|
||
const renderDatePicker = extractedDatePicker => { | ||
const dataPickerInput = cloneWithAriaDescribedByAndLabelId(extractedDatePicker.props.children); | ||
return React.cloneElement(extractedDatePicker, { | ||
children: dataPickerInput, | ||
}); | ||
}; | ||
|
||
const renderOtherChildren = children => { | ||
const elementClone = element => { | ||
if (element.type === "input") { | ||
return cloneWithAriaDescribedByAndLabelId(element); | ||
} | ||
return element; | ||
}; | ||
return children.map(elementClone); | ||
}; | ||
|
||
const renderListBox = extractedListBox => | ||
React.cloneElement(extractedListBox, { | ||
refLabel, | ||
}); | ||
|
||
const renderFormElement = extractedFormElement => { | ||
const formElementChildren = extractedFormElement.map(formElement => | ||
formElement.props.children.map(item => { | ||
if (item.type.displayName === "FormElement.Content") { | ||
return React.cloneElement(item, { wrapperAriaDescribedBy: ariaDescribedBy }); | ||
} | ||
return item; | ||
}) | ||
); | ||
|
||
return extractedFormElement.map((formElement, index) => | ||
React.cloneElement(formElement, { children: formElementChildren[index] }) | ||
); | ||
}; | ||
|
||
const renderFunctionMap = { | ||
Input: renderInput, | ||
Checkbox: renderCheckboxes, | ||
"Radio.Group": renderRadioGroup, | ||
DatePicker: renderDatePicker, | ||
ListBox: renderListBox, | ||
FormElement: renderFormElement, | ||
TimePicker: renderTimePicker, | ||
children: renderOtherChildren, | ||
}; | ||
|
||
function renderExtractedChildren() { | ||
for (const [key, value] of Object.entries(extractedChildren)) { | ||
return renderFunctionMap[key](value); | ||
} | ||
} | ||
|
||
if (!children) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<sc.ContentContainer data-pka-anchor="form-element.content" {...moreProps}> | ||
{typeof children === "function" ? children({ idForLabel, refLabel, ariaDescribedBy }) : children} | ||
{renderExtractedChildren()} | ||
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. add a console.error or throw new Error if |
||
</sc.ContentContainer> | ||
); | ||
} | ||
|
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.
this you should follow the new format that @allison-c introduced, write this on the [unreleased] section read in our slack channel in paprika the announcement about it 🙏 :)