Skip to content

Commit

Permalink
Extend FormLayoutCustomField options to improve accessibility and d…
Browse files Browse the repository at this point in the history
…esign consistency (#174)

New options:

- `disabled`
- `innerFieldSize`
- `labelForId`
- `required`
- `validationState`
  • Loading branch information
adamkudrna committed Apr 20, 2021
1 parent 1bac30a commit eb87026
Show file tree
Hide file tree
Showing 7 changed files with 246 additions and 30 deletions.
82 changes: 69 additions & 13 deletions src/lib/components/layout/FormLayout/FormLayoutCustomField.jsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,62 @@
import PropTypes from 'prop-types';
import React from 'react';
import getRootSizeClassName from '../../../helpers/getRootSizeClassName';
import getRootValidationStateClassName from '../../../helpers/getRootValidationStateClassName';
import { withProviderContext } from '../../../provider';
import styles from './FormLayoutCustomField.scss';

const renderLabel = (id, label, labelForId) => {
if (labelForId && label) {
return (
<label
htmlFor={labelForId}
id={id && `${id}__label`}
className={styles.label}
>
{label}
</label>
);
}

if (label) {
return (
<div
id={id && `${id}__label`}
className={styles.label}
>
{label}
</div>
);
}

return null;
};

export const FormLayoutCustomField = ({
children,
fullWidth,
id,
disabled,
innerFieldSize,
label,
labelForId,
layout,
required,
validationState,
}) => (
<div
id={id}
className={`
${styles.root}
${fullWidth ? styles.isRootFullWidth : ''}
${layout === 'vertical' ? styles.rootLayoutVertical : styles.rootLayoutHorizontal}
`.trim()}
className={[
styles.root,
fullWidth ? styles.isRootFullWidth : '',
layout === 'vertical' ? styles.rootLayoutVertical : styles.rootLayoutHorizontal,
disabled ? styles.isRootDisabled : '',
required ? styles.isRootRequired : '',
getRootSizeClassName(innerFieldSize, styles),
getRootValidationStateClassName(validationState, styles),
].join(' ')}
>
{label && (
<div
id={id && `${id}__label`}
className={styles.label}
>
{label}
</div>
)}
{renderLabel(id, label, labelForId)}
<div
id={id && `${id}__field`}
className={styles.field}
Expand All @@ -37,17 +68,26 @@ export const FormLayoutCustomField = ({

FormLayoutCustomField.defaultProps = {
children: null,
disabled: false,
fullWidth: false,
id: undefined,
innerFieldSize: null,
label: null,
labelForId: undefined,
layout: 'vertical',
required: false,
validationState: null,
};

FormLayoutCustomField.propTypes = {
/**
* Custom HTML or React component(s).
*/
children: PropTypes.node,
/**
* If `true`, label will be shown as disabled.
*/
disabled: PropTypes.bool,
/**
* If `true`, the field will span the full width of its parent.
*/
Expand All @@ -56,14 +96,30 @@ FormLayoutCustomField.propTypes = {
* ID of the root HTML element.
*/
id: PropTypes.string,
/**
* Size of contained form field used to properly align label.
*/
innerFieldSize: PropTypes.oneOf(['small', 'medium', 'large']),
/**
* Optional label of the field.
*/
label: PropTypes.string,
/**
* Optional ID of labelled field to keep accessibility features.
*/
labelForId: PropTypes.string,
/**
* Layout of the field, controlled by parent FormLayout.
*/
layout: PropTypes.oneOf(['horizontal', 'vertical']),
/**
* If `true`, label will be styled as required.
*/
required: PropTypes.bool,
/**
* Alter the field to provide feedback based on validation result.
*/
validationState: PropTypes.oneOf(['invalid', 'valid', 'warning']),
};

export const FormLayoutCustomFieldWithContext = withProviderContext(FormLayoutCustomField, 'FormLayoutCustomField');
Expand Down
40 changes: 40 additions & 0 deletions src/lib/components/layout/FormLayout/FormLayoutCustomField.scss
Original file line number Diff line number Diff line change
@@ -1,9 +1,36 @@
@use '../../../styles/tools/form-fields/foundation';
@use '../../../styles/tools/form-fields/box-field-layout';
@use '../../../styles/tools/form-fields/box-field-sizes';
@use '../../../styles/tools/form-fields/variants';

// Foundation
.root {
@include box-field-layout.in-form-layout();
@include variants.visual(custom);
}

.label {
@include foundation.label();
}

.isRootRequired .label {
@include foundation.label-required();
}

// States
.isRootStateInvalid {
@include variants.validation(invalid);
}

.isRootStateValid {
@include variants.validation(valid);
}

.isRootStateWarning {
@include variants.validation(warning);
}

// Layouts
.rootLayoutVertical,
.rootLayoutHorizontal {
@include box-field-layout.vertical();
Expand All @@ -21,3 +48,16 @@
.isRootFullWidth .field {
justify-self: stretch;
}

// Sizes
.rootSizeSmall {
@include box-field-sizes.size(small);
}

.rootSizeMedium {
@include box-field-sizes.size(medium);
}

.rootSizeLarge {
@include box-field-sizes.size(large);
}
126 changes: 122 additions & 4 deletions src/lib/components/layout/FormLayout/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -253,15 +253,123 @@ FormLayout elements. FormLayoutCustomFields are designed to work solely inside
the FormLayout component.

<Playground>
<FormLayout fieldLayout="horizontal">
<FormLayout fieldLayout="horizontal" labelWidth="auto">
<TextField id="my-text-field-custom-1" label="A form element" />
<FormLayoutCustomField label="Optional label">
<Placeholder bordered>Custom content</Placeholder>
<FormLayoutCustomField label="Optional custom field label">
<Placeholder bordered>Custom field content</Placeholder>
</FormLayoutCustomField>
<TextField id="my-text-field-custom-2" label="Another form element" />
</FormLayout>
</Playground>

👉 While you can set FormLayoutCustomField as `disabled`, `valid` or `required`
and its styles may affect contained form fields through CSS cascade, don't
forget to mirror the aforementioned properties to the contained form fields too
as API options as such are **not** inherited.

### Label Alignment

If you are in a situation with one or more box form fields inside your
FormLayoutCustomField, you may want to have its label aligned with the fields
inside. Since it's
[not quite possible to do this automatically](https://github.com/react-ui-org/react-ui/issues/265)
due to limited browser support, there is `innerFieldSize` option which accepts
any of existing box field sizes (small, medium, or large) and is intended right
for this task.

<Playground>
<FormLayout fieldLayout="horizontal" labelWidth="auto">
<TextField id="my-text-field-custom-alignment-1" label="A form element" />
<FormLayoutCustomField
innerFieldSize="medium"
label="Custom field label aligned to inner text input"
>
<TextField
id="my-text-field-custom-alignment-2"
isLabelVisible={false}
label="A form element"
placeholder="Text field with invisible label"
/>
</FormLayoutCustomField>
<TextField
id="my-text-field-custom-alignment-3"
label="Another form element"
/>
</FormLayout>
</Playground>

### Validation States

Custom fields support the same validation states as regular form fields to
provide labels with optional feedback style.

<Playground>
<FormLayout fieldLayout="horizontal" labelWidth="auto">
<TextField id="my-text-field-custom-validation-1" label="A form element" />
<FormLayoutCustomField
label="Custom field label in valid state"
validationState="valid"
>
<Placeholder bordered>Custom field content</Placeholder>
</FormLayoutCustomField>
<TextField
id="my-text-field-custom-validation-2"
label="Another form element"
/>
</FormLayout>
</Playground>

### Accessibility

If possible, use the `labelForId` option to provide ID of contained form field
so the field remains accessible via custom field label.

You can also specify size of contained form field so custom field label is
properly vertically aligned.

<Playground>
{() => {
const [isChecked, setIsChecked] = React.useState(false);
return (
<FormLayout fieldLayout="horizontal" labelWidth="auto">
<TextField
id="my-text-field-custom-accessibility-1"
label="A form element"
/>
<FormLayoutCustomField
fullWidth
label="Custom field label aligned with medium form field"
labelForId="my-text-field-custom-accessibility-2"
innerFieldSize="medium"
>
<Toolbar align="middle" dense>
<ToolbarItem>
<TextField
id="my-text-field-custom-accessibility-2"
isLabelVisible={false}
label="A form element"
placeholder="Text field with invisible label"
/>
</ToolbarItem>
<ToolbarItem>
<CheckboxField
changeHandler={() => setIsChecked(!isChecked)}
checked={isChecked}
id="my-checkbox-field-custom-accessibility-1"
label="Another form field"
/>
</ToolbarItem>
</Toolbar>
</FormLayoutCustomField>
<TextField
id="my-text-field-custom-accessibility-3"
label="Another form element"
/>
</FormLayout>
)
}}
</Playground>

## Full Example

This is a demo of all components supported by FormLayout.
Expand Down Expand Up @@ -403,7 +511,7 @@ This is a demo of all components supported by FormLayout.

<Props table of={FormLayout} />

### FormLayoutCustomField
### FormLayoutCustomField API

A place for custom content inside FormLayout.

Expand All @@ -417,3 +525,13 @@ A place for custom content inside FormLayout.
| `--rui-form-layout-horizontal-label-limited-width` | Label width in limited-width layout |
| `--rui-form-layout-horizontal-label-default-width` | Label width in the default layout |
| `--rui-form-layout-row-gap` | Gap between individual rows |

### FormLayoutCustomField Theming

FormLayoutCustomField can be styled using a small subset of
[other form fields theming options](/customize/theming/forms).

| Custom Property | Description |
|------------------------------------------------------|--------------------------------------------------------------|
| `--rui-form-field-custom-default-surrounding-text-color` | Custom field label color in default state |
| `--rui-form-field-custom-disabled-surrounding-text-color` | Custom field label color in disabled-like state |
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,14 @@ describe('rendering', () => {
it('renders correctly with all props', () => {
const tree = shallow((
<FormLayoutCustomField
disabled
fullWidth
label="Label"
labelForId="target-id"
id="my-custom-field"
innerFieldSize="small"
layout="horizontal"
required
>
<span>Custom text in form 1</span>
<span>Custom text in form 2</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@

exports[`rendering renders correctly with a single child 1`] = `
<div
className="root
rootLayoutVertical"
className="root rootLayoutVertical "
>
<div
className="field"
Expand All @@ -18,17 +16,16 @@ exports[`rendering renders correctly with a single child 1`] = `

exports[`rendering renders correctly with all props 1`] = `
<div
className="root
isRootFullWidth
rootLayoutHorizontal"
className="root isRootFullWidth rootLayoutHorizontal isRootDisabled isRootRequired rootSizeSmall "
id="my-custom-field"
>
<div
<label
className="label"
htmlFor="target-id"
id="my-custom-field__label"
>
Label
</div>
</label>
<div
className="field"
id="my-custom-field__field"
Expand All @@ -48,9 +45,7 @@ exports[`rendering renders correctly with all props 1`] = `

exports[`rendering renders correctly with multiple children 1`] = `
<div
className="root
rootLayoutVertical"
className="root rootLayoutVertical "
>
<div
className="field"
Expand Down
Loading

0 comments on commit eb87026

Please sign in to comment.