diff --git a/CHANGELOG.md b/CHANGELOG.md
index eb4f944195b..6232e6ba685 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@
- Converted `EuiSuggest` to Typescript ([#2692](https://github.com/elastic/eui/pull/2692))
- Converted `EuiErrorBoundary` to Typescript ([#2690](https://github.com/elastic/eui/pull/2690))
- Updated `EuiNavDrawer` to accept React fragments ([#2710](https://github.com/elastic/eui/pull/2710))
+- Added `EuiFormFieldset` and `EuiFormLegend` components ([#2706](https://github.com/elastic/eui/pull/2706))
**Bug fixes**
diff --git a/src-docs/src/views/form_controls/fieldset.tsx b/src-docs/src/views/form_controls/fieldset.tsx
new file mode 100644
index 00000000000..9a7ddb18fb5
--- /dev/null
+++ b/src-docs/src/views/form_controls/fieldset.tsx
@@ -0,0 +1,13 @@
+import React from 'react';
+
+import { EuiFormFieldset } from '../../../../src/components/form/form_fieldset';
+import { EuiSwitch } from '../../../../src/components/form/switch';
+import { EuiSpacer } from '../../../../src/components/spacer';
+
+export default () => (
+
+ {}} checked={false} />
+
+ {}} checked={true} />
+
+);
diff --git a/src-docs/src/views/form_controls/form_controls_example.js b/src-docs/src/views/form_controls/form_controls_example.js
index 4c61e9ea9f4..685883913e9 100644
--- a/src-docs/src/views/form_controls/form_controls_example.js
+++ b/src-docs/src/views/form_controls/form_controls_example.js
@@ -17,6 +17,8 @@ import {
EuiFieldSearch,
EuiFieldText,
EuiFilePicker,
+ EuiFormFieldset,
+ EuiFormLegend,
EuiFormControlLayout,
EuiFormControlLayoutDelimited,
EuiLink,
@@ -25,6 +27,7 @@ import {
EuiSelect,
EuiSwitch,
EuiTextArea,
+ EuiSpacer,
} from '../../../../src/components';
import FieldSearch from './field_search';
@@ -79,6 +82,10 @@ import PrependAppend from './prepend_append';
const PrependAppendSource = require('!!raw-loader!./prepend_append');
const PrependAppendHtml = renderToHtml(PrependAppend);
+import Fieldset from './fieldset';
+const fieldsetSource = require('!!raw-loader!./fieldset');
+const fieldsetHtml = renderToHtml(Fieldset);
+
import FormControlLayout from './form_control_layout';
const formControlLayoutSource = require('!!raw-loader!./form_control_layout');
const formControlLayoutHtml = renderToHtml(FormControlLayout);
@@ -319,6 +326,61 @@ export const FormControlsExample = {
},
demo: ,
},
+ {
+ title: 'Fieldset and legend',
+ source: [
+ {
+ type: GuideSectionTypes.JS,
+ code: fieldsetSource,
+ },
+ {
+ type: GuideSectionTypes.HTML,
+ code: fieldsetHtml,
+ },
+ ],
+ text: (
+
+
+ "[Use a fieldset and legend] for groups of related controls
+ where the individual labels for each control do not provide a
+ sufficient description, and an additional group level
+ description is needed."{' '}
+
+ WCAG Spec
+
+
+ }
+ />
+
+
+ EuiFieldset simply wraps its children in a{' '}
+ <fieldset> with the option to add a{' '}
+ <legend> via the legend{' '}
+ object prop.
+
+
+ ),
+ props: {
+ EuiFormFieldset,
+ EuiFormLegend,
+ },
+ demo:
,
+ snippet: [
+ `
+ /* Controls */
+`,
+ `
+ /* Controls */
+`,
+ ],
+ },
{
title: 'Prepend and Append',
text: (
diff --git a/src/components/form/_index.scss b/src/components/form/_index.scss
index ae9fcb9d333..0cd3dd54e79 100644
--- a/src/components/form/_index.scss
+++ b/src/components/form/_index.scss
@@ -8,6 +8,7 @@
@import 'form';
@import 'form_control_layout/index';
@import 'form_error_text/index';
+@import 'form_fieldset/index';
@import 'form_help_text/index';
@import 'form_label/index';
@import 'form_row/index';
diff --git a/src/components/form/form_fieldset/__snapshots__/form_fieldset.test.tsx.snap b/src/components/form/form_fieldset/__snapshots__/form_fieldset.test.tsx.snap
new file mode 100644
index 00000000000..5317bcbf64e
--- /dev/null
+++ b/src/components/form/form_fieldset/__snapshots__/form_fieldset.test.tsx.snap
@@ -0,0 +1,22 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiFormFieldset is rendered 1`] = `
+
+`;
+
+exports[`EuiFormFieldset props legend is rendered 1`] = `
+
+`;
diff --git a/src/components/form/form_fieldset/__snapshots__/form_legend.test.tsx.snap b/src/components/form/form_fieldset/__snapshots__/form_legend.test.tsx.snap
new file mode 100644
index 00000000000..2f0257d2b10
--- /dev/null
+++ b/src/components/form/form_fieldset/__snapshots__/form_legend.test.tsx.snap
@@ -0,0 +1,31 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiFormLegend is rendered 1`] = `
+
+`;
+
+exports[`EuiFormLegend props compressed is rendered 1`] = `
+
+`;
+
+exports[`EuiFormLegend props hidden is rendered 1`] = `
+
+`;
diff --git a/src/components/form/form_fieldset/_form_legend.scss b/src/components/form/form_fieldset/_form_legend.scss
new file mode 100644
index 00000000000..0b79ffc6f06
--- /dev/null
+++ b/src/components/form/form_fieldset/_form_legend.scss
@@ -0,0 +1,11 @@
+.euiFormLegend {
+ @include euiFormLabel;
+
+ &:not(.euiFormLegend-isHidden) {
+ margin-bottom: $euiSizeS;
+
+ &.euiFormLegend--compressed {
+ margin-bottom: $euiSizeXS;
+ }
+ }
+}
diff --git a/src/components/form/form_fieldset/_index.scss b/src/components/form/form_fieldset/_index.scss
new file mode 100644
index 00000000000..2751e94c43c
--- /dev/null
+++ b/src/components/form/form_fieldset/_index.scss
@@ -0,0 +1 @@
+@import 'form_legend';
diff --git a/src/components/form/form_fieldset/form_fieldset.test.tsx b/src/components/form/form_fieldset/form_fieldset.test.tsx
new file mode 100644
index 00000000000..b236b048dcc
--- /dev/null
+++ b/src/components/form/form_fieldset/form_fieldset.test.tsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import { render } from 'enzyme';
+import { requiredProps } from '../../../test/required_props';
+
+import { EuiFormFieldset } from './form_fieldset';
+
+describe('EuiFormFieldset', () => {
+ test('is rendered', () => {
+ const component = render(
+
+
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ describe('props', () => {
+ test('legend is rendered', () => {
+ const component = render(
+
+
+
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+ });
+});
diff --git a/src/components/form/form_fieldset/form_fieldset.tsx b/src/components/form/form_fieldset/form_fieldset.tsx
new file mode 100644
index 00000000000..72be13d2758
--- /dev/null
+++ b/src/components/form/form_fieldset/form_fieldset.tsx
@@ -0,0 +1,28 @@
+import React, { HTMLAttributes, FunctionComponent } from 'react';
+import { CommonProps } from '../../common';
+import { EuiFormLegendProps, EuiFormLegend } from './form_legend';
+
+export interface EuiFormFieldsetProps
+ extends CommonProps,
+ HTMLAttributes {
+ /**
+ * Adds an EuiFormLegend element as the first child
+ */
+ legend?: EuiFormLegendProps;
+}
+
+export const EuiFormFieldset: FunctionComponent = ({
+ children,
+ className,
+ legend,
+ ...rest
+}) => {
+ const legendDisplay = !!legend && ;
+
+ return (
+
+ );
+};
diff --git a/src/components/form/form_fieldset/form_legend.test.tsx b/src/components/form/form_fieldset/form_legend.test.tsx
new file mode 100644
index 00000000000..2a3676563bb
--- /dev/null
+++ b/src/components/form/form_fieldset/form_legend.test.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { render } from 'enzyme';
+import { requiredProps } from '../../../test/required_props';
+
+import { EuiFormLegend } from './form_legend';
+
+describe('EuiFormLegend', () => {
+ test('is rendered', () => {
+ const component = render(
+ Legend
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ describe('props', () => {
+ test('hidden is rendered', () => {
+ const component = render(
+ Legend
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+
+ test('compressed is rendered', () => {
+ const component = render(
+ Legend
+ );
+
+ expect(component).toMatchSnapshot();
+ });
+ });
+});
diff --git a/src/components/form/form_fieldset/form_legend.tsx b/src/components/form/form_fieldset/form_legend.tsx
new file mode 100644
index 00000000000..d5d6344aee3
--- /dev/null
+++ b/src/components/form/form_fieldset/form_legend.tsx
@@ -0,0 +1,44 @@
+import React, { HTMLAttributes, FunctionComponent, ReactNode } from 'react';
+import { CommonProps } from '../../common';
+import classNames from 'classnames';
+import { EuiScreenReaderOnly } from '../../accessibility';
+
+export type EuiFormLegendProps = HTMLAttributes &
+ CommonProps & {
+ children: ReactNode;
+ /**
+ * For a hidden legend that is still visible to the screen reader, set to 'hidden'
+ */
+ display?: 'hidden' | 'visible';
+ compressed?: boolean;
+ };
+
+export const EuiFormLegend: FunctionComponent = ({
+ children,
+ className,
+ display = 'visible',
+ compressed,
+ ...rest
+}) => {
+ const isLegendHidden = display === 'hidden';
+ const classes = classNames(
+ 'euiFormLegend',
+ {
+ 'euiFormLegend-isHidden': isLegendHidden,
+ 'euiFormLegend--compressed': compressed,
+ },
+ className
+ );
+
+ return (
+
+ );
+};
diff --git a/src/components/form/form_fieldset/index.ts b/src/components/form/form_fieldset/index.ts
new file mode 100644
index 00000000000..e1eb28b81d0
--- /dev/null
+++ b/src/components/form/form_fieldset/index.ts
@@ -0,0 +1,2 @@
+export { EuiFormFieldset, EuiFormFieldsetProps } from './form_fieldset';
+export { EuiFormLegend, EuiFormLegendProps } from './form_legend';
diff --git a/src/components/form/form_label/_form_label.scss b/src/components/form/form_label/_form_label.scss
index 887d7de476c..14055e077af 100644
--- a/src/components/form/form_label/_form_label.scss
+++ b/src/components/form/form_label/_form_label.scss
@@ -2,11 +2,9 @@
* 1. Focused state overrides invalid state.
*/
.euiFormLabel {
- @include euiFontSizeXS;
+ @include euiFormLabel;
display: inline-block;
transition: all $euiAnimSpeedFast $euiAnimSlightResistance;
- color: $euiTitleColor;
- font-weight: $euiFontWeightSemiBold;
&.euiFormLabel-isInvalid {
color: $euiColorDanger; /* 1 */
diff --git a/src/components/form/index.js b/src/components/form/index.js
index 0c0d51a554b..b07c9f0c34f 100644
--- a/src/components/form/index.js
+++ b/src/components/form/index.js
@@ -11,6 +11,7 @@ export {
EuiFormControlLayoutDelimited,
} from './form_control_layout';
export { EuiFormErrorText } from './form_error_text';
+export { EuiFormFieldset, EuiFormLegend } from './form_fieldset';
export { EuiFormHelpText } from './form_help_text';
export { EuiFormLabel } from './form_label';
export { EuiFormRow } from './form_row';
diff --git a/src/components/index.js b/src/components/index.js
index febbba31948..f4cb7cf6652 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -118,8 +118,10 @@ export {
EuiFormControlLayout,
EuiFormControlLayoutDelimited,
EuiFormErrorText,
+ EuiFormFieldset,
EuiFormHelpText,
EuiFormLabel,
+ EuiFormLegend,
EuiFormRow,
EuiRadio,
EuiRadioGroup,
diff --git a/src/global_styling/mixins/_form.scss b/src/global_styling/mixins/_form.scss
index 1aa4e831e1e..56b0e87bab1 100644
--- a/src/global_styling/mixins/_form.scss
+++ b/src/global_styling/mixins/_form.scss
@@ -1,3 +1,10 @@
+// Labels
+@mixin euiFormLabel {
+ @include euiFontSizeXS;
+ color: $euiTitleColor;
+ font-weight: $euiFontWeightSemiBold;
+}
+
@mixin euiFormControlLayoutPadding($numOfIcons, $side: 'right', $compressed: false) {
$firstIconSize: $euiFormControlPadding + $euiSize + $euiFormControlPadding;
$secondIconSize: $euiFormControlPadding + $euiSize;