Skip to content
This repository has been archived by the owner on May 17, 2023. It is now read-only.

Commit

Permalink
Merge pull request #1695 from auth0/required-fields
Browse files Browse the repository at this point in the history
[DXDP-872] Implement "required" fields
  • Loading branch information
loginist authored Jan 28, 2020
2 parents d16d3a3 + 89290bf commit 0ca6d22
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 216 deletions.
12 changes: 12 additions & 0 deletions core/components/molecules/form/field/field.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,18 @@ or other rich formatting to displayed text.
</Form>
```

### Required fields

Here's an example letting the user know this field is required using the `required` prop.

```js
<Form>
<Form.Field label="Callback URL" required>
<TextInput type="text" placeholder="Enter something" />
</Form.Field>
</Form>
```

### Actions

You can add actions to a `TextInput` by passing an array of `Button`:
Expand Down
186 changes: 98 additions & 88 deletions core/components/molecules/form/field/field.tsx
Original file line number Diff line number Diff line change
@@ -1,111 +1,120 @@
import * as React from 'react'
import styled from '../../../styled'

import { spacing, misc } from '../../../tokens'
import uniqueId from '../../../_helpers/uniqueId'
import FormContext from '../form-context'
import Automation from '../../../_helpers/automation-attribute'

import StyledLabel from '../label'
import StyledError from '../error'
import HelpText from '../help-text'
import TextArea from '../../../atoms/textarea'
import Switch from '../../../atoms/switch'
import Checkbox from '../../../atoms/checkbox'
import Radio from '../../../atoms/radio'
import { ActionWithIcon } from '../../../_helpers/action-shape'
import containerStyles from '../../../_helpers/container-styles'
import * as React from "react";
import styled from "../../../styled";

import { spacing, misc } from "../../../tokens";
import uniqueId from "../../../_helpers/uniqueId";
import FormContext from "../form-context";
import Automation from "../../../_helpers/automation-attribute";

import StyledLabel from "../label";
import StyledError from "../error";
import HelpText from "../help-text";
import TextArea from "../../../atoms/textarea";
import Switch from "../../../atoms/switch";
import Checkbox from "../../../atoms/checkbox";
import Radio from "../../../atoms/radio";
import { ActionWithIcon } from "../../../_helpers/action-shape";
import containerStyles from "../../../_helpers/container-styles";

export interface IFieldProps {
/** HTML ID of the component */
id?: string
id?: string;
/** HTML name of the component */
name?: string
name?: string;
/** Form Label */
label?: string
label?: string;
/** Text that further explains the purpose of the field, or provides more detail */
helpText?: React.ReactNode
helpText?: React.ReactNode;
/** Error message to show in case of failed validation */
error?: string
error?: string;
/** Actions to be attached to input */
actions?: Array<JSX.Element | ActionWithIcon>
actions?: Array<JSX.Element | ActionWithIcon>;
/** checkbox alignment */
checkbox?: boolean
hasError?: boolean
htmlFor?: string
children?: React.ReactNode
checkbox?: boolean;
/** Shows the user this field is required */
required?: boolean;
hasError?: boolean;
htmlFor?: string;
children?: React.ReactNode;
/** @internal */
fieldComponent?: any
fieldComponent?: any;
}

const shouldFieldUseCheckboxStyle = (props) => {
if (props.checkbox) { return true }
if (props.checkbox) {
return true;
}
if (props.children) {
const children = React.Children.toArray(props.children)
const type = children[0].type
return type === Checkbox || type === Radio || type === Checkbox.Group
const children = React.Children.toArray(props.children);
const type = children[0].type;
return type === Checkbox || type === Radio || type === Checkbox.Group;
}
return false
}
return false;
};

const { Provider, Consumer } = React.createContext<{ formFieldId?: string }>({})
const { Provider, Consumer } = React.createContext<{ formFieldId?: string }>({});

const FieldInput = (props) => {
const { Component, ...fieldProps } = props
const { Component, ...fieldProps } = props;
/*
old API
we proxy through all the props to the input element
*/
if (Component) { return <Component {...fieldProps} /> }
if (Component) {
return <Component {...fieldProps} />;
}

/*
New API
We create a context around the field to pass the field id
*/
const { children, id } = fieldProps
return <Provider value={{ formFieldId: id }}>{children}</Provider>
}
const { children, id } = fieldProps;
return <Provider value={{ formFieldId: id }}>{children}</Provider>;
};

const ariaDescribedBy = (helperTextId, errorTextId) => {
if (errorTextId) {
return { 'aria-invalid': true, 'aria-errormessage': errorTextId }
return { "aria-invalid": true, "aria-errormessage": errorTextId };
}

if (helperTextId) {
return { 'aria-describedby': helperTextId }
return { "aria-describedby": helperTextId };
}

return {}
}
return {};
};

const applyAriaToFieldChild = (children, inputId, helperTextId, errorTextId) =>
const applyAriaToFieldChild = (children, inputId, helperTextId, errorTextId, isRequired) =>
React.Children.map(children, (child) => {
if (!child) { return null }
if (!child) {
return null;
}
return React.cloneElement(child, {
id: inputId,
...ariaDescribedBy(helperTextId, errorTextId)
})
})
...ariaDescribedBy(helperTextId, errorTextId),
"aria-required": isRequired
});
});

const getIdFromChild = (child) => child.props.id
const getIdFromChild = (child) => child.props.id;

const getIdFromChildren = (rawChildren) => {
const children = React.Children.toArray(rawChildren)
const children = React.Children.toArray(rawChildren);
if (children.length === 0) {
return null
return null;
}
return getIdFromChild(children[0])
}
return getIdFromChild(children[0]);
};

const Field = (props: IFieldProps) => {
/* Get unique id for label */
const id = getIdFromChildren(props.children) || uniqueId(props.label)
const { error, htmlFor, ...fieldProps } = props
const useCheckboxStyle = shouldFieldUseCheckboxStyle(props)
const Label = useCheckboxStyle ? Field.CheckboxLabel : StyledLabel
const FieldSetWrapper = useCheckboxStyle ? Field.FieldSetElement : React.Fragment
const helperTextId = props.helpText ? id + '-helper-text' : null
const errorTextId = props.error ? id + '-error-text' : null
const id = getIdFromChildren(props.children) || uniqueId(props.label);
const { error, htmlFor, ...fieldProps } = props;
const useCheckboxStyle = shouldFieldUseCheckboxStyle(props);
const Label = useCheckboxStyle ? Field.CheckboxLabel : StyledLabel;
const FieldSetWrapper = useCheckboxStyle ? Field.FieldSetElement : React.Fragment;
const helperTextId = props.helpText ? id + "-helper-text" : null;
const errorTextId = props.error ? id + "-error-text" : null;

return (
<FormContext.Consumer>
Expand All @@ -114,15 +123,12 @@ const Field = (props: IFieldProps) => {
// to make them accesible.
// There is a bug due to a browser bug https://github.com/w3c/csswg-drafts/issues/321
<FieldSetWrapper>
<Field.Element
layout={context.layout}
fullWidth={context.fullWidth}
{...Automation('form.field')}
>
<Field.Element layout={context.layout} fullWidth={context.fullWidth} {...Automation("form.field")}>
<Field.LabelLayout checkbox={useCheckboxStyle} layout={context.layout}>
<Label htmlFor={id}>{props.label}</Label>
{props.required && <Field.RequiredIndicator />}
</Field.LabelLayout>
<Field.ContentLayout layout={context.layout} {...Automation('form.field.content')}>
<Field.ContentLayout layout={context.layout} {...Automation("form.field.content")}>
{props.fieldComponent ? (
<props.fieldComponent
id={id}
Expand All @@ -131,7 +137,7 @@ const Field = (props: IFieldProps) => {
{...ariaDescribedBy(helperTextId, errorTextId)}
/>
) : (
applyAriaToFieldChild(props.children, id, helperTextId, errorTextId)
applyAriaToFieldChild(props.children, id, helperTextId, errorTextId, props.required)
)}
{(props.error || props.helpText) && (
<Field.FeedbackContainer>
Expand All @@ -144,8 +150,8 @@ const Field = (props: IFieldProps) => {
</FieldSetWrapper>
)}
</FormContext.Consumer>
)
}
);
};

Field.Element = styled.div`
${containerStyles};
Expand All @@ -158,9 +164,9 @@ Field.Element = styled.div`
}
@media (min-width: 768px) {
grid-gap: ${(props) => (props.layout === 'label-on-left' ? spacing.medium : spacing.xsmall)};
grid-gap: ${(props) => (props.layout === "label-on-left" ? spacing.medium : spacing.xsmall)};
grid-template-columns: ${(props) => (props.layout === 'label-on-left' ? '200px 1fr' : '1fr')};
grid-template-columns: ${(props) => (props.layout === "label-on-left" ? "200px 1fr" : "1fr")};
}
${TextArea.Element} {
Expand All @@ -170,39 +176,43 @@ Field.Element = styled.div`
${Switch.Element} {
/* Adds a space so the label aligns with the switch */
@media (min-width: 768px) {
margin-top: ${(props) => (props.layout === 'label-on-left' ? '6px' : '0')};
margin-top: ${(props) => (props.layout === "label-on-left" ? "6px" : "0")};
}
}
`
`;

Field.RequiredIndicator = styled.span.attrs({ children: "*" })`
color: red;
margin-left: ${spacing.unit / 2}px;
`;

Field.FieldSetElement = styled.fieldset`
&:not(:last-child):not(:only-child) {
margin-bottom: ${spacing.medium};
}
`
Field.CheckboxLabel = StyledLabel.withComponent('legend')
`;
Field.CheckboxLabel = StyledLabel.withComponent("legend");

Field.LabelLayout = styled.div`
@media (min-width: 768px) {
text-align: ${(props) => (props.layout === 'label-on-left' ? 'right' : 'left')};
padding-top: ${(props) =>
!props.checkbox && props.layout === 'label-on-left' ? misc.inputs.padding : '0'};
text-align: ${(props) => (props.layout === "label-on-left" ? "right" : "left")};
padding-top: ${(props) => (!props.checkbox && props.layout === "label-on-left" ? misc.inputs.padding : "0")};
}
`
Field.ContentLayout = styled.div``
`;
Field.ContentLayout = styled.div``;

Field.displayName = 'Form Field'
Field.displayName = "Form Field";

Field.FeedbackContainer = styled.div`
margin-top: ${spacing.xsmall};
`
`;

Field.defaultProps = {
label: '',
label: "",
helpText: null,
error: null
}
};

Field.ContextConsumer = Consumer
Field.Error = StyledError
export default Field
Field.ContextConsumer = Consumer;
Field.Error = StyledError;
export default Field;
Loading

0 comments on commit 0ca6d22

Please sign in to comment.