diff --git a/src-docs/src/services/playground/knobs.js b/src-docs/src/services/playground/knobs.js index 5dbacd61925..512c946f2ac 100644 --- a/src-docs/src/services/playground/knobs.js +++ b/src-docs/src/services/playground/knobs.js @@ -55,7 +55,7 @@ export const markup = (text) => { } return token; }); - return [...values,
]; + return [...values, ' ']; }); }; diff --git a/src-docs/src/views/accordion/accordion_form.js b/src-docs/src/views/accordion/accordion_form.js index d676b39c967..24761fadce7 100644 --- a/src-docs/src/views/accordion/accordion_form.js +++ b/src-docs/src/views/accordion/accordion_form.js @@ -21,9 +21,7 @@ const repeatableForm = ( - - - + diff --git a/src-docs/src/views/copy/copy.js b/src-docs/src/views/copy/copy.js index 1a2812e4ada..fb15aa28615 100644 --- a/src-docs/src/views/copy/copy.js +++ b/src-docs/src/views/copy/copy.js @@ -5,7 +5,6 @@ import { EuiButton, EuiFieldText, EuiSpacer, - EuiFormRow, } from '../../../../src/components/'; export default () => { @@ -17,9 +16,11 @@ export default () => { return (
- - - + diff --git a/src-docs/src/views/flyout/flyout_max_width.js b/src-docs/src/views/flyout/flyout_max_width.js index be304813d87..94f32af174c 100644 --- a/src-docs/src/views/flyout/flyout_max_width.js +++ b/src-docs/src/views/flyout/flyout_max_width.js @@ -70,12 +70,11 @@ export default () => { - - - + name="first" + /> { return ( - - - + { }; return ( - - - + compressed + /> { isFullWidth /> - - - + + { return ( - - - + name="first" + isLoading + compressed + /> ( - - - + /> - onChange(e)} - aria-label="Use aria labels when no actual label is in use" - /> - + onChange(e)} + label="This text field has its own label prop" + /> ); } 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 34b1a1f8df2..9e57dbb12f3 100644 --- a/src-docs/src/views/form_controls/form_controls_example.js +++ b/src-docs/src/views/form_controls/form_controls_example.js @@ -2,8 +2,6 @@ import React, { Fragment } from 'react'; import { Link } from 'react-router-dom'; -import { renderToHtml } from '../../services'; - import { GuideSectionTypes } from '../../components'; import { @@ -28,6 +26,7 @@ import { EuiSwitch, EuiTextArea, EuiSpacer, + EuiText, } from '../../../../src/components'; import { @@ -43,7 +42,6 @@ import { import FieldSearch from './field_search'; const fieldSearchSource = require('!!raw-loader!./field_search'); -const fieldSearchHtml = renderToHtml(FieldSearch); const fieldSearchSnippet = [ ` +

+ A simple wrapper around{' '} + {''}. +

+ + +

+ EuiTextField is currently the + only field component to accept{' '} + + EuiFormRow + {' '} + props directly. For instance, when providing the{' '} + label prop, it will automically wrap itself + in a EuiFormRow. Other examples of promoted + props include helpText,{' '} + error , and display. + Providing form row props at this level also decreases the need + for duplicate props like isInvalid and{' '} + fullWidth. +

+
+
+ + ), source: [ { type: GuideSectionTypes.JS, code: fieldTextSource, }, - { - type: GuideSectionTypes.HTML, - code: fieldTextHtml, - }, ], snippet: fieldTextSnippet, props: { @@ -231,10 +238,6 @@ export const FormControlsExample = { type: GuideSectionTypes.JS, code: fieldSearchSource, }, - { - type: GuideSectionTypes.HTML, - code: fieldSearchHtml, - }, ], snippet: fieldSearchSnippet, props: { @@ -250,10 +253,6 @@ export const FormControlsExample = { type: GuideSectionTypes.JS, code: fieldNumberSource, }, - { - type: GuideSectionTypes.HTML, - code: fieldNumberHtml, - }, ], snippet: fieldNumberSnippet, props: { @@ -269,10 +268,6 @@ export const FormControlsExample = { type: GuideSectionTypes.JS, code: fieldPasswordSource, }, - { - type: GuideSectionTypes.HTML, - code: fieldPasswordHtml, - }, ], snippet: fieldPasswordSnippet, props: { @@ -288,10 +283,6 @@ export const FormControlsExample = { type: GuideSectionTypes.JS, code: selectSource, }, - { - type: GuideSectionTypes.HTML, - code: selectHtml, - }, ], text: (

@@ -323,10 +314,6 @@ export const FormControlsExample = { type: GuideSectionTypes.JS, code: textAreaSource, }, - { - type: GuideSectionTypes.HTML, - code: textAreaHtml, - }, ], snippet: textAreaSnippet, props: { @@ -342,10 +329,6 @@ export const FormControlsExample = { type: GuideSectionTypes.JS, code: filePickerSource, }, - { - type: GuideSectionTypes.HTML, - code: filePickerHtml, - }, ], text: (

@@ -397,10 +380,6 @@ export const FormControlsExample = { type: GuideSectionTypes.JS, code: checkboxSource, }, - { - type: GuideSectionTypes.HTML, - code: checkboxHtml, - }, ], snippet: checkboxSnippet, props: { @@ -416,10 +395,6 @@ export const FormControlsExample = { type: GuideSectionTypes.JS, code: checkboxGroupSource, }, - { - type: GuideSectionTypes.HTML, - code: checkboxGroupHtml, - }, ], props: { EuiCheckboxGroup, @@ -443,10 +418,6 @@ export const FormControlsExample = { type: GuideSectionTypes.JS, code: radioSource, }, - { - type: GuideSectionTypes.HTML, - code: radioHtml, - }, ], snippet: radioSnippet, props: { @@ -462,10 +433,6 @@ export const FormControlsExample = { type: GuideSectionTypes.JS, code: radioGroupSource, }, - { - type: GuideSectionTypes.HTML, - code: radioGroupHtml, - }, ], props: { EuiRadioGroup, @@ -493,10 +460,6 @@ export const FormControlsExample = { type: GuideSectionTypes.JS, code: switchSource, }, - { - type: GuideSectionTypes.HTML, - code: switchHtml, - }, ], snippet: switchSnippet, props: { @@ -512,10 +475,6 @@ export const FormControlsExample = { type: GuideSectionTypes.JS, code: fieldsetSource, }, - { - type: GuideSectionTypes.HTML, - code: fieldsetHtml, - }, ], text: ( @@ -588,10 +547,6 @@ export const FormControlsExample = { type: GuideSectionTypes.JS, code: PrependAppendSource, }, - { - type: GuideSectionTypes.HTML, - code: PrependAppendHtml, - }, ], demo: , snippet: [ @@ -612,10 +567,6 @@ export const FormControlsExample = { type: GuideSectionTypes.JS, code: formControlLayoutSource, }, - { - type: GuideSectionTypes.HTML, - code: formControlLayoutHtml, - }, ], text: ( @@ -653,10 +604,6 @@ export const FormControlsExample = { type: GuideSectionTypes.JS, code: formControlLayoutRangeSource, }, - { - type: GuideSectionTypes.HTML, - code: formControlLayoutRangeHtml, - }, ], text: ( diff --git a/src-docs/src/views/form_controls/playground.js b/src-docs/src/views/form_controls/playground.js index 3cef0404b5e..f7e48e2d7cb 100644 --- a/src-docs/src/views/form_controls/playground.js +++ b/src-docs/src/views/form_controls/playground.js @@ -22,6 +22,27 @@ export const FieldTextConfig = () => { : EuiFieldText.__docgenInfo; const propsToUse = propUtilityForPlayground(docgenInfo.props); + propsToUse.label = { + ...propsToUse.label, + type: PropTypes.String, + value: 'Text field label', + }; + + propsToUse.error = { + ...propsToUse.error, + type: PropTypes.String, + }; + + propsToUse.labelAppend = { + ...propsToUse.labelAppend, + type: PropTypes.String, + }; + + propsToUse.helpText = { + ...propsToUse.helpText, + type: PropTypes.String, + }; + propsToUse.append = { ...propsToUse.append, type: PropTypes.String, diff --git a/src-docs/src/views/form_controls/prepend_append.js b/src-docs/src/views/form_controls/prepend_append.js index 4e5751a1222..788112f2866 100644 --- a/src-docs/src/views/form_controls/prepend_append.js +++ b/src-docs/src/views/form_controls/prepend_append.js @@ -180,6 +180,16 @@ export default () => { readOnly={isReadOnly} aria-label="Use aria labels when no actual label is in use" /> + + ); }; diff --git a/src-docs/src/views/form_layouts/described_form_group.js b/src-docs/src/views/form_layouts/described_form_group.js index de21d37e96e..5ddac1d6f6a 100644 --- a/src-docs/src/views/form_layouts/described_form_group.js +++ b/src-docs/src/views/form_layouts/described_form_group.js @@ -35,14 +35,10 @@ export default () => { } > - - - + No description}> - - - + Multiple fields} @@ -98,13 +94,12 @@ export default () => { /> - - - + ); diff --git a/src-docs/src/views/form_layouts/form_layouts_example.js b/src-docs/src/views/form_layouts/form_layouts_example.js index 1c4f541df4f..a966010d863 100644 --- a/src-docs/src/views/form_layouts/form_layouts_example.js +++ b/src-docs/src/views/form_layouts/form_layouts_example.js @@ -142,11 +142,7 @@ export const FormLayoutsExample = { } > - - - + `, }, { diff --git a/src-docs/src/views/form_layouts/form_rows.js b/src-docs/src/views/form_layouts/form_rows.js index 4a12b75d422..a1263887843 100644 --- a/src-docs/src/views/form_layouts/form_rows.js +++ b/src-docs/src/views/form_layouts/form_rows.js @@ -55,9 +55,11 @@ export default () => { return ( - - - + diff --git a/src-docs/src/views/form_layouts/inline.js b/src-docs/src/views/form_layouts/inline.js index 5f84b020ddc..0c06adb5c82 100644 --- a/src-docs/src/views/form_layouts/inline.js +++ b/src-docs/src/views/form_layouts/inline.js @@ -11,14 +11,10 @@ import { export default () => ( - - - + - - - + diff --git a/src-docs/src/views/form_layouts/inline_popover.js b/src-docs/src/views/form_layouts/inline_popover.js index 29cefd4cc27..097a4be50c2 100644 --- a/src-docs/src/views/form_layouts/inline_popover.js +++ b/src-docs/src/views/form_layouts/inline_popover.js @@ -61,9 +61,7 @@ export default () => { - - - + @@ -97,9 +95,7 @@ export default () => { /> - - - + diff --git a/src-docs/src/views/form_layouts/inline_sizing.js b/src-docs/src/views/form_layouts/inline_sizing.js index 56f70f5bc54..7813bd20bdd 100644 --- a/src-docs/src/views/form_layouts/inline_sizing.js +++ b/src-docs/src/views/form_layouts/inline_sizing.js @@ -18,9 +18,7 @@ export default () => ( - - - + diff --git a/src-docs/src/views/form_validation/validation.js b/src-docs/src/views/form_validation/validation.js index 93e1e1426f9..13bb454129e 100644 --- a/src-docs/src/views/form_validation/validation.js +++ b/src-docs/src/views/form_validation/validation.js @@ -35,18 +35,19 @@ export default () => { return ( - - - + - - - + /> diff --git a/src-docs/src/views/html_id_generator/bothPrefixSuffix.js b/src-docs/src/views/html_id_generator/bothPrefixSuffix.js index 08ee7ea6a10..4e460ff9fd6 100644 --- a/src-docs/src/views/html_id_generator/bothPrefixSuffix.js +++ b/src-docs/src/views/html_id_generator/bothPrefixSuffix.js @@ -6,7 +6,6 @@ import { EuiFlexItem, EuiSpacer, EuiCode, - EuiFormRow, } from '../../../../src/components'; import { htmlIdGenerator } from '../../../../src/services'; @@ -36,22 +35,20 @@ export const PrefixSufix = () => { alignItems="center" > - - - + - - - + diff --git a/src-docs/src/views/html_id_generator/html_id_generator_prefix.js b/src-docs/src/views/html_id_generator/html_id_generator_prefix.js index 4e6d38279c0..70b11c85246 100644 --- a/src-docs/src/views/html_id_generator/html_id_generator_prefix.js +++ b/src-docs/src/views/html_id_generator/html_id_generator_prefix.js @@ -6,7 +6,6 @@ import { EuiFlexItem, EuiSpacer, EuiCode, - EuiFormRow, } from '../../../../src/components'; import { htmlIdGenerator } from '../../../../src/services'; @@ -29,13 +28,12 @@ export const HtmlIdGeneratorPrefix = () => { alignItems="center" > - - - + diff --git a/src-docs/src/views/html_id_generator/html_id_generator_suffix.js b/src-docs/src/views/html_id_generator/html_id_generator_suffix.js index 3412360104e..7a5f6ab8db1 100644 --- a/src-docs/src/views/html_id_generator/html_id_generator_suffix.js +++ b/src-docs/src/views/html_id_generator/html_id_generator_suffix.js @@ -6,7 +6,6 @@ import { EuiFlexItem, EuiSpacer, EuiCode, - EuiFormRow, } from '../../../../src/components'; import { htmlIdGenerator } from '../../../../src/services'; @@ -29,13 +28,12 @@ export const HtmlIdGeneratorSuffix = () => { alignItems="center" > - - - + diff --git a/src-docs/src/views/i18n/context.js b/src-docs/src/views/i18n/context.js index 64959bb9cff..9c96063add7 100644 --- a/src-docs/src/views/i18n/context.js +++ b/src-docs/src/views/i18n/context.js @@ -6,7 +6,6 @@ import { EuiFieldText, EuiFlexGroup, EuiFlexItem, - EuiFormRow, EuiSpacer, EuiI18n, EuiI18nNumber, @@ -42,13 +41,10 @@ const ContextConsumer = () => { - - - + placeholder={useEuiI18n('euiContext.placeholder', 'John Doe')} + /> diff --git a/src-docs/src/views/i18n/i18n_attribute.js b/src-docs/src/views/i18n/i18n_attribute.js index 5100bcf9606..96b567ba175 100644 --- a/src-docs/src/views/i18n/i18n_attribute.js +++ b/src-docs/src/views/i18n/i18n_attribute.js @@ -4,7 +4,6 @@ import { EuiCode, EuiFieldText, EuiI18n, - EuiFormRow, EuiTitle, useEuiI18n, EuiSpacer, @@ -17,21 +16,18 @@ export default () => {

useEuiI18n used in an attribute

- This text field's placeholder reads from{' '} euiI18nAttribute.placeholderName } - > - - + placeholder={useEuiI18n( + 'euiI18nAttribute.placeholderName', + 'John Doe' + )} + />

@@ -41,16 +37,15 @@ export default () => { {(placeholderName) => ( - This text field's placeholder reads from{' '} euiI18nAttribute.placeholderName } - > - - + placeholder={placeholderName} + /> )} diff --git a/src-docs/src/views/modal/guidelines.js b/src-docs/src/views/modal/guidelines.js index 56af4cfbc22..2e96c0ca9d3 100644 --- a/src-docs/src/views/modal/guidelines.js +++ b/src-docs/src/views/modal/guidelines.js @@ -116,9 +116,8 @@ export default () => ( Save dashboard - - - + + @@ -149,12 +148,10 @@ export default () => (

Step 1 of 3: the basics

- - - - - - + + + +
diff --git a/src-docs/src/views/suggest/global_filter_form.js b/src-docs/src/views/suggest/global_filter_form.js index 1132a77daac..e0ae977469b 100644 --- a/src-docs/src/views/suggest/global_filter_form.js +++ b/src-docs/src/views/suggest/global_filter_form.js @@ -208,9 +208,12 @@ const GlobalFilterForm = (props) => { {useCustomLabel && (
- - - + +
)} diff --git a/src/components/color_picker/__snapshots__/color_picker.test.tsx.snap b/src/components/color_picker/__snapshots__/color_picker.test.tsx.snap index dc13c8023fb..49158dc632d 100644 --- a/src/components/color_picker/__snapshots__/color_picker.test.tsx.snap +++ b/src/components/color_picker/__snapshots__/color_picker.test.tsx.snap @@ -27,6 +27,7 @@ exports[`renders EuiColorPicker 1`] = ` autocomplete="off" class="euiFieldText euiColorPicker__input euiFieldText--withIcon" data-test-subj="euiColorPickerAnchor test subject string" + id="euiFieldText_generated-id" type="text" value="#FFEEDD" /> @@ -92,6 +93,7 @@ exports[`renders EuiColorPicker with a clearable input 1`] = ` autocomplete="off" class="euiFieldText euiColorPicker__input euiFieldText--withIcon" data-test-subj="euiColorPickerAnchor test subject string" + id="euiFieldText_generated-id" type="text" value="#FFEEDD" /> @@ -167,6 +169,7 @@ exports[`renders EuiColorPicker with a color swatch when color is defined 1`] = autocomplete="off" class="euiFieldText euiColorPicker__input euiFieldText--withIcon" data-test-subj="euiColorPickerAnchor test subject string" + id="euiFieldText_generated-id" type="text" value="#FFFFFF" /> @@ -230,6 +233,7 @@ exports[`renders EuiColorPicker with a custom placeholder 1`] = ` autocomplete="off" class="euiFieldText euiColorPicker__input euiFieldText--withIcon" data-test-subj="euiColorPickerAnchor test subject string" + id="euiFieldText_generated-id" placeholder="Auto" type="text" value="" @@ -294,6 +298,7 @@ exports[`renders EuiColorPicker with an empty swatch when color is "" 1`] = ` autocomplete="off" class="euiFieldText euiColorPicker__input euiFieldText--withIcon" data-test-subj="euiColorPickerAnchor test subject string" + id="euiFieldText_generated-id" placeholder="Transparent" type="text" value="" @@ -358,6 +363,7 @@ exports[`renders EuiColorPicker with an empty swatch when color is null 1`] = ` autocomplete="off" class="euiFieldText euiColorPicker__input euiFieldText--withIcon" data-test-subj="euiColorPickerAnchor test subject string" + id="euiFieldText_generated-id" placeholder="Transparent" type="text" value="" @@ -429,6 +435,7 @@ exports[`renders a EuiColorPicker with a prepend and append 1`] = ` autocomplete="off" class="euiFieldText euiColorPicker__input euiColorPicker__input--inGroup euiFieldText--withIcon" data-test-subj="euiColorPickerAnchor test subject string" + id="euiFieldText_generated-id" type="text" value="#FFEEDD" /> @@ -499,6 +506,7 @@ exports[`renders a EuiColorPicker with an alpha range selector 1`] = ` autocomplete="off" class="euiFieldText euiColorPicker__input euiFieldText--withIcon" data-test-subj="euiColorPickerAnchor test subject string" + id="euiFieldText_generated-id" type="text" value="#FFEEDD" /> @@ -564,6 +572,7 @@ exports[`renders compressed EuiColorPicker 1`] = ` autocomplete="off" class="euiFieldText euiColorPicker__input euiFieldText--withIcon euiFieldText--compressed" data-test-subj="euiColorPickerAnchor test subject string" + id="euiFieldText_generated-id" type="text" value="#FFEEDD" /> @@ -630,6 +639,7 @@ exports[`renders disabled EuiColorPicker 1`] = ` class="euiFieldText euiColorPicker__input euiFieldText--withIcon" data-test-subj="euiColorPickerAnchor test subject string" disabled="" + id="euiFieldText_generated-id" type="text" value="#FFEEDD" /> @@ -695,6 +705,7 @@ exports[`renders fullWidth EuiColorPicker 1`] = ` autocomplete="off" class="euiFieldText euiColorPicker__input euiFieldText--withIcon euiFieldText--fullWidth" data-test-subj="euiColorPickerAnchor test subject string" + id="euiFieldText_generated-id" type="text" value="#FFEEDD" /> @@ -932,6 +943,7 @@ exports[`renders readOnly EuiColorPicker 1`] = ` autocomplete="off" class="euiFieldText euiColorPicker__input euiFieldText--withIcon" data-test-subj="euiColorPickerAnchor test subject string" + id="euiFieldText_generated-id" readonly="" type="text" value="#FFEEDD" diff --git a/src/components/form/described_form_group/__snapshots__/described_form_group.test.tsx.snap b/src/components/form/described_form_group/__snapshots__/described_form_group.test.tsx.snap index c8117412922..8c8129e8c6c 100644 --- a/src/components/form/described_form_group/__snapshots__/described_form_group.test.tsx.snap +++ b/src/components/form/described_form_group/__snapshots__/described_form_group.test.tsx.snap @@ -74,6 +74,7 @@ exports[`EuiDescribedFormGroup is rendered 1`] = ` hasChildLabel={true} hasEmptyLabelSpace={false} labelType="label" + passThrough={true} >
- - + - + for="euiFieldText_generated-id" + > + label + + labelAppend +
+
+
+
+ + + +
+
+
+ error +
+
+ helpText +
+
+
+`; + +exports[`EuiFieldText is rendered 1`] = ` +
+
+ + + +
+
+`; + +exports[`EuiFieldText props append is rendered 1`] = ` +
+
+ + + +
+ +
+`; + +exports[`EuiFieldText props compressed is rendered 1`] = ` +
+
+ + + +
+
`; exports[`EuiFieldText props controlOnly is rendered 1`] = ` `; +exports[`EuiFieldText props disabled is rendered 1`] = ` +
+
+ + + +
+
+`; + exports[`EuiFieldText props fullWidth is rendered 1`] = ` - - - - - +
+ + + +
+
+`; + +exports[`EuiFieldText props icon is rendered 1`] = ` +
+
+ + + +
+ + +
+
+
`; exports[`EuiFieldText props isInvalid is rendered 1`] = ` - - - - - + + + +
+
`; exports[`EuiFieldText props isLoading is rendered 1`] = ` - +
+ + + +
+ +
+
+
+`; + +exports[`EuiFieldText props placeholder is rendered 1`] = ` +
- - - - +
+ + + +
+
+`; + +exports[`EuiFieldText props prepend is rendered 1`] = ` +
+ +
+ + + +
+
`; exports[`EuiFieldText props readOnly is rendered 1`] = ` - - - - - +
+ + + +
+
`; diff --git a/src/components/form/field_text/field_text.test.tsx b/src/components/form/field_text/field_text.test.tsx index 86fae243ef1..fa79485fdef 100644 --- a/src/components/form/field_text/field_text.test.tsx +++ b/src/components/form/field_text/field_text.test.tsx @@ -12,13 +12,6 @@ import { requiredProps } from '../../../test/required_props'; import { EuiFieldText } from './field_text'; -jest.mock('../form_control_layout', () => { - const formControlLayout = jest.requireActual('../form_control_layout'); - return { - ...formControlLayout, - EuiFormControlLayout: 'eui-form-control-layout', - }; -}); jest.mock('../validatable_control', () => ({ EuiValidatableControl: 'eui-validatable-control', })); @@ -29,9 +22,7 @@ describe('EuiFieldText', () => { {}} onChange={() => {}} {...requiredProps} @@ -42,6 +33,12 @@ describe('EuiFieldText', () => { }); describe('props', () => { + test('icon is rendered', () => { + const component = render(); + + expect(component).toMatchSnapshot(); + }); + test('isInvalid is rendered', () => { const component = render(); @@ -66,10 +63,61 @@ describe('EuiFieldText', () => { expect(component).toMatchSnapshot(); }); + test('disabled is rendered', () => { + const component = render(); + + expect(component).toMatchSnapshot(); + }); + + test('compressed is rendered', () => { + const component = render(); + + expect(component).toMatchSnapshot(); + }); + test('controlOnly is rendered', () => { const component = render(); expect(component).toMatchSnapshot(); }); + + test('placeholder is rendered', () => { + const component = render(); + + expect(component).toMatchSnapshot(); + }); + + test('prepend is rendered', () => { + const component = render(); + + expect(component).toMatchSnapshot(); + }); + + test('append is rendered', () => { + const component = render(); + + expect(component).toMatchSnapshot(); + }); + }); + + describe('form row props', () => { + test('are rendered', () => { + const component = render( + + ); + + expect(component).toMatchSnapshot(); + }); }); }); diff --git a/src/components/form/field_text/field_text.tsx b/src/components/form/field_text/field_text.tsx index 25c9f529131..2d804326d26 100644 --- a/src/components/form/field_text/field_text.tsx +++ b/src/components/form/field_text/field_text.tsx @@ -10,14 +10,32 @@ import React, { InputHTMLAttributes, Ref, FunctionComponent } from 'react'; import { CommonProps } from '../../common'; import classNames from 'classnames'; +import { EuiFormControlLayoutProps } from '../form_control_layout'; import { - EuiFormControlLayout, - EuiFormControlLayoutProps, -} from '../form_control_layout'; + EuiFormControlLayoutUpdated, + renderSideNode, +} from '../form_control_layout/form_control_layout_updated'; import { EuiValidatableControl } from '../validatable_control'; -export type EuiFieldTextProps = InputHTMLAttributes & +import { + EuiFormRow, + EuiFormRowCommonProps, + euiFormRowDisplayIsCompressed, +} from '../form_row/form_row'; + +import { htmlIdGenerator } from '../../../services'; + +export type _EuiFieldTextSupportedRowDisplays = + | 'row' + | 'rowCompressed' + | 'columnCompressed'; + +export type EuiFieldTextProps = Omit< + EuiFormRowCommonProps, + 'children' | 'display' | 'hasChildLabel' | 'describedByIds' | 'inputId' +> & + InputHTMLAttributes & CommonProps & { icon?: EuiFormControlLayoutProps['icon']; isInvalid?: boolean; @@ -25,29 +43,36 @@ export type EuiFieldTextProps = InputHTMLAttributes & isLoading?: boolean; readOnly?: boolean; inputRef?: Ref; + placeholder?: HTMLInputElement['placeholder']; /** - * Creates an input group with element(s) coming before input. + * Creates an input group with element(s) coming before input; * `string` | `ReactElement` or an array of these */ prepend?: EuiFormControlLayoutProps['prepend']; /** - * Creates an input group with element(s) coming after input. + * Creates an input group with element(s) coming after input; * `string` | `ReactElement` or an array of these */ append?: EuiFormControlLayoutProps['append']; /** * Completely removes form control layout wrapper and ignores - * icon, prepend, and append. Best used inside EuiFormControlLayoutDelimited. + * icon, prepend, and append and all form row props; + * Best used inside EuiFormControlLayoutDelimited */ controlOnly?: boolean; /** - * when `true` creates a shorter height input + * When `true` creates a shorter height input */ compressed?: boolean; + + /** + * Custom list of supported row displays + */ + display?: _EuiFieldTextSupportedRowDisplays; }; export const EuiFieldText: FunctionComponent = ({ @@ -66,8 +91,39 @@ export const EuiFieldText: FunctionComponent = ({ append, readOnly, controlOnly, + disabled, + // FormRowProps + helpText, + error, + label, + labelAppend, + labelProps, + hasEmptyLabelSpace, + display = 'row', + 'aria-describedby': ariaDescribedBy, + isDisabled: _isDisabled, ...rest }) => { + // Set a final disabled + const isDisabled = _isDisabled || disabled; + + // Force an id if one was not passed + id = id || htmlIdGenerator('euiFieldText')(); + + const { finalNodes: prependNodes, finalNodeIDs: prependIDs } = renderSideNode( + 'prepend', + id, + prepend + ); + const { finalNodes: appendNodes, finalNodeIDs: appendIDs } = renderSideNode( + 'append', + id, + append + ); + + // Force compressed if `display` is compressed + compressed = euiFormRowDisplayIsCompressed(display) || compressed; + const classes = classNames('euiFieldText', className, { 'euiFieldText--withIcon': icon, 'euiFieldText--fullWidth': fullWidth, @@ -87,6 +143,10 @@ export const EuiFieldText: FunctionComponent = ({ value={value} ref={inputRef} readOnly={readOnly} + disabled={isDisabled} + aria-describedby={ + classNames(ariaDescribedBy, prependIDs, appendIDs) || undefined + } {...rest} /> @@ -94,18 +154,38 @@ export const EuiFieldText: FunctionComponent = ({ if (controlOnly) return control; - return ( - {control} - + ); + + if (!label && !error && !helpText && !hasEmptyLabelSpace) + return formControlLayout; + + const formRowProps = { + id, + helpText, + error, + label, + labelAppend, + labelProps, + hasEmptyLabelSpace, + display, + fullWidth, + isInvalid, + isDisabled, + passThrough: false, + }; + + return {formControlLayout}; }; diff --git a/src/components/form/form_control_layout/form_control_layout.tsx b/src/components/form/form_control_layout/form_control_layout.tsx index 58e57e6a48c..92b61c3aac5 100644 --- a/src/components/form/form_control_layout/form_control_layout.tsx +++ b/src/components/form/form_control_layout/form_control_layout.tsx @@ -25,7 +25,9 @@ import { EuiFormLabel } from '../form_label'; export { ICON_SIDES } from './form_control_layout_icons'; type StringOrReactElement = string | ReactElement; -type PrependAppendType = StringOrReactElement | StringOrReactElement[]; +export type _EuiPrependAppendType = + | StringOrReactElement + | StringOrReactElement[]; export type EuiFormControlLayoutProps = CommonProps & HTMLAttributes & { @@ -33,12 +35,12 @@ export type EuiFormControlLayoutProps = CommonProps & * Creates an input group with element(s) coming before children. * `string` | `ReactElement` or an array of these */ - prepend?: PrependAppendType; + prepend?: _EuiPrependAppendType; /** * Creates an input group with element(s) coming after children. * `string` | `ReactElement` or an array of these */ - append?: PrependAppendType; + append?: _EuiPrependAppendType; children?: ReactNode; icon?: EuiFormControlLayoutIconsProps['icon']; clear?: EuiFormControlLayoutIconsProps['clear']; @@ -107,7 +109,7 @@ export class EuiFormControlLayout extends Component { renderSideNode( side: 'append' | 'prepend', - nodes?: PrependAppendType, + nodes?: _EuiPrependAppendType, inputId?: string ) { if (!nodes) { diff --git a/src/components/form/form_control_layout/form_control_layout_updated.tsx b/src/components/form/form_control_layout/form_control_layout_updated.tsx new file mode 100644 index 00000000000..8cbe65acc89 --- /dev/null +++ b/src/components/form/form_control_layout/form_control_layout_updated.tsx @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { + cloneElement, + FunctionComponent, + ReactElement, + ReactNode, +} from 'react'; +import classNames from 'classnames'; + +import { EuiFormControlLayoutIcons } from './form_control_layout_icons'; +import { EuiFormLabel } from '../form_label'; +import { + EuiFormControlLayoutProps, + _EuiPrependAppendType, +} from './form_control_layout'; + +export type _EuiPrependAppendSide = 'append' | 'prepend'; + +export const _createPrependAppendIDs = ( + inputId: string, + side: _EuiPrependAppendSide, + index: number +) => { + return `${inputId}-${side}${index}`; +}; + +export const EuiFormControlLayoutUpdated: FunctionComponent< + Omit & { + /** + * `inputId` is required to attach appropritely with prepend/append + */ + inputId: string; + /** + * Final nodes are rendered via field component + */ + prepend?: ReactNode; + append?: ReactNode; + } +> = ({ + children, + icon, + clear, + fullWidth, + isLoading, + isDisabled, + compressed, + className, + prepend, + append, + readOnly, + inputId, + ...rest +}) => { + const classes = classNames( + 'euiFormControlLayout', + { + 'euiFormControlLayout--fullWidth': fullWidth, + 'euiFormControlLayout--compressed': compressed, + 'euiFormControlLayout--readOnly': readOnly, + 'euiFormControlLayout--group': prepend || append, + 'euiFormControlLayout-isDisabled': isDisabled, + }, + className + ); + + return ( +
+ {prepend} +
+ {children} + + +
+ {append} +
+ ); +}; + +export const renderSideNode = ( + side: _EuiPrependAppendSide, + inputId: string, + /** + * Nodes can be a single string or ReactElement, or an array of these + */ + nodes?: _EuiPrependAppendType +) => { + let finalNodes: ReactNode[] | undefined; + const finalNodeIDs: string[] = []; + + // All string types get wrapped in labels, + // Otherwise are cloned to have a class name applied + + if (typeof nodes === 'string') { + // If they passed a simple string + const id = _createPrependAppendIDs(inputId, side, 0); + finalNodeIDs.push(id); + finalNodes = [createFormLabel(side, nodes, id)]; + } else if (nodes) { + // Otherwise map through each + finalNodes = React.Children.map(nodes, (item, index) => { + const nodeIsString = typeof item === 'string'; + + if (item && nodeIsString) { + const id = + nodeIsString && _createPrependAppendIDs(inputId, side, index); + finalNodeIDs.push(id); + return createFormLabel(side, item as string, id); + } else { + // @ts-ignore Ugh TS + return createSideNode(side, item, index); + } + }); + } + + return { finalNodes, finalNodeIDs }; +}; + +export const createFormLabel = ( + side: _EuiPrependAppendSide, + string: string, + id: string +) => { + return ( + + {string} + + ); +}; + +export const createSideNode = ( + side: _EuiPrependAppendSide, + node?: ReactElement, + key?: React.Key +): ReactNode => { + if (!node) return; + return cloneElement(node, { + className: classNames( + `euiFormControlLayout__${side}`, + node.props.className + ), + key: key, + }); +}; diff --git a/src/components/form/form_row/__snapshots__/form_row.test.tsx.snap b/src/components/form/form_row/__snapshots__/form_row.test.tsx.snap index 49bf5f2de39..f312c8fc4e1 100644 --- a/src/components/form/form_row/__snapshots__/form_row.test.tsx.snap +++ b/src/components/form/form_row/__snapshots__/form_row.test.tsx.snap @@ -13,6 +13,7 @@ exports[`EuiFormRow behavior onBlur is called in child 1`] = ` } labelType="label" + passThrough={true} >
} labelType="label" + passThrough={true} >
} labelType="label" + passThrough={true} >
} labelType="label" + passThrough={true} >
`; + +exports[`EuiFormRow props label with labelProps rendered 1`] = ` +
+
+ + label + +
+
+ +
+
+`; + +exports[`EuiFormRow props passThrough does not pass props to cloned child when false 1`] = ` +
+
+ +
+
+`; diff --git a/src/components/form/form_row/form_row.test.tsx b/src/components/form/form_row/form_row.test.tsx index e0772dca615..3c40265f495 100644 --- a/src/components/form/form_row/form_row.test.tsx +++ b/src/components/form/form_row/form_row.test.tsx @@ -56,6 +56,37 @@ describe('EuiFormRow', () => { ); }); + test('ties together parts for accessibility if input has its own id', () => { + const props = { + label: 'Label', + helpText: 'Help text', + isInvalid: true, + error: ['Error one', 'Error two'], + }; + + const tree = shallow( + + + + ); + + // Input is labeled by the label. + expect(tree.find('input').prop('id')).toEqual('inputId'); + expect(tree.find('EuiFormLabel').prop('htmlFor')).toEqual('inputId'); + + // Input is described by help and error text. + expect(tree.find('EuiFormHelpText').prop('id')).toEqual('inputId-help-0'); + expect(tree.find('EuiFormErrorText').at(0).prop('id')).toEqual( + 'inputId-error-0' + ); + expect(tree.find('EuiFormErrorText').at(1).prop('id')).toEqual( + 'inputId-error-1' + ); + expect(tree.find('input').prop('aria-describedby')).toEqual( + 'inputId-help-0 inputId-error-0 inputId-error-1' + ); + }); + describe('props', () => { test('label is rendered', () => { const component = shallow( @@ -67,6 +98,16 @@ describe('EuiFormRow', () => { expect(component).toMatchSnapshot(); }); + test('label with labelProps rendered', () => { + const component = shallow( + + + + ); + + expect(component).toMatchSnapshot(); + }); + test('label append is rendered', () => { const component = shallow( @@ -209,6 +250,16 @@ describe('EuiFormRow', () => { }); }); + test('passThrough does not pass props to cloned child when false', () => { + const component = render( + + + + ); + + expect(component).toMatchSnapshot(); + }); + describe('display type', () => { DISPLAYS.forEach((display) => { test(`${display} is rendered`, () => { diff --git a/src/components/form/form_row/form_row.tsx b/src/components/form/form_row/form_row.tsx index d83b8a31e48..00267b03f41 100644 --- a/src/components/form/form_row/form_row.tsx +++ b/src/components/form/form_row/form_row.tsx @@ -44,7 +44,13 @@ interface EuiFormRowState { id: string; } -type EuiFormRowCommonProps = CommonProps & { +export function euiFormRowDisplayIsCompressed( + display?: EuiFormRowDisplayKeys +): boolean { + return display ? display.includes('Compressed') : false; +} + +export type EuiFormRowCommonProps = CommonProps & { /** * When `rowCompressed`, just tightens up the spacing; * Set to `columnCompressed` if compressed @@ -55,7 +61,6 @@ type EuiFormRowCommonProps = CommonProps & { * as the child is a switch. */ display?: EuiFormRowDisplayKeys; - hasEmptyLabelSpace?: boolean; fullWidth?: boolean; /** * IDs of additional elements that should be part of children's `aria-describedby` @@ -69,22 +74,48 @@ type EuiFormRowCommonProps = CommonProps & { * ReactElement to render as this component's content */ children: ReactElement; + /** + * The content for the `