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 5 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
147 changes: 145 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,166 @@ 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 labelRefSet = 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 = !labelRefSet ? { id: idForLabel } : null;

if (idProp) {
labelRefSet = 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) => {
return Array.isArray(elements) ? elements.map(cloneFn) : cloneFn(elements);
};

function renderInput() {
const extractedInputs = extractedChildren[supportedComponentNames.Input];
if (extractedInputs) {
return getAccessibleElement(extractedInputs, cloneWithAriaDescribedByAndLabelId);
}

return null;
}

function renderCheckboxes() {
const extractedCheckboxes = extractedChildren[supportedComponentNames.Checkbox];
if (extractedCheckboxes) {
return getAccessibleElement(extractedCheckboxes, cloneWithAriaDescribedBy);
}

return null;
}

function renderRadioGroup() {
const extractedRadioGroup = extractedChildren[supportedComponentNames.RadioGroup];
if (extractedRadioGroup) {
const radioElements = extractedRadioGroup.props.children.map(cloneWithAriaDescribedBy);
return React.cloneElement(extractedRadioGroup, {
children: radioElements,
});
}

return null;
}

function renderTimePicker() {
const extractedTimePicker = extractedChildren[supportedComponentNames.TimePicker];
if (extractedTimePicker) {
return cloneWithAriaDescribedByAndLabelId(extractedTimePicker);
}

return null;
}

function renderDatePicker() {
const extractedDatePicker = extractedChildren[supportedComponentNames.DatePicker];
if (extractedDatePicker) {
const dataPickerInput = cloneWithAriaDescribedByAndLabelId(extractedDatePicker.props.children);
return React.cloneElement(extractedDatePicker, {
children: dataPickerInput,
});
}

return null;
}

function renderOtherChildren() {
const elementClone = element => {
if (element.type === "input") {
return cloneWithAriaDescribedByAndLabelId(element);
}
return element;
};

return extractedChildren.children.map(elementClone);
}

function renderListBox() {
const extractedListBox = extractedChildren[supportedComponentNames.ListBox];
if (extractedListBox) {
return React.cloneElement(extractedListBox, {
refLabel,
});
}

return null;
}

if (!children) {
return null;
}

function renderFormElement() {
const extractedFormElement = extractedChildren[supportedComponentNames.FormElement];
if (extractedFormElement) {
const formElementChildren = extractedFormElement.map(formElement => {
return formElement.props.children.map(item => {
if (item.type.displayName === "FormElement.Content") {
return React.cloneElement(item, { wrapperAriaDescribedBy: ariaDescribedBy });
}
return item;
});
});

return extractedFormElement.map((formElement, index) => {
return React.cloneElement(formElement, { children: formElementChildren[index] });
});
}

return null;
}

return (
<sc.ContentContainer data-pka-anchor="form-element.content" {...moreProps}>
{typeof children === "function" ? children({ idForLabel, refLabel, ariaDescribedBy }) : children}
{renderInput()}
{renderCheckboxes()}
{renderRadioGroup()}
{renderDatePicker()}
{renderListBox()}
{renderFormElement()}
{renderTimePicker()}
{renderOtherChildren()}
</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