Skip to content
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

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/FormElement/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
-- Changed the spacing between label, content and instructions

[@tristanjasper](https://github.com/tristanjasper).

## [0.3.29] - 2020-08-20
Copy link
Contributor

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 🙏 :)


### Changed

-- Simplified formElement api for consumers

[@tristanjasper](https://github.com/tristanjasper).
48 changes: 19 additions & 29 deletions packages/FormElement/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this a breaking change or our FormElements still support the renderFunction?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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>;
```
Expand All @@ -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>;
```
Expand All @@ -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>;
```
Expand Down
113 changes: 111 additions & 2 deletions packages/FormElement/src/components/Content/Content.js
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 = {
Expand All @@ -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()}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a console.error or throw new Error if typeof children === "function"

</sc.ContentContainer>
);
}
Expand Down
16 changes: 6 additions & 10 deletions packages/FormElement/stories/examples/AccessibilityExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,12 @@ const ExampleStory = () => {
</span>
</FormElement.Instructions>
<FormElement.Content>
{({ idForLabel, ariaDescribedBy }) => (
<Input
id={idForLabel}
onChange={handleChange}
value={value}
placeholder="Form placeholder"
aria-describedby={ariaDescribedBy}
hasError={Boolean(errorText.length)}
/>
)}
<Input
onChange={handleChange}
value={value}
placeholder="Form placeholder"
hasError={Boolean(errorText.length)}
/>
</FormElement.Content>
<FormElement.Description>
<span>This is description text</span>
Expand Down
Loading